Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 3 additions & 1 deletion src/_locales/de/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}}
}
4 changes: 3 additions & 1 deletion src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}}
}
13 changes: 11 additions & 2 deletions src/background/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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 };
}
24 changes: 17 additions & 7 deletions src/options/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') };
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/popup/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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':
Expand Down
21 changes: 21 additions & 0 deletions src/shared/host-storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Loading