1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-22 16:18:08 +02:00

Merge pull request #12432 from calixteman/scripting_api

JS - Add the basic architecture to be able to execute embedded js
This commit is contained in:
Brendan Dahl 2020-10-22 19:57:58 -07:00 committed by GitHub
commit 1eaf9c961b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 823 additions and 0 deletions

View file

@ -468,6 +468,8 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
element.setAttribute("value", textContent);
}
element.setAttribute("id", id);
element.addEventListener("input", function (event) {
storage.setValue(id, event.target.value);
});
@ -476,6 +478,35 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
event.target.setSelectionRange(0, 0);
});
if (this.data.actions) {
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 });
}
});
for (const eventType of Object.keys(this.data.actions)) {
switch (eventType) {
case "Format":
element.addEventListener("blur", function (event) {
window.dispatchEvent(
new CustomEvent("dispatchEventInSandbox", {
detail: {
id,
name: "Format",
value: event.target.value,
},
})
);
});
break;
}
}
}
element.disabled = this.data.readOnly;
element.name = this.data.fieldName;

View file

@ -0,0 +1,46 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class AForm {
constructor(document, app, util) {
this._document = document;
this._app = app;
this._util = util;
}
AFNumber_Format(
nDec,
sepStyle,
negStyle,
currStyle,
strCurrency,
bCurrencyPrepend
) {
const event = this._document._event;
if (!event.value) {
return;
}
nDec = Math.abs(nDec);
const value = event.value.trim().replace(",", ".");
let number = Number.parseFloat(value);
if (isNaN(number) || !isFinite(number)) {
number = 0;
}
event.value = number.toFixed(nDec);
}
}
export { AForm };

61
src/scripting_api/app.js Normal file
View file

@ -0,0 +1,61 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventDispatcher } from "./event.js";
import { NotSupportedError } from "./error.js";
import { PDFObject } from "./pdf_object.js";
class App extends PDFObject {
constructor(data) {
super(data);
this._document = data._document;
this._objects = Object.create(null);
this._eventDispatcher = new EventDispatcher(
this._document,
data.calculationOrder,
this._objects
);
// used in proxy.js to check that this the object with the backdoor
this._isApp = true;
}
// This function is called thanks to the proxy
// when we call app['random_string'] to dispatch the event.
_dispatchEvent(pdfEvent) {
this._eventDispatcher.dispatch(pdfEvent);
}
get activeDocs() {
return [this._document.wrapped];
}
set activeDocs(_) {
throw new NotSupportedError("app.activeDocs");
}
alert(
cMsg,
nIcon = 0,
nType = 0,
cTitle = "PDF.js",
oDoc = null,
oCheckbox = null
) {
this._send({ command: "alert", value: cMsg });
}
}
export { App };

View file

@ -0,0 +1,38 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PDFObject } from "./pdf_object.js";
class Console extends PDFObject {
clear() {
this._send({ id: "clear" });
}
hide() {
/* Not implemented */
}
println(msg) {
if (typeof msg === "string") {
this._send({ command: "println", value: "PDF.js Console:: " + msg });
}
}
show() {
/* Not implemented */
}
}
export { Console };

48
src/scripting_api/doc.js Normal file
View file

@ -0,0 +1,48 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PDFObject } from "./pdf_object.js";
class Doc extends PDFObject {
constructor(data) {
super(data);
this._printParams = null;
this._fields = Object.create(null);
this._event = null;
}
calculateNow() {
this._eventDispatcher.calculateNow();
}
getField(cName) {
if (typeof cName !== "string") {
throw new TypeError("Invalid field name: must be a string");
}
if (cName in this._fields) {
return this._fields[cName];
}
for (const [name, field] of Object.entries(this._fields)) {
if (name.includes(cName)) {
return field;
}
}
return undefined;
}
}
export { Doc };

View file

@ -0,0 +1,23 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class NotSupportedError extends Error {
constructor(name) {
super(`${name} isn't supported in PDF.js`);
this.name = "NotSupportedError";
}
}
export { NotSupportedError };

View file

@ -0,0 +1,79 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class Event {
constructor(data) {
this.change = data.change || "";
this.changeEx = data.changeEx || null;
this.commitKey = data.commitKey || 0;
this.fieldFull = data.fieldFull || false;
this.keyDown = data.keyDown || false;
this.modifier = data.modifier || false;
this.name = data.name;
this.rc = true;
this.richChange = data.richChange || [];
this.richChangeEx = data.richChangeEx || [];
this.richValue = data.richValue || [];
this.selEnd = data.selEnd || 0;
this.selStart = data.selStart || 0;
this.shift = data.shift || false;
this.source = data.source || null;
this.target = data.target || null;
this.targetName = data.targetName || "";
this.type = "Field";
this.value = data.value || null;
this.willCommit = data.willCommit || false;
}
}
class EventDispatcher {
constructor(document, calculationOrder, objects) {
this._document = document;
this._calculationOrder = calculationOrder;
this._objects = objects;
this._document.obj._eventDispatcher = this;
}
dispatch(baseEvent) {
const id = baseEvent.id;
if (!(id in this._objects)) {
return;
}
const name = baseEvent.name.replace(" ", "");
const source = this._objects[id];
const event = (this._document.obj._event = new Event(baseEvent));
const oldValue = source.obj.value;
this.runActions(source, source, event, name);
if (event.rc && oldValue !== event.value) {
source.wrapped.value = event.value;
}
}
runActions(source, target, event, eventName) {
event.source = source.wrapped;
event.target = target.wrapped;
event.name = eventName;
event.rc = true;
if (!target.obj._runActions(event)) {
return true;
}
return event.rc;
}
}
export { Event, EventDispatcher };

123
src/scripting_api/field.js Normal file
View file

@ -0,0 +1,123 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PDFObject } from "./pdf_object.js";
class Field extends PDFObject {
constructor(data) {
super(data);
this.alignment = data.alignment || "left";
this.borderStyle = data.borderStyle || "";
this.buttonAlignX = data.buttonAlignX || 50;
this.buttonAlignY = data.buttonAlignY || 50;
this.buttonFitBounds = data.buttonFitBounds;
this.buttonPosition = data.buttonPosition;
this.buttonScaleHow = data.buttonScaleHow;
this.ButtonScaleWhen = data.buttonScaleWhen;
this.calcOrderIndex = data.calcOrderIndex;
this.charLimit = data.charLimit;
this.comb = data.comb;
this.commitOnSelChange = data.commitOnSelChange;
this.currentValueIndices = data.currentValueIndices;
this.defaultStyle = data.defaultStyle;
this.defaultValue = data.defaultValue;
this.doNotScroll = data.doNotScroll;
this.doNotSpellCheck = data.doNotSpellCheck;
this.delay = data.delay;
this.display = data.display;
this.doc = data.doc;
this.editable = data.editable;
this.exportValues = data.exportValues;
this.fileSelect = data.fileSelect;
this.fillColor = data.fillColor;
this.hidden = data.hidden;
this.highlight = data.highlight;
this.lineWidth = data.lineWidth;
this.multiline = data.multiline;
this.multipleSelection = data.multipleSelection;
this.name = data.name;
this.numItems = data.numItems;
this.page = data.page;
this.password = data.password;
this.print = data.print;
this.radiosInUnison = data.radiosInUnison;
this.readonly = data.readonly;
this.rect = data.rect;
this.required = data.required;
this.richText = data.richText;
this.richValue = data.richValue;
this.rotation = data.rotation;
this.strokeColor = data.strokeColor;
this.style = data.style;
this.submitName = data.submitName;
this.textColor = data.textColor;
this.textFont = data.textFont;
this.textSize = data.textSize;
this.type = data.type;
this.userName = data.userName;
this.value = data.value || "";
this.valueAsString = data.valueAsString;
// Private
this._actions = Object.create(null);
const doc = (this._document = data.doc);
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)
);
}
}
setAction(cTrigger, cScript) {
if (typeof cTrigger !== "string" || typeof cScript !== "string") {
return;
}
if (!(cTrigger in this._actions)) {
this._actions[cTrigger] = [];
}
this._actions[cTrigger].push(cScript);
}
setFocus() {
this._send({ id: this._id, focus: true });
}
_runActions(event) {
const eventName = event.name;
if (!(eventName in this._actions)) {
return false;
}
const actions = this._actions[eventName];
try {
for (const action of actions) {
action(event);
}
} catch (error) {
event.rc = false;
const value =
`\"${error.toString()}\" for event ` +
`\"${eventName}\" in object ${this._id}.` +
`\n${error.stack}`;
this._send({ command: "error", value });
}
return true;
}
}
export { Field };

View file

@ -0,0 +1,57 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AForm } from "./aform.js";
import { App } from "./app.js";
import { Console } from "./console.js";
import { Doc } from "./doc.js";
import { Field } from "./field.js";
import { ProxyHandler } from "./proxy.js";
import { Util } from "./util.js";
function initSandbox(data, extra, out) {
const proxyHandler = new ProxyHandler(data.dispatchEventName);
const { send, crackURL } = extra;
const doc = new Doc({ send });
const _document = { obj: doc, wrapped: new Proxy(doc, proxyHandler) };
const app = new App({
send,
_document,
calculationOrder: data.calculationOrder,
});
const util = new Util({ crackURL });
const aform = new AForm(doc, app, util);
for (const [name, objs] of Object.entries(data.objects)) {
const obj = objs[0];
obj.send = send;
obj.doc = _document.wrapped;
const field = new Field(obj);
const wrapped = (doc._fields[name] = new Proxy(field, proxyHandler));
app._objects[obj.id] = { obj: field, wrapped };
}
out.global = Object.create(null);
out.app = new Proxy(app, proxyHandler);
out.console = new Proxy(new Console({ send }), proxyHandler);
out.util = new Proxy(util, proxyHandler);
for (const name of Object.getOwnPropertyNames(AForm.prototype)) {
if (name.startsWith("AF")) {
out[name] = aform[name].bind(aform);
}
}
}
export { initSandbox };

View file

@ -0,0 +1,24 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class PDFObject {
constructor(data) {
this._expandos = Object.create(null);
this._send = data.send || null;
this._id = data.id || null;
}
}
export { PDFObject };

125
src/scripting_api/proxy.js Normal file
View file

@ -0,0 +1,125 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class ProxyHandler {
constructor(dispatchEventName) {
this.dispatchEventName = dispatchEventName;
}
get(obj, prop) {
if (obj._isApp && prop === this.dispatchEventName) {
// a backdoor to be able to call _dispatchEvent method
// the value of 'dispatchEvent' is generated randomly
// and injected in the code
return obj._dispatchEvent.bind(obj);
}
// script may add some properties to the object
if (prop in obj._expandos) {
const val = obj._expandos[prop];
if (typeof val === "function") {
return val.bind(obj);
}
return val;
}
if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) {
// return only public properties
// i.e. the ones not starting with a '_'
const val = obj[prop];
if (typeof val === "function") {
return val.bind(obj);
}
return val;
}
return undefined;
}
set(obj, prop, value) {
if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) {
const old = obj[prop];
obj[prop] = value;
if (obj._send && obj._id !== null && typeof old !== "function") {
const data = { id: obj._id };
data[prop] = value;
// send the updated value to the other side
obj._send(data);
}
} else {
obj._expandos[prop] = value;
}
return true;
}
has(obj, prop) {
return (
prop in obj._expandos ||
(typeof prop === "string" && !prop.startsWith("_") && prop in obj)
);
}
getPrototypeOf(obj) {
return null;
}
setPrototypeOf(obj, proto) {
return false;
}
isExtensible(obj) {
return true;
}
preventExtensions(obj) {
return false;
}
getOwnPropertyDescriptor(obj, prop) {
if (prop in obj._expandos) {
return {
configurable: true,
enumerable: true,
value: obj._expandos[prop],
};
}
if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) {
return { configurable: true, enumerable: true, value: obj[prop] };
}
return undefined;
}
defineProperty(obj, key, descriptor) {
Object.defineProperty(obj._expandos, key, descriptor);
return true;
}
deleteProperty(obj, prop) {
if (prop in obj._expandos) {
delete obj._expandos[prop];
}
}
ownKeys(obj) {
const fromExpandos = Reflect.ownKeys(obj._expandos);
const fromObj = Reflect.ownKeys(obj).filter(k => !k.startsWith("_"));
return fromExpandos.concat(fromObj);
}
}
export { ProxyHandler };

30
src/scripting_api/util.js Normal file
View file

@ -0,0 +1,30 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PDFObject } from "./pdf_object.js";
class Util extends PDFObject {
constructor(data) {
super(data);
this._crackURL = data.crackURL;
}
crackURL(cURL) {
return this._crackURL(cURL);
}
}
export { Util };