Skip to content

Commit

Permalink
New user script example (mdn#438)
Browse files Browse the repository at this point in the history
* New user script example

This PR is the content from [PR 426](mdn#426), originally submitted by Irene, addressing the feedback on the readme content.

* Including the following changes:
* renaming the content script example and updating the readme file to reference the user script example
* renaming the user script example and versioning as v1
* addressing feedback on the user script example: limiting input to hosts and script, addressing code and HTML/CSS comments

I haven't changed the "eating" script example, I feel it is useful to have similar demonstrations in the content and user script examples to make it easier for developers to compare and contrast.

* Various changes for feedback

* Added a user script ID that is stored in the registered script's metadata and then used in the API script to key the data stored in local storage

* Update to emphasize scoping of local storage using an ID provided in the user script metadata

Co-authored-by: Richard Bloor <rbloor@atlassian.com>
  • Loading branch information
rebloor and Richard Bloor authored Mar 19, 2020
1 parent b89c53a commit 177f603
Show file tree
Hide file tree
Showing 18 changed files with 285 additions and 15 deletions.
14 changes: 8 additions & 6 deletions user-script/README.md → content-script-register/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
# 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
* one or more [match patterns](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Match_patterns), comma-separated. The content script will only be loaded into pages whose URLs match these patterns.

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 = '<h1>This page has been eaten<h1>'`

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.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -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"
}
},
Expand All @@ -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
},

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="user-script.css"/>
<link rel="stylesheet" href="content-script.css"/>
</head>

<body>
Expand All @@ -20,7 +20,7 @@

</div>

<script src="user-script.js"></script>
<script src="content-script.js"></script>
</body>

</html>
File renamed without changes.
15 changes: 12 additions & 3 deletions examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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.",
Expand Down
20 changes: 20 additions & 0 deletions user-script-register/README.md
Original file line number Diff line number Diff line change
@@ -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.
24 changes: 24 additions & 0 deletions user-script-register/background.js
Original file line number Diff line number Diff line change
@@ -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);
27 changes: 27 additions & 0 deletions user-script-register/customUserScriptAPIs.js
Original file line number Diff line number Diff line change
@@ -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");
1 change: 1 addition & 0 deletions user-script-register/icons/LICENSE
Original file line number Diff line number Diff line change
@@ -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/.
1 change: 1 addition & 0 deletions user-script-register/icons/if_source_code_103710.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions user-script-register/manifest.json
Original file line number Diff line number Diff line change
@@ -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": [
"<all_urls>",
"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"
}
}
16 changes: 16 additions & 0 deletions user-script-register/popup/user-script.css
Original file line number Diff line number Diff line change
@@ -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;
}
33 changes: 33 additions & 0 deletions user-script-register/popup/user-script.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!DOCTYPE html>

<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="user-script.css"/>
</head>

<body>

<div>

<label for="hosts">Hosts</label>
<input id="hosts" type="text">

<label for="code">Code</label>
<textarea id="code" rows="10"></textarea>

<label for="userScriptID">User Script ID</label>
<input id="userScriptID" type="text">

<button id="register">Register script</button>

<div id="lastError" style="color: red;">
</div>
<div id="lastResult" style="color: blue;">
</div>
</div>

<script src="user-script.js"></script>
</body>

</html>
97 changes: 97 additions & 0 deletions user-script-register/popup/user-script.js
Original file line number Diff line number Diff line change
@@ -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 = \`<h1>This page has been eaten: $\{JSON.stringify({oldStoredValue, newStoredValue})}<h1>\`
}
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);

0 comments on commit 177f603

Please sign in to comment.