508 lines
13 KiB
JavaScript
508 lines
13 KiB
JavaScript
/*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright © 2022 Corporation for Digital Scholarship
|
|
Vienna, Virginia, USA
|
|
https://www.zotero.org
|
|
|
|
This file is part of Zotero.
|
|
|
|
Zotero is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Zotero is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
|
|
Zotero.Plugins = new function () {
|
|
var { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
|
|
var scopes = new Map();
|
|
var observers = new Set();
|
|
var addonVersions = new Map();
|
|
|
|
const REASONS = {
|
|
APP_STARTUP: 1,
|
|
APP_SHUTDOWN: 2,
|
|
ADDON_ENABLE: 3,
|
|
ADDON_DISABLE: 4,
|
|
ADDON_INSTALL: 5,
|
|
ADDON_UNINSTALL: 6,
|
|
ADDON_UPGRADE: 7,
|
|
ADDON_DOWNGRADE: 8,
|
|
MAIN_WINDOW_LOAD: 9,
|
|
MAIN_WINDOW_UNLOAD: 10,
|
|
};
|
|
|
|
|
|
this.init = async function () {
|
|
this._addonObserver.init();
|
|
|
|
// In Fx102, getActiveAddons(["extension"]) doesn't always return fully loaded addon objects
|
|
// if getAllAddons() hasn't been called, so use getAllAddons() and do the checks ourselves
|
|
var addons = await AddonManager.getAllAddons();
|
|
for (let addon of addons) {
|
|
if (addon.type != 'extension' || !addon.isActive) continue;
|
|
addonVersions.set(addon.id, addon.version);
|
|
_loadScope(addon);
|
|
setDefaultPrefs(addon);
|
|
registerLocales(addon);
|
|
await _callMethod(addon, 'startup', REASONS.APP_STARTUP);
|
|
}
|
|
|
|
Zotero.addShutdownListener(async () => {
|
|
var { addons } = await AddonManager.getActiveAddons(["extension"]);
|
|
for (let addon of addons) {
|
|
await _callMethod(addon, 'shutdown', REASONS.APP_SHUTDOWN);
|
|
}
|
|
});
|
|
|
|
const mainWindowListener = {
|
|
onOpenWindow: function (xulWindow) {
|
|
let domWindow = xulWindow.docShell.domWindow;
|
|
async function onload() {
|
|
domWindow.removeEventListener("load", onload, false);
|
|
if (
|
|
domWindow.location.href
|
|
!== "chrome://zotero/content/zoteroPane.xhtml"
|
|
) {
|
|
return;
|
|
}
|
|
let { addons } = await AddonManager.getActiveAddons(["extension"]);
|
|
for (let addon of addons) {
|
|
await _callMethod(addon, 'onMainWindowLoad', REASONS.MAIN_WINDOW_LOAD, { window: domWindow });
|
|
}
|
|
}
|
|
domWindow.addEventListener("load", onload, false);
|
|
},
|
|
onCloseWindow: async function (xulWindow) {
|
|
let domWindow = xulWindow.docShell.domWindow;
|
|
if (
|
|
domWindow.location.href !== "chrome://zotero/content/zoteroPane.xhtml"
|
|
) {
|
|
return;
|
|
}
|
|
let { addons } = await AddonManager.getActiveAddons(["extension"]);
|
|
for (let addon of addons) {
|
|
await _callMethod(addon, 'onMainWindowUnload', REASONS.MAIN_WINDOW_LOAD, { window: domWindow });
|
|
}
|
|
},
|
|
};
|
|
Services.wm.addListener(mainWindowListener);
|
|
};
|
|
|
|
|
|
/**
|
|
* Adapted from loadBootstrapScope() in Firefox 60 ESR
|
|
*
|
|
* https://searchfox.org/mozilla-esr60/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm#4233
|
|
*/
|
|
function _loadScope(addon) {
|
|
var scope = new Cu.Sandbox(
|
|
Services.scriptSecurityManager.getSystemPrincipal(),
|
|
{
|
|
sandboxName: addon.id,
|
|
wantGlobalProperties: [
|
|
"atob",
|
|
"btoa",
|
|
"Blob",
|
|
"crypto",
|
|
"CSS",
|
|
"ChromeUtils",
|
|
"DOMParser",
|
|
"fetch",
|
|
"File",
|
|
"FileReader",
|
|
"TextDecoder",
|
|
"TextEncoder",
|
|
"URL",
|
|
"URLSearchParams",
|
|
"XMLHttpRequest"
|
|
]
|
|
}
|
|
);
|
|
for (let name in REASONS) {
|
|
scope[name] = REASONS[name];
|
|
}
|
|
Object.assign(
|
|
scope,
|
|
{
|
|
Zotero,
|
|
ChromeWorker,
|
|
IOUtils,
|
|
Localization,
|
|
PathUtils,
|
|
Services,
|
|
Worker,
|
|
XMLSerializer,
|
|
|
|
// Add additional global functions
|
|
setTimeout,
|
|
clearTimeout,
|
|
setInterval,
|
|
clearInterval,
|
|
requestIdleCallback,
|
|
cancelIdleCallback,
|
|
}
|
|
);
|
|
|
|
scopes.set(addon.id, scope);
|
|
|
|
try {
|
|
let uri = addon.getResourceURI().spec + 'bootstrap.js';
|
|
Services.scriptloader.loadSubScriptWithOptions(
|
|
uri,
|
|
{
|
|
target: scope,
|
|
ignoreCache: true
|
|
}
|
|
);
|
|
}
|
|
catch (e) {
|
|
Zotero.logError(e);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Adapted from callBootstrapMethod() in Firefox 60 ESR
|
|
*
|
|
* https://searchfox.org/mozilla-esr60/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm#4343
|
|
*/
|
|
async function _callMethod(addon, method, reason, extraParams) {
|
|
try {
|
|
let id = addon.id;
|
|
Zotero.debug(`Calling bootstrap method '${method}' for plugin ${id} `
|
|
+ `version ${addon.version} with reason ${_getReasonName(reason)}`);
|
|
|
|
let scope = scopes.get(id);
|
|
|
|
let func;
|
|
try {
|
|
func = scope[method] || Cu.evalInSandbox(`${method};`, scope);
|
|
}
|
|
catch (e) {}
|
|
|
|
if (!func) {
|
|
Zotero.warn(`Plugin ${id} is missing bootstrap method '${method}'`);
|
|
return;
|
|
}
|
|
|
|
let params = {
|
|
id: addon.id,
|
|
version: addon.version,
|
|
rootURI: addon.getResourceURI().spec
|
|
};
|
|
if (extraParams) {
|
|
Object.assign(params, extraParams);
|
|
}
|
|
let result;
|
|
try {
|
|
result = func.call(scope, params, reason);
|
|
// If bootstrap method returns a promise, wait for it
|
|
if (result && result.then) {
|
|
await result;
|
|
}
|
|
}
|
|
catch (e) {
|
|
Zotero.logError(`Error running bootstrap method '${method}' on ${id}`);
|
|
Zotero.logError(e);
|
|
}
|
|
|
|
for (let observer of observers) {
|
|
if (observer[method]) {
|
|
try {
|
|
let maybePromise = observer[method](params, reason);
|
|
if (maybePromise && maybePromise.then) {
|
|
await maybePromise;
|
|
}
|
|
}
|
|
catch (e) {
|
|
Zotero.logError(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Needed?
|
|
/*if (method == "startup") {
|
|
activeAddon.startupPromise = Promise.resolve(result);
|
|
activeAddon.startupPromise.catch(Cu.reportError);
|
|
}*/
|
|
}
|
|
catch (e) {
|
|
Zotero.logError(e);
|
|
}
|
|
}
|
|
|
|
|
|
function _getReasonName(reason) {
|
|
for (let i in REASONS) {
|
|
if (reason == REASONS[i]) {
|
|
return i;
|
|
}
|
|
}
|
|
return "UNKNOWN";
|
|
}
|
|
|
|
|
|
function _unloadScope(id) {
|
|
scopes.delete(id);
|
|
}
|
|
|
|
|
|
this.getRootURI = async function (id) {
|
|
var addon = await AddonManager.getAddonByID(id);
|
|
return addon.getResourceURI().spec;
|
|
};
|
|
|
|
|
|
/**
|
|
* Resolve a URI in the context of a plugin. If the passed URI is relative, it will be resolved relative to the
|
|
* plugin root URI. If it's absolute, it will be returned unchanged.
|
|
*
|
|
* @param {String} id Plugin ID
|
|
* @param {String | URL} uri
|
|
* @throws {TypeError} On an invalid URI
|
|
* @return {Promise<String>}
|
|
*/
|
|
this.resolveURI = async function (id, uri) {
|
|
// We can't use addon.getResourceURI(path) here because that only accepts a relative path
|
|
return new URL(uri, await this.getRootURI(id)).href;
|
|
};
|
|
|
|
|
|
this.getName = async function (id) {
|
|
var addon = await AddonManager.getAddonByID(id);
|
|
return addon.name;
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {String} id
|
|
* @param {Number} idealSize In logical pixels (scaled automatically on hiDPI displays)
|
|
* @returns {Promise<String | null>}
|
|
*/
|
|
this.getIconURI = async function (id, idealSize) {
|
|
var addon = await AddonManager.getAddonByID(id);
|
|
return AddonManager.getPreferredIconURL(addon, idealSize, Services.appShell.hiddenDOMWindow);
|
|
};
|
|
|
|
|
|
function setDefaultPrefs(addon) {
|
|
var branch = Services.prefs.getDefaultBranch("");
|
|
var obj = {
|
|
pref(pref, value) {
|
|
switch (typeof value) {
|
|
case 'boolean':
|
|
branch.setBoolPref(pref, value);
|
|
break;
|
|
case 'string':
|
|
branch.setStringPref(pref, value);
|
|
break;
|
|
case 'number':
|
|
branch.setIntPref(pref, value);
|
|
break;
|
|
default:
|
|
Zotero.logError(`Invalid type '${typeof value}' for pref '${pref}'`);
|
|
}
|
|
}
|
|
};
|
|
try {
|
|
Services.scriptloader.loadSubScript(
|
|
addon.getResourceURI("prefs.js").spec,
|
|
obj
|
|
);
|
|
}
|
|
catch (e) {
|
|
if (!e.toString().startsWith('Error opening input stream')) {
|
|
Zotero.logError(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function clearDefaultPrefs(addon) {
|
|
var branch = Services.prefs.getDefaultBranch("");
|
|
var obj = {
|
|
pref(pref, _value) {
|
|
if (!branch.prefHasUserValue(pref)) {
|
|
branch.deleteBranch(pref);
|
|
}
|
|
}
|
|
};
|
|
try {
|
|
Services.scriptloader.loadSubScript(
|
|
addon.getResourceURI("prefs.js").spec,
|
|
obj
|
|
);
|
|
}
|
|
catch (e) {
|
|
if (!e.toString().startsWith('Error opening input stream')) {
|
|
Zotero.logError(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Automatically register l10n sources for enabled plugins.
|
|
//
|
|
// A Fluent file located at
|
|
// [plugin root]/locale/en-US/make-it-red.ftl
|
|
// could be included in an XHTML file as
|
|
// <link rel="localization" href="make-it-red.ftl"/>
|
|
//
|
|
// If a plugin doesn't have a subdirectory for the active locale, en-US strings
|
|
// will be used as a fallback.
|
|
function registerLocales(addon) {
|
|
let source = new L10nFileSource(
|
|
addon.id,
|
|
'app',
|
|
Services.locale.availableLocales,
|
|
addon.getResourceURI().spec + 'locale/{locale}/',
|
|
);
|
|
L10nRegistry.getInstance().registerSources([source]);
|
|
}
|
|
|
|
|
|
function unregisterLocales(addon) {
|
|
L10nRegistry.getInstance().removeSources([addon.id]);
|
|
}
|
|
|
|
|
|
function getVersionChangeReason(oldVersion, newVersion) {
|
|
return Zotero.Utilities.semverCompare(oldVersion, newVersion) <= 0
|
|
? REASONS.ADDON_UPGRADE
|
|
: REASONS.ADDON_DOWNGRADE;
|
|
}
|
|
|
|
|
|
/**
|
|
* Add an observer to be notified of lifecycle events on all plugins.
|
|
*
|
|
* @param observer
|
|
* @param {Function} [observer.install]
|
|
* @param {Function} [observer.startup]
|
|
* @param {Function} [observer.shutdown]
|
|
* @param {Function} [observer.uninstall]
|
|
*/
|
|
this.addObserver = function (observer) {
|
|
observers.add(observer);
|
|
};
|
|
|
|
|
|
this.removeObserver = function (observer) {
|
|
observers.delete(observer);
|
|
};
|
|
|
|
|
|
this._addonObserver = {
|
|
initialized: false,
|
|
|
|
uninstalling: new Set(),
|
|
|
|
init() {
|
|
if (!this.initialized) {
|
|
AddonManager.addAddonListener(this);
|
|
this.initialized = true;
|
|
}
|
|
},
|
|
|
|
async onInstalling(addon) {
|
|
Zotero.debug("Installing plugin " + addon.id);
|
|
|
|
var currentVersion = addonVersions.get(addon.id);
|
|
if (currentVersion) {
|
|
let existingAddon = await AddonManager.getAddonByID(addon.id);
|
|
let reason = getVersionChangeReason(currentVersion, addon.version);
|
|
if (existingAddon.isActive) {
|
|
await _callMethod(existingAddon, 'shutdown', reason);
|
|
}
|
|
await _callMethod(existingAddon, 'uninstall', reason);
|
|
Services.obs.notifyObservers(null, "startupcache-invalidate");
|
|
}
|
|
},
|
|
|
|
async onInstalled(addon) {
|
|
if (addon.type !== "extension") {
|
|
return;
|
|
}
|
|
Zotero.debug("Installed plugin " + addon.id);
|
|
|
|
// Determine if this is a new install, an upgrade, or a downgrade
|
|
let previousVersion = addonVersions.get(addon.id);
|
|
let reason = previousVersion
|
|
? getVersionChangeReason(previousVersion, addon.version)
|
|
: REASONS.ADDON_INSTALL;
|
|
addonVersions.set(addon.id, addon.version);
|
|
|
|
_loadScope(addon);
|
|
setDefaultPrefs(addon);
|
|
registerLocales(addon);
|
|
await _callMethod(addon, 'install', reason);
|
|
if (addon.isActive) {
|
|
await _callMethod(addon, 'startup', reason);
|
|
}
|
|
},
|
|
|
|
async onEnabling(addon) {
|
|
if (addon.type !== "extension") {
|
|
return;
|
|
}
|
|
Zotero.debug("Enabling plugin " + addon.id);
|
|
setDefaultPrefs(addon);
|
|
registerLocales(addon);
|
|
await _callMethod(addon, 'startup', REASONS.ADDON_ENABLE);
|
|
},
|
|
|
|
async onDisabled(addon) {
|
|
if (addon.type !== "extension") {
|
|
return;
|
|
}
|
|
Zotero.debug("Disabling plugin " + addon.id);
|
|
await _callMethod(addon, 'shutdown', REASONS.ADDON_DISABLE);
|
|
unregisterLocales(addon);
|
|
clearDefaultPrefs(addon);
|
|
},
|
|
|
|
async onUninstalling(addon) {
|
|
Zotero.debug("Uninstalling plugin " + addon.id);
|
|
this.uninstalling.add(addon.id);
|
|
if (addon.isActive) {
|
|
await _callMethod(addon, 'shutdown', REASONS.ADDON_UNINSTALL);
|
|
}
|
|
await _callMethod(addon, 'uninstall', REASONS.ADDON_UNINSTALL);
|
|
Services.obs.notifyObservers(null, "startupcache-invalidate");
|
|
unregisterLocales(addon);
|
|
clearDefaultPrefs(addon);
|
|
},
|
|
|
|
async onUninstalled(addon) {
|
|
Zotero.debug("Uninstalled plugin " + addon.id);
|
|
_unloadScope(addon.id);
|
|
addonVersions.delete(addon.id);
|
|
},
|
|
|
|
async onOperationCancelled(addon) {
|
|
if (!this.uninstalling.has(addon.id) || addon.type !== "extension") {
|
|
return;
|
|
}
|
|
Zotero.debug("Cancelled uninstallation of plugin " + addon.id);
|
|
this.uninstalling.delete(addon.id);
|
|
await _callMethod(addon, 'install', REASONS.ADDON_INSTALL);
|
|
if (addon.isActive) {
|
|
setDefaultPrefs(addon);
|
|
registerLocales(addon);
|
|
await _callMethod(addon, 'startup', REASONS.ADDON_INSTALL);
|
|
}
|
|
}
|
|
};
|
|
};
|