the huge 117+ fixes update
This commit is contained in:
parent
baf06e964f
commit
ccae90fb3e
110 changed files with 7809 additions and 2864 deletions
477
chrome/utils/BootstrapLoader.jsm
Normal file
477
chrome/utils/BootstrapLoader.jsm
Normal file
|
@ -0,0 +1,477 @@
|
|||
/* 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";
|
||||
|
||||
let EXPORTED_SYMBOLS = [];
|
||||
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
const Services =
|
||||
globalThis.Services ||
|
||||
ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
Blocklist: "resource://gre/modules/Blocklist.jsm",
|
||||
ConsoleAPI: "resource://gre/modules/Console.jsm",
|
||||
InstallRDF: "chrome://userchromejs/content/RDFManifestConverter.jsm",
|
||||
});
|
||||
|
||||
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.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.optionsType == 1 /*AddonManager.OPTIONS_TYPE_DIALOG*/)
|
||||
this.querySelector(
|
||||
'panel-item[data-l10n-id="preferences-addon-button"]'
|
||||
).hidden = false;
|
||||
};
|
||||
}
|
||||
}, "chrome-document-loaded");
|
||||
|
||||
const { AddonManager } = ChromeUtils.import(
|
||||
"resource://gre/modules/AddonManager.jsm"
|
||||
);
|
||||
const { XPIDatabase, AddonInternal } = ChromeUtils.import(
|
||||
"resource://gre/modules/addons/XPIDatabase.jsm"
|
||||
);
|
||||
|
||||
const { defineAddonWrapperProperty } = Cu.import(
|
||||
"resource://gre/modules/addons/XPIDatabase.jsm"
|
||||
);
|
||||
defineAddonWrapperProperty("optionsType", function optionsType() {
|
||||
if (!this.isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let addon = this.__AddonInternal__;
|
||||
let hasOptionsURL = !!this.optionsURL;
|
||||
|
||||
if (addon.optionsType) {
|
||||
switch (parseInt(addon.optionsType, 10)) {
|
||||
case 1 /*AddonManager.OPTIONS_TYPE_DIALOG*/:
|
||||
case AddonManager.OPTIONS_TYPE_TAB:
|
||||
case AddonManager.OPTIONS_TYPE_INLINE_BROWSER:
|
||||
return hasOptionsURL ? addon.optionsType : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
XPIDatabase.isDisabledLegacy = () => false;
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "BOOTSTRAP_REASONS", () => {
|
||||
const { XPIProvider } = ChromeUtils.import(
|
||||
"resource://gre/modules/addons/XPIProvider.jsm"
|
||||
);
|
||||
return XPIProvider.BOOTSTRAP_REASONS;
|
||||
});
|
||||
|
||||
const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
|
||||
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";
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
XPCOMUtils.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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
441
chrome/utils/RDFDataSource.jsm
Normal file
441
chrome/utils/RDFDataSource.jsm
Normal file
|
@ -0,0 +1,441 @@
|
|||
/* 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 */
|
||||
|
||||
var EXPORTED_SYMBOLS = ["RDFLiteral", "RDFBlankNode", "RDFResource", "RDFDataSource"];
|
||||
|
||||
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
const Services = globalThis.Services || ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
|
||||
|
||||
XPCOMUtils.defineLazyGlobalGetters(this, ["DOMParser", "Element", "fetch"]);
|
||||
|
||||
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
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
102
chrome/utils/RDFManifestConverter.jsm
Normal file
102
chrome/utils/RDFManifestConverter.jsm
Normal file
|
@ -0,0 +1,102 @@
|
|||
/* 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";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["InstallRDF"];
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "RDFDataSource",
|
||||
"chrome://userchromejs/content/RDFDataSource.jsm");
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
102
chrome/utils/aboutconfig/aboutconfig.xhtml
Normal file
102
chrome/utils/aboutconfig/aboutconfig.xhtml
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?xml version="1.0"?>
|
||||
|
||||
<!-- 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/. -->
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://global/skin/in-content/info-pages.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://userchromejs/content/aboutconfig/config.css" type="text/css"?>
|
||||
|
||||
<window id="config"
|
||||
title="about:config"
|
||||
csp="default-src chrome:; object-src 'none'"
|
||||
data-l10n-id="config-window"
|
||||
aria-describedby="warningTitle warningText"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
windowtype="Preferences:ConfigManager"
|
||||
role="application"
|
||||
width="750"
|
||||
height="500">
|
||||
|
||||
<script src="chrome://userchromejs/content/aboutconfig/config.js"/>
|
||||
<script src="chrome://global/content/editMenuOverlay.js"/>
|
||||
<script src="chrome://global/content/globalOverlay.js"/>
|
||||
|
||||
<menupopup id="configContext">
|
||||
<menuitem id="toggleSelected" label="Toggle" accesskey="T" default="true"/>
|
||||
<menuitem id="modifySelected" label="Modify" accesskey="M" default="true"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="copyPref" label="Copy" accesskey="C"/>
|
||||
<menuitem id="copyName" label="Copy Name" accesskey="N"/>
|
||||
<menuitem id="copyValue" label="Copy Value" accesskey="V"/>
|
||||
<menu label="New" accesskey="w">
|
||||
<menupopup>
|
||||
<menuitem id="configString" label="String" accesskey="S"/>
|
||||
<menuitem id="configInt" label="Integer" accesskey="I"/>
|
||||
<menuitem id="configBool" label="Boolean" accesskey="B"/>
|
||||
</menupopup>
|
||||
</menu>
|
||||
<menuitem id="resetSelected" label="Reset" accesskey="R"/>
|
||||
</menupopup>
|
||||
|
||||
<keyset id="configTreeKeyset" disabled="true">
|
||||
<key id="keyVKReturn" keycode="VK_RETURN"/>
|
||||
<key id="configFocuSearch" modifiers="accel" key="r"/>
|
||||
<key id="configFocuSearch2" modifiers="accel" key="f"/>
|
||||
</keyset>
|
||||
<deck id="configDeck" flex="1">
|
||||
<vbox id="warningScreen" flex="1" align="center" style="overflow: auto;">
|
||||
<spacer flex="1"/>
|
||||
<vbox id="warningBox" class="container">
|
||||
<hbox class="title" flex="1">
|
||||
<label id="warningTitle" value="This might void your warranty!" class="title-text" flex="1"></label>
|
||||
</hbox>
|
||||
<vbox class="description" flex="1">
|
||||
<label id="warningText">Changing these advanced settings can be harmful to the stability, security, and performance of this application. You should only continue if you are sure of what you are doing.</label>
|
||||
<checkbox id="showWarningNextTime" label="Show this warning next time" checked="true"/>
|
||||
<hbox class="button-container">
|
||||
<button id="warningButton" label="I accept the risk!" class="primary"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
</vbox>
|
||||
<spacer style="-moz-box-flex: 2"/>
|
||||
</vbox>
|
||||
<vbox flex="1">
|
||||
<hbox id="filterRow" align="center">
|
||||
<label value="Search:" accesskey="r" control="textbox"/>
|
||||
<search-textbox id="textbox" flex="1"
|
||||
aria-controls="configTree"/>
|
||||
</hbox>
|
||||
<tree id="configTree" flex="1" seltype="single"
|
||||
enableColumnDrag="true" context="configContext">
|
||||
<treecols>
|
||||
<!--
|
||||
The below code may suggest that 'ordinal' is still a supported XUL
|
||||
attribute. It is not. This is a crutch so that we can continue
|
||||
persisting the CSS -moz-box-ordinal-group attribute, which is the
|
||||
appropriate replacement for the ordinal attribute but cannot yet
|
||||
be easily persisted. The code that synchronizes the attribute with
|
||||
the CSS lives in toolkit/content/widget/tree.js and is specific to
|
||||
tree elements.
|
||||
-->
|
||||
<treecol id="prefCol" label="Preference Name" style="-moz-box-flex: 7"
|
||||
ignoreincolumnpicker="true"
|
||||
persist="hidden width ordinal sortDirection"/>
|
||||
<splitter class="tree-splitter" />
|
||||
<treecol id="lockCol" label="Status" flex="1"
|
||||
persist="hidden width ordinal sortDirection"/>
|
||||
<splitter class="tree-splitter" />
|
||||
<treecol id="typeCol" label="Type" flex="1"
|
||||
persist="hidden width ordinal sortDirection"/>
|
||||
<splitter class="tree-splitter" />
|
||||
<treecol id="valueCol" label="Value" style="-moz-box-flex: 10"
|
||||
persist="hidden width ordinal sortDirection"/>
|
||||
</treecols>
|
||||
|
||||
<treechildren id="configTreeBody"/>
|
||||
</tree>
|
||||
</vbox>
|
||||
</deck>
|
||||
</window>
|
49
chrome/utils/aboutconfig/config.css
Normal file
49
chrome/utils/aboutconfig/config.css
Normal file
|
@ -0,0 +1,49 @@
|
|||
/* 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/. */
|
||||
|
||||
#warningScreen {
|
||||
font-size: 15px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-inline-start: calc(48px + 4.6em);
|
||||
padding-inline-end: 48px;
|
||||
}
|
||||
|
||||
.title {
|
||||
background-image: url("chrome://global/skin/icons/warning.svg");
|
||||
fill: #fcd100;
|
||||
}
|
||||
|
||||
#warningTitle {
|
||||
font-weight: lighter;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
#warningText {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
#warningButton {
|
||||
margin-top: 0.6em;
|
||||
}
|
||||
|
||||
#filterRow {
|
||||
margin-top: 4px;
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
|
||||
#configTree {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
#configTreeBody::-moz-tree-cell-text(user) {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#configTreeBody::-moz-tree-cell-text(locked) {
|
||||
font-style: italic;
|
||||
}
|
781
chrome/utils/aboutconfig/config.js
Normal file
781
chrome/utils/aboutconfig/config.js
Normal file
|
@ -0,0 +1,781 @@
|
|||
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
|
||||
/* 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/. */
|
||||
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const nsIPrefLocalizedString = Ci.nsIPrefLocalizedString;
|
||||
const nsISupportsString = Ci.nsISupportsString;
|
||||
const nsIPrefBranch = Ci.nsIPrefBranch;
|
||||
const nsIClipboardHelper = Ci.nsIClipboardHelper;
|
||||
|
||||
const nsClipboardHelper_CONTRACTID = "@mozilla.org/widget/clipboardhelper;1";
|
||||
|
||||
const gPrefBranch = Services.prefs;
|
||||
const gClipboardHelper = Cc[nsClipboardHelper_CONTRACTID].getService(
|
||||
nsIClipboardHelper
|
||||
);
|
||||
|
||||
var gLockProps = ["default", "user", "locked"];
|
||||
// we get these from a string bundle
|
||||
var gLockStrs = [];
|
||||
var gTypeStrs = [];
|
||||
|
||||
const PREF_IS_DEFAULT_VALUE = 0;
|
||||
const PREF_IS_MODIFIED = 1;
|
||||
const PREF_IS_LOCKED = 2;
|
||||
|
||||
var gPrefHash = {};
|
||||
var gPrefArray = [];
|
||||
var gPrefView = gPrefArray; // share the JS array
|
||||
var gSortedColumn = "prefCol";
|
||||
var gSortFunction = null;
|
||||
var gSortDirection = 1; // 1 is ascending; -1 is descending
|
||||
var gFilter = null;
|
||||
|
||||
var view = {
|
||||
get rowCount() {
|
||||
return gPrefView.length;
|
||||
},
|
||||
getCellText(index, col) {
|
||||
if (!(index in gPrefView)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
var value = gPrefView[index][col.id];
|
||||
|
||||
switch (col.id) {
|
||||
case "lockCol":
|
||||
return gLockStrs[value];
|
||||
case "typeCol":
|
||||
return gTypeStrs[value];
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
},
|
||||
getRowProperties(index) {
|
||||
return "";
|
||||
},
|
||||
getCellProperties(index, col) {
|
||||
if (index in gPrefView) {
|
||||
return gLockProps[gPrefView[index].lockCol];
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
getColumnProperties(col) {
|
||||
return "";
|
||||
},
|
||||
treebox: null,
|
||||
selection: null,
|
||||
isContainer(index) {
|
||||
return false;
|
||||
},
|
||||
isContainerOpen(index) {
|
||||
return false;
|
||||
},
|
||||
isContainerEmpty(index) {
|
||||
return false;
|
||||
},
|
||||
isSorted() {
|
||||
return true;
|
||||
},
|
||||
canDrop(index, orientation) {
|
||||
return false;
|
||||
},
|
||||
drop(row, orientation) {},
|
||||
setTree(out) {
|
||||
this.treebox = out;
|
||||
},
|
||||
getParentIndex(rowIndex) {
|
||||
return -1;
|
||||
},
|
||||
hasNextSibling(rowIndex, afterIndex) {
|
||||
return false;
|
||||
},
|
||||
getLevel(index) {
|
||||
return 1;
|
||||
},
|
||||
getImageSrc(row, col) {
|
||||
return "";
|
||||
},
|
||||
toggleOpenState(index) {},
|
||||
cycleHeader(col) {
|
||||
var index = this.selection.currentIndex;
|
||||
if (col.id == gSortedColumn) {
|
||||
gSortDirection = -gSortDirection;
|
||||
gPrefArray.reverse();
|
||||
if (gPrefView != gPrefArray) {
|
||||
gPrefView.reverse();
|
||||
}
|
||||
if (index >= 0) {
|
||||
index = gPrefView.length - index - 1;
|
||||
}
|
||||
} else {
|
||||
var pref = null;
|
||||
if (index >= 0) {
|
||||
pref = gPrefView[index];
|
||||
}
|
||||
|
||||
var old = document.getElementById(gSortedColumn);
|
||||
old.removeAttribute("sortDirection");
|
||||
gPrefArray.sort((gSortFunction = gSortFunctions[col.id]));
|
||||
if (gPrefView != gPrefArray) {
|
||||
gPrefView.sort(gSortFunction);
|
||||
}
|
||||
gSortedColumn = col.id;
|
||||
if (pref) {
|
||||
index = getViewIndexOfPref(pref);
|
||||
}
|
||||
}
|
||||
col.element.setAttribute(
|
||||
"sortDirection",
|
||||
gSortDirection > 0 ? "ascending" : "descending"
|
||||
);
|
||||
this.treebox.invalidate();
|
||||
if (index >= 0) {
|
||||
this.selection.select(index);
|
||||
this.treebox.ensureRowIsVisible(index);
|
||||
}
|
||||
},
|
||||
selectionChanged() {},
|
||||
cycleCell(row, col) {},
|
||||
isEditable(row, col) {
|
||||
return false;
|
||||
},
|
||||
setCellValue(row, col, value) {},
|
||||
setCellText(row, col, value) {},
|
||||
isSeparator(index) {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
// find the index in gPrefView of a pref object
|
||||
// or -1 if it does not exist in the filtered view
|
||||
function getViewIndexOfPref(pref) {
|
||||
var low = -1,
|
||||
high = gPrefView.length;
|
||||
var index = (low + high) >> 1;
|
||||
while (index > low) {
|
||||
var mid = gPrefView[index];
|
||||
if (mid == pref) {
|
||||
return index;
|
||||
}
|
||||
if (gSortFunction(mid, pref) < 0) {
|
||||
low = index;
|
||||
} else {
|
||||
high = index;
|
||||
}
|
||||
index = (low + high) >> 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// find the index in gPrefView where a pref object belongs
|
||||
function getNearestViewIndexOfPref(pref) {
|
||||
var low = -1,
|
||||
high = gPrefView.length;
|
||||
var index = (low + high) >> 1;
|
||||
while (index > low) {
|
||||
if (gSortFunction(gPrefView[index], pref) < 0) {
|
||||
low = index;
|
||||
} else {
|
||||
high = index;
|
||||
}
|
||||
index = (low + high) >> 1;
|
||||
}
|
||||
return high;
|
||||
}
|
||||
|
||||
// find the index in gPrefArray of a pref object
|
||||
function getIndexOfPref(pref) {
|
||||
var low = -1,
|
||||
high = gPrefArray.length;
|
||||
var index = (low + high) >> 1;
|
||||
while (index > low) {
|
||||
var mid = gPrefArray[index];
|
||||
if (mid == pref) {
|
||||
return index;
|
||||
}
|
||||
if (gSortFunction(mid, pref) < 0) {
|
||||
low = index;
|
||||
} else {
|
||||
high = index;
|
||||
}
|
||||
index = (low + high) >> 1;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
function getNearestIndexOfPref(pref) {
|
||||
var low = -1,
|
||||
high = gPrefArray.length;
|
||||
var index = (low + high) >> 1;
|
||||
while (index > low) {
|
||||
if (gSortFunction(gPrefArray[index], pref) < 0) {
|
||||
low = index;
|
||||
} else {
|
||||
high = index;
|
||||
}
|
||||
index = (low + high) >> 1;
|
||||
}
|
||||
return high;
|
||||
}
|
||||
|
||||
var gPrefListener = {
|
||||
observe(subject, topic, prefName) {
|
||||
if (topic != "nsPref:changed") {
|
||||
return;
|
||||
}
|
||||
|
||||
var arrayIndex = gPrefArray.length;
|
||||
var viewIndex = arrayIndex;
|
||||
var selectedIndex = view.selection.currentIndex;
|
||||
var pref;
|
||||
var updateView = false;
|
||||
var updateArray = false;
|
||||
var addedRow = false;
|
||||
if (prefName in gPrefHash) {
|
||||
pref = gPrefHash[prefName];
|
||||
viewIndex = getViewIndexOfPref(pref);
|
||||
arrayIndex = getIndexOfPref(pref);
|
||||
fetchPref(prefName, arrayIndex);
|
||||
// fetchPref replaces the existing pref object
|
||||
pref = gPrefHash[prefName];
|
||||
if (viewIndex >= 0) {
|
||||
// Might need to update the filtered view
|
||||
gPrefView[viewIndex] = gPrefHash[prefName];
|
||||
view.treebox.invalidateRow(viewIndex);
|
||||
}
|
||||
if (gSortedColumn == "lockCol" || gSortedColumn == "valueCol") {
|
||||
updateArray = true;
|
||||
gPrefArray.splice(arrayIndex, 1);
|
||||
if (gFilter && gFilter.test(pref.prefCol + ";" + pref.valueCol)) {
|
||||
updateView = true;
|
||||
gPrefView.splice(viewIndex, 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fetchPref(prefName, arrayIndex);
|
||||
pref = gPrefArray.pop();
|
||||
updateArray = true;
|
||||
addedRow = true;
|
||||
if (gFilter && gFilter.test(pref.prefCol + ";" + pref.valueCol)) {
|
||||
updateView = true;
|
||||
}
|
||||
}
|
||||
if (updateArray) {
|
||||
// Reinsert in the data array
|
||||
var newIndex = getNearestIndexOfPref(pref);
|
||||
gPrefArray.splice(newIndex, 0, pref);
|
||||
|
||||
if (updateView) {
|
||||
// View is filtered, reinsert in the view separately
|
||||
newIndex = getNearestViewIndexOfPref(pref);
|
||||
gPrefView.splice(newIndex, 0, pref);
|
||||
} else if (gFilter) {
|
||||
// View is filtered, but nothing to update
|
||||
return;
|
||||
}
|
||||
|
||||
if (addedRow) {
|
||||
view.treebox.rowCountChanged(newIndex, 1);
|
||||
}
|
||||
|
||||
// Invalidate the changed range in the view
|
||||
var low = Math.min(viewIndex, newIndex);
|
||||
var high = Math.max(viewIndex, newIndex);
|
||||
view.treebox.invalidateRange(low, high);
|
||||
|
||||
if (selectedIndex == viewIndex) {
|
||||
selectedIndex = newIndex;
|
||||
} else if (selectedIndex >= low && selectedIndex <= high) {
|
||||
selectedIndex += newIndex > viewIndex ? -1 : 1;
|
||||
}
|
||||
if (selectedIndex >= 0) {
|
||||
view.selection.select(selectedIndex);
|
||||
if (selectedIndex == newIndex) {
|
||||
view.treebox.ensureRowIsVisible(selectedIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function prefObject(prefName, prefIndex) {
|
||||
this.prefCol = prefName;
|
||||
}
|
||||
|
||||
prefObject.prototype = {
|
||||
lockCol: PREF_IS_DEFAULT_VALUE,
|
||||
typeCol: nsIPrefBranch.PREF_STRING,
|
||||
valueCol: "",
|
||||
};
|
||||
|
||||
function fetchPref(prefName, prefIndex) {
|
||||
var pref = new prefObject(prefName);
|
||||
|
||||
gPrefHash[prefName] = pref;
|
||||
gPrefArray[prefIndex] = pref;
|
||||
|
||||
if (gPrefBranch.prefIsLocked(prefName)) {
|
||||
pref.lockCol = PREF_IS_LOCKED;
|
||||
} else if (gPrefBranch.prefHasUserValue(prefName)) {
|
||||
pref.lockCol = PREF_IS_MODIFIED;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (gPrefBranch.getPrefType(prefName)) {
|
||||
case gPrefBranch.PREF_BOOL:
|
||||
pref.typeCol = gPrefBranch.PREF_BOOL;
|
||||
// convert to a string
|
||||
pref.valueCol = gPrefBranch.getBoolPref(prefName).toString();
|
||||
break;
|
||||
case gPrefBranch.PREF_INT:
|
||||
pref.typeCol = gPrefBranch.PREF_INT;
|
||||
// convert to a string
|
||||
pref.valueCol = gPrefBranch.getIntPref(prefName).toString();
|
||||
break;
|
||||
default:
|
||||
case gPrefBranch.PREF_STRING:
|
||||
pref.valueCol = gPrefBranch.getStringPref(prefName);
|
||||
// Try in case it's a localized string (will throw an exception if not)
|
||||
if (
|
||||
pref.lockCol == PREF_IS_DEFAULT_VALUE &&
|
||||
/^chrome:\/\/.+\/locale\/.+\.properties/.test(pref.valueCol)
|
||||
) {
|
||||
pref.valueCol = gPrefBranch.getComplexValue(
|
||||
prefName,
|
||||
nsIPrefLocalizedString
|
||||
).data;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// Also catch obscure cases in which you can't tell in advance
|
||||
// that the pref exists but has no user or default value...
|
||||
}
|
||||
}
|
||||
|
||||
async function onConfigLoad() {
|
||||
let configContext = document.getElementById("configContext");
|
||||
configContext.addEventListener("popupshowing", function(event) {
|
||||
if (event.target == this) {
|
||||
updateContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
let commandListeners = {
|
||||
toggleSelected: ModifySelected,
|
||||
modifySelected: ModifySelected,
|
||||
copyPref,
|
||||
copyName,
|
||||
copyValue,
|
||||
resetSelected: ResetSelected,
|
||||
};
|
||||
|
||||
configContext.addEventListener("command", e => {
|
||||
if (e.target.id in commandListeners) {
|
||||
commandListeners[e.target.id]();
|
||||
}
|
||||
});
|
||||
|
||||
let configString = document.getElementById("configString");
|
||||
configString.addEventListener("command", function() {
|
||||
NewPref(nsIPrefBranch.PREF_STRING);
|
||||
});
|
||||
|
||||
let configInt = document.getElementById("configInt");
|
||||
configInt.addEventListener("command", function() {
|
||||
NewPref(nsIPrefBranch.PREF_INT);
|
||||
});
|
||||
|
||||
let configBool = document.getElementById("configBool");
|
||||
configBool.addEventListener("command", function() {
|
||||
NewPref(nsIPrefBranch.PREF_BOOL);
|
||||
});
|
||||
|
||||
let keyVKReturn = document.getElementById("keyVKReturn");
|
||||
keyVKReturn.addEventListener("command", ModifySelected);
|
||||
|
||||
let textBox = document.getElementById("textbox");
|
||||
textBox.addEventListener("command", FilterPrefs);
|
||||
|
||||
let configFocuSearch = document.getElementById("configFocuSearch");
|
||||
configFocuSearch.addEventListener("command", function() {
|
||||
textBox.focus();
|
||||
});
|
||||
|
||||
let configFocuSearch2 = document.getElementById("configFocuSearch2");
|
||||
configFocuSearch2.addEventListener("command", function() {
|
||||
textBox.focus();
|
||||
});
|
||||
|
||||
let warningButton = document.getElementById("warningButton");
|
||||
warningButton.addEventListener("command", ShowPrefs);
|
||||
|
||||
let configTree = document.getElementById("configTree");
|
||||
configTree.addEventListener("select", function() {
|
||||
window.updateCommands("select");
|
||||
});
|
||||
|
||||
let configTreeBody = document.getElementById("configTreeBody");
|
||||
configTreeBody.addEventListener("dblclick", function(event) {
|
||||
if (event.button == 0) {
|
||||
ModifySelected();
|
||||
}
|
||||
});
|
||||
|
||||
gLockStrs[PREF_IS_DEFAULT_VALUE] = 'default';
|
||||
gLockStrs[PREF_IS_MODIFIED] = 'modified';
|
||||
gLockStrs[PREF_IS_LOCKED] = 'locked';
|
||||
gTypeStrs[nsIPrefBranch.PREF_STRING] = 'string';
|
||||
gTypeStrs[nsIPrefBranch.PREF_INT] = 'integer';
|
||||
gTypeStrs[nsIPrefBranch.PREF_BOOL] = 'boolean';
|
||||
|
||||
var showWarning = gPrefBranch.getBoolPref("general.warnOnAboutConfig");
|
||||
|
||||
if (showWarning) {
|
||||
document.getElementById("warningButton").focus();
|
||||
} else {
|
||||
ShowPrefs();
|
||||
}
|
||||
}
|
||||
|
||||
// Unhide the warning message
|
||||
function ShowPrefs() {
|
||||
document.getElementById('configDeck').lastElementChild.style.visibility = 'visible';
|
||||
gPrefBranch.getChildList("").forEach(fetchPref);
|
||||
|
||||
var descending = document.getElementsByAttribute(
|
||||
"sortDirection",
|
||||
"descending"
|
||||
);
|
||||
if (descending.item(0)) {
|
||||
gSortedColumn = descending[0].id;
|
||||
gSortDirection = -1;
|
||||
} else {
|
||||
var ascending = document.getElementsByAttribute(
|
||||
"sortDirection",
|
||||
"ascending"
|
||||
);
|
||||
if (ascending.item(0)) {
|
||||
gSortedColumn = ascending[0].id;
|
||||
} else {
|
||||
document
|
||||
.getElementById(gSortedColumn)
|
||||
.setAttribute("sortDirection", "ascending");
|
||||
}
|
||||
}
|
||||
gSortFunction = gSortFunctions[gSortedColumn];
|
||||
gPrefArray.sort(gSortFunction);
|
||||
|
||||
gPrefBranch.addObserver("", gPrefListener);
|
||||
|
||||
var configTree = document.getElementById("configTree");
|
||||
configTree.view = view;
|
||||
configTree.controllers.insertControllerAt(0, configController);
|
||||
|
||||
document.getElementById("configDeck").setAttribute("selectedIndex", 1);
|
||||
document.getElementById("configTreeKeyset").removeAttribute("disabled");
|
||||
if (!document.getElementById("showWarningNextTime").checked) {
|
||||
gPrefBranch.setBoolPref("general.warnOnAboutConfig", false);
|
||||
}
|
||||
|
||||
// Process about:config?filter=<string>
|
||||
var textbox = document.getElementById("textbox");
|
||||
// About URIs don't support query params, so do this manually
|
||||
var loc = document.location.href;
|
||||
var matches = /[?&]filter\=([^&]+)/i.exec(loc);
|
||||
if (matches) {
|
||||
textbox.value = decodeURIComponent(matches[1]);
|
||||
}
|
||||
|
||||
// Even if we did not set the filter string via the URL query,
|
||||
// textbox might have been set via some other mechanism
|
||||
if (textbox.value) {
|
||||
FilterPrefs();
|
||||
}
|
||||
textbox.focus();
|
||||
}
|
||||
|
||||
function onConfigUnload() {
|
||||
if (
|
||||
document.getElementById("configDeck").getAttribute("selectedIndex") == 1
|
||||
) {
|
||||
gPrefBranch.removeObserver("", gPrefListener);
|
||||
var configTree = document.getElementById("configTree");
|
||||
configTree.view = null;
|
||||
configTree.controllers.removeController(configController);
|
||||
}
|
||||
}
|
||||
|
||||
function FilterPrefs() {
|
||||
if (
|
||||
document.getElementById("configDeck").getAttribute("selectedIndex") != 1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
var substring = document.getElementById("textbox").value;
|
||||
// Check for "/regex/[i]"
|
||||
if (substring.charAt(0) == "/") {
|
||||
var r = substring.match(/^\/(.*)\/(i?)$/);
|
||||
try {
|
||||
gFilter = RegExp(r[1], r[2]);
|
||||
} catch (e) {
|
||||
return; // Do nothing on incomplete or bad RegExp
|
||||
}
|
||||
} else if (substring) {
|
||||
gFilter = RegExp(
|
||||
substring
|
||||
.replace(/([^* \w])/g, "\\$1")
|
||||
.replace(/^\*+/, "")
|
||||
.replace(/\*+/g, ".*"),
|
||||
"i"
|
||||
);
|
||||
} else {
|
||||
gFilter = null;
|
||||
}
|
||||
|
||||
var prefCol =
|
||||
view.selection && view.selection.currentIndex < 0
|
||||
? null
|
||||
: gPrefView[view.selection.currentIndex].prefCol;
|
||||
var oldlen = gPrefView.length;
|
||||
gPrefView = gPrefArray;
|
||||
if (gFilter) {
|
||||
gPrefView = [];
|
||||
for (var i = 0; i < gPrefArray.length; ++i) {
|
||||
if (gFilter.test(gPrefArray[i].prefCol + ";" + gPrefArray[i].valueCol)) {
|
||||
gPrefView.push(gPrefArray[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
view.treebox.invalidate();
|
||||
view.treebox.rowCountChanged(oldlen, gPrefView.length - oldlen);
|
||||
gotoPref(prefCol);
|
||||
}
|
||||
|
||||
function prefColSortFunction(x, y) {
|
||||
if (x.prefCol > y.prefCol) {
|
||||
return gSortDirection;
|
||||
}
|
||||
if (x.prefCol < y.prefCol) {
|
||||
return -gSortDirection;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function lockColSortFunction(x, y) {
|
||||
if (x.lockCol != y.lockCol) {
|
||||
return gSortDirection * (y.lockCol - x.lockCol);
|
||||
}
|
||||
return prefColSortFunction(x, y);
|
||||
}
|
||||
|
||||
function typeColSortFunction(x, y) {
|
||||
if (x.typeCol != y.typeCol) {
|
||||
return gSortDirection * (y.typeCol - x.typeCol);
|
||||
}
|
||||
return prefColSortFunction(x, y);
|
||||
}
|
||||
|
||||
function valueColSortFunction(x, y) {
|
||||
if (x.valueCol > y.valueCol) {
|
||||
return gSortDirection;
|
||||
}
|
||||
if (x.valueCol < y.valueCol) {
|
||||
return -gSortDirection;
|
||||
}
|
||||
return prefColSortFunction(x, y);
|
||||
}
|
||||
|
||||
const gSortFunctions = {
|
||||
prefCol: prefColSortFunction,
|
||||
lockCol: lockColSortFunction,
|
||||
typeCol: typeColSortFunction,
|
||||
valueCol: valueColSortFunction,
|
||||
};
|
||||
|
||||
const gCategoryLabelForSortColumn = {
|
||||
prefCol: "SortByName",
|
||||
lockCol: "SortByStatus",
|
||||
typeCol: "SortByType",
|
||||
valueCol: "SortByValue",
|
||||
};
|
||||
|
||||
const configController = {
|
||||
supportsCommand: function supportsCommand(command) {
|
||||
return command == "cmd_copy";
|
||||
},
|
||||
isCommandEnabled: function isCommandEnabled(command) {
|
||||
return view.selection && view.selection.currentIndex >= 0;
|
||||
},
|
||||
doCommand: function doCommand(command) {
|
||||
copyPref();
|
||||
},
|
||||
onEvent: function onEvent(event) {},
|
||||
};
|
||||
|
||||
function updateContextMenu() {
|
||||
var lockCol = PREF_IS_LOCKED;
|
||||
var typeCol = nsIPrefBranch.PREF_STRING;
|
||||
var valueCol = "";
|
||||
var copyDisabled = true;
|
||||
var prefSelected = view.selection.currentIndex >= 0;
|
||||
|
||||
if (prefSelected) {
|
||||
var prefRow = gPrefView[view.selection.currentIndex];
|
||||
lockCol = prefRow.lockCol;
|
||||
typeCol = prefRow.typeCol;
|
||||
valueCol = prefRow.valueCol;
|
||||
copyDisabled = false;
|
||||
}
|
||||
|
||||
var copyPref = document.getElementById("copyPref");
|
||||
copyPref.setAttribute("disabled", copyDisabled);
|
||||
|
||||
var copyName = document.getElementById("copyName");
|
||||
copyName.setAttribute("disabled", copyDisabled);
|
||||
|
||||
var copyValue = document.getElementById("copyValue");
|
||||
copyValue.setAttribute("disabled", copyDisabled);
|
||||
|
||||
var resetSelected = document.getElementById("resetSelected");
|
||||
resetSelected.setAttribute("disabled", lockCol != PREF_IS_MODIFIED);
|
||||
|
||||
var canToggle = typeCol == nsIPrefBranch.PREF_BOOL && valueCol != "";
|
||||
// indicates that a pref is locked or no pref is selected at all
|
||||
var isLocked = lockCol == PREF_IS_LOCKED;
|
||||
|
||||
var modifySelected = document.getElementById("modifySelected");
|
||||
modifySelected.setAttribute("disabled", isLocked);
|
||||
modifySelected.hidden = canToggle;
|
||||
|
||||
var toggleSelected = document.getElementById("toggleSelected");
|
||||
toggleSelected.setAttribute("disabled", isLocked);
|
||||
toggleSelected.hidden = !canToggle;
|
||||
}
|
||||
|
||||
function copyPref() {
|
||||
var pref = gPrefView[view.selection.currentIndex];
|
||||
gClipboardHelper.copyString(pref.prefCol + ";" + pref.valueCol);
|
||||
}
|
||||
|
||||
function copyName() {
|
||||
gClipboardHelper.copyString(gPrefView[view.selection.currentIndex].prefCol);
|
||||
}
|
||||
|
||||
function copyValue() {
|
||||
gClipboardHelper.copyString(gPrefView[view.selection.currentIndex].valueCol);
|
||||
}
|
||||
|
||||
function ModifySelected() {
|
||||
if (view.selection.currentIndex >= 0) {
|
||||
ModifyPref(gPrefView[view.selection.currentIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
function ResetSelected() {
|
||||
var entry = gPrefView[view.selection.currentIndex];
|
||||
gPrefBranch.clearUserPref(entry.prefCol);
|
||||
}
|
||||
|
||||
async function NewPref(type) {
|
||||
var result = { value: "" };
|
||||
var dummy = { value: 0 };
|
||||
|
||||
let [newTitle, newPrompt] = [`New ${gTypeStrs[type]} value`, 'Enter the preference name'];
|
||||
|
||||
if (
|
||||
Services.prompt.prompt(window, newTitle, newPrompt, result, null, dummy)
|
||||
) {
|
||||
result.value = result.value.trim();
|
||||
if (!result.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
var pref;
|
||||
if (result.value in gPrefHash) {
|
||||
pref = gPrefHash[result.value];
|
||||
} else {
|
||||
pref = {
|
||||
prefCol: result.value,
|
||||
lockCol: PREF_IS_DEFAULT_VALUE,
|
||||
typeCol: type,
|
||||
valueCol: "",
|
||||
};
|
||||
}
|
||||
if (ModifyPref(pref)) {
|
||||
setTimeout(gotoPref, 0, result.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function gotoPref(pref) {
|
||||
// make sure the pref exists and is displayed in the current view
|
||||
var index = pref in gPrefHash ? getViewIndexOfPref(gPrefHash[pref]) : -1;
|
||||
if (index >= 0) {
|
||||
view.selection.select(index);
|
||||
view.treebox.ensureRowIsVisible(index);
|
||||
} else {
|
||||
view.selection.clearSelection();
|
||||
view.selection.currentIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
async function ModifyPref(entry) {
|
||||
if (entry.lockCol == PREF_IS_LOCKED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let [title] = [`Enter ${gTypeStrs[entry.typeCol]} value`];
|
||||
|
||||
if (entry.typeCol == nsIPrefBranch.PREF_BOOL) {
|
||||
var check = { value: entry.valueCol == "false" };
|
||||
if (
|
||||
!entry.valueCol &&
|
||||
!Services.prompt.select(
|
||||
window,
|
||||
title,
|
||||
entry.prefCol,
|
||||
[false, true],
|
||||
check
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
gPrefBranch.setBoolPref(entry.prefCol, check.value);
|
||||
} else {
|
||||
var result = { value: entry.valueCol };
|
||||
var dummy = { value: 0 };
|
||||
if (
|
||||
!Services.prompt.prompt(window, title, entry.prefCol, result, null, dummy)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (entry.typeCol == nsIPrefBranch.PREF_INT) {
|
||||
// | 0 converts to integer or 0; - 0 to float or NaN.
|
||||
// Thus, this check should catch all cases.
|
||||
var val = result.value | 0;
|
||||
if (val != result.value - 0) {
|
||||
const [err_title, err_text] = ['Invalid value', 'The text you entered is not a number.'];
|
||||
|
||||
Services.prompt.alert(window, err_title, err_text);
|
||||
return false;
|
||||
}
|
||||
gPrefBranch.setIntPref(entry.prefCol, val);
|
||||
} else {
|
||||
gPrefBranch.setStringPref(entry.prefCol, result.value);
|
||||
}
|
||||
}
|
||||
|
||||
Services.prefs.savePrefFile(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
window.onload = onConfigLoad;
|
||||
window.onunload = onConfigUnload;
|
2
chrome/utils/chrome.manifest
Normal file
2
chrome/utils/chrome.manifest
Normal file
|
@ -0,0 +1,2 @@
|
|||
content userchromejs ./
|
||||
resource userchromejs ../
|
42
chrome/utils/hookFunction.jsm
Normal file
42
chrome/utils/hookFunction.jsm
Normal file
|
@ -0,0 +1,42 @@
|
|||
let EXPORTED_SYMBOLS = ['hookFunction'];
|
||||
|
||||
/**
|
||||
* Add hooks to a function to execute before and after it. The function to modify is functionContext[functionName]. Call only once per function - modification is not supported.
|
||||
*
|
||||
* Other addons wishing to access the original function may do so using the .originalFunction member of the replacement function. This member can also be set if required, to insert a new function replacement into the chain rather than appending.
|
||||
*
|
||||
* @param functionContext The object on which the function is a property
|
||||
* @param functionName The name of the property containing the function (on functionContext)
|
||||
* @param onBeforeFunction A function to be called before the hooked function is executed. It will be passed the same parameters as the hooked function. It's return value will be passed on to onAfterFunction
|
||||
* @param onAfterFunction A function to be called after the hooked function is executed. The parameters passed to it are: onBeforeFunction return value, arguments object from original hooked function, return value from original hooked function. It's return value will be returned in place of that of the original function.
|
||||
* @returns A function which can be called to safely un-hook the hook
|
||||
*/
|
||||
function hookFunction(functionContext, functionName, onBeforeFunction, onAfterFunction) {
|
||||
let originalFunction = functionContext[functionName];
|
||||
|
||||
if (!originalFunction) {
|
||||
throw new Error("Could not find function " + functionName);
|
||||
}
|
||||
|
||||
let replacementFunction = function() {
|
||||
let onBeforeResult = null;
|
||||
if (onBeforeFunction) {
|
||||
onBeforeResult = onBeforeFunction.apply(this, arguments);
|
||||
}
|
||||
let originalResult = replacementFunction.originalFunction.apply(this, arguments);
|
||||
if (onAfterFunction) {
|
||||
return onAfterFunction.call(this, onBeforeResult, arguments, originalResult);
|
||||
} else {
|
||||
return originalResult;
|
||||
}
|
||||
}
|
||||
replacementFunction.originalFunction = originalFunction;
|
||||
functionContext[functionName] = replacementFunction;
|
||||
|
||||
return function () {
|
||||
// Not safe to simply assign originalFunction back again, as something else might have chained onto this function, which would then break the chain
|
||||
// Unassigning these variables prevent any effects of the hook, though the function itself remains in place.
|
||||
onBeforeFunction = null;
|
||||
onAfterFunction = null;
|
||||
};
|
||||
}
|
845
chrome/utils/passwordmgr/passwordManager.js
Normal file
845
chrome/utils/passwordmgr/passwordManager.js
Normal file
|
@ -0,0 +1,845 @@
|
|||
/* 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/. */
|
||||
|
||||
/** * =================== SAVED SIGNONS CODE =================== ***/
|
||||
/* eslint-disable-next-line no-var */
|
||||
var { AppConstants } = ChromeUtils.import(
|
||||
"resource://gre/modules/AppConstants.jsm"
|
||||
);
|
||||
/* eslint-disable-next-line no-var */
|
||||
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"DeferredTask",
|
||||
"resource://gre/modules/DeferredTask.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm"
|
||||
);
|
||||
|
||||
// Default value for signon table sorting
|
||||
let lastSignonSortColumn = "origin";
|
||||
let lastSignonSortAscending = true;
|
||||
|
||||
let showingPasswords = false;
|
||||
|
||||
// password-manager lists
|
||||
let signons = [];
|
||||
let deletedSignons = [];
|
||||
|
||||
// Elements that would be used frequently
|
||||
let filterField;
|
||||
let togglePasswordsButton;
|
||||
let signonsIntro;
|
||||
let removeButton;
|
||||
let removeAllButton;
|
||||
let signonsTree;
|
||||
|
||||
let signonReloadDisplay = {
|
||||
observe(subject, topic, data) {
|
||||
if (topic == "passwordmgr-storage-changed") {
|
||||
switch (data) {
|
||||
case "addLogin":
|
||||
case "modifyLogin":
|
||||
case "removeLogin":
|
||||
case "removeAllLogins":
|
||||
if (!signonsTree) {
|
||||
return;
|
||||
}
|
||||
signons.length = 0;
|
||||
LoadSignons();
|
||||
// apply the filter if needed
|
||||
if (filterField && filterField.value != "") {
|
||||
FilterPasswords();
|
||||
}
|
||||
signonsTree.ensureRowIsVisible(
|
||||
signonsTree.view.selection.currentIndex
|
||||
);
|
||||
break;
|
||||
}
|
||||
Services.obs.notifyObservers(null, "passwordmgr-dialog-updated");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Formatter for localization.
|
||||
let dateFormatter = new Services.intl.DateTimeFormat(undefined, {
|
||||
dateStyle: "medium",
|
||||
});
|
||||
let dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
});
|
||||
|
||||
function Startup() {
|
||||
// be prepared to reload the display if anything changes
|
||||
Services.obs.addObserver(signonReloadDisplay, "passwordmgr-storage-changed");
|
||||
|
||||
signonsTree = document.getElementById("signonsTree");
|
||||
filterField = document.getElementById("filter");
|
||||
togglePasswordsButton = document.getElementById("togglePasswords");
|
||||
signonsIntro = document.getElementById("signonsIntro");
|
||||
removeButton = document.getElementById("removeSignon");
|
||||
removeAllButton = document.getElementById("removeAllSignons");
|
||||
|
||||
togglePasswordsButton.label = "Show Passwords";
|
||||
togglePasswordsButton.accessKey = "P";
|
||||
signonsIntro.textContent = "Logins for the following sites are stored on your computer";
|
||||
removeAllButton.label = "Remove All";
|
||||
removeAllButton.accessKey = "A";
|
||||
|
||||
if (Services.policies && !Services.policies.isAllowed("passwordReveal")) {
|
||||
togglePasswordsButton.hidden = true;
|
||||
}
|
||||
|
||||
document
|
||||
.getElementsByTagName("treecols")[0]
|
||||
.addEventListener("click", event => {
|
||||
let { target, button } = event;
|
||||
let sortField = target.getAttribute("data-field-name");
|
||||
|
||||
if (target.nodeName != "treecol" || button != 0 || !sortField) {
|
||||
return;
|
||||
}
|
||||
|
||||
SignonColumnSort(sortField);
|
||||
Services.telemetry
|
||||
.getKeyedHistogramById("PWMGR_MANAGE_SORTED")
|
||||
.add(sortField);
|
||||
});
|
||||
|
||||
LoadSignons();
|
||||
|
||||
// filter the table if requested by caller
|
||||
if (
|
||||
window.arguments &&
|
||||
window.arguments[0] &&
|
||||
window.arguments[0].filterString
|
||||
) {
|
||||
setFilter(window.arguments[0].filterString);
|
||||
}
|
||||
|
||||
FocusFilterBox();
|
||||
}
|
||||
|
||||
function Shutdown() {
|
||||
Services.obs.removeObserver(
|
||||
signonReloadDisplay,
|
||||
"passwordmgr-storage-changed"
|
||||
);
|
||||
}
|
||||
|
||||
function setFilter(aFilterString) {
|
||||
filterField.value = aFilterString;
|
||||
FilterPasswords();
|
||||
}
|
||||
|
||||
let signonsTreeView = {
|
||||
_filterSet: [],
|
||||
_lastSelectedRanges: [],
|
||||
selection: null,
|
||||
|
||||
rowCount: 0,
|
||||
setTree(tree) {},
|
||||
getImageSrc(row, column) {
|
||||
if (column.element.getAttribute("id") !== "siteCol") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const signon = GetVisibleLogins()[row];
|
||||
|
||||
return PlacesUtils.urlWithSizeRef(window, "page-icon:" + signon.origin, 16);
|
||||
},
|
||||
getCellValue(row, column) {},
|
||||
getCellText(row, column) {
|
||||
let time;
|
||||
let signon = GetVisibleLogins()[row];
|
||||
switch (column.id) {
|
||||
case "siteCol":
|
||||
return signon.httpRealm
|
||||
? signon.origin + " (" + signon.httpRealm + ")"
|
||||
: signon.origin;
|
||||
case "userCol":
|
||||
return signon.username || "";
|
||||
case "passwordCol":
|
||||
return signon.password || "";
|
||||
case "timeCreatedCol":
|
||||
time = new Date(signon.timeCreated);
|
||||
return dateFormatter.format(time);
|
||||
case "timeLastUsedCol":
|
||||
time = new Date(signon.timeLastUsed);
|
||||
return dateAndTimeFormatter.format(time);
|
||||
case "timePasswordChangedCol":
|
||||
time = new Date(signon.timePasswordChanged);
|
||||
return dateFormatter.format(time);
|
||||
case "timesUsedCol":
|
||||
return signon.timesUsed;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
},
|
||||
isEditable(row, col) {
|
||||
if (col.id == "userCol" || col.id == "passwordCol") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isSeparator(index) {
|
||||
return false;
|
||||
},
|
||||
isSorted() {
|
||||
return false;
|
||||
},
|
||||
isContainer(index) {
|
||||
return false;
|
||||
},
|
||||
cycleHeader(column) {},
|
||||
getRowProperties(row) {
|
||||
return "";
|
||||
},
|
||||
getColumnProperties(column) {
|
||||
return "";
|
||||
},
|
||||
getCellProperties(row, column) {
|
||||
if (column.element.getAttribute("id") == "siteCol") {
|
||||
return "ltr";
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
setCellText(row, col, value) {
|
||||
let table = GetVisibleLogins();
|
||||
function _editLogin(field) {
|
||||
if (value == table[row][field]) {
|
||||
return;
|
||||
}
|
||||
let existingLogin = table[row].clone();
|
||||
table[row][field] = value;
|
||||
table[row].timePasswordChanged = Date.now();
|
||||
Services.logins.modifyLogin(existingLogin, table[row]);
|
||||
signonsTree.invalidateRow(row);
|
||||
}
|
||||
|
||||
if (col.id == "userCol") {
|
||||
_editLogin("username");
|
||||
} else if (col.id == "passwordCol") {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
_editLogin("password");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function SortTree(column, ascending) {
|
||||
let table = GetVisibleLogins();
|
||||
// remember which item was selected so we can restore it after the sort
|
||||
let selections = GetTreeSelections();
|
||||
let selectedNumber = selections.length ? table[selections[0]].number : -1;
|
||||
function compareFunc(a, b) {
|
||||
let valA, valB;
|
||||
switch (column) {
|
||||
case "origin":
|
||||
let realmA = a.httpRealm;
|
||||
let realmB = b.httpRealm;
|
||||
realmA = realmA == null ? "" : realmA.toLowerCase();
|
||||
realmB = realmB == null ? "" : realmB.toLowerCase();
|
||||
|
||||
valA = a[column].toLowerCase() + realmA;
|
||||
valB = b[column].toLowerCase() + realmB;
|
||||
break;
|
||||
case "username":
|
||||
case "password":
|
||||
valA = a[column].toLowerCase();
|
||||
valB = b[column].toLowerCase();
|
||||
break;
|
||||
|
||||
default:
|
||||
valA = a[column];
|
||||
valB = b[column];
|
||||
}
|
||||
|
||||
if (valA < valB) {
|
||||
return -1;
|
||||
}
|
||||
if (valA > valB) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// do the sort
|
||||
table.sort(compareFunc);
|
||||
if (!ascending) {
|
||||
table.reverse();
|
||||
}
|
||||
|
||||
// restore the selection
|
||||
let selectedRow = -1;
|
||||
if (selectedNumber >= 0 && false) {
|
||||
for (let s = 0; s < table.length; s++) {
|
||||
if (table[s].number == selectedNumber) {
|
||||
// update selection
|
||||
// note: we need to deselect before reselecting in order to trigger ...Selected()
|
||||
signonsTree.view.selection.select(-1);
|
||||
signonsTree.view.selection.select(s);
|
||||
selectedRow = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// display the results
|
||||
signonsTree.invalidate();
|
||||
if (selectedRow >= 0) {
|
||||
signonsTree.ensureRowIsVisible(selectedRow);
|
||||
}
|
||||
}
|
||||
|
||||
function LoadSignons() {
|
||||
// loads signons into table
|
||||
try {
|
||||
signons = Services.logins.getAllLogins();
|
||||
} catch (e) {
|
||||
signons = [];
|
||||
}
|
||||
signons.forEach(login => login.QueryInterface(Ci.nsILoginMetaInfo));
|
||||
signonsTreeView.rowCount = signons.length;
|
||||
|
||||
// sort and display the table
|
||||
signonsTree.view = signonsTreeView;
|
||||
// The sort column didn't change. SortTree (called by
|
||||
// SignonColumnSort) assumes we want to toggle the sort
|
||||
// direction but here we don't so we have to trick it
|
||||
lastSignonSortAscending = !lastSignonSortAscending;
|
||||
SignonColumnSort(lastSignonSortColumn);
|
||||
|
||||
// disable "remove all signons" button if there are no signons
|
||||
if (!signons.length) {
|
||||
removeAllButton.setAttribute("disabled", "true");
|
||||
togglePasswordsButton.setAttribute("disabled", "true");
|
||||
} else {
|
||||
removeAllButton.removeAttribute("disabled");
|
||||
togglePasswordsButton.removeAttribute("disabled");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function GetVisibleLogins() {
|
||||
return signonsTreeView._filterSet.length
|
||||
? signonsTreeView._filterSet
|
||||
: signons;
|
||||
}
|
||||
|
||||
function GetTreeSelections() {
|
||||
let selections = [];
|
||||
let select = signonsTree.view.selection;
|
||||
if (select) {
|
||||
let count = select.getRangeCount();
|
||||
let min = {};
|
||||
let max = {};
|
||||
for (let i = 0; i < count; i++) {
|
||||
select.getRangeAt(i, min, max);
|
||||
for (let k = min.value; k <= max.value; k++) {
|
||||
if (k != -1) {
|
||||
selections[selections.length] = k;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selections;
|
||||
}
|
||||
|
||||
function SignonSelected() {
|
||||
let selections = GetTreeSelections();
|
||||
if (selections.length) {
|
||||
removeButton.removeAttribute("disabled");
|
||||
} else {
|
||||
removeButton.setAttribute("disabled", true);
|
||||
}
|
||||
}
|
||||
|
||||
function DeleteSignon() {
|
||||
let syncNeeded = !!signonsTreeView._filterSet.length;
|
||||
let tree = signonsTree;
|
||||
let view = signonsTreeView;
|
||||
let table = GetVisibleLogins();
|
||||
|
||||
// Turn off tree selection notifications during the deletion
|
||||
tree.view.selection.selectEventsSuppressed = true;
|
||||
|
||||
// remove selected items from list (by setting them to null) and place in deleted list
|
||||
let selections = GetTreeSelections();
|
||||
for (let s = selections.length - 1; s >= 0; s--) {
|
||||
let i = selections[s];
|
||||
deletedSignons.push(table[i]);
|
||||
table[i] = null;
|
||||
}
|
||||
|
||||
// collapse list by removing all the null entries
|
||||
for (let j = 0; j < table.length; j++) {
|
||||
if (table[j] == null) {
|
||||
let k = j;
|
||||
while (k < table.length && table[k] == null) {
|
||||
k++;
|
||||
}
|
||||
table.splice(j, k - j);
|
||||
view.rowCount -= k - j;
|
||||
tree.rowCountChanged(j, j - k);
|
||||
}
|
||||
}
|
||||
|
||||
// update selection and/or buttons
|
||||
if (table.length) {
|
||||
// update selection
|
||||
let nextSelection =
|
||||
selections[0] < table.length ? selections[0] : table.length - 1;
|
||||
tree.view.selection.select(nextSelection);
|
||||
} else {
|
||||
// disable buttons
|
||||
removeButton.setAttribute("disabled", "true");
|
||||
removeAllButton.setAttribute("disabled", "true");
|
||||
}
|
||||
tree.view.selection.selectEventsSuppressed = false;
|
||||
FinalizeSignonDeletions(syncNeeded);
|
||||
}
|
||||
|
||||
async function DeleteAllSignons() {
|
||||
// Confirm the user wants to remove all passwords
|
||||
let dummy = { value: false };
|
||||
if (
|
||||
Services.prompt.confirmEx(
|
||||
window,
|
||||
"Remove all passwords",
|
||||
"Are you sure you wish to remove all passwords?",
|
||||
Services.prompt.STD_YES_NO_BUTTONS + Services.prompt.BUTTON_POS_1_DEFAULT,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
dummy
|
||||
) == 1
|
||||
) {
|
||||
// 1 == "No" button
|
||||
return;
|
||||
}
|
||||
|
||||
let syncNeeded = !!signonsTreeView._filterSet.length;
|
||||
let view = signonsTreeView;
|
||||
let table = GetVisibleLogins();
|
||||
|
||||
// remove all items from table and place in deleted table
|
||||
for (let i = 0; i < table.length; i++) {
|
||||
deletedSignons.push(table[i]);
|
||||
}
|
||||
table.length = 0;
|
||||
|
||||
// clear out selections
|
||||
view.selection.select(-1);
|
||||
|
||||
// update the tree view and notify the tree
|
||||
view.rowCount = 0;
|
||||
|
||||
signonsTree.rowCountChanged(0, -deletedSignons.length);
|
||||
signonsTree.invalidate();
|
||||
|
||||
// disable buttons
|
||||
removeButton.setAttribute("disabled", "true");
|
||||
removeAllButton.setAttribute("disabled", "true");
|
||||
FinalizeSignonDeletions(syncNeeded);
|
||||
Services.telemetry.getHistogramById("PWMGR_MANAGE_DELETED_ALL").add(1);
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"weave:telemetry:histogram",
|
||||
"PWMGR_MANAGE_DELETED_ALL"
|
||||
);
|
||||
}
|
||||
|
||||
async function TogglePasswordVisible() {
|
||||
if (showingPasswords || (await masterPasswordLogin(AskUserShowPasswords))) {
|
||||
showingPasswords = !showingPasswords;
|
||||
togglePasswordsButton.label = showingPasswords ? "Hide Passwords" : "Show Passwords";
|
||||
togglePasswordsButton.accessKey = "P";
|
||||
document.getElementById("passwordCol").hidden = !showingPasswords;
|
||||
FilterPasswords();
|
||||
}
|
||||
|
||||
// Notify observers that the password visibility toggling is
|
||||
// completed. (Mostly useful for tests)
|
||||
Services.obs.notifyObservers(null, "passwordmgr-password-toggle-complete");
|
||||
Services.telemetry
|
||||
.getHistogramById("PWMGR_MANAGE_VISIBILITY_TOGGLED")
|
||||
.add(showingPasswords);
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"weave:telemetry:histogram",
|
||||
"PWMGR_MANAGE_VISIBILITY_TOGGLED"
|
||||
);
|
||||
}
|
||||
|
||||
async function AskUserShowPasswords() {
|
||||
let dummy = { value: false };
|
||||
|
||||
// Confirm the user wants to display passwords
|
||||
return (
|
||||
Services.prompt.confirmEx(
|
||||
window,
|
||||
null,
|
||||
"Are you sure you wish to show your passwords?",
|
||||
Services.prompt.STD_YES_NO_BUTTONS,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
dummy
|
||||
) == 0
|
||||
); // 0=="Yes" button
|
||||
}
|
||||
|
||||
function FinalizeSignonDeletions(syncNeeded) {
|
||||
for (let s = 0; s < deletedSignons.length; s++) {
|
||||
Services.logins.removeLogin(deletedSignons[s]);
|
||||
Services.telemetry.getHistogramById("PWMGR_MANAGE_DELETED").add(1);
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"weave:telemetry:histogram",
|
||||
"PWMGR_MANAGE_DELETED"
|
||||
);
|
||||
}
|
||||
// If the deletion has been performed in a filtered view, reflect the deletion in the unfiltered table.
|
||||
// See bug 405389.
|
||||
if (syncNeeded) {
|
||||
try {
|
||||
signons = Services.logins.getAllLogins();
|
||||
} catch (e) {
|
||||
signons = [];
|
||||
}
|
||||
}
|
||||
deletedSignons.length = 0;
|
||||
}
|
||||
|
||||
function HandleSignonKeyPress(e) {
|
||||
// If editing is currently performed, don't do anything.
|
||||
if (signonsTree.getAttribute("editing")) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
e.keyCode == KeyboardEvent.DOM_VK_DELETE ||
|
||||
(AppConstants.platform == "macosx" &&
|
||||
e.keyCode == KeyboardEvent.DOM_VK_BACK_SPACE)
|
||||
) {
|
||||
DeleteSignon();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function getColumnByName(column) {
|
||||
switch (column) {
|
||||
case "origin":
|
||||
return document.getElementById("siteCol");
|
||||
case "username":
|
||||
return document.getElementById("userCol");
|
||||
case "password":
|
||||
return document.getElementById("passwordCol");
|
||||
case "timeCreated":
|
||||
return document.getElementById("timeCreatedCol");
|
||||
case "timeLastUsed":
|
||||
return document.getElementById("timeLastUsedCol");
|
||||
case "timePasswordChanged":
|
||||
return document.getElementById("timePasswordChangedCol");
|
||||
case "timesUsed":
|
||||
return document.getElementById("timesUsedCol");
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function SignonColumnSort(column) {
|
||||
let sortedCol = getColumnByName(column);
|
||||
let lastSortedCol = getColumnByName(lastSignonSortColumn);
|
||||
|
||||
// clear out the sortDirection attribute on the old column
|
||||
lastSortedCol.removeAttribute("sortDirection");
|
||||
|
||||
// determine if sort is to be ascending or descending
|
||||
lastSignonSortAscending =
|
||||
column == lastSignonSortColumn ? !lastSignonSortAscending : true;
|
||||
|
||||
// sort
|
||||
lastSignonSortColumn = column;
|
||||
SortTree(lastSignonSortColumn, lastSignonSortAscending);
|
||||
|
||||
// set the sortDirection attribute to get the styling going
|
||||
// first we need to get the right element
|
||||
sortedCol.setAttribute(
|
||||
"sortDirection",
|
||||
lastSignonSortAscending ? "ascending" : "descending"
|
||||
);
|
||||
}
|
||||
|
||||
function SignonClearFilter() {
|
||||
let singleSelection = signonsTreeView.selection.count == 1;
|
||||
|
||||
// Clear the Tree Display
|
||||
signonsTreeView.rowCount = 0;
|
||||
signonsTree.rowCountChanged(0, -signonsTreeView._filterSet.length);
|
||||
signonsTreeView._filterSet = [];
|
||||
|
||||
// Just reload the list to make sure deletions are respected
|
||||
LoadSignons();
|
||||
|
||||
// Restore selection
|
||||
if (singleSelection) {
|
||||
signonsTreeView.selection.clearSelection();
|
||||
for (let i = 0; i < signonsTreeView._lastSelectedRanges.length; ++i) {
|
||||
let range = signonsTreeView._lastSelectedRanges[i];
|
||||
signonsTreeView.selection.rangedSelect(range.min, range.max, true);
|
||||
}
|
||||
} else {
|
||||
signonsTreeView.selection.select(0);
|
||||
}
|
||||
signonsTreeView._lastSelectedRanges = [];
|
||||
|
||||
signonsIntro.textContent = "Logins for the following sites are stored on your computer";
|
||||
removeAllButton.label = "Remove All";
|
||||
removeAllButton.accessKey = "A";
|
||||
}
|
||||
|
||||
function FocusFilterBox() {
|
||||
if (filterField.getAttribute("focused") != "true") {
|
||||
filterField.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function SignonMatchesFilter(aSignon, aFilterValue) {
|
||||
if (aSignon.origin.toLowerCase().includes(aFilterValue)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
aSignon.username &&
|
||||
aSignon.username.toLowerCase().includes(aFilterValue)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
aSignon.httpRealm &&
|
||||
aSignon.httpRealm.toLowerCase().includes(aFilterValue)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
showingPasswords &&
|
||||
aSignon.password &&
|
||||
aSignon.password.toLowerCase().includes(aFilterValue)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function _filterPasswords(aFilterValue, view) {
|
||||
aFilterValue = aFilterValue.toLowerCase();
|
||||
return signons.filter(s => SignonMatchesFilter(s, aFilterValue));
|
||||
}
|
||||
|
||||
function SignonSaveState() {
|
||||
// Save selection
|
||||
let seln = signonsTreeView.selection;
|
||||
signonsTreeView._lastSelectedRanges = [];
|
||||
let rangeCount = seln.getRangeCount();
|
||||
for (let i = 0; i < rangeCount; ++i) {
|
||||
let min = {};
|
||||
let max = {};
|
||||
seln.getRangeAt(i, min, max);
|
||||
signonsTreeView._lastSelectedRanges.push({
|
||||
min: min.value,
|
||||
max: max.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function FilterPasswords() {
|
||||
if (filterField.value == "") {
|
||||
SignonClearFilter();
|
||||
return;
|
||||
}
|
||||
|
||||
let newFilterSet = _filterPasswords(filterField.value, signonsTreeView);
|
||||
if (!signonsTreeView._filterSet.length) {
|
||||
// Save Display Info for the Non-Filtered mode when we first
|
||||
// enter Filtered mode.
|
||||
SignonSaveState();
|
||||
}
|
||||
signonsTreeView._filterSet = newFilterSet;
|
||||
|
||||
// Clear the display
|
||||
let oldRowCount = signonsTreeView.rowCount;
|
||||
signonsTreeView.rowCount = 0;
|
||||
signonsTree.rowCountChanged(0, -oldRowCount);
|
||||
// Set up the filtered display
|
||||
signonsTreeView.rowCount = signonsTreeView._filterSet.length;
|
||||
signonsTree.rowCountChanged(0, signonsTreeView.rowCount);
|
||||
|
||||
// if the view is not empty then select the first item
|
||||
if (signonsTreeView.rowCount > 0) {
|
||||
signonsTreeView.selection.select(0);
|
||||
}
|
||||
|
||||
signonsIntro.textContent = "The following logins match your search:";
|
||||
removeAllButton.label = "Remove All Shown";
|
||||
removeAllButton.accessKey = "A";
|
||||
}
|
||||
|
||||
function CopySiteUrl() {
|
||||
// Copy selected site url to clipboard
|
||||
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
|
||||
Ci.nsIClipboardHelper
|
||||
);
|
||||
let row = signonsTree.currentIndex;
|
||||
let url = signonsTreeView.getCellText(row, { id: "siteCol" });
|
||||
clipboard.copyString(url);
|
||||
}
|
||||
|
||||
async function CopyPassword() {
|
||||
// Don't copy passwords if we aren't already showing the passwords & a master
|
||||
// password hasn't been entered.
|
||||
if (!showingPasswords && !(await masterPasswordLogin())) {
|
||||
return;
|
||||
}
|
||||
// Copy selected signon's password to clipboard
|
||||
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
|
||||
Ci.nsIClipboardHelper
|
||||
);
|
||||
let row = signonsTree.currentIndex;
|
||||
let password = signonsTreeView.getCellText(row, { id: "passwordCol" });
|
||||
clipboard.copyString(password);
|
||||
Services.telemetry.getHistogramById("PWMGR_MANAGE_COPIED_PASSWORD").add(1);
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"weave:telemetry:histogram",
|
||||
"PWMGR_MANAGE_COPIED_PASSWORD"
|
||||
);
|
||||
}
|
||||
|
||||
function CopyUsername() {
|
||||
// Copy selected signon's username to clipboard
|
||||
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
|
||||
Ci.nsIClipboardHelper
|
||||
);
|
||||
let row = signonsTree.currentIndex;
|
||||
let username = signonsTreeView.getCellText(row, { id: "userCol" });
|
||||
clipboard.copyString(username);
|
||||
Services.telemetry.getHistogramById("PWMGR_MANAGE_COPIED_USERNAME").add(1);
|
||||
Services.obs.notifyObservers(
|
||||
null,
|
||||
"weave:telemetry:histogram",
|
||||
"PWMGR_MANAGE_COPIED_USERNAME"
|
||||
);
|
||||
}
|
||||
|
||||
function EditCellInSelectedRow(columnName) {
|
||||
let row = signonsTree.currentIndex;
|
||||
let columnElement = getColumnByName(columnName);
|
||||
signonsTree.startEditing(
|
||||
row,
|
||||
signonsTree.columns.getColumnFor(columnElement)
|
||||
);
|
||||
}
|
||||
|
||||
function LaunchSiteUrl() {
|
||||
let row = signonsTree.currentIndex;
|
||||
let url = signonsTreeView.getCellText(row, { id: "siteCol" });
|
||||
window.openWebLinkIn(url, "tab");
|
||||
}
|
||||
|
||||
function UpdateContextMenu() {
|
||||
let singleSelection = signonsTreeView.selection.count == 1;
|
||||
let menuItems = new Map();
|
||||
let menupopup = document.getElementById("signonsTreeContextMenu");
|
||||
for (let menuItem of menupopup.querySelectorAll("menuitem")) {
|
||||
menuItems.set(menuItem.id, menuItem);
|
||||
}
|
||||
|
||||
if (!singleSelection) {
|
||||
for (let menuItem of menuItems.values()) {
|
||||
menuItem.setAttribute("disabled", "true");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedRow = signonsTree.currentIndex;
|
||||
|
||||
// Don't display "Launch Site URL" if we're not a browser.
|
||||
if (window.openWebLinkIn) {
|
||||
menuItems.get("context-launchsiteurl").removeAttribute("disabled");
|
||||
} else {
|
||||
menuItems.get("context-launchsiteurl").setAttribute("disabled", "true");
|
||||
menuItems.get("context-launchsiteurl").setAttribute("hidden", "true");
|
||||
}
|
||||
|
||||
// Disable "Copy Username" if the username is empty.
|
||||
if (signonsTreeView.getCellText(selectedRow, { id: "userCol" }) != "") {
|
||||
menuItems.get("context-copyusername").removeAttribute("disabled");
|
||||
} else {
|
||||
menuItems.get("context-copyusername").setAttribute("disabled", "true");
|
||||
}
|
||||
|
||||
menuItems.get("context-copysiteurl").removeAttribute("disabled");
|
||||
menuItems.get("context-editusername").removeAttribute("disabled");
|
||||
menuItems.get("context-copypassword").removeAttribute("disabled");
|
||||
|
||||
// Disable "Edit Password" if the password column isn't showing.
|
||||
if (!document.getElementById("passwordCol").hidden) {
|
||||
menuItems.get("context-editpassword").removeAttribute("disabled");
|
||||
} else {
|
||||
menuItems.get("context-editpassword").setAttribute("disabled", "true");
|
||||
}
|
||||
}
|
||||
|
||||
async function masterPasswordLogin(noPasswordCallback) {
|
||||
// This does no harm if master password isn't set.
|
||||
let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance(
|
||||
Ci.nsIPK11TokenDB
|
||||
);
|
||||
let token = tokendb.getInternalKeyToken();
|
||||
|
||||
// If there is no master password, still give the user a chance to opt-out of displaying passwords
|
||||
if (token.checkPassword("")) {
|
||||
return noPasswordCallback ? noPasswordCallback() : true;
|
||||
}
|
||||
|
||||
// So there's a master password. But since checkPassword didn't succeed, we're logged out (per nsIPK11Token.idl).
|
||||
try {
|
||||
// Relogin and ask for the master password.
|
||||
token.login(true); // 'true' means always prompt for token password. User will be prompted until
|
||||
// clicking 'Cancel' or entering the correct password.
|
||||
} catch (e) {
|
||||
// An exception will be thrown if the user cancels the login prompt dialog.
|
||||
// User is also logged out of Software Security Device.
|
||||
}
|
||||
|
||||
return token.isLoggedIn();
|
||||
}
|
||||
|
||||
function escapeKeyHandler() {
|
||||
// If editing is currently performed, don't do anything.
|
||||
if (signonsTree.getAttribute("editing")) {
|
||||
return;
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
|
||||
function OpenMigrator() {
|
||||
const { MigrationUtils } = ChromeUtils.import(
|
||||
"resource:///modules/MigrationUtils.jsm"
|
||||
);
|
||||
// We pass in the type of source we're using for use in telemetry:
|
||||
MigrationUtils.showMigrationWizard(window, [
|
||||
MigrationUtils.MIGRATION_ENTRYPOINT_PASSWORDS,
|
||||
]);
|
||||
}
|
135
chrome/utils/passwordmgr/passwordManager.xhtml
Normal file
135
chrome/utils/passwordmgr/passwordManager.xhtml
Normal file
|
@ -0,0 +1,135 @@
|
|||
<?xml version="1.0"?> <!-- -*- Mode: SGML; indent-tabs-mode: nil -*- -->
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://global/skin/passwordmgr.css" type="text/css"?>
|
||||
|
||||
<window id="SignonViewerDialog"
|
||||
windowtype="Toolkit:PasswordManager"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||
onload="Startup();"
|
||||
onunload="Shutdown();"
|
||||
title="Saved Logins"
|
||||
style="min-width: 45em;"
|
||||
persist="width height screenX screenY">
|
||||
|
||||
<script src="chrome://browser/content/utilityOverlay.js"/>
|
||||
<script src="chrome://userchromejs/content/passwordmgr/passwordManager.js"/>
|
||||
|
||||
<keyset>
|
||||
<key keycode="VK_ESCAPE" oncommand="escapeKeyHandler();"/>
|
||||
<key key="w" modifiers="accel" oncommand="escapeKeyHandler();"/>
|
||||
<key key="f" modifiers="accel" oncommand="FocusFilterBox();"/>
|
||||
<key key="k" modifiers="accel" oncommand="FocusFilterBox();"/>
|
||||
</keyset>
|
||||
|
||||
<popupset id="signonsTreeContextSet">
|
||||
<menupopup id="signonsTreeContextMenu"
|
||||
onpopupshowing="UpdateContextMenu()">
|
||||
<menuitem id="context-copysiteurl"
|
||||
label="Copy URL"
|
||||
accesskey="y"
|
||||
oncommand="CopySiteUrl()"/>
|
||||
<menuitem id="context-launchsiteurl"
|
||||
label="Visit URL"
|
||||
accesskey="V"
|
||||
oncommand="LaunchSiteUrl()"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="context-copyusername"
|
||||
label="Copy Username"
|
||||
accesskey="U"
|
||||
oncommand="CopyUsername()"/>
|
||||
<menuitem id="context-editusername"
|
||||
label="Edit Username"
|
||||
accesskey="d"
|
||||
oncommand="EditCellInSelectedRow('username')"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="context-copypassword"
|
||||
label="Copy Password"
|
||||
accesskey="C"
|
||||
oncommand="CopyPassword()"/>
|
||||
<menuitem id="context-editpassword"
|
||||
label="Edit Password"
|
||||
accesskey="E"
|
||||
oncommand="EditCellInSelectedRow('password')"/>
|
||||
</menupopup>
|
||||
</popupset>
|
||||
|
||||
<!-- saved signons -->
|
||||
<vbox id="savedsignons" class="contentPane" flex="1">
|
||||
<!-- filter -->
|
||||
<hbox align="center">
|
||||
<search-textbox id="filter" flex="1"
|
||||
aria-controls="signonsTree"
|
||||
oncommand="FilterPasswords();"
|
||||
accesskey="S"
|
||||
placeholder="Search"/>
|
||||
</hbox>
|
||||
|
||||
<label control="signonsTree" id="signonsIntro"/>
|
||||
<separator class="thin"/>
|
||||
<tree id="signonsTree" flex="1"
|
||||
width="750"
|
||||
style="height: 20em;"
|
||||
onkeypress="HandleSignonKeyPress(event)"
|
||||
onselect="SignonSelected();"
|
||||
editable="true"
|
||||
context="signonsTreeContextMenu">
|
||||
<treecols>
|
||||
<treecol id="siteCol" label="Site" style="-moz-box-flex: 40"
|
||||
data-field-name="origin" persist="width"
|
||||
ignoreincolumnpicker="true"
|
||||
sortDirection="ascending"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="userCol" label="Username" style="-moz-box-flex: 25"
|
||||
ignoreincolumnpicker="true"
|
||||
data-field-name="username" persist="width"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="passwordCol" label="Password" style="-moz-box-flex: 15"
|
||||
ignoreincolumnpicker="true"
|
||||
data-field-name="password" persist="width"
|
||||
hidden="true"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="timeCreatedCol" label="First Used" style="-moz-box-flex: 10"
|
||||
data-field-name="timeCreated" persist="width hidden"
|
||||
hidden="true"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="timeLastUsedCol" label="Last Used" style="-moz-box-flex: 20"
|
||||
data-field-name="timeLastUsed" persist="width hidden"
|
||||
hidden="true"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="timePasswordChangedCol" label="Last Changed" style="-moz-box-flex: 10"
|
||||
data-field-name="timePasswordChanged" persist="width hidden"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
<treecol id="timesUsedCol" label="Times Used" flex="1"
|
||||
data-field-name="timesUsed" persist="width hidden"
|
||||
hidden="true"/>
|
||||
<splitter class="tree-splitter"/>
|
||||
</treecols>
|
||||
<treechildren/>
|
||||
</tree>
|
||||
<separator class="thin"/>
|
||||
<hbox id="SignonViewerButtons">
|
||||
<button id="removeSignon" disabled="true"
|
||||
label="Remove"
|
||||
accesskey="R"
|
||||
oncommand="DeleteSignon();"/>
|
||||
<button id="removeAllSignons"
|
||||
oncommand="DeleteAllSignons();"/>
|
||||
<spacer flex="1"/>
|
||||
<button label="Import"
|
||||
accesskey="I"
|
||||
oncommand="OpenMigrator();"/>
|
||||
<button id="togglePasswords"
|
||||
oncommand="TogglePasswordVisible();"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<hbox align="end">
|
||||
<hbox class="actionButtons">
|
||||
<spacer flex="1"/>
|
||||
<button oncommand="window.close();"
|
||||
label="Close"
|
||||
accesskey="C"/>
|
||||
</hbox>
|
||||
</hbox>
|
||||
</window>
|
22
chrome/utils/passwordmgr/passwordmgr.css
Normal file
22
chrome/utils/passwordmgr/passwordmgr.css
Normal file
|
@ -0,0 +1,22 @@
|
|||
/* 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/. */
|
||||
|
||||
@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
|
||||
|
||||
.contentPane {
|
||||
margin: 9px 8px 5px;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
margin: 0 3px 6px;
|
||||
}
|
||||
|
||||
treechildren::-moz-tree-image(siteCol) {
|
||||
list-style-image: url(chrome://mozapps/skin/places/defaultFavicon.svg);
|
||||
-moz-context-properties: fill;
|
||||
fill: currentColor;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-inline-end: 5px;
|
||||
}
|
BIN
chrome/utils/styloaix/16.png
Normal file
BIN
chrome/utils/styloaix/16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 661 B |
BIN
chrome/utils/styloaix/16w.png
Normal file
BIN
chrome/utils/styloaix/16w.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 589 B |
320
chrome/utils/styloaix/autocomplete.js
Normal file
320
chrome/utils/styloaix/autocomplete.js
Normal file
|
@ -0,0 +1,320 @@
|
|||
/* 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";
|
||||
|
||||
const AutocompletePopup = require('devtools/client/shared/autocomplete-popup');
|
||||
|
||||
let loader;
|
||||
try {
|
||||
({ loader } = ChromeUtils.import('resource://devtools/shared/loader/Loader.jsm'));
|
||||
} catch (e) {
|
||||
// tb91
|
||||
({ loader } = ChromeUtils.import('resource://devtools/shared/Loader.jsm'));
|
||||
}
|
||||
|
||||
loader.lazyRequireGetter(
|
||||
this,
|
||||
"KeyCodes",
|
||||
"devtools/client/shared/keycodes",
|
||||
true
|
||||
);
|
||||
loader.lazyRequireGetter(
|
||||
this,
|
||||
"CSSCompleter",
|
||||
"devtools/client/shared/sourceeditor/css-autocompleter"
|
||||
);
|
||||
|
||||
const autocompleteMap = new WeakMap();
|
||||
|
||||
/**
|
||||
* Prepares an editor instance for autocompletion.
|
||||
*/
|
||||
function initializeAutoCompletion(ctx, options = {}) {
|
||||
const { cm, ed, Editor } = ctx;
|
||||
if (autocompleteMap.has(ed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const win = ed.container.contentWindow.wrappedJSObject;
|
||||
const { CodeMirror } = win;
|
||||
|
||||
let completer = null;
|
||||
const autocompleteKey =
|
||||
"Ctrl-" + Editor.keyFor("autocompletion", { noaccel: true });
|
||||
if (ed.config.mode == Editor.modes.css) {
|
||||
completer = new CSSCompleter({
|
||||
walker: options.walker,
|
||||
cssProperties: options.cssProperties,
|
||||
maxEntries: 1000,
|
||||
});
|
||||
}
|
||||
|
||||
function insertSelectedPopupItem() {
|
||||
const autocompleteState = autocompleteMap.get(ed);
|
||||
if (!popup || !popup.isOpen || !autocompleteState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!autocompleteState.suggestionInsertedOnce && popup.selectedItem) {
|
||||
autocompleteMap.get(ed).insertingSuggestion = true;
|
||||
insertPopupItem(ed, popup.selectedItem);
|
||||
}
|
||||
|
||||
popup.once("popup-closed", () => {
|
||||
// This event is used in tests.
|
||||
ed.emit("popup-hidden");
|
||||
});
|
||||
popup.hidePopup();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Give each popup a new name to avoid sharing the elements.
|
||||
|
||||
let popup = new AutocompletePopup(win.parent.document, {
|
||||
position: "bottom",
|
||||
autoSelect: true,
|
||||
onClick: insertSelectedPopupItem,
|
||||
});
|
||||
|
||||
const cycle = reverse => {
|
||||
if (popup?.isOpen) {
|
||||
// eslint-disable-next-line mozilla/no-compare-against-boolean-literals
|
||||
cycleSuggestions(ed, reverse == true);
|
||||
return null;
|
||||
}
|
||||
|
||||
return CodeMirror.Pass;
|
||||
};
|
||||
|
||||
let keyMap = {
|
||||
Tab: cycle,
|
||||
Down: cycle,
|
||||
"Shift-Tab": cycle.bind(null, true),
|
||||
Up: cycle.bind(null, true),
|
||||
Enter: () => {
|
||||
const wasHandled = insertSelectedPopupItem();
|
||||
return wasHandled ? true : CodeMirror.Pass;
|
||||
},
|
||||
};
|
||||
|
||||
const autoCompleteCallback = autoComplete.bind(null, ctx);
|
||||
const keypressCallback = onEditorKeypress.bind(null, ctx);
|
||||
keyMap[autocompleteKey] = autoCompleteCallback;
|
||||
cm.addKeyMap(keyMap);
|
||||
|
||||
cm.on("keydown", keypressCallback);
|
||||
ed.on("change", autoCompleteCallback);
|
||||
ed.on("destroy", destroy);
|
||||
|
||||
function destroy() {
|
||||
ed.off("destroy", destroy);
|
||||
cm.off("keydown", keypressCallback);
|
||||
ed.off("change", autoCompleteCallback);
|
||||
cm.removeKeyMap(keyMap);
|
||||
popup.destroy();
|
||||
keyMap = popup = completer = null;
|
||||
autocompleteMap.delete(ed);
|
||||
}
|
||||
|
||||
autocompleteMap.set(ed, {
|
||||
popup: popup,
|
||||
completer: completer,
|
||||
keyMap: keyMap,
|
||||
destroy: destroy,
|
||||
insertingSuggestion: false,
|
||||
suggestionInsertedOnce: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides suggestions to autocomplete the current token/word being typed.
|
||||
*/
|
||||
function autoComplete({ ed, cm }) {
|
||||
const autocompleteOpts = autocompleteMap.get(ed);
|
||||
const { completer, popup } = autocompleteOpts;
|
||||
if (
|
||||
!completer ||
|
||||
autocompleteOpts.insertingSuggestion ||
|
||||
autocompleteOpts.doNotAutocomplete
|
||||
) {
|
||||
autocompleteOpts.insertingSuggestion = false;
|
||||
return;
|
||||
}
|
||||
const cur = ed.getCursor();
|
||||
completer
|
||||
.complete(cm.getRange({ line: 0, ch: 0 }, cur), cur)
|
||||
.then(suggestions => {
|
||||
if (
|
||||
!suggestions ||
|
||||
!suggestions.length ||
|
||||
suggestions[0].preLabel == null
|
||||
) {
|
||||
autocompleteOpts.suggestionInsertedOnce = false;
|
||||
popup.once("popup-closed", () => {
|
||||
// This event is used in tests.
|
||||
ed.emit("after-suggest");
|
||||
});
|
||||
popup.hidePopup();
|
||||
return;
|
||||
}
|
||||
// The cursor is at the end of the currently entered part of the token,
|
||||
// like "backgr|" but we need to open the popup at the beginning of the
|
||||
// character "b". Thus we need to calculate the width of the entered part
|
||||
// of the token ("backgr" here).
|
||||
|
||||
const cursorElement = cm.display.cursorDiv.querySelector(
|
||||
".CodeMirror-cursor"
|
||||
);
|
||||
const left = suggestions[0].preLabel.length * cm.defaultCharWidth();
|
||||
popup.hidePopup();
|
||||
popup.setItems(suggestions);
|
||||
|
||||
popup.once("popup-opened", () => {
|
||||
// This event is used in tests.
|
||||
ed.emit("after-suggest");
|
||||
});
|
||||
popup.openPopup(cursorElement, -1 * left, 0);
|
||||
autocompleteOpts.suggestionInsertedOnce = false;
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a popup item into the current cursor location
|
||||
* in the editor.
|
||||
*/
|
||||
function insertPopupItem(ed, popupItem) {
|
||||
const { preLabel, text } = popupItem;
|
||||
const cur = ed.getCursor();
|
||||
const textBeforeCursor = ed.getText(cur.line).substring(0, cur.ch);
|
||||
const backwardsTextBeforeCursor = textBeforeCursor
|
||||
.split("")
|
||||
.reverse()
|
||||
.join("");
|
||||
const backwardsPreLabel = preLabel
|
||||
.split("")
|
||||
.reverse()
|
||||
.join("");
|
||||
|
||||
// If there is additional text in the preLabel vs the line, then
|
||||
// just insert the entire autocomplete text. An example:
|
||||
// if you type 'a' and select '#about' from the autocomplete menu,
|
||||
// then the final text needs to the end up as '#about'.
|
||||
if (backwardsPreLabel.indexOf(backwardsTextBeforeCursor) === 0) {
|
||||
ed.replaceText(text, { line: cur.line, ch: 0 }, cur);
|
||||
} else {
|
||||
ed.replaceText(text.slice(preLabel.length), cur, cur);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycles through provided suggestions by the popup in a top to bottom manner
|
||||
* when `reverse` is not true. Opposite otherwise.
|
||||
*/
|
||||
function cycleSuggestions(ed, reverse) {
|
||||
const autocompleteOpts = autocompleteMap.get(ed);
|
||||
const { popup } = autocompleteOpts;
|
||||
const cur = ed.getCursor();
|
||||
autocompleteOpts.insertingSuggestion = true;
|
||||
if (!autocompleteOpts.suggestionInsertedOnce) {
|
||||
autocompleteOpts.suggestionInsertedOnce = true;
|
||||
let firstItem;
|
||||
if (reverse) {
|
||||
firstItem = popup.getItemAtIndex(popup.itemCount - 1);
|
||||
popup.selectPreviousItem();
|
||||
} else {
|
||||
firstItem = popup.getItemAtIndex(0);
|
||||
if (firstItem.label == firstItem.preLabel && popup.itemCount > 1) {
|
||||
firstItem = popup.getItemAtIndex(1);
|
||||
popup.selectNextItem();
|
||||
}
|
||||
}
|
||||
if (popup.itemCount == 1) {
|
||||
popup.hidePopup();
|
||||
}
|
||||
insertPopupItem(ed, firstItem);
|
||||
} else {
|
||||
const fromCur = {
|
||||
line: cur.line,
|
||||
ch: cur.ch - popup.selectedItem.text.length,
|
||||
};
|
||||
if (reverse) {
|
||||
popup.selectPreviousItem();
|
||||
} else {
|
||||
popup.selectNextItem();
|
||||
}
|
||||
ed.replaceText(popup.selectedItem.text, fromCur, cur);
|
||||
}
|
||||
// This event is used in tests.
|
||||
ed.emit("suggestion-entered");
|
||||
}
|
||||
|
||||
/**
|
||||
* onkeydown handler for the editor instance to prevent autocompleting on some
|
||||
* keypresses.
|
||||
*/
|
||||
function onEditorKeypress({ ed, Editor }, cm, event) {
|
||||
const autocompleteOpts = autocompleteMap.get(ed);
|
||||
|
||||
// Do not try to autocomplete with multiple selections.
|
||||
if (ed.hasMultipleSelections()) {
|
||||
autocompleteOpts.doNotAutocomplete = true;
|
||||
autocompleteOpts.popup.hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.keyCode == KeyCodes.DOM_VK_SPACE
|
||||
) {
|
||||
// When Ctrl/Cmd + Space is pressed, two simultaneous keypresses are emitted
|
||||
// first one for just the Ctrl/Cmd and second one for combo. The first one
|
||||
// leave the autocompleteOpts.doNotAutocomplete as true, so we have to make
|
||||
// it false
|
||||
autocompleteOpts.doNotAutocomplete = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.ctrlKey || event.metaKey || event.altKey) {
|
||||
autocompleteOpts.doNotAutocomplete = true;
|
||||
autocompleteOpts.popup.hidePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.keyCode) {
|
||||
case KeyCodes.DOM_VK_RETURN:
|
||||
autocompleteOpts.doNotAutocomplete = true;
|
||||
break;
|
||||
case KeyCodes.DOM_VK_ESCAPE:
|
||||
if (autocompleteOpts.popup.isOpen) {
|
||||
// Prevent the Console input to open, but still remove the autocomplete popup.
|
||||
autocompleteOpts.doNotAutocomplete = true;
|
||||
autocompleteOpts.popup.hidePopup();
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
case KeyCodes.DOM_VK_LEFT:
|
||||
case KeyCodes.DOM_VK_RIGHT:
|
||||
case KeyCodes.DOM_VK_HOME:
|
||||
case KeyCodes.DOM_VK_END:
|
||||
autocompleteOpts.doNotAutocomplete = true;
|
||||
autocompleteOpts.popup.hidePopup();
|
||||
break;
|
||||
case KeyCodes.DOM_VK_BACK_SPACE:
|
||||
case KeyCodes.DOM_VK_DELETE:
|
||||
if (ed.config.mode == Editor.modes.css) {
|
||||
autocompleteOpts.completer.invalidateCache(ed.getCursor().line);
|
||||
}
|
||||
autocompleteOpts.doNotAutocomplete = true;
|
||||
autocompleteOpts.popup.hidePopup();
|
||||
break;
|
||||
default:
|
||||
autocompleteOpts.doNotAutocomplete = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Export functions
|
||||
|
||||
exports.initializeAutoCompletion = initializeAutoCompletion;
|
48
chrome/utils/styloaix/edit.css
Normal file
48
chrome/utils/styloaix/edit.css
Normal file
|
@ -0,0 +1,48 @@
|
|||
#styloaix-edit {
|
||||
min-width: 850px;
|
||||
}
|
||||
#errors {
|
||||
margin: 2px 0px 4px 5px;
|
||||
color: red;
|
||||
}
|
||||
#errors label {
|
||||
cursor: pointer;
|
||||
}
|
||||
#internal-code {
|
||||
font-family: monospace;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.findbar-closebutton, .findbar-highlight, *[anonid="find-case-sensitive"] {
|
||||
display: none;
|
||||
}
|
||||
#findbar {
|
||||
border-top: 0;
|
||||
}
|
||||
.devtools-horizontal-splitter {
|
||||
/* Overriding global.css */
|
||||
height: 3px !important;
|
||||
}
|
||||
input.devtools-textinput {
|
||||
padding: 4px;
|
||||
margin: 0 6px 0 4px;
|
||||
border: 1px solid ThreeDShadow;
|
||||
-moz-box-flex: 1;
|
||||
}
|
||||
.devtools-toolbarbutton {
|
||||
border-radius: 2px !important;
|
||||
border: 1px solid rgba(0,0,0,0.3) !important;
|
||||
padding: 1px 5px !important;
|
||||
}
|
||||
.devtools-separator {
|
||||
margin: 0 5px 0 4px !important;
|
||||
}
|
||||
.devtools-toolbarbutton[disabled="true"] {
|
||||
opacity: 0.2;
|
||||
}
|
||||
.update-url-row {
|
||||
display: none;
|
||||
}
|
||||
#editor-tools {
|
||||
padding: 5px;
|
||||
-moz-box-align: center;
|
||||
}
|
459
chrome/utils/styloaix/edit.js
Normal file
459
chrome/utils/styloaix/edit.js
Normal file
|
@ -0,0 +1,459 @@
|
|||
const { Services } = ChromeUtils.import('resource://gre/modules/Services.jsm');
|
||||
const { NetUtil } = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
|
||||
|
||||
let require;
|
||||
try {
|
||||
({ require } = ChromeUtils.import('resource://devtools/shared/loader/Loader.jsm'));
|
||||
} catch (e) {
|
||||
// tb91
|
||||
({ require } = ChromeUtils.import('resource://devtools/shared/Loader.jsm'));
|
||||
}
|
||||
|
||||
docShell.cssErrorReportingEnabled = true;
|
||||
|
||||
function require_mini (m) {
|
||||
let scope = {
|
||||
exports: {}
|
||||
};
|
||||
Services.scriptloader.loadSubScript('chrome://' + m + '.js', scope);
|
||||
return scope.exports;
|
||||
};
|
||||
|
||||
let url;
|
||||
let type;
|
||||
let id;
|
||||
let style;
|
||||
|
||||
if (isChromeWindow) {
|
||||
let params = window.arguments[0];
|
||||
url = params.url;
|
||||
type = params.type;
|
||||
id = params.id;
|
||||
} else {
|
||||
let params = new URLSearchParams(location.search);
|
||||
url = params.get('url');
|
||||
type = params.get('type');
|
||||
id = params.get('id');
|
||||
}
|
||||
|
||||
origin = 2;
|
||||
let lastOrigin;
|
||||
let unsaved = false;
|
||||
let previewCode;
|
||||
let previewOrigin;
|
||||
let previewActive = false;
|
||||
let isInstantPreview;
|
||||
let isInstantCheck;
|
||||
let timeoutRunning = false;
|
||||
let interval;
|
||||
let nameE;
|
||||
let initialCode = '';
|
||||
let sourceEditor;
|
||||
|
||||
function init () {
|
||||
if (id)
|
||||
style = UC.styloaix.styles.get(id);
|
||||
if (style) {
|
||||
origin = style.type;
|
||||
NetUtil.asyncFetch(
|
||||
{
|
||||
uri: style.url,
|
||||
loadingNode: document,
|
||||
securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT || Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS,
|
||||
contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
|
||||
},
|
||||
async function (stream) {
|
||||
const bstream = Cc['@mozilla.org/binaryinputstream;1'].createInstance(Ci.nsIBinaryInputStream);
|
||||
bstream.setInputStream(stream);
|
||||
|
||||
try {
|
||||
initialCode = bstream.readBytes(bstream.available());
|
||||
} catch {}
|
||||
|
||||
stream.close();
|
||||
|
||||
try {
|
||||
const converter = Cc['@mozilla.org/intl/scriptableunicodeconverter'].createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
converter.charset = 'utf-8';
|
||||
initialCode = converter.ConvertToUnicode(initialCode);
|
||||
} catch {}
|
||||
|
||||
initEditor();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
if (url)
|
||||
initialCode = '@-moz-document ' + type + '("' + url + '") {\n\n\n}';
|
||||
initEditor();
|
||||
}
|
||||
|
||||
nameE = document.getElementById('name');
|
||||
nameE.value = style?.name || '';
|
||||
updateTitle();
|
||||
nameE.addEventListener('input', function () {
|
||||
unsaved = true;
|
||||
toggleUI('save-button', true);
|
||||
});
|
||||
|
||||
document.getElementById('origin').value = origin;
|
||||
|
||||
document.getElementById('errors').addEventListener('click', function (e) {
|
||||
if (e.target == this)
|
||||
this.style.display = 'none';
|
||||
});
|
||||
|
||||
isInstantPreview = xPref.get(UC.styloaix.PREF_INSTANTPREVIEW);
|
||||
document.getElementById('instant-preview').checked = isInstantPreview;
|
||||
isInstantCheck = xPref.get(UC.styloaix.PREF_INSTANTCHECK);
|
||||
document.getElementById('instant-check').checked = isInstantCheck;
|
||||
if (style?.enabled === false)
|
||||
toggleUI('preview-button', true);
|
||||
interval = xPref.get(UC.styloaix.PREF_INSTANTINTERVAL);
|
||||
xPref.addListener(UC.styloaix.PREF_INSTANTINTERVAL, function (ms) {
|
||||
interval = ms;
|
||||
});
|
||||
}
|
||||
|
||||
function initEditor () {
|
||||
const Editor = require('devtools/client/shared/sourceeditor/editor');
|
||||
|
||||
const extraKeys = {
|
||||
[Editor.accel('S')]: save,
|
||||
'F3': 'findNext',
|
||||
'Shift-F3': 'findPrev'
|
||||
};
|
||||
|
||||
const lineWrapping = xPref.get(UC.styloaix.PREF_LINEWRAPPING);
|
||||
document.getElementById('wrap-lines').checked = lineWrapping;
|
||||
|
||||
sourceEditor = new Editor({
|
||||
mode: Editor.modes.css,
|
||||
contextMenu: 'sourceEditorContextMenu',
|
||||
extraKeys: extraKeys,
|
||||
lineNumbers: true,
|
||||
lineWrapping: lineWrapping,
|
||||
value: initialCode,
|
||||
maxHighlightLength: 10000
|
||||
});
|
||||
|
||||
sourceEditor.setupAutoCompletion = function () {
|
||||
this.extend(require_mini('userchromejs/content/styloaix/autocomplete'));
|
||||
this.initializeAutoCompletion();
|
||||
};
|
||||
|
||||
document.getElementById('editor').selectedIndex = 1;
|
||||
|
||||
sourceEditor.appendTo(document.getElementById('sourceeditor')).then(function () {
|
||||
sourceEditor.insertCommandsController();
|
||||
sourceEditor.focus();
|
||||
if (isInstantCheck)
|
||||
checkForErrors();
|
||||
});
|
||||
|
||||
sourceEditor.on('change', function () {
|
||||
changed();
|
||||
if (!isInstantCheck)
|
||||
toggleUI('check-for-errors-button', true);
|
||||
});
|
||||
}
|
||||
|
||||
function changed () {
|
||||
if ((isInstantPreview || isInstantCheck) && !timeoutRunning)
|
||||
instantTimeout();
|
||||
|
||||
unsaved = true;
|
||||
toggleUI('save-button', true);
|
||||
if (!isInstantPreview)
|
||||
toggleUI('preview-button', true);
|
||||
}
|
||||
|
||||
function instantTimeout () {
|
||||
timeoutRunning = true;
|
||||
setTimeout(() => {
|
||||
if (isInstantPreview) {
|
||||
if (previewActive)
|
||||
_uc.sss.unregisterSheet(previewCode, previewOrigin);
|
||||
else if (style?.enabled)
|
||||
style.unregister();
|
||||
previewCode = Services.io.newURI('data:text/css;charset=UTF-8,' + encodeURIComponent(codeElementWrapper.value));
|
||||
previewOrigin = origin;
|
||||
previewActive = true;
|
||||
_uc.sss.loadAndRegisterSheet(previewCode, previewOrigin);
|
||||
toggleUI('preview-button', false);
|
||||
if (origin === _uc.sss.AGENT_SHEET || lastOrigin === _uc.sss.AGENT_SHEET) {
|
||||
lastOrigin = origin;
|
||||
UC.styloaix.forceRefresh();
|
||||
}
|
||||
}
|
||||
if (isInstantCheck)
|
||||
checkForErrors();
|
||||
timeoutRunning = false;
|
||||
}, interval)
|
||||
}
|
||||
|
||||
function save () {
|
||||
if (!nameE.value)
|
||||
return alert('Style name must not be empty.');
|
||||
|
||||
if (style)
|
||||
style.unregister();
|
||||
|
||||
const finalTitle = nameE.value + (origin == 0 ? '.as' : origin == 1 ? '.us' : '') + '.css';
|
||||
const file = UC.styloaix.CSSDIR.clone();
|
||||
file.append(finalTitle);
|
||||
if (!file.exists())
|
||||
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
|
||||
|
||||
const ostream = Cc['@mozilla.org/network/file-output-stream;1'].createInstance(Ci.nsIFileOutputStream);
|
||||
ostream.init(file, -1, -1, 0);
|
||||
|
||||
const converter = Cc['@mozilla.org/intl/scriptableunicodeconverter'].createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
converter.charset = 'UTF-8';
|
||||
|
||||
const istream = converter.convertToInputStream(codeElementWrapper.value);
|
||||
|
||||
NetUtil.asyncCopy(istream, ostream, function (aResult) {
|
||||
if (Components.isSuccessCode(aResult)) {
|
||||
const enabled = style ? style.enabled : true;
|
||||
|
||||
if (style && finalTitle != style.fullName) {
|
||||
const oldFile = UC.styloaix.CSSDIR.clone()
|
||||
oldFile.append(style.fullName);
|
||||
oldFile.remove(false);
|
||||
UC.styloaix.styles.delete(style.fullName);
|
||||
if (!enabled) {
|
||||
UC.styloaix.disabledStyles.add(finalTitle);
|
||||
UC.styloaix.disabledStyles.delete(oldFile.leafName);
|
||||
}
|
||||
}
|
||||
|
||||
style = new UC.styloaix.UserStyle(file);
|
||||
style.enabled = enabled;
|
||||
updateTitle();
|
||||
UC.styloaix.styles.set(style.fullName, style);
|
||||
if (UC.styloaix.enabled && enabled) {
|
||||
style.register();
|
||||
toggleUI('preview-button', false);
|
||||
if (previewActive) {
|
||||
_uc.sss.unregisterSheet(previewCode, previewOrigin);
|
||||
previewActive = false;
|
||||
if (origin === _uc.sss.AGENT_SHEET || lastOrigin === _uc.sss.AGENT_SHEET)
|
||||
UC.styloaix.forceRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
unsaved = false;
|
||||
|
||||
toggleUI('save-button', false);
|
||||
} else {
|
||||
alert('Error!');
|
||||
}
|
||||
})
|
||||
|
||||
sourceEditor.focus();
|
||||
}
|
||||
|
||||
function updateTitle () {
|
||||
document.title = (style?.fullName || 'New Style') + ' - StyloaiX Editor';
|
||||
}
|
||||
|
||||
function toggleUI (id, state) {
|
||||
document.getElementById(id).disabled = !state;
|
||||
}
|
||||
|
||||
function preview () {
|
||||
if (previewActive)
|
||||
_uc.sss.unregisterSheet(previewCode, previewOrigin);
|
||||
else if (style?.enabled)
|
||||
style.unregister();
|
||||
previewCode = Services.io.newURI('data:text/css;charset=UTF-8,' + encodeURIComponent(codeElementWrapper.value));
|
||||
previewOrigin = origin;
|
||||
_uc.sss.loadAndRegisterSheet(previewCode, previewOrigin);
|
||||
previewActive = true;
|
||||
if (origin === _uc.sss.AGENT_SHEET || lastOrigin === _uc.sss.AGENT_SHEET) {
|
||||
lastOrigin = origin;
|
||||
UC.styloaix.forceRefresh();
|
||||
}
|
||||
|
||||
checkForErrors();
|
||||
toggleUI('preview-button', false);
|
||||
sourceEditor.focus();
|
||||
}
|
||||
|
||||
function checkForErrors () {
|
||||
const errors = document.getElementById('errors');
|
||||
errors.style.display = 'none';
|
||||
|
||||
while (errors.hasChildNodes())
|
||||
errors.lastChild.remove();
|
||||
|
||||
let count = 0;
|
||||
|
||||
const errorListener = {
|
||||
observe: (message) => {
|
||||
if (!count)
|
||||
errors.style.display = 'block';
|
||||
|
||||
const error = message.QueryInterface(Ci.nsIScriptError);
|
||||
const errMsg = error.lineNumber + ':' + error.columnNumber + ' - ' + error.errorMessage;
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.appendChild(document.createTextNode(errMsg));
|
||||
label.addEventListener('click', function () {
|
||||
goToLine(error.lineNumber, error.columnNumber);
|
||||
});
|
||||
errors.appendChild(label);
|
||||
errors.appendChild(document.createElement('br'));
|
||||
count++;
|
||||
|
||||
if (count == 10) {
|
||||
errors.appendChild(document.createTextNode('...'));
|
||||
Services.console.unregisterListener(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Services.console.registerListener(errorListener);
|
||||
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.appendChild(document.createTextNode(codeElementWrapper.value));
|
||||
document.documentElement.appendChild(styleEl);
|
||||
styleEl.remove();
|
||||
|
||||
setTimeout(() => {
|
||||
if (count < 10)
|
||||
Services.console.unregisterListener(errorListener);
|
||||
});
|
||||
|
||||
toggleUI('check-for-errors-button', false);
|
||||
sourceEditor.focus();
|
||||
}
|
||||
|
||||
function goToLine (line, col) {
|
||||
sourceEditor.focus();
|
||||
sourceEditor.setCursor({line: line - 1, ch: col});
|
||||
}
|
||||
|
||||
function insertCodeAtStart (snippet) {
|
||||
let position = codeElementWrapper.value.indexOf(snippet);
|
||||
if (position == -1) {
|
||||
codeElementWrapper.value = snippet + '\n' + codeElementWrapper.value;
|
||||
position = 0;
|
||||
}
|
||||
const positionEnd = position + snippet.length;
|
||||
|
||||
codeElementWrapper.setSelectionRange(positionEnd, positionEnd);
|
||||
sourceEditor.focus();
|
||||
}
|
||||
|
||||
function insertCodeAtCaret (snippet) {
|
||||
const selectionStart = codeElementWrapper.selectionStart;
|
||||
const selectionEnd = selectionStart + snippet.length;
|
||||
codeElementWrapper.value = codeElementWrapper.value.substring(0, codeElementWrapper.selectionStart) + snippet + codeElementWrapper.value.substring(codeElementWrapper.selectionEnd, codeElementWrapper.value.length);
|
||||
codeElementWrapper.setSelectionRange(selectionEnd, selectionEnd);
|
||||
sourceEditor.focus();
|
||||
}
|
||||
|
||||
function changeWordWrap (bool, persist) {
|
||||
if (persist)
|
||||
xPref.set(UC.styloaix.PREF_LINEWRAPPING, bool);
|
||||
sourceEditor.setOption('lineWrapping', bool);
|
||||
sourceEditor.focus();
|
||||
}
|
||||
|
||||
function instantPreview (bool, persist) {
|
||||
if (persist)
|
||||
xPref.set(UC.styloaix.PREF_INSTANTPREVIEW, bool);
|
||||
isInstantPreview = bool
|
||||
if (isInstantPreview && !timeoutRunning)
|
||||
instantTimeout();
|
||||
|
||||
sourceEditor.focus();
|
||||
}
|
||||
|
||||
function instantCheck (bool, persist) {
|
||||
if (persist)
|
||||
xPref.set(UC.styloaix.PREF_INSTANTCHECK, bool);
|
||||
isInstantCheck = bool
|
||||
if (isInstantCheck && !timeoutRunning)
|
||||
instantTimeout();
|
||||
|
||||
sourceEditor.focus();
|
||||
}
|
||||
|
||||
function insertDataURI() {
|
||||
const fp = Cc['@mozilla.org/filepicker;1'].createInstance(Ci.nsIFilePicker);
|
||||
fp.init(window, 'Choose File…', Ci.nsIFilePicker.modeOpen);
|
||||
fp.open(res => {
|
||||
if (res != Ci.nsIFilePicker.returnOK)
|
||||
return;
|
||||
|
||||
const contentType = Cc['@mozilla.org/mime;1'].getService(Ci.nsIMIMEService).getTypeFromFile(fp.file);
|
||||
const inputStream = Cc['@mozilla.org/network/file-input-stream;1'].createInstance(Ci.nsIFileInputStream);
|
||||
inputStream.init(fp.file, parseInt('01', 16), parseInt('0600', 8), 0);
|
||||
const stream = Cc['@mozilla.org/binaryinputstream;1'].createInstance(Ci.nsIBinaryInputStream);
|
||||
stream.setInputStream(inputStream);
|
||||
const encoded = btoa(stream.readBytes(stream.available()));
|
||||
stream.close();
|
||||
inputStream.close();
|
||||
insertCodeAtCaret('data:' + contentType + ';base64,' + encoded);
|
||||
});
|
||||
}
|
||||
|
||||
const codeElementWrapper = {
|
||||
get value() {
|
||||
return sourceEditor.getText();
|
||||
},
|
||||
|
||||
set value(v) {
|
||||
sourceEditor.setText(v);
|
||||
},
|
||||
|
||||
setSelectionRange: function (start, end) {
|
||||
sourceEditor.setSelection(sourceEditor.getPosition(start), sourceEditor.getPosition(end));
|
||||
},
|
||||
|
||||
get selectionStart() {
|
||||
return sourceEditor.getOffset(sourceEditor.getCursor('start'));
|
||||
},
|
||||
|
||||
get selectionEnd() {
|
||||
return sourceEditor.getOffset(sourceEditor.getCursor('end'));
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
const closeFn = window.close;
|
||||
let shouldHandle = true;
|
||||
|
||||
if (isChromeWindow) {
|
||||
window.close = function () {
|
||||
if (!unsaved || confirm('Do you want to close and lose unsaved changes?')) {
|
||||
shouldHandle = false;
|
||||
setTimeout(closeFn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('close', function (e) {
|
||||
e.preventDefault();
|
||||
window.close();
|
||||
})
|
||||
|
||||
window.addEventListener('beforeunload', function (e) {
|
||||
if (shouldHandle && unsaved)
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
window.addEventListener('unload', function (event) {
|
||||
if (previewActive) {
|
||||
_uc.sss.unregisterSheet(previewCode, previewOrigin);
|
||||
if (origin === _uc.sss.AGENT_SHEET || lastOrigin === _uc.sss.AGENT_SHEET)
|
||||
UC.styloaix.forceRefresh();
|
||||
}
|
||||
|
||||
if (style?.enabled && previewActive)
|
||||
style.register();
|
||||
});
|
||||
|
||||
window.addEventListener('DOMContentLoaded', init, {once: true});
|
140
chrome/utils/styloaix/edit.xhtml
Normal file
140
chrome/utils/styloaix/edit.xhtml
Normal file
|
@ -0,0 +1,140 @@
|
|||
<?xml version="1.0"?>
|
||||
|
||||
<!DOCTYPE window>
|
||||
|
||||
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://userchromejs/content/styloaix/edit.css" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://devtools/content/shared/toolbarbutton.css" type="text/css"?>
|
||||
|
||||
<window
|
||||
class="theme-body"
|
||||
id="styloaix-edit"
|
||||
title="StyloaiX Editor"
|
||||
persist="width height screenX screenY sizemode"
|
||||
height="350"
|
||||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<html:link rel="icon" href="chrome://userchromejs/content/styloaix/16.png"/>
|
||||
<html:link rel="localization" href="toolkit/global/textActions.ftl"/>
|
||||
<html:link rel="localization" href="devtools/client/styleeditor.ftl"/>
|
||||
|
||||
<script src="chrome://userchromejs/content/styloaix/edit.js"/>
|
||||
<script src="chrome://devtools/content/shared/theme-switching.js"/>
|
||||
<script src="chrome://global/content/globalOverlay.js"/>
|
||||
<script src="chrome://global/content/editMenuOverlay.js"/>
|
||||
<script>
|
||||
"use strict";
|
||||
/* import-globals-from ../../../toolkit/content/globalOverlay.js */
|
||||
/* import-globals-from ../../../toolkit/content/editMenuOverlay.js */
|
||||
/* exported goUpdateSourceEditorMenuItems */
|
||||
function goUpdateSourceEditorMenuItems() {
|
||||
goUpdateGlobalEditMenuItems();
|
||||
|
||||
['cmd_undo', 'cmd_redo', 'cmd_cut', 'cmd_paste',
|
||||
'cmd_delete', 'cmd_find', 'cmd_findAgain'].forEach(goUpdateCommand);
|
||||
}
|
||||
</script>
|
||||
|
||||
<popupset id="style-editor-popups">
|
||||
<menupopup id="sourceEditorContextMenu"
|
||||
incontentshell="false"
|
||||
onpopupshowing="goUpdateSourceEditorMenuItems()">
|
||||
<menuitem id="cMenu_undo"
|
||||
data-l10n-id="text-action-undo" command="cmd_undo"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="cMenu_cut"
|
||||
data-l10n-id="text-action-cut" command="cmd_cut"/>
|
||||
<menuitem id="cMenu_copy"
|
||||
data-l10n-id="text-action-copy" command="cmd_copy"/>
|
||||
<menuitem id="cMenu_paste"
|
||||
data-l10n-id="text-action-paste" command="cmd_paste"/>
|
||||
<menuitem id="cMenu_delete"
|
||||
data-l10n-id="text-action-delete" command="cmd_delete"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="cMenu_selectAll"
|
||||
data-l10n-id="text-action-select-all" command="cmd_selectAll"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="se-menu-find" data-l10n-id="styleeditor-find"
|
||||
command="cmd_find"/>
|
||||
<menuitem id="cMenu_findAgain" data-l10n-id="styleeditor-find-again"
|
||||
command="cmd_findAgain"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="se-menu-gotoLine"
|
||||
data-l10n-id="styleeditor-go-to-line"
|
||||
command="cmd_gotoLine"/>
|
||||
</menupopup>
|
||||
<menupopup id="sidebar-context" incontentshell="false">
|
||||
<menuitem id="context-openlinknewtab"
|
||||
data-l10n-id="styleeditor-open-link-new-tab"/>
|
||||
<menuitem id="context-copyurl"
|
||||
data-l10n-id="styleeditor-copy-url"/>
|
||||
</menupopup>
|
||||
</popupset>
|
||||
|
||||
<commandset id="sourceEditorCommands">
|
||||
<command id="cmd_gotoLine" oncommand="goDoCommand('cmd_gotoLine')"/>
|
||||
<command id="cmd_find" oncommand="goDoCommand('cmd_find')"/>
|
||||
<command id="cmd_findAgain" oncommand="goDoCommand('cmd_findAgain')"/>
|
||||
</commandset>
|
||||
|
||||
<keyset id="sourceEditorKeys"/>
|
||||
|
||||
<keyset>
|
||||
<key id="find-key" modifiers="control" key="F" oncommand="sourceEditor.codeMirror.execCommand('find')"/>
|
||||
<key id="save-key" modifiers="control" key="S" oncommand="save()"/>
|
||||
<key id="close-key" modifiers="control" key="W" oncommand="window.close()"/>
|
||||
</keyset>
|
||||
|
||||
<vbox id="main-area" flex="1" class="theme-toolbar">
|
||||
<hbox id="editor-tools">
|
||||
<button class="devtools-toolbarbutton" id="save-button" label="Save" accesskey="S" oncommand="save()" disabled="true"/>
|
||||
<button class="devtools-toolbarbutton" id="preview-button" label="Preview" accesskey="P" oncommand="preview()" disabled="true"/>
|
||||
<button class="devtools-toolbarbutton" id="check-for-errors-button" label="Check for Errors" accesskey="C" oncommand="checkForErrors()"/>
|
||||
|
||||
<vbox class="devtools-separator"></vbox>
|
||||
|
||||
<button class="devtools-toolbarbutton" label="Insert" accesskey="I" type="menu">
|
||||
<menupopup>
|
||||
<menuitem label="Firefox chrome URL" accesskey="F" oncommand="insertCodeAtStart('@-moz-document url(\u0022' + _uc.BROWSERCHROME + '\u0022) {\n\n\n}')"/>
|
||||
<menuitem label="Current Tab URL" accesskey="T" oncommand="insertCodeAtStart('@-moz-document url(\u0022' + Services.wm.getMostRecentBrowserWindow().gBrowser.currentURI.specIgnoringRef + '\u0022) {\n\n\n}')"/>
|
||||
<menuitem label="Current Tab domain" accesskey="D" oncommand="let uri = Services.wm.getMostRecentBrowserWindow().gBrowser.currentURI; let host = uri.asciiHost; insertCodeAtStart('@-moz-document ' + (host ? 'domain' : 'url') + '(\u0022' + (host || uri.currentURI.specIgnoringRef) + '\u0022) {\n\n\n}')"/>
|
||||
<menuitem label="HTML namespace as default" accesskey="H" oncommand="insertCodeAtStart('@namespace url(\u0022http://www.w3.org/1999/xhtml\u0022);')"/>
|
||||
<menuitem label="XUL namespace as default" accesskey="X" oncommand="insertCodeAtStart('@namespace url(\u0022http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul\u0022);');"/>
|
||||
<menuitem label="Chrome folder path" accesskey="C" oncommand="insertCodeAtCaret('resource://userchromejs/')"/>
|
||||
<menuitem label="Data URI…" accesskey="D" oncommand="insertDataURI()"/>
|
||||
</menupopup>
|
||||
</button>
|
||||
<checkbox id="wrap-lines" label="Wrap lines" accesskey="W" oncommand="changeWordWrap(this.checked, event.shiftKey)" tooltiptext="Press holding Shift to save setting" />
|
||||
<checkbox id="instant-preview" label="Instant Preview" accesskey="R" oncommand="instantPreview(this.checked, event.shiftKey)" tooltiptext="Press holding Shift to save setting" />
|
||||
<checkbox id="instant-check" label="Instant Check for Errors" accesskey="T" oncommand="instantCheck(this.checked, event.shiftKey)" tooltiptext="Press holding Shift to save setting" />
|
||||
<spacer flex="1"/>
|
||||
<label>Origin:</label>
|
||||
<menulist id="origin" oncommand="val = Number(this.value); if (origin != val) { lastOrigin = origin; origin = val; changed();}">
|
||||
<menupopup>
|
||||
<menuitem label="AGENT_SHEET" value="0"/>
|
||||
<menuitem label="USER_SHEET" value="1"/>
|
||||
<menuitem label="AUTHOR_SHEET" value="2" selected="true"/>
|
||||
</menupopup>
|
||||
</menulist>
|
||||
</hbox>
|
||||
<separator orient="horizontal" class="devtools-horizontal-splitter"/>
|
||||
<hbox align="center" style="margin-bottom: 4px;">
|
||||
<hbox style="margin-bottom: 2px;">
|
||||
<label control="name" accesskey="N">Name:</label>
|
||||
</hbox>
|
||||
<hbox flex="1">
|
||||
<html:input class="devtools-textinput" id="name"/>
|
||||
</hbox>
|
||||
</hbox>
|
||||
<separator orient="horizontal" class="devtools-horizontal-splitter"/>
|
||||
<deck id="editor" flex="1">
|
||||
<hbox flex="1" style="display: flex; margin: 1px 4px;">
|
||||
<html:textarea id="internal-code" style="width: 100%;box-sizing: border-box; resize: none;"/>
|
||||
</hbox>
|
||||
<hbox id="sourceeditor" flex="1"/>
|
||||
</deck>
|
||||
<vbox class="theme-toolbar" id="errors" style="display:none;"/>
|
||||
</vbox>
|
||||
|
||||
</window>
|
222
chrome/utils/userChrome.jsm
Normal file
222
chrome/utils/userChrome.jsm
Normal file
|
@ -0,0 +1,222 @@
|
|||
let EXPORTED_SYMBOLS = [];
|
||||
|
||||
const Services = globalThis.Services || ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
|
||||
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;
|
||||
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:ExtensionViewLoaded', 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:ExtensionViewLoaded', 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();
|
||||
Services.obs.addObserver(UserChrome_js, 'chrome-document-global-created', false);
|
||||
}
|
94
chrome/utils/xPref.jsm
Normal file
94
chrome/utils/xPref.jsm
Normal file
|
@ -0,0 +1,94 @@
|
|||
let EXPORTED_SYMBOLS = ['xPref'];
|
||||
|
||||
const Services = globalThis.Services || ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
|
||||
|
||||
var xPref = {
|
||||
// Retorna o valor da preferência, seja qual for o tipo, mas não
|
||||
// testei com tipos complexos como nsIFile, não sei como detectar
|
||||
// uma preferência assim, na verdade nunca vi uma
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
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,
|
||||
|
||||
// Detecta mudanças na preferência e retorna:
|
||||
// return[0]: valor da preferência alterada
|
||||
// return[1]: nome da preferência alterada
|
||||
// Guardar chamada numa var se quiser interrompê-la depois
|
||||
addListener: function (prefPath, trat) {
|
||||
this.observer = function (aSubject, aTopic, prefPath) {
|
||||
return trat(xPref.get(prefPath), prefPath);
|
||||
}
|
||||
|
||||
Services.prefs.addObserver(prefPath, this.observer);
|
||||
return {
|
||||
prefPath: prefPath,
|
||||
observer: this.observer
|
||||
};
|
||||
},
|
||||
|
||||
// Encerra pref observer
|
||||
// Só precisa passar a var definida quando adicionou
|
||||
removeListener: function (obs) {
|
||||
Services.prefs.removeObserver(obs.prefPath, obs.observer);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue