I used a solution proposed here.
It is implemented by creating a special protocol that is handled by your Firefox addon, which in turn requests the resources from its folder.
Note that in case the resource folder may contain something non-public then I would add additional checks to allow only these resources that really are intended to be web-accessible.
The custom-protocol code attached to the post mentioned above is available here:
/*
Makes any file within the data directory available to use in an iframe.
Replace this: require("sdk/self").data.url(...)
With this: require("name-of-this-file").url(...)
*/
var { Class } = require('sdk/core/heritage');
var { Unknown, Factory } = require('sdk/platform/xpcom');
var { Cc, Ci, Cr } = require('chrome');
var self = require("sdk/self");
var resourceProtocolHandler = Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService)
.getProtocolHandler('resource');
var scheme = "res-" + self.id.toLowerCase().replace(/[^a-z0-9+\-\.]/g, "-");
var AddonProtocolHandler = Class({
extends: Unknown,
interfaces: ['nsIProtocolHandler'],
scheme: scheme,
defaultPort: -1,
protocolFlags: Ci.nsIProtocolHandler.URI_STD
| Ci.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE
| Ci.nsIProtocolHandler.URI_SAFE_TO_LOAD_IN_SECURE_CONTEXT,
newURI: function(spec, originCharset, baseURI) {
let uri = Cc["@mozilla.org/network/standard-url;1"].createInstance(Ci.nsIStandardURL);
uri.init(uri.URLTYPE_STANDARD, this.defaultPort, spec, originCharset, baseURI);
return uri.QueryInterface(Ci.nsIURI);
},
newChannel: function(uri) {
if (uri.spec.indexOf(exports.url("")) != 0) {
throw Cr.NS_ERROR_ILLEGAL_VALUE;
}
var resourceUri = resourceProtocolHandler.newURI(uri.spec.replace(scheme + "://", "resource://"), uri.originCharset, null);
var channel = resourceProtocolHandler.newChannel(resourceUri);
channel.originalURI = uri;
return channel;
},
allowPort: (port, scheme) => false
});
Factory({
contract: "@mozilla.org/network/protocol;1?name=" + scheme,
Component: AddonProtocolHandler
});
exports.url = function(url) {
return self.data.url(url).replace("resource://", scheme + "://");
};
Additional note: for page scripts (not content scripts) this custom protocol helps in loading an iframe and other HTML elements, but not in loading a XMLHttpRequest or Worker. For the latter, still cross-origin restrictions apply and a security error is raised: "Access to restricted URI denied" in case of XMLHttpRequest, and "The operation is insecure." for Worker.
In contrast, under Chrome the XMLHttpRequest to web_accessible_resources is permitted. Worker does not implement access to web_accessible_resources under Chrome either.
BUT for Workers you can use this custom-protocol url for importScripts method. Using that you can also work around the problem of loading custom resources into Workers. This code works also in Chrome, as an alternative to using XMLHttpRequest.
var code = "self.onmessage = function (message)\
{\
self.onmessage = null;\
self.importScripts(message.data);\
};";
var blob = new Blob([code], {type: 'application/javascript'});
var blobUrl = URL.createObjectURL(blob);
var w = new Worker(blobUrl);
w.postMessage(*webAccessibleResourceUrl*);
URL.revokeObjectURL(blobUrl);