fix: guard unavailable APIs for Firefox Android

- Replace broken 'let chrome = browser' shim with proper API fallback
- Make contextMenus and notifications calls conditional
- Broadcast notify via runtime.sendMessage as fallback for Android
- Add try/catch around permissions.request and tabs.query
- Fix openOptionsPage fallback using tabs.create on Android
Cette révision appartient à :
2026-05-24 16:28:25 +02:00
Parent f406414fad
révision 0b105437c4
3 fichiers modifiés avec 215 ajouts et 193 suppressions
+76 -64
Voir le fichier
@@ -1,64 +1,91 @@
const str_keys = ["instance_url", "preset", "username", "password"] const str_keys = ["instance_url", "preset", "username", "password"]
const bool_keys = ["showContextMenu"] const bool_keys = ["showContextMenu"]
if (typeof chrome === 'undefined') { // Compatibilité Firefox / Chrome
let chrome = browser const api = typeof browser !== 'undefined' ? browser : chrome;
}
const notify = message => chrome.notifications.create({ // --- Détection des capacités ---
const hasNotifications = !!(api.notifications && api.notifications.create);
const hasContextMenus = !!(api.contextMenus && api.contextMenus.create);
/**
* Notifie l'utilisateur. Sur Firefox Android, les notifications système
* ne sont pas disponibles : on envoie un message au popup à la place.
*/
const notify = message => {
if (hasNotifications) {
try {
api.notifications.create({
"type": "basic", "type": "basic",
"iconUrl": chrome.runtime.getURL("icons/icon-128.png"), "iconUrl": api.runtime.getURL("icons/icon-128.png"),
"title": 'YTPTube Extension', "title": 'YTPTube Extension',
"message": message, "message": message,
}); });
} catch (e) {
const onMenuCreated = () => { console.warn('Notifications non disponibles:', e);
if (chrome.runtime.lastError) {
console.log(`error creating menu item. ${chrome.runtime.lastError}`);
} }
syncContextMenu().then(_ => '').catch(console.error); }
} // Toujours diffuser via runtime pour que le popup puisse afficher le message
try {
api.runtime.sendMessage({ command: "notify", message: message }).catch(() => {});
} catch (_) {}
};
// --- Menu contextuel (optionnel sur Android) ---
const onMenuCreated = () => {
if (api.runtime.lastError) {
console.log(`Erreur création menu: ${api.runtime.lastError.message}`);
return;
}
syncContextMenu().catch(console.error);
};
const shouldShowContextMenu = async () => { const shouldShowContextMenu = async () => {
let item = await chrome.storage.sync.get("showContextMenu"); let item = await api.storage.sync.get("showContextMenu");
return 'showContextMenu' in item ? item.showContextMenu : true; return 'showContextMenu' in item ? item.showContextMenu : true;
}; };
const syncContextMenu = async () => { const syncContextMenu = async () => {
if (!hasContextMenus) return;
try {
let showContextMenu = await shouldShowContextMenu(); let showContextMenu = await shouldShowContextMenu();
chrome.contextMenus.update("send-to-ytptube", { visible: showContextMenu }); api.contextMenus.update("send-to-ytptube", { visible: showContextMenu });
} } catch (e) {
console.warn('contextMenus.update indisponible:', e);
}
};
chrome.contextMenus.create({ if (hasContextMenus) {
try {
api.contextMenus.create({
id: "send-to-ytptube", id: "send-to-ytptube",
title: "Send to YTPTube", title: "Send to YTPTube",
contexts: ["link"] contexts: ["link"]
}, onMenuCreated); }, onMenuCreated);
} catch (e) {
const getCurrentUrl = async () => (await chrome.tabs.query({ currentWindow: true, active: true }))[0].url console.warn('contextMenus.create indisponible:', e);
const getOption = async key => {
let item = await chrome.storage.sync.get(key);
if (str_keys.includes(key)) {
return item[key] ?? '';
}
if (bool_keys.includes(key)) {
return item[key] ?? false;
} }
} }
// --- Utilitaires ---
const getCurrentUrl = async () => {
const tabs = await api.tabs.query({ currentWindow: true, active: true });
return tabs && tabs[0] ? tabs[0].url : null;
};
const getOption = async key => {
let item = await api.storage.sync.get(key);
if (str_keys.includes(key)) return item[key] ?? '';
if (bool_keys.includes(key)) return item[key] ?? false;
};
const sendRequest = async (path, data) => { const sendRequest = async (path, data) => {
let instanceUrl = await getOption("instance_url"); let instanceUrl = await getOption("instance_url");
if (!instanceUrl) { if (!instanceUrl) throw new Error('YTPTube instance url not configured.');
throw new Error('YTPTube instance url not configured.');
}
if (instanceUrl.endsWith('/')) { if (instanceUrl.endsWith('/')) instanceUrl = instanceUrl.slice(0, -1);
instanceUrl = instanceUrl.slice(0, -1);
}
let headers = {}; let headers = {};
const auth_username = await getOption("username"); const auth_username = await getOption("username");
const auth_password = await getOption("password"); const auth_password = await getOption("password");
if (auth_username && auth_password) { if (auth_username && auth_password) {
@@ -68,15 +95,16 @@ const sendRequest = async (path, data) => {
const url = new URL(instanceUrl); const url = new URL(instanceUrl);
url.pathname = path; url.pathname = path;
let opts = { method: Object.keys(data).length > 0 ? 'POST' : 'GET', headers: headers }; let opts = {
method: Object.keys(data).length > 0 ? 'POST' : 'GET',
headers: headers
};
if (data) { if (data && Object.keys(data).length > 0) {
opts.headers['Content-Type'] = 'application/json'; opts.headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(data); opts.body = JSON.stringify(data);
} }
console.debug(`Sending to '${instanceUrl}'.`, opts, headers.length > 0 ? 'with auth header' : 'without auth header');
const req = await fetch(url, opts); const req = await fetch(url, opts);
return { status: req.status, statusText: req.statusText, data: req }; return { status: req.status, statusText: req.statusText, data: req };
}; };
@@ -84,11 +112,7 @@ const sendRequest = async (path, data) => {
const sendUrl = async (user_url, preset = null) => { const sendUrl = async (user_url, preset = null) => {
try { try {
const requestData = { url: user_url }; const requestData = { url: user_url };
if (preset) { if (preset) requestData.preset = preset;
requestData.preset = preset;
}
console.debug('Sending request data:', requestData);
const data = await sendRequest('/api/history', requestData); const data = await sendRequest('/api/history', requestData);
if ([200, 201, 202].includes(data.status)) { if ([200, 201, 202].includes(data.status)) {
@@ -106,35 +130,26 @@ const sendUrl = async (user_url, preset = null) => {
} }
}; };
chrome.contextMenus.onClicked.addListener(async (info, _) => { // --- Écouteurs ---
if (info.menuItemId !== "send-to-ytptube") { if (hasContextMenus) {
return; api.contextMenus.onClicked.addListener(async (info, _) => {
} if (info.menuItemId !== "send-to-ytptube") return;
if (!info.linkUrl) { if (!info.linkUrl) { notify('No link url found'); return; }
notify('No link url found');
return;
}
// Get the default preset for context menu actions
const defaultPreset = await getOption("preset"); const defaultPreset = await getOption("preset");
await sendUrl(info.linkUrl, defaultPreset); await sendUrl(info.linkUrl, defaultPreset);
}); });
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { api.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.command !== "send-to-ytptube") { if (message.command !== "send-to-ytptube") return;
return;
}
(async () => { (async () => {
try { try {
let url = message.url || await getCurrentUrl(); let url = message.url || await getCurrentUrl();
if (!url) { if (!url) {
await notify('No url found');
sendResponse({ success: false, message: 'No url found' }); sendResponse({ success: false, message: 'No url found' });
return; return;
} }
const result = await sendUrl(url, message.preset); const result = await sendUrl(url, message.preset);
sendResponse(result); sendResponse(result);
} catch (error) { } catch (error) {
@@ -145,6 +160,3 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true; return true;
}); });
+71 -57
Voir le fichier
@@ -3,23 +3,38 @@
const str_keys = ["instance_url", "preset", "username", "password"] const str_keys = ["instance_url", "preset", "username", "password"]
let storedOriginPattern = null; let storedOriginPattern = null;
if (typeof chrome === 'undefined') { const api = typeof browser !== 'undefined' ? browser : chrome;
let chrome = browser
}
const notify = (message, no_inline) => { const notify = (message, no_inline) => {
if (!no_inline) { if (!no_inline) {
document.querySelector('#error_msg').innerText = message; const el = document.querySelector('#error_msg');
if (el) el.innerText = message;
} }
chrome.notifications.create({ // Notifications système optionnelles (non disponibles sur Firefox Android)
if (api.notifications && api.notifications.create) {
try {
api.notifications.create({
"type": "basic", "type": "basic",
"iconUrl": chrome.runtime.getURL("icons/icon-128.png"), "iconUrl": api.runtime.getURL("icons/icon-128.png"),
"title": 'YTPTube Extension', "title": 'YTPTube Extension',
"message": message, "message": message,
}); });
} catch (e) {
console.warn('Notifications indisponibles:', e);
}
}
} }
const showSaveStatus = (message, isSuccess = true) => {
const el = document.querySelector('#save_status');
if (!el) return;
el.textContent = message;
el.className = `notification ${isSuccess ? 'is-success' : 'is-danger'} mt-2`;
el.classList.remove('is-hidden');
setTimeout(() => el.classList.add('is-hidden'), 3000);
};
const buildOriginPattern = (instanceUrl) => { const buildOriginPattern = (instanceUrl) => {
try { try {
const url = new URL(instanceUrl); const url = new URL(instanceUrl);
@@ -30,32 +45,30 @@ const buildOriginPattern = (instanceUrl) => {
}; };
const ensureOriginPermission = async (originPattern) => { const ensureOriginPermission = async (originPattern) => {
if (!originPattern) { if (!originPattern) return false;
return false; if (!api.permissions || !api.permissions.request) return true;
try {
return await api.permissions.request({ origins: [originPattern] });
} catch (e) {
// Sur Firefox Android, permissions.request peut échouer si pas depuis un geste utilisateur
console.warn('permissions.request échoué:', e);
return true; // On continue quand même
} }
if (!chrome.permissions || !chrome.permissions.request) {
return true;
}
return await chrome.permissions.request({ origins: [originPattern] });
}; };
const removeOriginPermission = async (originPattern) => { const removeOriginPermission = async (originPattern) => {
if (!originPattern) { if (!originPattern || !api.permissions) return;
return; try {
const hasPermission = await api.permissions.contains({ origins: [originPattern] });
if (hasPermission) await api.permissions.remove({ origins: [originPattern] });
} catch (e) {
console.warn('removeOriginPermission échoué:', e);
} }
const hasPermission = await chrome.permissions.contains({ origins: [originPattern] });
if (!hasPermission) {
return;
}
await chrome.permissions.remove({ origins: [originPattern] });
}; };
const testConfig = async () => { const testConfig = async () => {
document.querySelector('#error_msg').innerText = ""; const errEl = document.querySelector('#error_msg');
if (errEl) errEl.innerText = "";
let instance_url = document.querySelector("#instance_url").value.trim(); let instance_url = document.querySelector("#instance_url").value.trim();
if (!instance_url) { if (!instance_url) {
@@ -63,10 +76,7 @@ const testConfig = async () => {
return false; return false;
} }
if (instance_url.endsWith('/')) { if (instance_url.endsWith('/')) instance_url = instance_url.slice(0, -1);
instance_url = instance_url.slice(0, -1);
}
document.querySelector("#instance_url").value = instance_url; document.querySelector("#instance_url").value = instance_url;
const originPattern = buildOriginPattern(instance_url); const originPattern = buildOriginPattern(instance_url);
@@ -83,57 +93,60 @@ const testConfig = async () => {
let username = document.querySelector("#username").value; let username = document.querySelector("#username").value;
let password = document.querySelector("#password").value; let password = document.querySelector("#password").value;
let headers = {} let headers = {}
if (username && password) { if (username && password) {
headers['Authorization'] = 'Basic ' + btoa(username + ':' + password); headers['Authorization'] = 'Basic ' + btoa(username + ':' + password);
} }
try { try {
const req = await fetch(`${instance_url}/api/ping`, { const req = await fetch(`${instance_url}/api/ping`, { method: 'GET', headers: headers });
method: 'GET',
headers: headers
});
if (200 === req.status) { if (200 === req.status) {
notify("Connection successful.", true); notify("Connection successful.", true);
showSaveStatus("Connection successful!", true);
return true; return true;
} }
let errorMsg = "Unknown error.";
const json = await req.json(); try { const json = await req.json(); errorMsg = json.error ?? errorMsg; } catch (_) {}
notify(json.error ?? "Unknown error."); notify(errorMsg);
return false;
} catch (e) { } catch (e) {
notify("Error: " + e); notify("Error: " + e);
}
return false; return false;
}
} }
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
function onError(error) { function onError(error) { console.log(`Error: ${error}`); }
console.log(`Error: ${error}`);
}
str_keys.forEach(k => { str_keys.forEach(k => {
chrome.storage.sync.get(k).then(r => document.querySelector(`#${k}`).value = r[k] || "", onError); api.storage.sync.get(k).then(r => {
const el = document.querySelector(`#${k}`);
if (el) el.value = r[k] || "";
}, onError);
}); });
chrome.storage.sync.get(["showContextMenu", "instance_origin"]).then(r => { api.storage.sync.get(["showContextMenu", "instance_origin"]).then(r => {
document.querySelector("#showContextMenu").checked = r.showContextMenu || false; const el = document.querySelector("#showContextMenu");
if (el) el.checked = r.showContextMenu || false;
storedOriginPattern = r.instance_origin || null; storedOriginPattern = r.instance_origin || null;
}, onError); }, onError);
// Cacher la section "menu contextuel" si non disponible (Firefox Android)
const contextMenuSection = document.querySelector('#context-menu-section');
if (contextMenuSection && !(api.contextMenus)) {
contextMenuSection.style.display = 'none';
}
}); });
document.getElementById("ytptube_options").addEventListener("submit", async e => { document.getElementById("ytptube_options").addEventListener("submit", async e => {
e.preventDefault(); e.preventDefault();
if (false === (await testConfig())) return false;
if (false === (await testConfig())) { let showContextMenu = false;
return false; const showContextMenuEl = document.querySelector("#showContextMenu");
} if (showContextMenuEl) showContextMenu = showContextMenuEl.checked;
let showContextMenu = document.querySelector("#showContextMenu").checked;
let data = { presets: { presets: ['default'], last_updated: 0 } } let data = { presets: { presets: ['default'], last_updated: 0 } }
str_keys.forEach(key => data[key] = document.querySelector(`#${key}`).value) str_keys.forEach(key => data[key] = document.querySelector(`#${key}`).value)
data["showContextMenu"] = showContextMenu; data["showContextMenu"] = showContextMenu;
@@ -145,17 +158,18 @@ document.getElementById("ytptube_options").addEventListener("submit", async e =>
data.instance_origin = newOriginPattern; data.instance_origin = newOriginPattern;
storedOriginPattern = newOriginPattern; storedOriginPattern = newOriginPattern;
chrome.storage.sync.set(data); await api.storage.sync.set(data);
chrome.contextMenus.update("send-to-ytptube", { visible: showContextMenu });
notify("Options saved.", true); if (api.contextMenus && api.contextMenus.update) {
setTimeout(() => {
try { try {
window.close() api.contextMenus.update("send-to-ytptube", { visible: showContextMenu });
} catch (e) { } catch (e) {
console.warn('contextMenus.update indisponible:', e);
} }
}, 1000); }
showSaveStatus("Options saved.", true);
setTimeout(() => { try { window.close() } catch (e) {} }, 1500);
}); });
document.getElementById("test_config").addEventListener("click", async e => { document.getElementById("test_config").addEventListener("click", async e => {
+45 -49
Voir le fichier
@@ -2,25 +2,23 @@
const $ = s => document.querySelector(s) const $ = s => document.querySelector(s)
if (typeof chrome === 'undefined') { const api = typeof browser !== 'undefined' ? browser : chrome;
let chrome = browser;
}
const getOption = async (key, default_data) => { const getOption = async (key, default_data) => {
let item = await chrome.storage.sync.get(key); let item = await api.storage.sync.get(key);
return item[key] || default_data; return item[key] ?? default_data;
} }
const notify = message => chrome.notifications.create({ // Écoute les notifications du background (remplace chrome.notifications sur Android)
"type": "basic", api.runtime.onMessage.addListener((message) => {
"iconUrl": chrome.runtime.getURL("icons/icon-128.png"), if (message.command === 'notify') {
"title": 'YTPTube Extension', // On ne sait pas si c'est succès ou erreur ici, on affiche neutre
"message": message showStatusMessage(message.message, !message.message.toLowerCase().includes('fail') && !message.message.toLowerCase().includes('error'));
}
}); });
const showLoading = (isLoading) => { const showLoading = (isLoading) => {
const submitBtn = $('#submit-btn') const submitBtn = $('#submit-btn')
if (isLoading) { if (isLoading) {
submitBtn.disabled = true submitBtn.disabled = true
submitBtn.classList.add('is-loading') submitBtn.classList.add('is-loading')
@@ -35,11 +33,7 @@ const showStatusMessage = (message, isSuccess = true) => {
statusDiv.textContent = message statusDiv.textContent = message
statusDiv.className = `notification ${isSuccess ? 'is-success' : 'is-danger'}` statusDiv.className = `notification ${isSuccess ? 'is-success' : 'is-danger'}`
statusDiv.classList.remove('is-hidden') statusDiv.classList.remove('is-hidden')
setTimeout(() => statusDiv.classList.add('is-hidden'), 4000)
// Hide the message after 3 seconds
setTimeout(() => {
statusDiv.classList.add('is-hidden')
}, 3000)
} }
$("#ytptube_popup").addEventListener("submit", async (e) => { $("#ytptube_popup").addEventListener("submit", async (e) => {
@@ -54,106 +48,108 @@ $("#ytptube_popup").addEventListener("submit", async (e) => {
showLoading(true) showLoading(true)
try { try {
const response = await chrome.runtime.sendMessage({ command: 'send-to-ytptube', url: url, preset: preset }) const response = await api.runtime.sendMessage({ command: 'send-to-ytptube', url: url, preset: preset })
if (response && response.success) { if (response && response.success) {
showStatusMessage('Request sent successfully!', true) showStatusMessage('Request sent successfully!', true)
} else { } else {
showStatusMessage(response?.message || 'Failed to send request.', false) showStatusMessage(response?.message || 'Failed to send request.', false)
} }
} catch (error) { } catch (error) {
showStatusMessage('Error sending request.', false) showStatusMessage('Error sending request: ' + error.message, false)
console.error('Error:', error) console.error('Error:', error)
} finally { } finally {
showLoading(false) showLoading(false)
} }
}) })
const getCurrentUrl = async () => (await chrome.tabs.query({ currentWindow: true, active: true }))[0].url const getCurrentUrl = async () => {
try {
const tabs = await api.tabs.query({ currentWindow: true, active: true });
return tabs && tabs[0] ? tabs[0].url : null;
} catch (e) {
console.warn('tabs.query failed:', e);
return null;
}
}
addEventListener('DOMContentLoaded', async _ => { addEventListener('DOMContentLoaded', async _ => {
let url = await getCurrentUrl() let url = await getCurrentUrl()
if (url && !url.startsWith('http')) url = ""
if (url && !url.startsWith('http')) {
url = ""
}
$('#user_url').value = url || '' $('#user_url').value = url || ''
const defaultPreset = await getOption("preset") const defaultPreset = await getOption("preset")
const cache = await getOption('presets', { const cache = await getOption('presets', { presets: [], last_updated: 0 })
presets: [],
last_updated: 0
})
if (Date.now() - cache.last_updated > 1000 * 60 * 60) { if (Date.now() - cache.last_updated > 1000 * 60 * 60) {
cache.presets = await getPresets() cache.presets = await getPresets()
cache.last_updated = Date.now() cache.last_updated = Date.now()
await chrome.storage.sync.set({ presets: cache }) await api.storage.sync.set({ presets: cache })
} }
const s = $('#preset') const s = $('#preset')
while (s.firstChild) s.removeChild(s.lastChild);
// -- remove existing options // S'assurer qu'il y a toujours au moins "default"
while (s.firstChild) { const presets = cache.presets.length > 0 ? cache.presets : ['default'];
s.removeChild(s.lastChild); presets.forEach(p => {
}
cache.presets.forEach(p => {
let option = document.createElement('option'); let option = document.createElement('option');
option.value = p; option.value = p;
option.text = p; option.text = p;
// -- set the default preset if (p === defaultPreset) option.selected = true;
if (p === defaultPreset) {
option.selected = true;
}
s.appendChild(option); s.appendChild(option);
}) })
}) })
$('#go-to-options').addEventListener('click', function () { $('#go-to-options').addEventListener('click', function () {
if (chrome.runtime.openOptionsPage) { if (api.runtime.openOptionsPage) {
chrome.runtime.openOptionsPage(); api.runtime.openOptionsPage();
} else { } else {
window.open(chrome.runtime.getURL('options.html')); // Sur Firefox Android, openOptionsPage peut être absent
const optionsUrl = api.runtime.getURL('options.html');
api.tabs.create({ url: optionsUrl }).catch(() => window.open(optionsUrl));
} }
}); });
$('#preset').addEventListener('change', async e => await chrome.storage.sync.set({ preset: e.target.value })) $('#preset').addEventListener('change', async e => await api.storage.sync.set({ preset: e.target.value }))
const getPresets = async () => { const getPresets = async () => {
let presets = [] let presets = []
const instanceUrl = await getOption("instance_url"); const instanceUrl = await getOption("instance_url");
if (!instanceUrl) { if (!instanceUrl) {
presets.push('YTPTube instance url not configured.'); presets.push('default');
return presets return presets
} }
let headers = {}; let headers = {};
const auth_username = await getOption('username'); const auth_username = await getOption('username');
const auth_password = await getOption('password'); const auth_password = await getOption('password');
if (auth_username && auth_password) { if (auth_username && auth_password) {
headers['Authorization'] = 'Basic ' + btoa(`${auth_username}:${auth_password}`); headers['Authorization'] = 'Basic ' + btoa(`${auth_username}:${auth_password}`);
} }
try {
const url = new URL(instanceUrl); const url = new URL(instanceUrl);
url.pathname = '/api/presets'; url.pathname = '/api/presets';
url.search = 'filter=name'; url.search = 'filter=name';
const req = await fetch(url, { method: 'GET', headers: { ...headers, 'Accept': 'application/json' } }); const req = await fetch(url, { method: 'GET', headers: { ...headers, 'Accept': 'application/json' } });
if (200 !== req.status) { if (200 !== req.status) {
presets.push('Error fetching presets from YTPTube.'); presets.push('default');
return presets return presets
} }
const data = await req.json() const data = await req.json()
if (data.items && Array.isArray(data.items)) { if (data.items && Array.isArray(data.items)) {
data.items.forEach(preset => presets.push(preset.name)) data.items.forEach(preset => presets.push(preset.name))
} else { } else if (Array.isArray(data)) {
data.forEach(preset => presets.push(preset.name)) data.forEach(preset => presets.push(preset.name))
} else {
presets.push('default');
}
} catch (e) {
console.error('Error fetching presets:', e);
presets.push('default');
} }
return presets return presets