Merge remote-tracking branch 'fork/configurable-hosts-via-options'

This commit is contained in:
Sebastian Sauer 2023-07-13 18:24:46 +02:00
commit 8dc05773e2
6 changed files with 338 additions and 15 deletions

@ -40,11 +40,11 @@ Recently chrome disallowed to install packed `crx` extension that are not listed
- On Firefox: enter `about:debugging#/runtime/this-firefox` into the address bar
- In the Extension page find `conventional comments button` and hit the reload button
## How to run it on a self-hosted instance
## How to enable it on a self-hosted instance
- Open manifest.json
- Add your domain to `permissions` and `content_scripts -> matches`
- Open the browser and install or update the extension
- Open the extension options
- Add your domain as a new line in the "Enabled Hosts" field
- Click Save
## Credits

@ -1,6 +1,6 @@
{
"name": "Conventional comments button",
"version": "0.0.3",
"version": "0.0.1",
"manifest_version": 3,
"description": "An extension to quickly add conventional comments",
"homepage_url": "https://conventionalcomments.org/",
@ -10,14 +10,19 @@
"128": "icons/icon128.png"
},
"default_locale": "en",
"permissions": ["scripting"],
"host_permissions": ["https://gitlab.com/*"],
"content_scripts": [
{
"matches": ["https://gitlab.com/*"],
"js": ["src/inject/inject.js"],
"css": ["src/inject/inject.css"],
"run_at": "document_idle"
}
]
"action": {},
"options_ui": {
"page": "src/options.html"
},
"permissions": [
"activeTab",
"storage",
"scripting"
],
"host_permissions": [
"*://*/"
],
"background": {
"service_worker": "src/background.js"
}
}

41
src/background.js Normal file

@ -0,0 +1,41 @@
const scriptId = "conventional-comments-button";
const defaultHost = 'https://gitlab.com';
async function registerContentScripts(hosts) {
hosts = hosts.split('\n');
for (var index in hosts) {
hosts[index] = hosts[index].trim() + "/*";
}
await chrome.scripting.registerContentScripts([{
id: scriptId,
matches: hosts,
js: ["src/inject/inject.js"],
css: ["src/inject/inject.css"],
runAt: "document_idle",
}]);
}
chrome.action.onClicked.addListener(() => {
chrome.runtime.openOptionsPage();
})
chrome.storage.sync.get({ hosts: defaultHost }, async function (result) {
console.log('Hosts is currently ', result);
var hosts = result.hosts;
console.log('Setting hosts to ' + hosts);
registerContentScripts(hosts);
});
chrome.storage.onChanged.addListener(async (changes, areaName) => {
if (changes['hosts']) {
await chrome.scripting.unregisterContentScripts({ ids: [scriptId] });
console.log("Setting new hosts to " + changes['hosts']['newValue']);
await registerContentScripts(changes['hosts']['newValue']);
}
}
)

@ -0,0 +1,233 @@
(function () {
'use strict';
function NestedProxy(target) {
return new Proxy(target, {
get(target, prop) {
if (typeof target[prop] !== 'function') {
return new NestedProxy(target[prop]);
}
return (...arguments_) =>
new Promise((resolve, reject) => {
target[prop](...arguments_, result => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve(result);
}
});
});
}
});
}
const chromeP = globalThis.chrome && new NestedProxy(globalThis.chrome);
const gotScripting = typeof chrome === 'object' && 'scripting' in chrome;
function castTarget(target) {
return typeof target === 'object' ? target : {
tabId: target,
frameId: 0,
};
}
async function executeFunction(target, function_, ...args) {
const { frameId, tabId } = castTarget(target);
if (gotScripting) {
const [injection] = await chrome.scripting.executeScript({
target: {
tabId,
frameIds: [frameId],
},
func: function_,
args,
});
return injection === null || injection === void 0 ? void 0 : injection.result;
}
const [result] = await chromeP.tabs.executeScript(tabId, {
code: `(${function_.toString()})(...${JSON.stringify(args)})`,
frameId,
});
return result;
}
function arrayOrUndefined(value) {
return typeof value === 'undefined' ? undefined : [value];
}
function insertCSS({ tabId, frameId, files, allFrames, matchAboutBlank, runAt, }) {
for (let content of files) {
if (typeof content === 'string') {
content = { file: content };
}
if (gotScripting) {
void chrome.scripting.insertCSS({
target: {
tabId,
frameIds: arrayOrUndefined(frameId),
allFrames,
},
files: 'file' in content ? [content.file] : undefined,
css: 'code' in content ? content.code : undefined,
});
}
else {
void chromeP.tabs.insertCSS(tabId, {
...content,
matchAboutBlank,
allFrames,
frameId,
runAt: runAt !== null && runAt !== void 0 ? runAt : 'document_start',
});
}
}
}
async function executeScript({ tabId, frameId, files, allFrames, matchAboutBlank, runAt, }) {
let lastInjection;
for (let content of files) {
if (typeof content === 'string') {
content = { file: content };
}
if (gotScripting) {
if ('code' in content) {
throw new Error('chrome.scripting does not support injecting strings of `code`');
}
void chrome.scripting.executeScript({
target: {
tabId,
frameIds: arrayOrUndefined(frameId),
allFrames,
},
files: [content.file],
});
}
else {
if ('code' in content) {
await lastInjection;
}
lastInjection = chromeP.tabs.executeScript(tabId, {
...content,
matchAboutBlank,
allFrames,
frameId,
runAt,
});
}
}
}
const patternValidationRegex = /^(https?|wss?|file|ftp|\*):\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^file:\/\/\/.*$|^resource:\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^about:/;
const isFirefox = typeof navigator === 'object' && navigator.userAgent.includes('Firefox/');
const allStarsRegex = isFirefox ? /^(https?|wss?):[/][/][^/]+([/].*)?$/ : /^https?:[/][/][^/]+([/].*)?$/;
const allUrlsRegex = /^(https?|file|ftp):[/]+/;
function getRawRegex(matchPattern) {
if (!patternValidationRegex.test(matchPattern)) {
throw new Error(matchPattern + ' is an invalid pattern, it must match ' + String(patternValidationRegex));
}
let [, protocol, host, pathname] = matchPattern.split(/(^[^:]+:[/][/])([^/]+)?/);
protocol = protocol
.replace('*', isFirefox ? '(https?|wss?)' : 'https?')
.replace(/[/]/g, '[/]');
host = (host !== null && host !== void 0 ? host : '')
.replace(/^[*][.]/, '([^/]+.)*')
.replace(/^[*]$/, '[^/]+')
.replace(/[.]/g, '[.]')
.replace(/[*]$/g, '[^.]+');
pathname = pathname
.replace(/[/]/g, '[/]')
.replace(/[.]/g, '[.]')
.replace(/[*]/g, '.*');
return '^' + protocol + host + '(' + pathname + ')?$';
}
function patternToRegex(...matchPatterns) {
if (matchPatterns.length === 0) {
return /$./;
}
if (matchPatterns.includes('<all_urls>')) {
return allUrlsRegex;
}
if (matchPatterns.includes('*://*/*')) {
return allStarsRegex;
}
return new RegExp(matchPatterns.map(x => getRawRegex(x)).join('|'));
}
const gotNavigation = typeof chrome === 'object' && 'webNavigation' in chrome;
async function isOriginPermitted(url) {
return chromeP.permissions.contains({
origins: [new URL(url).origin + '/*'],
});
}
async function wasPreviouslyLoaded(target, assets) {
const loadCheck = (key) => {
const wasLoaded = document[key];
document[key] = true;
return wasLoaded;
};
return executeFunction(target, loadCheck, JSON.stringify(assets));
}
async function registerContentScript(contentScriptOptions, callback) {
const { js = [], css = [], matchAboutBlank, matches, excludeMatches, runAt, } = contentScriptOptions;
let { allFrames } = contentScriptOptions;
if (gotNavigation) {
allFrames = false;
}
else if (allFrames) {
console.warn('`allFrames: true` requires the `webNavigation` permission to work correctly: https://github.com/fregante/content-scripts-register-polyfill#permissions');
}
const matchesRegex = patternToRegex(...matches);
const excludeMatchesRegex = patternToRegex(...excludeMatches !== null && excludeMatches !== void 0 ? excludeMatches : []);
const inject = async (url, tabId, frameId = 0) => {
if (!matchesRegex.test(url)
|| excludeMatchesRegex.test(url)
|| !await isOriginPermitted(url)
|| await wasPreviouslyLoaded({ tabId, frameId }, { js, css })
) {
return;
}
insertCSS({
tabId,
frameId,
files: css,
matchAboutBlank,
runAt,
});
await executeScript({
tabId,
frameId,
files: js,
matchAboutBlank,
runAt,
});
};
const tabListener = async (tabId, { status }, { url }) => {
if (status && url) {
void inject(url, tabId);
}
};
const navListener = async ({ tabId, frameId, url, }) => {
void inject(url, tabId, frameId);
};
if (gotNavigation) {
chrome.webNavigation.onCommitted.addListener(navListener);
}
else {
chrome.tabs.onUpdated.addListener(tabListener);
}
const registeredContentScript = {
async unregister() {
if (gotNavigation) {
chrome.webNavigation.onCommitted.removeListener(navListener);
}
else {
chrome.tabs.onUpdated.removeListener(tabListener);
}
},
};
if (typeof callback === 'function') {
callback(registeredContentScript);
}
return registeredContentScript;
}
if (typeof chrome === 'object' && !chrome.contentScripts) {
chrome.contentScripts = { register: registerContentScript };
}
}());

16
src/options.html Normal file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head><title>Coventional Comments Button Options</title></head>
<body>
<label>
Enabled Hosts (one per line):
</label>
<textarea id="hosts" rows="5" cols="49"></textarea>
<div id="status"></div>
<button id="save">Save</button>
<script src="options.js"></script>
</body>
</html>

28
src/options.js Normal file

@ -0,0 +1,28 @@
// Saves options to chrome.storage
function save_options() {
var hosts = document.getElementById('hosts').value;
chrome.storage.sync.set({
hosts: hosts,
}, function() {
// Update status to let user know options were saved.
var status = document.getElementById('status');
status.textContent = 'Options saved.';
setTimeout(function() {
status.textContent = '';
}, 750);
});
}
// Restores select box and checkbox state using the preferences
// stored in chrome.storage.
function restore_options() {
// Use default value color = 'red' and likesColor = true.
chrome.storage.sync.get({
hosts: 'https://gitlab.com'
}, function(items) {
document.getElementById('hosts').value = items.hosts;
});
}
document.addEventListener('DOMContentLoaded', restore_options);
document.getElementById('save').addEventListener('click',
save_options);