Bug 2058085 - Pick about:pdf files in the parent process. r=mossop,fluent-reviewers,flod
Move the file picker and PDF validation to AboutPDFParent so content never supplies a local path. Leave dropped PDFs to native drop handling. Differential Revision: https://phabricator.services.mozilla.com/D318472
This commit is contained in:
committed by
cdenizet@mozilla.com
parent
7baa0ba030
commit
aeffc52fd7
@@ -4,15 +4,13 @@
|
||||
|
||||
import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs";
|
||||
|
||||
const PDF_HEADER = "%PDF-";
|
||||
|
||||
export class AboutPDFChild extends RemotePageChild {
|
||||
actorCreated() {
|
||||
super.actorCreated();
|
||||
|
||||
this.exportFunctions([
|
||||
"RPMCanSetDefaultPDFHandler",
|
||||
"RPMOpenPDFFile",
|
||||
"RPMPickPDFFile",
|
||||
"RPMSetDefaultPDFHandler",
|
||||
]);
|
||||
}
|
||||
@@ -21,8 +19,14 @@ export class AboutPDFChild extends RemotePageChild {
|
||||
return this.wrapPromise(this.sendQuery("AboutPDF:CanSetDefaultPDFHandler"));
|
||||
}
|
||||
|
||||
RPMOpenPDFFile(file) {
|
||||
return this.wrapPromise(this.#openPDFFile(file));
|
||||
// User activation prevents the page from opening OS UI without a user
|
||||
// gesture. The parent still treats content messages as untrusted.
|
||||
RPMPickPDFFile() {
|
||||
if (!this.contentWindow.navigator.userActivation.isActive) {
|
||||
throw new Error("User activation is required");
|
||||
}
|
||||
|
||||
return this.wrapPromise(this.sendQuery("AboutPDF:PickFile"));
|
||||
}
|
||||
|
||||
RPMSetDefaultPDFHandler() {
|
||||
@@ -32,27 +36,4 @@ export class AboutPDFChild extends RemotePageChild {
|
||||
|
||||
return this.wrapPromise(this.sendQuery("AboutPDF:SetDefaultPDFHandler"));
|
||||
}
|
||||
|
||||
async #openPDFFile(file) {
|
||||
if (
|
||||
!file ||
|
||||
ChromeUtils.getClassName(file) !== "File" ||
|
||||
!file.name?.toLowerCase().endsWith(".pdf") ||
|
||||
!file.mozFullPath ||
|
||||
!(await this.#looksLikePDF(file))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.sendQuery("AboutPDF:OpenFile", {
|
||||
fileURL: PathUtils.toFileURI(file.mozFullPath),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cheap pre-filter so the page can flip to its error state instantly without
|
||||
// a round-trip to the parent. The parent re-validates before navigating.
|
||||
async #looksLikePDF(file) {
|
||||
return (await file.slice(0, PDF_HEADER.length).text()) === PDF_HEADER;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,23 @@ try {
|
||||
));
|
||||
} catch {}
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineLazyGetter(
|
||||
lazy,
|
||||
"l10n",
|
||||
() => new Localization(["toolkit/about/aboutPDF.ftl"])
|
||||
);
|
||||
|
||||
export class AboutPDFParent extends JSWindowActorParent {
|
||||
#filePickerOpenPromise = null;
|
||||
|
||||
receiveMessage(message) {
|
||||
switch (message.name) {
|
||||
case "AboutPDF:CanSetDefaultPDFHandler":
|
||||
return this.#canSetDefaultPDFHandler();
|
||||
case "AboutPDF:OpenFile":
|
||||
return this.#openFile(message.data?.fileURL);
|
||||
case "AboutPDF:PickFile":
|
||||
return this.#pickFile();
|
||||
case "AboutPDF:SetDefaultPDFHandler":
|
||||
return this.#setDefaultPDFHandler();
|
||||
}
|
||||
@@ -43,31 +53,54 @@ export class AboutPDFParent extends JSWindowActorParent {
|
||||
}
|
||||
}
|
||||
|
||||
async #openFile(fileURL) {
|
||||
if (typeof fileURL !== "string") {
|
||||
throw new Error("Expected a file URL");
|
||||
// Never accept a path from content: this load uses the system principal.
|
||||
// Native drop handling separately verifies dropped links against the drag
|
||||
// session.
|
||||
// Returns "opened", "canceled", or "invalid".
|
||||
async #pickFile() {
|
||||
if (this.#filePickerOpenPromise) {
|
||||
return "canceled";
|
||||
}
|
||||
|
||||
let uri = Services.io.newURI(fileURL);
|
||||
if (!uri.schemeIs("file")) {
|
||||
throw new Error("Expected a file URL");
|
||||
let browsingContext = this.browsingContext.top;
|
||||
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
|
||||
fp.init(
|
||||
this.browsingContext,
|
||||
await lazy.l10n.formatValue("about-pdf-file-picker-title"),
|
||||
Ci.nsIFilePicker.modeOpen
|
||||
);
|
||||
fp.appendFilters(Ci.nsIFilePicker.filterPDF);
|
||||
fp.appendFilters(Ci.nsIFilePicker.filterAll);
|
||||
|
||||
let result;
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
this.#filePickerOpenPromise = promise;
|
||||
try {
|
||||
fp.open(resolve);
|
||||
result = await this.#filePickerOpenPromise;
|
||||
} finally {
|
||||
this.#filePickerOpenPromise = null;
|
||||
}
|
||||
|
||||
let nsFile = uri.QueryInterface(Ci.nsIFileURL).file;
|
||||
if (!nsFile.leafName.toLowerCase().endsWith(".pdf")) {
|
||||
throw new Error("Expected a PDF file URL");
|
||||
}
|
||||
if (!nsFile.exists() || !nsFile.isFile()) {
|
||||
throw new Error("Expected an existing PDF file");
|
||||
}
|
||||
let file = await File.createFromNsIFile(nsFile);
|
||||
if (!(await this.#looksLikePDF(file))) {
|
||||
throw new Error("Expected PDF content");
|
||||
if (result !== Ci.nsIFilePicker.returnOK) {
|
||||
return "canceled";
|
||||
}
|
||||
|
||||
this.browsingContext.top.loadURI(uri, {
|
||||
let file = fp.file;
|
||||
if (
|
||||
!file?.leafName.toLowerCase().endsWith(".pdf") ||
|
||||
!(await this.#looksLikePDF(file))
|
||||
) {
|
||||
return "invalid";
|
||||
}
|
||||
|
||||
if (browsingContext.isDiscarded) {
|
||||
return "canceled";
|
||||
}
|
||||
browsingContext.loadURI(fp.fileURL, {
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
});
|
||||
return "opened";
|
||||
}
|
||||
|
||||
async #setDefaultPDFHandler() {
|
||||
@@ -83,6 +116,13 @@ export class AboutPDFParent extends JSWindowActorParent {
|
||||
}
|
||||
|
||||
async #looksLikePDF(file) {
|
||||
return (await file.slice(0, PDF_HEADER.length).text()) === PDF_HEADER;
|
||||
try {
|
||||
let bytes = await IOUtils.read(file.path, {
|
||||
maxBytes: PDF_HEADER.length,
|
||||
});
|
||||
return new TextDecoder().decode(bytes) === PDF_HEADER;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,12 +61,6 @@
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
id="file-input"
|
||||
accept=".pdf,application/pdf"
|
||||
hidden
|
||||
/>
|
||||
<div id="promo-row">
|
||||
<moz-promo
|
||||
id="promo"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 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/. */
|
||||
|
||||
/* global RPMCanSetDefaultPDFHandler, RPMGetBoolPref, RPMOpenPDFFile,
|
||||
/* global RPMCanSetDefaultPDFHandler, RPMGetBoolPref, RPMPickPDFFile,
|
||||
RPMSetDefaultPDFHandler, RPMSetPref */
|
||||
|
||||
const PROMO_DISMISSED_PREF = "browser.aboutpdf.promo.dismissed";
|
||||
@@ -10,23 +10,13 @@ const PROMO_DISMISSED_PREF = "browser.aboutpdf.promo.dismissed";
|
||||
const dropzone = document.getElementById("dropzone");
|
||||
const dropzoneHint = document.getElementById("dropzone-hint");
|
||||
const dropzoneError = document.getElementById("dropzone-error");
|
||||
const fileInput = document.getElementById("file-input");
|
||||
const browseFiles = document.getElementById("browse-files");
|
||||
const promo = document.getElementById("promo");
|
||||
const setDefault = document.getElementById("set-default");
|
||||
const dismissPromo = document.getElementById("dismiss-promo");
|
||||
|
||||
browseFiles.addEventListener("click", () => {
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
fileInput.addEventListener("change", () => {
|
||||
const file = fileInput.files[0];
|
||||
// Reset so the same file can be re-selected after an error.
|
||||
fileInput.value = "";
|
||||
if (file) {
|
||||
handleFile(file);
|
||||
}
|
||||
pickFile();
|
||||
});
|
||||
|
||||
dropzone.addEventListener("dragenter", e => {
|
||||
@@ -48,11 +38,19 @@ dropzone.addEventListener("dragleave", e => {
|
||||
});
|
||||
|
||||
dropzone.addEventListener("drop", e => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.remove("drag-over");
|
||||
const file = e.dataTransfer?.files[0];
|
||||
if (file) {
|
||||
handleFile(file);
|
||||
// Let native handling open .pdf files with a PDF MIME type; cancel others.
|
||||
const files = e.dataTransfer?.files;
|
||||
if (
|
||||
!files?.length ||
|
||||
![...files].every(
|
||||
file =>
|
||||
file.type === "application/pdf" &&
|
||||
file.name.toLowerCase().endsWith(".pdf")
|
||||
)
|
||||
) {
|
||||
e.preventDefault();
|
||||
showError("invalid");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -60,7 +58,7 @@ dropzone.addEventListener("drop", e => {
|
||||
// through #browse-files which has the real button semantics.
|
||||
dropzone.addEventListener("click", e => {
|
||||
if (!e.target.closest("#browse-files")) {
|
||||
fileInput.click();
|
||||
pickFile();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -106,14 +104,15 @@ async function updatePromoVisibility() {
|
||||
|
||||
let processing = false;
|
||||
|
||||
async function handleFile(file) {
|
||||
// The parent validates the selected file before opening it.
|
||||
async function pickFile() {
|
||||
if (processing) {
|
||||
return;
|
||||
}
|
||||
processing = true;
|
||||
showError(null);
|
||||
try {
|
||||
if (!(await RPMOpenPDFFile(file))) {
|
||||
if ((await RPMPickPDFFile()) === "invalid") {
|
||||
showError("invalid");
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -124,7 +123,7 @@ async function handleFile(file) {
|
||||
}
|
||||
}
|
||||
|
||||
// errorType: null (clear), "invalid" (file type), or "generic" (other failure).
|
||||
// errorType: null, "invalid" (not a PDF), or "generic".
|
||||
function showError(errorType) {
|
||||
if (!errorType) {
|
||||
dropzoneError.hidden = true;
|
||||
@@ -142,8 +141,7 @@ function showError(errorType) {
|
||||
dropzoneHint.hidden = errorType === "invalid";
|
||||
}
|
||||
|
||||
// Enter triggers the picker only when the dropzone itself is hovered and no
|
||||
// inner control has focus (which would handle Enter itself).
|
||||
// Enter opens the picker while the dropzone is hovered and no control has focus.
|
||||
document.addEventListener("keydown", e => {
|
||||
if (e.key === "Enter" && dropzone.matches(":hover")) {
|
||||
const active = document.activeElement;
|
||||
@@ -153,7 +151,7 @@ document.addEventListener("keydown", e => {
|
||||
active === document.documentElement
|
||||
) {
|
||||
e.preventDefault();
|
||||
fileInput.click();
|
||||
pickFile();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,11 +12,6 @@ add_task(async function testPageRenders() {
|
||||
ok(doc.getElementById("dropzone-hint"), "dropzone-hint exists");
|
||||
ok(doc.getElementById("browse-files"), "browse button exists");
|
||||
|
||||
const fileInput = doc.getElementById("file-input");
|
||||
ok(fileInput, "file input exists");
|
||||
is(fileInput.accept, ".pdf,application/pdf", "file input accept attribute");
|
||||
ok(fileInput.hidden, "file input is hidden");
|
||||
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => dropzone.title,
|
||||
"dropzone title is localized"
|
||||
@@ -57,3 +52,69 @@ add_task(async function testDragVisualState() {
|
||||
});
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function testDropOnlyCancelsNonPDFs() {
|
||||
const tab = await openAboutPDF();
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
|
||||
const doc = content.document;
|
||||
const dropzone = doc.getElementById("dropzone");
|
||||
const errorEl = doc.getElementById("dropzone-error");
|
||||
|
||||
// Build the DataTransfer in the page's principal so its files are readable.
|
||||
const sandbox = SpecialPowers.Cu.Sandbox(doc.nodePrincipal, {
|
||||
sandboxPrototype: content,
|
||||
});
|
||||
const buildDataTransfer = SpecialPowers.Cu.evalInSandbox(
|
||||
`files => {
|
||||
const dataTransfer = new DataTransfer();
|
||||
for (const { name, type } of files) {
|
||||
dataTransfer.items.add(new File(["%PDF-1.4"], name, { type }));
|
||||
}
|
||||
return dataTransfer;
|
||||
}`,
|
||||
sandbox
|
||||
);
|
||||
|
||||
// Record whether the page canceled the drop before canceling it for the test.
|
||||
function drop(...files) {
|
||||
const dataTransfer = buildDataTransfer(
|
||||
SpecialPowers.Cu.cloneInto(files, sandbox)
|
||||
);
|
||||
let canceledByPage;
|
||||
doc.addEventListener(
|
||||
"drop",
|
||||
e => {
|
||||
canceledByPage = e.defaultPrevented;
|
||||
// Prevent native drop handling in this synthetic test.
|
||||
e.preventDefault();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
dropzone.dispatchEvent(
|
||||
new content.DragEvent("drop", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
dataTransfer,
|
||||
})
|
||||
);
|
||||
return canceledByPage;
|
||||
}
|
||||
|
||||
const pdf = { name: "doc.pdf", type: "application/pdf" };
|
||||
const text = { name: "doc.txt", type: "text/plain" };
|
||||
|
||||
ok(!drop(pdf), "a dropped PDF is left to the browser to open");
|
||||
ok(errorEl.hidden, "no error is shown for a PDF");
|
||||
|
||||
ok(drop(text), "a dropped non-PDF is canceled");
|
||||
ok(!errorEl.hidden, "the invalid file error is shown");
|
||||
|
||||
ok(
|
||||
drop({ name: "doc.pdf", type: "text/plain" }),
|
||||
"a .pdf name with another type is canceled"
|
||||
);
|
||||
ok(drop(pdf, text), "a mixed set of dropped files is canceled");
|
||||
ok(drop(), "a drop without any file is canceled");
|
||||
});
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
@@ -3,100 +3,83 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const { MockFilePicker } = SpecialPowers;
|
||||
|
||||
const PDF_CONTENTS = `%PDF-1.4
|
||||
1 0 obj<</Pages 2 0 R>>endobj
|
||||
2 0 obj<</Kids[3 0 R]/Count 1>>endobj
|
||||
3 0 obj<</MediaBox[0 0 1 1]>>endobj
|
||||
trailer<</Root 1 0 R/Size 4>>`;
|
||||
|
||||
async function callOpenFile(actor, fileURL) {
|
||||
return actor.receiveMessage({
|
||||
name: "AboutPDF:OpenFile",
|
||||
data: { fileURL },
|
||||
add_setup(function () {
|
||||
MockFilePicker.init();
|
||||
registerCleanupFunction(() => {
|
||||
MockFilePicker.cleanup();
|
||||
});
|
||||
}
|
||||
|
||||
add_task(async function testRejectsNonStringURL() {
|
||||
const tab = await openAboutPDF();
|
||||
const actor = getAboutPDFActor(tab);
|
||||
await Assert.rejects(
|
||||
callOpenFile(actor, 42),
|
||||
/Expected a file URL/,
|
||||
"non-string fileURL rejected"
|
||||
);
|
||||
await Assert.rejects(
|
||||
callOpenFile(actor, undefined),
|
||||
/Expected a file URL/,
|
||||
"undefined fileURL rejected"
|
||||
);
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function testRejectsNonFileScheme() {
|
||||
function pickFile(actor, file) {
|
||||
MockFilePicker.setFiles(file ? [file] : []);
|
||||
MockFilePicker.returnValue = file
|
||||
? MockFilePicker.returnOK
|
||||
: MockFilePicker.returnCancel;
|
||||
return actor.receiveMessage({ name: "AboutPDF:PickFile" });
|
||||
}
|
||||
|
||||
add_task(async function testCanceledPicker() {
|
||||
const tab = await openAboutPDF();
|
||||
const actor = getAboutPDFActor(tab);
|
||||
await Assert.rejects(
|
||||
callOpenFile(actor, "https://example.com/foo.pdf"),
|
||||
/Expected a file URL/,
|
||||
"https URL rejected"
|
||||
is(
|
||||
await pickFile(getAboutPDFActor(tab), null),
|
||||
"canceled",
|
||||
"a canceled picker opens nothing"
|
||||
);
|
||||
is(
|
||||
tab.linkedBrowser.currentURI.spec,
|
||||
"about:pdf",
|
||||
"the tab did not navigate"
|
||||
);
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function testRejectsNonPDFExtension() {
|
||||
const path = await createTempFile(PDF_CONTENTS, { suffix: ".txt" });
|
||||
const file = await createTempFile(PDF_CONTENTS, { suffix: ".txt" });
|
||||
const tab = await openAboutPDF();
|
||||
const actor = getAboutPDFActor(tab);
|
||||
await Assert.rejects(
|
||||
callOpenFile(actor, PathUtils.toFileURI(path)),
|
||||
/Expected a PDF file URL/,
|
||||
is(
|
||||
await pickFile(getAboutPDFActor(tab), file),
|
||||
"invalid",
|
||||
".txt extension rejected"
|
||||
);
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
await safeRemove(path);
|
||||
});
|
||||
|
||||
add_task(async function testRejectsMissingFile() {
|
||||
const path = PathUtils.join(
|
||||
PathUtils.tempDir,
|
||||
`aboutPDF-missing-${Date.now()}.pdf`
|
||||
);
|
||||
const tab = await openAboutPDF();
|
||||
const actor = getAboutPDFActor(tab);
|
||||
await Assert.rejects(
|
||||
callOpenFile(actor, PathUtils.toFileURI(path)),
|
||||
/Expected an existing PDF file/,
|
||||
"non-existent file rejected"
|
||||
);
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
await safeRemove(file);
|
||||
});
|
||||
|
||||
add_task(async function testRejectsBadMagicBytes() {
|
||||
const path = await createTempFile("not actually a pdf");
|
||||
const file = await createTempFile("not actually a pdf");
|
||||
const tab = await openAboutPDF();
|
||||
const actor = getAboutPDFActor(tab);
|
||||
await Assert.rejects(
|
||||
callOpenFile(actor, PathUtils.toFileURI(path)),
|
||||
/Expected PDF content/,
|
||||
is(
|
||||
await pickFile(getAboutPDFActor(tab), file),
|
||||
"invalid",
|
||||
"file without %PDF- header rejected"
|
||||
);
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
await safeRemove(path);
|
||||
await safeRemove(file);
|
||||
});
|
||||
|
||||
add_task(async function testAcceptsValidPDFAndNavigates() {
|
||||
const path = await createTempFile(PDF_CONTENTS);
|
||||
const fileURL = PathUtils.toFileURI(path);
|
||||
const file = await createTempFile(PDF_CONTENTS);
|
||||
const fileURL = Services.io.newFileURI(file).spec;
|
||||
|
||||
const tab = await openAboutPDF();
|
||||
const actor = getAboutPDFActor(tab);
|
||||
|
||||
const navigated = BrowserTestUtils.browserLoaded(
|
||||
tab.linkedBrowser,
|
||||
false,
|
||||
url => url === fileURL
|
||||
);
|
||||
await callOpenFile(actor, fileURL);
|
||||
is(
|
||||
await pickFile(getAboutPDFActor(tab), file),
|
||||
"opened",
|
||||
"the picked PDF is opened"
|
||||
);
|
||||
await navigated;
|
||||
|
||||
is(
|
||||
@@ -111,5 +94,149 @@ add_task(async function testAcceptsValidPDFAndNavigates() {
|
||||
});
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
await safeRemove(path);
|
||||
await safeRemove(file);
|
||||
});
|
||||
|
||||
add_task(async function testPickerNeedsUserActivation() {
|
||||
// Avoid navigation if this unexpectedly succeeds.
|
||||
MockFilePicker.setFiles([]);
|
||||
MockFilePicker.returnValue = MockFilePicker.returnCancel;
|
||||
|
||||
const tab = await openAboutPDF();
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
|
||||
let threw = false;
|
||||
try {
|
||||
content.wrappedJSObject.RPMPickPDFFile();
|
||||
} catch {
|
||||
threw = true;
|
||||
}
|
||||
ok(threw, "a script can't open the picker without a user gesture");
|
||||
});
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function testBrowseButtonOpensPickedPDF() {
|
||||
const file = await createTempFile(PDF_CONTENTS);
|
||||
const fileURL = Services.io.newFileURI(file).spec;
|
||||
MockFilePicker.setFiles([file]);
|
||||
MockFilePicker.returnValue = MockFilePicker.returnOK;
|
||||
|
||||
const tab = await openAboutPDF();
|
||||
const navigated = BrowserTestUtils.browserLoaded(
|
||||
tab.linkedBrowser,
|
||||
false,
|
||||
url => url === fileURL
|
||||
);
|
||||
MockFilePicker.shown = false;
|
||||
// A scripted click would not grant user activation.
|
||||
await BrowserTestUtils.synthesizeMouseAtCenter(
|
||||
"#browse-files",
|
||||
{},
|
||||
tab.linkedBrowser
|
||||
);
|
||||
await TestUtils.waitForCondition(
|
||||
() => MockFilePicker.shown,
|
||||
"the browse button opened the picker"
|
||||
);
|
||||
await navigated;
|
||||
|
||||
is(
|
||||
tab.linkedBrowser.currentURI.spec,
|
||||
fileURL,
|
||||
"the browse button opens the picked PDF"
|
||||
);
|
||||
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
|
||||
const viewer = content.wrappedJSObject.PDFViewerApplication;
|
||||
await viewer.testingClose();
|
||||
});
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
await safeRemove(file);
|
||||
});
|
||||
|
||||
add_task(async function testBrowseButtonReportsANonPDF() {
|
||||
const file = await createTempFile(PDF_CONTENTS, { suffix: ".txt" });
|
||||
MockFilePicker.setFiles([file]);
|
||||
MockFilePicker.returnValue = MockFilePicker.returnOK;
|
||||
|
||||
const tab = await openAboutPDF();
|
||||
await BrowserTestUtils.synthesizeMouseAtCenter(
|
||||
"#browse-files",
|
||||
{},
|
||||
tab.linkedBrowser
|
||||
);
|
||||
await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
|
||||
const error = content.document.getElementById("dropzone-error");
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => !error.hidden,
|
||||
"the page reports the invalid file"
|
||||
);
|
||||
is(
|
||||
error.getAttribute("data-l10n-id"),
|
||||
"about-pdf-dropzone-invalid-file",
|
||||
"the invalid file error is shown"
|
||||
);
|
||||
});
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
await safeRemove(file);
|
||||
});
|
||||
|
||||
add_task(async function testSecondPickerIsIgnored() {
|
||||
const tab = await openAboutPDF();
|
||||
const actor = getAboutPDFActor(tab);
|
||||
|
||||
let opens = 0;
|
||||
let releaseFirst;
|
||||
MockFilePicker.setFiles([]);
|
||||
MockFilePicker.returnValue = MockFilePicker.returnCancel;
|
||||
MockFilePicker.showCallback = () => {
|
||||
opens++;
|
||||
// Keep the first picker open; close an unexpected second.
|
||||
return opens === 1
|
||||
? new Promise(resolve => {
|
||||
releaseFirst = resolve;
|
||||
})
|
||||
: Promise.resolve();
|
||||
};
|
||||
|
||||
const showing = actor.receiveMessage({ name: "AboutPDF:PickFile" });
|
||||
await TestUtils.waitForCondition(() => opens, "the first picker is showing");
|
||||
is(
|
||||
await actor.receiveMessage({ name: "AboutPDF:PickFile" }),
|
||||
"canceled",
|
||||
"the second request is refused"
|
||||
);
|
||||
is(opens, 1, "no second picker was opened while one was up");
|
||||
|
||||
releaseFirst();
|
||||
await showing;
|
||||
MockFilePicker.showCallback = null;
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
// Ignore file URLs from content; the parent must obtain the path itself.
|
||||
add_task(async function testIgnoresFileURLFromContent() {
|
||||
const file = await createTempFile(PDF_CONTENTS);
|
||||
const tab = await openAboutPDF();
|
||||
const actor = getAboutPDFActor(tab);
|
||||
|
||||
is(
|
||||
await actor.receiveMessage({
|
||||
name: "AboutPDF:OpenFile",
|
||||
data: { fileURL: Services.io.newFileURI(file).spec },
|
||||
}),
|
||||
undefined,
|
||||
"a file URL coming from the content process is not handled"
|
||||
);
|
||||
await TestUtils.waitForTick();
|
||||
is(
|
||||
tab.linkedBrowser.currentURI.spec,
|
||||
"about:pdf",
|
||||
"the tab did not navigate"
|
||||
);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
await safeRemove(file);
|
||||
});
|
||||
|
||||
@@ -21,17 +21,16 @@ async function createTempFile(contents, { suffix = ".pdf" } = {}) {
|
||||
const file = Services.dirsvc.get("TmpD", Ci.nsIFile);
|
||||
file.append(`aboutPDF-test${suffix}`);
|
||||
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
|
||||
const path = file.path;
|
||||
const bytes =
|
||||
typeof contents === "string"
|
||||
? new TextEncoder().encode(contents)
|
||||
: contents;
|
||||
await IOUtils.write(path, bytes);
|
||||
return path;
|
||||
await IOUtils.write(file.path, bytes);
|
||||
return file;
|
||||
}
|
||||
|
||||
async function safeRemove(path) {
|
||||
async function safeRemove(file) {
|
||||
try {
|
||||
await IOUtils.remove(path, { ignoreAbsent: true });
|
||||
await IOUtils.remove(file.path, { ignoreAbsent: true });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ about-pdf-dropzone-invalid-file = This file isn’t supported. Choose a PDF to c
|
||||
about-pdf-dropzone-error-generic = The upload didn’t go through. Try again later.
|
||||
about-pdf-browse-files =
|
||||
.label = Browse files
|
||||
# Title of the system file picker opened by the “Browse files” button.
|
||||
about-pdf-file-picker-title = Open PDF
|
||||
about-pdf-promo =
|
||||
.heading = Make { -brand-short-name } your go-to PDF editor
|
||||
.message = Open PDFs in { -brand-short-name } and handle the basics in one place, from highlights and signatures to merged files and comments.
|
||||
|
||||
@@ -87,7 +87,7 @@ export let RemotePageAccessManager = {
|
||||
"about:pdf": {
|
||||
RPMCanSetDefaultPDFHandler: ["*"],
|
||||
RPMGetBoolPref: ["browser.aboutpdf.promo.dismissed"],
|
||||
RPMOpenPDFFile: ["*"],
|
||||
RPMPickPDFFile: ["*"],
|
||||
RPMSetDefaultPDFHandler: ["*"],
|
||||
RPMSetPref: ["browser.aboutpdf.promo.dismissed"],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user