mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 22:20:51 +01:00
539 lines
17 KiB
TypeScript
539 lines
17 KiB
TypeScript
// composables/extensionMessageHandler.ts
|
|
import { invoke } from '@tauri-apps/api/core'
|
|
import type { IHaexHubExtension } from '~/types/haexhub'
|
|
import {
|
|
EXTENSION_PROTOCOL_NAME,
|
|
EXTENSION_PROTOCOL_PREFIX,
|
|
} from '~/config/constants'
|
|
import type { Platform } from '@tauri-apps/plugin-os'
|
|
|
|
interface ExtensionRequest {
|
|
id: string
|
|
method: string
|
|
params: Record<string, unknown>
|
|
timestamp: number
|
|
}
|
|
|
|
// Globaler Handler - nur einmal registriert
|
|
let globalHandlerRegistered = false
|
|
interface ExtensionInstance {
|
|
extension: IHaexHubExtension
|
|
windowId: string
|
|
}
|
|
const iframeRegistry = new Map<HTMLIFrameElement, ExtensionInstance>()
|
|
// Map event.source (WindowProxy) to extension instance for sandbox-compatible matching
|
|
const sourceRegistry = new Map<Window, ExtensionInstance>()
|
|
// Reverse map: window ID to Window for broadcasting (supports multiple windows per extension)
|
|
const windowIdToWindowMap = new Map<string, Window>()
|
|
|
|
// Store context values that need to be accessed outside setup
|
|
let contextGetters: {
|
|
getTheme: () => string
|
|
getLocale: () => string
|
|
getPlatform: () => Platform | undefined
|
|
} | null = null
|
|
|
|
const registerGlobalMessageHandler = () => {
|
|
if (globalHandlerRegistered) return
|
|
|
|
window.addEventListener('message', async (event: MessageEvent) => {
|
|
// Ignore console.forward messages - they're handled elsewhere
|
|
if (event.data?.type === 'console.forward') {
|
|
return
|
|
}
|
|
|
|
const request = event.data as ExtensionRequest
|
|
|
|
// Find extension instance by decoding event.origin (works with sandboxed iframes)
|
|
// Origin formats:
|
|
// - Desktop: haex-extension://<base64>
|
|
// - Android: http://haex-extension.localhost (need to check request URL for base64)
|
|
let instance: ExtensionInstance | undefined
|
|
|
|
// Debug: Find which extension sent this message
|
|
let sourceInfo = 'unknown source'
|
|
for (const [iframe, inst] of iframeRegistry.entries()) {
|
|
if (iframe.contentWindow === event.source) {
|
|
sourceInfo = `${inst.extension.name} (${inst.windowId})`
|
|
break
|
|
}
|
|
}
|
|
console.log(
|
|
'[ExtensionHandler] Received message from:',
|
|
sourceInfo,
|
|
'Method:',
|
|
request.method,
|
|
)
|
|
|
|
// Try to decode extension info from origin
|
|
if (event.origin) {
|
|
let base64Host: string | null = null
|
|
|
|
if (event.origin.startsWith(EXTENSION_PROTOCOL_PREFIX)) {
|
|
// Desktop format: haex-extension://<base64>
|
|
base64Host = event.origin.replace(EXTENSION_PROTOCOL_PREFIX, '')
|
|
console.log(
|
|
'[ExtensionHandler] Extracted base64 (custom protocol):',
|
|
base64Host,
|
|
)
|
|
} else if (
|
|
event.origin === `http://${EXTENSION_PROTOCOL_NAME}.localhost`
|
|
) {
|
|
// Android format: http://haex-extension.localhost/{base64} (origin doesn't contain extension info)
|
|
// We need to identify extension by iframe source or fallback to single-extension mode
|
|
console.log(
|
|
`[ExtensionHandler] Android format detected (http://${EXTENSION_PROTOCOL_NAME}.localhost)`,
|
|
)
|
|
// Fallback to single iframe mode
|
|
if (iframeRegistry.size === 1) {
|
|
const entry = Array.from(iframeRegistry.entries())[0]
|
|
if (entry) {
|
|
const [_, inst] = entry
|
|
instance = inst
|
|
sourceRegistry.set(event.source as Window, inst)
|
|
windowIdToWindowMap.set(inst.windowId, event.source as Window)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (base64Host && base64Host !== 'localhost') {
|
|
try {
|
|
const decodedInfo = JSON.parse(atob(base64Host)) as {
|
|
name: string
|
|
publicKey: string
|
|
version: string
|
|
}
|
|
|
|
// Find matching extension in registry
|
|
for (const [_, inst] of iframeRegistry.entries()) {
|
|
if (
|
|
inst.extension.name === decodedInfo.name &&
|
|
inst.extension.publicKey === decodedInfo.publicKey &&
|
|
inst.extension.version === decodedInfo.version
|
|
) {
|
|
instance = inst
|
|
// Register for future lookups
|
|
sourceRegistry.set(event.source as Window, inst)
|
|
windowIdToWindowMap.set(inst.windowId, event.source as Window)
|
|
break
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('[ExtensionHandler] Failed to decode origin:', e)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: Try to find extension instance by event.source (for localhost origin or legacy)
|
|
if (!instance) {
|
|
instance = sourceRegistry.get(event.source as Window)
|
|
|
|
// If not registered yet, find by matching iframe.contentWindow to event.source
|
|
if (!instance) {
|
|
for (const [iframe, inst] of iframeRegistry.entries()) {
|
|
if (iframe.contentWindow === event.source) {
|
|
instance = inst
|
|
// Register for future lookups
|
|
sourceRegistry.set(event.source as Window, inst)
|
|
windowIdToWindowMap.set(inst.windowId, event.source as Window)
|
|
console.log(
|
|
'[ExtensionHandler] Registered instance via contentWindow match:',
|
|
inst.windowId,
|
|
)
|
|
break
|
|
}
|
|
}
|
|
} else if (instance && !windowIdToWindowMap.has(instance.windowId)) {
|
|
// Also register in reverse map for broadcasting
|
|
windowIdToWindowMap.set(instance.windowId, event.source as Window)
|
|
}
|
|
}
|
|
|
|
if (!instance) {
|
|
console.warn(
|
|
'[ExtensionHandler] Could not identify extension instance from event.source.',
|
|
'Registered iframes:',
|
|
iframeRegistry.size,
|
|
)
|
|
return // Message ist nicht von einem registrierten IFrame
|
|
}
|
|
|
|
if (!request.id || !request.method) {
|
|
console.error('[ExtensionHandler] Invalid extension request:', request)
|
|
return
|
|
}
|
|
|
|
try {
|
|
let result: unknown
|
|
|
|
if (request.method.startsWith('haextension.context.')) {
|
|
result = await handleContextMethodAsync(request)
|
|
} else if (request.method.startsWith('haextension.storage.')) {
|
|
result = await handleStorageMethodAsync(request, instance)
|
|
} else if (request.method.startsWith('haextension.db.')) {
|
|
result = await handleDatabaseMethodAsync(request, instance.extension)
|
|
} else if (request.method.startsWith('haextension.fs.')) {
|
|
result = await handleFilesystemMethodAsync(request, instance.extension)
|
|
} else if (request.method.startsWith('haextension.http.')) {
|
|
result = await handleHttpMethodAsync(request, instance.extension)
|
|
} else if (request.method.startsWith('haextension.permissions.')) {
|
|
result = await handlePermissionsMethodAsync(request, instance.extension)
|
|
} else {
|
|
throw new Error(`Unknown method: ${request.method}`)
|
|
}
|
|
|
|
// Use event.source instead of contentWindow to work with sandboxed iframes
|
|
// For sandboxed iframes, event.origin is "null" (string), which is not valid for postMessage
|
|
const targetOrigin = event.origin === 'null' ? '*' : event.origin || '*'
|
|
|
|
;(event.source as Window)?.postMessage(
|
|
{
|
|
id: request.id,
|
|
result,
|
|
},
|
|
targetOrigin,
|
|
)
|
|
} catch (error) {
|
|
console.error('[ExtensionHandler] Extension request error:', error)
|
|
|
|
// Use event.source instead of contentWindow to work with sandboxed iframes
|
|
// For sandboxed iframes, event.origin is "null" (string), which is not valid for postMessage
|
|
const targetOrigin = event.origin === 'null' ? '*' : event.origin || '*'
|
|
|
|
;(event.source as Window)?.postMessage(
|
|
{
|
|
id: request.id,
|
|
error: {
|
|
code: 'INTERNAL_ERROR',
|
|
message: error instanceof Error ? error.message : 'Unknown error',
|
|
details: error,
|
|
},
|
|
},
|
|
targetOrigin,
|
|
)
|
|
}
|
|
})
|
|
|
|
globalHandlerRegistered = true
|
|
}
|
|
|
|
export const useExtensionMessageHandler = (
|
|
iframeRef: Ref<HTMLIFrameElement | undefined | null>,
|
|
extension: ComputedRef<IHaexHubExtension | undefined | null>,
|
|
windowId: Ref<string>,
|
|
) => {
|
|
// Initialize context getters (can use composables here because we're in setup)
|
|
const { currentTheme } = storeToRefs(useUiStore())
|
|
const { locale } = useI18n()
|
|
const { platform } = useDeviceStore()
|
|
// Store getters for use outside setup context
|
|
if (!contextGetters) {
|
|
contextGetters = {
|
|
getTheme: () => currentTheme.value?.value || 'system',
|
|
getLocale: () => locale.value,
|
|
getPlatform: () => platform,
|
|
}
|
|
}
|
|
|
|
// Registriere globalen Handler beim ersten Aufruf
|
|
registerGlobalMessageHandler()
|
|
|
|
// Registriere dieses IFrame
|
|
watchEffect(() => {
|
|
if (iframeRef.value && extension.value) {
|
|
iframeRegistry.set(iframeRef.value, {
|
|
extension: extension.value,
|
|
windowId: windowId.value,
|
|
})
|
|
}
|
|
})
|
|
|
|
// Cleanup beim Unmount
|
|
onUnmounted(() => {
|
|
if (iframeRef.value) {
|
|
const instance = iframeRegistry.get(iframeRef.value)
|
|
if (instance) {
|
|
// Remove from all maps
|
|
windowIdToWindowMap.delete(instance.windowId)
|
|
for (const [source, inst] of sourceRegistry.entries()) {
|
|
if (inst.windowId === instance.windowId) {
|
|
sourceRegistry.delete(source)
|
|
}
|
|
}
|
|
}
|
|
iframeRegistry.delete(iframeRef.value)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Export Funktion für manuelle IFrame-Registrierung (kein Composable!)
|
|
export const registerExtensionIFrame = (
|
|
iframe: HTMLIFrameElement,
|
|
extension: IHaexHubExtension,
|
|
windowId: string,
|
|
) => {
|
|
// Stelle sicher, dass der globale Handler registriert ist
|
|
registerGlobalMessageHandler()
|
|
|
|
// Warnung wenn Context Getters nicht initialisiert wurden
|
|
if (!contextGetters) {
|
|
console.warn(
|
|
'Context getters not initialized. Make sure useExtensionMessageHandler was called in setup context first.',
|
|
)
|
|
}
|
|
|
|
iframeRegistry.set(iframe, { extension, windowId })
|
|
}
|
|
|
|
export const unregisterExtensionIFrame = (iframe: HTMLIFrameElement) => {
|
|
// Also remove from source registry and instance map
|
|
const instance = iframeRegistry.get(iframe)
|
|
if (instance) {
|
|
// Find and remove all sources pointing to this instance
|
|
for (const [source, inst] of sourceRegistry.entries()) {
|
|
if (inst.windowId === instance.windowId) {
|
|
sourceRegistry.delete(source)
|
|
}
|
|
}
|
|
// Remove from instance-to-window map
|
|
windowIdToWindowMap.delete(instance.windowId)
|
|
}
|
|
iframeRegistry.delete(iframe)
|
|
}
|
|
|
|
// Export function to get Window for a specific instance (for broadcasting)
|
|
export const getInstanceWindow = (windowId: string): Window | undefined => {
|
|
return windowIdToWindowMap.get(windowId)
|
|
}
|
|
|
|
// Get all windows for an extension (all instances)
|
|
export const getAllInstanceWindows = (extensionId: string): Window[] => {
|
|
const windows: Window[] = []
|
|
for (const [_, instance] of iframeRegistry.entries()) {
|
|
if (instance.extension.id === extensionId) {
|
|
const win = windowIdToWindowMap.get(instance.windowId)
|
|
if (win) {
|
|
windows.push(win)
|
|
}
|
|
}
|
|
}
|
|
return windows
|
|
}
|
|
|
|
// Deprecated - kept for backwards compatibility
|
|
export const getExtensionWindow = (extensionId: string): Window | undefined => {
|
|
// Return first window for this extension
|
|
return getAllInstanceWindows(extensionId)[0]
|
|
}
|
|
|
|
// Broadcast context changes to all extension instances
|
|
export const broadcastContextToAllExtensions = (context: {
|
|
theme: string
|
|
locale: string
|
|
platform?: string
|
|
}) => {
|
|
const message = {
|
|
type: 'haextension.context.changed',
|
|
data: { context },
|
|
timestamp: Date.now(),
|
|
}
|
|
|
|
console.log('[ExtensionHandler] Broadcasting context to all extensions:', context)
|
|
|
|
// Send to all registered extension windows
|
|
for (const [_, instance] of iframeRegistry.entries()) {
|
|
const win = windowIdToWindowMap.get(instance.windowId)
|
|
if (win) {
|
|
console.log('[ExtensionHandler] Sending context to:', instance.extension.name, instance.windowId)
|
|
win.postMessage(message, '*')
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// Database Methods
|
|
// ==========================================
|
|
|
|
async function handleDatabaseMethodAsync(
|
|
request: ExtensionRequest,
|
|
extension: IHaexHubExtension, // Direkter Typ
|
|
) {
|
|
const params = request.params as {
|
|
query?: string
|
|
params?: unknown[]
|
|
}
|
|
|
|
switch (request.method) {
|
|
case 'haextension.db.query': {
|
|
const rows = await invoke<unknown[]>('extension_sql_select', {
|
|
sql: params.query || '',
|
|
params: params.params || [],
|
|
publicKey: extension.publicKey,
|
|
name: extension.name,
|
|
})
|
|
|
|
return {
|
|
rows,
|
|
rowsAffected: 0,
|
|
lastInsertId: undefined,
|
|
}
|
|
}
|
|
|
|
case 'haextension.db.execute': {
|
|
const rows = await invoke<unknown[]>('extension_sql_execute', {
|
|
sql: params.query || '',
|
|
params: params.params || [],
|
|
publicKey: extension.publicKey,
|
|
name: extension.name,
|
|
})
|
|
|
|
return {
|
|
rows,
|
|
rowsAffected: 1,
|
|
lastInsertId: undefined,
|
|
}
|
|
}
|
|
|
|
case 'haextension.db.transaction': {
|
|
const statements =
|
|
(request.params as { statements?: string[] }).statements || []
|
|
|
|
for (const stmt of statements) {
|
|
await invoke('extension_sql_execute', {
|
|
sql: stmt,
|
|
params: [],
|
|
publicKey: extension.publicKey,
|
|
name: extension.name,
|
|
})
|
|
}
|
|
|
|
return { success: true }
|
|
}
|
|
|
|
default:
|
|
throw new Error(`Unknown database method: ${request.method}`)
|
|
}
|
|
}
|
|
// ==========================================
|
|
// Filesystem Methods (TODO)
|
|
// ==========================================
|
|
|
|
async function handleFilesystemMethodAsync(
|
|
request: ExtensionRequest,
|
|
extension: IHaexHubExtension,
|
|
) {
|
|
if (!request || !extension) return
|
|
// TODO: Implementiere Filesystem Commands im Backend
|
|
throw new Error('Filesystem methods not yet implemented')
|
|
}
|
|
|
|
// ==========================================
|
|
// HTTP Methods (TODO)
|
|
// ==========================================
|
|
|
|
async function handleHttpMethodAsync(
|
|
request: ExtensionRequest,
|
|
extension: IHaexHubExtension,
|
|
) {
|
|
if (!extension || !request) {
|
|
throw new Error('Extension not found')
|
|
}
|
|
|
|
// TODO: Implementiere HTTP Commands im Backend
|
|
throw new Error('HTTP methods not yet implemented')
|
|
}
|
|
|
|
// ==========================================
|
|
// Permission Methods (TODO)
|
|
// ==========================================
|
|
|
|
async function handlePermissionsMethodAsync(
|
|
request: ExtensionRequest,
|
|
extension: IHaexHubExtension,
|
|
) {
|
|
if (!extension || !request) {
|
|
throw new Error('Extension not found')
|
|
}
|
|
|
|
// TODO: Implementiere Permission Request UI
|
|
throw new Error('Permission methods not yet implemented')
|
|
}
|
|
|
|
// ==========================================
|
|
// Context Methods
|
|
// ==========================================
|
|
|
|
async function handleContextMethodAsync(request: ExtensionRequest) {
|
|
switch (request.method) {
|
|
case 'haextension.context.get':
|
|
if (!contextGetters) {
|
|
throw new Error(
|
|
'Context not initialized. Make sure useExtensionMessageHandler is called in a component.',
|
|
)
|
|
}
|
|
return {
|
|
theme: contextGetters.getTheme(),
|
|
locale: contextGetters.getLocale(),
|
|
platform: contextGetters.getPlatform(),
|
|
}
|
|
|
|
default:
|
|
throw new Error(`Unknown context method: ${request.method}`)
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// Storage Methods
|
|
// ==========================================
|
|
|
|
async function handleStorageMethodAsync(
|
|
request: ExtensionRequest,
|
|
instance: ExtensionInstance,
|
|
) {
|
|
// Storage is now per-window, not per-extension
|
|
const storageKey = `ext_${instance.extension.id}_${instance.windowId}_`
|
|
console.log(
|
|
`[HaexHub Storage] ${request.method} for window ${instance.windowId}`,
|
|
)
|
|
|
|
switch (request.method) {
|
|
case 'haextension.storage.getItem': {
|
|
const key = request.params.key as string
|
|
return localStorage.getItem(storageKey + key)
|
|
}
|
|
|
|
case 'haextension.storage.setItem': {
|
|
const key = request.params.key as string
|
|
const value = request.params.value as string
|
|
localStorage.setItem(storageKey + key, value)
|
|
return null
|
|
}
|
|
|
|
case 'haextension.storage.removeItem': {
|
|
const key = request.params.key as string
|
|
localStorage.removeItem(storageKey + key)
|
|
return null
|
|
}
|
|
|
|
case 'haextension.storage.clear': {
|
|
// Remove only instance-specific keys
|
|
const keys = Object.keys(localStorage).filter((k) =>
|
|
k.startsWith(storageKey),
|
|
)
|
|
keys.forEach((k) => localStorage.removeItem(k))
|
|
return null
|
|
}
|
|
|
|
case 'haextension.storage.keys': {
|
|
// Return only instance-specific keys (without prefix)
|
|
const keys = Object.keys(localStorage)
|
|
.filter((k) => k.startsWith(storageKey))
|
|
.map((k) => k.substring(storageKey.length))
|
|
return keys
|
|
}
|
|
|
|
default:
|
|
throw new Error(`Unknown storage method: ${request.method}`)
|
|
}
|
|
}
|