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
4 changes: 4 additions & 0 deletions ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
CE8B0BBD248734240045EB2E /* KeymanPackageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8B0BBC248734240045EB2E /* KeymanPackageTests.swift */; };
CE8B0BBF248764ED0045EB2E /* KMPResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8B0BBE248764ED0045EB2E /* KMPResource.swift */; };
CE8B5BB22491DA540075CCB0 /* 13.0 Cloud to Package Migration.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CE8B5BB12491DA530075CCB0 /* 13.0 Cloud to Package Migration.bundle */; };
CE8E6B1E2FEC17B100F5E731 /* WebViewSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8E6B1D2FEC17A900F5E731 /* WebViewSchemeHandler.swift */; };
CE8EDEB123F53D1A009E1FF6 /* FileManagementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A079DD1223194B100581263 /* FileManagementTests.swift */; };
CE8EDEB323F53F96009E1FF6 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */; };
CE969BE8251AD8B500376D6A /* PackageWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE969BE7251AD8B500376D6A /* PackageWebViewController.swift */; };
Expand Down Expand Up @@ -459,6 +460,7 @@
CE8B0BBC248734240045EB2E /* KeymanPackageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeymanPackageTests.swift; sourceTree = "<group>"; };
CE8B0BBE248764ED0045EB2E /* KMPResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMPResource.swift; sourceTree = "<group>"; };
CE8B5BB12491DA530075CCB0 /* 13.0 Cloud to Package Migration.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "13.0 Cloud to Package Migration.bundle"; sourceTree = "<group>"; };
CE8E6B1D2FEC17A900F5E731 /* WebViewSchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewSchemeHandler.swift; sourceTree = "<group>"; };
CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = "<group>"; };
CE969BE7251AD8B500376D6A /* PackageWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageWebViewController.swift; sourceTree = "<group>"; };
CE96E42C24D1229A005B8E5A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
Expand Down Expand Up @@ -804,6 +806,7 @@
CE79B24823C711FF007E72AE /* KeyboardScaleMap.swift */,
C0EF3E7A1F95B65300CE9BD4 /* KeymanWebDelegate.swift */,
C0C16A881FA8146300F090BA /* KeymanWebViewController.swift */,
CE8E6B1D2FEC17A900F5E731 /* WebViewSchemeHandler.swift */,
C0A5FF361F6682EB00BE740C /* PopoverView.swift */,
);
path = Keyboard;
Expand Down Expand Up @@ -1522,6 +1525,7 @@
9A079DCA222E050E00581263 /* LexicalModelKeymanPackage.swift in Sources */,
9A60764422893A4E003BCFBA /* SettingsViewController.swift in Sources */,
C06085B41F9485E40057E5B9 /* UIButton+Helpers.swift in Sources */,
CE8E6B1E2FEC17B100F5E731 /* WebViewSchemeHandler.swift in Sources */,
C0959CD41F99C44E00B616BC /* Constants.swift in Sources */,
C0452BAD1F9F21270064431A /* Keyboard.swift in Sources */,
29B30C232B564F9900C342A4 /* KeymanEngineLogger.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ class KeymanWebViewController: UIViewController {
let storage: Storage
weak var delegate: KeymanWebDelegate?
private var useSpecialFont = false
private var userContentController = WKUserContentController()
private let userContentController = WKUserContentController()
private let schemeHandler: WebViewSchemeHandler
private let keymanWebViewName: String = "keyman"

// Views
Expand Down Expand Up @@ -69,8 +70,9 @@ class KeymanWebViewController: UIViewController {

init(storage: Storage) {
self.storage = storage
self.schemeHandler = WebViewSchemeHandler(storage: storage)

super.init(nibName: nil, bundle: nil)

_ = view
}

Expand Down Expand Up @@ -122,6 +124,7 @@ class KeymanWebViewController: UIViewController {
config.preferences = prefs
config.suppressesIncrementalRendering = false
config.userContentController = self.userContentController
config.setURLSchemeHandler(schemeHandler, forURLScheme: schemeHandler.scheme)

webView = KeymanWebView(frame: CGRect(origin: .zero, size: keyboardSize), configuration: config)
webView!.isOpaque = false
Expand Down Expand Up @@ -279,20 +282,28 @@ extension KeymanWebViewController {
// family does not have to match the name in the font file. It only has to be unique.
return [
"family": "\(keyboard.id)__\(isOsk ? "osk" : "display")",
"files": font.source.map { storage.fontURL(forResource: keyboard, filename: $0)!.absoluteString }
"files": font.source.map {
schemeHandler.buildUrlForFile(
fileURL: storage.fontURL(forResource: keyboard, filename: $0)!
).absoluteString
}
]
}

func setKeyboard(_ keyboard: InstallableKeyboard) throws {
let fileURL = storage.keyboardURL(for: keyboard)
let loadingURL = schemeHandler.buildUrlForFile(
fileURL: storage.keyboardURL(for: keyboard)
)

var stub: [String: Any] = [
"KI": "Keyboard_\(keyboard.id)",
"KN": keyboard.name,
"KLC": keyboard.languageID,
"KL": keyboard.languageName,
"KF": fileURL.absoluteString
"KF": loadingURL.absoluteString
]

if let packageID = keyboard.packageID {
stub["KP"] = packageID
}
Expand Down Expand Up @@ -367,7 +378,7 @@ extension KeymanWebViewController {
let stub: [String: Any] = [
"id": lexicalModel.id,
"languages": [lexicalModel.languageID], // Change when InstallableLexicalModel is updated to store an array
"path": fileURL.absoluteString
"path": schemeHandler.buildUrlForFile(fileURL: fileURL).absoluteString
]

guard FileManager.default.fileExists(atPath: fileURL.path) else {
Expand Down Expand Up @@ -861,7 +872,9 @@ extension KeymanWebViewController {

// MARK: - Show/hide views
func reloadKeyboard() {
webView!.loadFileURL(Storage.active.kmwURL, allowingReadAccessTo: Storage.active.baseDir)
let hostPageFileUrl = URL(fileURLWithPath: Resources.kmwFilename, relativeTo: storage.baseDir)
let hostPageUrl = schemeHandler.buildUrlForFile(fileURL: hostPageFileUrl)
webView!.load(URLRequest(url: hostPageUrl))
isLoading = true

updateSpacebarText()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Joshua Horton on 6/24/26.
*
* WebViewKeyboardLoader implements a URLSchemeHandler that allows
* the hosted Keyman Engine for Web to access all files, consistently,
* via a http-like protocol, preventing CORS access issues for files
* loaded dynamically.
*/
Comment thread
jahorton marked this conversation as resolved.

import WebKit
import UniformTypeIdentifiers
import os.log

func getMimeType(forExtension ext: String) -> String {
// Find the UTType associated with the file extension
if let utType = UTType(filenameExtension: ext) {
// Return the preferred MIME type if it exists
return utType.preferredMIMEType ?? "application/octet-stream"
}
return "application/octet-stream"
}

class WebViewSchemeHandler: NSObject, WKURLSchemeHandler {
let storage: Storage
let scheme = "keyman-engine"

init(storage: Storage) {
self.storage = storage
}

func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) {
guard let url = urlSchemeTask.request.url else {
return
}

var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
components.scheme = "file"

let fileUrl = components.url

let doError = { () -> Void in
let message = "Could not load url via WKURLSchemeHandler: \(url)"
let errorInfo = [
NSLocalizedDescriptionKey: message
]
let error = NSError(domain: "WebViewKeyboardLoader", code: 500, userInfo: errorInfo)

os_log("%{public}s", log:KeymanEngineLogger.settings, type: .error, message)
SentryManager.capture(error, message: message)

urlSchemeTask.didFailWithError(error)
}

guard fileUrl != nil else {
doError()
return
}

do {
let fileContents = try Data(contentsOf: fileUrl!)
let fileExtension = fileUrl!.pathExtension

let mimeType: String = getMimeType(forExtension: fileExtension)
let charset: String = mimeType.hasPrefix("text/") ? "; charset=utf-8" : ""

let response = HTTPURLResponse(
url: url,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: [
"Content-Type": "\(mimeType)\(charset)",
]
)!

urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(fileContents)
urlSchemeTask.didFinish()
} catch {
doError()
return
}
}

func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) {
}

func buildUrlForFile(fileURL: URL) -> URL {
var loadingURLBuilder = URLComponents()
loadingURLBuilder.scheme = scheme
loadingURLBuilder.path = fileURL.path
return loadingURLBuilder.url!
}
}
30 changes: 13 additions & 17 deletions web/src/engine/src/interfaces/pathConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,12 @@ export class PathConfiguration implements OSKResourcePathConfiguration {
private _fonts: string;
readonly protocol: string;

/*
* Pre-modularization code corresponding to `sourcePath`:
```
// Determine path and protocol of executing script, setting them as
// construction defaults.
//
// This can only be done during load when the active script will be the
// last script loaded. Otherwise the script must be identified by name.
var scripts = document.getElementsByTagName('script');
var ss = scripts[scripts.length-1].src;
var sPath = ss.substr(0,ss.lastIndexOf('/')+1);
```
*/
constructor(pathSpec: Required<PathOptionSpec>, sourcePath: string) {
const sourceURL = new URL(sourcePath);

sourcePath = addDelimiter(sourcePath);
this.sourcePath = sourcePath;
this.protocol = sourcePath.replace(/(.{3,5}:)(.*)/,'$1');
this.protocol = sourceURL.protocol;

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.

Sometimes a little bit of cleanup just makes things so much prettier


this.updateFromOptions(pathSpec);
}
Expand Down Expand Up @@ -75,8 +63,16 @@ export class PathConfiguration implements OSKResourcePathConfiguration {

p = addDelimiter(p);

// Absolute
if((p.replace(/^(http)s?:.*/,'$1') == 'http') || (p.replace(/^(file):.*/,'$1') == 'file')) {
// Absolute - with protocol specified
const protocolList = [
'http:',
'https:',
'file:',
// If using a custom origin (say, hosted in an iOS WebView via WKURLSchemeHandler)
this.protocol
];

if(protocolList.find((protocol) => p.startsWith(protocol))) {
return p;
}

Expand Down
5 changes: 3 additions & 2 deletions web/src/engine/src/osk/views/oskView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export function getResourcePath(config: ViewConfiguration) {
if(config.isEmbedded) {
resourcePathExt = '';
}
return `${config.pathConfig.resources}/${resourcePathExt}`
return `${config.pathConfig.resources}${resourcePathExt}`
}

export abstract class OSKView
Expand Down Expand Up @@ -267,6 +267,7 @@ export abstract class OSKView

for(const sheetFile of OSKView.STYLESHEET_FILES) {
const sheetHref = `${resourcePath}${sheetFile}`;

this.uiStyleSheetManager.linkExternalSheet(sheetHref);
}

Expand Down Expand Up @@ -856,7 +857,7 @@ export abstract class OSKView
isEmbedded: this.config.isEmbedded,
specialFont: {
family: 'SpecialOSK',
files: [`${resourcePath}/keymanweb-osk.ttf`],
files: [`${resourcePath}keymanweb-osk.ttf`],
path: '' // Not actually used.
},
gestureParams: this.config.gestureParams
Expand Down
2 changes: 1 addition & 1 deletion web/src/engine/src/osk/visualKeyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1398,7 +1398,7 @@ export class VisualKeyboard extends EventEmitter<EventMap> implements KeyboardVi
styleSheetManager: null,
specialFont: {
family: 'SpecialOSK',
files: [`${pathConfig.resources}/osk/keymanweb-osk.ttf`],
files: [`${pathConfig.resources}keymanweb-osk.ttf`],
path: '' // Not actually used.
}
});
Expand Down