Skip to content

Commit

Permalink
Add the possibility to collect Javascript actions
Browse files Browse the repository at this point in the history
  • Loading branch information
calixteman committed Oct 13, 2020
1 parent fd1d9cc commit dea74f0
Show file tree
Hide file tree
Showing 7 changed files with 359 additions and 2 deletions.
155 changes: 154 additions & 1 deletion src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
*/

import {
AnnotationActionEventType,
AnnotationBorderStyleType,
AnnotationFieldFlag,
AnnotationFlag,
AnnotationReplyType,
AnnotationType,
assert,
bytesToString,
escapeString,
getModificationDate,
isString,
Expand All @@ -30,7 +32,15 @@ import {
warn,
} from "../shared/util.js";
import { Catalog, FileSpec, ObjectLoader } from "./obj.js";
import { Dict, isDict, isName, isRef, isStream, Name } from "./primitives.js";
import {
Dict,
isDict,
isName,
isRef,
isStream,
Name,
RefSet,
} from "./primitives.js";
import { ColorSpace } from "./colorspace.js";
import { getInheritableProperty } from "./core_utils.js";
import { OperatorList } from "./operator_list.js";
Expand Down Expand Up @@ -569,6 +579,19 @@ class Annotation {
return null;
}

/**
* Get field data for usage in JS sandbox.
*
* Field object is defined here:
* https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/js_api_reference.pdf#page=16
*
* @public
* @memberof Annotation
*/
getFieldObject() {
return null;
}

/**
* Reset the annotation.
*
Expand Down Expand Up @@ -903,6 +926,7 @@ class WidgetAnnotation extends Annotation {

data.annotationType = AnnotationType.WIDGET;
data.fieldName = this._constructFieldName(dict);
data.actions = this._collectActions(params.xref, dict);

const fieldValue = getInheritableProperty({
dict,
Expand Down Expand Up @@ -937,13 +961,15 @@ class WidgetAnnotation extends Annotation {
}

data.readOnly = this.hasFieldFlag(AnnotationFieldFlag.READONLY);
data.hidden = this.hasFieldFlag(AnnotationFieldFlag.HIDDEN);

// Hide signatures because we cannot validate them, and unset the fieldValue
// since it's (most likely) a `Dict` which is non-serializable and will thus
// cause errors when sending annotations to the main-thread (issue 10347).
if (data.fieldType === "Sig") {
data.fieldValue = null;
this.setFlags(AnnotationFlag.HIDDEN);
data.hidden = true;
}
}

Expand Down Expand Up @@ -1366,6 +1392,77 @@ class WidgetAnnotation extends Annotation {
}
return localResources || Dict.empty;
}

_collectJS(entry, xref, list, parents) {
if (!entry) {
return;
}

let parent = null;
if (isRef(entry)) {
if (parents.has(entry)) {
// If we've already found entry then we've a cycle.
return;
}
parent = entry;
parents.put(parent);
entry = xref.fetch(entry);
}
if (Array.isArray(entry)) {
for (const element of entry) {
this._collectJS(element, xref, list, parents);
}
} else if (entry instanceof Dict) {
if (isName(entry.get("S"), "JavaScript") && entry.has("JS")) {
const js = entry.get("JS");
let code;
if (isStream(js)) {
code = bytesToString(js.getBytes());
} else {
code = js;
}
code = stringToPDFString(code);
if (code) {
list.push(code);
}
}
this._collectJS(entry.getRaw("Next"), xref, list, parents);
}

if (parent) {
parents.remove(parent);
}
}

_collectActions(xref, dict) {
const actions = Object.create(null);
if (dict.has("AA")) {
const additionalAction = dict.get("AA");
for (const key of additionalAction.getKeys()) {
if (key in AnnotationActionEventType) {
const actionDict = additionalAction.getRaw(key);
const parents = new RefSet();
const list = [];
this._collectJS(actionDict, xref, list, parents);
if (list.length > 0) {
actions[AnnotationActionEventType[key]] = list;
}
}
}
}
return actions;
}

getFieldObject() {
if (this.data.fieldType === "Sig") {
return {
id: this.data.id,
value: null,
type: "signature",
};
}
return null;
}
}

class TextWidgetAnnotation extends WidgetAnnotation {
Expand Down Expand Up @@ -1516,6 +1613,23 @@ class TextWidgetAnnotation extends WidgetAnnotation {

return chunks;
}

getFieldObject() {
return {
id: this.data.id,
value: this.data.fieldValue,
multiline: this.data.multiline,
password: this.hasFieldFlag(AnnotationFieldFlag.PASSWORD),
charLimit: this.data.maxLen,
comb: this.data.comb,
editable: !this.data.readOnly,
hidden: this.data.hidden,
name: this.data.fieldName,
rect: this.data.rect,
actions: this.data.actions,
type: "text",
};
}
}

class ButtonWidgetAnnotation extends WidgetAnnotation {
Expand Down Expand Up @@ -1793,6 +1907,28 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
docBaseUrl: params.pdfManager.docBaseUrl,
});
}

getFieldObject() {
let type = "button";
let value = null;
if (this.data.checkBox) {
type = "checkbox";
value = this.data.fieldValue && this.data.fieldValue !== "Off";
} else if (this.data.radioButton) {
type = "radiobutton";
value = this.data.fieldValue === this.data.buttonValue;
}
return {
id: this.data.id,
value,
editable: !this.data.readOnly,
name: this.data.fieldName,
rect: this.data.rect,
hidden: this.data.hidden,
actions: this.data.actions,
type,
};
}
}

class ChoiceWidgetAnnotation extends WidgetAnnotation {
Expand Down Expand Up @@ -1843,6 +1979,23 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
this.data.multiSelect = this.hasFieldFlag(AnnotationFieldFlag.MULTISELECT);
this._hasText = true;
}

getFieldObject() {
const type = this.data.combo ? "combobox" : "listbox";
const value =
this.data.fieldValue.length > 0 ? this.data.fieldValue[0] : null;
return {
id: this.data.id,
value,
editable: !this.data.readOnly,
name: this.data.fieldName,
rect: this.data.rect,
multipleSelection: this.data.multiSelect,
hidden: this.data.hidden,
actions: this.data.actions,
type,
};
}
}

class TextAnnotation extends MarkupAnnotation {
Expand Down
72 changes: 71 additions & 1 deletion src/core/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ class PDFDocument {
}

get formInfo() {
const formInfo = { hasAcroForm: false, hasXfa: false };
const formInfo = { hasAcroForm: false, hasXfa: false, fields: null };
const acroForm = this.catalog.acroForm;
if (!acroForm) {
return shadow(this, "formInfo", formInfo);
Expand Down Expand Up @@ -736,6 +736,9 @@ class PDFDocument {
const hasOnlyDocumentSignatures =
!!(sigFlags & 0x1) && this._hasOnlyDocumentSignatures(fields);
formInfo.hasAcroForm = hasFields && !hasOnlyDocumentSignatures;
if (formInfo.hasAcroForm) {
formInfo.fields = fields;
}
} catch (ex) {
if (ex instanceof MissingDataException) {
throw ex;
Expand Down Expand Up @@ -935,6 +938,73 @@ class PDFDocument {
? this.catalog.cleanup(manuallyTriggered)
: clearPrimitiveCaches();
}

_collectFieldObjects(name, fieldRef, promises) {
const field = this.xref.fetchIfRef(fieldRef);
if (field.has("T")) {
const partName = stringToPDFString(field.get("T"));
if (name === "") {
name = partName;
} else {
name = `${name}.${partName}`;
}
}

if (!(name in promises)) {
promises.set(name, []);
}
promises.get(name).push(
AnnotationFactory.create(
this.xref,
fieldRef,
this.pdfManager,
this._localIdFactory
)
.then(annotation => annotation.getFieldObject())
.catch(function (reason) {
warn(`_collectFieldObjects: "${reason}".`);
return null;
})
);

if (field.has("Kids")) {
const kids = field.get("Kids");
for (const kid of kids) {
this._collectFieldObjects(name, kid, promises);
}
}
}

get fieldObjects() {
const formInfo = this.formInfo;
if (!formInfo.hasAcroForm) {
return shadow(this, "fieldObjects", Promise.resolve(null));
}

const allFields = Object.create(null);
const fieldPromises = new Map();
for (const fieldRef of formInfo.fields) {
this._collectFieldObjects("", fieldRef, fieldPromises);
}

const allPromises = [];
for (const [name, promises] of fieldPromises.entries()) {
allPromises.push(
Promise.all(promises).then(fields => {
fields = fields.filter(field => field !== null);
if (fields.length > 0) {
allFields[name] = fields;
}
})
);
}

return shadow(
this,
"fieldObjects",
Promise.all(allPromises).then(() => allFields)
);
}
}

export { Page, PDFDocument };
4 changes: 4 additions & 0 deletions src/core/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,10 @@ class WorkerMessageHandler {
});
});

handler.on("GetFieldObjects", function (data) {
return pdfManager.ensureDoc("fieldObjects");
});

handler.on("SaveDocument", function ({
numPages,
annotationStorage,
Expand Down
12 changes: 12 additions & 0 deletions src/display/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,14 @@ class PDFDocumentProxy {
saveDocument(annotationStorage) {
return this._transport.saveDocument(annotationStorage);
}

/**
* @returns {Promise<Array<Object>>} A promise that is resolved with an
* {Array<Object>} containing field data for the JS sandbox.
*/
getFieldObjects() {
return this._transport.getFieldObjects();
}
}

/**
Expand Down Expand Up @@ -2549,6 +2557,10 @@ class WorkerTransport {
});
}

getFieldObjects() {
return this.messageHandler.sendWithPromise("GetFieldObjects", null);
}

getDestinations() {
return this.messageHandler.sendWithPromise("GetDestinations", null);
}
Expand Down
23 changes: 23 additions & 0 deletions src/shared/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,28 @@ const AnnotationBorderStyleType = {
UNDERLINE: 5,
};

const AnnotationActionEventType = {
E: "MouseEnter",
X: "MouseExit",
D: "MouseDown",
U: "MouseUp",
Fo: "Focus",
Bl: "Blur",
PO: "PageOpen",
PC: "PageClosed",
PV: "PageVisible",
PI: "PageInvisible",
K: "Keystroke",
F: "Format",
V: "Validate",
C: "Calculate",
WC: "WillClose",
WS: "WillSave",
DS: "DidSave",
WP: "WillPrint",
DP: "DidPrint",
};

const StreamType = {
UNKNOWN: "UNKNOWN",
FLATE: "FLATE",
Expand Down Expand Up @@ -971,6 +993,7 @@ export {
OPS,
VerbosityLevel,
UNSUPPORTED_FEATURES,
AnnotationActionEventType,
AnnotationBorderStyleType,
AnnotationFieldFlag,
AnnotationFlag,
Expand Down
Loading

0 comments on commit dea74f0

Please sign in to comment.