pdf.js/web/chromecom.js

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

445 lines
16 KiB
JavaScript
Raw Normal View History

/* Copyright 2013 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.
*/
/* globals chrome */
Switch to using ESLint, instead of JSHint, for linting *Please note that most of the necessary code adjustments were made in PR 7890.* ESLint has a number of advantageous properties, compared to JSHint. Among those are: - The ability to find subtle bugs, thanks to more rules (e.g. PR 7881). - Much more customizable in general, and many rules allow fine-tuned behaviour rather than the just the on/off rules in JSHint. - Many more rules that can help developers avoid bugs, and a lot of rules that can be used to enforce a consistent coding style. The latter should be particularily useful for new contributors (and reduce the amount of stylistic review comments necessary). - The ability to easily specify exactly what rules to use/not to use, as opposed to JSHint which has a default set. *Note:* in future JSHint version some of the rules we depend on will be removed, according to warnings in http://jshint.com/docs/options/, so we wouldn't be able to update without losing lint coverage. - More easily disable one, or more, rules temporarily. In JSHint this requires using a numeric code, which isn't very user friendly, whereas in ESLint the rule name is simply used instead. By default there's no rules enabled in ESLint, but there are some default rule sets available. However, to prevent linting failures if we update ESLint in the future, it seemed easier to just explicitly specify what rules we want. Obviously this makes the ESLint config file somewhat bigger than the old JSHint config file, but given how rarely that one has been updated over the years I don't think that matters too much. I've tried, to the best of my ability, to ensure that we enable the same rules for ESLint that we had for JSHint. Furthermore, I've also enabled a number of rules that seemed to make sense, both to catch possible errors *and* various style guide violations. Despite the ESLint README claiming that it's slower that JSHint, https://github.com/eslint/eslint#how-does-eslint-performance-compare-to-jshint, locally this patch actually reduces the runtime for `gulp` lint (by approximately 20-25%). A couple of stylistic rules that would have been nice to enable, but where our code currently differs to much to make it feasible: - `comma-dangle`, controls trailing commas in Objects and Arrays (among others). - `object-curly-spacing`, controls spacing inside of Objects. - `spaced-comment`, used to enforce spaces after `//` and `/*. (This is made difficult by the fact that there's still some usage of the old preprocessor left.) Rules that I indend to look into possibly enabling in follow-ups, if it seems to make sense: `no-else-return`, `no-lonely-if`, `brace-style` with the `allowSingleLine` parameter removed. Useful links: - http://eslint.org/docs/user-guide/configuring - http://eslint.org/docs/rules/
2016-12-15 23:52:29 +09:00
import { AppOptions } from "./app_options.js";
import { BaseExternalServices } from "./external_services.js";
import { BasePreferences } from "./preferences.js";
import { GenericL10n } from "./genericl10n.js";
import { GenericScripting } from "./generic_scripting.js";
Enable auto-formatting of the entire code-base using Prettier (issue 11444) Note that Prettier, purposely, has only limited [configuration options](https://prettier.io/docs/en/options.html). The configuration file is based on [the one in `mozilla central`](https://searchfox.org/mozilla-central/source/.prettierrc) with just a few additions (to avoid future breakage if the defaults ever changes). Prettier is being used for a couple of reasons: - To be consistent with `mozilla-central`, where Prettier is already in use across the tree. - To ensure a *consistent* coding style everywhere, which is automatically enforced during linting (since Prettier is used as an ESLint plugin). This thus ends "all" formatting disussions once and for all, removing the need for review comments on most stylistic matters. Many ESLint options are now redundant, and I've tried my best to remove all the now unnecessary options (but I may have missed some). Note also that since Prettier considers the `printWidth` option as a guide, rather than a hard rule, this patch resorts to a small hack in the ESLint config to ensure that *comments* won't become too long. *Please note:* This patch is generated automatically, by appending the `--fix` argument to the ESLint call used in the `gulp lint` task. It will thus require some additional clean-up, which will be done in a *separate* commit. (On a more personal note, I'll readily admit that some of the changes Prettier makes are *extremely* ugly. However, in the name of consistency we'll probably have to live with that.)
2019-12-25 23:59:37 +09:00
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME")) {
throw new Error(
'Module "pdfjs-web/chromecom" shall not be used outside CHROME build.'
);
}
(function rewriteUrlClosure() {
// Run this code outside DOMContentLoaded to make sure that the URL
// is rewritten as soon as possible.
const queryString = document.location.search.slice(1);
const m = /(^|&)file=([^&]*)/.exec(queryString);
const defaultUrl = m ? decodeURIComponent(m[2]) : "";
// Example: chrome-extension://.../http://example.com/file.pdf
const humanReadableUrl = "/" + defaultUrl + location.hash;
history.replaceState(history.state, "", humanReadableUrl);
if (top === window) {
chrome.runtime.sendMessage("showPageAction");
}
AppOptions.set("defaultUrl", defaultUrl);
})();
let viewerApp = { initialized: false };
function initCom(app) {
viewerApp = app;
// Ensure that PDFViewerApplication.initialBookmark reflects the current hash,
// in case the URL rewrite above results in a different hash.
viewerApp.initialBookmark = location.hash.slice(1);
}
const ChromeCom = {
/**
* Creates an event that the extension is listening for and will
* asynchronously respond by calling the callback.
*
* @param {string} action - The action to trigger.
* @param {string} [data] - The data to send.
* @param {Function} [callback] - Response callback that will be called with
* one data argument. When the request cannot be handled, the callback is
* immediately invoked with no arguments.
*/
request(action, data, callback) {
const message = {
action,
data,
};
if (!chrome.runtime) {
console.error("chrome.runtime is undefined.");
callback?.();
} else if (callback) {
chrome.runtime.sendMessage(message, callback);
} else {
chrome.runtime.sendMessage(message);
}
},
/**
* Resolves a PDF file path and attempts to detects length.
*
* @param {string} file - Absolute URL of PDF file.
* @param {Function} callback - A callback with resolved URL and file length.
*/
resolvePDFFile(file, callback) {
// Expand drive:-URLs to filesystem URLs (Chrome OS)
file = file.replace(
/^drive:/i,
"filesystem:" + location.origin + "/external/"
);
if (/^https?:/.test(file)) {
// Assumption: The file being opened is the file that was requested.
// There is no UI to input a different URL, so this assumption will hold
// for now.
setReferer(file, function () {
callback(file);
});
return;
}
if (/^file?:/.test(file)) {
getEmbedderOrigin(function (origin) {
// If the origin cannot be determined, let Chrome decide whether to
// allow embedding files. Otherwise, only allow local files to be
// embedded from local files or Chrome extensions.
// Even without this check, the file load in frames is still blocked,
// but this may change in the future (https://crbug.com/550151).
if (origin && !/^file:|^chrome-extension:/.test(origin)) {
viewerApp._documentError(
`Blocked ${origin} from loading ${file}. Refused to load ` +
"a local file in a non-local page for security reasons."
);
return;
}
isAllowedFileSchemeAccess(function (isAllowedAccess) {
if (isAllowedAccess) {
callback(file);
} else {
requestAccessToLocalFile(file, viewerApp.overlayManager, callback);
}
});
});
return;
}
callback(file);
},
};
function getEmbedderOrigin(callback) {
const origin = window === top ? location.origin : location.ancestorOrigins[0];
if (origin === "null") {
// file:-URLs, data-URLs, sandboxed frames, etc.
getParentOrigin(callback);
} else {
callback(origin);
}
}
function getParentOrigin(callback) {
ChromeCom.request("getParentOrigin", null, callback);
}
function isAllowedFileSchemeAccess(callback) {
ChromeCom.request("isAllowedFileSchemeAccess", null, callback);
}
function isRuntimeAvailable() {
try {
// When the extension is reloaded, the extension runtime is destroyed and
// the extension APIs become unavailable.
if (chrome.runtime?.getManifest()) {
return true;
}
} catch {}
return false;
}
function reloadIfRuntimeIsUnavailable() {
if (!isRuntimeAvailable()) {
location.reload();
}
}
let chromeFileAccessOverlayPromise;
function requestAccessToLocalFile(fileUrl, overlayManager, callback) {
const dialog = document.getElementById("chromeFileAccessDialog");
if (top !== window) {
// When the extension reloads after receiving new permissions, the pages
// have to be reloaded to restore the extension runtime. Auto-reload
// frames, because users should not have to reload the whole page just to
// update the viewer.
// Top-level frames are closed by Chrome upon reload, so there is no need
// for detecting unload of the top-level frame. Should this ever change
// (crbug.com/511670), then the user can just reload the tab.
window.addEventListener("focus", reloadIfRuntimeIsUnavailable);
dialog.addEventListener("close", function () {
window.removeEventListener("focus", reloadIfRuntimeIsUnavailable);
reloadIfRuntimeIsUnavailable();
});
}
chromeFileAccessOverlayPromise ||= overlayManager.register(
dialog,
/* canForceClose = */ true
);
chromeFileAccessOverlayPromise.then(function () {
const iconPath = chrome.runtime.getManifest().icons[48];
document.getElementById("chrome-pdfjs-logo-bg").style.backgroundImage =
"url(" + chrome.runtime.getURL(iconPath) + ")";
// Use Chrome's definition of UI language instead of PDF.js's #lang=...,
// because the shown string should match the UI at chrome://extensions.
// These strings are from chrome/app/resources/generated_resources_*.xtb.
const i18nFileAccessLabel = PDFJSDev.json(
"$ROOT/web/chrome-i18n-allow-access-to-file-urls.json"
)[chrome.i18n.getUILanguage?.()];
if (i18nFileAccessLabel) {
document.getElementById("chrome-file-access-label").textContent =
i18nFileAccessLabel;
}
const link = document.getElementById("chrome-link-to-extensions-page");
link.href = "chrome://extensions/?id=" + chrome.runtime.id;
link.onclick = function (e) {
// Direct navigation to chrome:// URLs is blocked by Chrome, so we
// have to ask the background page to open chrome://extensions/?id=...
e.preventDefault();
// Open in the current tab by default, because toggling the file access
// checkbox causes the extension to reload, and Chrome will close all
// tabs upon reload.
ChromeCom.request("openExtensionsPageForFileAccess", {
Fix inconsistent spacing and trailing commas in objects in `web/` files, so we can enable the `comma-dangle` and `object-curly-spacing` ESLint rules later on http://eslint.org/docs/rules/comma-dangle http://eslint.org/docs/rules/object-curly-spacing Given that we currently have quite inconsistent object formatting, fixing this in in *one* big patch probably wouldn't be feasible (since I cannot imagine anyone wanting to review that); hence I've opted to try and do this piecewise instead. *Please note:* This patch was created automatically, using the ESLint `--fix` command line option. In a couple of places this caused lines to become too long, and I've fixed those manually; please refer to the interdiff below for the only hand-edits in this patch. ```diff diff --git a/web/pdf_thumbnail_view.js b/web/pdf_thumbnail_view.js index 002dbf29..1de4e530 100644 --- a/web/pdf_thumbnail_view.js +++ b/web/pdf_thumbnail_view.js @@ -420,8 +420,8 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() { setPageLabel: function PDFThumbnailView_setPageLabel(label) { this.pageLabel = (typeof label === 'string' ? label : null); - this.l10n.get('thumb_page_title', { page: this.pageId, }, 'Page {{page}}'). - then((msg) => { + this.l10n.get('thumb_page_title', { page: this.pageId, }, + 'Page {{page}}').then((msg) => { this.anchor.title = msg; }); diff --git a/web/secondary_toolbar.js b/web/secondary_toolbar.js index 160e0410..6495fc5e 100644 --- a/web/secondary_toolbar.js +++ b/web/secondary_toolbar.js @@ -65,7 +65,8 @@ class SecondaryToolbar { { element: options.printButton, eventName: 'print', close: true, }, { element: options.downloadButton, eventName: 'download', close: true, }, { element: options.viewBookmarkButton, eventName: null, close: true, }, - { element: options.firstPageButton, eventName: 'firstpage', close: true, }, + { element: options.firstPageButton, eventName: 'firstpage', + close: true, }, { element: options.lastPageButton, eventName: 'lastpage', close: true, }, { element: options.pageRotateCwButton, eventName: 'rotatecw', close: false, }, @@ -76,7 +77,7 @@ class SecondaryToolbar { { element: options.cursorHandToolButton, eventName: 'switchcursortool', eventDetails: { tool: CursorTool.HAND, }, close: true, }, { element: options.documentPropertiesButton, - eventName: 'documentproperties', close: true, } + eventName: 'documentproperties', close: true, }, ]; this.items = { firstPage: options.firstPageButton, ```
2017-06-01 19:46:12 +09:00
newTab: e.ctrlKey || e.metaKey || e.button === 1 || window !== top,
});
};
// Show which file is being opened to help the user with understanding
// why this permission request is shown.
document.getElementById("chrome-url-of-local-file").textContent = fileUrl;
document.getElementById("chrome-file-fallback").onchange = function () {
const file = this.files[0];
if (file) {
const originalFilename = decodeURIComponent(fileUrl.split("/").pop());
let originalUrl = fileUrl;
if (originalFilename !== file.name) {
const msg =
"The selected file does not match the original file." +
"\nOriginal: " +
originalFilename +
"\nSelected: " +
file.name +
"\nDo you want to open the selected file?";
// eslint-disable-next-line no-alert
if (!confirm(msg)) {
this.value = "";
return;
}
// There is no way to retrieve the original URL from the File object.
// So just generate a fake path.
originalUrl = "file:///fakepath/to/" + encodeURIComponent(file.name);
}
callback(URL.createObjectURL(file), file.size, originalUrl);
overlayManager.close(dialog);
}
};
overlayManager.open(dialog);
});
}
if (window === top) {
// Chrome closes all extension tabs (crbug.com/511670) when the extension
// reloads. To counter this, the tab URL and history state is saved to
// localStorage and restored by extension-router.js.
// Unfortunately, the window and tab index are not restored. And if it was
// the only tab in an incognito window, then the tab is not restored either.
addEventListener("unload", function () {
// If the runtime is still available, the unload is most likely a normal
// tab closure. Otherwise it is most likely an extension reload.
if (!isRuntimeAvailable()) {
localStorage.setItem(
"unload-" + Date.now() + "-" + document.hidden + "-" + location.href,
JSON.stringify(history.state)
);
}
});
}
// This port is used for several purposes:
// 1. When disconnected, the background page knows that the frame has unload.
// 2. When the referrer was saved in history.state.chromecomState, it is sent
// to the background page.
// 3. When the background page knows the referrer of the page, the referrer is
// saved in history.state.chromecomState.
let port;
// Set the referer for the given URL.
// 0. Background: If loaded via a http(s) URL: Save referer.
// 1. Page -> background: send URL and referer from history.state
// 2. Background: Bind referer to URL (via webRequest).
// 3. Background -> page: Send latest referer and save to history.
// 4. Page: Invoke callback.
function setReferer(url, callback) {
if (!port) {
// The background page will accept the port, and keep adding the Referer
// request header to requests to |url| until the port is disconnected.
Fix inconsistent spacing and trailing commas in objects in `web/` files, so we can enable the `comma-dangle` and `object-curly-spacing` ESLint rules later on http://eslint.org/docs/rules/comma-dangle http://eslint.org/docs/rules/object-curly-spacing Given that we currently have quite inconsistent object formatting, fixing this in in *one* big patch probably wouldn't be feasible (since I cannot imagine anyone wanting to review that); hence I've opted to try and do this piecewise instead. *Please note:* This patch was created automatically, using the ESLint `--fix` command line option. In a couple of places this caused lines to become too long, and I've fixed those manually; please refer to the interdiff below for the only hand-edits in this patch. ```diff diff --git a/web/pdf_thumbnail_view.js b/web/pdf_thumbnail_view.js index 002dbf29..1de4e530 100644 --- a/web/pdf_thumbnail_view.js +++ b/web/pdf_thumbnail_view.js @@ -420,8 +420,8 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() { setPageLabel: function PDFThumbnailView_setPageLabel(label) { this.pageLabel = (typeof label === 'string' ? label : null); - this.l10n.get('thumb_page_title', { page: this.pageId, }, 'Page {{page}}'). - then((msg) => { + this.l10n.get('thumb_page_title', { page: this.pageId, }, + 'Page {{page}}').then((msg) => { this.anchor.title = msg; }); diff --git a/web/secondary_toolbar.js b/web/secondary_toolbar.js index 160e0410..6495fc5e 100644 --- a/web/secondary_toolbar.js +++ b/web/secondary_toolbar.js @@ -65,7 +65,8 @@ class SecondaryToolbar { { element: options.printButton, eventName: 'print', close: true, }, { element: options.downloadButton, eventName: 'download', close: true, }, { element: options.viewBookmarkButton, eventName: null, close: true, }, - { element: options.firstPageButton, eventName: 'firstpage', close: true, }, + { element: options.firstPageButton, eventName: 'firstpage', + close: true, }, { element: options.lastPageButton, eventName: 'lastpage', close: true, }, { element: options.pageRotateCwButton, eventName: 'rotatecw', close: false, }, @@ -76,7 +77,7 @@ class SecondaryToolbar { { element: options.cursorHandToolButton, eventName: 'switchcursortool', eventDetails: { tool: CursorTool.HAND, }, close: true, }, { element: options.documentPropertiesButton, - eventName: 'documentproperties', close: true, } + eventName: 'documentproperties', close: true, }, ]; this.items = { firstPage: options.firstPageButton, ```
2017-06-01 19:46:12 +09:00
port = chrome.runtime.connect({ name: "chromecom-referrer" });
}
port.onDisconnect.addListener(onDisconnect);
port.onMessage.addListener(onMessage);
// Initiate the information exchange.
port.postMessage({
referer: window.history.state?.chromecomState,
Fix inconsistent spacing and trailing commas in objects in `web/` files, so we can enable the `comma-dangle` and `object-curly-spacing` ESLint rules later on http://eslint.org/docs/rules/comma-dangle http://eslint.org/docs/rules/object-curly-spacing Given that we currently have quite inconsistent object formatting, fixing this in in *one* big patch probably wouldn't be feasible (since I cannot imagine anyone wanting to review that); hence I've opted to try and do this piecewise instead. *Please note:* This patch was created automatically, using the ESLint `--fix` command line option. In a couple of places this caused lines to become too long, and I've fixed those manually; please refer to the interdiff below for the only hand-edits in this patch. ```diff diff --git a/web/pdf_thumbnail_view.js b/web/pdf_thumbnail_view.js index 002dbf29..1de4e530 100644 --- a/web/pdf_thumbnail_view.js +++ b/web/pdf_thumbnail_view.js @@ -420,8 +420,8 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() { setPageLabel: function PDFThumbnailView_setPageLabel(label) { this.pageLabel = (typeof label === 'string' ? label : null); - this.l10n.get('thumb_page_title', { page: this.pageId, }, 'Page {{page}}'). - then((msg) => { + this.l10n.get('thumb_page_title', { page: this.pageId, }, + 'Page {{page}}').then((msg) => { this.anchor.title = msg; }); diff --git a/web/secondary_toolbar.js b/web/secondary_toolbar.js index 160e0410..6495fc5e 100644 --- a/web/secondary_toolbar.js +++ b/web/secondary_toolbar.js @@ -65,7 +65,8 @@ class SecondaryToolbar { { element: options.printButton, eventName: 'print', close: true, }, { element: options.downloadButton, eventName: 'download', close: true, }, { element: options.viewBookmarkButton, eventName: null, close: true, }, - { element: options.firstPageButton, eventName: 'firstpage', close: true, }, + { element: options.firstPageButton, eventName: 'firstpage', + close: true, }, { element: options.lastPageButton, eventName: 'lastpage', close: true, }, { element: options.pageRotateCwButton, eventName: 'rotatecw', close: false, }, @@ -76,7 +77,7 @@ class SecondaryToolbar { { element: options.cursorHandToolButton, eventName: 'switchcursortool', eventDetails: { tool: CursorTool.HAND, }, close: true, }, { element: options.documentPropertiesButton, - eventName: 'documentproperties', close: true, } + eventName: 'documentproperties', close: true, }, ]; this.items = { firstPage: options.firstPageButton, ```
2017-06-01 19:46:12 +09:00
requestUrl: url,
});
function onMessage(referer) {
if (referer) {
// The background extracts the Referer from the initial HTTP request for
// the PDF file. When the viewer is reloaded or when the user navigates
// back and forward, the background page will not observe a HTTP request
// with Referer. To make sure that the Referer is preserved, store it in
// history.state, which is preserved across reloads/navigations.
const state = window.history.state || {};
state.chromecomState = referer;
window.history.replaceState(state, "");
}
onCompleted();
}
function onDisconnect() {
// When the connection fails, ignore the error and call the callback.
port = null;
callback();
}
function onCompleted() {
port.onDisconnect.removeListener(onDisconnect);
port.onMessage.removeListener(onMessage);
callback();
}
}
// chrome.storage.sync is not supported in every Chromium-derivate.
// Note: The background page takes care of migrating values from
// chrome.storage.local to chrome.storage.sync when needed.
const storageArea = chrome.storage.sync || chrome.storage.local;
class Preferences extends BasePreferences {
async _writeToStorage(prefObj) {
return new Promise(resolve => {
if (prefObj === this.defaults) {
const keysToRemove = Object.keys(this.defaults);
// If the storage is reset, remove the keys so that the values from
// managed storage are applied again.
storageArea.remove(keysToRemove, function () {
resolve();
});
} else {
storageArea.set(prefObj, function () {
resolve();
});
}
});
}
async _readFromStorage(prefObj) {
return new Promise(resolve => {
const getPreferences = defaultPrefs => {
if (chrome.runtime.lastError) {
// Managed storage not supported, e.g. in Opera.
defaultPrefs = this.defaults;
}
storageArea.get(defaultPrefs, function (readPrefs) {
resolve({ prefs: readPrefs });
});
};
if (chrome.storage.managed) {
// Get preferences as set by the system administrator.
// See extensions/chromium/preferences_schema.json for more information.
// These preferences can be overridden by the user.
// Deprecated preferences are removed from web/default_preferences.json,
// but kept in extensions/chromium/preferences_schema.json for backwards
// compatibility with managed preferences.
const defaultManagedPrefs = Object.assign(
{
enableHandToolOnLoad: false,
disableTextLayer: false,
enhanceTextSelection: false,
showPreviousViewOnLoad: true,
disablePageMode: false,
},
this.defaults
);
chrome.storage.managed.get(defaultManagedPrefs, function (items) {
items ||= defaultManagedPrefs;
// Migration logic for deprecated preferences: If the new preference
// is not defined by an administrator (i.e. the value is the same as
// the default value), and a deprecated preference is set with a
// non-default value, migrate the deprecated preference value to the
// new preference value.
// Never remove this, because we have no means of modifying managed
// preferences.
// Migration code for https://github.com/mozilla/pdf.js/pull/7635.
if (items.enableHandToolOnLoad && !items.cursorToolOnLoad) {
items.cursorToolOnLoad = 1;
}
delete items.enableHandToolOnLoad;
// Migration code for https://github.com/mozilla/pdf.js/pull/9479.
if (items.textLayerMode !== 1 && items.disableTextLayer) {
items.textLayerMode = 0;
}
delete items.disableTextLayer;
delete items.enhanceTextSelection;
// Migration code for https://github.com/mozilla/pdf.js/pull/10502.
if (!items.showPreviousViewOnLoad && !items.viewOnLoad) {
items.viewOnLoad = 1;
}
delete items.showPreviousViewOnLoad;
delete items.disablePageMode;
getPreferences(items);
});
} else {
// Managed storage not supported, e.g. in old Chromium versions.
getPreferences(this.defaults);
}
});
}
}
class ExternalServices extends BaseExternalServices {
initPassiveLoading(callbacks) {
// defaultUrl is set in viewer.js
ChromeCom.resolvePDFFile(
AppOptions.get("defaultUrl"),
function (url, length, originalUrl) {
callbacks.onOpenWithURL(url, length, originalUrl);
}
);
}
async createL10n() {
return new GenericL10n(navigator.language);
}
createScripting() {
return new GenericScripting(AppOptions.get("sandboxBundleSrc"));
}
}
class MLManager {
async guess() {
return null;
}
}
export { ExternalServices, initCom, MLManager, Preferences };