Bug 1415507 - changes to tabs.saveAsPDF(); r?mixedpuppy
There are three changes:
1. Adds a print progress listener to tabs.saveAsPDF() in ext-tabs.js so that the
'saved' or 'replaced' status is not returned until the PDF file has been saved.
2. Adds four more "edge" properties to the pageSettings object to allow positioning
of the page headers and footers.
3. Adds automated tests for tabs.saveAsPDF() in browser_ext_tabs_saveAsPDF.js that
cover all returned statuses: saved, replaced, canceled, not_saved, not_replaced.
MozReview-Commit-ID: iljvT8wp11
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -877,60 +877,91 @@ this.tabs = class extends ExtensionAPI {
picker.appendFilter("PDF", "*.pdf");
picker.defaultExtension = "pdf";
picker.defaultString = activeTab.linkedBrowser.contentTitle + ".pdf";
return new Promise(resolve => {
picker.open(function(retval) {
if (retval == 0 || retval == 2) {
// OK clicked (retval == 0) or replace confirmed (retval == 2)
+
+ // Workaround: When trying to replace an existing file that is open in another application (i.e. a locked file),
+ // the print progress listener is never called. This workaround ensures that a correct status is always returned.
try {
let fstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
- fstream.init(picker.file, 0x2A, 0x1B6, 0); // write|create|truncate, file permissions rw-rw-rw- = 0666 = 0x1B6
- fstream.close(); // unlock file
+ fstream.init(picker.file, 0x2A, 0o666, 0); // ioflags = write|create|truncate, file permissions = rw-rw-rw-
+ fstream.close();
} catch (e) {
resolve(retval == 0 ? "not_saved" : "not_replaced");
return;
}
let psService = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(Ci.nsIPrintSettingsService);
let printSettings = psService.newPrintSettings;
+ printSettings.printerName = "";
+ printSettings.isInitializedFromPrinter = true;
+ printSettings.isInitializedFromPrefs = true;
+
printSettings.printToFile = true;
printSettings.toFileName = picker.file.path;
printSettings.printSilent = true;
printSettings.showPrintProgress = false;
printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
+ if (pageSettings.paperSizeUnit !== null) {
+ printSettings.paperSizeUnit = pageSettings.paperSizeUnit;
+ }
+ if (pageSettings.paperWidth !== null) {
+ printSettings.paperWidth = pageSettings.paperWidth;
+ }
+ if (pageSettings.paperHeight !== null) {
+ printSettings.paperHeight = pageSettings.paperHeight;
+ }
if (pageSettings.orientation !== null) {
printSettings.orientation = pageSettings.orientation;
}
if (pageSettings.scaling !== null) {
printSettings.scaling = pageSettings.scaling;
}
if (pageSettings.shrinkToFit !== null) {
printSettings.shrinkToFit = pageSettings.shrinkToFit;
}
if (pageSettings.showBackgroundColors !== null) {
printSettings.printBGColors = pageSettings.showBackgroundColors;
}
if (pageSettings.showBackgroundImages !== null) {
printSettings.printBGImages = pageSettings.showBackgroundImages;
}
- if (pageSettings.paperSizeUnit !== null) {
- printSettings.paperSizeUnit = pageSettings.paperSizeUnit;
+ if (pageSettings.edgeLeft !== null) {
+ printSettings.edgeLeft = pageSettings.edgeLeft;
+ }
+ if (pageSettings.edgeRight !== null) {
+ printSettings.edgeRight = pageSettings.edgeRight;
+ }
+ if (pageSettings.edgeTop !== null) {
+ printSettings.edgeTop = pageSettings.edgeTop;
+ }
+ if (pageSettings.edgeBottom !== null) {
+ printSettings.edgeBottom = pageSettings.edgeBottom;
}
- if (pageSettings.paperWidth !== null) {
- printSettings.paperWidth = pageSettings.paperWidth;
+ if (pageSettings.marginLeft !== null) {
+ printSettings.marginLeft = pageSettings.marginLeft;
+ }
+ if (pageSettings.marginRight !== null) {
+ printSettings.marginRight = pageSettings.marginRight;
}
- if (pageSettings.paperHeight !== null) {
- printSettings.paperHeight = pageSettings.paperHeight;
+ if (pageSettings.marginTop !== null) {
+ printSettings.marginTop = pageSettings.marginTop;
+ }
+ if (pageSettings.marginBottom !== null) {
+ printSettings.marginBottom = pageSettings.marginBottom;
}
if (pageSettings.headerLeft !== null) {
printSettings.headerStrLeft = pageSettings.headerLeft;
}
if (pageSettings.headerCenter !== null) {
printSettings.headerStrCenter = pageSettings.headerCenter;
}
if (pageSettings.headerRight !== null) {
@@ -940,32 +971,35 @@ this.tabs = class extends ExtensionAPI {
printSettings.footerStrLeft = pageSettings.footerLeft;
}
if (pageSettings.footerCenter !== null) {
printSettings.footerStrCenter = pageSettings.footerCenter;
}
if (pageSettings.footerRight !== null) {
printSettings.footerStrRight = pageSettings.footerRight;
}
- if (pageSettings.marginLeft !== null) {
- printSettings.marginLeft = pageSettings.marginLeft;
- }
- if (pageSettings.marginRight !== null) {
- printSettings.marginRight = pageSettings.marginRight;
- }
- if (pageSettings.marginTop !== null) {
- printSettings.marginTop = pageSettings.marginTop;
- }
- if (pageSettings.marginBottom !== null) {
- printSettings.marginBottom = pageSettings.marginBottom;
- }
- activeTab.linkedBrowser.print(activeTab.linkedBrowser.outerWindowID, printSettings, null);
+ let printProgressListener = {
+ onLocationChange(webProgress, request, location, flags) { },
+ onProgressChange(webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) { },
+ onSecurityChange(webProgress, request, state) { },
+ onStateChange(webProgress, request, flags, status) {
+ if ((flags & Ci.nsIWebProgressListener.STATE_STOP) && (flags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT)) {
+ resolve(retval == 0 ? "saved" : "replaced");
+ }
+ },
+ onStatusChange: function(webProgress, request, status, message) {
+ if (status != 0) {
+ resolve(retval == 0 ? "not_saved" : "not_replaced");
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener]),
+ };
- resolve(retval == 0 ? "saved" : "replaced");
+ activeTab.linkedBrowser.print(activeTab.linkedBrowser.outerWindowID, printSettings, printProgressListener);
} else {
// Cancel clicked (retval == 1)
resolve("canceled");
}
});
});
},
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -137,16 +137,31 @@
}
}
},
{
"id": "PageSettings",
"type": "object",
"description": "The page settings including: orientation, scale, background, margins, headers, footers.",
"properties": {
+ "paperSizeUnit": {
+ "type": "integer",
+ "optional": true,
+ "description": "The page size unit: 0 = inches, 1 = millimeters. Default: 0."
+ },
+ "paperWidth": {
+ "type": "number",
+ "optional": true,
+ "description": "The paper width in paper size units. Default: 8.5."
+ },
+ "paperHeight": {
+ "type": "number",
+ "optional": true,
+ "description": "The paper height in paper size units. Default: 11.0."
+ },
"orientation": {
"type": "integer",
"optional": true,
"description": "The page content orientation: 0 = portrait, 1 = landscape. Default: 0."
},
"scaling": {
"type": "number",
"optional": true,
@@ -162,30 +177,55 @@
"optional": true,
"description": "Whether the page background colors should be shown. Default: false."
},
"showBackgroundImages": {
"type": "boolean",
"optional": true,
"description": "Whether the page background images should be shown. Default: false."
},
- "paperSizeUnit": {
- "type": "integer",
+ "edgeLeft": {
+ "type": "number",
+ "optional": true,
+ "description": "The spacing between the left header/footer and the left edge of the paper (inches). Default: 0."
+ },
+ "edgeRight": {
+ "type": "number",
"optional": true,
- "description": "The page size unit: 0 = inches, 1 = millimeters. Default: 0."
+ "description": "The spacing between the right header/footer and the right edge of the paper (inches). Default: 0."
},
- "paperWidth": {
+ "edgeTop": {
+ "type": "number",
+ "optional": true,
+ "description": "The spacing between the top of the headers and the top edge of the paper (inches). Default: 0"
+ },
+ "edgeBottom": {
"type": "number",
"optional": true,
- "description": "The paper width in paper size units. Default: 8.5."
+ "description": "The spacing between the bottom of the footers and the bottom edge of the paper (inches). Default: 0."
},
- "paperHeight": {
+ "marginLeft": {
+ "type": "number",
+ "optional": true,
+ "description": "The margin between the page content and the left edge of the paper (inches). Default: 0.5."
+ },
+ "marginRight": {
"type": "number",
"optional": true,
- "description": "The paper height in paper size units. Default: 11.0."
+ "description": "The margin between the page content and the right edge of the paper (inches). Default: 0.5."
+ },
+ "marginTop": {
+ "type": "number",
+ "optional": true,
+ "description": "The margin between the page content and the top edge of the paper (inches). Default: 0.5."
+ },
+ "marginBottom": {
+ "type": "number",
+ "optional": true,
+ "description": "The margin between the page content and the bottom edge of the paper (inches). Default: 0.5."
},
"headerLeft": {
"type": "string",
"optional": true,
"description": "The text for the page's left header. Default: '&T'."
},
"headerCenter": {
"type": "string",
@@ -206,36 +246,16 @@
"type": "string",
"optional": true,
"description": "The text for the page's center footer. Default: ''."
},
"footerRight": {
"type": "string",
"optional": true,
"description": "The text for the page's right footer. Default: '&D'."
- },
- "marginLeft": {
- "type": "number",
- "optional": true,
- "description": "The margin between the page content and the left edge of the paper (inches). Default: 0.5."
- },
- "marginRight": {
- "type": "number",
- "optional": true,
- "description": "The margin between the page content and the right edge of the paper (inches). Default: 0.5."
- },
- "marginTop": {
- "type": "number",
- "optional": true,
- "description": "The margin between the page content and the top edge of the paper (inches). Default: 0.5."
- },
- "marginBottom": {
- "type": "number",
- "optional": true,
- "description": "The margin between the page content and the bottom edge of the paper (inches). Default: 0.5."
}
}
},
{
"id": "TabStatus",
"type": "string",
"enum": ["loading", "complete"],
"description": "Whether the tabs have completed loading."
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -164,16 +164,18 @@ skip-if = !e10s
[browser_ext_tabs_onHighlighted.js]
[browser_ext_tabs_onUpdated.js]
[browser_ext_tabs_opener.js]
[browser_ext_tabs_printPreview.js]
[browser_ext_tabs_query.js]
[browser_ext_tabs_readerMode.js]
[browser_ext_tabs_reload.js]
[browser_ext_tabs_reload_bypass_cache.js]
+[browser_ext_tabs_saveAsPDF.js]
+skip-if = os == 'mac' # Save as PDF not supported on Mac OS X
[browser_ext_tabs_sendMessage.js]
[browser_ext_tabs_cookieStoreId.js]
[browser_ext_tabs_update.js]
[browser_ext_tabs_zoom.js]
[browser_ext_tabs_update_url.js]
[browser_ext_themes_icons.js]
[browser_ext_themes_validation.js]
[browser_ext_url_overrides_newtab.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_saveAsPDF.js
@@ -0,0 +1,103 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function testReturnStatus(expectedStatus) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/");
+
+ let saveDir = FileUtils.getDir("TmpD", [`testSaveDir-${Math.random()}`], true);
+
+ let saveFile = saveDir.clone();
+ saveFile.append("testSaveFile.pdf");
+ if (saveFile.exists()) {
+ saveFile.remove(false);
+ }
+
+ if (expectedStatus == "replaced") {
+ // Create file that can be replaced
+ saveFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ } else if (expectedStatus == "not_saved") {
+ // Create directory with same name as file - so that file cannot be saved
+ saveFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0o666);
+ } else if (expectedStatus == "not_replaced") {
+ // Create file that cannot be replaced
+ saveFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o444);
+ }
+
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+
+ if (expectedStatus == "replaced" || expectedStatus == "not_replaced") {
+ MockFilePicker.returnValue = MockFilePicker.returnReplace;
+ } else if (expectedStatus == "canceled") {
+ MockFilePicker.returnValue = MockFilePicker.returnCancel;
+ } else {
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ }
+
+ MockFilePicker.displayDirectory = saveDir;
+ MockFilePicker.showCallback = function(fp) {
+ MockFilePicker.setFiles([saveFile]);
+ MockFilePicker.filterIndex = 0; // *.* - all file extensions
+ };
+
+ let manifest = {
+ "description": expectedStatus,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: manifest,
+
+ background: async function() {
+ let pageSettings = {};
+
+ let status = await browser.tabs.saveAsPDF(pageSettings);
+
+ let expected = chrome.runtime.getManifest().description;
+
+ browser.test.assertEq(expected, status, "saveAsPDF " + expected);
+
+ browser.test.notifyPass("tabs.saveAsPDF");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.saveAsPDF");
+ await extension.unload();
+
+ if (expectedStatus == "saved" || expectedStatus == "replaced") {
+ // Check that first four bytes of saved PDF file are "%PDF"
+ let text = await OS.File.read(saveFile.path, {encoding: "utf-8", bytes: 4});
+ is(text, "%PDF", "Got correct magic number");
+ }
+
+ MockFilePicker.cleanup();
+
+ if (expectedStatus == "not_saved" || expectedStatus == "not_replaced") {
+ saveFile.permissions = 0o666;
+ }
+
+ saveDir.remove(true);
+
+ await BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function testSaveAsPDF_saved() {
+ await testReturnStatus("saved");
+});
+
+add_task(async function testSaveAsPDF_replaced() {
+ await testReturnStatus("replaced");
+});
+
+add_task(async function testSaveAsPDF_canceled() {
+ await testReturnStatus("canceled");
+});
+
+add_task(async function testSaveAsPDF_not_saved() {
+ await testReturnStatus("not_saved");
+});
+
+add_task(async function testSaveAsPDF_not_replaced() {
+ await testReturnStatus("not_replaced");
+});