diff --git a/background.js b/background.js deleted file mode 100644 index 2b0086b..0000000 --- a/background.js +++ /dev/null @@ -1,76 +0,0 @@ -browser.runtime.onMessage.addListener(async (msg) => { - switch (msg.action) { - case 'track': - await track(...msg.args); - return; - case 'addTask': - await addTask(...msg.args); - return; - default: - throw new Error(`unknown action: ${msg.action}`); - } -}); - -async function track(cardId) { - const task = await addTask(cardId); - const card = await trektor.trelloGateway.getCard(cardId); - const cardName = stripStoryPointsAndTaskToken(card.name); - const response = await trektor.togglGateway.startTimeEntry(task.id, cardName); - return response.data; -} - -async function addTask(cardId) { - const card = await trektor.trelloGateway.getCard(cardId); - - const taskPrefixes = card.labels - .map((label) => label.name.match(/(?<=#)[a-z0-9]+$/)?.[0]) - .filter((prefix) => prefix !== undefined); - - if (taskPrefixes.length === 0) { - throw new Error('Card has no valid project labels.'); - } - if (taskPrefixes.length > 1) { - throw new Error('Card has multiple project labels.'); - } - const taskPrefix = taskPrefixes[0]; - - let taskName = card.name.match(/(?<=#)[A-Za-z0-9_-]+/)?.[0]; - - if (taskName === undefined) { - taskName = `${taskPrefix}_${card.idShort}`; - - await trektor.trelloGateway.updateCard(card.id, { - name: `${card.name} #${taskName}`, - }); - } - - const workspaces = await trektor.togglGateway.getWorkspaces(); - - if (workspaces.length === 0) { - throw new Error('Could not find any toggl workspaces.'); - } - if (workspaces.length > 1) { - throw new Error('Found multiple toggl workspaces. Not sure how to deal with that...'); - } - const allProjects = await trektor.togglGateway.getProjects(workspaces[0].id); - const projects = allProjects.filter((project) => project.name.endsWith(`(${taskPrefix})`)); - - if (projects.length === 0) { - throw new Error('Could not find any matching toggl project.'); - } - if (projects.length > 1) { - throw new Error('Found multiple matching toggl projects. Not sure how to deal with that...'); - } - const tasks = await trektor.togglGateway.getTasks(projects[0].id); - const task = tasks.find((task) => task.name === taskName); - if (task !== undefined) return task; - - const response = await trektor.togglGateway.createTask(projects[0].id, taskName) - return response.data; -} - -function stripStoryPointsAndTaskToken(cardName) { - return cardName - .replace(/^(\s*\(\d+\))?\s*/, '') // story points, e.g. (3) - .replace(/\s*#[a-z0-9_]+\s*$/, ''); // task token, e.g. #orga_5417 -} diff --git a/manifest.json b/manifest.json index a04da4a..402a2d0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,13 +1,8 @@ { - "manifest_version": 2, + "manifest_version": 3, "name": "Trektor", "description": "Browser-Extension zum automatischen Anlegen von Toggl tracking tasks", "version": "0.0.11", - "browser_specific_settings": { - "gecko": { - "id": "trektor@aboutsource.net" - } - }, "icons": { "64": "icons/64.png" }, @@ -17,8 +12,8 @@ "https://trello.com/*" ], "js": [ - "vendor/browser-polyfill.js", - "content_script.js" + "scripts/content_script.js", + "scripts/trektor.js" ], "css": [ "content_style.css" @@ -26,17 +21,20 @@ } ], "background": { + "service_worker": "scripts/chromium.js", "scripts": [ - "vendor/browser-polyfill.js", - "trektor.js", - "background.js" + "scripts/trektor.js", + "scripts/background.js", + "scripts/firefox.js" ] }, "permissions": [ - "https://api.trello.com/*", - "https://api.track.toggl.com/*", "storage" ], + "host_permissions": [ + "https://api.trello.com/*", + "https://api.track.toggl.com/*" + ], "options_ui": { "page": "options/index.html" } diff --git a/options/index.html b/options/index.html index 3a45c93..a07ce6a 100644 --- a/options/index.html +++ b/options/index.html @@ -2,34 +2,25 @@ + Trektor settings - +

Willkommen bei Trektor!

+
✔ Alle Berechtigungen vorhanden
+
✘ Berechtigung fehlt!
+ - Trello Token + Trello Token ↗ - - + - Toggl Token + Toggl Token ↗ + - - - diff --git a/options/script.js b/options/script.js index fd1c05c..32ed71e 100644 --- a/options/script.js +++ b/options/script.js @@ -1,9 +1,42 @@ +if (typeof browser == "undefined") { + globalThis.browser = chrome +} + +const permissions = { + origins: [ + "https://api.trello.com/*", + "https://api.track.toggl.com/*", + "https://trello.com/*" + ] +} + document.querySelectorAll("input").forEach((field) => { field.addEventListener("input", (e) => { browser.storage.local.set({ [e.target.name]: e.target.value }); }); browser.storage.local.get(field.name).then(({ [field.name]: value }) => { - field.value = (value === undefined) ? '' : value; + field.value = (value === undefined) ? "" : value; }); }); + +checkPermissions() + +document.getElementById("request").addEventListener("click", () => { requestPermissions() }) + +function requestPermissions() { + console.log("hallo") + browser.permissions.request(permissions, () => { checkPermissions() }) +} + +function checkPermissions() { + browser.permissions.contains(permissions, (contains) => { + if (contains) { + document.getElementById("permissions-granted").style.display = "block"; + document.getElementById("permissions-not-granted").style.display = "none"; + } else { + document.getElementById("permissions-granted").style.display = "none"; + document.getElementById("permissions-not-granted").style.display = "block"; + } + }) +} diff --git a/options/style.css b/options/style.css new file mode 100644 index 0000000..cea7222 --- /dev/null +++ b/options/style.css @@ -0,0 +1,30 @@ +a { + display: block; + color: blue; + margin: 10px; +} +input { + width: 100%; + box-sizing: border-box; + margin-bottom: 10px; +} +body { + font-family: sans-serif; + width: 90%; + max-width: 500px; + margin: 1.5rem auto; +} + +#permissions-granted { + background-color: rgba(0, 255, 0, 0.5); + border: 2px green solid; +} +#permissions-not-granted { + background-color: rgba(255, 20, 0, 0.5); + border: 2px red solid; +} +.notification { + text-align: center; + padding: 10px; + border-radius: 10px; +} diff --git a/scripts/background.js b/scripts/background.js new file mode 100644 index 0000000..4e27767 --- /dev/null +++ b/scripts/background.js @@ -0,0 +1,90 @@ +class BackgroundWorker { + + constructor() { + } + + run() { + trektor.browser.runtime.onMessage.addListener(async (msg) => { + switch (msg.action) { + case "track": + await this.track(...msg.args); + return; + case "addTask": + await this.addTask(...msg.args); + return; + case "options": + trektor.browser.runtime.openOptionsPage(); + default: + throw new Error(`unknown action: ${msg.action}`); + } + }); + + trektor.browser.runtime.onInstalled.addListener((details) => { + if (details.reason == "install") trektor.browser.runtime.openOptionsPage(); + }) + } + + async track(cardId) { + const task = await this.addTask(cardId); + const card = await trektor.trelloGateway.getCard(cardId); + const cardName = this.stripStoryPointsAndTaskToken(card.name); + const response = await trektor.togglGateway.startTimeEntry(task.id, cardName); + return response.data; + } + + async addTask(cardId) { + const card = await trektor.trelloGateway.getCard(cardId); + + const taskPrefixes = card.labels + .map((label) => label.name.match(/(?<=#)[a-z0-9]+$/)?.[0]) + .filter((prefix) => prefix !== undefined); + + if (taskPrefixes.length === 0) { + throw new Error("Card has no valid project labels."); + } + if (taskPrefixes.length > 1) { + throw new Error("Card has multiple project labels."); + } + const taskPrefix = taskPrefixes[0]; + + let taskName = card.name.match(/(?<=#)[A-Za-z0-9_-]+/)?.[0]; + + if (taskName === undefined) { + taskName = `${taskPrefix}_${card.idShort}`; + + await trektor.trelloGateway.updateCard(card.id, { + name: `${card.name} #${taskName}`, + }); + } + + const workspaces = await trektor.togglGateway.getWorkspaces(); + + if (workspaces.length === 0) { + throw new Error("Could not find any toggl workspaces."); + } + if (workspaces.length > 1) { + throw new Error("Found multiple toggl workspaces. Not sure how to deal with that..."); + } + const allProjects = await trektor.togglGateway.getProjects(workspaces[0].id); + const projects = allProjects.filter((project) => project.name.endsWith(`(${taskPrefix})`)); + + if (projects.length === 0) { + throw new Error("Could not find any matching toggl project."); + } + if (projects.length > 1) { + throw new Error("Found multiple matching toggl projects. Not sure how to deal with that..."); + } + const tasks = await trektor.togglGateway.getTasks(projects[0].id); + const task = tasks.find((task) => task.name === taskName); + if (task !== undefined) return task; + + const response = await trektor.togglGateway.createTask(projects[0].id, taskName) + return response.data; + } + + stripStoryPointsAndTaskToken(cardName) { + return cardName + .replace(/^(\s*\(\d+\))?\s*/, "") // story points, e.g. (3) + .replace(/\s*#[a-z0-9_]+\s*$/, ""); // task token, e.g. #orga_5417 + } +} diff --git a/scripts/chromium.js b/scripts/chromium.js new file mode 100644 index 0000000..d8bb7a8 --- /dev/null +++ b/scripts/chromium.js @@ -0,0 +1,3 @@ +importScripts('background.js', 'trektor.js'); + +new BackgroundWorker().run() diff --git a/content_script.js b/scripts/content_script.js similarity index 86% rename from content_script.js rename to scripts/content_script.js index 1fcd717..346d19a 100644 --- a/content_script.js +++ b/scripts/content_script.js @@ -34,14 +34,14 @@ async function addButton() { trackButtonIcon.classList.add("trektor-state-loading"); try { - await browser.runtime.sendMessage({ + await trektor.browser.runtime.sendMessage({ action: "track", args: [window.location.pathname.split("/", 3)[2]], }); trackButtonIcon.classList.replace("icon-clock", "icon-check-circle"); window.setTimeout(() => trackButtonIcon.classList.replace("icon-check-circle", "icon-clock"), 2000); } catch (err) { - window.alert(err); + if (window.confirm(err + "\n\n Open Trektor options page now?")) trektor.browser.runtime.sendMessage({ action: "options" }); } finally { trackButtonIcon.classList.remove("trektor-state-loading"); } @@ -49,12 +49,12 @@ async function addButton() { addButton.addEventListener("click", async () => { try { - await browser.runtime.sendMessage({ + await trektor.browser.runtime.sendMessage({ action: "addTask", args: [window.location.pathname.split("/", 3)[2]], }); } catch (err) { - window.alert(err); + if (window.confirm(err + "\n\n Open Trektor options page now?")) trektor.browser.runtime.sendMessage({ action: "options" }); } }); } @@ -71,7 +71,7 @@ function awaitSelector(selector, timeout) { resolve(element); window.clearInterval(interval); } else if (timeout < 0) { - reject(new Error('timeout')); + reject(new Error("timeout")); window.clearInterval(interval); } timeout -= 100; @@ -82,7 +82,3 @@ function awaitSelector(selector, timeout) { window.addEventListener("pushstate", () => { if (window.location.pathname.startsWith("/c/")) addButton(); }); - -window.addEventListener('load', () => { - if (window.location.pathname.startsWith("/c/")) addButton(); -}); diff --git a/scripts/firefox.js b/scripts/firefox.js new file mode 100644 index 0000000..9660239 --- /dev/null +++ b/scripts/firefox.js @@ -0,0 +1 @@ +new BackgroundWorker().run() diff --git a/trektor.js b/scripts/trektor.js similarity index 89% rename from trektor.js rename to scripts/trektor.js index cc5d72c..b77fd79 100644 --- a/trektor.js +++ b/scripts/trektor.js @@ -1,3 +1,7 @@ +if (typeof browser == "undefined") { + globalThis.browser = chrome +} + class TrelloGateway { static ENDPOINT = "https://api.trello.com/1"; static API_KEY = "2379d540412e417f6f0696c1397f38a6"; @@ -30,7 +34,7 @@ class TrelloGateway { }); if (response.status === 401) { - throw new Error('Invalid or expired trello token.'); + throw new Error('Invalid or expired trello token. \n Get a new one in the settings.'); } else { return response.json(); } @@ -88,14 +92,16 @@ class TogglGateway { }); if (response.status === 403) { - throw new Error('Invalid toggl token.'); + throw new Error('Invalid toggl token. \n Get a valid one from the settings.'); } else { return response.json(); } } } -window.trektor = { + +const trektor = { trelloGateway: new TrelloGateway(browser.storage.local), togglGateway: new TogglGateway(browser.storage.local), -}; + browser: browser, +} \ No newline at end of file diff --git a/vendor/browser-polyfill.js b/vendor/browser-polyfill.js deleted file mode 100644 index 230b763..0000000 --- a/vendor/browser-polyfill.js +++ /dev/null @@ -1,1277 +0,0 @@ -(function (global, factory) { - if (typeof define === "function" && define.amd) { - define("webextension-polyfill", ["module"], factory); - } else if (typeof exports !== "undefined") { - factory(module); - } else { - var mod = { - exports: {} - }; - factory(mod); - global.browser = mod.exports; - } -})(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (module) { - /* webextension-polyfill - v0.8.0 - Tue Apr 20 2021 11:27:38 */ - - /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ - - /* vim: set sts=2 sw=2 et tw=80: */ - - /* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; - - if (typeof browser === "undefined" || Object.getPrototypeOf(browser) !== Object.prototype) { - const CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE = "The message port closed before a response was received."; - const SEND_RESPONSE_DEPRECATION_WARNING = "Returning a Promise is the preferred way to send a reply from an onMessage/onMessageExternal listener, as the sendResponse will be removed from the specs (See https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage)"; // Wrapping the bulk of this polyfill in a one-time-use function is a minor - // optimization for Firefox. Since Spidermonkey does not fully parse the - // contents of a function until the first time it's called, and since it will - // never actually need to be called, this allows the polyfill to be included - // in Firefox nearly for free. - - const wrapAPIs = extensionAPIs => { - // NOTE: apiMetadata is associated to the content of the api-metadata.json file - // at build time by replacing the following "include" with the content of the - // JSON file. - const apiMetadata = { - "alarms": { - "clear": { - "minArgs": 0, - "maxArgs": 1 - }, - "clearAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "bookmarks": { - "create": { - "minArgs": 1, - "maxArgs": 1 - }, - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getChildren": { - "minArgs": 1, - "maxArgs": 1 - }, - "getRecent": { - "minArgs": 1, - "maxArgs": 1 - }, - "getSubTree": { - "minArgs": 1, - "maxArgs": 1 - }, - "getTree": { - "minArgs": 0, - "maxArgs": 0 - }, - "move": { - "minArgs": 2, - "maxArgs": 2 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeTree": { - "minArgs": 1, - "maxArgs": 1 - }, - "search": { - "minArgs": 1, - "maxArgs": 1 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - }, - "browserAction": { - "disable": { - "minArgs": 0, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "enable": { - "minArgs": 0, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "getBadgeBackgroundColor": { - "minArgs": 1, - "maxArgs": 1 - }, - "getBadgeText": { - "minArgs": 1, - "maxArgs": 1 - }, - "getPopup": { - "minArgs": 1, - "maxArgs": 1 - }, - "getTitle": { - "minArgs": 1, - "maxArgs": 1 - }, - "openPopup": { - "minArgs": 0, - "maxArgs": 0 - }, - "setBadgeBackgroundColor": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setBadgeText": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setIcon": { - "minArgs": 1, - "maxArgs": 1 - }, - "setPopup": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setTitle": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - } - }, - "browsingData": { - "remove": { - "minArgs": 2, - "maxArgs": 2 - }, - "removeCache": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeCookies": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeDownloads": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeFormData": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeHistory": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeLocalStorage": { - "minArgs": 1, - "maxArgs": 1 - }, - "removePasswords": { - "minArgs": 1, - "maxArgs": 1 - }, - "removePluginData": { - "minArgs": 1, - "maxArgs": 1 - }, - "settings": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "commands": { - "getAll": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "contextMenus": { - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - }, - "cookies": { - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAllCookieStores": { - "minArgs": 0, - "maxArgs": 0 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "set": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "devtools": { - "inspectedWindow": { - "eval": { - "minArgs": 1, - "maxArgs": 2, - "singleCallbackArg": false - } - }, - "panels": { - "create": { - "minArgs": 3, - "maxArgs": 3, - "singleCallbackArg": true - }, - "elements": { - "createSidebarPane": { - "minArgs": 1, - "maxArgs": 1 - } - } - } - }, - "downloads": { - "cancel": { - "minArgs": 1, - "maxArgs": 1 - }, - "download": { - "minArgs": 1, - "maxArgs": 1 - }, - "erase": { - "minArgs": 1, - "maxArgs": 1 - }, - "getFileIcon": { - "minArgs": 1, - "maxArgs": 2 - }, - "open": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "pause": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeFile": { - "minArgs": 1, - "maxArgs": 1 - }, - "resume": { - "minArgs": 1, - "maxArgs": 1 - }, - "search": { - "minArgs": 1, - "maxArgs": 1 - }, - "show": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - } - }, - "extension": { - "isAllowedFileSchemeAccess": { - "minArgs": 0, - "maxArgs": 0 - }, - "isAllowedIncognitoAccess": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "history": { - "addUrl": { - "minArgs": 1, - "maxArgs": 1 - }, - "deleteAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "deleteRange": { - "minArgs": 1, - "maxArgs": 1 - }, - "deleteUrl": { - "minArgs": 1, - "maxArgs": 1 - }, - "getVisits": { - "minArgs": 1, - "maxArgs": 1 - }, - "search": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "i18n": { - "detectLanguage": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAcceptLanguages": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "identity": { - "launchWebAuthFlow": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "idle": { - "queryState": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "management": { - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "getSelf": { - "minArgs": 0, - "maxArgs": 0 - }, - "setEnabled": { - "minArgs": 2, - "maxArgs": 2 - }, - "uninstallSelf": { - "minArgs": 0, - "maxArgs": 1 - } - }, - "notifications": { - "clear": { - "minArgs": 1, - "maxArgs": 1 - }, - "create": { - "minArgs": 1, - "maxArgs": 2 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "getPermissionLevel": { - "minArgs": 0, - "maxArgs": 0 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - }, - "pageAction": { - "getPopup": { - "minArgs": 1, - "maxArgs": 1 - }, - "getTitle": { - "minArgs": 1, - "maxArgs": 1 - }, - "hide": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setIcon": { - "minArgs": 1, - "maxArgs": 1 - }, - "setPopup": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setTitle": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "show": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - } - }, - "permissions": { - "contains": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "request": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "runtime": { - "getBackgroundPage": { - "minArgs": 0, - "maxArgs": 0 - }, - "getPlatformInfo": { - "minArgs": 0, - "maxArgs": 0 - }, - "openOptionsPage": { - "minArgs": 0, - "maxArgs": 0 - }, - "requestUpdateCheck": { - "minArgs": 0, - "maxArgs": 0 - }, - "sendMessage": { - "minArgs": 1, - "maxArgs": 3 - }, - "sendNativeMessage": { - "minArgs": 2, - "maxArgs": 2 - }, - "setUninstallURL": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "sessions": { - "getDevices": { - "minArgs": 0, - "maxArgs": 1 - }, - "getRecentlyClosed": { - "minArgs": 0, - "maxArgs": 1 - }, - "restore": { - "minArgs": 0, - "maxArgs": 1 - } - }, - "storage": { - "local": { - "clear": { - "minArgs": 0, - "maxArgs": 0 - }, - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getBytesInUse": { - "minArgs": 0, - "maxArgs": 1 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "set": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "managed": { - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getBytesInUse": { - "minArgs": 0, - "maxArgs": 1 - } - }, - "sync": { - "clear": { - "minArgs": 0, - "maxArgs": 0 - }, - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getBytesInUse": { - "minArgs": 0, - "maxArgs": 1 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "set": { - "minArgs": 1, - "maxArgs": 1 - } - } - }, - "tabs": { - "captureVisibleTab": { - "minArgs": 0, - "maxArgs": 2 - }, - "create": { - "minArgs": 1, - "maxArgs": 1 - }, - "detectLanguage": { - "minArgs": 0, - "maxArgs": 1 - }, - "discard": { - "minArgs": 0, - "maxArgs": 1 - }, - "duplicate": { - "minArgs": 1, - "maxArgs": 1 - }, - "executeScript": { - "minArgs": 1, - "maxArgs": 2 - }, - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getCurrent": { - "minArgs": 0, - "maxArgs": 0 - }, - "getZoom": { - "minArgs": 0, - "maxArgs": 1 - }, - "getZoomSettings": { - "minArgs": 0, - "maxArgs": 1 - }, - "goBack": { - "minArgs": 0, - "maxArgs": 1 - }, - "goForward": { - "minArgs": 0, - "maxArgs": 1 - }, - "highlight": { - "minArgs": 1, - "maxArgs": 1 - }, - "insertCSS": { - "minArgs": 1, - "maxArgs": 2 - }, - "move": { - "minArgs": 2, - "maxArgs": 2 - }, - "query": { - "minArgs": 1, - "maxArgs": 1 - }, - "reload": { - "minArgs": 0, - "maxArgs": 2 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeCSS": { - "minArgs": 1, - "maxArgs": 2 - }, - "sendMessage": { - "minArgs": 2, - "maxArgs": 3 - }, - "setZoom": { - "minArgs": 1, - "maxArgs": 2 - }, - "setZoomSettings": { - "minArgs": 1, - "maxArgs": 2 - }, - "update": { - "minArgs": 1, - "maxArgs": 2 - } - }, - "topSites": { - "get": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "webNavigation": { - "getAllFrames": { - "minArgs": 1, - "maxArgs": 1 - }, - "getFrame": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "webRequest": { - "handlerBehaviorChanged": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "windows": { - "create": { - "minArgs": 0, - "maxArgs": 1 - }, - "get": { - "minArgs": 1, - "maxArgs": 2 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 1 - }, - "getCurrent": { - "minArgs": 0, - "maxArgs": 1 - }, - "getLastFocused": { - "minArgs": 0, - "maxArgs": 1 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - } - }; - - if (Object.keys(apiMetadata).length === 0) { - throw new Error("api-metadata.json has not been included in browser-polyfill"); - } - /** - * A WeakMap subclass which creates and stores a value for any key which does - * not exist when accessed, but behaves exactly as an ordinary WeakMap - * otherwise. - * - * @param {function} createItem - * A function which will be called in order to create the value for any - * key which does not exist, the first time it is accessed. The - * function receives, as its only argument, the key being created. - */ - - - class DefaultWeakMap extends WeakMap { - constructor(createItem, items = undefined) { - super(items); - this.createItem = createItem; - } - - get(key) { - if (!this.has(key)) { - this.set(key, this.createItem(key)); - } - - return super.get(key); - } - - } - /** - * Returns true if the given object is an object with a `then` method, and can - * therefore be assumed to behave as a Promise. - * - * @param {*} value The value to test. - * @returns {boolean} True if the value is thenable. - */ - - - const isThenable = value => { - return value && typeof value === "object" && typeof value.then === "function"; - }; - /** - * Creates and returns a function which, when called, will resolve or reject - * the given promise based on how it is called: - * - * - If, when called, `chrome.runtime.lastError` contains a non-null object, - * the promise is rejected with that value. - * - If the function is called with exactly one argument, the promise is - * resolved to that value. - * - Otherwise, the promise is resolved to an array containing all of the - * function's arguments. - * - * @param {object} promise - * An object containing the resolution and rejection functions of a - * promise. - * @param {function} promise.resolve - * The promise's resolution function. - * @param {function} promise.reject - * The promise's rejection function. - * @param {object} metadata - * Metadata about the wrapped method which has created the callback. - * @param {boolean} metadata.singleCallbackArg - * Whether or not the promise is resolved with only the first - * argument of the callback, alternatively an array of all the - * callback arguments is resolved. By default, if the callback - * function is invoked with only a single argument, that will be - * resolved to the promise, while all arguments will be resolved as - * an array if multiple are given. - * - * @returns {function} - * The generated callback function. - */ - - - const makeCallback = (promise, metadata) => { - return (...callbackArgs) => { - if (extensionAPIs.runtime.lastError) { - promise.reject(new Error(extensionAPIs.runtime.lastError.message)); - } else if (metadata.singleCallbackArg || callbackArgs.length <= 1 && metadata.singleCallbackArg !== false) { - promise.resolve(callbackArgs[0]); - } else { - promise.resolve(callbackArgs); - } - }; - }; - - const pluralizeArguments = numArgs => numArgs == 1 ? "argument" : "arguments"; - /** - * Creates a wrapper function for a method with the given name and metadata. - * - * @param {string} name - * The name of the method which is being wrapped. - * @param {object} metadata - * Metadata about the method being wrapped. - * @param {integer} metadata.minArgs - * The minimum number of arguments which must be passed to the - * function. If called with fewer than this number of arguments, the - * wrapper will raise an exception. - * @param {integer} metadata.maxArgs - * The maximum number of arguments which may be passed to the - * function. If called with more than this number of arguments, the - * wrapper will raise an exception. - * @param {boolean} metadata.singleCallbackArg - * Whether or not the promise is resolved with only the first - * argument of the callback, alternatively an array of all the - * callback arguments is resolved. By default, if the callback - * function is invoked with only a single argument, that will be - * resolved to the promise, while all arguments will be resolved as - * an array if multiple are given. - * - * @returns {function(object, ...*)} - * The generated wrapper function. - */ - - - const wrapAsyncFunction = (name, metadata) => { - return function asyncFunctionWrapper(target, ...args) { - if (args.length < metadata.minArgs) { - throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); - } - - if (args.length > metadata.maxArgs) { - throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); - } - - return new Promise((resolve, reject) => { - if (metadata.fallbackToNoCallback) { - // This API method has currently no callback on Chrome, but it return a promise on Firefox, - // and so the polyfill will try to call it with a callback first, and it will fallback - // to not passing the callback if the first call fails. - try { - target[name](...args, makeCallback({ - resolve, - reject - }, metadata)); - } catch (cbError) { - console.warn(`${name} API method doesn't seem to support the callback parameter, ` + "falling back to call it without a callback: ", cbError); - target[name](...args); // Update the API method metadata, so that the next API calls will not try to - // use the unsupported callback anymore. - - metadata.fallbackToNoCallback = false; - metadata.noCallback = true; - resolve(); - } - } else if (metadata.noCallback) { - target[name](...args); - resolve(); - } else { - target[name](...args, makeCallback({ - resolve, - reject - }, metadata)); - } - }); - }; - }; - /** - * Wraps an existing method of the target object, so that calls to it are - * intercepted by the given wrapper function. The wrapper function receives, - * as its first argument, the original `target` object, followed by each of - * the arguments passed to the original method. - * - * @param {object} target - * The original target object that the wrapped method belongs to. - * @param {function} method - * The method being wrapped. This is used as the target of the Proxy - * object which is created to wrap the method. - * @param {function} wrapper - * The wrapper function which is called in place of a direct invocation - * of the wrapped method. - * - * @returns {Proxy} - * A Proxy object for the given method, which invokes the given wrapper - * method in its place. - */ - - - const wrapMethod = (target, method, wrapper) => { - return new Proxy(method, { - apply(targetMethod, thisObj, args) { - return wrapper.call(thisObj, target, ...args); - } - - }); - }; - - let hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); - /** - * Wraps an object in a Proxy which intercepts and wraps certain methods - * based on the given `wrappers` and `metadata` objects. - * - * @param {object} target - * The target object to wrap. - * - * @param {object} [wrappers = {}] - * An object tree containing wrapper functions for special cases. Any - * function present in this object tree is called in place of the - * method in the same location in the `target` object tree. These - * wrapper methods are invoked as described in {@see wrapMethod}. - * - * @param {object} [metadata = {}] - * An object tree containing metadata used to automatically generate - * Promise-based wrapper functions for asynchronous. Any function in - * the `target` object tree which has a corresponding metadata object - * in the same location in the `metadata` tree is replaced with an - * automatically-generated wrapper function, as described in - * {@see wrapAsyncFunction} - * - * @returns {Proxy} - */ - - const wrapObject = (target, wrappers = {}, metadata = {}) => { - let cache = Object.create(null); - let handlers = { - has(proxyTarget, prop) { - return prop in target || prop in cache; - }, - - get(proxyTarget, prop, receiver) { - if (prop in cache) { - return cache[prop]; - } - - if (!(prop in target)) { - return undefined; - } - - let value = target[prop]; - - if (typeof value === "function") { - // This is a method on the underlying object. Check if we need to do - // any wrapping. - if (typeof wrappers[prop] === "function") { - // We have a special-case wrapper for this method. - value = wrapMethod(target, target[prop], wrappers[prop]); - } else if (hasOwnProperty(metadata, prop)) { - // This is an async method that we have metadata for. Create a - // Promise wrapper for it. - let wrapper = wrapAsyncFunction(prop, metadata[prop]); - value = wrapMethod(target, target[prop], wrapper); - } else { - // This is a method that we don't know or care about. Return the - // original method, bound to the underlying object. - value = value.bind(target); - } - } else if (typeof value === "object" && value !== null && (hasOwnProperty(wrappers, prop) || hasOwnProperty(metadata, prop))) { - // This is an object that we need to do some wrapping for the children - // of. Create a sub-object wrapper for it with the appropriate child - // metadata. - value = wrapObject(value, wrappers[prop], metadata[prop]); - } else if (hasOwnProperty(metadata, "*")) { - // Wrap all properties in * namespace. - value = wrapObject(value, wrappers[prop], metadata["*"]); - } else { - // We don't need to do any wrapping for this property, - // so just forward all access to the underlying object. - Object.defineProperty(cache, prop, { - configurable: true, - enumerable: true, - - get() { - return target[prop]; - }, - - set(value) { - target[prop] = value; - } - - }); - return value; - } - - cache[prop] = value; - return value; - }, - - set(proxyTarget, prop, value, receiver) { - if (prop in cache) { - cache[prop] = value; - } else { - target[prop] = value; - } - - return true; - }, - - defineProperty(proxyTarget, prop, desc) { - return Reflect.defineProperty(cache, prop, desc); - }, - - deleteProperty(proxyTarget, prop) { - return Reflect.deleteProperty(cache, prop); - } - - }; // Per contract of the Proxy API, the "get" proxy handler must return the - // original value of the target if that value is declared read-only and - // non-configurable. For this reason, we create an object with the - // prototype set to `target` instead of using `target` directly. - // Otherwise we cannot return a custom object for APIs that - // are declared read-only and non-configurable, such as `chrome.devtools`. - // - // The proxy handlers themselves will still use the original `target` - // instead of the `proxyTarget`, so that the methods and properties are - // dereferenced via the original targets. - - let proxyTarget = Object.create(target); - return new Proxy(proxyTarget, handlers); - }; - /** - * Creates a set of wrapper functions for an event object, which handles - * wrapping of listener functions that those messages are passed. - * - * A single wrapper is created for each listener function, and stored in a - * map. Subsequent calls to `addListener`, `hasListener`, or `removeListener` - * retrieve the original wrapper, so that attempts to remove a - * previously-added listener work as expected. - * - * @param {DefaultWeakMap} wrapperMap - * A DefaultWeakMap object which will create the appropriate wrapper - * for a given listener function when one does not exist, and retrieve - * an existing one when it does. - * - * @returns {object} - */ - - - const wrapEvent = wrapperMap => ({ - addListener(target, listener, ...args) { - target.addListener(wrapperMap.get(listener), ...args); - }, - - hasListener(target, listener) { - return target.hasListener(wrapperMap.get(listener)); - }, - - removeListener(target, listener) { - target.removeListener(wrapperMap.get(listener)); - } - - }); - - const onRequestFinishedWrappers = new DefaultWeakMap(listener => { - if (typeof listener !== "function") { - return listener; - } - /** - * Wraps an onRequestFinished listener function so that it will return a - * `getContent()` property which returns a `Promise` rather than using a - * callback API. - * - * @param {object} req - * The HAR entry object representing the network request. - */ - - - return function onRequestFinished(req) { - const wrappedReq = wrapObject(req, {} - /* wrappers */ - , { - getContent: { - minArgs: 0, - maxArgs: 0 - } - }); - listener(wrappedReq); - }; - }); // Keep track if the deprecation warning has been logged at least once. - - let loggedSendResponseDeprecationWarning = false; - const onMessageWrappers = new DefaultWeakMap(listener => { - if (typeof listener !== "function") { - return listener; - } - /** - * Wraps a message listener function so that it may send responses based on - * its return value, rather than by returning a sentinel value and calling a - * callback. If the listener function returns a Promise, the response is - * sent when the promise either resolves or rejects. - * - * @param {*} message - * The message sent by the other end of the channel. - * @param {object} sender - * Details about the sender of the message. - * @param {function(*)} sendResponse - * A callback which, when called with an arbitrary argument, sends - * that value as a response. - * @returns {boolean} - * True if the wrapped listener returned a Promise, which will later - * yield a response. False otherwise. - */ - - - return function onMessage(message, sender, sendResponse) { - let didCallSendResponse = false; - let wrappedSendResponse; - let sendResponsePromise = new Promise(resolve => { - wrappedSendResponse = function (response) { - if (!loggedSendResponseDeprecationWarning) { - console.warn(SEND_RESPONSE_DEPRECATION_WARNING, new Error().stack); - loggedSendResponseDeprecationWarning = true; - } - - didCallSendResponse = true; - resolve(response); - }; - }); - let result; - - try { - result = listener(message, sender, wrappedSendResponse); - } catch (err) { - result = Promise.reject(err); - } - - const isResultThenable = result !== true && isThenable(result); // If the listener didn't returned true or a Promise, or called - // wrappedSendResponse synchronously, we can exit earlier - // because there will be no response sent from this listener. - - if (result !== true && !isResultThenable && !didCallSendResponse) { - return false; - } // A small helper to send the message if the promise resolves - // and an error if the promise rejects (a wrapped sendMessage has - // to translate the message into a resolved promise or a rejected - // promise). - - - const sendPromisedResult = promise => { - promise.then(msg => { - // send the message value. - sendResponse(msg); - }, error => { - // Send a JSON representation of the error if the rejected value - // is an instance of error, or the object itself otherwise. - let message; - - if (error && (error instanceof Error || typeof error.message === "string")) { - message = error.message; - } else { - message = "An unexpected error occurred"; - } - - sendResponse({ - __mozWebExtensionPolyfillReject__: true, - message - }); - }).catch(err => { - // Print an error on the console if unable to send the response. - console.error("Failed to send onMessage rejected reply", err); - }); - }; // If the listener returned a Promise, send the resolved value as a - // result, otherwise wait the promise related to the wrappedSendResponse - // callback to resolve and send it as a response. - - - if (isResultThenable) { - sendPromisedResult(result); - } else { - sendPromisedResult(sendResponsePromise); - } // Let Chrome know that the listener is replying. - - - return true; - }; - }); - - const wrappedSendMessageCallback = ({ - reject, - resolve - }, reply) => { - if (extensionAPIs.runtime.lastError) { - // Detect when none of the listeners replied to the sendMessage call and resolve - // the promise to undefined as in Firefox. - // See https://github.com/mozilla/webextension-polyfill/issues/130 - if (extensionAPIs.runtime.lastError.message === CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE) { - resolve(); - } else { - reject(new Error(extensionAPIs.runtime.lastError.message)); - } - } else if (reply && reply.__mozWebExtensionPolyfillReject__) { - // Convert back the JSON representation of the error into - // an Error instance. - reject(new Error(reply.message)); - } else { - resolve(reply); - } - }; - - const wrappedSendMessage = (name, metadata, apiNamespaceObj, ...args) => { - if (args.length < metadata.minArgs) { - throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); - } - - if (args.length > metadata.maxArgs) { - throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); - } - - return new Promise((resolve, reject) => { - const wrappedCb = wrappedSendMessageCallback.bind(null, { - resolve, - reject - }); - args.push(wrappedCb); - apiNamespaceObj.sendMessage(...args); - }); - }; - - const staticWrappers = { - devtools: { - network: { - onRequestFinished: wrapEvent(onRequestFinishedWrappers) - } - }, - runtime: { - onMessage: wrapEvent(onMessageWrappers), - onMessageExternal: wrapEvent(onMessageWrappers), - sendMessage: wrappedSendMessage.bind(null, "sendMessage", { - minArgs: 1, - maxArgs: 3 - }) - }, - tabs: { - sendMessage: wrappedSendMessage.bind(null, "sendMessage", { - minArgs: 2, - maxArgs: 3 - }) - } - }; - const settingMetadata = { - clear: { - minArgs: 1, - maxArgs: 1 - }, - get: { - minArgs: 1, - maxArgs: 1 - }, - set: { - minArgs: 1, - maxArgs: 1 - } - }; - apiMetadata.privacy = { - network: { - "*": settingMetadata - }, - services: { - "*": settingMetadata - }, - websites: { - "*": settingMetadata - } - }; - return wrapObject(extensionAPIs, staticWrappers, apiMetadata); - }; - - if (typeof chrome != "object" || !chrome || !chrome.runtime || !chrome.runtime.id) { - throw new Error("This script should only be loaded in a browser extension."); - } // The build process adds a UMD wrapper around this file, which makes the - // `module` variable available. - - - module.exports = wrapAPIs(chrome); - } else { - module.exports = browser; - } -}); -//# sourceMappingURL=browser-polyfill.js.map