Bug 1415507 - changes to tabs.saveAsPDF(); r?mixedpuppy draft
authordw-dev <dw-dev@gmx.com>
Fri, 19 Jan 2018 14:13:33 +0000
changeset 722676 294c01149b7dff72326beebb705fe3cb8940202f
parent 721495 4e429d313fd2e0f9202271ee8f3fb798817ec3e7
child 746655 c06051ec6cf4a8822f23d8d2fcfe7d627a9920a4
push id96193
push userbmo:dw-dev@gmx.com
push dateFri, 19 Jan 2018 14:15:06 +0000
reviewersmixedpuppy
bugs1415507
milestone59.0a1
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
browser/components/extensions/ext-tabs.js
browser/components/extensions/schemas/tabs.json
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_tabs_saveAsPDF.js
--- 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");
+});