diff --git a/README.md b/README.md index 48d9ac5..97b4378 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ If you configure more than one instance, the popup shows an **Instance** dropdow | `storage` | Required | Saves your configured Part-DB instances and settings | | `activeTab` | Required | Reads the page HTML on demand when you click Submit | | `scripting` | Required | Executes the page-reading code in the active tab | -| Host access to your Part-DB URL(s) | Optional — granted when you save a server URL | Required to reach your Part-DB instance(s); scoped to exactly the URLs you configure | +| Host access to your Part-DB URL(s) | Optional — granted when you save a server URL | Required to reach your Part-DB instance(s); scoped to the hosts of the URLs you configure (all ports of that host, as Firefox does not support ports in host permissions) | No host permissions are requested at install time. The extension has no passive presence on web pages — it reads page content only at the moment you click Submit. diff --git a/src/_locales/de/messages.json b/src/_locales/de/messages.json index 42f86f9..35be741 100644 --- a/src/_locales/de/messages.json +++ b/src/_locales/de/messages.json @@ -207,5 +207,7 @@ }, "options_error_permission_denied": { "message": "Browser-Berechtigung für diese URL wurde verweigert. Die Erweiterung benötigt sie, um Ihren Part-DB-Server zu erreichen." - } + }, + "popup_error_missing_host_permission": {"message": "Die Erweiterung darf nicht auf diesen Part-DB-Server zugreifen. Öffnen Sie die Einstellungen und klicken Sie auf „Einstellungen speichern“, um die Berechtigung erneut zu erteilen."}, + "options_error_permission_failed": {"message": "Browser-Berechtigung konnte nicht angefordert werden: $ERROR$", "placeholders": {"error": {"content": "$1", "example": "Invalid pattern"}}} } diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 6686f13..8b9dd71 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -207,5 +207,7 @@ }, "options_error_permission_denied": { "message": "Browser permission for this URL was denied. The extension needs it to reach your Part-DB server." - } + }, + "popup_error_missing_host_permission": {"message": "The extension is not allowed to access this Part-DB server. Open the settings and click “Save Settings” to grant the permission again."}, + "options_error_permission_failed": {"message": "Could not request browser permission: $ERROR$", "placeholders": {"error": {"content": "$1", "example": "Invalid pattern"}}} } diff --git a/src/background/service-worker.js b/src/background/service-worker.js index 5af165d..7044413 100644 --- a/src/background/service-worker.js +++ b/src/background/service-worker.js @@ -51,7 +51,7 @@ async function handleGetInfo(hostId) { await chrome.storage.local.set({ infoCacheByHost }); return { success: true, data }; } catch (err) { - return { success: false, error: 'network_error', message: err.message }; + return networkErrorResult(baseUrl, err); } } @@ -99,6 +99,15 @@ async function handleSubmit(hostId, html, url, title, provider) { } return { success: true }; } catch (err) { - return { success: false, error: 'network_error', message: err.message }; + return networkErrorResult(baseUrl, err); } } + +async function networkErrorResult(baseUrl, err) { + // Without host permission the browser blocks the request and only reports a generic network error. + // Detect this case to give the user an actionable hint. + if (!(await hasHostPermission(baseUrl))) { + return { success: false, error: 'missing_host_permission' }; + } + return { success: false, error: 'network_error', message: err.message }; +} diff --git a/src/options/options.js b/src/options/options.js index 062908f..ce6c41a 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -64,14 +64,24 @@ function addHostRow(host, { focus = false } = {}) { return rowEl; } -async function requestHostPermission(url) { - let origin; +/** + * Requests host permissions for all given URLs with a single permissions.request() call. + * This must be called synchronously from the user input handler (before any await), as Firefox only allows + * permissions.request() while handling a user action, and that context is lost after the first await. + */ +async function requestHostPermissions(urls) { + let origins; try { - origin = new URL(url).origin + '/*'; + origins = [...new Set(urls.map(hostPermissionPattern))]; } catch { return { ok: false, error: chrome.i18n.getMessage('options_error_invalid_url') }; } - const granted = await chrome.permissions.request({ origins: [origin] }); + let granted; + try { + granted = await chrome.permissions.request({ origins }); + } catch (err) { + return { ok: false, error: chrome.i18n.getMessage('options_error_permission_failed', [err.message]) }; + } if (!granted) { return { ok: false, error: chrome.i18n.getMessage('options_error_permission_denied') }; } @@ -100,7 +110,7 @@ async function testHostRow(rowEl) { return; } - const perm = await requestHostPermission(url); + const perm = await requestHostPermissions([url]); if (!perm.ok) { showRowTestResult(rowEl, 'error', perm.error); return; @@ -155,8 +165,8 @@ async function saveAllSettings() { newHosts.push(host); } - for (const host of newHosts) { - const perm = await requestHostPermission(host.url); + if (newHosts.length > 0) { + const perm = await requestHostPermissions(newHosts.map((h) => h.url)); if (!perm.ok) { showSaveStatus('error', perm.error); return; diff --git a/src/popup/popup.js b/src/popup/popup.js index 0cdf023..64b5ca0 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -162,6 +162,8 @@ function infoErrorMessage(result) { switch (result.error) { case 'network_error': return chrome.i18n.getMessage('popup_error_network', [result.message || 'network error']); + case 'missing_host_permission': + return chrome.i18n.getMessage('popup_error_missing_host_permission'); case 'no_permission': return chrome.i18n.getMessage('popup_error_no_permission'); case 'feature_disabled': @@ -244,6 +246,8 @@ function errorMessage(result) { return chrome.i18n.getMessage('popup_error_page_too_large_limit'); case 'network_error': return chrome.i18n.getMessage('popup_error_network', [result.message || 'network error']); + case 'missing_host_permission': + return chrome.i18n.getMessage('popup_error_missing_host_permission'); case 'no_permission': return chrome.i18n.getMessage('popup_error_no_permission'); case 'feature_disabled': diff --git a/src/shared/host-storage.js b/src/shared/host-storage.js index 6c58466..aa43af2 100644 --- a/src/shared/host-storage.js +++ b/src/shared/host-storage.js @@ -61,3 +61,24 @@ async function getLastHostId() { async function setLastHostId(id) { await chrome.storage.local.set({ lastHostId: id }); } + +/** + * Builds the host permission match pattern for a Part-DB base URL. + * + * The port is deliberately left out: Firefox does not support ports in match patterns, so a pattern like + * "http://192.168.1.10:8080/*" never matches any request there, and fetches to Part-DB instances on + * non-standard ports fail with "NetworkError when attempting to fetch resource". + * A pattern without a port matches every port of that host in both Chrome and Firefox. + */ +function hostPermissionPattern(url) { + const parsed = new URL(url); + return `${parsed.protocol}//${parsed.hostname}/*`; +} + +async function hasHostPermission(url) { + try { + return await chrome.permissions.contains({ origins: [hostPermissionPattern(url)] }); + } catch { + return false; + } +}