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
34 changes: 33 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 Down Expand Up @@ -42,6 +42,10 @@ export class KeymanEngine extends KeymanEngineBase<WebviewConfiguration, Context
this.hardKeyboard = new PassthroughKeyboard(config.hardDevice);
}

protected createKeyboardLoader(): DOMKeyboardLoader {
return new DOMKeyboardLoader(this.interface, this.config.applyCacheBusting, this.fetch);
}

async init(options: Required<WebviewInitOptionSpec>) {
const device = new DeviceSpec(
'native',
Expand Down Expand Up @@ -159,4 +163,32 @@ export class KeymanEngine extends KeymanEngineBase<WebviewConfiguration, Context
get context() {
return this.contextManager.activeTextStore;
}

/**
* 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.
*/
private fetch(uri: string): Promise<Response> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to have this designed as a protected function in KeymanEngineBase which we override here, in KeymanEngineBase.ts it would be something like:

protected fetch(uri: string): Promise<Response> {
  throw new Error('not implemented');
}

and in browser/src/KeymanEngine.ts:

protected fetch(uri: string): Promise<Response> {
  return window.fetch(uri);
}

and then this would also be protected.

That moves the responsibility for defining what fetch does into the same place for both WebView and Browser components.

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);
});
};

}
2 changes: 1 addition & 1 deletion web/src/engine/src/keyboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export { KeyMapping } from "./keyMapping.js";
export { type SystemStoreMutationHandler, MutableSystemStore, SystemStore, SystemStoreIDs, type SystemStoreDictionary } from "./systemStore.js";
export { type VariableStores, VariableStoreSerializer } from "./variableStore.js";

export { DOMKeyboardLoader } from './keyboards/loaders/domKeyboardLoader.js';
export { type FetchFunction, DOMKeyboardLoader } from './keyboards/loaders/domKeyboardLoader.js';
export { SyntheticTextStore } from "./syntheticTextStore.js";
export { TextStore } from "./textStore.js";
export { TextStoreLanguageProcessorInterface } from "./textStoreLanguageProcessorInterface.js";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import { KeyboardHarness, MinimalKeymanGlobal } from '../keyboardHarness.js';
import { KeyboardLoaderBase } from '../keyboardLoaderBase.js';
import { KeyboardLoadErrorBuilder } from '../keyboardLoadError.js';

export type FetchFunction = (uri: string) => Promise<Response>;

export class DOMKeyboardLoader extends KeyboardLoaderBase {
public readonly element: HTMLIFrameElement;
private readonly performCacheBusting: boolean;
private readonly fetchKeyboardFunc: FetchFunction;

constructor()
constructor(harness: KeyboardHarness);
constructor(harness: KeyboardHarness, cacheBust?: boolean)
constructor(harness?: KeyboardHarness, cacheBust?: boolean) {
constructor(harness: KeyboardHarness, cacheBust?: boolean, fetchKeyboardFunc?: FetchFunction)
constructor(harness?: KeyboardHarness, cacheBust?: boolean, fetchKeyboardFunc?: FetchFunction) {
if(harness && harness._jsGlobal != window) {
// Copy the String typing over; preserve string extensions!
harness._jsGlobal['String'] = window['String'];
Expand All @@ -27,6 +30,8 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase {
}

this.performCacheBusting = cacheBust || false;
const defaultFetchFunc: FetchFunction = (uri) => fetch(uri);
this.fetchKeyboardFunc = fetchKeyboardFunc ?? defaultFetchFunc;
}

protected async loadKeyboardBlob(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Uint8Array> {
Expand All @@ -36,7 +41,7 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase {

let response: Response;
try {
response = await fetch(uri);
response = await this.fetchKeyboardFunc(uri);
} catch (e) {
throw errorBuilder.keyboardDownloadError(e);
}
Expand Down Expand Up @@ -85,4 +90,4 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase {
f.call(context, script);
}

}
}
8 changes: 6 additions & 2 deletions web/src/engine/src/main/keymanEngineBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ export class KeymanEngineBase<
});
}

protected createKeyboardLoader(): DOMKeyboardLoader {
return new DOMKeyboardLoader(this.interface, this.config.applyCacheBusting);
}

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 +246,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 +602,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.
14 changes: 0 additions & 14 deletions web/src/test/auto/headless/engine/loadKeyboardHelper.ts

This file was deleted.