JS -- Add a sandbox based on quickjs

* quickjs-eval.js has been generated using https://github.com/mozilla/pdf.js.quickjs/
 * lazy load of sandbox code
 * Rewrite tests to use the sandbox
 * Add a task `watch-sandbox` which update bundle pdf.sandbox.js on change in the sandbox code
This commit is contained in:
Calixte Denizet 2020-11-09 14:45:02 +01:00
parent d3936ac9d2
commit c7974e9996
15 changed files with 886 additions and 245 deletions

View File

@ -7,6 +7,7 @@ external/webL10n/
external/cmapscompress/
external/builder/fixtures/
external/builder/fixtures_esprima/
external/quickjs/quickjs-eval.js
src/shared/cffStandardStrings.js
src/shared/fonts_utils.js
test/tmp/

42
external/quickjs/quickjs-eval.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -33,6 +33,7 @@ var stream = require("stream");
var exec = require("child_process").exec;
var spawn = require("child_process").spawn;
var spawnSync = require("child_process").spawnSync;
var stripComments = require("gulp-strip-comments");
var streamqueue = require("streamqueue");
var merge = require("merge-stream");
var zip = require("gulp-zip");
@ -105,6 +106,7 @@ const DEFINES = Object.freeze({
COMPONENTS: false,
LIB: false,
IMAGE_DECODERS: false,
NO_SOURCE_MAP: false,
});
function transform(charEncoding, transformFunction) {
@ -182,7 +184,8 @@ function createWebpackConfig(defines, output) {
var enableSourceMaps =
!bundleDefines.MOZCENTRAL &&
!bundleDefines.CHROME &&
!bundleDefines.TESTING;
!bundleDefines.TESTING &&
!bundleDefines.NO_SOURCE_MAP;
var skipBabel = bundleDefines.SKIP_BABEL;
// `core-js` (see https://github.com/zloirock/core-js/issues/514),
@ -343,6 +346,53 @@ function createScriptingBundle(defines) {
.pipe(replaceJSRootName(scriptingAMDName, "pdfjsScripting"));
}
function createSandboxBundle(defines, code) {
var sandboxAMDName = "pdfjs-dist/build/pdf.sandbox";
var sandboxOutputName = "pdf.sandbox.js";
var sandboxFileConfig = createWebpackConfig(defines, {
filename: sandboxOutputName,
library: sandboxAMDName,
libraryTarget: "umd",
umdNamedDefine: true,
});
// The code is the one from the bundle pdf.scripting.js
// so in order to have it in a string (which will be eval-ed
// in the sandbox) we must escape some chars.
// This way we've all the code (initialization+sandbox) in
// the same bundle.
code = code.replace(/["\\\n\t]/g, match => {
if (match === "\n") {
return "\\n";
}
if (match === "\t") {
return "\\t";
}
return `\\${match}`;
});
return (
gulp
.src("./src/scripting_api/quickjs-sandbox.js")
.pipe(webpack2Stream(sandboxFileConfig))
.pipe(replaceWebpackRequire())
.pipe(replaceJSRootName(sandboxAMDName, "pdfjsSandbox"))
// put the code in a string to be eval-ed in the sandbox
.pipe(replace("/* INITIALIZATION_CODE */", `${code}`))
);
}
function buildSandbox(defines, dir) {
const scriptingDefines = builder.merge(defines, { NO_SOURCE_MAP: true });
return createScriptingBundle(scriptingDefines)
.pipe(stripComments())
.pipe(gulp.dest(dir + "build"))
.on("data", file => {
const content = file.contents.toString();
createSandboxBundle(defines, content).pipe(gulp.dest(dir + "build"));
fs.unlinkSync(dir + "build/pdf.scripting.js");
});
}
function createWorkerBundle(defines) {
var workerAMDName = "pdfjs-dist/build/pdf.worker";
var workerOutputName = "pdf.worker.js";
@ -494,6 +544,25 @@ function makeRef(done, bot) {
});
}
gulp.task("sandbox", function (done) {
const defines = builder.merge(DEFINES, { GENERIC: true });
buildSandbox(defines, GENERIC_DIR);
done();
});
gulp.task("watch-sandbox", function (done) {
const defines = builder.merge(DEFINES, { GENERIC: true });
buildSandbox(defines, GENERIC_DIR);
const watcher = gulp.watch([
"src/scripting_api/*.js",
"external/quickjs/*.js",
]);
watcher.on("change", function () {
buildSandbox(defines, GENERIC_DIR);
});
done();
});
gulp.task("default", function (done) {
console.log("Available tasks:");
var tasks = Object.keys(gulp.registry().tasks());
@ -762,26 +831,47 @@ function buildGeneric(defines, dir) {
// HTML5 browsers, which implement modern ECMAScript features.
gulp.task(
"generic",
gulp.series("buildnumber", "default_preferences", "locale", function () {
console.log();
console.log("### Creating generic viewer");
var defines = builder.merge(DEFINES, { GENERIC: true });
gulp.series(
"buildnumber",
"default_preferences",
"locale",
function () {
console.log();
console.log("### Creating generic viewer");
var defines = builder.merge(DEFINES, { GENERIC: true });
return buildGeneric(defines, GENERIC_DIR);
})
return buildGeneric(defines, GENERIC_DIR);
},
"sandbox"
)
);
// Builds the generic production viewer that should be compatible with most
// older HTML5 browsers.
gulp.task(
"generic-es5",
gulp.series("buildnumber", "default_preferences", "locale", function () {
console.log();
console.log("### Creating generic (ES5) viewer");
var defines = builder.merge(DEFINES, { GENERIC: true, SKIP_BABEL: false });
gulp.series(
"buildnumber",
"default_preferences",
"locale",
function () {
console.log();
console.log("### Creating generic (ES5) viewer");
var defines = builder.merge(DEFINES, {
GENERIC: true,
SKIP_BABEL: false,
});
return buildGeneric(defines, GENERIC_ES5_DIR);
})
return buildGeneric(defines, GENERIC_ES5_DIR);
},
function () {
const defines = builder.merge(DEFINES, {
GENERIC: true,
SKIP_BABEL: false,
});
return buildSandbox(defines, GENERIC_ES5_DIR);
}
)
);
function buildComponents(defines, dir) {
@ -908,33 +998,61 @@ function buildMinified(defines, dir) {
gulp.task(
"minified-pre",
gulp.series("buildnumber", "default_preferences", "locale", function () {
console.log();
console.log("### Creating minified viewer");
var defines = builder.merge(DEFINES, { MINIFIED: true, GENERIC: true });
gulp.series(
"buildnumber",
"default_preferences",
"locale",
function () {
console.log();
console.log("### Creating minified viewer");
var defines = builder.merge(DEFINES, { MINIFIED: true, GENERIC: true });
return buildMinified(defines, MINIFIED_DIR);
})
return buildSandbox(defines, MINIFIED_DIR);
},
function () {
var defines = builder.merge(DEFINES, { MINIFIED: true, GENERIC: true });
return buildMinified(defines, MINIFIED_DIR);
}
)
);
gulp.task(
"minified-es5-pre",
gulp.series("buildnumber", "default_preferences", "locale", function () {
console.log();
console.log("### Creating minified (ES5) viewer");
var defines = builder.merge(DEFINES, {
MINIFIED: true,
GENERIC: true,
SKIP_BABEL: false,
});
gulp.series(
"buildnumber",
"default_preferences",
"locale",
function () {
console.log();
console.log("### Creating minified (ES5) viewer");
var defines = builder.merge(DEFINES, {
MINIFIED: true,
GENERIC: true,
SKIP_BABEL: false,
});
return buildMinified(defines, MINIFIED_ES5_DIR);
})
return buildSandbox(defines, MINIFIED_ES5_DIR);
},
function () {
var defines = builder.merge(DEFINES, {
MINIFIED: true,
GENERIC: true,
SKIP_BABEL: false,
});
return buildMinified(defines, MINIFIED_ES5_DIR);
}
)
);
async function parseMinified(dir) {
var pdfFile = fs.readFileSync(dir + "/build/pdf.js").toString();
var pdfWorkerFile = fs.readFileSync(dir + "/build/pdf.worker.js").toString();
var pdfSandboxFile = fs
.readFileSync(dir + "/build/pdf.sandbox.js")
.toString();
var pdfImageDecodersFile = fs
.readFileSync(dir + "/image_decoders/pdf.image_decoders.js")
.toString();
@ -968,6 +1086,10 @@ async function parseMinified(dir) {
dir + "/build/pdf.worker.min.js",
(await Terser.minify(pdfWorkerFile, options)).code
);
fs.writeFileSync(
dir + "/build/pdf.sandbox.min.js",
(await Terser.minify(pdfSandboxFile, options)).code
);
fs.writeFileSync(
dir + "image_decoders/pdf.image_decoders.min.js",
(await Terser.minify(pdfImageDecodersFile, options)).code
@ -980,9 +1102,14 @@ async function parseMinified(dir) {
fs.unlinkSync(dir + "/web/debugger.js");
fs.unlinkSync(dir + "/build/pdf.js");
fs.unlinkSync(dir + "/build/pdf.worker.js");
fs.unlinkSync(dir + "/build/pdf.sandbox.js");
fs.renameSync(dir + "/build/pdf.min.js", dir + "/build/pdf.js");
fs.renameSync(dir + "/build/pdf.worker.min.js", dir + "/build/pdf.worker.js");
fs.renameSync(
dir + "/build/pdf.sandbox.min.js",
dir + "/build/pdf.sandbox.js"
);
fs.renameSync(
dir + "/image_decoders/pdf.image_decoders.min.js",
dir + "/image_decoders/pdf.image_decoders.js"
@ -1176,7 +1303,14 @@ gulp.task(
})
);
gulp.task("chromium", gulp.series("chromium-pre"));
gulp.task(
"chromium",
gulp.series("chromium-pre", function () {
var defines = builder.merge(DEFINES, { CHROME: true, SKIP_BABEL: false });
var CHROME_BUILD_CONTENT_DIR = BUILD_DIR + "/chromium/content/";
return buildSandbox(defines, CHROME_BUILD_CONTENT_DIR);
})
);
gulp.task("jsdoc", function (done) {
console.log();
@ -1276,7 +1410,7 @@ function buildLib(defines, dir) {
return merge([
gulp.src(
[
"src/{core,display,scripting_api,shared}/*.js",
"src/{core,display,shared}/*.js",
"!src/shared/{cffStandardStrings,fonts_utils}.js",
"src/{pdf,pdf.worker}.js",
],
@ -1294,24 +1428,46 @@ function buildLib(defines, dir) {
gulp.task(
"lib",
gulp.series("buildnumber", "default_preferences", function () {
var defines = builder.merge(DEFINES, { GENERIC: true, LIB: true });
gulp.series(
"buildnumber",
"default_preferences",
function () {
var defines = builder.merge(DEFINES, { GENERIC: true, LIB: true });
return buildLib(defines, "build/lib/");
})
return buildLib(defines, "build/lib/");
},
function () {
var defines = builder.merge(DEFINES, { GENERIC: true, LIB: true });
return buildSandbox(defines, "build/lib/");
}
)
);
gulp.task(
"lib-es5",
gulp.series("buildnumber", "default_preferences", function () {
var defines = builder.merge(DEFINES, {
GENERIC: true,
LIB: true,
SKIP_BABEL: false,
});
gulp.series(
"buildnumber",
"default_preferences",
function () {
var defines = builder.merge(DEFINES, {
GENERIC: true,
LIB: true,
SKIP_BABEL: false,
});
return buildLib(defines, "build/lib-es5/");
})
return buildLib(defines, "build/lib-es5/");
},
function () {
var defines = builder.merge(DEFINES, {
GENERIC: true,
LIB: true,
SKIP_BABEL: false,
});
return buildSandbox(defines, "build/lib-es5/");
}
)
);
function compressPublish(targetName, dir) {
@ -1382,6 +1538,7 @@ gulp.task(
gulp.task(
"unittest",
gulp.series("testing-pre", "generic", "components", function () {
process.env.TZ = "UTC";
return createTestSource("unit");
})
);

100
package-lock.json generated
View File

@ -2079,6 +2079,15 @@
"ansi-wrap": "^0.1.0"
}
},
"ansi-cyan": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz",
"integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=",
"dev": true,
"requires": {
"ansi-wrap": "0.1.0"
}
},
"ansi-gray": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz",
@ -2088,6 +2097,15 @@
"ansi-wrap": "0.1.0"
}
},
"ansi-red": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz",
"integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=",
"dev": true,
"requires": {
"ansi-wrap": "0.1.0"
}
},
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
@ -3882,6 +3900,15 @@
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true
},
"decomment": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/decomment/-/decomment-0.9.3.tgz",
"integrity": "sha512-5skH5BfUL3n09RDmMVaHS1QGCiZRnl2nArUwmsE9JRY93Ueh3tihYl5wIrDdAuXnoFhxVis/DmRWREO2c6DG3w==",
"dev": true,
"requires": {
"esprima": "4.0.1"
}
},
"decompress-response": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
@ -6920,6 +6947,79 @@
"replacestream": "^4.0.0"
}
},
"gulp-strip-comments": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/gulp-strip-comments/-/gulp-strip-comments-2.5.2.tgz",
"integrity": "sha512-lb1bW7rsPWDD8f4ZPSguDvmCdjKmjr5HR4yZb9ros3sLl5AfW7oUj8KzY9/VRisT7dG8dL7hVHzNpQEVxfwZGQ==",
"dev": true,
"requires": {
"decomment": "^0.9.0",
"plugin-error": "^0.1.2",
"through2": "^2.0.3"
},
"dependencies": {
"arr-diff": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz",
"integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=",
"dev": true,
"requires": {
"arr-flatten": "^1.0.1",
"array-slice": "^0.2.3"
}
},
"arr-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz",
"integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=",
"dev": true
},
"array-slice": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
"integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=",
"dev": true
},
"extend-shallow": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz",
"integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=",
"dev": true,
"requires": {
"kind-of": "^1.1.0"
}
},
"kind-of": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz",
"integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=",
"dev": true
},
"plugin-error": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz",
"integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=",
"dev": true,
"requires": {
"ansi-cyan": "^0.1.1",
"ansi-red": "^0.1.1",
"arr-diff": "^1.0.1",
"arr-union": "^2.0.1",
"extend-shallow": "^1.1.2"
}
},
"through2": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
"integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
"dev": true,
"requires": {
"readable-stream": "~2.3.6",
"xtend": "~4.0.1"
}
}
}
},
"gulp-zip": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/gulp-zip/-/gulp-zip-5.0.2.tgz",

View File

@ -30,6 +30,7 @@
"gulp-postcss": "^9.0.0",
"gulp-rename": "^2.0.0",
"gulp-replace": "^1.0.0",
"gulp-strip-comments": "^2.5.2",
"gulp-zip": "^5.0.2",
"jasmine": "^3.6.3",
"jsdoc": "^3.6.6",

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, testMode = false }) {
function initSandbox({ data, extra, out }) {
const proxyHandler = new ProxyHandler(data.dispatchEventName);
const { send, crackURL } = extra;
const doc = new Doc({
@ -58,14 +58,6 @@ function initSandbox({ data, extra, out, testMode = false }) {
out[name] = aform[name].bind(aform);
}
}
if (
(typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || TESTING")) &&
testMode
) {
out._app = app;
}
}
export { initSandbox };

View File

@ -0,0 +1,103 @@
/* 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 ModuleLoader from "../../external/quickjs/quickjs-eval.js";
class Sandbox {
constructor(module, testMode) {
this._evalInSandbox = module.cwrap("evalInSandbox", null, [
"string",
"int",
]);
this._dispatchEventName = null;
this._module = module;
this._testMode = testMode;
this._alertOnError = 1;
}
create(data) {
const sandboxData = JSON.stringify(data);
const extra = [
"send",
"setTimeout",
"clearTimeout",
"setInterval",
"clearInterval",
"crackURL",
];
const extraStr = extra.join(",");
let code = [
"exports = Object.create(null);",
"module = Object.create(null);",
// Next line is replaced by code from initialization.js
// when we create the bundle for the sandbox.
"/* INITIALIZATION_CODE */",
`data = ${sandboxData};`,
`module.exports.initSandbox({ data, extra: {${extraStr}}, out: this});`,
"delete exports;",
"delete module;",
"delete data;",
];
if (!this._testMode) {
code = code.concat(extra.map(name => `delete ${name};`));
code.push("delete debugMe;");
}
this._evalInSandbox(code.join("\n"), this._alertOnError);
this._dispatchEventName = data.dispatchEventName;
}
dispatchEvent(event) {
if (this._dispatchEventName === null) {
throw new Error("Sandbox must have been initialized");
}
event = JSON.stringify(event);
this._evalInSandbox(
`app["${this._dispatchEventName}"](${event});`,
this._alertOnError
);
}
dumpMemoryUse() {
this._module.ccall("dumpMemoryUse", null, []);
}
nukeSandbox() {
this._dispatchEventName = null;
this._module.ccall("nukeSandbox", null, []);
this._module = null;
this._evalInSandbox = null;
}
evalForTesting(code, key) {
if (this._testMode) {
this._evalInSandbox(
`send({ id: "${key}", result: ${code} });`,
this._alertOnError
);
}
}
}
function QuickJSSandbox(testMode = false) {
testMode =
testMode &&
(typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || TESTING"));
return ModuleLoader().then(module => {
return new Sandbox(module, testMode);
});
}
export { QuickJSSandbox };

View File

@ -49,6 +49,9 @@ class Util extends PDFObject {
}
crackURL(cURL) {
if (typeof cURL !== "string") {
throw new TypeError("First argument of util.crackURL must be a string");
}
return this._crackURL(cURL);
}

View File

@ -32,7 +32,6 @@
"pdf_find_utils_spec.js",
"pdf_history_spec.js",
"primitives_spec.js",
"scripting_spec.js",
"stream_spec.js",
"type1_parser_spec.js",
"ui_utils_spec.js",

View File

@ -13,258 +13,426 @@
* limitations under the License.
*/
import { initSandbox } from "../../src/scripting_api/initialization.js";
import { loadScript } from "../../src/display/display_utils.js";
describe("Scripting", function () {
let sandbox, send_queue, test_id, ref;
function getId() {
const id = `${ref++}R`;
return id;
}
beforeAll(function (done) {
test_id = 0;
ref = 1;
send_queue = new Map();
window.dispatchEvent = event => {
if (send_queue.has(event.detail.id)) {
const prev = send_queue.get(event.detail.id);
Object.assign(prev, event.detail);
} else {
send_queue.set(event.detail.id, event.detail);
}
};
const promise = loadScript("../../build/generic/build/pdf.sandbox.js").then(
() => {
return window.pdfjsSandbox.QuickJSSandbox(true);
}
);
sandbox = {
createSandbox(data) {
promise.then(sbx => sbx.create(data));
},
dispatchEventInSandbox(data) {
return promise.then(sbx => sbx.dispatchEvent(data));
},
nukeSandbox() {
promise.then(sbx => sbx.nukeSandbox());
},
eval(code, key) {
return promise.then(sbx => sbx.evalForTesting(code, key));
},
};
done();
});
afterAll(function () {
sandbox.nukeSandbox();
sandbox = null;
send_queue = null;
});
describe("Sandbox", function () {
it("should send a value, execute an action and get back a new value", function (done) {
function compute(n) {
let s = 0;
for (let i = 0; i < n; i++) {
s += i;
}
return s;
}
const number = 123;
const expected = ((number - 1) * number) / 2;
const refId = getId();
const data = {
objects: {
field: [
{
id: refId,
value: "",
actions: {
Keystroke: [
`${compute.toString()}event.value = compute(parseInt(event.value));`,
],
},
type: "text",
},
],
},
calculationOrder: [],
dispatchEventName: "_dispatchMe",
};
sandbox.createSandbox(data);
sandbox
.dispatchEventInSandbox({
id: refId,
value: `${number}`,
name: "Keystroke",
willCommit: true,
})
.then(() => {
expect(send_queue.has(refId)).toEqual(true);
expect(send_queue.get(refId)).toEqual({
id: refId,
valueAsString: expected,
});
done();
})
.catch(done.fail);
});
});
describe("Util", function () {
let sandbox, util;
function myeval(code) {
const key = (test_id++).toString();
return sandbox.eval(code, key).then(() => {
return send_queue.get(key).result;
});
}
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;
sandbox.createSandbox({
objects: {},
calculationOrder: [],
dispatchEventName: "_dispatchMe",
});
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();
const date = `new Date("Sun Apr 15 2007 03:14:15")`;
Promise.all([
myeval(`util.printd(0, ${date})`).then(value => {
expect(value).toEqual("D:20070415031415");
}),
myeval(`util.printd(1, ${date})`).then(value => {
expect(value).toEqual("2007.04.15 03:14:15");
}),
myeval(`util.printd(2, ${date})`).then(value => {
expect(value).toEqual("4/15/07 3:14:15 am");
}),
myeval(`util.printd("mmmm mmm mm m", ${date})`).then(value => {
expect(value).toEqual("April Apr 04 4");
}),
myeval(`util.printd("dddd ddd dd d", ${date})`).then(value => {
expect(value).toEqual("Sunday Sun 15 15");
}),
]).then(() => 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();
const date = new Date("Sun Apr 15 2007 03:14:15");
Promise.all([
myeval(`util.scand(0, "D:20070415031415").toString()`).then(value => {
expect(new Date(value)).toEqual(date);
}),
myeval(`util.scand(1, "2007.04.15 03:14:15").toString()`).then(
value => {
expect(new Date(value)).toEqual(date);
}
),
myeval(`util.scand(2, "4/15/07 3:14:15 am").toString()`).then(
value => {
expect(new Date(value)).toEqual(date);
}
),
]).then(() => 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();
Promise.all([
myeval(
`util.printf("Integer numbers: %d, %d,...", 1.234, 56.789)`
).then(value => {
expect(value).toEqual("Integer numbers: 1, 56,...");
}),
myeval(`util.printf("Hex numbers: %x, %x,...", 1234, 56789)`).then(
value => {
expect(value).toEqual("Hex numbers: 4D2, DDD5,...");
}
),
myeval(
`util.printf("Hex numbers with 0x: %#x, %#x,...", 1234, 56789)`
).then(value => {
expect(value).toEqual("Hex numbers with 0x: 0x4D2, 0xDDD5,...");
}),
myeval(`util.printf("Decimal number: %,0+.3f", 1234567.89123)`).then(
value => {
expect(value).toEqual("Decimal number: +1,234,567.891");
}
),
myeval(`util.printf("Decimal number: %,0+8.3f", 1.234567)`).then(
value => {
expect(value).toEqual("Decimal number: + 1.235");
}
),
]).then(() => done());
});
it("should print a string with no argument", function (done) {
expect(util.printf("hello world")).toEqual("hello world");
done();
myeval(`util.printf("hello world")`)
.then(value => {
expect(value).toEqual("hello world");
})
.then(() => 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();
it(" print a string with a percent", function (done) {
myeval(`util.printf("%%s")`)
.then(value => {
expect(value).toEqual("%%s");
})
.then(() => 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();
myeval(`util.printx("9 (999) 999-9999", "aaa14159697489zzz")`)
.then(value => {
expect(value).toEqual("1 (415) 969-7489");
})
.then(() => done());
});
});
});
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,
};
it("should trigger an event and modify the source", function (done) {
const refId = getId();
const data = {
objects: {
field314R: [
field: [
{
id: "314R",
id: refId,
value: "",
actions: {},
type: "text",
},
],
field271R: [
{
id: "271R",
value: "",
actions: {},
actions: {
test: [`event.source.value = "123";`],
},
type: "text",
},
],
},
calculationOrder: ["271R"],
calculationOrder: [],
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();
sandbox.createSandbox(data);
sandbox
.dispatchEventInSandbox({
id: refId,
value: "",
name: "test",
willCommit: true,
})
.then(() => {
expect(send_queue.has(refId)).toEqual(true);
expect(send_queue.get(refId)).toEqual({
id: refId,
value: "123",
});
done();
})
.catch(done.fail);
});
it("should trigger a Keystroke event and invalidate it", function (done) {
_app._objects["314R"].obj._actions.set("Keystroke", [
event => {
event.rc = false;
const refId = getId();
const data = {
objects: {
field: [
{
id: refId,
value: "",
actions: {
Keystroke: [`event.rc = false;`],
},
type: "text",
},
],
},
]);
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();
calculationOrder: [],
dispatchEventName: "_dispatchMe",
};
sandbox.createSandbox(data);
sandbox
.dispatchEventInSandbox({
id: refId,
value: "hell",
name: "Keystroke",
willCommit: false,
change: "o",
selStart: 4,
selEnd: 4,
})
.then(() => {
expect(send_queue.has(refId)).toEqual(true);
expect(send_queue.get(refId)).toEqual({
id: refId,
value: "hell",
selRange: [4, 4],
});
done();
})
.catch(done.fail);
});
it("should trigger a Keystroke event and change it", function (done) {
_app._objects["314R"].obj._actions.set("Keystroke", [
event => {
event.change = "a";
const refId = getId();
const data = {
objects: {
field: [
{
id: refId,
value: "",
actions: {
Keystroke: [`event.change = "a";`],
},
type: "text",
},
],
},
]);
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();
calculationOrder: [],
dispatchEventName: "_dispatchMe",
};
sandbox.createSandbox(data);
sandbox
.dispatchEventInSandbox({
id: refId,
value: "hell",
name: "Keystroke",
willCommit: false,
change: "o",
selStart: 4,
selEnd: 4,
})
.then(() => {
expect(send_queue.has(refId)).toEqual(true);
expect(send_queue.get(refId)).toEqual({
id: refId,
value: "hella",
});
done();
})
.catch(done.fail);
});
it("should trigger an invalid commit Keystroke event", function (done) {
_app._objects["314R"].obj._actions.set("Validate", [
event => {
event.rc = false;
const refId = getId();
const data = {
objects: {
field: [
{
id: refId,
value: "",
actions: {
test: [`event.rc = false;`],
},
type: "text",
},
],
},
]);
sandbox.app._dispatchMe({
id: "314R",
value: "hello",
name: "Keystroke",
willCommit: true,
});
expect(send_queue.length).toEqual(0);
done();
calculationOrder: [],
dispatchEventName: "_dispatchMe",
};
sandbox.createSandbox(data);
sandbox
.dispatchEventInSandbox({
id: refId,
value: "",
name: "test",
willCommit: true,
})
.then(() => {
expect(send_queue.has(refId)).toEqual(false);
done();
})
.catch(done.fail);
});
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";
const refId1 = getId();
const refId2 = getId();
const data = {
objects: {
field1: [
{
id: refId1,
value: "",
actions: {
Validate: [`event.value = "world";`],
},
type: "text",
},
],
field2: [
{
id: refId2,
value: "",
actions: {
Calculate: [`event.value = "hello";`],
},
type: "text",
},
],
},
]);
_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();
calculationOrder: [refId2],
dispatchEventName: "_dispatchMe",
};
sandbox.createSandbox(data);
sandbox
.dispatchEventInSandbox({
id: refId1,
value: "hello",
name: "Keystroke",
willCommit: true,
})
.then(() => {
expect(send_queue.has(refId1)).toEqual(true);
expect(send_queue.get(refId1)).toEqual({
id: refId1,
value: "world",
valueAsString: "world",
});
done();
})
.catch(done.fail);
});
});
});

View File

@ -223,6 +223,14 @@ const defaultOptions = {
value: false,
kind: OptionKind.API,
},
scriptingSrc: {
/** @type {string} */
value:
typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")
? "../build/generic/build/pdf.sandbox.js"
: "../build/pdf.sandbox.js",
kind: OptionKind.VIEWER,
},
verbosity: {
/** @type {number} */
value: 1,

43
web/devcom.js Normal file
View File

@ -0,0 +1,43 @@
/* Copyright 2017 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 { DefaultExternalServices, PDFViewerApplication } from "./app.js";
import { loadScript, shadow } from "pdfjs-lib";
const DevCom = {};
class DevExternalServices extends DefaultExternalServices {
static get scripting() {
const promise = loadScript("../build/pdf.sandbox.js").then(() => {
return window.pdfjsSandbox.QuickJSSandbox();
});
const sandbox = {
createSandbox(data) {
promise.then(sbx => sbx.create(data));
},
dispatchEventInSandbox(event) {
promise.then(sbx => sbx.dispatchEvent(event));
},
destroySandbox() {
promise.then(sbx => sbx.nukeSandbox());
},
};
return shadow(this, "scripting", sandbox);
}
}
PDFViewerApplication.externalServices = DevExternalServices;
export { DevCom };

View File

@ -259,7 +259,7 @@ class FirefoxScripting {
FirefoxCom.requestSync("createSandbox", data);
}
static dispatchEventInSandbox(event, sandboxID) {
static dispatchEventInSandbox(event) {
FirefoxCom.requestSync("dispatchEventInSandbox", event);
}

View File

@ -14,6 +14,8 @@
*/
import { DefaultExternalServices, PDFViewerApplication } from "./app.js";
import { loadScript, shadow } from "pdfjs-lib";
import { AppOptions } from "./app_options.js";
import { BasePreferences } from "./preferences.js";
import { DownloadManager } from "./download_manager.js";
import { GenericL10n } from "./genericl10n.js";
@ -49,6 +51,25 @@ class GenericExternalServices extends DefaultExternalServices {
static createL10n({ locale = "en-US" }) {
return new GenericL10n(locale);
}
static get scripting() {
const promise = loadScript(AppOptions.get("scriptingSrc")).then(() => {
return window.pdfjsSandbox.QuickJSSandbox();
});
const sandbox = {
createSandbox(data) {
promise.then(sbx => sbx.create(data));
},
dispatchEventInSandbox(event) {
promise.then(sbx => sbx.dispatchEvent(event));
},
destroySandbox() {
promise.then(sbx => sbx.nukeSandbox());
},
};
return shadow(this, "scripting", sandbox);
}
}
PDFViewerApplication.externalServices = GenericExternalServices;

View File

@ -56,6 +56,9 @@ if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC")) {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("CHROME")) {
require("./chromecom.js");
}
if (typeof PDFJSDev === "undefined") {
import("./devcom.js");
}
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("CHROME || GENERIC")) {
require("./pdf_print_service.js");
}