Bug 1513710 - Bring back stringbundle on-demand for certificate export code, and add a test for certificate exports. r=jaws, a=RyanVM
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Fri, 14 Dec 2018 20:45:58 +0000
changeset 509010 5281e679d23227f2074b569ee6532cb221634461
parent 509009 7cc9c0386e876a8768f0b78fd11372851a29c3a1
child 509011 395d963105c1c9f58e68b6ebec1bb4c622855fed
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws, RyanVM
bugs1513710
milestone65.0
Bug 1513710 - Bring back stringbundle on-demand for certificate export code, and add a test for certificate exports. r=jaws, a=RyanVM Differential Revision: https://phabricator.services.mozilla.com/D14555
browser/components/preferences/in-content/tests/browser.ini
browser/components/preferences/in-content/tests/browser_cert_export.js
security/manager/pki/resources/content/pippki.js
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -28,16 +28,17 @@ skip-if = (os == 'win') # Bug 1480314
 [browser_search_subdialogs_within_preferences_6.js]
 [browser_search_subdialogs_within_preferences_7.js]
 [browser_search_subdialogs_within_preferences_8.js]
 [browser_search_subdialogs_within_preferences_site_data.js]
 [browser_bug795764_cachedisabled.js]
 [browser_bug1018066_resetScrollPosition.js]
 [browser_bug1020245_openPreferences_to_paneContent.js]
 [browser_bug1184989_prevent_scrolling_when_preferences_flipped.js]
+[browser_cert_export.js]
 [browser_engines.js]
 support-files =
   browser_bug1184989_prevent_scrolling_when_preferences_flipped.xul
 [browser_change_app_handler.js]
 skip-if = os != "win" # Windows-specific handler application selection dialog
 [browser_checkspelling.js]
 [browser_cloud_storage.js]
 [browser_connection.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/tests/browser_cert_export.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+
+function createTemporarySaveDirectory() {
+  var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+  saveDir.append("testsavedir");
+  if (!saveDir.exists()) {
+    info("create testsavedir!");
+    saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+  }
+  info("return from createTempSaveDir: " + saveDir.path);
+  return saveDir;
+}
+
+add_task(async function checkCertExportWorks() {
+  await openPreferencesViaOpenPreferencesAPI("privacy", {leaveOpen: true});
+  let certButton = gBrowser.selectedBrowser.contentDocument.getElementById("viewCertificatesButton");
+  certButton.scrollIntoView();
+  let certDialogLoaded = promiseLoadSubDialog("chrome://pippki/content/certManager.xul");
+  certButton.click();
+  let dialogWin = await certDialogLoaded;
+  let doc = dialogWin.document;
+  doc.getElementById("certmanagertabs").selectedTab = doc.getElementById("ca_tab");
+  let expectedCert;
+  let treeView = doc.getElementById("ca-tree").treeBoxObject.view;
+  // Select any which cert. Ignore parent rows (ie rows without certs):
+  for (let i = 0; i < treeView.rowCount; i++) {
+    treeView.selection.select(i);
+    dialogWin.getSelectedCerts();
+    let certs = dialogWin.selected_certs; // yuck... but this is how the dialog works.
+    if (certs && certs.length == 1 && certs[0]) {
+      expectedCert = certs[0];
+      // OK, we managed to select a cert!
+      break;
+    }
+  }
+
+  let exportButton = doc.getElementById("ca_exportButton");
+  is(exportButton.disabled, false, "Should enable export button");
+  // Create the folder the link will be saved into.
+  var destDir = createTemporarySaveDirectory();
+  var destFile = destDir.clone();
+  MockFilePicker.init(window);
+  registerCleanupFunction(function() {
+    MockFilePicker.cleanup();
+    destDir.remove(true);
+  });
+  MockFilePicker.displayDirectory = destDir;
+  MockFilePicker.showCallback = function(fp) {
+    info("showCallback");
+    let fileName = fp.defaultString;
+    info("fileName: " + fileName);
+    destFile.append(fileName);
+    MockFilePicker.setFiles([destFile]);
+    MockFilePicker.filterIndex = 0; // Save an x509 PEM copy of the cert.
+    info("done showCallback");
+  };
+  let finishedExporting = TestUtils.topicObserved("cert-export-finished");
+  exportButton.click();
+  await finishedExporting;
+  if (destFile && destFile.exists()) {
+    let contents = await OS.File.read(destFile.path, {encoding: "utf-8"});
+    is(contents, dialogWin.getPEMString(expectedCert), "Should have written correct contents");
+  } else {
+    ok(false, "No cert saved!");
+  }
+  BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
--- a/security/manager/pki/resources/content/pippki.js
+++ b/security/manager/pki/resources/content/pippki.js
@@ -97,18 +97,23 @@ function certToFilename(cert) {
   // implementations, so we include the extension in the file name as well. This
   // is what the documentation for Ci.nsIFilePicker.defaultString says we should do
   // anyways.
   return `${filename}.${DEFAULT_CERT_EXTENSION}`;
 }
 
 async function exportToFile(parent, cert) {
   var bundle = document.getElementById("pippki_bundle");
+  if (!bundle) {
+    bundle = document.createElement("stringbundle");
+    bundle.setAttribute("src", "chrome://pippki/locale/pippki.properties");
+    document.documentElement.appendChild(bundle);
+  }
   if (!cert) {
-    return undefined;
+    return;
   }
 
   let results = await asyncDetermineUsages(cert);
   let chain = getBestChain(results);
   if (!chain) {
     chain = [cert];
   }
 
@@ -117,87 +122,86 @@ async function exportToFile(parent, cert
   fp.defaultString = certToFilename(cert);
   fp.defaultExtension = DEFAULT_CERT_EXTENSION;
   fp.appendFilter(bundle.getString("CertFormatBase64"), "*.crt; *.pem");
   fp.appendFilter(bundle.getString("CertFormatBase64Chain"), "*.crt; *.pem");
   fp.appendFilter(bundle.getString("CertFormatDER"), "*.der");
   fp.appendFilter(bundle.getString("CertFormatPKCS7"), "*.p7c");
   fp.appendFilter(bundle.getString("CertFormatPKCS7Chain"), "*.p7c");
   fp.appendFilters(Ci.nsIFilePicker.filterAll);
-  return new Promise(resolve => {
-    fp.open(res => {
-      resolve(fpCallback(res));
-    });
+  let filePickerResult = await new Promise(resolve => {
+    fp.open(resolve);
   });
 
-  function fpCallback(res) {
-    if (res != Ci.nsIFilePicker.returnOK &&
-        res != Ci.nsIFilePicker.returnReplace) {
-      return;
-    }
+  if (filePickerResult != Ci.nsIFilePicker.returnOK &&
+      filePickerResult != Ci.nsIFilePicker.returnReplace) {
+    return;
+  }
 
-    var content = "";
-    switch (fp.filterIndex) {
-      case 1:
-        content = getPEMString(cert);
-        for (let i = 1; i < chain.length; i++) {
-          content += getPEMString(chain[i]);
-        }
+  var content = "";
+  switch (fp.filterIndex) {
+    case 1:
+      content = getPEMString(cert);
+      for (let i = 1; i < chain.length; i++) {
+        content += getPEMString(chain[i]);
+      }
+      break;
+    case 2:
+      content = getDERString(cert);
+      break;
+    case 3:
+      content = getPKCS7String([cert]);
+      break;
+    case 4:
+      content = getPKCS7String(chain);
+      break;
+    case 0:
+    default:
+      content = getPEMString(cert);
+      break;
+  }
+  var msg;
+  var written = 0;
+  try {
+    var file = Cc["@mozilla.org/file/local;1"].
+               createInstance(Ci.nsIFile);
+    file.initWithPath(fp.file.path);
+    var fos = Cc["@mozilla.org/network/file-output-stream;1"].
+              createInstance(Ci.nsIFileOutputStream);
+    // flags: PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE
+    fos.init(file, 0x02 | 0x08 | 0x20, 0o0644, 0);
+    written = fos.write(content, content.length);
+    fos.close();
+  } catch (e) {
+    switch (e.result) {
+      case Cr.NS_ERROR_FILE_ACCESS_DENIED:
+        msg = bundle.getString("writeFileAccessDenied");
         break;
-      case 2:
-        content = getDERString(cert);
-        break;
-      case 3:
-        content = getPKCS7String([cert]);
+      case Cr.NS_ERROR_FILE_IS_LOCKED:
+        msg = bundle.getString("writeFileIsLocked");
         break;
-      case 4:
-        content = getPKCS7String(chain);
+      case Cr.NS_ERROR_FILE_NO_DEVICE_SPACE:
+      case Cr.NS_ERROR_FILE_DISK_FULL:
+        msg = bundle.getString("writeFileNoDeviceSpace");
         break;
-      case 0:
       default:
-        content = getPEMString(cert);
+        msg = e.message;
         break;
     }
-    var msg;
-    var written = 0;
-    try {
-      var file = Cc["@mozilla.org/file/local;1"].
-                 createInstance(Ci.nsIFile);
-      file.initWithPath(fp.file.path);
-      var fos = Cc["@mozilla.org/network/file-output-stream;1"].
-                createInstance(Ci.nsIFileOutputStream);
-      // flags: PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE
-      fos.init(file, 0x02 | 0x08 | 0x20, 0o0644, 0);
-      written = fos.write(content, content.length);
-      fos.close();
-    } catch (e) {
-      switch (e.result) {
-        case Cr.NS_ERROR_FILE_ACCESS_DENIED:
-          msg = bundle.getString("writeFileAccessDenied");
-          break;
-        case Cr.NS_ERROR_FILE_IS_LOCKED:
-          msg = bundle.getString("writeFileIsLocked");
-          break;
-        case Cr.NS_ERROR_FILE_NO_DEVICE_SPACE:
-        case Cr.NS_ERROR_FILE_DISK_FULL:
-          msg = bundle.getString("writeFileNoDeviceSpace");
-          break;
-        default:
-          msg = e.message;
-          break;
-      }
+  }
+  if (written != content.length) {
+    if (msg.length == 0) {
+      msg = bundle.getString("writeFileUnknownError");
     }
-    if (written != content.length) {
-      if (msg.length == 0) {
-        msg = bundle.getString("writeFileUnknownError");
-      }
-      alertPromptService(bundle.getString("writeFileFailure"),
-                         bundle.getFormattedString("writeFileFailed",
-                         [fp.file.path, msg]));
-    }
+    alertPromptService(bundle.getString("writeFileFailure"),
+                       bundle.getFormattedString("writeFileFailed",
+                       [fp.file.path, msg]));
+  }
+  if (Cu.isInAutomation) {
+    Services.obs.notifyObservers(null, "cert-export-finished");
   }
 }
 
 const PRErrorCodeSuccess = 0;
 
 // Certificate usages we care about in the certificate viewer.
 const certificateUsageSSLClient              = 0x0001;
 const certificateUsageSSLServer              = 0x0002;