/* 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 .
*/
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 ?? '';
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 ?? '';
const type = payload?.type ?? '';
const mode = payload?.mode ?? '';
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:///
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'
);
}
}
};