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:
parent
3b1d547738
commit
558e58f354
@ -17,6 +17,7 @@ import {
|
|||||||
addDefaultProtocolToUrl,
|
addDefaultProtocolToUrl,
|
||||||
collectActions,
|
collectActions,
|
||||||
MissingDataException,
|
MissingDataException,
|
||||||
|
recoverJsURL,
|
||||||
toRomanNumerals,
|
toRomanNumerals,
|
||||||
tryConvertUrlEncoding,
|
tryConvertUrlEncoding,
|
||||||
} from "./core_utils.js";
|
} from "./core_utils.js";
|
||||||
@ -1399,29 +1400,12 @@ class Catalog {
|
|||||||
js = jsAction;
|
js = jsAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (js) {
|
const jsURL = js && recoverJsURL(stringToPDFString(js));
|
||||||
// Attempt to recover valid URLs from `JS` entries with certain
|
if (jsURL) {
|
||||||
// white-listed formats:
|
url = jsURL.url;
|
||||||
// - window.open('http://example.com')
|
resultObj.newWindow = jsURL.newWindow;
|
||||||
// - 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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
/* falls through */
|
/* falls through */
|
||||||
default:
|
default:
|
||||||
if (
|
if (
|
||||||
|
@ -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 {
|
export {
|
||||||
addDefaultProtocolToUrl,
|
addDefaultProtocolToUrl,
|
||||||
collectActions,
|
collectActions,
|
||||||
@ -483,6 +511,7 @@ export {
|
|||||||
readInt8,
|
readInt8,
|
||||||
readUint16,
|
readUint16,
|
||||||
readUint32,
|
readUint32,
|
||||||
|
recoverJsURL,
|
||||||
toRomanNumerals,
|
toRomanNumerals,
|
||||||
tryConvertUrlEncoding,
|
tryConvertUrlEncoding,
|
||||||
validateCSSFont,
|
validateCSSFont,
|
||||||
|
@ -26,10 +26,14 @@ import {
|
|||||||
$toStyle,
|
$toStyle,
|
||||||
XFAObject,
|
XFAObject,
|
||||||
} from "./xfa_object.js";
|
} 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 { getMeasurement, stripQuotes } from "./utils.js";
|
||||||
import { selectFont } from "./fonts.js";
|
import { selectFont } from "./fonts.js";
|
||||||
import { TextMeasure } from "./text.js";
|
import { TextMeasure } from "./text.js";
|
||||||
import { warn } from "../../shared/util.js";
|
|
||||||
|
|
||||||
function measureToString(m) {
|
function measureToString(m) {
|
||||||
if (typeof m === "string") {
|
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 {
|
export {
|
||||||
computeBbox,
|
computeBbox,
|
||||||
createWrapper,
|
createWrapper,
|
||||||
fixDimensions,
|
fixDimensions,
|
||||||
fixTextIndent,
|
fixTextIndent,
|
||||||
|
fixURL,
|
||||||
isPrintOnly,
|
isPrintOnly,
|
||||||
layoutClass,
|
layoutClass,
|
||||||
layoutNode,
|
layoutNode,
|
||||||
|
@ -76,6 +76,7 @@ import {
|
|||||||
createWrapper,
|
createWrapper,
|
||||||
fixDimensions,
|
fixDimensions,
|
||||||
fixTextIndent,
|
fixTextIndent,
|
||||||
|
fixURL,
|
||||||
isPrintOnly,
|
isPrintOnly,
|
||||||
layoutClass,
|
layoutClass,
|
||||||
layoutNode,
|
layoutNode,
|
||||||
@ -100,6 +101,7 @@ import {
|
|||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
import { stringToBytes, Util, warn } from "../../shared/util.js";
|
import { stringToBytes, Util, warn } from "../../shared/util.js";
|
||||||
import { getMetrics } from "./fonts.js";
|
import { getMetrics } from "./fonts.js";
|
||||||
|
import { recoverJsURL } from "../core_utils.js";
|
||||||
import { searchNode } from "./som.js";
|
import { searchNode } from "./som.js";
|
||||||
|
|
||||||
const TEMPLATE_NS_ID = NamespaceIds.template.id;
|
const TEMPLATE_NS_ID = NamespaceIds.template.id;
|
||||||
@ -1066,7 +1068,10 @@ class Button extends XFAObject {
|
|||||||
|
|
||||||
[$toHTML](availableSpace) {
|
[$toHTML](availableSpace) {
|
||||||
// TODO: highlight.
|
// TODO: highlight.
|
||||||
return HTMLResult.success({
|
|
||||||
|
const parent = this[$getParent]();
|
||||||
|
const grandpa = parent[$getParent]();
|
||||||
|
const htmlButton = {
|
||||||
name: "button",
|
name: "button",
|
||||||
attributes: {
|
attributes: {
|
||||||
id: this[$uid],
|
id: this[$uid],
|
||||||
@ -1074,8 +1079,39 @@ class Button extends XFAObject {
|
|||||||
style: {},
|
style: {},
|
||||||
},
|
},
|
||||||
children: [],
|
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 {
|
class Calculate extends XFAObject {
|
||||||
@ -2897,7 +2933,12 @@ class Field extends XFAObject {
|
|||||||
ui.attributes.style = Object.create(null);
|
ui.attributes.style = Object.create(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let aElement = null;
|
||||||
|
|
||||||
if (this.ui.button) {
|
if (this.ui.button) {
|
||||||
|
if (ui.children.length === 1) {
|
||||||
|
[aElement] = ui.children.splice(0, 1);
|
||||||
|
}
|
||||||
Object.assign(ui.attributes.style, borderStyle);
|
Object.assign(ui.attributes.style, borderStyle);
|
||||||
} else {
|
} else {
|
||||||
Object.assign(style, borderStyle);
|
Object.assign(style, borderStyle);
|
||||||
@ -2955,6 +2996,10 @@ class Field extends XFAObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (aElement) {
|
||||||
|
ui.children.push(aElement);
|
||||||
|
}
|
||||||
|
|
||||||
if (!caption) {
|
if (!caption) {
|
||||||
if (ui.attributes.class) {
|
if (ui.attributes.class) {
|
||||||
// Even if no caption this class will help to center the ui.
|
// Even if no caption this class will help to center the ui.
|
||||||
|
@ -30,12 +30,12 @@ import {
|
|||||||
} from "./xfa_object.js";
|
} from "./xfa_object.js";
|
||||||
import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
|
import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
|
||||||
import {
|
import {
|
||||||
addDefaultProtocolToUrl,
|
fixTextIndent,
|
||||||
tryConvertUrlEncoding,
|
fixURL,
|
||||||
} from "../core_utils.js";
|
measureToString,
|
||||||
import { fixTextIndent, measureToString, setFontFamily } from "./html_utils.js";
|
setFontFamily,
|
||||||
|
} from "./html_utils.js";
|
||||||
import { getMeasurement, HTMLResult, stripQuotes } from "./utils.js";
|
import { getMeasurement, HTMLResult, stripQuotes } from "./utils.js";
|
||||||
import { createValidAbsoluteUrl } from "../../shared/util.js";
|
|
||||||
|
|
||||||
const XHTML_NS_ID = NamespaceIds.xhtml.id;
|
const XHTML_NS_ID = NamespaceIds.xhtml.id;
|
||||||
|
|
||||||
@ -326,16 +326,7 @@ class XhtmlObject extends XmlObject {
|
|||||||
class A extends XhtmlObject {
|
class A extends XhtmlObject {
|
||||||
constructor(attributes) {
|
constructor(attributes) {
|
||||||
super(attributes, "a");
|
super(attributes, "a");
|
||||||
let href = "";
|
this.href = fixURL(attributes.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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1108,7 +1108,7 @@ describe("annotation", function () {
|
|||||||
jsEntry: "window.open('http://www.example.com/test.pdf')",
|
jsEntry: "window.open('http://www.example.com/test.pdf')",
|
||||||
expectedUrl: new URL("http://www.example.com/test.pdf").href,
|
expectedUrl: new URL("http://www.example.com/test.pdf").href,
|
||||||
expectedUnsafeUrl: "http://www.example.com/test.pdf",
|
expectedUnsafeUrl: "http://www.example.com/test.pdf",
|
||||||
expectedNewWindow: undefined,
|
expectedNewWindow: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check that we accept a white-listed {Stream} 'JS' entry.
|
// Check that we accept a white-listed {Stream} 'JS' entry.
|
||||||
|
@ -17,18 +17,20 @@ import { isNodeJS } from "../../src/shared/is_node.js";
|
|||||||
import { XFAFactory } from "../../src/core/xfa/factory.js";
|
import { XFAFactory } from "../../src/core/xfa/factory.js";
|
||||||
|
|
||||||
describe("XFAFactory", function () {
|
describe("XFAFactory", function () {
|
||||||
function searchHtmlNode(root, name, value, byAttributes = false) {
|
function searchHtmlNode(root, name, value, byAttributes = false, nth = [0]) {
|
||||||
if (
|
if (
|
||||||
(!byAttributes && root[name] === value) ||
|
(!byAttributes && root[name] === value) ||
|
||||||
(byAttributes && root.attributes && root.attributes[name] === value)
|
(byAttributes && root.attributes && root.attributes[name] === value)
|
||||||
) {
|
) {
|
||||||
|
if (nth[0]-- === 0) {
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!root.children) {
|
if (!root.children) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
for (const child of root.children) {
|
for (const child of root.children) {
|
||||||
const node = searchHtmlNode(child, name, value, byAttributes);
|
const node = searchHtmlNode(child, name, value, byAttributes, nth);
|
||||||
if (node) {
|
if (node) {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
@ -588,4 +590,60 @@ describe("XFAFactory", function () {
|
|||||||
expect(a.value).toEqual("qwerty/");
|
expect(a.value).toEqual("qwerty/");
|
||||||
expect(a.attributes.href).toEqual("");
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -220,6 +220,11 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.xfaLink {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.xfaCheckbox,
|
.xfaCheckbox,
|
||||||
.xfaRadio {
|
.xfaRadio {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
Loading…
Reference in New Issue
Block a user