XFA - Add <a> element in button when an url is detected (bug 1716758)

- it aims to fix https://bugzilla.mozilla.org/show_bug.cgi?id=1716758;
  - some buttons have a JS action with the pattern `app.launchURL(...)` (or similar) so extract when it's possible the url and generate a <a> element with the href equals to the found url;
  - pdf.js already had some code to handle that so this patch slightly refactor that.
This commit is contained in:
Calixte Denizet 2021-09-25 14:46:40 +02:00
parent 3b1d547738
commit 558e58f354
8 changed files with 173 additions and 44 deletions

View File

@ -17,6 +17,7 @@ import {
addDefaultProtocolToUrl,
collectActions,
MissingDataException,
recoverJsURL,
toRomanNumerals,
tryConvertUrlEncoding,
} from "./core_utils.js";
@ -1399,29 +1400,12 @@ 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;
}
const jsURL = js && recoverJsURL(stringToPDFString(js));
if (jsURL) {
url = jsURL.url;
resultObj.newWindow = jsURL.newWindow;
break;
}
}
/* falls through */
default:
if (

View File

@ -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,

View File

@ -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,

View File

@ -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,8 +1079,39 @@ 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 <a>
htmlButton.children.push({
name: "a",
attributes: {
id: "link" + this[$uid],
href,
target,
class: ["xfaLink"],
style: {},
},
children: [],
});
}
return HTMLResult.success(htmlButton);
}
}
class Calculate extends XFAObject {
@ -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.

View File

@ -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) || "";
}
}

View File

@ -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.

View File

@ -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)
) {
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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3">
<subform name="root" mergeMode="matchTemplate">
<pageSet>
<pageArea>
<contentArea x="123pt" w="456pt" h="789pt"/>
<medium stock="default" short="456pt" long="789pt"/>
</pageArea>
</pageSet>
<subform name="first">
<field y="1pt" w="11pt" h="22pt" x="2pt">
<ui>
<button/>
</ui>
<event activity="click" name="event__click">
<script contentType="application/x-javascript">
app.launchURL("https://github.com/mozilla/pdf.js", true);
</script>
</event>
</field>
<field y="1pt" w="11pt" h="22pt" x="2pt">
<ui>
<button/>
</ui>
<event activity="click" name="event__click">
<script contentType="application/x-javascript">
xfa.host.gotoURL("https://github.com/allizom/pdf.js");
</script>
</event>
</field>
</subform>
</subform>
</template>
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
<xfa:data>
</xfa:data>
</xfa:datasets>
</xdp:xdp>
`;
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);
});
});

View File

@ -220,6 +220,11 @@
text-align: center;
}
.xfaLink {
width: 100%;
height: 100%;
}
.xfaCheckbox,
.xfaRadio {
width: 100%;