diff --git a/src/core/catalog.js b/src/core/catalog.js index 10ec83db9..f4bfd1161 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -17,6 +17,7 @@ import { addDefaultProtocolToUrl, collectActions, MissingDataException, + recoverJsURL, toRomanNumerals, tryConvertUrlEncoding, } from "./core_utils.js"; @@ -1399,28 +1400,11 @@ class Catalog { js = jsAction; } - if (js) { - // Attempt to recover valid URLs from `JS` entries with certain - // white-listed formats: - // - window.open('http://example.com') - // - app.launchURL('http://example.com', true) - const URL_OPEN_METHODS = ["app.launchURL", "window.open"]; - const regex = new RegExp( - "^\\s*(" + - URL_OPEN_METHODS.join("|").split(".").join("\\.") + - ")\\((?:'|\")([^'\"]*)(?:'|\")(?:,\\s*(\\w+)\\)|\\))", - "i" - ); - - const jsUrl = regex.exec(stringToPDFString(js)); - if (jsUrl && jsUrl[2]) { - url = jsUrl[2]; - - if (jsUrl[3] === "true" && jsUrl[1] === "app.launchURL") { - resultObj.newWindow = true; - } - break; - } + const jsURL = js && recoverJsURL(stringToPDFString(js)); + if (jsURL) { + url = jsURL.url; + resultObj.newWindow = jsURL.newWindow; + break; } /* falls through */ default: diff --git a/src/core/core_utils.js b/src/core/core_utils.js index 93e8c22fb..bf3f18388 100644 --- a/src/core/core_utils.js +++ b/src/core/core_utils.js @@ -467,6 +467,34 @@ function tryConvertUrlEncoding(url) { } } +function recoverJsURL(str) { + // Attempt to recover valid URLs from `JS` entries with certain + // white-listed formats: + // - window.open('http://example.com') + // - app.launchURL('http://example.com', true) + // - xfa.host.gotoURL('http://example.com') + const URL_OPEN_METHODS = ["app.launchURL", "window.open", "xfa.host.gotoURL"]; + const regex = new RegExp( + "^\\s*(" + + URL_OPEN_METHODS.join("|").split(".").join("\\.") + + ")\\((?:'|\")([^'\"]*)(?:'|\")(?:,\\s*(\\w+)\\)|\\))", + "i" + ); + + const jsUrl = regex.exec(str); + if (jsUrl && jsUrl[2]) { + const url = jsUrl[2]; + let newWindow = false; + + if (jsUrl[3] === "true" && jsUrl[1] === "app.launchURL") { + newWindow = true; + } + return { url, newWindow }; + } + + return null; +} + export { addDefaultProtocolToUrl, collectActions, @@ -483,6 +511,7 @@ export { readInt8, readUint16, readUint32, + recoverJsURL, toRomanNumerals, tryConvertUrlEncoding, validateCSSFont, diff --git a/src/core/xfa/html_utils.js b/src/core/xfa/html_utils.js index b2194e3e7..22a5c1785 100644 --- a/src/core/xfa/html_utils.js +++ b/src/core/xfa/html_utils.js @@ -26,10 +26,14 @@ import { $toStyle, XFAObject, } from "./xfa_object.js"; +import { + addDefaultProtocolToUrl, + tryConvertUrlEncoding, +} from "../core_utils.js"; +import { createValidAbsoluteUrl, warn } from "../../shared/util.js"; import { getMeasurement, stripQuotes } from "./utils.js"; import { selectFont } from "./fonts.js"; import { TextMeasure } from "./text.js"; -import { warn } from "../../shared/util.js"; function measureToString(m) { if (typeof m === "string") { @@ -633,11 +637,24 @@ function setFontFamily(xfaFont, node, fontFinder, style) { } } +function fixURL(str) { + if (typeof str === "string") { + let url = addDefaultProtocolToUrl(str); + url = tryConvertUrlEncoding(url); + const absoluteUrl = createValidAbsoluteUrl(url); + if (absoluteUrl) { + return absoluteUrl.href; + } + } + return null; +} + export { computeBbox, createWrapper, fixDimensions, fixTextIndent, + fixURL, isPrintOnly, layoutClass, layoutNode, diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js index 04dbdbda9..5619a964d 100644 --- a/src/core/xfa/template.js +++ b/src/core/xfa/template.js @@ -76,6 +76,7 @@ import { createWrapper, fixDimensions, fixTextIndent, + fixURL, isPrintOnly, layoutClass, layoutNode, @@ -100,6 +101,7 @@ import { } from "./utils.js"; import { stringToBytes, Util, warn } from "../../shared/util.js"; import { getMetrics } from "./fonts.js"; +import { recoverJsURL } from "../core_utils.js"; import { searchNode } from "./som.js"; const TEMPLATE_NS_ID = NamespaceIds.template.id; @@ -1066,7 +1068,10 @@ class Button extends XFAObject { [$toHTML](availableSpace) { // TODO: highlight. - return HTMLResult.success({ + + const parent = this[$getParent](); + const grandpa = parent[$getParent](); + const htmlButton = { name: "button", attributes: { id: this[$uid], @@ -1074,7 +1079,38 @@ class Button extends XFAObject { style: {}, }, children: [], - }); + }; + + for (const event of grandpa.event.children) { + // if (true) break; + if (event.activity !== "click" || !event.script) { + continue; + } + const jsURL = recoverJsURL(event.script[$content]); + if (!jsURL) { + continue; + } + const href = fixURL(jsURL.url); + if (!href) { + continue; + } + const target = jsURL.newWindow ? "_blank" : undefined; + + // we've an url so generate a + htmlButton.children.push({ + name: "a", + attributes: { + id: "link" + this[$uid], + href, + target, + class: ["xfaLink"], + style: {}, + }, + children: [], + }); + } + + return HTMLResult.success(htmlButton); } } @@ -2897,7 +2933,12 @@ class Field extends XFAObject { ui.attributes.style = Object.create(null); } + let aElement = null; + if (this.ui.button) { + if (ui.children.length === 1) { + [aElement] = ui.children.splice(0, 1); + } Object.assign(ui.attributes.style, borderStyle); } else { Object.assign(style, borderStyle); @@ -2955,6 +2996,10 @@ class Field extends XFAObject { } } + if (aElement) { + ui.children.push(aElement); + } + if (!caption) { if (ui.attributes.class) { // Even if no caption this class will help to center the ui. diff --git a/src/core/xfa/xhtml.js b/src/core/xfa/xhtml.js index 51aa7730a..cdc6fef31 100644 --- a/src/core/xfa/xhtml.js +++ b/src/core/xfa/xhtml.js @@ -30,12 +30,12 @@ import { } from "./xfa_object.js"; import { $buildXFAObject, NamespaceIds } from "./namespaces.js"; import { - addDefaultProtocolToUrl, - tryConvertUrlEncoding, -} from "../core_utils.js"; -import { fixTextIndent, measureToString, setFontFamily } from "./html_utils.js"; + fixTextIndent, + fixURL, + measureToString, + setFontFamily, +} from "./html_utils.js"; import { getMeasurement, HTMLResult, stripQuotes } from "./utils.js"; -import { createValidAbsoluteUrl } from "../../shared/util.js"; const XHTML_NS_ID = NamespaceIds.xhtml.id; @@ -326,16 +326,7 @@ class XhtmlObject extends XmlObject { class A extends XhtmlObject { constructor(attributes) { super(attributes, "a"); - let href = ""; - if (typeof attributes.href === "string") { - let url = addDefaultProtocolToUrl(attributes.href); - url = tryConvertUrlEncoding(url); - const absoluteUrl = createValidAbsoluteUrl(url); - if (absoluteUrl) { - href = absoluteUrl.href; - } - } - this.href = href; + this.href = fixURL(attributes.href) || ""; } } diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 3953426d0..9a0c22ca7 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -1108,7 +1108,7 @@ describe("annotation", function () { jsEntry: "window.open('http://www.example.com/test.pdf')", expectedUrl: new URL("http://www.example.com/test.pdf").href, expectedUnsafeUrl: "http://www.example.com/test.pdf", - expectedNewWindow: undefined, + expectedNewWindow: false, }); // Check that we accept a white-listed {Stream} 'JS' entry. diff --git a/test/unit/xfa_tohtml_spec.js b/test/unit/xfa_tohtml_spec.js index bf3cd8f19..eccf75a1d 100644 --- a/test/unit/xfa_tohtml_spec.js +++ b/test/unit/xfa_tohtml_spec.js @@ -17,18 +17,20 @@ import { isNodeJS } from "../../src/shared/is_node.js"; import { XFAFactory } from "../../src/core/xfa/factory.js"; describe("XFAFactory", function () { - function searchHtmlNode(root, name, value, byAttributes = false) { + function searchHtmlNode(root, name, value, byAttributes = false, nth = [0]) { if ( (!byAttributes && root[name] === value) || (byAttributes && root.attributes && root.attributes[name] === value) ) { - return root; + if (nth[0]-- === 0) { + return root; + } } if (!root.children) { return null; } for (const child of root.children) { - const node = searchHtmlNode(child, name, value, byAttributes); + const node = searchHtmlNode(child, name, value, byAttributes, nth); if (node) { return node; } @@ -588,4 +590,60 @@ describe("XFAFactory", function () { expect(a.value).toEqual("qwerty/"); expect(a.attributes.href).toEqual(""); }); + + it("should replace button with an URL by a link", function () { + const xml = ` + + + + + + + + + `; + const factory = new XFAFactory({ "xdp:xdp": xml }); + + expect(factory.numberPages).toEqual(1); + + const pages = factory.getPages(); + let a = searchHtmlNode(pages, "name", "a"); + expect(a.attributes.href).toEqual("https://github.com/mozilla/pdf.js"); + expect(a.attributes.target).toEqual("_blank"); + + a = searchHtmlNode(pages, "name", "a", false, [1]); + expect(a.attributes.href).toEqual("https://github.com/allizom/pdf.js"); + expect(a.attributes.target).toBe(undefined); + }); }); diff --git a/web/xfa_layer_builder.css b/web/xfa_layer_builder.css index b2ee798f7..cfb06ea15 100644 --- a/web/xfa_layer_builder.css +++ b/web/xfa_layer_builder.css @@ -220,6 +220,11 @@ text-align: center; } +.xfaLink { + width: 100%; + height: 100%; +} + .xfaCheckbox, .xfaRadio { width: 100%;