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/cmapscompress/
external/builder/fixtures/ external/builder/fixtures/
external/builder/fixtures_esprima/ external/builder/fixtures_esprima/
external/quickjs/quickjs-eval.js
src/shared/cffStandardStrings.js src/shared/cffStandardStrings.js
src/shared/fonts_utils.js src/shared/fonts_utils.js
test/tmp/ 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 exec = require("child_process").exec;
var spawn = require("child_process").spawn; var spawn = require("child_process").spawn;
var spawnSync = require("child_process").spawnSync; var spawnSync = require("child_process").spawnSync;
var stripComments = require("gulp-strip-comments");
var streamqueue = require("streamqueue"); var streamqueue = require("streamqueue");
var merge = require("merge-stream"); var merge = require("merge-stream");
var zip = require("gulp-zip"); var zip = require("gulp-zip");
@ -105,6 +106,7 @@ const DEFINES = Object.freeze({
COMPONENTS: false, COMPONENTS: false,
LIB: false, LIB: false,
IMAGE_DECODERS: false, IMAGE_DECODERS: false,
NO_SOURCE_MAP: false,
}); });
function transform(charEncoding, transformFunction) { function transform(charEncoding, transformFunction) {
@ -182,7 +184,8 @@ function createWebpackConfig(defines, output) {
var enableSourceMaps = var enableSourceMaps =
!bundleDefines.MOZCENTRAL && !bundleDefines.MOZCENTRAL &&
!bundleDefines.CHROME && !bundleDefines.CHROME &&
!bundleDefines.TESTING; !bundleDefines.TESTING &&
!bundleDefines.NO_SOURCE_MAP;
var skipBabel = bundleDefines.SKIP_BABEL; var skipBabel = bundleDefines.SKIP_BABEL;
// `core-js` (see https://github.com/zloirock/core-js/issues/514), // `core-js` (see https://github.com/zloirock/core-js/issues/514),
@ -343,6 +346,53 @@ function createScriptingBundle(defines) {
.pipe(replaceJSRootName(scriptingAMDName, "pdfjsScripting")); .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) { function createWorkerBundle(defines) {
var workerAMDName = "pdfjs-dist/build/pdf.worker"; var workerAMDName = "pdfjs-dist/build/pdf.worker";
var workerOutputName = "pdf.worker.js"; 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) { gulp.task("default", function (done) {
console.log("Available tasks:"); console.log("Available tasks:");
var tasks = Object.keys(gulp.registry().tasks()); var tasks = Object.keys(gulp.registry().tasks());
@ -762,26 +831,47 @@ function buildGeneric(defines, dir) {
// HTML5 browsers, which implement modern ECMAScript features. // HTML5 browsers, which implement modern ECMAScript features.
gulp.task( gulp.task(
"generic", "generic",
gulp.series("buildnumber", "default_preferences", "locale", function () { gulp.series(
console.log(); "buildnumber",
console.log("### Creating generic viewer"); "default_preferences",
var defines = builder.merge(DEFINES, { GENERIC: true }); "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 // Builds the generic production viewer that should be compatible with most
// older HTML5 browsers. // older HTML5 browsers.
gulp.task( gulp.task(
"generic-es5", "generic-es5",
gulp.series("buildnumber", "default_preferences", "locale", function () { gulp.series(
console.log(); "buildnumber",
console.log("### Creating generic (ES5) viewer"); "default_preferences",
var defines = builder.merge(DEFINES, { GENERIC: true, SKIP_BABEL: false }); "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) { function buildComponents(defines, dir) {
@ -908,33 +998,61 @@ function buildMinified(defines, dir) {
gulp.task( gulp.task(
"minified-pre", "minified-pre",
gulp.series("buildnumber", "default_preferences", "locale", function () { gulp.series(
console.log(); "buildnumber",
console.log("### Creating minified viewer"); "default_preferences",
var defines = builder.merge(DEFINES, { MINIFIED: true, GENERIC: true }); "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( gulp.task(
"minified-es5-pre", "minified-es5-pre",
gulp.series("buildnumber", "default_preferences", "locale", function () { gulp.series(
console.log(); "buildnumber",
console.log("### Creating minified (ES5) viewer"); "default_preferences",
var defines = builder.merge(DEFINES, { "locale",
MINIFIED: true, function () {
GENERIC: true, console.log();
SKIP_BABEL: false, 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) { async function parseMinified(dir) {
var pdfFile = fs.readFileSync(dir + "/build/pdf.js").toString(); var pdfFile = fs.readFileSync(dir + "/build/pdf.js").toString();
var pdfWorkerFile = fs.readFileSync(dir + "/build/pdf.worker.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 var pdfImageDecodersFile = fs
.readFileSync(dir + "/image_decoders/pdf.image_decoders.js") .readFileSync(dir + "/image_decoders/pdf.image_decoders.js")
.toString(); .toString();
@ -968,6 +1086,10 @@ async function parseMinified(dir) {
dir + "/build/pdf.worker.min.js", dir + "/build/pdf.worker.min.js",
(await Terser.minify(pdfWorkerFile, options)).code (await Terser.minify(pdfWorkerFile, options)).code
); );
fs.writeFileSync(
dir + "/build/pdf.sandbox.min.js",
(await Terser.minify(pdfSandboxFile, options)).code
);
fs.writeFileSync( fs.writeFileSync(
dir + "image_decoders/pdf.image_decoders.min.js", dir + "image_decoders/pdf.image_decoders.min.js",
(await Terser.minify(pdfImageDecodersFile, options)).code (await Terser.minify(pdfImageDecodersFile, options)).code
@ -980,9 +1102,14 @@ async function parseMinified(dir) {
fs.unlinkSync(dir + "/web/debugger.js"); fs.unlinkSync(dir + "/web/debugger.js");
fs.unlinkSync(dir + "/build/pdf.js"); fs.unlinkSync(dir + "/build/pdf.js");
fs.unlinkSync(dir + "/build/pdf.worker.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.min.js", dir + "/build/pdf.js");
fs.renameSync(dir + "/build/pdf.worker.min.js", dir + "/build/pdf.worker.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( fs.renameSync(
dir + "/image_decoders/pdf.image_decoders.min.js", dir + "/image_decoders/pdf.image_decoders.min.js",
dir + "/image_decoders/pdf.image_decoders.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) { gulp.task("jsdoc", function (done) {
console.log(); console.log();
@ -1276,7 +1410,7 @@ function buildLib(defines, dir) {
return merge([ return merge([
gulp.src( gulp.src(
[ [
"src/{core,display,scripting_api,shared}/*.js", "src/{core,display,shared}/*.js",
"!src/shared/{cffStandardStrings,fonts_utils}.js", "!src/shared/{cffStandardStrings,fonts_utils}.js",
"src/{pdf,pdf.worker}.js", "src/{pdf,pdf.worker}.js",
], ],
@ -1294,24 +1428,46 @@ function buildLib(defines, dir) {
gulp.task( gulp.task(
"lib", "lib",
gulp.series("buildnumber", "default_preferences", function () { gulp.series(
var defines = builder.merge(DEFINES, { GENERIC: true, LIB: true }); "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( gulp.task(
"lib-es5", "lib-es5",
gulp.series("buildnumber", "default_preferences", function () { gulp.series(
var defines = builder.merge(DEFINES, { "buildnumber",
GENERIC: true, "default_preferences",
LIB: true, function () {
SKIP_BABEL: false, 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) { function compressPublish(targetName, dir) {
@ -1382,6 +1538,7 @@ gulp.task(
gulp.task( gulp.task(
"unittest", "unittest",
gulp.series("testing-pre", "generic", "components", function () { gulp.series("testing-pre", "generic", "components", function () {
process.env.TZ = "UTC";
return createTestSource("unit"); return createTestSource("unit");
}) })
); );

100
package-lock.json generated
View File

@ -2079,6 +2079,15 @@
"ansi-wrap": "^0.1.0" "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": { "ansi-gray": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz",
@ -2088,6 +2097,15 @@
"ansi-wrap": "0.1.0" "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": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
@ -3882,6 +3900,15 @@
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true "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": { "decompress-response": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
@ -6920,6 +6947,79 @@
"replacestream": "^4.0.0" "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": { "gulp-zip": {
"version": "5.0.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/gulp-zip/-/gulp-zip-5.0.2.tgz", "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-postcss": "^9.0.0",
"gulp-rename": "^2.0.0", "gulp-rename": "^2.0.0",
"gulp-replace": "^1.0.0", "gulp-replace": "^1.0.0",
"gulp-strip-comments": "^2.5.2",
"gulp-zip": "^5.0.2", "gulp-zip": "^5.0.2",
"jasmine": "^3.6.3", "jasmine": "^3.6.3",
"jsdoc": "^3.6.6", "jsdoc": "^3.6.6",

View File

@ -22,7 +22,7 @@ import { ProxyHandler } from "./proxy.js";
import { Util } from "./util.js"; import { Util } from "./util.js";
import { ZoomType } from "./constants.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 proxyHandler = new ProxyHandler(data.dispatchEventName);
const { send, crackURL } = extra; const { send, crackURL } = extra;
const doc = new Doc({ const doc = new Doc({
@ -58,14 +58,6 @@ function initSandbox({ data, extra, out, testMode = false }) {
out[name] = aform[name].bind(aform); out[name] = aform[name].bind(aform);
} }
} }
if (
(typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || TESTING")) &&
testMode
) {
out._app = app;
}
} }
export { initSandbox }; 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) { crackURL(cURL) {
if (typeof cURL !== "string") {
throw new TypeError("First argument of util.crackURL must be a string");
}
return this._crackURL(cURL); return this._crackURL(cURL);
} }

View File

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

View File

@ -13,258 +13,426 @@
* limitations under the License. * limitations under the License.
*/ */
import { initSandbox } from "../../src/scripting_api/initialization.js"; import { loadScript } from "../../src/display/display_utils.js";
describe("Scripting", function () { 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 () { 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) { beforeAll(function (done) {
sandbox = Object.create(null); sandbox.createSandbox({
const extra = { send: null, crackURL: null }; objects: {},
const data = { objects: {}, calculationOrder: [] }; calculationOrder: [],
initSandbox({ data, extra, out: sandbox }); dispatchEventName: "_dispatchMe",
util = sandbox.util; });
done(); done();
}); });
afterAll(function () {
sandbox = util = null;
});
describe("printd", function () { describe("printd", function () {
it("should print a date according to a format", function (done) { it("should print a date according to a format", function (done) {
const date = new Date("April 15, 1707 3:14:15"); const date = `new Date("Sun Apr 15 2007 03:14:15")`;
expect(util.printd(0, date)).toEqual("D:17070415031415"); Promise.all([
expect(util.printd(1, date)).toEqual("1707.04.15 03:14:15"); myeval(`util.printd(0, ${date})`).then(value => {
expect(util.printd(2, date)).toEqual("4/15/07 3:14:15 am"); expect(value).toEqual("D:20070415031415");
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"); myeval(`util.printd(1, ${date})`).then(value => {
done(); 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 () { describe("scand", function () {
it("should parse a date according to a format", function (done) { it("should parse a date according to a format", function (done) {
const date = new Date("April 15, 1707 3:14:15"); const date = new Date("Sun Apr 15 2007 03:14:15");
expect(util.scand(0, "D:17070415031415")).toEqual(date); Promise.all([
expect(util.scand(1, "1707.04.15 03:14:15")).toEqual(date); myeval(`util.scand(0, "D:20070415031415").toString()`).then(value => {
expect(util.scand(2, "4/15/07 3:14:15 am")).toEqual( expect(new Date(value)).toEqual(date);
new Date("April 15, 2007 3:14:15") }),
); myeval(`util.scand(1, "2007.04.15 03:14:15").toString()`).then(
done(); 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 () { describe("printf", function () {
it("should print some data according to a format", function (done) { it("should print some data according to a format", function (done) {
expect( Promise.all([
util.printf("Integer numbers: %d, %d,...", 1.234, 56.789) myeval(
).toEqual("Integer numbers: 1, 56,..."); `util.printf("Integer numbers: %d, %d,...", 1.234, 56.789)`
expect(util.printf("Hex numbers: %x, %x,...", 1234, 56789)).toEqual( ).then(value => {
"Hex numbers: 4D2, DDD5,..." expect(value).toEqual("Integer numbers: 1, 56,...");
); }),
expect( myeval(`util.printf("Hex numbers: %x, %x,...", 1234, 56789)`).then(
util.printf("Hex numbers with 0x: %#x, %#x,...", 1234, 56789) value => {
).toEqual("Hex numbers with 0x: 0x4D2, 0xDDD5,..."); expect(value).toEqual("Hex numbers: 4D2, DDD5,...");
expect(util.printf("Decimal number: %,0+.3f", 1234567.89123)).toEqual( }
"Decimal number: +1,234,567.891" ),
); myeval(
expect(util.printf("Decimal number: %,0+8.3f", 1.234567)).toEqual( `util.printf("Hex numbers with 0x: %#x, %#x,...", 1234, 56789)`
"Decimal number: + 1.235" ).then(value => {
); expect(value).toEqual("Hex numbers with 0x: 0x4D2, 0xDDD5,...");
done(); }),
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) { it("should print a string with no argument", function (done) {
expect(util.printf("hello world")).toEqual("hello world"); myeval(`util.printf("hello world")`)
done(); .then(value => {
expect(value).toEqual("hello world");
})
.then(() => done());
}); });
it("should print a string with a percent", function (done) { it(" print a string with a percent", function (done) {
expect(util.printf("%%s")).toEqual("%%s"); myeval(`util.printf("%%s")`)
expect(util.printf("%%s", "hello")).toEqual("%%s"); .then(value => {
done(); expect(value).toEqual("%%s");
})
.then(() => done());
}); });
}); });
describe("printx", function () { describe("printx", function () {
it("should print some data according to a format", function (done) { it("should print some data according to a format", function (done) {
expect(util.printx("9 (999) 999-9999", "aaa14159697489zzz")).toEqual( myeval(`util.printx("9 (999) 999-9999", "aaa14159697489zzz")`)
"1 (415) 969-7489" .then(value => {
); expect(value).toEqual("1 (415) 969-7489");
done(); })
.then(() => done());
}); });
}); });
}); });
describe("Events", function () { describe("Events", function () {
let sandbox, send_queue, _app; it("should trigger an event and modify the source", function (done) {
const refId = getId();
beforeEach(function (done) {
send_queue = [];
sandbox = Object.create(null);
const extra = {
send(data) {
send_queue.push(data);
},
crackURL: null,
};
const data = { const data = {
objects: { objects: {
field314R: [ field: [
{ {
id: "314R", id: refId,
value: "", value: "",
actions: {}, actions: {
type: "text", test: [`event.source.value = "123";`],
}, },
],
field271R: [
{
id: "271R",
value: "",
actions: {},
type: "text", type: "text",
}, },
], ],
}, },
calculationOrder: ["271R"], calculationOrder: [],
dispatchEventName: "_dispatchMe", dispatchEventName: "_dispatchMe",
}; };
sandbox.createSandbox(data);
initSandbox({ sandbox
data, .dispatchEventInSandbox({
extra, id: refId,
out: sandbox, value: "",
testMode: true, name: "test",
}); willCommit: true,
})
_app = sandbox._app; .then(() => {
send_queue = []; expect(send_queue.has(refId)).toEqual(true);
done(); expect(send_queue.get(refId)).toEqual({
}); id: refId,
value: "123",
afterAll(function () { });
sandbox = send_queue = _app = null; done();
}); })
.catch(done.fail);
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) { it("should trigger a Keystroke event and invalidate it", function (done) {
_app._objects["314R"].obj._actions.set("Keystroke", [ const refId = getId();
event => { const data = {
event.rc = false; objects: {
field: [
{
id: refId,
value: "",
actions: {
Keystroke: [`event.rc = false;`],
},
type: "text",
},
],
}, },
]); calculationOrder: [],
dispatchEventName: "_dispatchMe",
sandbox.app._dispatchMe({ };
id: "314R", sandbox.createSandbox(data);
value: "hell", sandbox
name: "Keystroke", .dispatchEventInSandbox({
willCommit: false, id: refId,
change: "o", value: "hell",
selStart: 4, name: "Keystroke",
selEnd: 4, willCommit: false,
}); change: "o",
expect(send_queue.length).toEqual(1); selStart: 4,
expect(send_queue[0]).toEqual({ selEnd: 4,
id: "314R", })
value: "hell", .then(() => {
selRange: [4, 4], expect(send_queue.has(refId)).toEqual(true);
}); expect(send_queue.get(refId)).toEqual({
id: refId,
done(); value: "hell",
selRange: [4, 4],
});
done();
})
.catch(done.fail);
}); });
it("should trigger a Keystroke event and change it", function (done) { it("should trigger a Keystroke event and change it", function (done) {
_app._objects["314R"].obj._actions.set("Keystroke", [ const refId = getId();
event => { const data = {
event.change = "a"; objects: {
field: [
{
id: refId,
value: "",
actions: {
Keystroke: [`event.change = "a";`],
},
type: "text",
},
],
}, },
]); calculationOrder: [],
dispatchEventName: "_dispatchMe",
sandbox.app._dispatchMe({ };
id: "314R", sandbox.createSandbox(data);
value: "hell", sandbox
name: "Keystroke", .dispatchEventInSandbox({
willCommit: false, id: refId,
change: "o", value: "hell",
selStart: 4, name: "Keystroke",
selEnd: 4, willCommit: false,
}); change: "o",
expect(send_queue.length).toEqual(1); selStart: 4,
expect(send_queue[0]).toEqual({ id: "314R", value: "hella" }); selEnd: 4,
})
done(); .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) { it("should trigger an invalid commit Keystroke event", function (done) {
_app._objects["314R"].obj._actions.set("Validate", [ const refId = getId();
event => { const data = {
event.rc = false; objects: {
field: [
{
id: refId,
value: "",
actions: {
test: [`event.rc = false;`],
},
type: "text",
},
],
}, },
]); calculationOrder: [],
dispatchEventName: "_dispatchMe",
sandbox.app._dispatchMe({ };
id: "314R", sandbox.createSandbox(data);
value: "hello", sandbox
name: "Keystroke", .dispatchEventInSandbox({
willCommit: true, id: refId,
}); value: "",
expect(send_queue.length).toEqual(0); name: "test",
willCommit: true,
done(); })
.then(() => {
expect(send_queue.has(refId)).toEqual(false);
done();
})
.catch(done.fail);
}); });
it("should trigger a valid commit Keystroke event", function (done) { it("should trigger a valid commit Keystroke event", function (done) {
let output = ""; const refId1 = getId();
_app._objects["314R"].obj._actions.set("Validate", [ const refId2 = getId();
event => { const data = {
event.value = "world"; objects: {
output += "foo"; field1: [
{
id: refId1,
value: "",
actions: {
Validate: [`event.value = "world";`],
},
type: "text",
},
],
field2: [
{
id: refId2,
value: "",
actions: {
Calculate: [`event.value = "hello";`],
},
type: "text",
},
],
}, },
]); calculationOrder: [refId2],
_app._objects["271R"].obj._actions.set("Calculate", [ dispatchEventName: "_dispatchMe",
event => { };
event.value = "hello"; sandbox.createSandbox(data);
output += "bar"; sandbox
}, .dispatchEventInSandbox({
]); id: refId1,
value: "hello",
sandbox.app._dispatchMe({ name: "Keystroke",
id: "314R", willCommit: true,
value: "hello", })
name: "Keystroke", .then(() => {
willCommit: true, expect(send_queue.has(refId1)).toEqual(true);
}); expect(send_queue.get(refId1)).toEqual({
id: refId1,
expect(send_queue.length).toEqual(4); value: "world",
expect(send_queue[0]).toEqual({ id: "314R", value: "world" }); valueAsString: "world",
expect(send_queue[1]).toEqual({ id: "271R", value: "hello" }); });
expect(send_queue[2]).toEqual({ id: "271R", valueAsString: "hello" }); done();
expect(send_queue[3]).toEqual({ id: "314R", valueAsString: "world" }); })
expect(output).toEqual("foobar"); .catch(done.fail);
done();
}); });
}); });
}); });

View File

@ -223,6 +223,14 @@ const defaultOptions = {
value: false, value: false,
kind: OptionKind.API, 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: { verbosity: {
/** @type {number} */ /** @type {number} */
value: 1, 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); FirefoxCom.requestSync("createSandbox", data);
} }
static dispatchEventInSandbox(event, sandboxID) { static dispatchEventInSandbox(event) {
FirefoxCom.requestSync("dispatchEventInSandbox", event); FirefoxCom.requestSync("dispatchEventInSandbox", event);
} }

View File

@ -14,6 +14,8 @@
*/ */
import { DefaultExternalServices, PDFViewerApplication } from "./app.js"; import { DefaultExternalServices, PDFViewerApplication } from "./app.js";
import { loadScript, shadow } from "pdfjs-lib";
import { AppOptions } from "./app_options.js";
import { BasePreferences } from "./preferences.js"; import { BasePreferences } from "./preferences.js";
import { DownloadManager } from "./download_manager.js"; import { DownloadManager } from "./download_manager.js";
import { GenericL10n } from "./genericl10n.js"; import { GenericL10n } from "./genericl10n.js";
@ -49,6 +51,25 @@ class GenericExternalServices extends DefaultExternalServices {
static createL10n({ locale = "en-US" }) { static createL10n({ locale = "en-US" }) {
return new GenericL10n(locale); 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; PDFViewerApplication.externalServices = GenericExternalServices;

View File

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