fix: extension lifecycle leaks, image allowlist, generic image label

Audit fixes for the extension:

- Track and disconnect the changed::excluded-apps GSettings handler in
  disable() (it was the one connection whose id was never captured).
- Track the _stopDaemon 1.5s force-kill timer and cancel any leftover on
  enable(), so the source can't outlive a disable->enable cycle.
- Guard the exclusion-path delete with _proxy?.DeleteItemAsync.
- Mirror the daemon's image allowlist (drop avif/svg) in _pickMime.
- Show a generic "Image" label for all image rows instead of singling out
  PNG; the thumbnail identifies the image and the on-clipboard format (often
  PNG even for a copied GIF/WebP) is an implementation detail.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Eduard Tolosa 2026-05-26 17:29:37 -05:00
parent e7b0392d92
commit 332ec4064a
2 changed files with 21 additions and 7 deletions

View file

@ -62,10 +62,16 @@ export default class StrataExtension extends Extension {
enable() {
this._shuttingDown = false;
this._daemonRestartAttempts = 0;
// Cancel any pending force-kill timer left over from a previous disable
// (enable→disable→enable within 1.5s) so it can't outlive this cycle.
if (this._daemonKillTimerId) {
GLib.Source.remove(this._daemonKillTimerId);
this._daemonKillTimerId = null;
}
this._settings = this.getSettings();
this._excludedApps = this._settings.get_strv('excluded-apps');
this._settings.connect('changed::excluded-apps', () => {
this._excludedAppsChangedId = this._settings.connect('changed::excluded-apps', () => {
this._excludedApps = this._settings.get_strv('excluded-apps');
});
@ -119,6 +125,10 @@ export default class StrataExtension extends Extension {
for (const id of this._configChangedIds) this._settings.disconnect(id);
this._configChangedIds = null;
}
if (this._excludedAppsChangedId && this._settings) {
this._settings.disconnect(this._excludedAppsChangedId);
this._excludedAppsChangedId = null;
}
this._panel?.destroy();
this._panel = null;
this._indicator?.destroy();
@ -229,8 +239,7 @@ export default class StrataExtension extends Extension {
const PREFERRED = [
// Raster images (size-capped at MAX_IMAGE).
'image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp',
'image/bmp', 'image/tiff', 'image/avif', 'image/x-icon',
'image/svg+xml',
'image/bmp', 'image/tiff', 'image/x-icon',
// Plain text (UTF-8 preferred, then locale, then X11 legacy aliases).
// Ranked above rich-text so editors that expose both text/plain and
// text/html give us raw source, not styled markup.
@ -374,8 +383,10 @@ export default class StrataExtension extends Extension {
// Graceful shutdown via D-Bus first.
this._proxy?.ShutdownRemote(() => {});
} catch (_) {}
// Give it 1.5s then force-terminate.
GLib.timeout_add(GLib.PRIORITY_LOW, 1500, () => {
// Give it 1.5s then force-terminate. Track the source so a re-enable
// within that window can cancel it (see enable()).
this._daemonKillTimerId = GLib.timeout_add(GLib.PRIORITY_LOW, 1500, () => {
this._daemonKillTimerId = null;
try { daemonToStop.send_signal(15); } catch (_) {} // SIGTERM
return GLib.SOURCE_REMOVE;
});
@ -532,7 +543,7 @@ export default class StrataExtension extends Extension {
// Exclusion check - no clipboard I/O, just string comparison.
if (this._isExcluded(this._currentFocusedApp)) {
try {
await this._proxy.DeleteItemAsync(id);
await this._proxy?.DeleteItemAsync(id);
} catch (e) {
// ignore - item may already be gone
}

View file

@ -224,7 +224,10 @@ export const ClipboardItem = GObject.registerClass({
let subText = '';
if (mimeType.startsWith('image/')) {
mainText = mimeType === 'image/png' ? 'PNG image' : 'Image';
// Generic label - the thumbnail identifies the image, and the
// on-clipboard format (often PNG even for a copied GIF/WebP) is an
// implementation detail that misleads more than it informs.
mainText = 'Image';
} else if (isUrl(preview)) {
mainText = preview.trim();
try {