Files
system-taskbar/ding/app/widgetWebContext.js

1043 lines
31 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* DING: Desktop Icons New Generation for GNOME Shell
*
* Gtk4 Port Copyright (C) 2022 - 2025 Sundeep Mediratta (smedius@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {Gdk, Gio, GLib, Gtk, Soup, WebKit} from '../dependencies/gi.js';
import {_} from '../dependencies/gettext.js';
import {HtmlWidgetHost, WidgetApi} from '../dependencies/localFiles.js';
export {WebWidgetContext};
/**
* WebWidgetContext
*
* Single runtime for all HTML widgets:
* - Owns shared WebKit.WebContext and WebKit.UserContentManager.
* - Injects WIDGET_API (window.ding) into all frames.
* - Receives script messages ("dingWidget") and parses JSON payloads.
* - Delegates semantics to WidgetManager (config, host state, prefs).
*
* Lifetime:
* - Created lazily by WidgetManager when the first HTML widget is created.
* - Destroyed explicitly by WidgetManager when the last HTML widget is removed.
*/
const WebWidgetContext = class {
constructor(desktopManager, widgetManager) {
this._desktopManager = desktopManager;
this._widgetManager = widgetManager;
this._prefs = desktopManager.Prefs;
this.Enums = desktopManager.Enums;
this._mainApp = desktopManager.mainApp;
this._desktopIconsUtil = desktopManager.DesktopIconsUtil;
this._webContext = null;
this._userContentManager = null;
this._networkSession = null;
this._scriptHandlerId = 0;
this._cspString = null;
this._prefsWindow = null;
this._prefsWebView = null;
this._prefsInstanceId = null;
this._instanceRoots = new Map();
this._setCspString();
}
// ---------------------------------------------------------------------
// Public WebKit runtime access
// ---------------------------------------------------------------------
get webContext() {
this._initWebKitRuntime();
return this._webContext;
}
get userContentManager() {
this._initWebKitRuntime();
return this._userContentManager;
}
destroy() {
this.closePreferencesIfAny();
if (this._userContentManager && this._scriptHandlerId) {
this._userContentManager.disconnect(this._scriptHandlerId);
this._scriptHandlerId = 0;
}
if (this._userContentManager) {
this._userContentManager.unregister_script_message_handler(
'dingWidget',
null
);
}
this._userContentManager = null;
this._webContext = null;
}
/*
* Create a WebView for a widget instance and bind its FS root.
*
* @param {string} widgetId - logical widget ID (e.g. 'weather')
* @param {string} instanceId - UUID-like instance ID
* @param {Gio.File} rootDir - widget bundle root directory
*/
async newViewForInstance(widgetId, instanceId) {
// Ensure runtime is set up before constructing a view.
this._initWebKitRuntime();
const webViewOptions = {
web_context: this._webContext,
user_content_manager: this._userContentManager,
network_session: this._networkSession,
};
const webView = new WebKit.WebView(webViewOptions);
const rootDir = await this._getInstanceRoot(instanceId);
// Per-view FS jail root (we ignore URL host in scheme handler)
webView._dingWidgetRoot = rootDir;
webView._dingWidgetId = widgetId;
webView._dingInstanceId = instanceId;
const settings = webView.get_settings();
settings.set_enable_write_console_messages_to_stdout(true);
settings.set_enable_webgl(true);
webView.set_background_color(new Gdk.RGBA({
red: 0,
green: 0,
blue: 0,
alpha: 0,
}));
webView.set_name('ding-widget-webview');
webView.set_hexpand(true);
webView.set_vexpand(true);
return webView;
}
// ---------------------------------------------------------------------
// Preferences window helpers (called from WidgetManager)
// ---------------------------------------------------------------------
/**
* Open or focus the preferences window for a given instance.
*
* - Only one prefs window at a time (shared runtime).
* - Only opens if that instance is currently selected.
* - If already open for another instance, closes and reopens for this one.
*
* WidgetManager should:
* - Calls this in response to gear-icon click for the selected widget.
* - Calls closePreferencesForInstance() or closePreferencesIfForInstance()
* when unselecting/destroying the widget.
*
* @param {string} instanceId
* @param {string} prefsUri
*/
openPreferencesForInstance(instanceId, prefsUri) {
if (!instanceId || !prefsUri)
return;
// Only for currently selected instance
const selectedId = this._widgetManager.getSelectedInstanceId();
if (!selectedId || selectedId !== instanceId)
return;
if (this._prefsWindow && this._prefsInstanceId === instanceId) {
this._prefsWindow.present();
return;
}
this.closePreferencesIfAny();
const inst = this._widgetManager.getInstance(instanceId);
if (!inst) {
console.warn(
'WebWidgetContext.openPreferencesForInstance: no instance',
instanceId
);
return;
}
const defaultWidth = 420;
const defaultHeight = 520;
const window = new Gtk.Window({
title: _('Widget Preferences'),
default_width: defaultWidth,
default_height: defaultHeight,
});
const closeShortcut = new Gtk.ShortcutController({
propagation_phase: Gtk.PropagationPhase.CAPTURE,
});
closeShortcut.add_shortcut(new Gtk.Shortcut({
trigger: Gtk.ShortcutTrigger.parse_string('Escape'),
action: Gtk.CallbackAction.new(() => {
window.close();
return true;
}),
}));
window.add_controller(closeShortcut);
const parentWindow = this._mainApp.get_active_window();
if (parentWindow)
window.set_transient_for(parentWindow);
const host = new HtmlWidgetHost({
instanceId,
widgetId: inst.widgetId,
frameRect: {x: 0, y: 0, width: defaultWidth, height: defaultHeight},
widgetRegistry: this._widgetManager._widgetRegistry,
webContext: this,
mode: 'prefs',
prefsUri,
});
this._prefsHost = host;
host.actor.set_name('ding-prefs-frame');
window.set_child(host.actor);
window.connect('close-request', () => {
this._prefsHost?.destroy();
this._prefsHost = null;
this._prefsWindow = null;
this._prefsInstanceId = null;
return false;
});
this._prefsWindow = window;
this._prefsInstanceId = instanceId;
window.present();
}
closePreferencesForInstance(instanceId) {
if (!instanceId || instanceId !== this._prefsInstanceId)
return;
this.closePreferencesIfAny();
}
closePreferencesIfAny() {
if (!this._prefsWindow)
return;
this._prefsHost?.destroy();
this._prefsHost = null;
this._prefsWindow.destroy();
this._prefsWindow = null;
this._prefsInstanceId = null;
}
// ---------------------------------------------------------------------
// Internal: WebKit runtime setup
// ---------------------------------------------------------------------
_initPaths() {
const appId = this._mainApp.get_application_id();
const baseData = GLib.build_filenamev([
GLib.get_user_data_dir(),
appId,
'webkit',
]);
const baseCache = GLib.build_filenamev([
GLib.get_user_cache_dir(),
appId,
'webkit',
]);
this._dataBase = this._desktopIconsUtil.ensureDir(baseData);
this._cacheBase = this._desktopIconsUtil.ensureDir(baseCache);
}
_initWebKitRuntime() {
if (this._webContext && this._userContentManager)
return;
this._initPaths();
// Shared WebKit plumbing: one WebContext, one UserContentManager
this._webContext = new WebKit.WebContext();
this._webContext.set_cache_model(WebKit.CacheModel.DOCUMENT_VIEWER);
try {
const cacheDir = this._desktopIconsUtil.ensureDir(
GLib.build_filenamev([this._cacheBase, 'cache'])
);
const storageDir = this._desktopIconsUtil.ensureDir(
GLib.build_filenamev([this._dataBase, 'storage'])
);
this._networkSession =
WebKit.NetworkSession.new(storageDir, cacheDir);
} catch (e) {
logError(e, 'WidgetWebKit: WebContext directory setup failed');
}
this._userContentManager = new WebKit.UserContentManager();
const defaultWorld = null; // default JS world
// Register script message handler for window.ding → "dingWidget"
try {
this._userContentManager.register_script_message_handler(
'dingWidget',
defaultWorld
);
} catch (e) {
console.error(
'WebWidgetContext: failed to register dingWidget handler:',
e
);
}
this._webContext.register_uri_scheme(
'ding-widget',
this._onDingWidgetUriRequest.bind(this)
);
this._scriptHandlerId = this._userContentManager.connect(
'script-message-received::dingWidget',
this._onWidgetScriptMessage.bind(this)
);
const whitelist = null;
const blacklist = null;
try {
const userScript = WebKit.UserScript.new(
WidgetApi.WIDGET_API,
WebKit.UserContentInjectedFrames.ALL_FRAMES,
WebKit.UserScriptInjectionTime.START,
whitelist,
blacklist
);
this._userContentManager.add_script(userScript);
} catch (e) {
console.error(
'WebWidgetContext: failed to install WIDGET_API user script:',
e
);
}
}
_setCspString() {
const profile = this.Enums.DEFAULT_CSP_PROFILE;
let cspString = '';
switch (profile) {
case this.Enums.CspProfile.STRICT:
cspString = WidgetApi.CSP_STRICT;
break;
case this.Enums.CspProfile.DEV:
cspString = WidgetApi.CSP_DEV;
break;
case this.Enums.CspProfile.RELAXED:
cspString = WidgetApi.RELAXED;
break;
default:
console.warn('Unknown CSP profile, enforcing STRICT');
cspString = this.Enums.CspProfile.STRICT;
}
this._cspString = cspString.replace(/\s+/g, ' ').trim();
}
// ---------------------------------------------------------------------
// Internal: JS API bridge (window.ding)
// ---------------------------------------------------------------------
// Debug Helpers
_debugHostState(op, inst, patch) {
if (!(this.Enums.WIDGET_MANAGER_DEBUG &
this.Enums.WidgetManagerDebugFlags.HOST_STATE))
return;
const id = inst?.instanceId ?? '<none>';
console.log('>>> WebWidgetContext[HOST]', op, 'id=', id, 'patch=', patch);
}
_debugWidgetMessage(payload, direction = 'in') {
if (!(this.Enums.WIDGET_MANAGER_DEBUG &
this.Enums.WidgetManagerDebugFlags.WIDGET_MESSAGES))
return;
const id = payload?.instanceId ?? '<none>';
const type = payload?.type ?? '<none>';
const mode = payload?.mode ?? '<none>';
const arrow = direction === 'out' ? '>>>' : '<<<';
console.log(
`${arrow} WebWidgetContext[WIDGET]`,
'type=',
type,
'id=',
id,
'mode=',
mode,
payload
);
}
// Script Handler
_onWidgetScriptMessage(_manager, jsResult) {
let jsValue;
try {
if (typeof jsResult.get_js_value === 'function')
jsValue = jsResult.get_js_value();
else if (typeof jsResult.get_value === 'function')
jsValue = jsResult.get_value();
else
jsValue = jsResult;
} catch (e) {
console.error('WebWidgetContext: failed to read widget message:', e);
return;
}
if (jsValue === undefined || jsValue === null)
return;
let json = null;
try {
if (jsValue.is_string && jsValue.is_string())
json = jsValue.to_string();
else if (jsValue.to_json && jsValue.is_object && jsValue.is_object())
json = jsValue.to_json(0); // stringify objects
else if (jsValue.to_string)
json = jsValue.to_string();
} catch (e) {
console.error(
'WebWidgetContext: failed to convert widget message to string:',
e
);
return;
}
if (typeof json !== 'string') {
console.warn(
'WebWidgetContext: unexpected widget message payload',
json,
typeof json,
'raw jsValue:',
jsValue,
'ctor:',
jsValue?.constructor?.name
);
return;
}
let payload;
try {
payload = JSON.parse(json);
} catch (e) {
console.error('WebWidgetContext: invalid widget JSON payload:', e);
return;
}
if (!payload || typeof payload !== 'object')
return;
const {
instanceId,
type,
message,
} = payload;
this._debugWidgetMessage(payload);
// Log messages are always allowed through
if (type === 'log') {
console.log(
'HtmlWidget log:',
'(instanceId=',
instanceId,
')',
message
);
return;
}
if (!instanceId || typeof instanceId !== 'string')
return;
const manager = this._widgetManager;
if (!manager)
return;
this._dispatchWidgetMessage(manager, payload);
}
async _dispatchWidgetMessage(manager, payload) {
const {
instanceId,
type,
config,
requestId,
mode,
} = payload || {};
const inst = manager.getInstance(instanceId);
if (!inst)
return;
let webView;
try {
webView = await inst.host.getWebViewAsync();
} catch (e) {
return;
}
const uri = webView?.get_uri?.() ?? '';
if (!uri.startsWith(`ding-widget://${instanceId}/`))
return;
// Delegate semantics to WidgetManager, reusing its existing helpers.
switch (type) {
case 'updateConfig':
if (config && typeof config === 'object')
manager.updateInstanceConfig(instanceId, config);
// Broadcast so widget + prefs can update live
this._pushConfigChangedForInstance(inst, mode);
break;
case 'getConfig': {
this._doWidgetGetConfig(inst, mode, requestId);
break;
}
case 'hostReady': {
this._pushFullHostStateForInstance(inst);
this._pushConfigChangedForInstance(inst);
break;
}
case 'openPreferences': {
if (!inst.hasPreferences || !inst.prefsUri)
break;
this.openPreferencesForInstance(instanceId, inst.prefsUri);
break;
}
case 'closePreferences': {
this.closePreferencesForInstance(instanceId);
break;
}
case 'backendRequest': {
const hasBackend = typeof inst.host?.backendRequest === 'function';
if (!hasBackend) {
this._postNoBackendError(inst, payload);
break;
}
await inst.host.backendRequest(inst, payload);
break;
}
case 'backendSend': {
const hasBackend = typeof inst.host?.backendSend === 'function';
if (!hasBackend) {
this._debugWidgetMessage({
instanceId,
type: 'backendSendDropped',
name: payload?.name,
}, 'out');
break;
}
inst.host.backendSend(inst, payload);
break;
}
default:
// Unknown message type; ignore for now
break;
}
}
// Script Helpers
_postNoBackendError(inst, payload) {
// Ensure the JSAPI Promise resolves/rejects; otherwise it hangs.
const instanceId = inst?.instanceId ?? payload?.instanceId;
const reply = {
_dingInternal: true,
type: 'backendReply',
instanceId,
requestId: payload?.requestId,
ok: false,
error: {
code: 'E_NO_BACKEND',
message: 'This widget has no backend configured',
},
};
this._debugWidgetMessage({
instanceId,
type: 'backendReply',
requestId: payload?.requestId,
ok: false,
}, 'out');
this._routeAndPost(payload?.mode, inst, reply);
}
_postToWidget(inst, msg) {
const host = inst?.host;
if (!host)
return;
host.postMessage(msg);
}
_postToPrefs(inst, msg) {
if (!inst)
return;
if (this._prefsHost &&
this._prefsInstanceId === inst.instanceId
)
this._prefsHost.postMessage(msg);
}
_postToBoth(inst, msg) {
this._postToWidget(inst, msg);
this._postToPrefs(inst, msg);
}
_routeAndPost(mode, inst, msg) {
switch (mode) {
case 'prefs':
this._postToPrefs(inst, msg);
break;
case 'widget':
this._postToWidget(inst, msg);
break;
default:
this._postToBoth(inst, msg);
}
}
_doWidgetGetConfig(inst, mode, requestId) {
const reply = {
_dingInternal: true,
requestId,
config: inst.config || {},
};
this._debugWidgetMessage({
instanceId: inst?.instanceId,
type: 'getConfigReply',
requestId,
mode,
config: reply.config,
}, 'out');
this._routeAndPost(mode, inst, reply);
}
_pushConfigChangedForInstance(inst, mode = null) {
const msg = {
_dingInternal: true,
type: 'configChanged',
instanceId: inst.instanceId,
config: inst.config || {},
reason: 'configSaved',
sourceMode: mode,
};
this._debugWidgetMessage({
instanceId: inst?.instanceId,
type: 'configChanged',
mode,
config: inst.config,
}, 'out');
this._postToBoth(inst, msg);
}
_pushFullHostStateForInstance(inst) {
const state = this._widgetManager.computeHostStateForInstance(inst);
this._debugHostState('full', inst, state);
this._pushPatchtoTarget(inst, state);
}
_pushPatchtoTarget(inst, patch) {
if (!inst || inst.kind !== 'html' || !inst.host)
return;
inst.host.setHostStatePatch(patch);
if (this._prefsHost && inst.instanceId === this._prefsInstanceId)
this._prefsHost.setHostStatePatch(patch);
}
updateHtmlWidgetSelected(inst, selected) {
const patch = {selected};
this._debugHostState('selected', inst, patch);
this._pushPatchtoTarget(inst, patch);
}
updateHtmlWidgetAnimation(inst, reducedMotion) {
const patch = {reducedMotion};
this._debugHostState('reducedMotion', inst, patch);
this._pushPatchtoTarget(inst, patch);
}
updateHtmlWidgetLayer(inst, onTop) {
const patch = {editMode: !!onTop};
this._debugHostState('editMode', inst, patch);
this._pushPatchtoTarget(inst, patch);
}
updateHtmlWidgetTheme(inst, theme) {
const patch = {theme};
this._debugHostState('theme', inst, patch);
this._pushPatchtoTarget(inst, patch);
}
/* -----------------------------------------------------------------
* Instance roots and FS isolation
* -----------------------------------------------------------------*/
async _getInstanceRoot(instanceId) {
if (this._instanceRoots.has(instanceId))
return this._instanceRoots.get(instanceId);
const inst = this._widgetManager.getInstance(instanceId);
const widgetId = inst.widgetId;
const registry = this._widgetManager._widgetRegistry;
const desc = await registry.getDescriptor(widgetId)
.catch(e => console.error(`No description for ${widgetId}`, e));
const dir = desc?.dir;
if (!dir) {
console.warn(
'WebWidgetContext: no descriptor.dir for instance',
instanceId
);
return null;
}
this._instanceRoots.set(instanceId, dir);
return dir;
}
/*
* URI scheme handler for ding-widget://instanceId/path
*/
_onDingWidgetUriRequest(request) {
this._onDingWidgetUriRequestAsync(request).catch(e => {
console.error(
'WebWidgetContext: unhandled error in ding-widget handler:',
e
);
});
}
async _onDingWidgetUriRequestAsync(request) {
const sep = GLib.DIR_SEPARATOR_S;
const finishError = (code, message) => {
request.finish_error(new GLib.Error(
Gio.IOErrorEnum,
code,
message
));
};
let uri;
try {
uri = request.get_uri?.() ?? null;
} catch (e) {
console.error('WebWidgetContext: URI request without URI:', e);
finishError(Gio.IOErrorEnum.INVALID_ARGUMENT, 'Missing URI');
return;
}
if (!uri) {
console.error('WebWidgetContext: URI request had no URI');
finishError(Gio.IOErrorEnum.INVALID_ARGUMENT, 'Missing URI');
return;
}
let guri;
try {
guri = GLib.Uri.parse(uri, GLib.UriFlags.NONE);
} catch (e) {
console.error('WebWidgetContext: failed to parse URI:', uri, e);
finishError(Gio.IOErrorEnum.INVALID_ARGUMENT, 'Invalid URI');
return;
}
// Basic parse: ding-widget://<instanceId>/<relPath>
const scheme = 'ding-widget';
if (guri.get_scheme() !== scheme) {
console.error('WebWidgetContext: unexpected scheme URI:', uri);
finishError(
Gio.IOErrorEnum.INVALID_ARGUMENT,
'Unexpected URI scheme'
);
return;
}
const instanceId = guri.get_host();
if (!instanceId) {
console.error('WebWidgetContext: missing instanceId in URI', uri);
finishError(
Gio.IOErrorEnum.INVALID_ARGUMENT,
'Missing instanceId in widget URI'
);
return;
}
const webView = request.get_web_view();
if (!webView._dingWidgetRoot || webView._dingInstanceId !== instanceId) {
finishError(
Gio.IOErrorEnum.PERMISSION_DENIED,
'Widget root not bound to this view'
);
return;
}
const rootDir = await this._getInstanceRoot(instanceId);
if (!rootDir) {
console.error(
'WebWidgetContext: no root dir registered for instance',
instanceId,
'URI =',
uri
);
finishError(
Gio.IOErrorEnum.NOT_FOUND,
'Widget root not registered for this instance'
);
return;
}
// Extra guard: ensure the bound root on the WebView matches registry
try {
const boundRootPath = webView._dingWidgetRoot?.get_path?.();
const registryRootPath = rootDir.get_path?.();
if (!boundRootPath || !registryRootPath ||
boundRootPath !== registryRootPath) {
finishError(
Gio.IOErrorEnum.PERMISSION_DENIED,
'Widget root mismatch for this view'
);
return;
}
} catch (e) {
finishError(
Gio.IOErrorEnum.FAILED,
'Failed to verify widget root'
);
return;
}
// Normalize relPath:
// - treat URI path as widget-root-relative (strip leading "/")
// - strip leading "./" segments (so "./clock.css" works)
const path = guri.get_path?.() ?? '';
const effectiveRelPath =
path
.replace(/^\/+/, '')
.replace(/^(\.\/)+/, '');
if (!effectiveRelPath) {
finishError(Gio.IOErrorEnum.NOT_FOUND, 'No file specified');
return;
}
// Lexical confinement: canonicalize +
// prefix check to block "../" traversal.
const rootPath = rootDir.get_path();
if (!rootPath) {
console.error('WebWidgetContext: cannot enforce confinement (no root path)');
finishError(Gio.IOErrorEnum.FAILED, 'Cannot enforce confinement');
return;
}
try {
const candidatePath = GLib.build_filenamev([
rootPath,
effectiveRelPath,
]);
const canonRoot = GLib.canonicalize_filename(rootPath, null);
const canonFile = GLib.canonicalize_filename(candidatePath, null);
const normalizedRoot = canonRoot.endsWith(sep)
? canonRoot
: `${canonRoot}${sep}`;
if (!canonFile.startsWith(normalizedRoot)) {
console.error(
'WebWidgetContext: attempted escape from root:',
canonFile,
'not under',
normalizedRoot
);
finishError(
Gio.IOErrorEnum.PERMISSION_DENIED,
'Path escapes widget root'
);
return;
}
} catch (e) {
console.error(
'WebWidgetContext: exception during confinement check:', e
);
finishError(Gio.IOErrorEnum.FAILED, 'Confinement check failed');
return;
}
// Symlink confinement: reject symlinks anywhere
// in the path (NOFOLLOW_SYMLINKS).
let file = rootDir;
try {
const parts = effectiveRelPath.split('/').filter(p => p.length > 0);
for (const part of parts) {
file = file.get_child(part);
const info = file.query_info(
'standard::type,standard::is-symlink',
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
null
);
if (info.get_is_symlink()) {
finishError(Gio.IOErrorEnum.PERMISSION_DENIED,
'Symlinks are not allowed in widget paths'
);
return;
}
}
} catch (e) {
finishError(
Gio.IOErrorEnum.NOT_FOUND,
'File not found in widget root'
);
return;
}
let bytes;
try {
const [loadedBytes] = await file.load_bytes_async(null);
bytes = loadedBytes;
} catch (e) {
console.error(
'WebWidgetContext: exception loading file (async)',
file.get_path?.(),
e
);
finishError(
Gio.IOErrorEnum.NOT_FOUND, 'File not found in widget root');
return;
}
// Guess MIME type using filename + data,
// but then *force* sane types for HTML/CSS/JS.
let mimeType = null;
let filePathForMime = '';
try {
filePathForMime = file.get_path?.() ?? '';
const [mimetype] = Gio.content_type_guess(
filePathForMime,
bytes.toArray ? bytes.toArray() : null
);
if (mimetype)
mimeType = mimetype;
} catch (e) {
console.error('WebWidgetContext: content_type_guess failed', e);
}
// Force explicit types by extension important for the main HTML.
if (filePathForMime.endsWith('.html') ||
filePathForMime.endsWith('.htm'))
mimeType = 'text/html';
else if (filePathForMime.endsWith('.css'))
mimeType = 'text/css';
else if (filePathForMime.endsWith('.js'))
mimeType = 'application/javascript';
if (!mimeType)
mimeType = 'application/octet-stream';
try {
const stream = Gio.MemoryInputStream.new_from_bytes(bytes);
const length = bytes.get_size?.() ?? bytes.length ?? -1;
const response = new WebKit.URISchemeResponse({
stream,
'stream-length': length,
});
response.set_content_type(mimeType);
// To Do: set cspstring depending on widgetID with a manager...
if (this._cspString) {
const headers = new Soup.MessageHeaders(
Soup.MessageHeadersType.RESPONSE
);
headers.append('Content-Security-Policy', this._cspString);
response.set_http_headers(headers);
}
request.finish_with_response(response);
} catch (e) {
console.error(
'WebWidgetContext: failed to finish ding-widget request for',
uri,
e
);
finishError(
Gio.IOErrorEnum.FAILED,
'Failed to serve widget resource'
);
}
}
};