JS -- Fix events dispatchment and add tests

* dispatch event to take into account calculation order
 * use a map for actions in Field
This commit is contained in:
Calixte Denizet 2020-11-03 19:24:07 +01:00
parent 83658c974d
commit 2dfac4cb41
7 changed files with 459 additions and 104 deletions

View File

@ -501,6 +501,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
element.setAttribute("value", textContent);
}
element.userValue = textContent;
element.setAttribute("id", id);
element.addEventListener("input", function (event) {
@ -512,26 +513,76 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
});
if (this.enableScripting && this.hasJSActions) {
element.addEventListener("updateFromSandbox", function (event) {
const data = event.detail;
if ("value" in data) {
event.target.value = event.detail.value;
} else if ("focus" in data) {
event.target.focus({ preventScroll: false });
element.addEventListener("focus", event => {
if (event.target.userValue) {
event.target.value = event.target.userValue;
}
});
if (this.data.actions !== null) {
if (this.data.actions) {
element.addEventListener("updateFromSandbox", function (event) {
const detail = event.detail;
const actions = {
value() {
const value = detail.value;
if (value === undefined || value === null) {
// remove data
event.target.userValue = "";
} else {
event.target.userValue = value;
}
},
valueAsString() {
const value = detail.valueAsString;
if (value === undefined || value === null) {
// remove data
event.target.value = "";
} else {
event.target.value = value;
}
storage.setValue(id, event.target.value);
},
focus() {
event.target.focus({ preventScroll: false });
},
userName() {
const tooltip = detail.userName;
event.target.title = tooltip;
},
hidden() {
event.target.style.display = detail.hidden ? "none" : "block";
},
editable() {
event.target.disabled = !detail.editable;
},
selRange() {
const [selStart, selEnd] = detail.selRange;
if (selStart >= 0 && selEnd < event.target.value.length) {
event.target.setSelectionRange(selStart, selEnd);
}
},
};
for (const name of Object.keys(detail)) {
if (name in actions) {
actions[name]();
}
}
});
for (const eventType of Object.keys(this.data.actions)) {
switch (eventType) {
case "Format":
element.addEventListener("blur", function (event) {
element.addEventListener("change", function (event) {
window.dispatchEvent(
new CustomEvent("dispatchEventInSandbox", {
detail: {
id,
name: "Format",
name: "Keystroke",
value: event.target.value,
willCommit: true,
commitKey: 1,
selStart: event.target.selectionStart,
selEnd: event.target.selectionEnd,
},
})
);

View File

@ -30,6 +30,7 @@ class InfoProxyHandler {
class Doc extends PDFObject {
constructor(data) {
super(data);
this.calculate = true;
this.baseURL = data.baseURL || "";
this.calculate = true;

View File

@ -26,14 +26,14 @@ class Event {
this.richChange = data.richChange || [];
this.richChangeEx = data.richChangeEx || [];
this.richValue = data.richValue || [];
this.selEnd = data.selEnd || 0;
this.selStart = data.selStart || 0;
this.selEnd = data.selEnd || -1;
this.selStart = data.selStart || -1;
this.shift = data.shift || false;
this.source = data.source || null;
this.target = data.target || null;
this.targetName = data.targetName || "";
this.targetName = "";
this.type = "Field";
this.value = data.value || null;
this.value = data.value || "";
this.willCommit = data.willCommit || false;
}
}
@ -47,6 +47,21 @@ class EventDispatcher {
this._document.obj._eventDispatcher = this;
}
mergeChange(event) {
let value = event.value;
if (typeof value !== "string") {
value = value.toString();
}
const prefix =
event.selStart >= 0 ? value.substring(0, event.selStart) : "";
const postfix =
event.selEnd >= 0 && event.selEnd <= value.length
? value.substring(event.selEnd)
: "";
return `${prefix}${event.change}${postfix}`;
}
dispatch(baseEvent) {
const id = baseEvent.id;
if (!(id in this._objects)) {
@ -56,11 +71,71 @@ class EventDispatcher {
const name = baseEvent.name.replace(" ", "");
const source = this._objects[id];
const event = (this._document.obj._event = new Event(baseEvent));
const oldValue = source.obj.value;
let savedChange;
if (source.obj._isButton()) {
source.obj._id = id;
event.value = source.obj._getExportValue(event.value);
}
if (name === "Keystroke") {
savedChange = {
value: event.value,
change: event.change,
selStart: event.selStart,
selEnd: event.selEnd,
};
} else if (name === "Blur" || name === "Focus") {
Object.defineProperty(event, "value", {
configurable: false,
writable: false,
enumerable: true,
value: event.value,
});
} else if (name === "Validate") {
this.runValidation(source, event);
return;
}
this.runActions(source, source, event, name);
if (event.rc && oldValue !== event.value) {
source.wrapped.value = event.value;
if (name === "Keystroke") {
if (event.rc) {
if (event.willCommit) {
this.runValidation(source, event);
} else if (
event.change !== savedChange.change ||
event.selStart !== savedChange.selStart ||
event.selEnd !== savedChange.selEnd
) {
source.wrapped.value = this.mergeChange(event);
}
} else if (!event.willCommit) {
source.obj._send({
id: source.obj._id,
value: savedChange.value,
selRange: [savedChange.selStart, savedChange.selEnd],
});
}
}
}
runValidation(source, event) {
const hasRan = this.runActions(source, source, event, "Validate");
if (event.rc) {
if (hasRan) {
source.wrapped.value = event.value;
} else {
source.obj.value = event.value;
}
if (this._document.obj.calculate) {
this.runCalculate(source, event);
}
event.value = source.obj.value;
this.runActions(source, source, event, "Format");
source.wrapped.valueAsString = event.value;
}
}
@ -68,11 +143,43 @@ class EventDispatcher {
event.source = source.wrapped;
event.target = target.wrapped;
event.name = eventName;
event.targetName = target.obj.name;
event.rc = true;
if (!target.obj._runActions(event)) {
return true;
return target.obj._runActions(event);
}
calculateNow() {
if (this._calculationOrder.length === 0) {
return;
}
const first = this._calculationOrder[0];
const source = this._objects[first];
const event = (this._document.obj._event = new Event({}));
this.runCalculate(source, event);
}
runCalculate(source, event) {
if (this._calculationOrder.length === 0) {
return;
}
for (const targetId of this._calculationOrder) {
if (!(targetId in this._objects)) {
continue;
}
const target = this._objects[targetId];
this.runActions(source, target, event, "Calculate");
this.runActions(target, target, event, "Validate");
if (!event.rc) {
continue;
}
target.wrapped.value = event.value;
this.runActions(target, target, event, "Format");
target.wrapped.valueAsString = event.value;
}
return event.rc;
}
}

View File

@ -71,17 +71,8 @@ class Field extends PDFObject {
this.valueAsString = data.valueAsString;
// Private
this._actions = Object.create(null);
const doc = (this._document = data.doc);
if (data.actions !== null) {
for (const [eventType, actions] of Object.entries(data.actions)) {
// This code is running in a sandbox so it's safe to use Function
this._actions[eventType] = actions.map(action =>
// eslint-disable-next-line no-new-func
Function("event", `with (this) {${action}}`).bind(doc)
);
}
}
this._document = data.doc;
this._actions = this._createActionsMap(data.actions);
}
setAction(cTrigger, cScript) {
@ -91,20 +82,45 @@ class Field extends PDFObject {
if (!(cTrigger in this._actions)) {
this._actions[cTrigger] = [];
}
this._actions[cTrigger].push(cScript);
this._actions[cTrigger].push(
// eslint-disable-next-line no-new-func
Function("event", `with (this) {${cScript}}`).bind(this._document)
);
}
setFocus() {
this._send({ id: this._id, focus: true });
}
_createActionsMap(actions) {
const actionsMap = new Map();
if (actions) {
const doc = this._document;
for (const [eventType, actionsForEvent] of Object.entries(actions)) {
// This stuff is running in a sandbox so it's safe to use Function
actionsMap.set(
eventType,
actionsForEvent.map(action =>
// eslint-disable-next-line no-new-func
Function("event", `with (this) {${action}}`).bind(doc)
)
);
}
}
return actionsMap;
}
_isButton() {
return false;
}
_runActions(event) {
const eventName = event.name;
if (!(eventName in this._actions)) {
if (!this._actions.has(eventName)) {
return false;
}
const actions = this._actions[eventName];
const actions = this._actions.get(eventName);
try {
for (const action of actions) {
action(event);
@ -115,7 +131,7 @@ class Field extends PDFObject {
`"${error.toString()}" for event ` +
`"${eventName}" in object ${this._id}.` +
`\n${error.stack}`;
this._send({ command: "error", value });
this._send({ id: "error", value });
}
return true;

View File

@ -22,7 +22,7 @@ import { ProxyHandler } from "./proxy.js";
import { Util } from "./util.js";
import { ZoomType } from "./constants.js";
function initSandbox(data, extra, out) {
function initSandbox({ data, extra, out, testMode = false }) {
const proxyHandler = new ProxyHandler(data.dispatchEventName);
const { send, crackURL } = extra;
const doc = new Doc({
@ -58,6 +58,14 @@ function initSandbox(data, extra, out) {
out[name] = aform[name].bind(aform);
}
}
if (
(typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || TESTING")) &&
testMode
) {
out._app = app;
}
}
export { initSandbox };

View File

@ -15,83 +15,255 @@
import { initSandbox } from "../../src/scripting_api/initialization.js";
describe("Util", function () {
let sandbox, util;
describe("Scripting", function () {
describe("Util", function () {
let sandbox, util;
beforeAll(function (done) {
sandbox = Object.create(null);
const extra = { send: null, crackURL: null };
const data = { objects: {}, calculationOrder: [] };
initSandbox(data, extra, sandbox);
util = sandbox.util;
done();
});
afterAll(function () {
sandbox = util = null;
});
describe("printd", function () {
it("should print a date according to a format", function (done) {
const date = new Date("April 15, 1707 3:14:15");
expect(util.printd(0, date)).toEqual("D:17070415031415");
expect(util.printd(1, date)).toEqual("1707.04.15 03:14:15");
expect(util.printd(2, date)).toEqual("4/15/07 3:14:15 am");
expect(util.printd("mmmm mmm mm m", date)).toEqual("April Apr 04 4");
expect(util.printd("dddd ddd dd d", date)).toEqual("Friday Fri 15 15");
done();
});
});
describe("scand", function () {
it("should parse a date according to a format", function (done) {
const date = new Date("April 15, 1707 3:14:15");
expect(util.scand(0, "D:17070415031415")).toEqual(date);
expect(util.scand(1, "1707.04.15 03:14:15")).toEqual(date);
expect(util.scand(2, "4/15/07 3:14:15 am")).toEqual(
new Date("April 15, 2007 3:14:15")
);
done();
});
});
describe("printf", function () {
it("should print some data according to a format", function (done) {
expect(util.printf("Integer numbers: %d, %d,...", 1.234, 56.789)).toEqual(
"Integer numbers: 1, 56,..."
);
expect(util.printf("Hex numbers: %x, %x,...", 1234, 56789)).toEqual(
"Hex numbers: 4D2, DDD5,..."
);
expect(
util.printf("Hex numbers with 0x: %#x, %#x,...", 1234, 56789)
).toEqual("Hex numbers with 0x: 0x4D2, 0xDDD5,...");
expect(util.printf("Decimal number: %,0+.3f", 1234567.89123)).toEqual(
"Decimal number: +1,234,567.891"
);
expect(util.printf("Decimal number: %,0+8.3f", 1.234567)).toEqual(
"Decimal number: + 1.235"
);
beforeAll(function (done) {
sandbox = Object.create(null);
const extra = { send: null, crackURL: null };
const data = { objects: {}, calculationOrder: [] };
initSandbox({ data, extra, out: sandbox });
util = sandbox.util;
done();
});
it("should print a string with no argument", function (done) {
expect(util.printf("hello world")).toEqual("hello world");
done();
afterAll(function () {
sandbox = util = null;
});
it("should print a string with a percent", function (done) {
expect(util.printf("%%s")).toEqual("%%s");
expect(util.printf("%%s", "hello")).toEqual("%%s");
done();
describe("printd", function () {
it("should print a date according to a format", function (done) {
const date = new Date("April 15, 1707 3:14:15");
expect(util.printd(0, date)).toEqual("D:17070415031415");
expect(util.printd(1, date)).toEqual("1707.04.15 03:14:15");
expect(util.printd(2, date)).toEqual("4/15/07 3:14:15 am");
expect(util.printd("mmmm mmm mm m", date)).toEqual("April Apr 04 4");
expect(util.printd("dddd ddd dd d", date)).toEqual("Friday Fri 15 15");
done();
});
});
describe("scand", function () {
it("should parse a date according to a format", function (done) {
const date = new Date("April 15, 1707 3:14:15");
expect(util.scand(0, "D:17070415031415")).toEqual(date);
expect(util.scand(1, "1707.04.15 03:14:15")).toEqual(date);
expect(util.scand(2, "4/15/07 3:14:15 am")).toEqual(
new Date("April 15, 2007 3:14:15")
);
done();
});
});
describe("printf", function () {
it("should print some data according to a format", function (done) {
expect(
util.printf("Integer numbers: %d, %d,...", 1.234, 56.789)
).toEqual("Integer numbers: 1, 56,...");
expect(util.printf("Hex numbers: %x, %x,...", 1234, 56789)).toEqual(
"Hex numbers: 4D2, DDD5,..."
);
expect(
util.printf("Hex numbers with 0x: %#x, %#x,...", 1234, 56789)
).toEqual("Hex numbers with 0x: 0x4D2, 0xDDD5,...");
expect(util.printf("Decimal number: %,0+.3f", 1234567.89123)).toEqual(
"Decimal number: +1,234,567.891"
);
expect(util.printf("Decimal number: %,0+8.3f", 1.234567)).toEqual(
"Decimal number: + 1.235"
);
done();
});
it("should print a string with no argument", function (done) {
expect(util.printf("hello world")).toEqual("hello world");
done();
});
it("should print a string with a percent", function (done) {
expect(util.printf("%%s")).toEqual("%%s");
expect(util.printf("%%s", "hello")).toEqual("%%s");
done();
});
});
describe("printx", function () {
it("should print some data according to a format", function (done) {
expect(util.printx("9 (999) 999-9999", "aaa14159697489zzz")).toEqual(
"1 (415) 969-7489"
);
done();
});
});
});
describe("printx", function () {
it("should print some data according to a format", function (done) {
expect(util.printx("9 (999) 999-9999", "aaa14159697489zzz")).toEqual(
"1 (415) 969-7489"
);
describe("Events", function () {
let sandbox, send_queue, _app;
beforeEach(function (done) {
send_queue = [];
sandbox = Object.create(null);
const extra = {
send(data) {
send_queue.push(data);
},
crackURL: null,
};
const data = {
objects: {
field314R: [
{
id: "314R",
value: "",
actions: {},
type: "text",
},
],
field271R: [
{
id: "271R",
value: "",
actions: {},
type: "text",
},
],
},
calculationOrder: ["271R"],
dispatchEventName: "_dispatchMe",
};
initSandbox({
data,
extra,
out: sandbox,
testMode: true,
});
_app = sandbox._app;
send_queue = [];
done();
});
afterAll(function () {
sandbox = send_queue = _app = null;
});
it("should trigger an event and modify the source", function (done) {
_app._objects["314R"].obj._actions.set("test", [
event => {
event.source.value = "123";
},
]);
sandbox.app._dispatchMe({
id: "314R",
value: "",
name: "test",
willCommit: true,
});
expect(send_queue.length).toEqual(1);
expect(send_queue[0]).toEqual({ id: "314R", value: "123" });
done();
});
it("should trigger a Keystroke event and invalidate it", function (done) {
_app._objects["314R"].obj._actions.set("Keystroke", [
event => {
event.rc = false;
},
]);
sandbox.app._dispatchMe({
id: "314R",
value: "hell",
name: "Keystroke",
willCommit: false,
change: "o",
selStart: 4,
selEnd: 4,
});
expect(send_queue.length).toEqual(1);
expect(send_queue[0]).toEqual({
id: "314R",
value: "hell",
selRange: [4, 4],
});
done();
});
it("should trigger a Keystroke event and change it", function (done) {
_app._objects["314R"].obj._actions.set("Keystroke", [
event => {
event.change = "a";
},
]);
sandbox.app._dispatchMe({
id: "314R",
value: "hell",
name: "Keystroke",
willCommit: false,
change: "o",
selStart: 4,
selEnd: 4,
});
expect(send_queue.length).toEqual(1);
expect(send_queue[0]).toEqual({ id: "314R", value: "hella" });
done();
});
it("should trigger an invalid commit Keystroke event", function (done) {
_app._objects["314R"].obj._actions.set("Validate", [
event => {
event.rc = false;
},
]);
sandbox.app._dispatchMe({
id: "314R",
value: "hello",
name: "Keystroke",
willCommit: true,
});
expect(send_queue.length).toEqual(0);
done();
});
it("should trigger a valid commit Keystroke event", function (done) {
let output = "";
_app._objects["314R"].obj._actions.set("Validate", [
event => {
event.value = "world";
output += "foo";
},
]);
_app._objects["271R"].obj._actions.set("Calculate", [
event => {
event.value = "hello";
output += "bar";
},
]);
sandbox.app._dispatchMe({
id: "314R",
value: "hello",
name: "Keystroke",
willCommit: true,
});
expect(send_queue.length).toEqual(4);
expect(send_queue[0]).toEqual({ id: "314R", value: "world" });
expect(send_queue[1]).toEqual({ id: "271R", value: "hello" });
expect(send_queue[2]).toEqual({ id: "271R", valueAsString: "hello" });
expect(send_queue[3]).toEqual({ id: "314R", valueAsString: "world" });
expect(output).toEqual("foobar");
done();
});
});

View File

@ -1369,6 +1369,7 @@ const PDFViewerApplication = {
if (!objects || !AppOptions.get("enableScripting")) {
return;
}
const calculationOrder = await pdfDocument.getCalculationOrderIds();
const scripting = this.externalServices.scripting;
const {
info,
@ -1431,7 +1432,6 @@ const PDFViewerApplication = {
});
const dispatchEventName = generateRandomStringForSandbox(objects);
const calculationOrder = [];
const { length } = await pdfDocument.getDownloadInfo();
const filename =
contentDispositionFilename || getPDFFileNameFromURL(this.url);