diff --git a/chrome/content/zotero/HiddenBrowser.jsm b/chrome/content/zotero/HiddenBrowser.jsm index 33070a5947..f2368307ba 100644 --- a/chrome/content/zotero/HiddenBrowser.jsm +++ b/chrome/content/zotero/HiddenBrowser.jsm @@ -51,7 +51,12 @@ const browserFrameMap = new WeakMap(); **/ const HiddenBrowser = { /** - * @param {String) source - HTTP URL, file: URL, or file path + * @param {String} source - HTTP URL, file: URL, or file path + * @param {Object} options + * @param {Boolean} [options.allowJavaScript] + * @param {Object} [options.docShell] Fields to set on Browser.docShell + * @param {Boolean} [options.requireSuccessfulStatus] + * @param {Zotero.CookieSandbox} [options.cookieSandbox] */ async create(source, options = {}) { let url; @@ -69,6 +74,9 @@ const HiddenBrowser = { var windowlessBrowser = await frame.get(); windowlessBrowser.browsingContext.allowJavascript = options.allowJavaScript !== false; windowlessBrowser.docShell.allowImages = false; + if (options.docShell) { + Object.assign(windowlessBrowser.docShell, options.docShell); + } var doc = windowlessBrowser.document; var browser = doc.createXULElement("browser"); browser.setAttribute("type", "content"); @@ -77,8 +85,23 @@ const HiddenBrowser = { browser.setAttribute("disableglobalhistory", "true"); doc.documentElement.appendChild(browser); + if (options.cookieSandbox) { + options.cookieSandbox.attachToBrowser(browser); + } + browserFrameMap.set(browser, frame); + if (Zotero.Debug.enabled) { + let weakBrowser = new WeakRef(browser); + setTimeout(() => { + let browser = weakBrowser.deref(); + if (browser && browserFrameMap.has(browser)) { + Zotero.debug('Browser object still alive after 60 seconds - memory leak?'); + Zotero.debug('Viewing URI ' + browser.currentURI?.spec) + } + }, 1000 * 60); + } + // Next bit adapted from Mozilla's HeadlessShell.jsm const principal = Services.scriptSecurityManager.getSystemPrincipal(); try { @@ -139,6 +162,21 @@ const HiddenBrowser = { return false; } + if (options.requireSuccessfulStatus) { + let { channelInfo } = await this.getPageData(browser, ['channelInfo']); + if (channelInfo && (channelInfo.responseStatus < 200 || channelInfo.responseStatus >= 400)) { + let response = `${channelInfo.responseStatus} ${channelInfo.responseStatusText}`; + Zotero.debug(`HiddenBrowser.create: ${url} failed with ${response}`, 2); + throw new Zotero.HTTP.UnexpectedStatusException( + { + status: channelInfo.responseStatus + }, + url, + `Invalid response ${response} for ${url}` + ); + } + } + return browser; }, diff --git a/test/tests/HiddenBrowserTest.js b/test/tests/HiddenBrowserTest.js index 924c718049..d7e019c278 100644 --- a/test/tests/HiddenBrowserTest.js +++ b/test/tests/HiddenBrowserTest.js @@ -3,6 +3,27 @@ describe("HiddenBrowser", function() { "chrome://zotero/content/HiddenBrowser.jsm" ); + describe("#create()", function () { + var httpd; + var port = 16213; + var baseURL = `http://127.0.0.1:${port}/`; + + before(function () { + Cu.import("resource://zotero-unit/httpd.js"); + httpd = new HttpServer(); + httpd.start(port); + }); + + after(async function () { + await new Promise(resolve => httpd.stop(resolve)); + }); + + it("should fail on non-2xx response with requireSuccessfulStatus", async function () { + let e = await getPromiseError(HiddenBrowser.create(baseURL + 'nonexistent', { requireSuccessfulStatus: true })); + assert.instanceOf(e, Zotero.HTTP.UnexpectedStatusException); + }); + }); + describe("#getPageData()", function () { it("should handle local UTF-8 HTML file", async function () { var path = OS.Path.join(getTestDataDirectory().path, 'test-hidden.html');