From 6206aaf88532d153f0e01e22772b7f6e1c979afd Mon Sep 17 00:00:00 2001 From: Akshay Nair Date: Mon, 19 Aug 2024 14:36:34 +0530 Subject: Enabled user js for firefox + global keybindings manager --- modules/firefox.home/autoconfig.js | 24 ++ .../firefox.home/chrome/globalKeybindings.uc.js | 102 +++++ modules/firefox.home/chrome/setupNewTab.uc.js | 20 + modules/firefox.home/chrome/userChrome.css | 120 ++++++ .../firefox.home/chrome/utils/BootstrapLoader.js | 419 ++++++++++++++++++++ .../chrome/utils/RDFDataSource.sys.mjs | 434 +++++++++++++++++++++ .../chrome/utils/RDFManifestConverter.sys.mjs | 99 +++++ modules/firefox.home/chrome/utils/chrome.manifest | 2 + modules/firefox.home/chrome/utils/userChrome.jsm | 231 +++++++++++ modules/firefox.home/chrome/utils/xPref.jsm | 85 ++++ modules/firefox.home/default.nix | 73 +++- modules/firefox.home/tridactyl/tridactylrc | 56 +-- modules/firefox.home/userChrome.css | 120 ------ 13 files changed, 1609 insertions(+), 176 deletions(-) create mode 100644 modules/firefox.home/autoconfig.js create mode 100644 modules/firefox.home/chrome/globalKeybindings.uc.js create mode 100644 modules/firefox.home/chrome/setupNewTab.uc.js create mode 100644 modules/firefox.home/chrome/userChrome.css create mode 100644 modules/firefox.home/chrome/utils/BootstrapLoader.js create mode 100644 modules/firefox.home/chrome/utils/RDFDataSource.sys.mjs create mode 100644 modules/firefox.home/chrome/utils/RDFManifestConverter.sys.mjs create mode 100644 modules/firefox.home/chrome/utils/chrome.manifest create mode 100644 modules/firefox.home/chrome/utils/userChrome.jsm create mode 100644 modules/firefox.home/chrome/utils/xPref.jsm delete mode 100644 modules/firefox.home/userChrome.css (limited to 'modules') 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/chrome/userChrome.css b/modules/firefox.home/chrome/userChrome.css new file mode 100644 index 0000000..67809b0 --- /dev/null +++ b/modules/firefox.home/chrome/userChrome.css @@ -0,0 +1,120 @@ +:root { + --ff-accent-color: #4e3aA3; + --ff-bg-color: #0f0c19; + --ff-bg-color-1: #16121f; + --ff-bg-color-2: #26222f; + --ff-urlbar-height: 28px; +} + +#navigator-toolbox { + --tab-min-height: 16px; + --tab-border-radius: 0; + --tab-block-margin: 0; + --toolbarbutton-inner-padding: 6px; + --toolbar-bgcolor: var(--ff-bg-color); + --toolbar-field-background-color: var(--ff-bg-color-2); + --toolbarbutton-border-radius: 0; + --toolbar-start-end-padding: 2px; +} + +toolbarbutton { + --toolbarbutton-inner-padding: 6px; +} + +#TabsToolbar { + background-color: var(--ff-bg-color) !important; +} + +.browser-toolbar { + padding: 0 !important; + min-height: 0; +} + + + +/* Tabs */ + +.tabbrowser-tab { + padding: 0 !important; + font-size: 0.86rem !important; +} + +.tabbrowser-tab .tab-label-container { + height: 2.2rem !important; +} + +.tabbrowser-tab .tab-text { + line-height: 0; +} + +.tabbrowser-tab .tab-background { + background-color: transparent !important; + border-bottom: 3px solid transparent; +} +.tabbrowser-tab .tab-background[selected] { + background-color: rgba(255, 255, 255, 0.04) !important; + border-bottom-color: var(--ff-accent-color); +} +.tabbrowser-tab:hover .tab-background:not([selected]) { + background-color: rgba(255, 255, 255, 0.06) !important; +} + +.tab-close-button { + opacity: 0.6; + padding: 2px !important; + width: 14px !important; + height: 14px !important; +} + +#tabbrowser-tabs[haspinnedtabs]:not([positionpinnedtabs]) > #tabbrowser-arrowscrollbox > .tabbrowser-tab:nth-child(1 of :not([pinned], [hidden])) { + margin-inline-start: 0 !important; +} + + + + + + +/* Nav bar with url */ + +#nav-bar { + border-top: none !important; + background-color: var(--ff-bg-color-1) !important; +} + +#urlbar-container { + min-height: var(--ff-urlbar-height) !important; +} + +#urlbar { + font-size: 0.95rem !important; + min-height: 0 !important; + height: var(--ff-bg-color-1) !important; +} + +.urlbar-input-container { + background-color: var(--ff-bg-color-2) !important; + border-color: transparent !important; + min-height: 0 !important; +} + +#urlbar[breakout-extend] .urlbar-input-container { + background-color: var(--ff-bg-color-2) !important; +} +#urlbar[breakout-extend] .urlbar-input-container #identity-box { + display: none; +} + +#urlbar-container { + margin: 0 !important; +} + +#urlbar[focused='true'] > #urlbar-background { + --bxackground-color: var(--ff-bg-color) !important; + box-shadow: none !important; +} + +#navigator-toolbox { + --border: none !important; +} + 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 { "": "escapehatch", "": "escapehatch" } +set browsermaps { "": "escapehatch", "": "noh" } -" Tab control +" Unbind default keys unbind d unbind D -unbind -unbind -bind tabclose -bind tabnext -bind tabprev -bind tabmove +1 -bind tabmove -1 - +unbind +unbind unbind t +unbind tt unbind T -" unbind tt -" bind tt tabopen file:///home/imsohexy/.config/qutebrowser/homepage/index.html +unbind J +unbind K -unbind -unbind -bind scrollpx 0 200 -bind scrollpx 0 -200 +bind o fillcmdline tabopen +bind O fillcmdline open +bind p clipboard tabopen +bind P clipboard open -" Open tab by index -bind 1 tab 1 -bind tab 1 -bind 2 tab 2 -bind tab 2 -bind 3 tab 3 -bind tab 3 -bind 4 tab 4 -bind tab 4 -bind 5 tab 5 -bind tab 5 -bind 6 tab 6 -bind tab 6 -bind 7 tab 7 -bind tab 7 -bind 8 tab 8 -bind tab 8 -bind 9 tab 9 -bind tab 9 -bind 0 tab 10 -bind tab 10 +bind --mode=normal J scrollpx 0 200 +bind --mode=normal K scrollpx 0 -200 diff --git a/modules/firefox.home/userChrome.css b/modules/firefox.home/userChrome.css deleted file mode 100644 index 67809b0..0000000 --- a/modules/firefox.home/userChrome.css +++ /dev/null @@ -1,120 +0,0 @@ -:root { - --ff-accent-color: #4e3aA3; - --ff-bg-color: #0f0c19; - --ff-bg-color-1: #16121f; - --ff-bg-color-2: #26222f; - --ff-urlbar-height: 28px; -} - -#navigator-toolbox { - --tab-min-height: 16px; - --tab-border-radius: 0; - --tab-block-margin: 0; - --toolbarbutton-inner-padding: 6px; - --toolbar-bgcolor: var(--ff-bg-color); - --toolbar-field-background-color: var(--ff-bg-color-2); - --toolbarbutton-border-radius: 0; - --toolbar-start-end-padding: 2px; -} - -toolbarbutton { - --toolbarbutton-inner-padding: 6px; -} - -#TabsToolbar { - background-color: var(--ff-bg-color) !important; -} - -.browser-toolbar { - padding: 0 !important; - min-height: 0; -} - - - -/* Tabs */ - -.tabbrowser-tab { - padding: 0 !important; - font-size: 0.86rem !important; -} - -.tabbrowser-tab .tab-label-container { - height: 2.2rem !important; -} - -.tabbrowser-tab .tab-text { - line-height: 0; -} - -.tabbrowser-tab .tab-background { - background-color: transparent !important; - border-bottom: 3px solid transparent; -} -.tabbrowser-tab .tab-background[selected] { - background-color: rgba(255, 255, 255, 0.04) !important; - border-bottom-color: var(--ff-accent-color); -} -.tabbrowser-tab:hover .tab-background:not([selected]) { - background-color: rgba(255, 255, 255, 0.06) !important; -} - -.tab-close-button { - opacity: 0.6; - padding: 2px !important; - width: 14px !important; - height: 14px !important; -} - -#tabbrowser-tabs[haspinnedtabs]:not([positionpinnedtabs]) > #tabbrowser-arrowscrollbox > .tabbrowser-tab:nth-child(1 of :not([pinned], [hidden])) { - margin-inline-start: 0 !important; -} - - - - - - -/* Nav bar with url */ - -#nav-bar { - border-top: none !important; - background-color: var(--ff-bg-color-1) !important; -} - -#urlbar-container { - min-height: var(--ff-urlbar-height) !important; -} - -#urlbar { - font-size: 0.95rem !important; - min-height: 0 !important; - height: var(--ff-bg-color-1) !important; -} - -.urlbar-input-container { - background-color: var(--ff-bg-color-2) !important; - border-color: transparent !important; - min-height: 0 !important; -} - -#urlbar[breakout-extend] .urlbar-input-container { - background-color: var(--ff-bg-color-2) !important; -} -#urlbar[breakout-extend] .urlbar-input-container #identity-box { - display: none; -} - -#urlbar-container { - margin: 0 !important; -} - -#urlbar[focused='true'] > #urlbar-background { - --bxackground-color: var(--ff-bg-color) !important; - box-shadow: none !important; -} - -#navigator-toolbox { - --border: none !important; -} - -- cgit v1.3.1