Bug 1415507 - changes to tabs.saveAsPDF(); r=mixedpuppy a=lizzard
authordw-dev <dw-dev@gmx.com>
Fri, 19 Jan 2018 14:13:33 +0000
changeset 454584 b8321f5cb428ad97609a75616eab56881efee200
parent 454583 8ac8d4c54c2b402cee26fd9a1e51acdf68f2d5c0
child 454585 eb62496a89f3990281f6fcb195d1565630f424ed
push id1648
push usermtabara@mozilla.com
push dateThu, 01 Mar 2018 12:45:47 +0000
treeherdermozilla-release@cbb9688c2eeb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy, lizzard
bugs1415507
milestone59.0
Bug 1415507 - changes to tabs.saveAsPDF(); r=mixedpuppy a=lizzard 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
@@ -916,60 +916,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) {
@@ -979,32 +1010,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
@@ -160,16 +160,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,
@@ -185,30 +200,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",
@@ -229,36 +269,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
@@ -165,16 +165,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_sharingState.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]
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");
+});