[fix] client/simple: insecure ctx clipboard copy

Uses the deprecated [`execCommand()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand)
to copy content to clipboard if accessing the instance through HTTP, this method
isn't going away soon.

Closes https://github.com/searxng/searxng/issues/5359
This commit is contained in:
Ivan Gabaldon 2025-10-24 11:28:07 +02:00 committed by Markus Heiser
parent b770a46e1f
commit 8dacbbbb15
5 changed files with 59 additions and 22 deletions

View File

@ -407,12 +407,31 @@ const toggleHelp = (keyBindings: typeof baseKeyBinding): void => {
}; };
const copyURLToClipboard = async (): Promise<void> => { const copyURLToClipboard = async (): Promise<void> => {
const currentUrlElement = document.querySelector<HTMLAnchorElement>(".result[data-vim-selected] h3 a"); const selectedResult = document.querySelector<HTMLElement>(".result[data-vim-selected]");
assertElement(currentUrlElement); if (!selectedResult) return;
const url = currentUrlElement.getAttribute("href"); const resultAnchor = selectedResult.querySelector<HTMLAnchorElement>("a");
assertElement(resultAnchor);
const url = resultAnchor.getAttribute("href");
if (url) { if (url) {
await navigator.clipboard.writeText(url); if (window.isSecureContext) {
await navigator.clipboard.writeText(url);
} else {
const selection = window.getSelection();
if (selection) {
const node = document.createElement("span");
node.textContent = url;
resultAnchor.appendChild(node);
const range = document.createRange();
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
document.execCommand("copy");
node.remove();
}
}
} }
}; };

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import { http, listen, settings } from "../core/toolkit.ts"; import { assertElement, http, listen, settings } from "../core/toolkit.ts";
let engineDescriptions: Record<string, [string, string]> | undefined; let engineDescriptions: Record<string, [string, string]> | undefined;
@ -52,19 +52,25 @@ for (const engine of disableAllEngines) {
listen("click", engine, () => toggleEngines(false, engineToggles)); listen("click", engine, () => toggleEngines(false, engineToggles));
} }
const copyHashButton: HTMLElement | null = document.querySelector<HTMLElement>("#copy-hash"); listen("click", "#copy-hash", async function (this: HTMLElement) {
if (copyHashButton) { const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
listen("click", copyHashButton, async (event: Event) => { assertElement(target);
event.preventDefault();
const { copiedText, hash } = copyHashButton.dataset; if (window.isSecureContext) {
if (!(copiedText && hash)) return; await navigator.clipboard.writeText(target.innerText);
} else {
try { const selection = window.getSelection();
await navigator.clipboard.writeText(hash); if (selection) {
copyHashButton.innerText = copiedText; const range = document.createRange();
} catch (error) { range.selectNodeContents(target);
console.error("Failed to copy hash:", error); selection.removeAllRanges();
selection.addRange(range);
document.execCommand("copy");
} }
}); }
}
const copiedText = this.dataset.copiedText;
if (copiedText) {
this.innerText = copiedText;
}
});

View File

@ -121,7 +121,19 @@ listen("click", "#copy_url", async function (this: HTMLElement) {
const target = this.parentElement?.querySelector<HTMLPreElement>("pre"); const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
assertElement(target); assertElement(target);
await navigator.clipboard.writeText(target.innerText); if (window.isSecureContext) {
await navigator.clipboard.writeText(target.innerText);
} else {
const selection = window.getSelection();
if (selection) {
const range = document.createRange();
range.selectNodeContents(target);
selection.removeAllRanges();
selection.addRange(range);
document.execCommand("copy");
}
}
const copiedText = this.dataset.copiedText; const copiedText = this.dataset.copiedText;
if (copiedText) { if (copiedText) {
this.innerText = copiedText; this.innerText = copiedText;

View File

@ -1,7 +1,7 @@
<div id="search_url" role="complementary" aria-labelledby="search_url-title"> <div id="search_url" role="complementary" aria-labelledby="search_url-title">
<details class="sidebar-collapsible"> <details class="sidebar-collapsible">
<summary class="title" id="search_url-title">{{ _('Search URL') }}</summary> <summary class="title" id="search_url-title">{{ _('Search URL') }}</summary>
<button id="copy_url" type="submit" data-copied-text="{{ _('Copied') }}">{{ _('Copy') }}</button> <button id="copy_url" type="button" class="button" data-copied-text="{{ _('Copied') }}">{{ _('Copy') }}</button>
<div class="selectable_url"> <div class="selectable_url">
<pre>{{ url_for('search', _external=True) }}?q={{ q|urlencode }}&amp;language={{ current_language }}&amp;time_range={{ time_range }}&amp;safesearch={{ safesearch }} <pre>{{ url_for('search', _external=True) }}?q={{ q|urlencode }}&amp;language={{ current_language }}&amp;time_range={{ time_range }}&amp;safesearch={{ safesearch }}
{%- if pageno > 1 -%} {%- if pageno > 1 -%}

View File

@ -51,7 +51,7 @@
{{- preferences_url_params|e }} {{- preferences_url_params|e }}
</pre>{{- '' -}} </pre>{{- '' -}}
</div> </div>
<button id="copy-hash" class="button" data-hash="{{- preferences_url_params|e -}}" data-copied-text="{{- _('Copied') -}}">{{- _('Copy') -}}</button> <button id="copy-hash" type="button" class="button" data-hash="{{- preferences_url_params|e -}}" data-copied-text="{{- _('Copied') -}}">{{- _('Copy') -}}</button>
</div> </div>
<h4> <h4>
{{- _('Insert copied preferences hash (without URL) to restore') -}}:{{- '' -}} {{- _('Insert copied preferences hash (without URL) to restore') -}}:{{- '' -}}