Fix plugin loading on Firefox 153.3 (untrusted URI)

As of Firefox 153.3.0esr, Services.scriptloader refuses jar:file: and file:
URIs unless allowUnsafeURL is passed (Mozilla bug 1974213), so no plugin
loads: bootstrap.js fails with "Trying to load untrusted URI", then
"Plugin ... is missing bootstrap method 'startup'".

- Pass allowUnsafeURL when loading a plugin's bootstrap.js and prefs.js,
  and preference pane scripts
- Give plugins a Services.scriptloader that allows it for URIs within the
  plugin itself, since plugins commonly load their own scripts from their
  XPI with loadSubScript(). A load without a target still goes into the
  plugin scope, as a direct call would.

Add a test that installs a fixture plugin that loads a script from its XPI
and sets a default pref.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
TheNEwmanator15 2026-09-25 10:18:17 +01:00
parent cc951f9916
commit efd24c11fa
4 changed files with 88 additions and 5 deletions

View file

@ -317,7 +317,11 @@ var Zotero_Preferences = {
sameZoneAs: window,
});
for (let script of pane.scripts) {
Services.scriptloader.loadSubScript(script, pane.scope);
// Plugin panes load scripts from jar:file: URIs
Services.scriptloader.loadSubScriptWithOptions(script, {
target: pane.scope,
allowUnsafeURL: true
});
}
}
if (pane.stylesheets) {

View file

@ -197,7 +197,54 @@ Zotero.Plugins = new function () {
return Reflect.get(target, property, receiver);
}
});
// fx153.3: The script loader now refuses jar:file: URIs unless allowUnsafeURL is
// passed (Mozilla bug 1974213), and plugins load their own scripts from their
// XPI that way. Allow it for URIs within the plugin, and keep loading into the
// plugin scope when no target is given, as a direct call would.
let rootURI = addon.getResourceURI().spec;
let withinPlugin = url => typeof url == 'string' && url.startsWith(rootURI);
// The loader's methods are non-configurable, so proxy an empty object instead
let scriptloader = new Proxy({}, {
has(_, property) {
return property in Services.scriptloader;
},
get(_, property) {
let target = Services.scriptloader;
if (property === 'loadSubScript') {
return function (url, targetObj, charset) {
if (!withinPlugin(url)) {
return target.loadSubScript(url, targetObj || scope, charset);
}
let options = { target: targetObj || scope, allowUnsafeURL: true };
if (charset) {
options.charset = charset;
}
return target.loadSubScriptWithOptions(url, options);
};
}
if (property === 'loadSubScriptWithOptions') {
return function (url, options = {}) {
options = { ...options, target: options.target || scope };
if (withinPlugin(url) && !('allowUnsafeURL' in options)) {
options.allowUnsafeURL = true;
}
return target.loadSubScriptWithOptions(url, options);
};
}
let value = Reflect.get(target, property, target);
return typeof value == 'function' ? value.bind(target) : value;
}
});
scope.Services = new Proxy(Services, {
get(target, property) {
if (property === 'scriptloader') {
return scriptloader;
}
return Reflect.get(target, property, target);
}
});
scopes.set(addon.id, scope);
try {
@ -206,7 +253,10 @@ Zotero.Plugins = new function () {
uri,
{
target: scope,
ignoreCache: true
ignoreCache: true,
// Plugins are loaded from jar:file: URIs, which the script loader
// otherwise refuses (Mozilla bug 1974213)
allowUnsafeURL: true
}
);
}
@ -528,7 +578,8 @@ Zotero.Plugins = new function () {
addon.getResourceURI("prefs.js").spec,
{
target: obj,
ignoreCache: true
ignoreCache: true,
allowUnsafeURL: true
}
);
}
@ -554,7 +605,8 @@ Zotero.Plugins = new function () {
addon.getResourceURI("prefs.js").spec,
{
target: obj,
ignoreCache: true
ignoreCache: true,
allowUnsafeURL: true
}
);
}

Binary file not shown.

27
test/tests/pluginsTest.js Normal file
View file

@ -0,0 +1,27 @@
describe("Zotero.Plugins", function () {
var { AddonManager } = ChromeUtils.importESModule("resource://gre/modules/AddonManager.sys.mjs");
describe("Loading", function () {
var addon;
afterEach(async function () {
if (addon) {
await addon.uninstall();
addon = null;
}
Zotero.Prefs.clear('pluginLoadingTest.pref');
});
// The fixture's bootstrap.js loads main.js from its own XPI with
// Services.scriptloader.loadSubScript() and no target, as most plugins do
it("should load scripts and default prefs from a plugin's XPI", async function () {
let file = getTestDataDirectory();
file.append('plugin-loading-test.xpi');
addon = await AddonManager.installTemporaryAddon(file);
await waitForCallback(() => Zotero.PluginLoadingTest, 100, 10);
assert.equal(Zotero.PluginLoadingTest, 'loaded');
assert.equal(Zotero.Prefs.get('pluginLoadingTest.pref'), 'default');
});
});
});