finally finish webRequest

This commit is contained in:
wuggy 2026-09-14 15:06:27 -07:00
commit f628239ef3
9 changed files with 197 additions and 69 deletions

View file

@ -5466,7 +5466,7 @@ function handleLinkClick(event, href, linkNode) {
urlSecurityCheck(href, doc.nodePrincipal); urlSecurityCheck(href, doc.nodePrincipal);
let params = { let params = {
charset: doc.characterSet, charset: doc.characterSet,
currentBrowser: gBrowser.getBrowserForDocument(doc), currentBrowser: gBrowser.getBrowserForContentWindow(doc.defaultView.top),
frameOuterWindowID: doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) frameOuterWindowID: doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils).outerWindowID, .getInterface(Ci.nsIDOMWindowUtils).outerWindowID,
allowMixedContent: persistAllowMixedContentInChildTab, allowMixedContent: persistAllowMixedContentInChildTab,

View file

@ -38,7 +38,7 @@ enum MozContentPolicyType {
* A thin wrapper around nsIChannel and nsIHttpChannel that allows JS * A thin wrapper around nsIChannel and nsIHttpChannel that allows JS
* callers to access them without XPConnect overhead. * callers to access them without XPConnect overhead.
*/ */
[ChromeOnly, Exposed=Window] [ChromeOnly, Exposed=(Window,System)]
interface ChannelWrapper : EventTarget { interface ChannelWrapper : EventTarget {
/** /**
* Returns the wrapper instance for the given channel. The same wrapper is * Returns the wrapper instance for the given channel. The same wrapper is

View file

@ -4,8 +4,6 @@ var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
"resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebRequest", XPCOMUtils.defineLazyModuleGetter(this, "WebRequest",
"resource://gre/modules/WebRequest.jsm"); "resource://gre/modules/WebRequest.jsm");
@ -75,11 +73,16 @@ function WebRequestEventManager(context, eventName) {
} }
} }
if (data.registerTraceableChannel) {
let remoteTab = context.xulBrowser && context.xulBrowser.frameLoader
? context.xulBrowser.frameLoader.remoteTab : null;
data.registerTraceableChannel({id: context.extension.id}, remoteTab);
}
return context.runSafe(callback, data2); return context.runSafe(callback, data2);
}; };
let filter2 = {}; let filter2 = {};
filter2.urls = new MatchPattern(filter.urls); filter2.urls = filter.urls;
if (filter.types) { if (filter.types) {
filter2.types = filter.types; filter2.types = filter.types;
} }
@ -105,7 +108,9 @@ function WebRequestEventManager(context, eventName) {
} }
} }
WebRequest[eventName].addListener(listener, filter2, info2); WebRequest[eventName].addListener(listener, filter2, info2, {
policy: {id: context.extension.id, allowedOrigins: context.extension.whiteListedHosts},
});
return () => { return () => {
WebRequest[eventName].removeListener(listener); WebRequest[eventName].removeListener(listener);
}; };
@ -117,14 +122,6 @@ function WebRequestEventManager(context, eventName) {
WebRequestEventManager.prototype = Object.create(SingletonEventManager.prototype); WebRequestEventManager.prototype = Object.create(SingletonEventManager.prototype);
function makeWebRequestEvent(context, eventName) { function makeWebRequestEvent(context, eventName) {
if (!(eventName in WebRequest) || !WebRequest[eventName]) {
let name = `webRequest.${eventName}`;
return new SingletonEventManager(context, name, () => {
Cu.reportError(`webRequest.${eventName} is not supported by this runtime.`);
return () => {};
}).api();
}
return new WebRequestEventManager(context, eventName).api(); return new WebRequestEventManager(context, eventName).api();
} }
@ -156,7 +153,7 @@ extensions.registerSchemaAPI("webRequest", "addon_parent", context => {
return WebRequest.getSecurityInfo({ return WebRequest.getSecurityInfo({
id: requestId, id: requestId,
policy: context.extension.policy, policy: {id: context.extension.id},
remoteTab, remoteTab,
options, options,
}); });

View file

@ -0,0 +1,50 @@
"use strict";
add_task(function* test_created_navigation_target_dispatch() {
let scope = {};
Services.scriptloader.loadSubScript("resource://gre/modules/WebNavigation.jsm", scope);
let {Manager, WebNavigation} = scope;
let event = WebNavigation.onCreatedNavigationTarget;
let source = {}, target = {};
let received = [];
let listener = data => received.push(data);
event.addListener(listener, {matches: url => url == "https://example.com/target"});
do_register_cleanup(() => event.removeListener(listener));
for (let sourceFirst of [true, false]) {
let data = {
url: "https://example.com/target", sourceWindowId: 123,
createdWindowId: sourceFirst ? 456 : 789,
};
Manager.onCreatedNavigationTarget(sourceFirst ? source : target,
Object.assign({isSourceTab: sourceFirst}, data));
equal(received.length, 0, "wait for both browsers");
Manager.onCreatedNavigationTarget(sourceFirst ? target : source,
Object.assign({isSourceTab: !sourceFirst}, data));
equal(received.length, 1, "dispatch once, in either message order");
equal(received[0].browser, target);
equal(received[0].sourceTabBrowser, source);
equal(received[0].sourceWindowId, 123);
received.length = 0;
}
function notify(url) {
Services.obs.notifyObservers({wrappedJSObject: {
url, sourceTabBrowser: source, createdTabBrowser: target,
sourceFrameOuterWindowID: 123,
}}, "webNavigation-createdNavigationTarget", null);
}
notify("https://other.example/target");
equal(received.length, 0, "filter applies to chrome-created targets too");
notify("https://example.com/target");
equal(received.length, 1);
Manager.onCreatedNavigationTarget(source, {
isSourceTab: true, createdWindowId: 1000, sourceWindowId: 123,
url: "https://example.com/target",
});
event.removeListener(listener);
equal(Manager.createdNavigationTargetByOuterWindowId.size, 0,
"unregistering releases unmatched messages and timers");
notify("https://example.com/target");
equal(received.length, 1, "removed listeners are not invoked");
});

View file

@ -0,0 +1,66 @@
"use strict";
// Exercise real channels: a successful import alone does not prove that the
// observer, native wrapper, filters and blocking response are connected.
add_task(function* test_webrequest_backend() {
let {WebRequest} = Cu.import("resource://gre/modules/WebRequest.jsm", {});
let server = createHttpServer();
let hits = 0;
server.registerPathHandler("/request", (request, response) => {
++hits;
response.setStatusLine(request.httpVersion, 200, "OK");
response.write("allowed");
});
let url = `http://localhost:${server.identity.primaryPort}/request`;
function request(system = false) {
let uri = Services.io.newURI(url, null, null);
let channel = NetUtil.newChannel({
uri,
loadingPrincipal: system ? Services.scriptSecurityManager.getSystemPrincipal()
: Services.scriptSecurityManager.createCodebasePrincipal(uri, {}),
securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
contentPolicyType: Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST,
});
return new Promise(resolve => {
NetUtil.asyncFetch(channel, (stream, status) => resolve(status));
});
}
let calls = 0;
let block = data => {
++calls;
equal(data.url, url);
equal(data.type, "xmlhttprequest");
equal(typeof data.requestId, "string");
return {cancel: true};
};
let event = WebRequest.onBeforeRequest;
do_register_cleanup(() => event.removeListener(block));
event.addListener(block, {urls: ["http://localhost/*"]}, ["blocking"]);
equal(yield request(), Cr.NS_ERROR_ABORT, "blocking listener cancels the channel");
equal(calls, 1);
equal(hits, 0, "cancelled request never reaches the server");
event.removeListener(block);
equal(yield request(), Cr.NS_OK, "removing the listener restores loading");
for (let filter of [
{urls: ["http://example.org/*"]},
{urls: ["<all_urls>"], types: ["image"]},
{urls: ["<all_urls>"], incognito: true},
{urls: ["<all_urls>"], tabId: 123},
]) {
event.addListener(block, filter, ["blocking"]);
equal(yield request(), Cr.NS_OK, "nonmatching requests are untouched");
event.removeListener(block);
}
event.addListener(block, {urls: ["<all_urls>"]}, ["blocking"], {
policy: {id: "backend-test", allowedOrigins: new (Cu.import(
"resource://gre/modules/MatchPattern.jsm", {}).MatchPattern)(["http://example.org/*"])},
});
equal(yield request(), Cr.NS_OK, "host permissions constrain the listener");
event.removeListener(block);
event.addListener(block, {urls: ["<all_urls>"]}, ["blocking"]);
equal(yield request(true), Cr.NS_OK, "system requests are not exposed");
event.removeListener(block);
equal(calls, 1, "only the matching content request was dispatched");
});

View file

@ -8,6 +8,8 @@ support-files =
tags = webextensions tags = webextensions
[test_csp_custom_policies.js] [test_csp_custom_policies.js]
[test_webrequest_backend.js]
[test_webnavigation_created_target.js]
[test_csp_validator.js] [test_csp_validator.js]
[test_ext_alarms.js] [test_ext_alarms.js]
[test_ext_alarms_does_not_fire.js] [test_ext_alarms_does_not_fire.js]

View file

@ -4,11 +4,11 @@
"use strict"; "use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
const EXPORTED_SYMBOLS = ["SecurityInfo"]; const EXPORTED_SYMBOLS = ["SecurityInfo"];
const { XPCOMUtils } = ChromeUtils.import( const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
"resource://gre/modules/XPCOMUtils.jsm"
);
const wpl = Ci.nsIWebProgressListener; const wpl = Ci.nsIWebProgressListener;
XPCOMUtils.defineLazyServiceGetter( XPCOMUtils.defineLazyServiceGetter(

View file

@ -4,6 +4,8 @@
"use strict"; "use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
const EXPORTED_SYMBOLS = ["WebRequest"]; const EXPORTED_SYMBOLS = ["WebRequest"];
/* exported WebRequest */ /* exported WebRequest */
@ -12,29 +14,36 @@ const EXPORTED_SYMBOLS = ["WebRequest"];
const { nsIHttpActivityObserver, nsISocketTransport } = Ci; const { nsIHttpActivityObserver, nsISocketTransport } = Ci;
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
const { XPCOMUtils } = ChromeUtils.import( const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
ExtensionParent: "resource://gre/modules/ExtensionParent.jsm",
ExtensionUtils: "resource://gre/modules/ExtensionUtils.jsm",
WebRequestUpload: "resource://gre/modules/WebRequestUpload.jsm",
SecurityInfo: "resource://gre/modules/SecurityInfo.jsm",
});
// WebRequest.jsm's only consumer is ext-webRequest.js, so we can depend on XPCOMUtils.defineLazyModuleGetter(this, "ExtensionParent", "resource://gre/modules/ExtensionParent.jsm");
// the apiManager.global being initialized. XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils", "resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "tabTracker", () => { XPCOMUtils.defineLazyModuleGetter(this, "WebRequestUpload", "resource://gre/modules/WebRequestUpload.jsm");
return ExtensionParent.apiManager.global.tabTracker; XPCOMUtils.defineLazyModuleGetter(this, "SecurityInfo", "resource://gre/modules/SecurityInfo.jsm");
});
XPCOMUtils.defineLazyGetter(this, "getCookieStoreIdForOriginAttributes", () => {
return ExtensionParent.apiManager.global.getCookieStoreIdForOriginAttributes; function getCookieStoreIdForOriginAttributes(attrs) {
}); if (attrs.privateBrowsingId) {
return "firefox-private";
}
return attrs.userContextId ? "firefox-container-" + attrs.userContextId : "firefox-default";
}
XPCOMUtils.defineLazyServiceGetter(this, "categoryManager",
"@mozilla.org/categorymanager;1", "nsICategoryManager");
XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", "resource://gre/modules/MatchPattern.jsm");
function matchesRequest(channel, opts, extraData) {
return !channel.isSystemLoad &&
(!opts.urlPattern || opts.urlPattern.matches(channel.finalURI)) &&
(!opts.policy || opts.policy.allowedOrigins.matches(channel.finalURI)) &&
channel.matches(opts.filter, opts.policy ? opts.policy.id : "", extraData);
}
function runLater(job) { function runLater(job) {
Services.tm.dispatchToMainThread(job); Services.tm.mainThread.dispatch(job, Ci.nsIThread.DISPATCH_NORMAL);
} }
function parseFilter(filter) { function parseFilter(filter) {
@ -165,7 +174,7 @@ class HeaderChanger {
} }
const checkRestrictedHeaderValue = (value, opts = {}) => { const checkRestrictedHeaderValue = (value, opts = {}) => {
let uri = Services.io.newURI(`https://${value}/`); let uri = Services.io.newURI(`https://${value}/`, null, null);
let { policy } = opts; let { policy } = opts;
if (policy && !policy.allowedOrigins.matches(uri)) { if (policy && !policy.allowedOrigins.matches(uri)) {
@ -281,7 +290,7 @@ var ChannelEventSink = {
_classID: Components.ID("115062f8-92f1-11e5-8b7f-080027b0f7ec"), _classID: Components.ID("115062f8-92f1-11e5-8b7f-080027b0f7ec"),
_contractID: "@mozilla.org/webrequest/channel-event-sink;1", _contractID: "@mozilla.org/webrequest/channel-event-sink;1",
QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink", "nsIFactory"]), QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink, Ci.nsIFactory]),
init() { init() {
Components.manager Components.manager
@ -295,7 +304,7 @@ var ChannelEventSink = {
}, },
register() { register() {
Services.catMan.addCategoryEntry( categoryManager.addCategoryEntry(
"net-channel-event-sinks", "net-channel-event-sinks",
this._contractID, this._contractID,
this._contractID, this._contractID,
@ -305,7 +314,7 @@ var ChannelEventSink = {
}, },
unregister() { unregister() {
Services.catMan.deleteCategoryEntry( categoryManager.deleteCategoryEntry(
"net-channel-event-sinks", "net-channel-event-sinks",
this._contractID, this._contractID,
false false
@ -460,7 +469,7 @@ class AuthRequestor {
this.httpObserver.runChannelListener(wrapper, "onAuthRequired", data); this.httpObserver.runChannelListener(wrapper, "onAuthRequired", data);
return { return {
QueryInterface: ChromeUtils.generateQI(["nsICancelable"]), QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
cancel() { cancel() {
try { try {
callback.onAuthCancelled(context, false); callback.onAuthCancelled(context, false);
@ -474,10 +483,10 @@ class AuthRequestor {
} }
} }
AuthRequestor.prototype.QueryInterface = ChromeUtils.generateQI([ AuthRequestor.prototype.QueryInterface = XPCOMUtils.generateQI([
"nsIInterfaceRequestor", Ci.nsIInterfaceRequestor,
"nsIAuthPromptProvider", Ci.nsIAuthPromptProvider,
"nsIAuthPrompt2", Ci.nsIAuthPrompt2,
]); ]);
// Most WebRequest events are implemented via the observer services, but // Most WebRequest events are implemented via the observer services, but
@ -563,21 +572,21 @@ HttpObserverManager = {
this.listeners.onSendHeaders.size; this.listeners.onSendHeaders.size;
if (needOpening && !this.openingInitialized) { if (needOpening && !this.openingInitialized) {
this.openingInitialized = true; this.openingInitialized = true;
Services.obs.addObserver(this, "http-on-modify-request"); Services.obs.addObserver(this, "http-on-modify-request", false);
} else if (!needOpening && this.openingInitialized) { } else if (!needOpening && this.openingInitialized) {
this.openingInitialized = false; this.openingInitialized = false;
Services.obs.removeObserver(this, "http-on-modify-request"); Services.obs.removeObserver(this, "http-on-modify-request");
} }
if (needBeforeConnect && !this.beforeConnectInitialized) { if (needBeforeConnect && !this.beforeConnectInitialized) {
this.beforeConnectInitialized = true; this.beforeConnectInitialized = true;
Services.obs.addObserver(this, "http-on-before-connect"); Services.obs.addObserver(this, "http-on-before-connect", false);
} else if (!needBeforeConnect && this.beforeConnectInitialized) { } else if (!needBeforeConnect && this.beforeConnectInitialized) {
this.beforeConnectInitialized = false; this.beforeConnectInitialized = false;
Services.obs.removeObserver(this, "http-on-before-connect"); Services.obs.removeObserver(this, "http-on-before-connect");
} }
let haveBlocking = Object.values(this.listeners).some(listeners => let haveBlocking = Object.values(this.listeners).some(listeners =>
Array.from(listeners.values()).some(listener => listener.blockingAllowed) Array.from(listeners.values()).some(listener => listener.blocking)
); );
this.needTracing = this.needTracing =
@ -593,9 +602,9 @@ HttpObserverManager = {
if (needExamine && !this.examineInitialized) { if (needExamine && !this.examineInitialized) {
this.examineInitialized = true; this.examineInitialized = true;
Services.obs.addObserver(this, "http-on-examine-response"); Services.obs.addObserver(this, "http-on-examine-response", false);
Services.obs.addObserver(this, "http-on-examine-cached-response"); Services.obs.addObserver(this, "http-on-examine-cached-response", false);
Services.obs.addObserver(this, "http-on-examine-merged-response"); Services.obs.addObserver(this, "http-on-examine-merged-response", false);
} else if (!needExamine && this.examineInitialized) { } else if (!needExamine && this.examineInitialized) {
this.examineInitialized = false; this.examineInitialized = false;
Services.obs.removeObserver(this, "http-on-examine-response"); Services.obs.removeObserver(this, "http-on-examine-response");
@ -692,7 +701,7 @@ HttpObserverManager = {
// Make a trip through the event loop to make sure errors have a // Make a trip through the event loop to make sure errors have a
// chance to be processed before we fall back to a generic error // chance to be processed before we fall back to a generic error
// string. // string.
Services.tm.dispatchToMainThread(() => { runLater(() => {
channel.errorCheck(); channel.errorCheck();
if (!channel.errorString) { if (!channel.errorString) {
this.runChannelListener(channel, "onErrorOccurred", { this.runChannelListener(channel, "onErrorOccurred", {
@ -712,7 +721,7 @@ HttpObserverManager = {
}, },
getRequestData(channel, extraData) { getRequestData(channel, extraData) {
let originAttributes = channel.loadInfo?.originAttributes; let originAttributes = channel.channel.loadInfo?.originAttributes;
let data = { let data = {
requestId: String(channel.id), requestId: String(channel.id),
url: channel.finalURL, url: channel.finalURL,
@ -725,6 +734,9 @@ HttpObserverManager = {
documentUrl: channel.documentURL || undefined, documentUrl: channel.documentURL || undefined,
tabId: this.getBrowserData(channel).tabId, tabId: this.getBrowserData(channel).tabId,
browser: channel.browserElement,
windowId: channel.windowId,
isSystemPrincipal: channel.isSystemLoad,
frameId: channel.windowId, frameId: channel.windowId,
parentWindowId: channel.parentWindowId, parentWindowId: channel.parentWindowId,
@ -783,7 +795,8 @@ HttpObserverManager = {
let browserData = wrapper._browserData; let browserData = wrapper._browserData;
if (!browserData) { if (!browserData) {
if (wrapper.browserElement) { if (wrapper.browserElement) {
browserData = tabTracker.getBrowserData(wrapper.browserElement); browserData = {};
ExtensionParent.apiManager.emit("fill-browser-data", wrapper.browserElement, browserData);
} else { } else {
browserData = { tabId: -1, windowId: -1 }; browserData = { tabId: -1, windowId: -1 };
} }
@ -815,7 +828,7 @@ HttpObserverManager = {
return; return;
} }
} }
if (!channel.matches(opts.filter, opts.policy ? opts.policy.id : "", extraData)) { if (!matchesRequest(channel, opts, extraData)) {
return; return;
} }
@ -957,7 +970,7 @@ HttpObserverManager = {
if (result.redirectUrl) { if (result.redirectUrl) {
try { try {
channel.suspended = false; channel.suspended = false;
channel.redirectTo(Services.io.newURI(result.redirectUrl)); channel.redirectTo(Services.io.newURI(result.redirectUrl, null, null));
// Web Extensions using the WebRequest API are allowed // Web Extensions using the WebRequest API are allowed
// to redirect a channel to a data: URI, hence we mark // to redirect a channel to a data: URI, hence we mark
@ -967,7 +980,7 @@ HttpObserverManager = {
// RedirectTo() implementation explicitly drops the flag // RedirectTo() implementation explicitly drops the flag
// to avoid additional redirects not caused by the // to avoid additional redirects not caused by the
// Web Extension. // Web Extension.
channel.loadInfo.allowInsecureRedirectToDataURI = true; channel.channel.loadInfo.allowInsecureRedirectToDataURI = true;
// To pass CORS checks, we pretend the current request's // To pass CORS checks, we pretend the current request's
// response allows the triggering origin to access. // response allows the triggering origin to access.
@ -1047,7 +1060,7 @@ HttpObserverManager = {
} }
for (let opts of listener.values()) { for (let opts of listener.values()) {
if (channel.matches(opts.filter, opts.policy ? opts.policy.id : "", extraData)) { if (matchesRequest(channel, opts, extraData)) {
return true; return true;
} }
} }
@ -1096,6 +1109,7 @@ HttpEvent.prototype = {
addListener(callback, filter = null, options = null, optionsObject = null) { addListener(callback, filter = null, options = null, optionsObject = null) {
let opts = parseExtra(options, this.options, optionsObject); let opts = parseExtra(options, this.options, optionsObject);
opts.filter = parseFilter(filter); opts.filter = parseFilter(filter);
opts.urlPattern = opts.filter.urls ? new MatchPattern(opts.filter.urls) : null;
HttpObserverManager.addListener(this.internalEvent, callback, opts); HttpObserverManager.addListener(this.internalEvent, callback, opts);
}, },

View file

@ -4,21 +4,19 @@
"use strict"; "use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
const EXPORTED_SYMBOLS = ["WebRequestUpload"]; const EXPORTED_SYMBOLS = ["WebRequestUpload"];
/* exported WebRequestUpload */ /* exported WebRequestUpload */
const { XPCOMUtils } = ChromeUtils.import( const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
"resource://gre/modules/XPCOMUtils.jsm"
);
const { ExtensionUtils } = ChromeUtils.import( const { ExtensionUtils } = Cu.import("resource://gre/modules/ExtensionUtils.jsm", {});
"resource://gre/modules/ExtensionUtils.jsm"
);
const { DefaultMap } = ExtensionUtils; const { DefaultMap } = ExtensionUtils;
XPCOMUtils.defineLazyGlobalGetters(this, ["TextEncoder"]); Cu.importGlobalProperties(["TextEncoder"]);
XPCOMUtils.defineLazyServiceGetter( XPCOMUtils.defineLazyServiceGetter(
this, this,
@ -475,7 +473,8 @@ function* getRawDataChunked(
// the file, rather than its data. // the file, rather than its data.
if ( if (
unbuffered instanceof Ci.nsIFileInputStream || unbuffered instanceof Ci.nsIFileInputStream ||
unbuffered instanceof Ci.mozIRemoteLazyInputStream ("mozIRemoteLazyInputStream" in Ci &&
unbuffered instanceof Ci.mozIRemoteLazyInputStream)
) { ) {
// But this is not actually supported yet. // But this is not actually supported yet.
yield { file: "<file>" }; yield { file: "<file>" };