diff --git a/devtools/client/jsonview/converter-child.js b/devtools/client/jsonview/converter-child.js index 61aa0c9a36..65327c3953 100644 --- a/devtools/client/jsonview/converter-child.js +++ b/devtools/client/jsonview/converter-child.js @@ -23,10 +23,6 @@ const childProcessMessageManager = Cc["@mozilla.org/childprocessmessagemanager;1"] .getService(Ci.nsISyncMessageSender); -// Amount of space that will be allocated for the stream's backing-store. -// Must be power of 2. Used to copy the data stream in onStopRequest. -const SEGMENT_SIZE = Math.pow(2, 17); - const JSON_VIEW_MIME_TYPE = "application/vnd.mozilla.json.view"; const CONTRACT_ID = "@mozilla.org/streamconv;1?from=" + JSON_VIEW_MIME_TYPE + "&to=*/*"; @@ -61,9 +57,8 @@ let Converter = Class({ * 1. asyncConvertData captures the listener * 2. onStartRequest fires, initializes stuff, modifies the listener * to match our output type - * 3. onDataAvailable transcodes the data into a UTF-8 string - * 4. onStopRequest gets the collected data and converts it, - * spits it to the listener + * 3. onDataAvailable spits it back to the listener + * 4. onStopRequest spits it back to the listener * 5. convert does nothing, it's just the synchronous version * of asyncConvertData */ @@ -76,244 +71,222 @@ let Converter = Class({ }, onDataAvailable: function (request, context, inputStream, offset, count) { - // From https://developer.mozilla.org/en/Reading_textual_data - let is = Cc["@mozilla.org/intl/converter-input-stream;1"] - .createInstance(Ci.nsIConverterInputStream); - is.init(inputStream, this.charset, -1, - Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); - - // Seed it with something positive - while (count) { - let str = {}; - let bytesRead = is.readString(count, str); - if (!bytesRead) { - break; - } - count -= bytesRead; - this.data += str.value; - } + this.listener.onDataAvailable(...arguments); }, onStartRequest: function (request, context) { - this.data = ""; - this.uri = request.QueryInterface(Ci.nsIChannel).URI.spec; + // Set the content type to HTML in order to parse the doctype, styles + // and scripts, but later a element will switch the tokenizer + // to the plaintext state in order to parse the JSON. + request.QueryInterface(Ci.nsIChannel); + request.contentType = "text/html"; - // Sets the charset if it is available. (For documents loaded from the - // filesystem, this is not set.) - this.charset = - request.QueryInterface(Ci.nsIChannel).contentCharset || "UTF-8"; + // JSON enforces UTF-8 charset (see bug 741776). + request.contentCharset = "UTF-8"; + + // Changing the content type breaks saving functionality. Fix it. + fixSave(request); - this.channel = request; - this.channel.contentType = "text/html"; - this.channel.contentCharset = "UTF-8"; // Because content might still have a reference to this window, // force setting it to a null principal to avoid it being same- // origin with (other) content. - this.channel.loadInfo.resetPrincipalsToNullPrincipal(); + request.loadInfo.resetPrincipalsToNullPrincipal(); - this.listener.onStartRequest(this.channel, context); - }, - - /** - * This should go something like this: - * 1. Make sure we have a unicode string. - * 2. Convert it to a Javascript object. - * 2.1 Removes the callback - * 3. Convert that to HTML? Or XUL? - * 4. Spit it back out at the listener - */ - onStopRequest: function (request, context, statusCode) { - let headers = { - response: [], - request: [] - }; + // Start the request. + this.listener.onStartRequest(request, context); + // Initialize stuff. let win = NetworkHelper.getWindowForRequest(request); + exportData(win, request); + win.addEventListener("DOMContentLoaded", event => { + win.addEventListener("contentMessage", onContentMessage, false, true); + }, {once: true}); - let Locale = { - $STR: key => { - try { - return jsonViewStrings.GetStringFromName(key); - } catch (err) { - console.error(err); - return undefined; - } - } - }; - - JsonViewUtils.exportIntoContentScope(win, Locale, "Locale"); - - Events.once(win, "DOMContentLoaded", event => { - win.addEventListener("contentMessage", - this.onContentMessage.bind(this), false, true); - }); - - // The request doesn't have to be always nsIHttpChannel - // (e.g. in case of data: URLs) - if (request instanceof Ci.nsIHttpChannel) { - request.visitResponseHeaders({ - visitHeader: function (name, value) { - headers.response.push({name: name, value: value}); - } - }); - - request.visitRequestHeaders({ - visitHeader: function (name, value) { - headers.request.push({name: name, value: value}); - } - }); - } - - let outputDoc = ""; - - try { - headers = JSON.stringify(headers); - outputDoc = this.toHTML(this.data, headers, this.uri); - } catch (e) { - console.error("JSON Viewer ERROR " + e); - outputDoc = this.toErrorPage(e, this.data, this.uri); - } - - let storage = Cc["@mozilla.org/storagestream;1"] - .createInstance(Ci.nsIStorageStream); - - storage.init(SEGMENT_SIZE, 0xffffffff, null); - let out = storage.getOutputStream(0); - - let binout = Cc["@mozilla.org/binaryoutputstream;1"] - .createInstance(Ci.nsIBinaryOutputStream); - - binout.setOutputStream(out); - binout.writeUtf8Z(outputDoc); - binout.close(); - - // We need to trim 4 bytes off the front (this could be underlying bug). - let trunc = 4; - let instream = storage.newInputStream(trunc); - - // Pass the data to the main content listener - this.listener.onDataAvailable(this.channel, context, instream, 0, - instream.available()); - - this.listener.onStopRequest(this.channel, context, statusCode); + // Insert the initial HTML code. + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + let stream = converter.convertToInputStream(initialHTML(win.document)); + this.listener.onDataAvailable(request, context, stream, 0, stream.available()); + }, + onStopRequest: function (request, context, statusCode) { + this.listener.onStopRequest(request, context, statusCode); this.listener = null; - }, - - htmlEncode: function (t) { - return t !== null ? t.toString() - .replace(/&/g, "&amp;") - .replace(/"/g, "&quot;") - .replace(/</g, "&lt;") - .replace(/>/g, "&gt;") : ""; - }, - - toHTML: function (json, headers, title) { - let themeClassName = "theme-" + JsonViewUtils.getCurrentTheme(); - let clientBaseUrl = "resource://devtools/client/"; - let baseUrl = clientBaseUrl + "jsonview/"; - let themeVarsUrl = clientBaseUrl + "themes/variables.css"; - let commonUrl = clientBaseUrl + "themes/common.css"; - let toolbarsUrl = clientBaseUrl + "themes/toolbars.css"; - - let os; - let platform = Services.appinfo.OS; - if (platform.startsWith("WINNT")) { - os = "win"; - } else if (platform.startsWith("Darwin")) { - os = "mac"; - } else { - os = "linux"; - } - - return "<!DOCTYPE html>\n" + - "<html platform=\"" + os + "\" class=\"" + themeClassName + "\">" + - "<head><title>" + this.htmlEncode(title) + "</title>" + - "<base href=\"" + this.htmlEncode(baseUrl) + "\">" + - "<link rel=\"stylesheet\" type=\"text/css\" href=\"" + - themeVarsUrl + "\">" + - "<link rel=\"stylesheet\" type=\"text/css\" href=\"" + - commonUrl + "\">" + - "<link rel=\"stylesheet\" type=\"text/css\" href=\"" + - toolbarsUrl + "\">" + - "<link rel=\"stylesheet\" type=\"text/css\" href=\"css/main.css\">" + - "<script data-main=\"viewer-config\" src=\"lib/require.js\"></script>" + - "</head><body>" + - "<div id=\"content\"></div>" + - "<div id=\"json\">" + this.htmlEncode(json) + "</div>" + - "<div id=\"headers\">" + this.htmlEncode(headers) + "</div>" + - "</body></html>"; - }, - - toErrorPage: function (error, data, uri) { - // Escape unicode nulls - data = data.replace("\u0000", "\uFFFD"); - - let errorInfo = error + ""; - - let output = "<div id=\"error\">" + "error parsing"; - if (errorInfo.message) { - output += "<div class=\"errormessage\">" + errorInfo.message + "</div>"; - } - - output += "</div><div id=\"json\">" + this.highlightError(data, - errorInfo.line, errorInfo.column) + "</div>"; - - return "<!DOCTYPE html>\n" + - "<html><head><title>" + this.htmlEncode(uri + " - Error") + "</title>" + - "<base href=\"" + this.htmlEncode(this.data.url()) + "\">" + - "</head><body>" + - output + - "</body></html>"; - }, - - // Chrome <-> Content communication - - onContentMessage: function (e) { - // Do not handle events from different documents. - let win = NetworkHelper.getWindowForRequest(this.channel); - if (win != e.target) { - return; - } - - let value = e.detail.value; - switch (e.detail.type) { - case "copy": - Clipboard.set(value, "text"); - break; - - case "copy-headers": - this.copyHeaders(value); - break; - - case "save": - childProcessMessageManager.sendAsyncMessage( - "devtools:jsonview:save", value); - } - }, - - copyHeaders: function (headers) { - let value = ""; - let eol = (Services.appinfo.OS !== "WINNT") ? "\n" : "\r\n"; - - let responseHeaders = headers.response; - for (let i = 0; i < responseHeaders.length; i++) { - let header = responseHeaders[i]; - value += header.name + ": " + header.value + eol; - } - - value += eol; - - let requestHeaders = headers.request; - for (let i = 0; i < requestHeaders.length; i++) { - let header = requestHeaders[i]; - value += header.name + ": " + header.value + eol; - } - - Clipboard.set(value, "text"); } }); +// Lets "save as" save the original JSON, not the viewer. +// To save with the proper extension we need the original content type, +// which has been replaced by application/vnd.mozilla.json.view +function fixSave(request) { + let originalType; + if (request instanceof Ci.nsIHttpChannel) { + try { + let header = request.getResponseHeader("Content-Type"); + originalType = header.split(";")[0]; + } catch (err) { + // Handled below + } + } else { + let uri = request.QueryInterface(Ci.nsIChannel).URI.spec; + let match = uri.match(/^data:(.*?)[,;]/); + if (match) { + originalType = match[1]; + } + } + const JSON_TYPES = ["application/json", "application/manifest+json"]; + if (!JSON_TYPES.includes(originalType)) { + originalType = JSON_TYPES[0]; + } + request.QueryInterface(Ci.nsIWritablePropertyBag); + request.setProperty("contentType", originalType); +} + +// Exports variables that will be accessed by the non-privileged scripts. +function exportData(win, request) { + let Locale = { + $STR: key => { + try { + return jsonViewStrings.GetStringFromName(key); + } catch (err) { + console.error(err); + return undefined; + } + } + }; + JsonViewUtils.exportIntoContentScope(win, Locale, "Locale"); + + let headers = { + response: [], + request: [] + }; + // The request doesn't have to be always nsIHttpChannel + // (e.g. in case of data: URLs) + if (request instanceof Ci.nsIHttpChannel) { + request.visitResponseHeaders({ + visitHeader: function (name, value) { + headers.response.push({name: name, value: value}); + } + }); + request.visitRequestHeaders({ + visitHeader: function (name, value) { + headers.request.push({name: name, value: value}); + } + }); + } + JsonViewUtils.exportIntoContentScope(win, headers, "headers"); +} + +// Serializes a qualifiedName and an optional set of attributes into an HTML +// start tag. Be aware qualifiedName and attribute names are not validated. +// Attribute values are escaped with escapingString algorithm in attribute mode +// (https://html.spec.whatwg.org/multipage/syntax.html#escapingString). +function startTag(qualifiedName, attributes = {}) { + return Object.entries(attributes).reduce(function (prev, [attr, value]) { + return prev + " " + attr + "=\"" + + value.replace(/&/g, "&amp;") + .replace(/\u00a0/g, "&nbsp;") + .replace(/"/g, "&quot;") + + "\""; + }, "<" + qualifiedName) + ">"; +} + +// Builds an HTML string that will be used to load stylesheets and scripts, +// and switch the parser to plaintext state. +function initialHTML(doc) { + let os; + let platform = Services.appinfo.OS; + if (platform.startsWith("WINNT")) { + os = "win"; + } else if (platform.startsWith("Darwin")) { + os = "mac"; + } else { + os = "linux"; + } + + let base = doc.createElement("base"); + base.href = "resource://devtools/client/jsonview/"; + + let style = doc.createElement("link"); + style.rel = "stylesheet"; + style.type = "text/css"; + style.href = "css/main.css"; + + let script = doc.createElement("script"); + script.src = "lib/require.js"; + script.dataset.main = "viewer-config"; + script.defer = true; + + let head = doc.createElement("head"); + head.append(base, style, script); + + return "<!DOCTYPE html>\n" + + startTag("html", { + "platform": os, + "class": "theme-" + JsonViewUtils.getCurrentTheme(), + "dir": Services.locale.isAppLocaleRTL ? "rtl" : "ltr" + }) + + head.outerHTML + + startTag("body") + + startTag("div", {"id": "content"}) + + startTag("plaintext", {"id": "json"}); +} + +// Chrome <-> Content communication +function onContentMessage(e) { + // Do not handle events from different documents. + let win = this; + if (win != e.target) { + return; + } + + let value = e.detail.value; + switch (e.detail.type) { + case "copy": + copyString(win, value); + break; + + case "copy-headers": + copyHeaders(win, value); + break; + + case "save": + childProcessMessageManager.sendAsyncMessage( + "devtools:jsonview:save", value); + } +} + +function copyHeaders(win, headers) { + let value = ""; + let eol = (Services.appinfo.OS !== "WINNT") ? "\n" : "\r\n"; + + let responseHeaders = headers.response; + for (let i = 0; i < responseHeaders.length; i++) { + let header = responseHeaders[i]; + value += header.name + ": " + header.value + eol; + } + + value += eol; + + let requestHeaders = headers.request; + for (let i = 0; i < requestHeaders.length; i++) { + let header = requestHeaders[i]; + value += header.name + ": " + header.value + eol; + } + + copyString(win, value); +} + +function copyString(win, string) { + win.document.addEventListener("copy", event => { + event.clipboardData.setData("text/plain", string); + event.preventDefault(); + }, {once: true}); + + win.document.execCommand("copy", false, null); +} + // Stream converter component definition let service = xpcom.Service({ id: components.ID(CLASS_ID), diff --git a/devtools/client/jsonview/css/general.css b/devtools/client/jsonview/css/general.css index 0c68d65e77..d80720f4fe 100644 --- a/devtools/client/jsonview/css/general.css +++ b/devtools/client/jsonview/css/general.css @@ -28,9 +28,9 @@ pre { font-family: var(--monospace-font-family); } -#json, -#headers { +#json { display: none; + white-space: pre-wrap; } /******************************************************************************/ diff --git a/devtools/client/jsonview/css/main.css b/devtools/client/jsonview/css/main.css index 04f3cb87ca..c75d7cea0c 100644 --- a/devtools/client/jsonview/css/main.css +++ b/devtools/client/jsonview/css/main.css @@ -3,7 +3,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -@import "resource://devtools/client/shared/components/reps/reps.css"; +@import "resource://devtools/client/themes/variables.css"; +@import "resource://devtools/client/themes/common.css"; +@import "resource://devtools/client/themes/toolbars.css"; @import "resource://devtools/client/shared/components/tree/tree-view.css"; @import "resource://devtools/client/shared/components/tabs/tabs.css"; diff --git a/devtools/client/jsonview/json-viewer.js b/devtools/client/jsonview/json-viewer.js index d96081da2f..38cb6d7ecd 100644 --- a/devtools/client/jsonview/json-viewer.js +++ b/devtools/client/jsonview/json-viewer.js @@ -12,28 +12,28 @@ define(function (require, exports, module) { const { MainTabbedArea } = createFactories(require("./components/main-tabbed-area")); const json = document.getElementById("json"); - const headers = document.getElementById("headers"); - - let jsonData; - - try { - jsonData = JSON.parse(json.textContent); - } catch (err) { - jsonData = err + ""; - } // Application state object. let input = { jsonText: json.textContent, jsonPretty: null, - json: jsonData, - headers: JSON.parse(headers.textContent), + headers: window.headers, tabActive: 0, prettified: false }; + // Remove BOM, if present. + if (input.jsonText.startsWith("\ufeff")) { + input.jsonText = input.jsonText.slice(1); + } + + try { + input.json = JSON.parse(input.jsonText); + } catch (err) { + input.json = err; + } + json.remove(); - headers.remove(); /** * Application actions/commands. This list implements all commands @@ -61,7 +61,7 @@ define(function (require, exports, module) { theApp.setState({jsonText: input.jsonText}); } else { if (!input.jsonPretty) { - input.jsonPretty = JSON.stringify(jsonData, null, " "); + input.jsonPretty = JSON.stringify(input.json, null, " "); } theApp.setState({jsonText: input.jsonPretty}); } diff --git a/devtools/client/jsonview/utils.js b/devtools/client/jsonview/utils.js index a70afdc68c..6ab697c892 100644 --- a/devtools/client/jsonview/utils.js +++ b/devtools/client/jsonview/utils.js @@ -96,6 +96,8 @@ exports.exportIntoContentScope = function (win, obj, defineAs) { Cu.exportFunction(propValue, clone, { defineAs: propName }); + } else { + clone[propName] = Cu.cloneInto(propValue, win); } } }; diff --git a/dom/base/nsContentPolicy.cpp b/dom/base/nsContentPolicy.cpp index 5511b90864..534466103c 100644 --- a/dom/base/nsContentPolicy.cpp +++ b/dom/base/nsContentPolicy.cpp @@ -22,6 +22,7 @@ #include "nsIDOMWindow.h" #include "nsITabChild.h" #include "nsIContent.h" +#include "nsIImageLoadingContent.h" #include "nsILoadContext.h" #include "nsCOMArray.h" #include "nsContentUtils.h" @@ -145,6 +146,16 @@ nsContentPolicy::CheckPolicy(CPMethod policyMethod, decision); if (NS_SUCCEEDED(rv) && NS_CP_REJECTED(*decision)) { + // If we are blocking an image, we have to let the + // ImageLoadingContent know that we blocked the load. + if (externalType == nsIContentPolicy::TYPE_IMAGE || + externalType == nsIContentPolicy::TYPE_IMAGESET) { + nsCOMPtr<nsIImageLoadingContent> img = + do_QueryInterface(requestingContext); + if (img) { + img->SetBlockedRequest(*decision); + } + } /* policy says no, no point continuing to check */ return NS_OK; } @@ -193,6 +204,16 @@ nsContentPolicy::CheckPolicy(CPMethod policyMethod, decision); if (NS_SUCCEEDED(rv) && NS_CP_REJECTED(*decision)) { + // If we are blocking an image, we have to let the + // ImageLoadingContent know that we blocked the load. + if (externalType == nsIContentPolicy::TYPE_IMAGE || + externalType == nsIContentPolicy::TYPE_IMAGESET) { + nsCOMPtr<nsIImageLoadingContent> img = + do_QueryInterface(requestingContext); + if (img) { + img->SetBlockedRequest(*decision); + } + } /* policy says no, no point continuing to check */ return NS_OK; } diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 1f9c179472..800f40fa1e 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -8478,12 +8478,9 @@ nsContentUtils::InternalContentPolicyTypeToExternalOrWorker(nsContentPolicyType bool nsContentUtils::IsPreloadType(nsContentPolicyType aType) { - if (aType == nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD || - aType == nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD || - aType == nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD) { - return true; - } - return false; + return (aType == nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD || + aType == nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD || + aType == nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD); } nsresult diff --git a/dom/base/nsDocument.cpp b/dom/base/nsDocument.cpp index d8abc174fc..6b8e11db05 100644 --- a/dom/base/nsDocument.cpp +++ b/dom/base/nsDocument.cpp @@ -9222,19 +9222,23 @@ already_AddRefed<nsIURI> nsDocument::ResolvePreloadImage(nsIURI *aBaseURI, const nsAString& aSrcAttr, const nsAString& aSrcsetAttr, - const nsAString& aSizesAttr) + const nsAString& aSizesAttr, + bool *aIsImgSet) { nsString sourceURL; + bool isImgSet; if (mPreloadPictureDepth == 1 && !mPreloadPictureFoundSource.IsVoid()) { // We're in a <picture> element and found a URI from a source previous to // this image, use it. sourceURL = mPreloadPictureFoundSource; + isImgSet = true; } else { // Otherwise try to use this <img> as a source HTMLImageElement::SelectSourceForTagWithAttrs(this, false, aSrcAttr, aSrcsetAttr, aSizesAttr, NullString(), NullString(), sourceURL); + isImgSet = !aSrcsetAttr.IsEmpty(); } // Empty sources are not loaded by <img> (i.e. not resolved to the baseURI) @@ -9252,6 +9256,8 @@ nsDocument::ResolvePreloadImage(nsIURI *aBaseURI, return nullptr; } + *aIsImgSet = isImgSet; + // We don't clear mPreloadPictureFoundSource because subsequent <img> tags in // this this <picture> share the same <sources> (though this is not valid per // spec) @@ -9260,16 +9266,12 @@ nsDocument::ResolvePreloadImage(nsIURI *aBaseURI, void nsDocument::MaybePreLoadImage(nsIURI* uri, const nsAString &aCrossOriginAttr, - ReferrerPolicy aReferrerPolicy) + ReferrerPolicy aReferrerPolicy, bool aIsImgSet) { // Early exit if the img is already present in the img-cache // which indicates that the "real" load has already started and // that we shouldn't preload it. - int16_t blockingStatus; - if (nsContentUtils::IsImageInCache(uri, static_cast<nsIDocument *>(this)) || - !nsContentUtils::CanLoadImage(uri, static_cast<nsIDocument *>(this), - this, NodePrincipal(), &blockingStatus, - nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD)) { + if (nsContentUtils::IsImageInCache(uri, static_cast<nsIDocument *>(this))) { return; } @@ -9288,6 +9290,10 @@ nsDocument::MaybePreLoadImage(nsIURI* uri, const nsAString &aCrossOriginAttr, MOZ_CRASH("Unknown CORS mode!"); } + nsContentPolicyType policyType = + aIsImgSet ? nsIContentPolicy::TYPE_IMAGESET : + nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD; + // Image not in cache - trigger preload RefPtr<imgRequestProxy> request; nsresult rv = @@ -9301,7 +9307,7 @@ nsDocument::MaybePreLoadImage(nsIURI* uri, const nsAString &aCrossOriginAttr, loadFlags, NS_LITERAL_STRING("img"), getter_AddRefs(request), - nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD); + policyType); // Pin image-reference to avoid evicting it from the img-cache before // the "real" load occurs. Unpinned in DispatchContentLoadedEvents and diff --git a/dom/base/nsDocument.h b/dom/base/nsDocument.h index 95fd57545b..2b29b98fa5 100644 --- a/dom/base/nsDocument.h +++ b/dom/base/nsDocument.h @@ -948,11 +948,13 @@ public: ResolvePreloadImage(nsIURI *aBaseURI, const nsAString& aSrcAttr, const nsAString& aSrcsetAttr, - const nsAString& aSizesAttr) override; + const nsAString& aSizesAttr, + bool *aIsImgSet) override; virtual void MaybePreLoadImage(nsIURI* uri, const nsAString &aCrossOriginAttr, - ReferrerPolicy aReferrerPolicy) override; + ReferrerPolicy aReferrerPolicy, + bool aIsImgSet) override; virtual void ForgetImagePreload(nsIURI* aURI) override; virtual void MaybePreconnect(nsIURI* uri, diff --git a/dom/base/nsIDocument.h b/dom/base/nsIDocument.h index e5d12ab8f6..d76a12d713 100644 --- a/dom/base/nsIDocument.h +++ b/dom/base/nsIDocument.h @@ -2260,21 +2260,27 @@ public: * nesting and possible sources, which are used to inform URL selection * responsive <picture> or <img srcset> images. Unset attributes are expected * to be marked void. + * If this image is for <picture> or <img srcset>, aIsImgSet will be set to + * true, false otherwise. */ virtual already_AddRefed<nsIURI> ResolvePreloadImage(nsIURI *aBaseURI, const nsAString& aSrcAttr, const nsAString& aSrcsetAttr, - const nsAString& aSizesAttr) = 0; + const nsAString& aSizesAttr, + bool *aIsImgSet) = 0; /** * Called by nsParser to preload images. Can be removed and code moved * to nsPreloadURIs::PreloadURIs() in file nsParser.cpp whenever the * parser-module is linked with gklayout-module. aCrossOriginAttr should * be a void string if the attr is not present. + * aIsImgSet is the value got from calling ResolvePreloadImage, it is true + * when this image is for loading <picture> or <img srcset> images. */ virtual void MaybePreLoadImage(nsIURI* uri, const nsAString& aCrossOriginAttr, - ReferrerPolicyEnum aReferrerPolicy) = 0; + ReferrerPolicyEnum aReferrerPolicy, + bool aIsImgSet) = 0; /** * Called by images to forget an image preload when they start doing diff --git a/dom/base/nsIImageLoadingContent.idl b/dom/base/nsIImageLoadingContent.idl index fea261a34c..eacc4ac3a9 100644 --- a/dom/base/nsIImageLoadingContent.idl +++ b/dom/base/nsIImageLoadingContent.idl @@ -103,6 +103,15 @@ interface nsIImageLoadingContent : imgINotificationObserver */ imgIRequest getRequest(in long aRequestType); + /** + * Call this function when the request was blocked by any of the + * security policies enforced. + * + * @param aContentDecision the decision returned from nsIContentPolicy + * (any of the types REJECT_*) + */ + void setBlockedRequest(in int16_t aContentDecision); + /** * @return true if the current request's size is available. */ diff --git a/dom/base/nsImageLoadingContent.cpp b/dom/base/nsImageLoadingContent.cpp index 0c6c37b443..4aad55941c 100644 --- a/dom/base/nsImageLoadingContent.cpp +++ b/dom/base/nsImageLoadingContent.cpp @@ -44,6 +44,7 @@ #include "mozAutoDocUpdate.h" #include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/AutoRestore.h" #include "mozilla/EventStates.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/ImageTracker.h" @@ -94,7 +95,8 @@ nsImageLoadingContent::nsImageLoadingContent() mNewRequestsWillNeedAnimationReset(false), mStateChangerDepth(0), mCurrentRequestRegistered(false), - mPendingRequestRegistered(false) + mPendingRequestRegistered(false), + mIsStartingImageLoad(false) { if (!nsContentUtils::GetImgLoaderForChannel(nullptr, nullptr)) { mLoadingEnabled = false; @@ -785,6 +787,11 @@ nsImageLoadingContent::LoadImage(nsIURI* aNewURI, nsIDocument* aDocument, nsLoadFlags aLoadFlags) { + MOZ_ASSERT(!mIsStartingImageLoad, "some evil code is reentering LoadImage."); + if (mIsStartingImageLoad) { + return NS_OK; + } + // Pending load/error events need to be canceled in some situations. This // is not documented in the spec, but can cause site compat problems if not // done. See bug 1309461 and https://github.com/whatwg/html/issues/1872. @@ -814,6 +821,21 @@ nsImageLoadingContent::LoadImage(nsIURI* aNewURI, } } + AutoRestore<bool> guard(mIsStartingImageLoad); + mIsStartingImageLoad = true; + + // Data documents, or documents from DOMParser shouldn't perform image loading. + if (aDocument->IsLoadedAsData()) { + // This is the only codepath on which we can reach SetBlockedRequest while + // our pending request exists. Just clear it out here if we do have one. + ClearPendingRequest(NS_BINDING_ABORTED, + Some(OnNonvisible::DISCARD_IMAGES)); + SetBlockedRequest(nsIContentPolicy::REJECT_REQUEST); + FireEvent(NS_LITERAL_STRING("error")); + FireEvent(NS_LITERAL_STRING("loadend")); + return NS_OK; + } + // URI equality check. // // We skip the equality check if our current image was blocked, since in that @@ -844,23 +866,8 @@ nsImageLoadingContent::LoadImage(nsIURI* aNewURI, "Principal mismatch?"); #endif - // Are we blocked? - int16_t cpDecision = nsIContentPolicy::REJECT_REQUEST; nsContentPolicyType policyType = PolicyTypeForLoad(aImageLoadType); - nsContentUtils::CanLoadImage(aNewURI, - static_cast<nsIImageLoadingContent*>(this), - aDocument, - aDocument->NodePrincipal(), - &cpDecision, - policyType); - if (!NS_CP_ACCEPTED(cpDecision)) { - FireEvent(NS_LITERAL_STRING("error")); - FireEvent(NS_LITERAL_STRING("loadend")); - SetBlockedRequest(aNewURI, cpDecision); - return NS_OK; - } - nsLoadFlags loadFlags = aLoadFlags; int32_t corsmode = GetCORSMode(); if (corsmode == CORS_ANONYMOUS) { @@ -878,7 +885,6 @@ nsImageLoadingContent::LoadImage(nsIURI* aNewURI, referrerPolicy = imgReferrerPolicy; } - // Not blocked. Do the load. RefPtr<imgRequestProxy>& req = PrepareNextRequest(aImageLoadType); nsCOMPtr<nsIContent> content = do_QueryInterface(static_cast<nsIImageLoadingContent*>(this)); @@ -932,7 +938,6 @@ nsImageLoadingContent::LoadImage(nsIURI* aNewURI, FireEvent(NS_LITERAL_STRING("error")); FireEvent(NS_LITERAL_STRING("loadend")); - return NS_OK; } return NS_OK; @@ -1212,46 +1217,42 @@ nsImageLoadingContent::PrepareNextRequest(ImageLoadType aImageLoadType) mMostRecentRequestChange = now; } - // If we don't have a usable current request, get rid of any half-baked - // request that might be sitting there and make this one current. - if (!HaveSize(mCurrentRequest)) - return PrepareCurrentRequest(aImageLoadType); - // Otherwise, make it pending. - return PreparePendingRequest(aImageLoadType); + // We only want to cancel the existing current request if size is not + // available. bz says the web depends on this behavior. + // Otherwise, we get rid of any half-baked request that might be sitting there + // and make this one current. + // TODO: Bug 583491 + // Investigate/Cleanup NS_ERROR_IMAGE_SRC_CHANGED use in nsImageFrame.cpp + return HaveSize(mCurrentRequest) ? + PreparePendingRequest(aImageLoadType) : + PrepareCurrentRequest(aImageLoadType); } -void -nsImageLoadingContent::SetBlockedRequest(nsIURI* aURI, int16_t aContentDecision) +nsresult +nsImageLoadingContent::SetBlockedRequest(int16_t aContentDecision) { + // If this is not calling from LoadImage, for example, from ServiceWorker, + // bail out. + if (!mIsStartingImageLoad) { + return NS_OK; + } + // Sanity MOZ_ASSERT(!NS_CP_ACCEPTED(aContentDecision), "Blocked but not?"); - // We do some slightly illogical stuff here to maintain consistency with - // old behavior that people probably depend on. Even in the case where the - // new image is blocked, the old one should really be canceled with the - // reason "image source changed". However, apparently there's some abuse - // over in nsImageFrame where the displaying of the "broken" icon for the - // next image depends on the cancel reason of the previous image. ugh. - // XXX(seth): So shouldn't we fix nsImageFrame?! - ClearPendingRequest(NS_ERROR_IMAGE_BLOCKED, - Some(OnNonvisible::DISCARD_IMAGES)); - - // For the blocked case, we only want to cancel the existing current request - // if size is not available. bz says the web depends on this behavior. - if (!HaveSize(mCurrentRequest)) { + // We should never have a pending request after we got blocked. + MOZ_ASSERT(!mPendingRequest, "mPendingRequest should be null."); + if (HaveSize(mCurrentRequest)) { + // PreparePendingRequest set mPendingRequestFlags, now since we've decided + // to block it, we reset it back to 0. + mPendingRequestFlags = 0; + } else { mImageBlockingStatus = aContentDecision; - uint32_t keepFlags = mCurrentRequestFlags & REQUEST_IS_IMAGESET; - ClearCurrentRequest(NS_ERROR_IMAGE_BLOCKED, - Some(OnNonvisible::DISCARD_IMAGES)); - - // We still want to remember what URI we were and if it was an imageset, - // despite not having an actual request. These are both cleared as part of - // ClearCurrentRequest() before a new request is started. - mCurrentURI = aURI; - mCurrentRequestFlags = keepFlags; } + + return NS_OK; } RefPtr<imgRequestProxy>& @@ -1262,7 +1263,7 @@ nsImageLoadingContent::PrepareCurrentRequest(ImageLoadType aImageLoadType) mImageBlockingStatus = nsIContentPolicy::ACCEPT; // Get rid of anything that was there previously. - ClearCurrentRequest(NS_ERROR_IMAGE_SRC_CHANGED, + ClearCurrentRequest(NS_BINDING_ABORTED, Some(OnNonvisible::DISCARD_IMAGES)); if (mNewRequestsWillNeedAnimationReset) { @@ -1281,7 +1282,7 @@ RefPtr<imgRequestProxy>& nsImageLoadingContent::PreparePendingRequest(ImageLoadType aImageLoadType) { // Get rid of anything that was there previously. - ClearPendingRequest(NS_ERROR_IMAGE_SRC_CHANGED, + ClearPendingRequest(NS_BINDING_ABORTED, Some(OnNonvisible::DISCARD_IMAGES)); if (mNewRequestsWillNeedAnimationReset) { diff --git a/dom/base/nsImageLoadingContent.h b/dom/base/nsImageLoadingContent.h index 5f7daff72f..cfb2a62072 100644 --- a/dom/base/nsImageLoadingContent.h +++ b/dom/base/nsImageLoadingContent.h @@ -302,18 +302,11 @@ protected: */ RefPtr<imgRequestProxy>& PrepareNextRequest(ImageLoadType aImageLoadType); - /** - * Called when we would normally call PrepareNextRequest(), but the request was - * blocked. - */ - void SetBlockedRequest(nsIURI* aURI, int16_t aContentDecision); - /** * Returns a COMPtr reference to the current/pending image requests, cleaning * up and canceling anything that was there before. Note that if you just want * to get rid of one of the requests, you should call - * Clear*Request(NS_BINDING_ABORTED) instead, since it passes a more appropriate - * aReason than Prepare*Request() does (NS_ERROR_IMAGE_SRC_CHANGED). + * Clear*Request(NS_BINDING_ABORTED) instead. * * @param aImageLoadType The ImageLoadType for this request */ @@ -459,6 +452,14 @@ private: // registered with the refresh driver. bool mCurrentRequestRegistered; bool mPendingRequestRegistered; + + // This member is used in SetBlockedRequest, if it's true, then this call is + // triggered from LoadImage. + // If this is false, it means this call is from other places like + // ServiceWorker, then we will ignore call to SetBlockedRequest for now. + // + // Also we use this variable to check if some evil code is reentering LoadImage. + bool mIsStartingImageLoad; }; #endif // nsImageLoadingContent_h__ diff --git a/dom/security/nsContentSecurityManager.cpp b/dom/security/nsContentSecurityManager.cpp index 08fd9afd91..5c6701992f 100644 --- a/dom/security/nsContentSecurityManager.cpp +++ b/dom/security/nsContentSecurityManager.cpp @@ -10,6 +10,7 @@ #include "nsIStreamListener.h" #include "nsCDefaultURIFixup.h" #include "nsIURIFixup.h" +#include "nsIImageLoadingContent.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/TabChild.h" @@ -123,7 +124,7 @@ nsContentSecurityManager::CheckFTPSubresourceLoad(nsIChannel* aChannel) nsCOMPtr<nsIURI> uri; nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri)); - NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_SUCCESS(rv, rv); if (!uri) { return NS_OK; } @@ -801,6 +802,8 @@ nsContentSecurityManager::CheckChannel(nsIChannel* aChannel) // within nsCorsListenerProxy rv = DoCheckLoadURIChecks(uri, loadInfo); NS_ENSURE_SUCCESS(rv, rv); + // TODO: Bug 1371237 + // consider calling SetBlockedRequest in nsContentSecurityManager::CheckChannel } return NS_OK; diff --git a/image/imgLoader.cpp b/image/imgLoader.cpp index f7fb657bd1..996ca01cf1 100644 --- a/image/imgLoader.cpp +++ b/image/imgLoader.cpp @@ -1684,6 +1684,7 @@ imgLoader::ValidateRequestWithNewChannel(imgRequest* request, rv = newChannel->AsyncOpen2(listener); if (NS_WARN_IF(NS_FAILED(rv))) { + req->CancelAndForgetObserver(rv); return false; } diff --git a/netwerk/protocol/http/nsCORSListenerProxy.cpp b/netwerk/protocol/http/nsCORSListenerProxy.cpp index b9355c82bf..70035be28a 100644 --- a/netwerk/protocol/http/nsCORSListenerProxy.cpp +++ b/netwerk/protocol/http/nsCORSListenerProxy.cpp @@ -907,6 +907,9 @@ nsCORSListenerProxy::UpdateChannel(nsIChannel* aChannel, NS_ENSURE_SUCCESS(rv, rv); } + // TODO: Bug 1353683 + // consider calling SetBlockedRequest in nsCORSListenerProxy::UpdateChannel + // // Check that the uri is ok to load rv = nsContentUtils::GetSecurityManager()-> CheckLoadURIWithPrincipal(mRequestingPrincipal, uri, diff --git a/parser/html/nsHtml5TreeOpExecutor.cpp b/parser/html/nsHtml5TreeOpExecutor.cpp index 5c3f32d6fb..95f177376d 100644 --- a/parser/html/nsHtml5TreeOpExecutor.cpp +++ b/parser/html/nsHtml5TreeOpExecutor.cpp @@ -954,8 +954,9 @@ nsHtml5TreeOpExecutor::PreloadImage(const nsAString& aURL, const nsAString& aImageReferrerPolicy) { nsCOMPtr<nsIURI> baseURI = BaseURIForPreload(); + bool isImgSet = false; nsCOMPtr<nsIURI> uri = mDocument->ResolvePreloadImage(baseURI, aURL, aSrcset, - aSizes); + aSizes, &isImgSet); if (uri && ShouldPreloadURI(uri)) { // use document wide referrer policy mozilla::net::ReferrerPolicy referrerPolicy = mSpeculationReferrerPolicy; @@ -969,7 +970,7 @@ nsHtml5TreeOpExecutor::PreloadImage(const nsAString& aURL, } } - mDocument->MaybePreLoadImage(uri, aCrossOrigin, referrerPolicy); + mDocument->MaybePreLoadImage(uri, aCrossOrigin, referrerPolicy, isImgSet); } }