diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java index 9152f843604..418d23cb8e7 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java @@ -265,6 +265,8 @@ public void initKMKeyboard(final Context context) { clearCache(true); getSettings().setJavaScriptEnabled(true); getSettings().setAllowFileAccess(true); + // allow loading keyboards with a file:// url + getSettings().setAllowFileAccessFromFileURLs(true); // Normally, this would be true to prevent the WebView from accessing the network. // But this needs to false for sending embedded KMW crash reports to Sentry (keymanapp/keyman#3825) diff --git a/web/src/app/browser/src/browserKeyboardLoader.ts b/web/src/app/browser/src/browserKeyboardLoader.ts new file mode 100644 index 00000000000..f5f8cc57e5d --- /dev/null +++ b/web/src/app/browser/src/browserKeyboardLoader.ts @@ -0,0 +1,15 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { DOMKeyboardLoader, KeyboardHarness } from 'keyman/engine/keyboard'; + +export class BrowserKeyboardLoader extends DOMKeyboardLoader { + constructor(harness: KeyboardHarness, cacheBust: boolean) { + super(harness, cacheBust); + } + + protected fetch(uri: string): Promise { + return window.fetch(uri); + } +} diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index af3bae2d207..12b29ac6b62 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -8,7 +8,7 @@ import { } from 'keyman/engine/osk'; import { ErrorStub, KeyboardStub, CloudQueryResult, toPrefixedKeyboardId } from 'keyman/engine/keyboard-storage'; import { DeviceSpec } from 'keyman/common/web-utils'; -import { JSKeyboard, Keyboard } from "keyman/engine/keyboard"; +import { DOMKeyboardLoader, JSKeyboard, Keyboard } from "keyman/engine/keyboard"; import KeyboardObject = KeymanWebKeyboard.KeyboardObject; import * as views from './viewsAnchorpoint.js'; @@ -30,6 +30,7 @@ import { BeepHandler } from './beepHandler.js'; import { KeyboardInterface } from './keyboardInterface.js'; import { WorkerFactory } from '@keymanapp/lexical-model-layer/web'; import { KeyboardDetails } from './keyboardDetails.js'; +import { BrowserKeyboardLoader } from './browserKeyboardLoader.js'; export class KeymanEngine extends KeymanEngineBase { touchLanguageMenu?: LanguageMenu; @@ -720,4 +721,8 @@ export class KeymanEngine extends KeymanEngineBase { // Ideally, we would be able to auto-detect `sourceUri`: https://stackoverflow.com/a/60244278. @@ -159,4 +160,8 @@ export class KeymanEngine extends KeymanEngineBase { + return new Promise(function (resolve, reject) { + const httpRequest = new XMLHttpRequest(); + httpRequest.onload = function () { + resolve(new Response(httpRequest.response, { status: httpRequest.status })); + }; + httpRequest.onerror = (e) => { + reject(e); + }; + httpRequest.open('GET', uri); + httpRequest.responseType = "arraybuffer"; + httpRequest.send(null); + }); + }; + +} diff --git a/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts b/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts index 6b961ac831a..d4d8e126f8a 100644 --- a/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts +++ b/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts @@ -7,7 +7,7 @@ import { KeyboardHarness, MinimalKeymanGlobal } from '../keyboardHarness.js'; import { KeyboardLoaderBase } from '../keyboardLoaderBase.js'; import { KeyboardLoadErrorBuilder } from '../keyboardLoadError.js'; -export class DOMKeyboardLoader extends KeyboardLoaderBase { +export abstract class DOMKeyboardLoader extends KeyboardLoaderBase { public readonly element: HTMLIFrameElement; private readonly performCacheBusting: boolean; @@ -36,7 +36,7 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase { let response: Response; try { - response = await fetch(uri); + response = await this.fetch(uri); } catch (e) { throw errorBuilder.keyboardDownloadError(e); } @@ -85,4 +85,5 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase { f.call(context, script); } -} \ No newline at end of file + protected abstract fetch(uri: string): Promise; +} diff --git a/web/src/engine/src/main/keymanEngineBase.ts b/web/src/engine/src/main/keymanEngineBase.ts index bdb43e05ead..d12b91c27f9 100644 --- a/web/src/engine/src/main/keymanEngineBase.ts +++ b/web/src/engine/src/main/keymanEngineBase.ts @@ -35,7 +35,7 @@ function determineBaseLayout(): string { export type KeyEventFullResultCallback = (result: ProcessorAction, error?: Error) => void; export type KeyEventFullHandler = (event: KeyEvent, callback?: KeyEventFullResultCallback) => void; -export class KeymanEngineBase< +export abstract class KeymanEngineBase< ConfigurationT extends EngineConfiguration, ContextManagerT extends ContextManagerBase, HardKeyboardT extends HardKeyboardBase @@ -225,6 +225,8 @@ export class KeymanEngineBase< }); } + protected abstract createKeyboardLoader(): DOMKeyboardLoader; + public async init(optionSpec: Required){ // There may be some valid mutations possible even on repeated calls? // The original seems to allow it. @@ -242,7 +244,7 @@ export class KeymanEngineBase< // Since we're not sandboxing keyboard loads yet, we just use `window` as the jsGlobal object. // All components initialized below require a properly-configured `config.paths` or similar. - const keyboardLoader = new DOMKeyboardLoader(this.interface, config.applyCacheBusting); + const keyboardLoader = this.createKeyboardLoader(); this.keyboardRequisitioner = new KeyboardRequisitioner(keyboardLoader, new DOMCloudRequester(), this.config.paths); this.modelCache = new ModelCache(); const kbdCache = this.keyboardRequisitioner.cache; @@ -598,4 +600,4 @@ export class KeymanEngineBase< }; } -// Intent: define common behaviors for both primary app types; each then subclasses & extends where needed. \ No newline at end of file +// Intent: define common behaviors for both primary app types; each then subclasses & extends where needed. diff --git a/web/src/test/auto/dom/cases/browser/contextManager.tests.ts b/web/src/test/auto/dom/cases/browser/contextManager.tests.ts index ce96c2a3561..231be62ab6b 100644 --- a/web/src/test/auto/dom/cases/browser/contextManager.tests.ts +++ b/web/src/test/auto/dom/cases/browser/contextManager.tests.ts @@ -5,7 +5,7 @@ import { LegacyEventEmitter } from 'keyman/engine/events'; import { StubAndKeyboardCache, toPrefixedKeyboardId as prefixed } from 'keyman/engine/keyboard-storage'; import { KeyboardInterfaceBase } from 'keyman/engine/main'; -import { KeyboardHarness, MinimalKeymanGlobal, DOMKeyboardLoader } from 'keyman/engine/keyboard'; +import { KeyboardHarness, MinimalKeymanGlobal } from 'keyman/engine/keyboard'; import { KeyboardMap, loadKeyboardsFromStubs } from '../../kbdLoader.js'; import { DeviceSpec, ManagedPromise, timedPromise } from 'keyman/common/web-utils'; @@ -13,6 +13,7 @@ import { DEFAULT_BROWSER_TIMEOUT } from '@keymanapp/common-test-resources/test-t import sinon from 'sinon'; import { assert } from 'chai'; +import { TestingDOMKeyboardLoader } from '../../test_utils.js'; const TEST_PHYSICAL_DEVICE = { formFactor: 'desktop', @@ -192,7 +193,7 @@ describe('app/browser: ContextManager', function () { }, () => new LegacyEventEmitter()); // Needed for the keyboard tests later. - keyboardLoader = new DOMKeyboardLoader(new KeyboardHarness(window, MinimalKeymanGlobal)); + keyboardLoader = new TestingDOMKeyboardLoader(new KeyboardHarness(window, MinimalKeymanGlobal)); keyboardCache = new StubAndKeyboardCache(keyboardLoader); contextManager.configure({ diff --git a/web/src/test/auto/dom/cases/keyboard-storage/keyboardRequisitioner.tests.ts b/web/src/test/auto/dom/cases/keyboard-storage/keyboardRequisitioner.tests.ts index 5ac6f496e1b..8f9e64ae54e 100644 --- a/web/src/test/auto/dom/cases/keyboard-storage/keyboardRequisitioner.tests.ts +++ b/web/src/test/auto/dom/cases/keyboard-storage/keyboardRequisitioner.tests.ts @@ -2,10 +2,10 @@ import { assert } from 'chai'; import sinon from 'sinon'; import { KeyboardHarness, MinimalKeymanGlobal } from 'keyman/engine/keyboard'; -import { DOMKeyboardLoader } from 'keyman/engine/keyboard'; import { PathConfiguration } from 'keyman/engine/interfaces'; import { CloudQueryEngine, KeyboardRequisitioner, type KeyboardStub } from 'keyman/engine/keyboard-storage'; import { DOMCloudRequester } from 'keyman/engine/keyboard-storage'; +import { TestingDOMKeyboardLoader } from '../../test_utils.js'; const pathConfig = new PathConfiguration({ root: '', @@ -61,7 +61,7 @@ function mockQuery(querier: CloudQueryEngine, queryResultsFile: string) { describe("KeyboardRequisitioner", function () { it('queries for remote stubs and loads their keyboards', async () => { - const keyboardLoader = new DOMKeyboardLoader(new KeyboardHarness(window, MinimalKeymanGlobal)); + const keyboardLoader = new TestingDOMKeyboardLoader(new KeyboardHarness(window, MinimalKeymanGlobal)); const keyboardRequisitioner = new KeyboardRequisitioner(keyboardLoader, new DOMCloudRequester(true), pathConfig); const cache = keyboardRequisitioner.cache; @@ -83,7 +83,7 @@ describe("KeyboardRequisitioner", function () { }); it('loads keyboards for page-local, API-added stubs', async () => { - const keyboardLoader = new DOMKeyboardLoader(new KeyboardHarness(window, MinimalKeymanGlobal)); + const keyboardLoader = new TestingDOMKeyboardLoader(new KeyboardHarness(window, MinimalKeymanGlobal)); const keyboardRequisitioner = new KeyboardRequisitioner(keyboardLoader, new DOMCloudRequester(true), pathConfig); const cache = keyboardRequisitioner.cache; @@ -108,4 +108,4 @@ describe("KeyboardRequisitioner", function () { assert.strictEqual(cache.getKeyboardForStub(stub), khmer_angkor); assert.isOk(khmer_angkor); }); -}); \ No newline at end of file +}); diff --git a/web/src/test/auto/dom/cases/keyboard/domKeyboardLoader.tests.ts b/web/src/test/auto/dom/cases/keyboard/domKeyboardLoader.tests.ts index e9ea8fe8c0e..55d18fb04c6 100644 --- a/web/src/test/auto/dom/cases/keyboard/domKeyboardLoader.tests.ts +++ b/web/src/test/auto/dom/cases/keyboard/domKeyboardLoader.tests.ts @@ -1,11 +1,11 @@ import { assert } from 'chai'; -import { DOMKeyboardLoader } from 'keyman/engine/keyboard'; import { DeviceSpec } from 'keyman/common/web-utils'; import { KeyboardHarness, JSKeyboard, MinimalKeymanGlobal, KeyboardKeymanGlobal, KeyboardDownloadError, KeyboardScriptError, Keyboard, SyntheticTextStore } from 'keyman/engine/keyboard'; import { JSKeyboardInterface } from 'keyman/engine/js-processor'; import { assertThrowsAsync } from 'keyman/tools/testing/test-utils'; import { VariableStoreTestSerializer } from 'keyman/test/headless-resources'; +import { TestingDOMKeyboardLoader } from '../../test_utils.js'; declare let window: typeof globalThis; // KeymanEngine from the web/ folder... when available. @@ -29,7 +29,7 @@ describe('Keyboard loading in DOM', function() { it('throws error when keyboard does not exist', async () => { const harness = new JSKeyboardInterface(window, MinimalKeymanGlobal, new VariableStoreTestSerializer()); - const keyboardLoader = new DOMKeyboardLoader(harness); + const keyboardLoader = new TestingDOMKeyboardLoader(harness); const nonExisting = '/does/not/exist.js'; await assertThrowsAsync(async () => await keyboardLoader.loadKeyboardFromPath(nonExisting), @@ -38,7 +38,7 @@ describe('Keyboard loading in DOM', function() { it('throws error when keyboard is invalid', async () => { const harness = new JSKeyboardInterface(window, MinimalKeymanGlobal, new VariableStoreTestSerializer()); - const keyboardLoader = new DOMKeyboardLoader(harness); + const keyboardLoader = new TestingDOMKeyboardLoader(harness); const nonKeyboardPath = '/common/test/resources/index.mjs'; await assertThrowsAsync(async () => await keyboardLoader.loadKeyboardFromPath(nonKeyboardPath), @@ -47,7 +47,7 @@ describe('Keyboard loading in DOM', function() { it('`window`, disabled rule processing', async () => { const harness = new KeyboardHarness(window, MinimalKeymanGlobal); - let keyboardLoader = new DOMKeyboardLoader(harness); + let keyboardLoader = new TestingDOMKeyboardLoader(harness); let keyboard: Keyboard = await keyboardLoader.loadKeyboardFromPath('/common/test/resources/keyboards/khmer_angkor.js'); assert.isOk(keyboard); @@ -67,7 +67,7 @@ describe('Keyboard loading in DOM', function() { it('`window`, enabled rule processing', async () => { const jsHarness = new JSKeyboardInterface(window, MinimalKeymanGlobal, new VariableStoreTestSerializer()); - const keyboardLoader = new DOMKeyboardLoader(jsHarness); + const keyboardLoader = new TestingDOMKeyboardLoader(jsHarness); const keyboard: Keyboard = await keyboardLoader.loadKeyboardFromPath('/common/test/resources/keyboards/khmer_angkor.js'); const jsKeyboard = keyboard as JSKeyboard; jsHarness.activeKeyboard = jsKeyboard; @@ -99,7 +99,7 @@ describe('Keyboard loading in DOM', function() { it('load keyboards successfully in parallel without side effects', async () => { let jsHarness = new JSKeyboardInterface(window, MinimalKeymanGlobal, new VariableStoreTestSerializer()); - let keyboardLoader = new DOMKeyboardLoader(jsHarness); + let keyboardLoader = new TestingDOMKeyboardLoader(jsHarness); // Preload a keyboard and make it active. const test_kbd: Keyboard = await keyboardLoader.loadKeyboardFromPath('/common/test/resources/keyboards/test_917.js'); diff --git a/web/src/test/auto/dom/kbdLoader.ts b/web/src/test/auto/dom/kbdLoader.ts index 3c1dd21eef7..fedaac372df 100644 --- a/web/src/test/auto/dom/kbdLoader.ts +++ b/web/src/test/auto/dom/kbdLoader.ts @@ -1,5 +1,4 @@ import { - DOMKeyboardLoader, JSKeyboard, Keyboard, KeyboardProperties, @@ -9,8 +8,9 @@ import { import { JSKeyboardInterface } from 'keyman/engine/js-processor'; import { KeyboardInfoPair } from 'keyman/engine/main'; import { VariableStoreTestSerializer } from 'keyman/test/headless-resources'; +import { TestingDOMKeyboardLoader } from './test_utils.js'; -const loader = new DOMKeyboardLoader(new JSKeyboardInterface(window, MinimalKeymanGlobal, new VariableStoreTestSerializer())); +const loader = new TestingDOMKeyboardLoader(new JSKeyboardInterface(window, MinimalKeymanGlobal, new VariableStoreTestSerializer())); export function loadKeyboardFromPath(path: string) { return loader.loadKeyboardFromPath(path); @@ -59,4 +59,4 @@ export function loadKeyboardsFromStubs(apiStubs: any, baseDir: string) { } return priorPromise.then(() => keyboards); -} \ No newline at end of file +} diff --git a/web/src/test/auto/dom/test_utils.ts b/web/src/test/auto/dom/test_utils.ts index c015988707c..44ebe8b8576 100644 --- a/web/src/test/auto/dom/test_utils.ts +++ b/web/src/test/auto/dom/test_utils.ts @@ -1,6 +1,8 @@ // Defines an object for dynamically adding elements for testing purposes. // Designed for use with the robustAttachment.html fixture. +import { DOMKeyboardLoader, KeyboardHarness } from "keyman/engine/keyboard"; + export class DynamicElements { static inputCounter = 0; @@ -82,3 +84,13 @@ export class DynamicElements { return editable.id; } } + +export class TestingDOMKeyboardLoader extends DOMKeyboardLoader { + constructor(harness: KeyboardHarness, cacheBust?: boolean) { + super(harness, cacheBust); + } + + protected fetch(uri: string): Promise { + return window.fetch(uri); + } +} diff --git a/web/src/test/auto/headless/engine/loadKeyboardHelper.ts b/web/src/test/auto/headless/engine/loadKeyboardHelper.ts deleted file mode 100644 index 5ffaed719e7..00000000000 --- a/web/src/test/auto/headless/engine/loadKeyboardHelper.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Keyman is copyright (C) SIL Global. MIT License. - */ - -import fs from 'node:fs'; - -import { getKeymanRoot } from 'keyman/test/resources'; - -const KEYMAN_ROOT = getKeymanRoot(); - -export function loadKeyboardBlob(filename: string) { - const data = fs.readFileSync(`${KEYMAN_ROOT}${filename}`, null); - return new Uint8Array(data); -}