diff --git a/src/pdf.sandbox.external.js b/src/pdf.sandbox.external.js index 88cec04c1..68bb48a2d 100644 --- a/src/pdf.sandbox.external.js +++ b/src/pdf.sandbox.external.js @@ -81,6 +81,13 @@ class SandboxSupportBase { ) { return; } + + if (callbackId === 0) { + // This callbackId corresponds to the one used for userActivation. + // So here, we cancel the last userActivation. + this.win.clearTimeout(this.timeoutIds.get(callbackId)); + } + const id = this.win.setTimeout(() => { this.timeoutIds.delete(callbackId); this.callSandboxFunction("timeoutCb", { diff --git a/src/scripting_api/app.js b/src/scripting_api/app.js index c63c15180..2b2a14308 100644 --- a/src/scripting_api/app.js +++ b/src/scripting_api/app.js @@ -23,6 +23,7 @@ const VIEWER_TYPE = "PDF.js"; const VIEWER_VARIATION = "Full"; const VIEWER_VERSION = 21.00720099; const FORMS_VERSION = 21.00720099; +const USERACTIVATION_CALLBACKID = 0; class App extends PDFObject { constructor(data) { @@ -45,7 +46,8 @@ class App extends PDFObject { this._eventDispatcher = new EventDispatcher( this._document, data.calculationOrder, - this._objects + this._objects, + data.externalCall ); this._timeoutIds = new WeakMap(); @@ -63,7 +65,7 @@ class App extends PDFObject { } this._timeoutCallbackIds = new Map(); - this._timeoutCallbackId = 0; + this._timeoutCallbackId = USERACTIVATION_CALLBACKID + 1; this._globalEval = data.globalEval; this._externalCall = data.externalCall; } @@ -85,6 +87,11 @@ class App extends PDFObject { } _evalCallback({ callbackId, interval }) { + if (callbackId === USERACTIVATION_CALLBACKID) { + // Special callback id for userActivation stuff. + this._document.obj._userActivation = false; + return; + } const expr = this._timeoutCallbackIds.get(callbackId); if (!interval) { this._unregisterTimeoutCallback(callbackId); @@ -429,6 +436,11 @@ class App extends PDFObject { oDoc = null, oCheckbox = null ) { + if (!this._document.obj._userActivation) { + return 0; + } + this._document.obj._userActivation = false; + if (cMsg && typeof cMsg === "object") { nType = cMsg.nType; cMsg = cMsg.cMsg; @@ -475,8 +487,18 @@ class App extends PDFObject { } execMenuItem(item) { + if (!this._document.obj._userActivation) { + return; + } + this._document.obj._userActivation = false; + switch (item) { case "SaveAs": + if (this._document.obj._disableSaving) { + return; + } + this._send({ command: item }); + break; case "FirstPage": case "LastPage": case "NextPage": @@ -489,6 +511,9 @@ class App extends PDFObject { this._send({ command: "zoom", value: "page-fit" }); break; case "Print": + if (this._document.obj._disablePrinting) { + return; + } this._send({ command: "print" }); break; } @@ -629,4 +654,4 @@ class App extends PDFObject { } } -export { App }; +export { App, USERACTIVATION_CALLBACKID }; diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index adbc71100..a2b0702ee 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -96,6 +96,9 @@ class Doc extends PDFObject { this._actions = createActionsMap(data.actions); this._globalEval = data.globalEval; this._pageActions = new Map(); + this._userActivation = false; + this._disablePrinting = false; + this._disableSaving = false; } _dispatchDocEvent(name) { @@ -108,12 +111,27 @@ class Doc extends PDFObject { "DidPrint", "OpenAction", ]); + // When a pdf has just been opened it doesn't really make sense + // to save it: it's up to the user to decide if they want to do that. + // A pdf can contain an action /FooBar which will trigger a save + // even if there are no WillSave/DidSave (which are themselves triggered + // after a save). + this._disableSaving = true; for (const actionName of this._actions.keys()) { if (!dontRun.has(actionName)) { this._runActions(actionName); } } this._runActions("OpenAction"); + this._disableSaving = false; + } else if (name === "WillPrint") { + this._disablePrinting = true; + this._runActions(name); + this._disablePrinting = false; + } else if (name === "WillSave") { + this._disableSaving = true; + this._runActions(name); + this._disableSaving = false; } else { this._runActions(name); } @@ -361,6 +379,11 @@ class Doc extends PDFObject { } set layout(value) { + if (!this._userActivation) { + return; + } + this._userActivation = false; + if (typeof value !== "string") { return; } @@ -480,6 +503,11 @@ class Doc extends PDFObject { } set pageNum(value) { + if (!this._userActivation) { + return; + } + this._userActivation = false; + if (typeof value !== "number" || value < 0 || value >= this._numPages) { return; } @@ -628,6 +656,11 @@ class Doc extends PDFObject { } set zoomType(type) { + if (!this._userActivation) { + return; + } + this._userActivation = false; + if (typeof type !== "string") { return; } @@ -662,6 +695,11 @@ class Doc extends PDFObject { } set zoom(value) { + if (!this._userActivation) { + return; + } + this._userActivation = false; + if (typeof value !== "number" || value < 8.33 || value > 6400) { return; } @@ -1057,6 +1095,11 @@ class Doc extends PDFObject { bAnnotations = true, printParams = null ) { + if (this._disablePrinting || !this._userActivation) { + return; + } + this._userActivation = false; + if (bUI && typeof bUI === "object") { nStart = bUI.nStart; nEnd = bUI.nEnd; diff --git a/src/scripting_api/event.js b/src/scripting_api/event.js index 5bacaf952..7b0fdecb8 100644 --- a/src/scripting_api/event.js +++ b/src/scripting_api/event.js @@ -13,6 +13,10 @@ * limitations under the License. */ +import { USERACTIVATION_CALLBACKID } from "./doc.js"; + +const USERACTIVATION_MAXTIME_VALIDITY = 5000; + class Event { constructor(data) { this.change = data.change || ""; @@ -39,10 +43,11 @@ class Event { } class EventDispatcher { - constructor(document, calculationOrder, objects) { + constructor(document, calculationOrder, objects, externalCall) { this._document = document; this._calculationOrder = calculationOrder; this._objects = objects; + this._externalCall = externalCall; this._document.obj._eventDispatcher = this; this._isCalculating = false; @@ -66,6 +71,14 @@ class EventDispatcher { return `${prefix}${event.change}${postfix}`; } + userActivation() { + this._document.obj._userActivation = true; + this._externalCall("setTimeout", [ + USERACTIVATION_CALLBACKID, + USERACTIVATION_MAXTIME_VALIDITY, + ]); + } + dispatch(baseEvent) { const id = baseEvent.id; if (!(id in this._objects)) { @@ -76,19 +89,27 @@ class EventDispatcher { event.name = baseEvent.name; } if (id === "doc") { - if (event.name === "Open") { + const eventName = event.name; + if (eventName === "Open") { // Before running the Open event, we format all the fields // (see bug 1766987). this.formatAll(); } + if ( + !["DidPrint", "DidSave", "WillPrint", "WillSave"].includes(eventName) + ) { + this.userActivation(); + } this._document.obj._dispatchDocEvent(event.name); } else if (id === "page") { + this.userActivation(); this._document.obj._dispatchPageEvent( event.name, baseEvent.actions, baseEvent.pageNumber ); } else if (id === "app" && baseEvent.name === "ResetForm") { + this.userActivation(); for (const fieldId of baseEvent.ids) { const obj = this._objects[fieldId]; obj?.obj._reset(); @@ -102,6 +123,8 @@ class EventDispatcher { const event = (globalThis.event = new Event(baseEvent)); let savedChange; + this.userActivation(); + if (source.obj._isButton()) { source.obj._id = id; event.value = source.obj._getExportValue(event.value);