author | Wes Johnston <wjohnston@mozilla.com> |
Tue, 28 Oct 2014 16:05:32 -0700 | |
changeset 219145 | a8f9ed3d7554f4eb51cf6f0f6295f00dc22bc6a1 |
parent 219144 | 477d76b9d58d93cdbb183e76eee52c936ee4d53c |
child 219146 | 00b588dac8bc373f019a400baa894808721d7fc2 |
push id | 27956 |
push user | kwierso@gmail.com |
push date | Fri, 12 Dec 2014 00:47:19 +0000 |
treeherder | mozilla-central@32a2c5bd2f68 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | paolo |
bugs | 1063217 |
milestone | 37.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/toolkit/components/jsdownloads/moz.build +++ b/toolkit/components/jsdownloads/moz.build @@ -2,8 +2,9 @@ # vim: set filetype=python: # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. DIRS += ['public', 'src'] XPCSHELL_TESTS_MANIFESTS += ['test/data/xpcshell.ini', 'test/unit/xpcshell.ini'] +BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm +++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm @@ -25,28 +25,34 @@ * DownloadSaver * Template for an object that actually transfers the data for the download. * * DownloadCopySaver * Saver object that simply copies the entire source file to the target. * * DownloadLegacySaver * Saver object that integrates with the legacy nsITransfer interface. + * + * DownloadPDFSaver + * This DownloadSaver type creates a PDF file from the current document in a + * given window, specified using the windowRef property of the DownloadSource + * object associated with the download. */ "use strict"; this.EXPORTED_SYMBOLS = [ "Download", "DownloadSource", "DownloadTarget", "DownloadError", "DownloadSaver", "DownloadCopySaver", "DownloadLegacySaver", + "DownloadPDFSaver", ]; //////////////////////////////////////////////////////////////////////////////// //// Globals const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; @@ -63,26 +69,31 @@ XPCOMUtils.defineLazyModuleGetter(this, XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm") XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory", "@mozilla.org/browser/download-history;1", Ci.nsIDownloadHistory); XPCOMUtils.defineLazyServiceGetter(this, "gExternalAppLauncher", "@mozilla.org/uriloader/external-helper-app-service;1", Ci.nsPIExternalAppLauncher); XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService", "@mozilla.org/uriloader/external-helper-app-service;1", Ci.nsIExternalHelperAppService); +XPCOMUtils.defineLazyServiceGetter(this, "gPrintSettingsService", + "@mozilla.org/gfx/printsettings-service;1", + Ci.nsIPrintSettingsService); const BackgroundFileSaverStreamListener = Components.Constructor( "@mozilla.org/network/background-file-saver;1?mode=streamlistener", "nsIBackgroundFileSaver"); /** * Returns true if the given value is a primitive string or a String object. */ @@ -539,17 +550,17 @@ this.Download.prototype = { * @return {Promise} * @resolves When the instruction to launch the file has been * successfully given to the operating system. Note that * the OS might still take a while until the file is actually * launched. * @rejects JavaScript exception if there was an error trying to launch * the file. */ - launch: function() { + launch: function () { if (!this.succeeded) { return Promise.reject( new Error("launch can only be called if the download succeeded") ); } return DownloadIntegration.launchDownload(this); }, @@ -906,21 +917,26 @@ this.Download.prototype = { */ toSerializable: function () { let serializable = { source: this.source.toSerializable(), target: this.target.toSerializable(), }; + let saver = this.saver.toSerializable(); + if (!saver) { + // If we are unable to serialize the saver, we won't persist the download. + return null; + } + // Simplify the representation for the most common saver type. If the saver // is an object instead of a simple string, we can't simplify it because we // need to persist all its properties, not only "type". This may happen for // savers of type "copy" as well as other types. - let saver = this.saver.toSerializable(); if (saver !== "copy") { serializable.saver = saver; } if (this.error) { serializable.errorObj = this.error.toSerializable(); } @@ -1121,16 +1137,20 @@ this.DownloadSource.prototype = { */ this.DownloadSource.fromSerializable = function (aSerializable) { let source = new DownloadSource(); if (isString(aSerializable)) { // Convert String objects to primitive strings at this point. source.url = aSerializable.toString(); } else if (aSerializable instanceof Ci.nsIURI) { source.url = aSerializable.spec; + } else if (aSerializable instanceof Ci.nsIDOMWindow) { + source.url = aSerializable.location.href; + source.isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(aSerializable); + source.windowRef = Cu.getWeakReference(aSerializable); } else { // Convert String objects to primitive strings at this point. source.url = aSerializable.url.toString(); if ("isPrivate" in aSerializable) { source.isPrivate = aSerializable.isPrivate; } if ("referrer" in aSerializable) { source.referrer = aSerializable.referrer; @@ -1521,16 +1541,19 @@ this.DownloadSaver.fromSerializable = fu let saver; switch (serializable.type) { case "copy": saver = DownloadCopySaver.fromSerializable(serializable); break; case "legacy": saver = DownloadLegacySaver.fromSerializable(serializable); break; + case "pdf": + saver = DownloadPDFSaver.fromSerializable(serializable); + break; default: throw new Error("Unrecoginzed download saver type."); } return saver; }; //////////////////////////////////////////////////////////////////////////////// //// DownloadCopySaver @@ -1944,17 +1967,17 @@ this.DownloadCopySaver.fromSerializable //////////////////////////////////////////////////////////////////////////////// //// DownloadLegacySaver /** * Saver object that integrates with the legacy nsITransfer interface. * * For more background on the process, see the DownloadLegacyTransfer object. */ -this.DownloadLegacySaver = function() +this.DownloadLegacySaver = function () { this.deferExecuted = Promise.defer(); this.deferCanceled = Promise.defer(); } this.DownloadLegacySaver.prototype = { __proto__: DownloadSaver.prototype, @@ -2297,8 +2320,164 @@ this.DownloadLegacySaver.prototype = { /** * Returns a new DownloadLegacySaver object. This saver type has a * deserializable form only when creating a new object in memory, because it * cannot be serialized to disk. */ this.DownloadLegacySaver.fromSerializable = function () { return new DownloadLegacySaver(); }; + +//////////////////////////////////////////////////////////////////////////////// +//// DownloadPDFSaver + +/** + * This DownloadSaver type creates a PDF file from the current document in a + * given window, specified using the windowRef property of the DownloadSource + * object associated with the download. + * + * In order to prevent the download from saving a different document than the one + * originally loaded in the window, any attempt to restart the download will fail. + * + * Since this DownloadSaver type requires a live document as a source, it cannot + * be persisted across sessions, unless the download already succeeded. + */ +this.DownloadPDFSaver = function () { +} + +this.DownloadPDFSaver.prototype = { + __proto__: DownloadSaver.prototype, + + /** + * An nsIWebBrowserPrint instance for printing this page. + * This is null when saving has not started or has completed, + * or while the operation is being canceled. + */ + _webBrowserPrint: null, + + /** + * Implements "DownloadSaver.execute". + */ + execute: function (aSetProgressBytesFn, aSetPropertiesFn) + { + return Task.spawn(function task_DCS_execute() { + if (!this.download.source.windowRef) { + throw new DownloadError({ + message: "PDF saver must be passed an open window, and cannot be restarted.", + becauseSourceFailed: true, + }); + } + + let win = this.download.source.windowRef.get(); + + // Set windowRef to null to avoid re-trying. + this.download.source.windowRef = null; + + if (!win) { + throw new DownloadError({ + message: "PDF saver can't save a window that has been closed.", + becauseSourceFailed: true, + }); + } + + this.addToHistory(); + + let targetPath = this.download.target.path; + + // An empty target file must exist for the PDF printer to work correctly. + let file = yield OS.File.open(targetPath, { truncate: true }); + yield file.close(); + + let printSettings = gPrintSettingsService.newPrintSettings; + + printSettings.printToFile = true; + printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF; + printSettings.toFileName = targetPath; + + printSettings.printSilent = true; + printSettings.showPrintProgress = false; + + printSettings.printBGImages = true; + printSettings.printBGColors = true; + printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs; + printSettings.headerStrCenter = ""; + printSettings.headerStrLeft = ""; + printSettings.headerStrRight = ""; + printSettings.footerStrCenter = ""; + printSettings.footerStrLeft = ""; + printSettings.footerStrRight = ""; + + this._webBrowserPrint = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebBrowserPrint); + + try { + yield new Promise((resolve, reject) => { + this._webBrowserPrint.print(printSettings, { + onStateChange: function (webProgress, request, stateFlags, status) { + if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + if (!Components.isSuccessCode(status)) { + reject(new DownloadError({ result: status, + inferCause: true })); + } else { + resolve(); + } + } + }, + onProgressChange: function (webProgress, request, curSelfProgress, + maxSelfProgress, curTotalProgress, + maxTotalProgress) { + aSetProgressBytesFn(curTotalProgress, maxTotalProgress, false); + }, + onLocationChange: function () {}, + onStatusChange: function () {}, + onSecurityChange: function () {}, + }); + }); + } finally { + // Remove the print object to avoid leaks + this._webBrowserPrint = null; + } + + let fileInfo = yield OS.File.stat(targetPath); + aSetProgressBytesFn(fileInfo.size, fileInfo.size, false); + }.bind(this)); + }, + + /** + * Implements "DownloadSaver.cancel". + */ + cancel: function DCS_cancel() + { + if (this._webBrowserPrint) { + this._webBrowserPrint.cancel(); + this._webBrowserPrint = null; + } + }, + + /** + * Implements "DownloadSaver.toSerializable". + */ + toSerializable: function () + { + if (this.download.succeeded) { + return DownloadCopySaver.prototype.toSerializable.call(this); + } + + // This object needs a window to recreate itself. If it didn't succeded + // it will not be possible to restart. Returning null here will + // prevent us from serializing it at all. + return null; + }, +}; + +/** + * Creates a new DownloadPDFSaver object, with its initial state derived from + * its serializable representation. + * + * @param aSerializable + * Serializable representation of a DownloadPDFSaver object. + * + * @return The newly created DownloadPDFSaver object. + */ +this.DownloadPDFSaver.fromSerializable = function (aSerializable) { + return new DownloadPDFSaver(); +}; +
--- a/toolkit/components/jsdownloads/src/DownloadStore.jsm +++ b/toolkit/components/jsdownloads/src/DownloadStore.jsm @@ -159,17 +159,23 @@ this.DownloadStore.prototype = { // Take a static snapshot of the current state of all the downloads. let storeData = { list: [] }; let atLeastOneDownload = false; for (let download of downloads) { try { if (!this.onsaveitem(download)) { continue; } - storeData.list.push(download.toSerializable()); + + let serializable = download.toSerializable(); + if (!serializable) { + // This item cannot be persisted across sessions. + continue; + } + storeData.list.push(serializable); atLeastOneDownload = true; } catch (ex) { // If an item cannot be converted to a serializable form, don't // prevent others from being saved. Cu.reportError(ex); } }
new file mode 100644 --- /dev/null +++ b/toolkit/components/jsdownloads/test/browser/browser.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + head.js + testFile.html + +[browser_DownloadPDFSaver.js] +skip-if = e10s || os != "win"
new file mode 100644 --- /dev/null +++ b/toolkit/components/jsdownloads/test/browser/browser_DownloadPDFSaver.js @@ -0,0 +1,101 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the PDF download saver, and tests using a window as a + * source for the copy download saver. + */ + +"use strict"; + +/** + * Helper function to make sure a window reference exists on the download source. + */ +function* test_download_windowRef(aTab, aDownload) { + ok(aDownload.source.windowRef, "Download source had a window reference"); + ok(aDownload.source.windowRef instanceof Ci.xpcIJSWeakReference, "Download window reference is a weak ref"); + is(aDownload.source.windowRef.get(), aTab.linkedBrowser.contentWindow, "Download window exists during test"); +} + +/** + * Helper function to check the state of a completed download. + */ +function* test_download_state_complete(aTab, aDownload, aPrivate, aCanceled) { + ok(aDownload.source, "Download has a source"); + is(aDownload.source.url, aTab.linkedBrowser.contentWindow.location, "Download source has correct url"); + is(aDownload.source.isPrivate, aPrivate, "Download source has correct private state"); + ok(aDownload.stopped, "Download is stopped"); + is(aCanceled, aDownload.canceled, "Download has correct canceled state"); + is(!aCanceled, aDownload.succeeded, "Download has correct succeeded state"); + is(aDownload.error, null, "Download error is not defined"); +} + +function* test_createDownload_common(aPrivate, aType) { + let tab = gBrowser.addTab(getRootDirectory(gTestPath) + "testFile.html"); + yield promiseBrowserLoaded(tab.linkedBrowser); + + if (aPrivate) { + tab.linkedBrowser.docShell.QueryInterface(Ci.nsILoadContext) + .usePrivateBrowsing = true; + } + + let download = yield Downloads.createDownload({ + source: tab.linkedBrowser.contentWindow, + target: { path: getTempFile(TEST_TARGET_FILE_NAME_PDF).path }, + saver: { type: aType }, + }); + + yield test_download_windowRef(tab, download); + yield download.start(); + + yield test_download_state_complete(tab, download, aPrivate, false); + if (aType == "pdf") { + let signature = yield OS.File.read(download.target.path, + { bytes: 4, encoding: "us-ascii" }); + is(signature, "%PDF", "File exists and signature matches"); + } else { + ok((yield OS.File.exists(download.target.path)), "File exists"); + } + + gBrowser.removeTab(tab); +} + +add_task(function* test_createDownload_pdf_private() { + yield test_createDownload_common(true, "pdf"); +}); +add_task(function* test_createDownload_pdf_not_private() { + yield test_createDownload_common(false, "pdf"); +}); + +// Even for the copy saver, using a window should produce valid results +add_task(function* test_createDownload_copy_private() { + yield test_createDownload_common(true, "copy"); +}); +add_task(function* test_createDownload_copy_not_private() { + yield test_createDownload_common(false, "copy"); +}); + +add_task(function* test_cancel_pdf_download() { + let tab = gBrowser.addTab(getRootDirectory(gTestPath) + "testFile.html"); + yield promiseBrowserLoaded(tab.linkedBrowser); + + let download = yield Downloads.createDownload({ + source: tab.linkedBrowser.contentWindow, + target: { path: getTempFile(TEST_TARGET_FILE_NAME_PDF).path }, + saver: "pdf", + }); + + yield test_download_windowRef(tab, download); + download.start(); + + // Immediately cancel the download to test that it is erased correctly. + yield download.cancel(); + yield test_download_state_complete(tab, download, false, true); + + let exists = yield OS.File.exists(download.target.path) + ok(!exists, "Target file does not exist"); + + gBrowser.removeTab(tab); +});
new file mode 100644 --- /dev/null +++ b/toolkit/components/jsdownloads/test/browser/head.js @@ -0,0 +1,89 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Provides infrastructure for automated download components tests. + */ + +"use strict"; + +//////////////////////////////////////////////////////////////////////////////// +//// Globals + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths", + "resource://gre/modules/DownloadPaths.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "HttpServer", + "resource://testing-common/httpd.js"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); + +const TEST_TARGET_FILE_NAME_PDF = "test-download.pdf"; + +//////////////////////////////////////////////////////////////////////////////// +//// Support functions + +// While the previous test file should have deleted all the temporary files it +// used, on Windows these might still be pending deletion on the physical file +// system. Thus, start from a new base number every time, to make a collision +// with a file that is still pending deletion highly unlikely. +let gFileCounter = Math.floor(Math.random() * 1000000); + +/** + * Returns a reference to a temporary file, that is guaranteed not to exist, and + * to have never been created before. + * + * @param aLeafName + * Suggested leaf name for the file to be created. + * + * @return nsIFile pointing to a non-existent file in a temporary directory. + * + * @note It is not enough to delete the file if it exists, or to delete the file + * after calling nsIFile.createUnique, because on Windows the delete + * operation in the file system may still be pending, preventing a new + * file with the same name to be created. + */ +function getTempFile(aLeafName) +{ + // Prepend a serial number to the extension in the suggested leaf name. + let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName); + let leafName = base + "-" + gFileCounter + ext; + gFileCounter++; + + // Get a file reference under the temporary directory for this test file. + let file = FileUtils.getFile("TmpD", [leafName]); + ok(!file.exists(), "Temp file does not exist"); + + registerCleanupFunction(function () { + if (file.exists()) { + file.remove(false); + } + }); + + return file; +} + +function promiseBrowserLoaded(browser) { + return new Promise(resolve => { + browser.addEventListener("load", function onLoad(event) { + if (event.target == browser.contentDocument) { + browser.removeEventListener("load", onLoad, true); + resolve(); + } + }, true); + }); +}
new file mode 100644 --- /dev/null +++ b/toolkit/components/jsdownloads/test/browser/testFile.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <title>Test Save as PDF</title> + </head> + <body> + <p>Save me as a PDF!</p> + </body> +</html>
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js +++ b/toolkit/components/jsdownloads/test/unit/test_DownloadStore.js @@ -52,23 +52,35 @@ add_task(function test_save_reload() listForSave.add(yield promiseNewDownload(httpUrl("source.txt"))); listForSave.add(yield Downloads.createDownload({ source: { url: httpUrl("empty.txt"), referrer: TEST_REFERRER_URL }, target: getTempFile(TEST_TARGET_FILE_NAME), })); + // This PDF download should not be serialized because it never succeeds. + let pdfDownload = yield Downloads.createDownload({ + source: { url: httpUrl("empty.txt"), + referrer: TEST_REFERRER_URL }, + target: getTempFile(TEST_TARGET_FILE_NAME), + saver: "pdf", + }); + listForSave.add(pdfDownload); + let legacyDownload = yield promiseStartLegacyDownload(); yield legacyDownload.cancel(); listForSave.add(legacyDownload); yield storeForSave.save(); yield storeForLoad.load(); + // Remove the PDF download because it should not appear in this list. + listForSave.remove(pdfDownload); + let itemsForSave = yield listForSave.getAll(); let itemsForLoad = yield listForLoad.getAll(); do_check_eq(itemsForSave.length, itemsForLoad.length); // Downloads should be reloaded in the same order. for (let i = 0; i < itemsForSave.length; i++) { // The reloaded downloads are different objects.
--- a/toolkit/components/jsdownloads/test/unit/test_Downloads.js +++ b/toolkit/components/jsdownloads/test/unit/test_Downloads.js @@ -56,16 +56,41 @@ add_task(function test_createDownload_pu source: { url: "about:blank" }, target: { path: tempPath }, saver: { type: "copy" } }); do_check_false(download.source.isPrivate); }); /** + * Tests createDownload for a pdf saver throws if only given a url. + */ +add_task(function test_createDownload_pdf() +{ + let download = yield Downloads.createDownload({ + source: { url: "about:blank" }, + target: { path: getTempFile(TEST_TARGET_FILE_NAME).path }, + saver: { type: "pdf" }, + }); + + try { + yield download.start(); + do_throw("The download should have failed."); + } catch (ex if ex instanceof Downloads.Error && ex.becauseSourceFailed) { } + + do_check_false(download.succeeded); + do_check_true(download.stopped); + do_check_false(download.canceled); + do_check_true(download.error !== null); + do_check_true(download.error.becauseSourceFailed); + do_check_false(download.error.becauseTargetFailed); + do_check_false(yield OS.File.exists(download.target.path)); +}); + +/** * Tests "fetch" with nsIURI and nsIFile as arguments. */ add_task(function test_fetch_uri_file_arguments() { let targetFile = getTempFile(TEST_TARGET_FILE_NAME); yield Downloads.fetch(NetUtil.newURI(httpUrl("source.txt")), targetFile); yield promiseVerifyContents(targetFile.path, TEST_DATA_SHORT); });