diff --git a/user-script/README.md b/content-script-register/README.md similarity index 51% rename from user-script/README.md rename to content-script-register/README.md index 61ad8f4a..8adf8252 100644 --- a/user-script/README.md +++ b/content-script-register/README.md @@ -1,7 +1,9 @@ -# User script +# Content script registration This extension demonstrates the `browser.contentScripts.register()` API, which enables an extension to register URL-matching content scripts at runtime. +The contentScripts.register() API is intended to enable an extension to register scripts that are packaged in the extension. If you want to register third-party user scripts, use the [userScripts API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts). Please refer to [user script registration](https://github.com/mdn/webextensions-examples/tree/master/user-script-register) for an example of that API. + This extension adds a browser action that shows a popup. The popup lets you specify: * some code that comprises your content script @@ -9,16 +11,16 @@ This extension adds a browser action that shows a popup. The popup lets you spec Once these are set up you can register the content script by clicking "Register script". The extension will then register a content script with the given properties by calling `browser.contentScripts.register()`. -To keep things simple, you can only have one script registered at any time: if you click "Register script" again, the old script is unregistered. To do this, the extension keeps a reference to the `RegisteredContentScript` object returned from `browser.contentScripts.register()`: this object provides the `unregister()` method. +To keep things simple, you can only have one script registered at any time: if you click "Register script" again, the active script is unregistered. To do this, the extension keeps a reference to the `RegisteredContentScript` object returned from `browser.contentScripts.register()`: this object provides the `unregister()` method. Note that the extension uses a background script to register the content scripts and to keep a reference to the `RegisteredContentScript` object. If it did not do this, then as soon as the popup window closed, the `RegisteredContentScript` would go out of scope and be destroyed, and the browser would then unregister the content script as part of cleanup. ## Default settings -The popup is initialized with some default values for the pattern and the code: +The popup is initialized with default values for the pattern and the code: -* the pattern `*://*.org/*`, which will load the script into any HTTP or HTTPS pages on a `.org` domain. +* the pattern `*://*.org/*`, which loads the script into any HTTP or HTTPS pages on a `.org` domain. * the code `document.body.innerHTML = '

This page has been eaten

'` -To try the extension out quickly, just click "Register script" with these defaults, and load http://example.org/ or -https://www.mozilla.org/. Then try changing the pattern or the code, and reloading these or related pages. +To try the extension, open the browser action, click "Register script" with the defaults, and load http://example.org/ or +https://www.mozilla.org/. Then change the pattern or the code, and reload these or related pages. diff --git a/user-script/background.js b/content-script-register/background.js similarity index 100% rename from user-script/background.js rename to content-script-register/background.js diff --git a/user-script/icons/LICENSE b/content-script-register/icons/LICENSE similarity index 100% rename from user-script/icons/LICENSE rename to content-script-register/icons/LICENSE diff --git a/user-script/icons/if_source_code_103710.svg b/content-script-register/icons/if_source_code_103710.svg similarity index 100% rename from user-script/icons/if_source_code_103710.svg rename to content-script-register/icons/if_source_code_103710.svg diff --git a/user-script/manifest.json b/content-script-register/manifest.json similarity index 73% rename from user-script/manifest.json rename to content-script-register/manifest.json index b233b836..c59c14a7 100644 --- a/user-script/manifest.json +++ b/content-script-register/manifest.json @@ -1,12 +1,12 @@ { "manifest_version": 2, - "name": "User script", + "name": "Content script registration", "version": "1.1", "browser_specific_settings": { "gecko": { - "id": "user-script-example@mozilla.org", + "id": "content-script-example@mozilla.org", "strict_min_version": "59.0a1" } }, @@ -24,8 +24,8 @@ "default_icon": { "32" : "icons/if_source_code_103710.svg" }, - "default_title": "User script", - "default_popup": "popup/user-script.html", + "default_title": "Content script", + "default_popup": "popup/content-script.html", "browser_style": true }, diff --git a/user-script/popup/user-script.css b/content-script-register/popup/content-script.css similarity index 100% rename from user-script/popup/user-script.css rename to content-script-register/popup/content-script.css diff --git a/user-script/popup/user-script.html b/content-script-register/popup/content-script.html similarity index 77% rename from user-script/popup/user-script.html rename to content-script-register/popup/content-script.html index 30739628..328696d5 100644 --- a/user-script/popup/user-script.html +++ b/content-script-register/popup/content-script.html @@ -3,7 +3,7 @@ - + @@ -20,7 +20,7 @@ - + diff --git a/user-script/popup/user-script.js b/content-script-register/popup/content-script.js similarity index 100% rename from user-script/popup/user-script.js rename to content-script-register/popup/content-script.js diff --git a/examples.json b/examples.json index b3864c55..d952b100 100644 --- a/examples.json +++ b/examples.json @@ -94,6 +94,15 @@ ], "name": "commands" }, + { + "description": "Illustrates how an extension can register URL-matching content scripts at runtime.", + "javascript_apis": [ + "contentScripts.register", + "runtime.onMessage", + "runtime.sendMessage" + ], + "name": "content-script-register" + }, { "description": "Add a context menu option to links to copy the link to the clipboard, as plain text and as a link in rich HTML.", "javascript_apis": [ @@ -546,13 +555,13 @@ "name": "user-agent-rewriter" }, { - "description": "Illustrates how an extension can register URL-matching content scripts at runtime.", + "description": "Illustrates how an extension can register URL-matching user scripts at runtime.", "javascript_apis": [ - "contentScripts.register", + "userScripts.register", "runtime.onMessage", "runtime.sendMessage" ], - "name": "user-script" + "name": "user-script-register" }, { "description": "Demonstrates how to use webpack to package npm modules in an extension.", diff --git a/user-script-register/README.md b/user-script-register/README.md new file mode 100644 index 00000000..2f111d87 --- /dev/null +++ b/user-script-register/README.md @@ -0,0 +1,20 @@ +# User script registration + +This extension demonstrates the [`browser.userScripts.register()`](https://wiki.developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts/Register) API. + +The extension includes an [API script](https://wiki.developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/user_scripts) (`customUserScriptAPIs.js`) that enables user scripts to make use of `browser.storage.local`. + +To enable a user script to be specified and registered, this extension includes a sidebar action. The sidebar enables you to define the following properties that control the execution of a registered script, with default values provided: + +* the host pattern `*://*.org/*`, which loads the script into any HTTP or HTTPS pages on a `.org` domain. +* script code that replaces the content of the pattern matched page with the message "This page has been eaten". The script also uses the API script stubs to save and recall the URL of each page "eaten". Information on the last and current "eaten" page is then included in the "eaten" message. +* a script ID that is stored in the user script metadata and then used in the API script to store separate values for each registered script. + +All other properties use their default value. + +Clicking "Register script" registers the script by calling `browser.userScripts.register()`. + +In this example, only one script can be registered at a time: registering a new script unregisters the active script. The extension does this by keeping a reference to the `RegisteredUserScript` object returned from `browser.userScripts.register()`: this object provides the `unregister()` method. + +To try the extension, click "Register script" with the defaults and load http://example.org/ or +https://www.mozilla.org/. Then change the pattern or the code and reload these or related pages. diff --git a/user-script-register/background.js b/user-script-register/background.js new file mode 100644 index 00000000..cb793111 --- /dev/null +++ b/user-script-register/background.js @@ -0,0 +1,24 @@ +'use strict'; + +let registered = null; + +async function registerScript(message) { + const { + hosts, + code, + userScriptID, + } = message; + + if (registered) { + await registered.unregister(); + registered = null; + } + + registered = await browser.userScripts.register({ + matches: hosts, + js: [{code}], + scriptMetadata: {userScriptID}, + }); +} + +browser.runtime.onMessage.addListener(registerScript); diff --git a/user-script-register/customUserScriptAPIs.js b/user-script-register/customUserScriptAPIs.js new file mode 100644 index 00000000..3f067066 --- /dev/null +++ b/user-script-register/customUserScriptAPIs.js @@ -0,0 +1,27 @@ + +browser.userScripts.onBeforeScript.addListener(script => { + + const scriptMetadata = script.metadata; + const id = scriptMetadata.userScriptID; + + function getScopedName(name) { + return `${id}:${name}`; + } + + script.defineGlobals({ + async GM_getValue(name) { + const scopedName = getScopedName(name); + const res = await browser.storage.local.get(scopedName); + console.log("GM_getValue", {id, name, res, scriptMetadata}); + return res[scopedName]; + }, + GM_setValue(name, value) { + console.log("GM_setValue", {id, name, value, scriptMetadata}); + return browser.storage.local.set({[getScopedName(name)]: value}); + }, + }); + + console.log("custom userScripts APIs defined"); +}); + +console.log("apiScript executed and userScripts.onBeforeScript listener subscribed"); \ No newline at end of file diff --git a/user-script-register/icons/LICENSE b/user-script-register/icons/LICENSE new file mode 100644 index 00000000..8b397ff0 --- /dev/null +++ b/user-script-register/icons/LICENSE @@ -0,0 +1 @@ +The icon "if_source_code_103710.svg" is from picol.org (http://www.picol.org/) and is used under the terms of the Creative Commons Attribution license: http://creativecommons.org/licenses/by/3.0/. diff --git a/user-script-register/icons/if_source_code_103710.svg b/user-script-register/icons/if_source_code_103710.svg new file mode 100644 index 00000000..503d9537 --- /dev/null +++ b/user-script-register/icons/if_source_code_103710.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/user-script-register/manifest.json b/user-script-register/manifest.json new file mode 100644 index 00000000..d3f45dec --- /dev/null +++ b/user-script-register/manifest.json @@ -0,0 +1,40 @@ +{ + + "manifest_version": 2, + "name": "User script registration", + "version": "1.0", + + "browser_specific_settings": { + "gecko": { + "id": "user-script-example@mozilla.org", + "strict_min_version": "65.0" + } + }, + + "description": "Demonstration of userScripts.register.", + "icons": { + "48": "icons/if_source_code_103710.svg" + }, + + "permissions": [ + "", + "storage" + ], + + "sidebar_action": { + "default_icon": { + "32" : "icons/if_source_code_103710.svg" + }, + "default_title": "User script", + "default_panel": "popup/user-script.html", + "browser_style": true + }, + + "background": { + "scripts": ["background.js"] + }, + + "user_scripts": { + "api_script": "customUserScriptAPIs.js" + } +} diff --git a/user-script-register/popup/user-script.css b/user-script-register/popup/user-script.css new file mode 100644 index 00000000..3aaa614e --- /dev/null +++ b/user-script-register/popup/user-script.css @@ -0,0 +1,16 @@ +body { + padding: 1.0em; +} + +input { + width: 100%; + margin-bottom: 1em; +} + +textarea { + width: 100%; + resize: none; + border: 1px solid #e4e4e4; + margin-bottom: 1em; + min-height: 380px; +} diff --git a/user-script-register/popup/user-script.html b/user-script-register/popup/user-script.html new file mode 100644 index 00000000..00838ab6 --- /dev/null +++ b/user-script-register/popup/user-script.html @@ -0,0 +1,33 @@ + + + + + + + + + + +
+ + + + + + + + + + + + +
+
+
+
+
+ + + + + diff --git a/user-script-register/popup/user-script.js b/user-script-register/popup/user-script.js new file mode 100644 index 00000000..389c9627 --- /dev/null +++ b/user-script-register/popup/user-script.js @@ -0,0 +1,97 @@ +'use strict'; + +const hostsInput = document.querySelector("#hosts"); +const codeInput = document.querySelector("#code"); +const userScriptIDInput = document.querySelector("#userScriptID"); +const lastErrorEl = document.querySelector("#lastError"); +const lastResultEl = document.querySelector("#lastResult"); + +const defaultHosts = "*://*.org/*"; +const defaultCode = `(async function () { + console.log("USER SCRIPT EXECUTING on", window.location.href, { + documentReadyState: document.readyState, + }); + const oldStoredValue = await GM_getValue("testkey"); + await GM_setValue("testkey", window.location.href); + const newStoredValue = await GM_getValue("testkey"); + + const overwriteBody = () => { + document.body.innerHTML = \`

This page has been eaten: $\{JSON.stringify({oldStoredValue, newStoredValue})}

\` + } + + if (document.body) { + overwriteBody(); + } else { + window.addEventListener("load", overwriteBody, {once: true}); + } +})();`; +const defaultUserScriptID = "user_script_01"; + +hostsInput.value = defaultHosts; +codeInput.value = defaultCode; +userScriptIDInput.value = defaultUserScriptID; + +async function loadLastSetValues() { + const params = await browser.storage.local.get(); + + const { + hosts, + code, + userScriptID, + } = params.lastSetValues || {}; + + hostsInput.value = hosts ? hosts.join(",") : defaultHosts; + codeInput.value = code ? code : defaultCode; + userScriptIDInput.value = userScriptID ? userScriptID : defaultUserScriptID; + + lastErrorEl.textContent = params.lastError || ""; +} + +function stringToArray(value) { + const res = value.split(",").map(el => el.trim()).filter(el => el !== ""); + + return res.length > 0 ? res : null; +} + +async function registerScript() { + const params = { + hosts: stringToArray(hostsInput.value), + code: codeInput.value, + userScriptID: userScriptID.value, + }; + + // Store the last submitted values to the extension storage + // (so that they can be restored when the popup is opened + // the next time). + await browser.storage.local.set({ + lastSetValues: params, + }); + + try { + // Clear the last userScripts.register result. + lastResultEl.textContent = ""; + + await browser.runtime.sendMessage(params); + lastResultEl.textContent = "UserScript successfully registered"; + // Clear the last userScripts.register error. + lastErrorEl.textContent = ""; + + // Clear the last error stored. + await browser.storage.local.remove("lastError"); + } catch (e) { + // There was an error on registering the contentScript, + // let's show the error message in the popup and store + // the last error into the extension storage. + + const lastError = `${e}`; + // Show the last userScripts.register error. + lastErrorEl.textContent = lastError; + + // Store the last error. + await browser.storage.local.set({lastError}); + } +} + +loadLastSetValues(); + +document.querySelector("#register").addEventListener('click', registerScript);