aboutsummaryrefslogtreecommitdiff
path: root/modules/firefox.home
diff options
context:
space:
mode:
authorAkshay Nair <phenax5@gmail.com>2024-08-19 14:36:34 +0530
committerAkshay Nair <phenax5@gmail.com>2024-08-19 14:36:34 +0530
commit6206aaf88532d153f0e01e22772b7f6e1c979afd (patch)
tree15573ffa93088ba2c10e86af2675045086975555 /modules/firefox.home
parent02612ccd49d561808ff931611b2ea865038b0a51 (diff)
downloadnixos-config-6206aaf88532d153f0e01e22772b7f6e1c979afd.tar.gz
nixos-config-6206aaf88532d153f0e01e22772b7f6e1c979afd.zip
Enabled user js for firefox + global keybindings manager
Diffstat (limited to 'modules/firefox.home')
-rw-r--r--modules/firefox.home/autoconfig.js24
-rw-r--r--modules/firefox.home/chrome/globalKeybindings.uc.js102
-rw-r--r--modules/firefox.home/chrome/setupNewTab.uc.js20
-rw-r--r--modules/firefox.home/chrome/userChrome.css (renamed from modules/firefox.home/userChrome.css)0
-rw-r--r--modules/firefox.home/chrome/utils/BootstrapLoader.js419
-rw-r--r--modules/firefox.home/chrome/utils/RDFDataSource.sys.mjs434
-rw-r--r--modules/firefox.home/chrome/utils/RDFManifestConverter.sys.mjs99
-rw-r--r--modules/firefox.home/chrome/utils/chrome.manifest2
-rw-r--r--modules/firefox.home/chrome/utils/userChrome.jsm231
-rw-r--r--modules/firefox.home/chrome/utils/xPref.jsm85
-rw-r--r--modules/firefox.home/default.nix73
-rw-r--r--modules/firefox.home/tridactyl/tridactylrc56
12 files changed, 1489 insertions, 56 deletions
diff --git a/modules/firefox.home/autoconfig.js b/modules/firefox.home/autoconfig.js
new file mode 100644
index 0000000..cc74675
--- /dev/null
+++ b/modules/firefox.home/autoconfig.js
@@ -0,0 +1,24 @@
+// Autoconfig
+
+pref('general.config.sandbox_enabled', false);
+pref('general.config.obscure_value', 0);
+lockPref('xpinstall.signatures.required', false);
+lockPref('extensions.install_origins.enabled', false);
+lockPref("extensions.experiments.enabled", true);
+
+// Setup user chrome support
+try {
+ const cmanifest = Services.dirsvc.get('UChrm', Ci.nsIFile);
+ cmanifest.append('utils');
+ cmanifest.append('chrome.manifest');
+ Components.manager.QueryInterface(Ci.nsIComponentRegistrar).autoRegister(cmanifest);
+} catch (ex) { };
+
+try {
+ Services.scriptloader.loadSubScript('chrome://userchromejs/content/BootstrapLoader.js');
+} catch (ex) { };
+
+try {
+ ChromeUtils.import('chrome://userchromejs/content/userChrome.jsm');
+} catch (ex) { };
+
diff --git a/modules/firefox.home/chrome/globalKeybindings.uc.js b/modules/firefox.home/chrome/globalKeybindings.uc.js
new file mode 100644
index 0000000..95d8a10
--- /dev/null
+++ b/modules/firefox.home/chrome/globalKeybindings.uc.js
@@ -0,0 +1,102 @@
+// ==UserScript==
+// @name Global Keybindings
+// @version 1.0
+// @description Setup global keybindings on all windows
+// @startup UC.globalKeybindings.init(win);
+// @shutdown UC.globalKeybindings.destroy();
+// ==/UserScript==
+
+
+(() => {
+ // Configure global keybindings
+ const KEYBINDINGS = () => ({
+ RELEASE: [
+ [ctrl(key('j')), nextTab()],
+ [ctrl(key('k')), prevTab()],
+ [alt(key('j')), moveSelectedTabBy(+1)],
+ [alt(key('k')), moveSelectedTabBy(-1)],
+ [ctrl(shift(key('J'))), moveSelectedTabBy(+1)],
+ [ctrl(shift(key('K'))), moveSelectedTabBy(-1)],
+
+ ...(Array.from({ length: 10 }, (_, idx) =>
+ [ctrl(key(idx === 9 ? 0 : idx + 1)), tabIndex(idx)],
+ )),
+ ],
+ PRESS: [
+ // Prevent the default browser's action
+ [ctrl(shift(key('J'))), preventDefault()],
+ [ctrl(shift(key('K'))), preventDefault()],
+ ],
+ });
+
+ const nextTab = () => () => updateTabIndex((n, len) => (n + 1) % len);
+ const prevTab = () => () => updateTabIndex((n, len) => n === 0 ? len - 1 : n - 1);
+ const tabIndex = idx => () => updateTabIndex((_n, _len) => idx)
+ const preventDefault = f => e => { e.preventDefault(); f?.(e); };
+ const moveSelectedTabBy = incr => () => updateSelectedTabIndex(incr);
+
+ const ctrl = b => e => b(e) && e.ctrlKey;
+ const alt = b => e => b(e) && e.altKey;
+ const shift = b => e => b(e) && e.shiftKey;
+ const key = key => e => `${e.key}` === `${key}`;
+
+ const updateTabIndex = (f) => {
+ const newIndex = f(gBrowser.tabContainer.selectedIndex, gBrowser.tabs.length)
+ if (typeof newIndex !== 'number') return;
+ if (newIndex >= gBrowser.tabContainer.allTabs.length) return;
+ if (newIndex < 0) return;
+ gBrowser.selectTabAtIndex(newIndex);
+ };
+
+ const updateSelectedTabIndex = (incr) => {
+ if (typeof incr !== 'number') return;
+ if (incr === +1) return gBrowser.moveTabForward();
+ if (incr === -1) return gBrowser.moveTabBackward();
+ const newIndex = gBrowser.tabContainer.selectedIndex + incr;
+ if (newIndex >= gBrowser.tabContainer.allTabs.length) return;
+ if (newIndex < 0) return;
+ gBrowser.moveTabTo(gBrowser.selectedTab, newIndex)
+ };
+
+ const module = {
+ enabled: true,
+
+ evaluateKeybindings: (bindings, e) => {
+ if (!module.enabled) return;
+
+ for (const [isKey, action] of bindings) {
+ if (isKey(e)) {
+ const shouldContinue = action(e);
+ if (!shouldContinue) break;
+ }
+ }
+ },
+
+ handleKeyUpEvent: e => module.evaluateKeybindings(KEYBINDINGS().RELEASE, e),
+ handleKeyDownEvent: e => module.evaluateKeybindings(KEYBINDINGS().PRESS, e),
+
+ init(win) {
+ let observe = () => {
+ Services.obs.removeObserver(observe, 'browser-window-before-show');
+ win.addEventListener('keyup', module.handleKeyUpEvent, true);
+ win.addEventListener('keydown', module.handleKeyDownEvent, true);
+ }
+
+ if (win.__SSi) {
+ win.addEventListener('keyup', module.handleKeyUpEvent, true);
+ win.addEventListener('keydown', module.handleKeyDownEvent, true);
+ } else {
+ Services.obs.addObserver(observe, 'browser-window-before-show');
+ }
+ },
+ destroy() {
+ _uc.windows((_doc, win) => {
+ win.removeEventListener('keyup', module.handleKeyUpEvent, true);
+ win.removeEventListener('keydown', module.handleKeyDownEvent, true);
+ });
+ delete UC.globalKeybindings;
+ },
+ };
+
+ UC.globalKeybindings = module;
+})();
diff --git a/modules/firefox.home/chrome/setupNewTab.uc.js b/modules/firefox.home/chrome/setupNewTab.uc.js
new file mode 100644
index 0000000..1eb971c
--- /dev/null
+++ b/modules/firefox.home/chrome/setupNewTab.uc.js
@@ -0,0 +1,20 @@
+// ==UserScript==
+// @name Custom New Tab
+// @version 1.0
+// @description Load a custom link or local file, instead of the default new tab page (about:newtab).
+// ==/UserScript==
+
+// For Firefox 72 onward, see the autoconfig alternative to this:
+// https://support.mozilla.org/questions/1251199#answer-1199709
+
+globalThis.newtabLinkStarted = true;
+
+if (!AboutNewTab) {
+ globalThis.AboutNewTab = ChromeUtils.import('resource:///modules/AboutNewTab.jsm').AboutNewTab;
+}
+
+const newTabUrl = xPref.get('browser.newtab.url') || 'about:blank';
+
+AboutNewTab.newTabURL = newTabUrl;
+
+console.log('Updated new tab url to', newTabUrl);
diff --git a/modules/firefox.home/userChrome.css b/modules/firefox.home/chrome/userChrome.css
index 67809b0..67809b0 100644
--- a/modules/firefox.home/userChrome.css
+++ b/modules/firefox.home/chrome/userChrome.css
diff --git a/modules/firefox.home/chrome/utils/BootstrapLoader.js b/modules/firefox.home/chrome/utils/BootstrapLoader.js
new file mode 100644
index 0000000..eddc1de
--- /dev/null
+++ b/modules/firefox.home/chrome/utils/BootstrapLoader.js
@@ -0,0 +1,419 @@
+/* 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';
+
+ChromeUtils.defineESModuleGetters(this, {
+ Blocklist: 'resource://gre/modules/Blocklist.sys.mjs',
+ ConsoleAPI: 'resource://gre/modules/Console.sys.mjs',
+ InstallRDF: 'chrome://userchromejs/content/RDFManifestConverter.sys.mjs',
+});
+
+Services.obs.addObserver(doc => {
+ if (doc.location.protocol + doc.location.pathname === 'about:addons' ||
+ doc.location.protocol + doc.location.pathname === 'chrome:/content/extensions/aboutaddons.html') {
+ const win = doc.defaultView;
+ let handleEvent_orig = win.customElements.get('addon-card').prototype.handleEvent;
+ win.customElements.get('addon-card').prototype.handleEvent = function (e) {
+ if (e.type === 'click' &&
+ e.target.getAttribute('action') === 'preferences' &&
+ this.addon.__AddonInternal__.optionsType == 1/*AddonManager.OPTIONS_TYPE_DIALOG*/) {
+ var windows = Services.wm.getEnumerator(null);
+ while (windows.hasMoreElements()) {
+ var win2 = windows.getNext();
+ if (win2.closed) {
+ continue;
+ }
+ if (win2.document.documentURI == this.addon.optionsURL) {
+ win2.focus();
+ return;
+ }
+ }
+ var features = 'chrome,titlebar,toolbar,centerscreen';
+ win.docShell.rootTreeItem.domWindow.openDialog(this.addon.optionsURL, this.addon.id, features);
+ } else {
+ handleEvent_orig.apply(this, arguments);
+ }
+ }
+ let update_orig = win.customElements.get('addon-options').prototype.update;
+ win.customElements.get('addon-options').prototype.update = function (card, addon) {
+ update_orig.apply(this, arguments);
+ if (addon.__AddonInternal__.optionsType == 1/*AddonManager.OPTIONS_TYPE_DIALOG*/)
+ this.querySelector('panel-item[data-l10n-id="preferences-addon-button"]').hidden = false;
+ }
+ }
+}, 'chrome-document-loaded');
+
+const { AddonManager } = ChromeUtils.importESModule('resource://gre/modules/AddonManager.sys.mjs');
+const { XPIDatabase, AddonInternal } = ChromeUtils.importESModule('resource://gre/modules/addons/XPIDatabase.sys.mjs');
+const { XPIExports } = ChromeUtils.importESModule('resource://gre/modules/addons/XPIExports.sys.mjs');
+
+var orig_verifyBundleSignedState = XPIExports.verifyBundleSignedState;
+XPIExports.verifyBundleSignedState = async (aBundle, aAddon) => {
+ if(!aAddon.isWebExtension && aAddon.type === "extension") return { signedState: undefined, signedTypes: [] };
+ return orig_verifyBundleSignedState(aBundle, aAddon);
+}
+
+XPIDatabase.isDisabledLegacy = () => false;
+
+ChromeUtils.defineLazyGetter(this, 'BOOTSTRAP_REASONS', () => {
+ const { XPIProvider } = ChromeUtils.importESModule('resource://gre/modules/addons/XPIProvider.sys.mjs');
+ return XPIProvider.BOOTSTRAP_REASONS;
+});
+
+const { Log } = ChromeUtils.importESModule('resource://gre/modules/Log.sys.mjs');
+var logger = Log.repository.getLogger('addons.bootstrap');
+
+/**
+ * Valid IDs fit this pattern.
+ */
+var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i;
+
+// Properties that exist in the install manifest
+const PROP_METADATA = ['id', 'version', 'type', 'internalName', 'updateURL',
+ 'optionsURL', 'optionsType', 'aboutURL', 'iconURL'];
+const PROP_LOCALE_SINGLE = ['name', 'description', 'creator', 'homepageURL'];
+const PROP_LOCALE_MULTI = ['developers', 'translators', 'contributors'];
+
+// Map new string type identifiers to old style nsIUpdateItem types.
+// Retired values:
+// 32 = multipackage xpi file
+// 8 = locale
+// 256 = apiextension
+// 128 = experiment
+// theme = 4
+const TYPES = {
+ extension: 2,
+ dictionary: 64,
+};
+
+const COMPATIBLE_BY_DEFAULT_TYPES = {
+ extension: true,
+ dictionary: true,
+};
+
+const hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty);
+
+function isXPI(filename) {
+ let ext = filename.slice(-4).toLowerCase();
+ return ext === '.xpi' || ext === '.zip';
+}
+
+/**
+ * Gets an nsIURI for a file within another file, either a directory or an XPI
+ * file. If aFile is a directory then this will return a file: URI, if it is an
+ * XPI file then it will return a jar: URI.
+ *
+ * @param {nsIFile} aFile
+ * The file containing the resources, must be either a directory or an
+ * XPI file
+ * @param {string} aPath
+ * The path to find the resource at, '/' separated. If aPath is empty
+ * then the uri to the root of the contained files will be returned
+ * @returns {nsIURI}
+ * An nsIURI pointing at the resource
+ */
+function getURIForResourceInFile(aFile, aPath) {
+ if (!isXPI(aFile.leafName)) {
+ let resource = aFile.clone();
+ if (aPath)
+ aPath.split('/').forEach(part => resource.append(part));
+
+ return Services.io.newFileURI(resource);
+ }
+
+ return buildJarURI(aFile, aPath);
+}
+
+/**
+ * Creates a jar: URI for a file inside a ZIP file.
+ *
+ * @param {nsIFile} aJarfile
+ * The ZIP file as an nsIFile
+ * @param {string} aPath
+ * The path inside the ZIP file
+ * @returns {nsIURI}
+ * An nsIURI for the file
+ */
+function buildJarURI(aJarfile, aPath) {
+ let uri = Services.io.newFileURI(aJarfile);
+ uri = 'jar:' + uri.spec + '!/' + aPath;
+ return Services.io.newURI(uri);
+}
+
+var BootstrapLoader = {
+ name: 'bootstrap',
+ manifestFile: 'install.rdf',
+ async loadManifest(pkg) {
+ /**
+ * Reads locale properties from either the main install manifest root or
+ * an em:localized section in the install manifest.
+ *
+ * @param {Object} aSource
+ * The resource to read the properties from.
+ * @param {boolean} isDefault
+ * True if the locale is to be read from the main install manifest
+ * root
+ * @param {string[]} aSeenLocales
+ * An array of locale names already seen for this install manifest.
+ * Any locale names seen as a part of this function will be added to
+ * this array
+ * @returns {Object}
+ * an object containing the locale properties
+ */
+ function readLocale(aSource, isDefault, aSeenLocales) {
+ let locale = {};
+ if (!isDefault) {
+ locale.locales = [];
+ for (let localeName of aSource.locales || []) {
+ if (!localeName) {
+ logger.warn('Ignoring empty locale in localized properties');
+ continue;
+ }
+ if (aSeenLocales.includes(localeName)) {
+ logger.warn('Ignoring duplicate locale in localized properties');
+ continue;
+ }
+ aSeenLocales.push(localeName);
+ locale.locales.push(localeName);
+ }
+
+ if (locale.locales.length == 0) {
+ logger.warn('Ignoring localized properties with no listed locales');
+ return null;
+ }
+ }
+
+ for (let prop of [...PROP_LOCALE_SINGLE, ...PROP_LOCALE_MULTI]) {
+ if (hasOwnProperty(aSource, prop)) {
+ locale[prop] = aSource[prop];
+ }
+ }
+
+ return locale;
+ }
+
+ let manifestData = await pkg.readString('install.rdf');
+ let manifest = InstallRDF.loadFromString(manifestData).decode();
+
+ let addon = new AddonInternal();
+ for (let prop of PROP_METADATA) {
+ if (hasOwnProperty(manifest, prop)) {
+ addon[prop] = manifest[prop];
+ }
+ }
+
+ if (!addon.type) {
+ addon.type = 'extension';
+ } else {
+ let type = addon.type;
+ addon.type = null;
+ for (let name in TYPES) {
+ if (TYPES[name] == type) {
+ addon.type = name;
+ break;
+ }
+ }
+ }
+
+ if (!(addon.type in TYPES))
+ throw new Error('Install manifest specifies unknown type: ' + addon.type);
+
+ if (!addon.id)
+ throw new Error('No ID in install manifest');
+ if (!gIDTest.test(addon.id))
+ throw new Error('Illegal add-on ID ' + addon.id);
+ if (!addon.version)
+ throw new Error('No version in install manifest');
+
+ addon.strictCompatibility = (!(addon.type in COMPATIBLE_BY_DEFAULT_TYPES) ||
+ manifest.strictCompatibility == 'true');
+
+ // Only read these properties for extensions.
+ if (addon.type == 'extension') {
+ if (manifest.bootstrap != 'true') {
+ throw new Error('Non-restartless extensions no longer supported');
+ }
+
+ if (addon.optionsType &&
+ addon.optionsType != 1/*AddonManager.OPTIONS_TYPE_DIALOG*/ &&
+ addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_BROWSER &&
+ addon.optionsType != AddonManager.OPTIONS_TYPE_TAB) {
+ throw new Error('Install manifest specifies unknown optionsType: ' + addon.optionsType);
+ }
+
+ if (addon.optionsType)
+ addon.optionsType = parseInt(addon.optionsType);
+ }
+
+ addon.defaultLocale = readLocale(manifest, true);
+
+ let seenLocales = [];
+ addon.locales = [];
+ for (let localeData of manifest.localized || []) {
+ let locale = readLocale(localeData, false, seenLocales);
+ if (locale)
+ addon.locales.push(locale);
+ }
+
+ let dependencies = new Set(manifest.dependencies);
+ addon.dependencies = Object.freeze(Array.from(dependencies));
+
+ let seenApplications = [];
+ addon.targetApplications = [];
+ for (let targetApp of manifest.targetApplications || []) {
+ if (!targetApp.id || !targetApp.minVersion ||
+ !targetApp.maxVersion) {
+ logger.warn('Ignoring invalid targetApplication entry in install manifest');
+ continue;
+ }
+ if (seenApplications.includes(targetApp.id)) {
+ logger.warn('Ignoring duplicate targetApplication entry for ' + targetApp.id +
+ ' in install manifest');
+ continue;
+ }
+ seenApplications.push(targetApp.id);
+ addon.targetApplications.push(targetApp);
+ }
+
+ // Note that we don't need to check for duplicate targetPlatform entries since
+ // the RDF service coalesces them for us.
+ addon.targetPlatforms = [];
+ for (let targetPlatform of manifest.targetPlatforms || []) {
+ let platform = {
+ os: null,
+ abi: null,
+ };
+
+ let pos = targetPlatform.indexOf('_');
+ if (pos != -1) {
+ platform.os = targetPlatform.substring(0, pos);
+ platform.abi = targetPlatform.substring(pos + 1);
+ } else {
+ platform.os = targetPlatform;
+ }
+
+ addon.targetPlatforms.push(platform);
+ }
+
+ addon.userDisabled = false;
+ addon.softDisabled = addon.blocklistState == Blocklist.STATE_SOFTBLOCKED;
+ addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
+
+ addon.userPermissions = null;
+
+ addon.icons = {};
+ if (await pkg.hasResource('icon.png')) {
+ addon.icons[32] = 'icon.png';
+ addon.icons[48] = 'icon.png';
+ }
+
+ if (await pkg.hasResource('icon64.png')) {
+ addon.icons[64] = 'icon64.png';
+ }
+
+ Object.defineProperty(addon, 'appDisabled', {
+ set: _ => {},
+ get: _ => false
+ });
+
+ Object.defineProperty(addon, 'signedState', {
+ set: _ => {},
+ get: _ => AddonManager.SIGNEDSTATE_NOT_REQUIRED
+ });
+
+ return addon;
+ },
+
+ loadScope(addon) {
+ let file = addon.file || addon._sourceBundle;
+ let uri = getURIForResourceInFile(file, 'bootstrap.js').spec;
+ let principal = Services.scriptSecurityManager.getSystemPrincipal();
+
+ let sandbox = new Cu.Sandbox(principal, {
+ sandboxName: uri,
+ addonId: addon.id,
+ wantGlobalProperties: ['ChromeUtils'],
+ metadata: { addonID: addon.id, URI: uri },
+ });
+
+ try {
+ Object.assign(sandbox, BOOTSTRAP_REASONS);
+
+ ChromeUtils.defineLazyGetter(sandbox, 'console', () =>
+ new ConsoleAPI({ consoleID: `addon/${addon.id}` }));
+
+ Services.scriptloader.loadSubScript(uri, sandbox);
+ } catch (e) {
+ logger.warn(`Error loading bootstrap.js for ${addon.id}`, e);
+ }
+
+ function findMethod(name) {
+ if (sandbox[name]) {
+ return sandbox[name];
+ }
+
+ try {
+ let method = Cu.evalInSandbox(name, sandbox);
+ return method;
+ } catch (err) { }
+
+ return () => {
+ logger.warn(`Add-on ${addon.id} is missing bootstrap method ${name}`);
+ };
+ }
+
+ let install = findMethod('install');
+ let uninstall = findMethod('uninstall');
+ let startup = findMethod('startup');
+ let shutdown = findMethod('shutdown');
+
+ return {
+ install(...args) {
+ install(...args);
+ // Forget any cached files we might've had from this extension.
+ Services.obs.notifyObservers(null, 'startupcache-invalidate');
+ },
+
+ uninstall(...args) {
+ uninstall(...args);
+ // Forget any cached files we might've had from this extension.
+ Services.obs.notifyObservers(null, 'startupcache-invalidate');
+ },
+
+ startup(...args) {
+ if (addon.type == 'extension') {
+ logger.debug(`Registering manifest for ${file.path}\n`);
+ Components.manager.addBootstrappedManifestLocation(file);
+ }
+ return startup(...args);
+ },
+
+ shutdown(data, reason) {
+ try {
+ return shutdown(data, reason);
+ } catch (err) {
+ throw err;
+ } finally {
+ if (reason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
+ logger.debug(`Removing manifest for ${file.path}\n`);
+ Components.manager.removeBootstrappedManifestLocation(file);
+ }
+ }
+ },
+ };
+ },
+};
+
+AddonManager.addExternalExtensionLoader(BootstrapLoader);
+
+if (AddonManager.isReady) {
+ AddonManager.getAllAddons().then(addons => {
+ addons.forEach(addon => {
+ if (addon.type == 'extension' && !addon.isWebExtension && !addon.userDisabled) {
+ addon.reload();
+ };
+ });
+ });
+} \ No newline at end of file
diff --git a/modules/firefox.home/chrome/utils/RDFDataSource.sys.mjs b/modules/firefox.home/chrome/utils/RDFDataSource.sys.mjs
new file mode 100644
index 0000000..c6380c1
--- /dev/null
+++ b/modules/firefox.home/chrome/utils/RDFDataSource.sys.mjs
@@ -0,0 +1,434 @@
+ /* 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/. */
+
+/**
+ * This module creates a new API for accessing and modifying RDF graphs. The
+ * goal is to be able to serialise the graph in a human readable form. Also
+ * if the graph was originally loaded from an RDF/XML the serialisation should
+ * closely match the original with any new data closely following the existing
+ * layout. The output should always be compatible with Mozilla's RDF parser.
+ *
+ * This is all achieved by using a DOM Document to hold the current state of the
+ * graph in XML form. This can be initially loaded and parsed from disk or
+ * a blank document used for an empty graph. As assertions are added to the
+ * graph, appropriate DOM nodes are added to the document to represent them
+ * along with any necessary whitespace to properly layout the XML.
+ *
+ * In general the order of adding assertions to the graph will impact the form
+ * the serialisation takes. If a resource is first added as the object of an
+ * assertion then it will eventually be serialised inside the assertion's
+ * property element. If a resource is first added as the subject of an assertion
+ * then it will be serialised at the top level of the XML.
+ */
+
+const NS_XML = "http://www.w3.org/XML/1998/namespace";
+const NS_XMLNS = "http://www.w3.org/2000/xmlns/";
+const NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+const NS_NC = "http://home.netscape.com/NC-rdf#";
+
+/* eslint prefer-template: 1 */
+
+function isElement(obj) {
+ return Element.isInstance(obj);
+}
+function isText(obj) {
+ return obj && typeof obj == "object" && ChromeUtils.getClassName(obj) == "Text";
+}
+
+/**
+ * Returns either an rdf namespaced attribute or an un-namespaced attribute
+ * value. Returns null if neither exists,
+ */
+function getRDFAttribute(element, name) {
+ if (element.hasAttributeNS(NS_RDF, name))
+ return element.getAttributeNS(NS_RDF, name);
+ if (element.hasAttribute(name))
+ return element.getAttribute(name);
+ return undefined;
+}
+
+/**
+ * Represents an assertion in the datasource
+ */
+class RDFAssertion {
+ constructor(subject, predicate, object) {
+ // The subject on this assertion, an RDFSubject
+ this._subject = subject;
+ // The predicate, a string
+ this._predicate = predicate;
+ // The object, an RDFNode
+ this._object = object;
+ // The datasource this assertion exists in
+ this._ds = this._subject._ds;
+ // Marks that _DOMnode is the subject's element
+ this._isSubjectElement = false;
+ // The DOM node that represents this assertion. Could be a property element,
+ // a property attribute or the subject's element for rdf:type
+ this._DOMNode = null;
+ }
+
+ getPredicate() {
+ return this._predicate;
+ }
+
+ getObject() {
+ return this._object;
+ }
+}
+
+class RDFNode {
+ equals(rdfnode) {
+ return (rdfnode.constructor === this.constructor &&
+ rdfnode._value == this._value);
+ }
+}
+
+/**
+ * A simple literal value
+ */
+export class RDFLiteral extends RDFNode {
+ constructor(value) {
+ super();
+ this._value = value;
+ }
+
+ getValue() {
+ return this._value;
+ }
+}
+
+/**
+ * This is an RDF node that can be a subject so a resource or a blank node
+ */
+class RDFSubject extends RDFNode {
+ constructor(ds) {
+ super();
+ // A lookup of the assertions with this as the subject. Keyed on predicate
+ this._assertions = {};
+ // A lookup of the assertions with this as the object. Keyed on predicate
+ this._backwards = {};
+ // The datasource this subject belongs to
+ this._ds = ds;
+ // The DOM elements in the document that represent this subject. Array of Element
+ this._elements = [];
+ }
+
+ /**
+ * Parses the given Element from the DOM document
+ */
+ /* eslint-disable complexity */
+ _parseElement(element) {
+ this._elements.push(element);
+
+ // There might be an inferred rdf:type assertion in the element name
+ if (element.namespaceURI != NS_RDF ||
+ element.localName != "Description") {
+ var assertion = new RDFAssertion(this, RDF_R("type"),
+ this._ds.getResource(element.namespaceURI + element.localName));
+ assertion._DOMnode = element;
+ assertion._isSubjectElement = true;
+ this._addAssertion(assertion);
+ }
+
+ // Certain attributes can be literal properties
+ for (let attr of element.attributes) {
+ if (attr.namespaceURI == NS_XML || attr.namespaceURI == NS_XMLNS ||
+ attr.nodeName == "xmlns")
+ continue;
+ if ((attr.namespaceURI == NS_RDF || !attr.namespaceURI) &&
+ (["nodeID", "about", "resource", "ID", "parseType"].includes(attr.localName)))
+ continue;
+ var object = null;
+ if (attr.namespaceURI == NS_RDF) {
+ if (attr.localName == "type")
+ object = this._ds.getResource(attr.nodeValue);
+ }
+ if (!object)
+ object = new RDFLiteral(attr.nodeValue);
+ assertion = new RDFAssertion(this, attr.namespaceURI + attr.localName, object);
+ assertion._DOMnode = attr;
+ this._addAssertion(assertion);
+ }
+
+ var child = element.firstChild;
+ element.listCounter = 1;
+ while (child) {
+ if (isElement(child)) {
+ object = null;
+ var predicate = child.namespaceURI + child.localName;
+ if (child.namespaceURI == NS_RDF) {
+ if (child.localName == "li") {
+ predicate = RDF_R(`_${element.listCounter}`);
+ element.listCounter++;
+ }
+ }
+
+ // Check for and bail out on unknown attributes on the property element
+ for (let attr of child.attributes) {
+ // Ignore XML namespaced attributes
+ if (attr.namespaceURI == NS_XML)
+ continue;
+ // These are reserved by XML for future use
+ if (attr.localName.substring(0, 3).toLowerCase() == "xml")
+ continue;
+ // We can handle these RDF attributes
+ if ((!attr.namespaceURI || attr.namespaceURI == NS_RDF) &&
+ ["resource", "nodeID"].includes(attr.localName))
+ continue;
+ // This is a special attribute we handle for compatibility with Mozilla RDF
+ if (attr.namespaceURI == NS_NC &&
+ attr.localName == "parseType")
+ continue;
+ }
+
+ var parseType = child.getAttributeNS(NS_NC, "parseType");
+
+ var resource = getRDFAttribute(child, "resource");
+ var nodeID = getRDFAttribute(child, "nodeID");
+
+ if (resource !== undefined) {
+ var base = Services.io.newURI(element.baseURI);
+ object = this._ds.getResource(base.resolve(resource));
+ } else if (nodeID !== undefined) {
+ object = this._ds.getBlankNode(nodeID);
+ } else {
+ var hasText = false;
+ var childElement = null;
+ var subchild = child.firstChild;
+ while (subchild) {
+ if (isText(subchild) && /\S/.test(subchild.nodeValue)) {
+ hasText = true;
+ } else if (isElement(subchild)) {
+ childElement = subchild;
+ }
+ subchild = subchild.nextSibling;
+ }
+
+ if (childElement) {
+ object = this._ds._getSubjectForElement(childElement);
+ object._parseElement(childElement);
+ } else
+ object = new RDFLiteral(child.textContent);
+ }
+
+ assertion = new RDFAssertion(this, predicate, object);
+ this._addAssertion(assertion);
+ assertion._DOMnode = child;
+ }
+ child = child.nextSibling;
+ }
+ }
+ /* eslint-enable complexity */
+
+ /**
+ * Adds a new assertion to the internal hashes. Should be called for every
+ * new assertion parsed or created programmatically.
+ */
+ _addAssertion(assertion) {
+ var predicate = assertion.getPredicate();
+ if (predicate in this._assertions)
+ this._assertions[predicate].push(assertion);
+ else
+ this._assertions[predicate] = [ assertion ];
+
+ var object = assertion.getObject();
+ if (object instanceof RDFSubject) {
+ // Create reverse assertion
+ if (predicate in object._backwards)
+ object._backwards[predicate].push(assertion);
+ else
+ object._backwards[predicate] = [ assertion ];
+ }
+ }
+
+ /**
+ * Returns all objects in assertions with this subject and the given predicate.
+ */
+ getObjects(predicate) {
+ if (predicate in this._assertions)
+ return Array.from(this._assertions[predicate],
+ i => i.getObject());
+
+ return [];
+ }
+
+ /**
+ * Retrieves the first property value for the given predicate.
+ */
+ getProperty(predicate) {
+ if (predicate in this._assertions)
+ return this._assertions[predicate][0].getObject();
+ return null;
+ }
+}
+
+/**
+ * Creates a new RDFResource for the datasource. Private.
+ */
+export class RDFResource extends RDFSubject {
+ constructor(ds, uri) {
+ super(ds);
+ // This is the uri that the resource represents.
+ this._uri = uri;
+ }
+}
+
+/**
+ * Creates a new blank node. Private.
+ */
+export class RDFBlankNode extends RDFSubject {
+ constructor(ds, nodeID) {
+ super(ds);
+ // The nodeID of this node. May be null if there is no ID.
+ this._nodeID = nodeID;
+ }
+
+ /**
+ * Sets attributes on the DOM element to mark it as representing this node
+ */
+ _applyToElement(element) {
+ if (!this._nodeID)
+ return;
+ if (USE_RDFNS_ATTR) {
+ var prefix = this._ds._resolvePrefix(element, RDF_R("nodeID"));
+ element.setAttributeNS(prefix.namespaceURI, prefix.qname, this._nodeID);
+ } else {
+ element.setAttribute("nodeID", this._nodeID);
+ }
+ }
+
+ /**
+ * Creates a new Element in the document for holding assertions about this
+ * subject. The URI controls what tagname to use.
+ */
+ _createNewElement(uri) {
+ // If there are already nodes representing this in the document then we need
+ // a nodeID to match them
+ if (!this._nodeID && this._elements.length > 0) {
+ this._ds._createNodeID(this);
+ for (let element of this._elements)
+ this._applyToElement(element);
+ }
+
+ return super._createNewElement.call(uri);
+ }
+
+ /**
+ * Adds a reference to this node to the given property Element.
+ */
+ _addReferenceToElement(element) {
+ if (this._elements.length > 0 && !this._nodeID) {
+ // In document elsewhere already
+ // Create a node ID and update the other nodes referencing
+ this._ds._createNodeID(this);
+ for (let element of this._elements)
+ this._applyToElement(element);
+ }
+
+ if (this._nodeID) {
+ if (USE_RDFNS_ATTR) {
+ let prefix = this._ds._resolvePrefix(element, RDF_R("nodeID"));
+ element.setAttributeNS(prefix.namespaceURI, prefix.qname, this._nodeID);
+ } else {
+ element.setAttribute("nodeID", this._nodeID);
+ }
+ } else {
+ // Add the empty blank node, this is generally right since further
+ // assertions will be added to fill this out
+ var newelement = this._ds._addElement(element, RDF_R("Description"));
+ newelement.listCounter = 1;
+ this._elements.push(newelement);
+ }
+ }
+
+ /**
+ * Removes any reference to this node from the given property Element.
+ */
+ _removeReferenceFromElement(element) {
+ if (element.hasAttributeNS(NS_RDF, "nodeID"))
+ element.removeAttributeNS(NS_RDF, "nodeID");
+ if (element.hasAttribute("nodeID"))
+ element.removeAttribute("nodeID");
+ }
+
+ getNodeID() {
+ return this._nodeID;
+ }
+}
+
+/**
+ * Creates a new RDFDataSource from the given document. The document will be
+ * changed as assertions are added and removed to the RDF. Pass a null document
+ * to start with an empty graph.
+ */
+export class RDFDataSource {
+ constructor(document) {
+ // All known resources, indexed on URI
+ this._resources = {};
+ // All blank nodes
+ this._allBlankNodes = [];
+
+ // The underlying DOM document for this datasource
+ this._document = document;
+ this._parseDocument();
+ }
+
+ static loadFromString(text) {
+ let parser = new DOMParser();
+ let document = parser.parseFromString(text, "application/xml");
+
+ return new this(document);
+ }
+
+ /**
+ * Returns an rdf subject for the given DOM Element. If the subject has not
+ * been seen before a new one is created.
+ */
+ _getSubjectForElement(element) {
+ var about = getRDFAttribute(element, "about");
+
+ if (about !== undefined) {
+ let base = Services.io.newURI(element.baseURI);
+ return this.getResource(base.resolve(about));
+ }
+ return this.getBlankNode(null);
+ }
+
+ /**
+ * Parses the document for subjects at the top level.
+ */
+ _parseDocument() {
+ var domnode = this._document.documentElement.firstChild;
+ while (domnode) {
+ if (isElement(domnode)) {
+ var subject = this._getSubjectForElement(domnode);
+ subject._parseElement(domnode);
+ }
+ domnode = domnode.nextSibling;
+ }
+ }
+
+ /**
+ * Gets a blank node. nodeID may be null and if so a new blank node is created.
+ * If a nodeID is given then the blank node with that ID is returned or created.
+ */
+ getBlankNode(nodeID) {
+ var rdfnode = new RDFBlankNode(this, nodeID);
+ this._allBlankNodes.push(rdfnode);
+ return rdfnode;
+ }
+
+ /**
+ * Gets the resource for the URI. The resource is created if it has not been
+ * used already.
+ */
+ getResource(uri) {
+ if (uri in this._resources)
+ return this._resources[uri];
+
+ var resource = new RDFResource(this, uri);
+ this._resources[uri] = resource;
+ return resource;
+ }
+} \ No newline at end of file
diff --git a/modules/firefox.home/chrome/utils/RDFManifestConverter.sys.mjs b/modules/firefox.home/chrome/utils/RDFManifestConverter.sys.mjs
new file mode 100644
index 0000000..25cae7e
--- /dev/null
+++ b/modules/firefox.home/chrome/utils/RDFManifestConverter.sys.mjs
@@ -0,0 +1,99 @@
+ /* 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";
+
+import { RDFDataSource } from "chrome://userchromejs/content/RDFDataSource.sys.mjs";
+
+const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest";
+
+function EM_R(aProperty) {
+ return `http://www.mozilla.org/2004/em-rdf#${aProperty}`;
+}
+
+function getValue(literal) {
+ return literal && literal.getValue();
+}
+
+function getProperty(resource, property) {
+ return getValue(resource.getProperty(EM_R(property)));
+}
+
+class Manifest {
+ constructor(ds) {
+ this.ds = ds;
+ }
+
+ static loadFromString(text) {
+ return new this(RDFDataSource.loadFromString(text));
+ }
+}
+
+export class InstallRDF extends Manifest {
+ _readProps(source, obj, props) {
+ for (let prop of props) {
+ let val = getProperty(source, prop);
+ if (val != null) {
+ obj[prop] = val;
+ }
+ }
+ }
+
+ _readArrayProp(source, obj, prop, target, decode = getValue) {
+ let result = Array.from(source.getObjects(EM_R(prop)),
+ target => decode(target));
+ if (result.length) {
+ obj[target] = result;
+ }
+ }
+
+ _readArrayProps(source, obj, props, decode = getValue) {
+ for (let [prop, target] of Object.entries(props)) {
+ this._readArrayProp(source, obj, prop, target, decode);
+ }
+ }
+
+ _readLocaleStrings(source, obj) {
+ this._readProps(source, obj, ["name", "description", "creator", "homepageURL"]);
+ this._readArrayProps(source, obj, {
+ locale: "locales",
+ developer: "developers",
+ translator: "translators",
+ contributor: "contributors",
+ });
+ }
+
+ decode() {
+ let root = this.ds.getResource(RDFURI_INSTALL_MANIFEST_ROOT);
+ let result = {};
+
+ let props = ["id", "version", "type", "updateURL", "optionsURL",
+ "optionsType", "aboutURL", "iconURL",
+ "bootstrap", "unpack", "strictCompatibility"];
+ this._readProps(root, result, props);
+
+ let decodeTargetApplication = source => {
+ let app = {};
+ this._readProps(source, app, ["id", "minVersion", "maxVersion"]);
+ return app;
+ };
+
+ let decodeLocale = source => {
+ let localized = {};
+ this._readLocaleStrings(source, localized);
+ return localized;
+ };
+
+ this._readLocaleStrings(root, result);
+
+ this._readArrayProps(root, result, {"targetPlatform": "targetPlatforms"});
+ this._readArrayProps(root, result, {"targetApplication": "targetApplications"},
+ decodeTargetApplication);
+ this._readArrayProps(root, result, {"localized": "localized"},
+ decodeLocale);
+ this._readArrayProps(root, result, {"dependency": "dependencies"},
+ source => getProperty(source, "id"));
+
+ return result;
+ }
+} \ No newline at end of file
diff --git a/modules/firefox.home/chrome/utils/chrome.manifest b/modules/firefox.home/chrome/utils/chrome.manifest
new file mode 100644
index 0000000..d9076b2
--- /dev/null
+++ b/modules/firefox.home/chrome/utils/chrome.manifest
@@ -0,0 +1,2 @@
+content userchromejs ./
+resource userchromejs ../
diff --git a/modules/firefox.home/chrome/utils/userChrome.jsm b/modules/firefox.home/chrome/utils/userChrome.jsm
new file mode 100644
index 0000000..17a9224
--- /dev/null
+++ b/modules/firefox.home/chrome/utils/userChrome.jsm
@@ -0,0 +1,231 @@
+let EXPORTED_SYMBOLS = [];
+
+const { xPref } = ChromeUtils.import('chrome://userchromejs/content/xPref.jsm');
+const { Management } = ChromeUtils.import('resource://gre/modules/Extension.jsm');
+const { AppConstants } = ChromeUtils.import('resource://gre/modules/AppConstants.jsm');
+
+let UC = {
+ webExts: new Map(),
+ sidebar: new Map()
+};
+
+let _uc = {
+ ALWAYSEXECUTE: 'rebuild_userChrome.uc.js',
+ BROWSERCHROME: AppConstants.MOZ_APP_NAME == 'thunderbird' ? 'chrome://messenger/content/messenger.xhtml' : 'chrome://browser/content/browser.xhtml',
+ BROWSERTYPE: AppConstants.MOZ_APP_NAME == 'thunderbird' ? 'mail:3pane' : 'navigator:browser',
+ BROWSERNAME: AppConstants.MOZ_APP_NAME.charAt(0).toUpperCase() + AppConstants.MOZ_APP_NAME.slice(1),
+ PREF_ENABLED: 'userChromeJS.enabled',
+ PREF_SCRIPTSDISABLED: 'userChromeJS.scriptsDisabled',
+
+ chromedir: Services.dirsvc.get('UChrm', Ci.nsIFile),
+ scriptsDir: '',
+
+ sss: Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService),
+
+ getScripts: function () {
+ this.scripts = {};
+ let files = this.chromedir.directoryEntries.QueryInterface(Ci.nsISimpleEnumerator);
+ while (files.hasMoreElements()) {
+ let file = files.getNext().QueryInterface(Ci.nsIFile);
+ if (/\.uc\.js$/i.test(file.leafName)) {
+ _uc.getScriptData(file);
+ }
+ }
+ },
+
+ getScriptData: function (aFile) {
+ let aContent = this.readFile(aFile);
+ let header = (aContent.match(/^\/\/ ==UserScript==\s*\n(?:.*\n)*?\/\/ ==\/UserScript==\s*\n/m) || [''])[0];
+ let match, rex = {
+ include: [],
+ exclude: []
+ };
+ let findNextRe = /^\/\/ @(include|exclude)\s+(.+)\s*$/gm;
+ while ((match = findNextRe.exec(header))) {
+ rex[match[1]].push(match[2].replace(/^main$/i, _uc.BROWSERCHROME).replace(/\*/g, '.*?'));
+ }
+ if (!rex.include.length) {
+ rex.include.push(_uc.BROWSERCHROME);
+ }
+ let exclude = rex.exclude.length ? '(?!' + rex.exclude.join('$|') + '$)' : '';
+
+ let def = ['', ''];
+ let author = (header.match(/\/\/ @author\s+(.+)\s*$/im) || def)[1];
+ let filename = aFile.leafName || '';
+
+ return this.scripts[filename] = {
+ filename: filename,
+ file: aFile,
+ url: Services.io.getProtocolHandler('file').QueryInterface(Ci.nsIFileProtocolHandler).getURLSpecFromDir(this.chromedir) + filename,
+ name: (header.match(/\/\/ @name\s+(.+)\s*$/im) || def)[1],
+ description: (header.match(/\/\/ @description\s+(.+)\s*$/im) || def)[1],
+ version: (header.match(/\/\/ @version\s+(.+)\s*$/im) || def)[1],
+ author: (header.match(/\/\/ @author\s+(.+)\s*$/im) || def)[1],
+ regex: new RegExp('^' + exclude + '(' + (rex.include.join('|') || '.*') + ')$', 'i'),
+ id: (header.match(/\/\/ @id\s+(.+)\s*$/im) || ['', filename.split('.uc.js')[0] + '@' + (author || 'userChromeJS')])[1],
+ homepageURL: (header.match(/\/\/ @homepageURL\s+(.+)\s*$/im) || def)[1],
+ downloadURL: (header.match(/\/\/ @downloadURL\s+(.+)\s*$/im) || def)[1],
+ updateURL: (header.match(/\/\/ @updateURL\s+(.+)\s*$/im) || def)[1],
+ optionsURL: (header.match(/\/\/ @optionsURL\s+(.+)\s*$/im) || def)[1],
+ startup: (header.match(/\/\/ @startup\s+(.+)\s*$/im) || def)[1],
+ shutdown: (header.match(/\/\/ @shutdown\s+(.+)\s*$/im) || def)[1],
+ onlyonce: /\/\/ @onlyonce\b/.test(header),
+ isRunning: false,
+ get isEnabled() {
+ return (xPref.get(_uc.PREF_SCRIPTSDISABLED) || '').split(',').indexOf(this.filename) == -1;
+ }
+ }
+ },
+
+ readFile: function (aFile, metaOnly = false) {
+ let stream = Cc['@mozilla.org/network/file-input-stream;1'].createInstance(Ci.nsIFileInputStream);
+ stream.init(aFile, 0x01, 0, 0);
+ let cvstream = Cc['@mozilla.org/intl/converter-input-stream;1'].createInstance(Ci.nsIConverterInputStream);
+ cvstream.init(stream, 'UTF-8', 1024, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+ let content = '',
+ data = {};
+ while (cvstream.readString(4096, data)) {
+ content += data.value;
+ if (metaOnly && content.indexOf('// ==/UserScript==') > 0) {
+ break;
+ }
+ }
+ cvstream.close();
+ return content.replace(/\r\n?/g, '\n');
+ },
+
+ everLoaded: [],
+
+ loadScript: function (script, win) {
+ if (!script.regex.test(win.location.href) || (script.filename != this.ALWAYSEXECUTE && !script.isEnabled)) {
+ return;
+ }
+
+ if (script.onlyonce && script.isRunning) {
+ if (script.startup) {
+ eval(script.startup);
+ }
+ return;
+ }
+
+ try {
+ Services.scriptloader.loadSubScript(script.url + '?' + script.file.lastModifiedTime,
+ script.onlyonce ? { window: win } : win);
+ script.isRunning = true;
+ if (script.startup) {
+ eval(script.startup);
+ }
+ if (!script.shutdown) {
+ this.everLoaded.push(script.id);
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ },
+
+ windows: function (fun, onlyBrowsers = true) {
+ let windows = Services.wm.getEnumerator(onlyBrowsers ? this.BROWSERTYPE : null);
+ while (windows.hasMoreElements()) {
+ let win = windows.getNext();
+ if (!win._uc)
+ continue;
+ if (!onlyBrowsers) {
+ let frames = win.docShell.getAllDocShellsInSubtree(Ci.nsIDocShellTreeItem.typeAll, Ci.nsIDocShell.ENUMERATE_FORWARDS);
+ let res = frames.some(frame => {
+ let fWin = frame.domWindow;
+ let {document, location} = fWin;
+ if (fun(document, fWin, location))
+ return true;
+ });
+ if (res)
+ break;
+ } else {
+ let {document, location} = win;
+ if (fun(document, win, location))
+ break;
+ }
+ }
+ },
+
+ createElement: function (doc, tag, atts, XUL = true) {
+ let el = XUL ? doc.createXULElement(tag) : doc.createElement(tag);
+ for (let att in atts) {
+ el.setAttribute(att, atts[att]);
+ }
+ return el
+ }
+};
+
+if (xPref.get(_uc.PREF_ENABLED) === undefined) {
+ xPref.set(_uc.PREF_ENABLED, true, true);
+}
+
+if (xPref.get(_uc.PREF_SCRIPTSDISABLED) === undefined) {
+ xPref.set(_uc.PREF_SCRIPTSDISABLED, '', true);
+}
+
+let UserChrome_js = {
+ observe: function (aSubject) {
+ aSubject.addEventListener('DOMContentLoaded', this, {once: true});
+ },
+
+ handleEvent: function (aEvent) {
+ let document = aEvent.originalTarget;
+ let window = document.defaultView;
+ this.load(window);
+ },
+
+ load: function (window) {
+ let location = window.location;
+
+ if (!this.sharedWindowOpened && location.href == 'chrome://extensions/content/dummy.xhtml') {
+ this.sharedWindowOpened = true;
+
+ Management.on('extension-browser-inserted', function (topic, browser) {
+ browser.messageManager.addMessageListener('Extension:BackgroundViewLoaded', this.messageListener.bind(this));
+ }.bind(this));
+ } else if (/^(chrome:(?!\/\/global\/content\/commonDialog\.x?html)|about:(?!blank))/i.test(location.href)) {
+ window.UC = UC;
+ window._uc = _uc;
+ window.xPref = xPref;
+ if (window._gBrowser) // bug 1443849
+ window.gBrowser = window._gBrowser;
+
+ if (xPref.get(_uc.PREF_ENABLED)) {
+ Object.values(_uc.scripts).forEach(script => {
+ _uc.loadScript(script, window);
+ });
+ } else if (!UC.rebuild) {
+ _uc.loadScript(_uc.scripts[_uc.ALWAYSEXECUTE], window);
+ }
+ }
+ },
+
+ messageListener: function (msg) {
+ const browser = msg.target;
+ const { addonId } = browser._contentPrincipal;
+
+ browser.messageManager.removeMessageListener('Extension:BackgroundViewLoaded', this.messageListener);
+
+ if (browser.ownerGlobal.location.href == 'chrome://extensions/content/dummy.xhtml') {
+ UC.webExts.set(addonId, browser);
+ Services.obs.notifyObservers(null, 'UCJS:WebExtLoaded', addonId);
+ } else {
+ let win = browser.ownerGlobal.windowRoot.ownerGlobal;
+ UC.sidebar.get(addonId)?.set(win, browser) || UC.sidebar.set(addonId, new Map([[win, browser]]));
+ Services.obs.notifyObservers(win, 'UCJS:SidebarLoaded', addonId);
+ }
+ }
+};
+
+if (!Services.appinfo.inSafeMode) {
+ _uc.chromedir.append(_uc.scriptsDir);
+ _uc.getScripts();
+ let windows = Services.wm.getEnumerator(null);
+ while (windows.hasMoreElements()) {
+ let win = windows.getNext();
+ if (!('UC' in win))
+ UserChrome_js.load(win)
+ }
+ Services.obs.addObserver(UserChrome_js, 'chrome-document-global-created', false);
+}
diff --git a/modules/firefox.home/chrome/utils/xPref.jsm b/modules/firefox.home/chrome/utils/xPref.jsm
new file mode 100644
index 0000000..dcb44b3
--- /dev/null
+++ b/modules/firefox.home/chrome/utils/xPref.jsm
@@ -0,0 +1,85 @@
+let EXPORTED_SYMBOLS = ['xPref'];
+
+var xPref = {
+ get: function(prefPath, def = false, valueIfUndefined, setDefault = true) {
+ let sPrefs = def ?
+ Services.prefs.getDefaultBranch(null) :
+ Services.prefs;
+
+ try {
+ switch (sPrefs.getPrefType(prefPath)) {
+ case 0:
+ if (valueIfUndefined != undefined)
+ return this.set(prefPath, valueIfUndefined, setDefault);
+ else
+ return undefined;
+ case 32:
+ return sPrefs.getStringPref(prefPath);
+ case 64:
+ return sPrefs.getIntPref(prefPath);
+ case 128:
+ return sPrefs.getBoolPref(prefPath);
+ }
+ } catch (ex) {
+ return undefined;
+ }
+ return;
+ },
+
+ set: function(prefPath, value, def = false) {
+ let sPrefs = def ?
+ Services.prefs.getDefaultBranch(null) :
+ Services.prefs;
+
+ try {
+ switch (typeof value) {
+ case 'string':
+ return sPrefs.setStringPref(prefPath, value) || value;
+ case 'number':
+ return sPrefs.setIntPref(prefPath, value) || value;
+ case 'boolean':
+ return sPrefs.setBoolPref(prefPath, value) || value;
+ }
+ } catch (e) { console.error(e) }
+ return;
+ },
+
+ lock: function(prefPath, value) {
+ let sPrefs = Services.prefs;
+ this.lockedBackupDef[prefPath] = this.get(prefPath, true);
+ if (sPrefs.prefIsLocked(prefPath))
+ sPrefs.unlockPref(prefPath);
+
+ this.set(prefPath, value, true);
+ sPrefs.lockPref(prefPath);
+ },
+
+ lockedBackupDef: {},
+
+ unlock: function(prefPath) {
+ Services.prefs.unlockPref(prefPath);
+ let bkp = this.lockedBackupDef[prefPath];
+ if (bkp == undefined)
+ Services.prefs.deleteBranch(prefPath);
+ else
+ this.set(prefPath, bkp, true);
+ },
+
+ clear: Services.prefs.clearUserPref,
+
+ addListener: function(prefPath, trat) {
+ this.observer = function(aSubject, aTopic, prefPath) {
+ return trat(xPref.get(prefPath), prefPath);
+ }
+
+ Services.prefs.addObserver(prefPath, this.observer);
+ return {
+ prefPath,
+ observer: this.observer
+ };
+ },
+
+ removeListener: function(obs) {
+ Services.prefs.removeObserver(obs.prefPath, obs.observer);
+ }
+}
diff --git a/modules/firefox.home/default.nix b/modules/firefox.home/default.nix
index b315a36..e6f5aad 100644
--- a/modules/firefox.home/default.nix
+++ b/modules/firefox.home/default.nix
@@ -12,15 +12,17 @@ let
"https://addons.mozilla.org/firefox/downloads/file/4218479/font_inspect-0.5.8.xpi"
"https://addons.mozilla.org/firefox/downloads/file/4314064/react_devtools-5.3.1.xpi"
- # content
+ # others
"https://addons.mozilla.org/firefox/downloads/file/4332776/languagetool-8.11.6.xpi"
"https://addons.mozilla.org/firefox/downloads/file/4317971/darkreader-4.9.88.xpi"
+ "https://addons.mozilla.org/firefox/downloads/file/3970625/screen_recorder-0.1.8.xpi"
];
# about:config
preferences = {
"browser.newtabpage.enabled" = false;
"browser.startup.homepage" = homepage-url;
+ "browser.newtab.url" = homepage-url;
"browser.startup.blankWindow" = false;
"browser.tabs.drawInTitlebar" = true;
@@ -29,17 +31,21 @@ let
"browser.theme.toolbar-theme" = 0;
"browser.aboutConfig.showWarning" = false;
"toolkit.legacyUserProfileCustomizations.stylesheets" = true;
- "devtools.theme" = "dark";
- "devtools.toolbox.alwaysOnTop" = true;
- "devtools.toolbox.host" = "window";
+ # "devtools.theme" = "dark";
+ "ui.systemUsesDarkTheme" = 2;
+ # "devtools.toolbox.alwaysOnTop" = true;
+ # "devtools.toolbox.host" = "window";
"layers.acceleration.force-enabled" = true;
"gfx.webrender.all" = true;
- "svg.context-properties.content.enabled" = true;
+ # "svg.context-properties.content.enabled" = true;
+ "widget.gtk.rounded-bottom-corners.enabled" = true;
+ "browser.urlbar.suggest.calculator" = true;
+ "browser.urlbar.unitConversion.enabled" = true;
"extensions.pocket.enabled" = false;
"extensions.pocket.showHome" = false;
"browser.urlbar.suggest.pocket" = false;
- "privacy.donottrackheader.enabled" = true;
+ # "privacy.donottrackheader.enabled" = true;
"privacy.globalprivacycontrol.enabled" = true;
"browser.search.suggest.enabled" = false;
"browser.urlbar.suggest.searches" = false;
@@ -48,11 +54,23 @@ let
"browser.dataFeatureRecommendations.enabled" = false;
"extensions.htmlaboutaddons.recommendations.enabled" = false;
+ "layout.css.has-selector.enabled" = true;
"browser.toolbars.bookmarks.visibility" = "never";
- "identity.fxaccounts.toolbar.enabled" = false;
+ # "identity.fxaccounts.toolbar.enabled" = false;
"browser.tabs.insertAfterCurrent" = true;
"browser.urlbar.shortcuts.bookmarks" = false;
"browser.urlbar.suggest.bookmark" = false;
+ "browser.newtab.privateAllowed" = true;
+
+ "browser.download.forbid_open_with" = true;
+ "browser.download.autohideButton" = true;
+ "browser.download.useDownloadDir" = false;
+
+ # HAcky stuff
+ # "general.config.sandbox_enabled" = false;
+ # "general.config.obscure_value" = false;
+ "xpinstall.signatures.required" = false;
+ "extensions.install_origins.enabled" = false;
};
# https://mozilla.github.io/policy-templates
@@ -60,30 +78,40 @@ let
ManagedBookmarks = [
{ toplevel_name = "Managed bookmarks"; }
{ name = "Daily Dev"; url = "https://app.daily.dev"; }
+ { name = "Email: Microsoft Outlook"; url = "https://outlook.office365.com/mail/"; }
+ { name = "Email: GMail"; url = "https://mail.google.com/mail/u/0/"; }
# { name = "Shtuff"; children = [] }
];
DisplayBookmarksToolbar = "never";
NoDefaultBookmarks = true;
- DisableTelemetry = true;
+ DisableAppUpdate = true;
+ DisableSystemAddonUpdate = true;
+ DisableProfileImport = true;
DisableFirefoxStudies = true;
+ DisableTelemetry = true;
+ DisableFeedbackCommands = true;
+ DisablePocket = true;
DisableAccounts = true;
DontCheckDefaultBrowser = true;
OfferToSaveLogins = false;
PasswordManagerEnabled = false;
- DefaultDownloadDirectory = "\${home}/Downloads/firefox";
+ DefaultDownloadDirectory = "${config.home.homeDirectory}/Downloads/firefox";
NewTabPage = false;
Homepage = { URL = homepage-url; StartPage = "previous-session"; };
+ OverrideFirstRunPage = "";
# PopupBlocking = [];
SearchEngines = {
Default = "DuckDuckGo";
SearchSuggestEnabled = false;
+ Remove = [ "Google" "Bing" "Amazon.com" "eBay" "Wikipedia" ];
+ PreventInstalls = false;
Add = [
{
Name = "DuckDuckGo";
URLTemplate = "https://duckduckgo.com/?q={searchTerms}";
- Alias = "google";
+ Alias = "ddg";
}
{
Name = "Google";
@@ -107,12 +135,30 @@ let
# };
# };
- Extensions = { Install = extensions; };
+ Extensions = {
+ Install = extensions;
+ Uninstall = [
+ "google@search.mozilla.org"
+ "bing@search.mozilla.org"
+ "amazondotcom@search.mozilla.org"
+ "ebay@search.mozilla.org"
+ "wikipedia@search.mozilla.org"
+ ];
+ };
+ ExtensionSettings = {
+ "google@search.mozilla.org".installation_mode = "blocked";
+ "bing@search.mozilla.org".installation_mode = "blocked";
+ "amazondotcom@search.mozilla.org".installation_mode = "blocked";
+ "ebay@search.mozilla.org".installation_mode = "blocked";
+ "wikipedia@search.mozilla.org".installation_mode = "blocked";
+ };
Preferences = lib.mapAttrs (_key: val: { Status = "locked"; Value = val; }) preferences;
};
- firefox = pkgs.wrapFirefox unwrapped-firefox-package {};
+ firefox = pkgs.wrapFirefox unwrapped-firefox-package {
+ extraPrefs = builtins.readFile ./autoconfig.js;
+ };
profilePath = "default";
in {
@@ -121,14 +167,13 @@ in {
package = firefox;
nativeMessagingHosts = [ pkgs.tridactyl-native ];
-
policies = policies;
};
home.file = {
".config/tridactyl".source = ./tridactyl;
- ".mozilla/firefox/${profilePath}/chrome/userChrome.css".text = builtins.readFile ./userChrome.css;
+ ".mozilla/firefox/${profilePath}/chrome".source = ./chrome;
".mozilla/firefox/profiles.ini".text = lib.generators.toINI {} {
Profile1 = {
diff --git a/modules/firefox.home/tridactyl/tridactylrc b/modules/firefox.home/tridactyl/tridactylrc
index cb1a043..2fe7075 100644
--- a/modules/firefox.home/tridactyl/tridactylrc
+++ b/modules/firefox.home/tridactyl/tridactylrc
@@ -1,56 +1,28 @@
colorscheme phenax
-bind o fillcmdline tabopen
-bind O fillcmdline open
-bind p clipboard tabopen
-bind P clipboard open
-
set hintnames numeric
+set hintfiltermode vimperator-reflow
set editorcmd sensible-terminal -e nvim
set newtab about:blank
set viewsource default
-set browsermaps { "<c-//>": "escapehatch", "<c-,>": "escapehatch" }
+set browsermaps { "<C-,>": "escapehatch", "<C-6>": "noh" }
-" Tab control
+" Unbind default keys
unbind d
unbind D
-unbind <c-s-j>
-unbind <c-s-k>
-bind <c-d> tabclose
-bind <c-j> tabnext
-bind <c-k> tabprev
-bind <cs-j> tabmove +1
-bind <cs-k> tabmove -1
-
+unbind <C-j>
+unbind <C-k>
unbind t
+unbind tt
unbind T
-" unbind tt
-" bind tt tabopen file:///home/imsohexy/.config/qutebrowser/homepage/index.html
+unbind J
+unbind K
-unbind <s-j>
-unbind <s-k>
-bind <s-j> scrollpx 0 200
-bind <s-k> scrollpx 0 -200
+bind o fillcmdline tabopen
+bind O fillcmdline open
+bind p clipboard tabopen
+bind P clipboard open
-" Open tab by index
-bind <space>1 tab 1
-bind <c-1> tab 1
-bind <space>2 tab 2
-bind <c-2> tab 2
-bind <space>3 tab 3
-bind <c-3> tab 3
-bind <space>4 tab 4
-bind <c-4> tab 4
-bind <space>5 tab 5
-bind <c-5> tab 5
-bind <space>6 tab 6
-bind <c-6> tab 6
-bind <space>7 tab 7
-bind <c-7> tab 7
-bind <space>8 tab 8
-bind <c-8> tab 8
-bind <space>9 tab 9
-bind <c-9> tab 9
-bind <space>0 tab 10
-bind <c-0> tab 10
+bind --mode=normal J scrollpx 0 200
+bind --mode=normal K scrollpx 0 -200