From 09b4fe6a3059ca2536318f287c56f00fbd9f5082 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 13 Nov 2023 11:05:03 +0100 Subject: [PATCH] Get the field name from its parent when it doesn't have one when collecting fields (bug 1864136) Some fields, somewhere under the Fields entry in Acroform, could have no name (in T) but with a parent which has a name but which isn't somewhere under Fields. As a side-effect, this patch prevents infinite loops because of potential cycles under Fields. --- src/core/document.js | 42 +++++++++++++++++++--- test/integration/scripting_spec.mjs | 56 +++++++++++++++++++++++++++++ test/integration/test_utils.mjs | 24 +++++++++++++ 3 files changed, 118 insertions(+), 4 deletions(-) diff --git a/src/core/document.js b/src/core/document.js index a7b1342ef..00467497f 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -1711,12 +1711,19 @@ class PDFDocument { : clearGlobalCaches(); } - async #collectFieldObjects(name, fieldRef, promises, annotationGlobals) { + async #collectFieldObjects( + name, + fieldRef, + promises, + annotationGlobals, + visitedRefs + ) { const { xref } = this; - if (!(fieldRef instanceof Ref)) { + if (!(fieldRef instanceof Ref) || visitedRefs.has(fieldRef)) { return; } + visitedRefs.put(fieldRef); const field = await xref.fetchAsync(fieldRef); if (!(field instanceof Dict)) { return; @@ -1724,6 +1731,25 @@ class PDFDocument { if (field.has("T")) { const partName = stringToPDFString(await field.getAsync("T")); name = name === "" ? partName : `${name}.${partName}`; + } else { + let obj = field; + while (true) { + obj = obj.getRaw("Parent"); + if (obj instanceof Ref) { + if (visitedRefs.has(obj)) { + break; + } + obj = await xref.fetchAsync(obj); + } + if (!(obj instanceof Dict)) { + break; + } + if (obj.has("T")) { + const partName = stringToPDFString(await obj.getAsync("T")); + name = name === "" ? partName : `${name}.${partName}`; + break; + } + } } if (!promises.has(name)) { @@ -1751,7 +1777,13 @@ class PDFDocument { const kids = await field.getAsync("Kids"); if (Array.isArray(kids)) { for (const kid of kids) { - await this.#collectFieldObjects(name, kid, promises, annotationGlobals); + await this.#collectFieldObjects( + name, + kid, + promises, + annotationGlobals, + visitedRefs + ); } } } @@ -1769,6 +1801,7 @@ class PDFDocument { return null; } + const visitedRefs = new RefSet(); const allFields = Object.create(null); const fieldPromises = new Map(); for (const fieldRef of await acroForm.getAsync("Fields")) { @@ -1776,7 +1809,8 @@ class PDFDocument { "", fieldRef, fieldPromises, - annotationGlobals + annotationGlobals, + visitedRefs ); } diff --git a/test/integration/scripting_spec.mjs b/test/integration/scripting_spec.mjs index 52b60b1db..1154c0869 100644 --- a/test/integration/scripting_spec.mjs +++ b/test/integration/scripting_spec.mjs @@ -16,6 +16,7 @@ import { clearInput, closePages, + getAnnotationStorage, getComputedStyleSelector, getFirstSerialized, getQuerySelector, @@ -24,6 +25,7 @@ import { kbSelectAll, loadAndWait, scrollIntoView, + waitForEntryInStorage, } from "./test_utils.mjs"; describe("Interaction", () => { @@ -2225,4 +2227,58 @@ describe("Interaction", () => { ); }); }); + + describe("Radio button without T value", () => { + let pages; + let otherPages; + + beforeAll(async () => { + otherPages = await Promise.all( + global.integrationSessions.map(async session => + session.browser.newPage() + ) + ); + pages = await loadAndWait("bug1860602.pdf", getSelector("22R")); + }); + + afterAll(async () => { + await closePages(pages); + await Promise.all(otherPages.map(page => page.close())); + }); + + it("must check that only one radio is selected", async () => { + await Promise.all( + pages.map(async ([browserName, page], i) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + await scrollIntoView(page, getSelector("22R")); + + await page.click(getSelector("25R")); + await waitForEntryInStorage(page, "25R", { value: true }); + + let storage = await getAnnotationStorage(page); + expect(storage) + .withContext(`In ${browserName}`) + .toEqual({ + "22R": { value: false }, + "25R": { value: true }, + "28R": { value: false }, + }); + + await page.click(getSelector("22R")); + await waitForEntryInStorage(page, "22R", { value: true }); + + storage = await getAnnotationStorage(page); + expect(storage) + .withContext(`In ${browserName}`) + .toEqual({ + "22R": { value: true }, + "25R": { value: false }, + "28R": { value: false }, + }); + }) + ); + }); + }); }); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index ea8c06d96..05e43f4a1 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -176,6 +176,28 @@ async function getFirstSerialized(page, filter = undefined) { return (await getSerialized(page, filter))[0]; } +function getAnnotationStorage(page) { + return page.evaluate(() => + Object.fromEntries( + window.PDFViewerApplication.pdfDocument.annotationStorage.serializable.map?.entries() || + [] + ) + ); +} + +function waitForEntryInStorage(page, key, value) { + return page.waitForFunction( + (k, v) => { + const { map } = + window.PDFViewerApplication.pdfDocument.annotationStorage.serializable; + return map && JSON.stringify(map.get(k)) === v; + }, + {}, + key, + JSON.stringify(value) + ); +} + function getEditors(page, kind) { return page.evaluate(aKind => { const elements = document.querySelectorAll(`.${aKind}Editor`); @@ -398,6 +420,7 @@ export { clearInput, closePages, dragAndDropAnnotation, + getAnnotationStorage, getComputedStyleSelector, getEditorDimensions, getEditors, @@ -427,6 +450,7 @@ export { scrollIntoView, serializeBitmapDimensions, waitForAnnotationEditorLayer, + waitForEntryInStorage, waitForEvent, waitForSelectedEditor, waitForSerialized,