mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 12:08:15 +00:00
195 lines
6.3 KiB
JavaScript
195 lines
6.3 KiB
JavaScript
export const HOST_ACCESS_REQUIRED_STATUS = 'host_permission_required';
|
|
|
|
export function normalizeTabId(value) {
|
|
if (typeof value === 'number') {
|
|
return Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
}
|
|
if (typeof value !== 'string') return null;
|
|
|
|
const normalized = value.trim();
|
|
if (!/^[1-9]\d*$/.test(normalized)) return null;
|
|
const tabId = Number(normalized);
|
|
return Number.isSafeInteger(tabId) ? tabId : null;
|
|
}
|
|
|
|
function browserSupportsPortMatchPatterns(chromeApi) {
|
|
// runtime.getBrowserInfo is a Firefox-only WebExtension API. Firefox does
|
|
// not accept ports in match patterns, while Chromium does.
|
|
return typeof chromeApi?.runtime?.getBrowserInfo !== 'function';
|
|
}
|
|
|
|
export function describeTabUrl(rawUrl, { includePort = true } = {}) {
|
|
if (typeof rawUrl !== 'string' || !rawUrl) return null;
|
|
|
|
try {
|
|
const url = new URL(rawUrl);
|
|
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
const permissionHost = includePort ? url.host : url.hostname;
|
|
return {
|
|
url: rawUrl,
|
|
host: url.host,
|
|
originPattern: `${url.protocol}//${permissionHost}/*`
|
|
};
|
|
}
|
|
if (url.protocol === 'file:') {
|
|
return {
|
|
url: rawUrl,
|
|
host: 'local file',
|
|
originPattern: 'file:///*'
|
|
};
|
|
}
|
|
} catch (_e) {
|
|
// Invalid and browser-internal URLs cannot receive host access.
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export async function inspectTabHostAccess(chromeApi, tabId) {
|
|
const tab = await chromeApi.tabs.get(tabId);
|
|
// executeScript targets the committed document. pendingUrl may already point
|
|
// at another origin while tab.url is still the document being injected into.
|
|
const descriptor = describeTabUrl(tab?.url || tab?.pendingUrl || '', {
|
|
includePort: browserSupportsPortMatchPatterns(chromeApi)
|
|
});
|
|
if (!descriptor) {
|
|
return {
|
|
tab,
|
|
url: tab?.url || '',
|
|
host: null,
|
|
originPattern: null,
|
|
granted: null
|
|
};
|
|
}
|
|
|
|
if (typeof chromeApi.permissions?.contains !== 'function') {
|
|
return { tab, ...descriptor, granted: null };
|
|
}
|
|
|
|
try {
|
|
const granted = await callBooleanPermissionMethod(
|
|
chromeApi,
|
|
'contains',
|
|
{ origins: [descriptor.originPattern] },
|
|
{ timeoutMs: 1000 }
|
|
);
|
|
return {
|
|
tab,
|
|
...descriptor,
|
|
granted: granted === null ? null : granted === true
|
|
};
|
|
} catch (_e) {
|
|
// Permission inspection is advisory. The actual script injection below
|
|
// remains the source of truth, including temporary activeTab access.
|
|
return { tab, ...descriptor, granted: null };
|
|
}
|
|
}
|
|
|
|
function callBooleanPermissionMethod(chromeApi, methodName, request, { timeoutMs = null } = {}) {
|
|
const method = chromeApi.permissions?.[methodName];
|
|
if (typeof method !== 'function') return Promise.resolve(null);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let settled = false;
|
|
let timeout = null;
|
|
const clearSettlementTimeout = () => {
|
|
if (timeout !== null) {
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
}
|
|
};
|
|
const finish = (value) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearSettlementTimeout();
|
|
resolve(value === true ? true : value === false ? false : null);
|
|
};
|
|
const fail = (error) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearSettlementTimeout();
|
|
reject(error instanceof Error ? error : new Error(String(error || 'Permission request failed')));
|
|
};
|
|
const callback = (value) => {
|
|
const lastError = chromeApi.runtime?.lastError;
|
|
if (lastError) {
|
|
fail(new Error(lastError.message || String(lastError)));
|
|
return;
|
|
}
|
|
finish(value);
|
|
};
|
|
|
|
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
timeout = setTimeout(() => finish(null), timeoutMs);
|
|
}
|
|
|
|
try {
|
|
const result = method.call(chromeApi.permissions, request, callback);
|
|
if (result && typeof result.then === 'function') {
|
|
result.then(finish, fail);
|
|
} else if (typeof result === 'boolean') {
|
|
finish(result);
|
|
}
|
|
// Callback-based Chromium/Firefox APIs return undefined and settle
|
|
// through callback. Promise-based implementations settle above.
|
|
} catch (error) {
|
|
fail(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function requestOriginPermission(chromeApi, originPattern) {
|
|
if (typeof originPattern !== 'string' || !originPattern) {
|
|
return Promise.resolve(null);
|
|
}
|
|
return callBooleanPermissionMethod(
|
|
chromeApi,
|
|
'request',
|
|
{ origins: [originPattern] },
|
|
{ timeoutMs: 60000 }
|
|
).catch(() => false);
|
|
}
|
|
|
|
export function isHostAccessError(error) {
|
|
const message = typeof error?.message === 'string' ? error.message : String(error || '');
|
|
return /host permission|permission to access (?:this|the|respective) host|cannot access contents of/i.test(message);
|
|
}
|
|
|
|
function hostAccessRequestDetails(tabId, originPattern) {
|
|
const request = { tabId };
|
|
if (typeof originPattern === 'string' && originPattern) {
|
|
request.pattern = originPattern;
|
|
}
|
|
return request;
|
|
}
|
|
|
|
export async function addTabHostAccessRequest(chromeApi, tabId, originPattern = null) {
|
|
if (typeof chromeApi.permissions?.addHostAccessRequest !== 'function') {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await chromeApi.permissions.addHostAccessRequest(
|
|
hostAccessRequestDetails(tabId, originPattern)
|
|
);
|
|
return true;
|
|
} catch (_e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function removeTabHostAccessRequest(chromeApi, tabId, originPattern = null) {
|
|
if (typeof chromeApi.permissions?.removeHostAccessRequest !== 'function') {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await chromeApi.permissions.removeHostAccessRequest(
|
|
hostAccessRequestDetails(tabId, originPattern)
|
|
);
|
|
return true;
|
|
} catch (_e) {
|
|
return false;
|
|
}
|
|
}
|