aboutsummaryrefslogtreecommitdiff
path: root/modules/firefox.home/chrome/utils
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/chrome/utils
parent02612ccd49d561808ff931611b2ea865038b0a51 (diff)
downloadnixos-config-6206aaf88532d153f0e01e22772b7f6e1c979afd.tar.gz
nixos-config-6206aaf88532d153f0e01e22772b7f6e1c979afd.zip
Enabled user js for firefox + global keybindings manager
Diffstat (limited to '')
-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
6 files changed, 1270 insertions, 0 deletions
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);
+ }
+}