Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions web/src/app/browser/src/browserKeyboardLoader.ts
Original file line number Diff line number Diff line change
@@ -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<Response> {
return window.fetch(uri);
}
}
7 changes: 6 additions & 1 deletion web/src/app/browser/src/keymanEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<BrowserConfiguration, ContextManager, HardwareEventKeyboard> {
touchLanguageMenu?: LanguageMenu;
Expand Down Expand Up @@ -720,4 +721,8 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
this.legacyAPIEvents.callEvent('unloaduserinterface', {});
this.ui?.shutdown();
}

protected createKeyboardLoader(): DOMKeyboardLoader {
return new BrowserKeyboardLoader(this.interface, this.config.applyCacheBusting);
}
}
7 changes: 6 additions & 1 deletion web/src/app/webview/src/keymanEngine.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DeviceSpec } from 'keyman/common/web-utils';
import { DefaultOutputRules, ProcessorAction } from 'keyman/engine/keyboard';
import { DefaultOutputRules, DOMKeyboardLoader, ProcessorAction } from 'keyman/engine/keyboard';
import { KeymanEngineBase, KeyboardInterfaceBase } from 'keyman/engine/main';
import { AnchoredOSKView, ViewConfiguration, StaticActivator } from 'keyman/engine/osk';
import { getAbsoluteX, getAbsoluteY } from 'keyman/engine/dom-utils';
Expand All @@ -10,6 +10,7 @@ import { ContextManager, HostTextStore } from './contextManager.js';
import { PassthroughKeyboard } from './passthroughKeyboard.js';
import { buildEmbeddedGestureConfig, setupEmbeddedListeners } from './oskConfiguration.js';
import { WorkerFactory } from '@keymanapp/lexical-model-layer';
import { WebviewKeyboardLoader } from './webviewKeyboardLoader.js';

export class KeymanEngine extends KeymanEngineBase<WebviewConfiguration, ContextManager, PassthroughKeyboard> {
// Ideally, we would be able to auto-detect `sourceUri`: https://stackoverflow.com/a/60244278.
Expand Down Expand Up @@ -159,4 +160,8 @@ export class KeymanEngine extends KeymanEngineBase<WebviewConfiguration, Context
get context() {
return this.contextManager.activeTextStore;
}

protected createKeyboardLoader(): DOMKeyboardLoader {
return new WebviewKeyboardLoader(this.interface, this.config.applyCacheBusting);
}
}
39 changes: 39 additions & 0 deletions web/src/app/webview/src/webviewKeyboardLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/

import { DOMKeyboardLoader, KeyboardHarness } from 'keyman/engine/keyboard';

export class WebviewKeyboardLoader extends DOMKeyboardLoader {
constructor(harness: KeyboardHarness, cacheBust: boolean) {
super(harness, cacheBust);
}

/**
* Fetches a resource from the specified URL.
*
* @param uri
* @returns A response promise
*
* @see https://stackoverflow.com/a/63582110
*
* Note: Using XMLHttpRequest allows us to work around the limitations of
* Fetch API's fetch() which doesn't support file:// URLs. At least in
* Keyman for Android we still have to explicitly allow file URLs.
*/
protected fetch(uri: string): Promise<Response> {
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);
});
};

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -85,4 +85,5 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase {
f.call(context, script);
}

}
protected abstract fetch(uri: string): Promise<Response>;
}
8 changes: 5 additions & 3 deletions web/src/engine/src/main/keymanEngineBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>,
HardKeyboardT extends HardKeyboardBase
Expand Down Expand Up @@ -225,6 +225,8 @@ export class KeymanEngineBase<
});
}

protected abstract createKeyboardLoader(): DOMKeyboardLoader;

public async init(optionSpec: Required<InitOptionSpec>){
// There may be some valid mutations possible even on repeated calls?
// The original seems to allow it.
Expand All @@ -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;
Expand Down Expand Up @@ -598,4 +600,4 @@ export class KeymanEngineBase<
};
}

// Intent: define common behaviors for both primary app types; each then subclasses & extends where needed.
// Intent: define common behaviors for both primary app types; each then subclasses & extends where needed.
5 changes: 3 additions & 2 deletions web/src/test/auto/dom/cases/browser/contextManager.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ 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';
import { DEFAULT_BROWSER_TIMEOUT } from '@keymanapp/common-test-resources/test-timeouts.mjs';
import sinon from 'sinon';

import { assert } from 'chai';
import { TestingDOMKeyboardLoader } from '../../test_utils.js';

const TEST_PHYSICAL_DEVICE = {
formFactor: 'desktop',
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: '',
Expand Down Expand Up @@ -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;

Expand All @@ -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;

Expand All @@ -108,4 +108,4 @@ describe("KeyboardRequisitioner", function () {
assert.strictEqual(cache.getKeyboardForStub(stub), khmer_angkor);
assert.isOk(khmer_angkor);
});
});
});
12 changes: 6 additions & 6 deletions web/src/test/auto/dom/cases/keyboard/domKeyboardLoader.tests.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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');
Expand Down
6 changes: 3 additions & 3 deletions web/src/test/auto/dom/kbdLoader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
DOMKeyboardLoader,
JSKeyboard,
Keyboard,
KeyboardProperties,
Expand All @@ -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);
Expand Down Expand Up @@ -59,4 +59,4 @@ export function loadKeyboardsFromStubs(apiStubs: any, baseDir: string) {
}

return priorPromise.then(() => keyboards);
}
}
12 changes: 12 additions & 0 deletions web/src/test/auto/dom/test_utils.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<Response> {
return window.fetch(uri);
}
}
14 changes: 0 additions & 14 deletions web/src/test/auto/headless/engine/loadKeyboardHelper.ts

This file was deleted.