Merge remote-tracking branch 'origin/master' into custom

This commit is contained in:
Roy Tam 2019-08-10 06:32:58 +08:00
commit 93601ea999
17 changed files with 336 additions and 308 deletions

View file

@ -23,10 +23,6 @@ const childProcessMessageManager =
Cc["@mozilla.org/childprocessmessagemanager;1"] Cc["@mozilla.org/childprocessmessagemanager;1"]
.getService(Ci.nsISyncMessageSender); .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 JSON_VIEW_MIME_TYPE = "application/vnd.mozilla.json.view";
const CONTRACT_ID = "@mozilla.org/streamconv;1?from=" + const CONTRACT_ID = "@mozilla.org/streamconv;1?from=" +
JSON_VIEW_MIME_TYPE + "&to=*/*"; JSON_VIEW_MIME_TYPE + "&to=*/*";
@ -61,9 +57,8 @@ let Converter = Class({
* 1. asyncConvertData captures the listener * 1. asyncConvertData captures the listener
* 2. onStartRequest fires, initializes stuff, modifies the listener * 2. onStartRequest fires, initializes stuff, modifies the listener
* to match our output type * to match our output type
* 3. onDataAvailable transcodes the data into a UTF-8 string * 3. onDataAvailable spits it back to the listener
* 4. onStopRequest gets the collected data and converts it, * 4. onStopRequest spits it back to the listener
* spits it to the listener
* 5. convert does nothing, it's just the synchronous version * 5. convert does nothing, it's just the synchronous version
* of asyncConvertData * of asyncConvertData
*/ */
@ -76,244 +71,222 @@ let Converter = Class({
}, },
onDataAvailable: function (request, context, inputStream, offset, count) { onDataAvailable: function (request, context, inputStream, offset, count) {
// From https://developer.mozilla.org/en/Reading_textual_data this.listener.onDataAvailable(...arguments);
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;
}
}, },
onStartRequest: function (request, context) { onStartRequest: function (request, context) {
this.data = ""; // Set the content type to HTML in order to parse the doctype, styles
this.uri = request.QueryInterface(Ci.nsIChannel).URI.spec; // and scripts, but later a <plaintext> 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 // JSON enforces UTF-8 charset (see bug 741776).
// filesystem, this is not set.) request.contentCharset = "UTF-8";
this.charset =
request.QueryInterface(Ci.nsIChannel).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, // Because content might still have a reference to this window,
// force setting it to a null principal to avoid it being same- // force setting it to a null principal to avoid it being same-
// origin with (other) content. // origin with (other) content.
this.channel.loadInfo.resetPrincipalsToNullPrincipal(); request.loadInfo.resetPrincipalsToNullPrincipal();
this.listener.onStartRequest(this.channel, context); // Start the request.
}, this.listener.onStartRequest(request, 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: []
};
// Initialize stuff.
let win = NetworkHelper.getWindowForRequest(request); let win = NetworkHelper.getWindowForRequest(request);
exportData(win, request);
win.addEventListener("DOMContentLoaded", event => {
win.addEventListener("contentMessage", onContentMessage, false, true);
}, {once: true});
let Locale = { // Insert the initial HTML code.
$STR: key => { let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
try { .createInstance(Ci.nsIScriptableUnicodeConverter);
return jsonViewStrings.GetStringFromName(key); converter.charset = "UTF-8";
} catch (err) { let stream = converter.convertToInputStream(initialHTML(win.document));
console.error(err); this.listener.onDataAvailable(request, context, stream, 0, stream.available());
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);
onStopRequest: function (request, context, statusCode) {
this.listener.onStopRequest(request, context, statusCode);
this.listener = null; 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 // Stream converter component definition
let service = xpcom.Service({ let service = xpcom.Service({
id: components.ID(CLASS_ID), id: components.ID(CLASS_ID),

View file

@ -28,9 +28,9 @@ pre {
font-family: var(--monospace-font-family); font-family: var(--monospace-font-family);
} }
#json, #json {
#headers {
display: none; display: none;
white-space: pre-wrap;
} }
/******************************************************************************/ /******************************************************************************/

View file

@ -3,7 +3,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * 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/tree/tree-view.css";
@import "resource://devtools/client/shared/components/tabs/tabs.css"; @import "resource://devtools/client/shared/components/tabs/tabs.css";

View file

@ -12,28 +12,28 @@ define(function (require, exports, module) {
const { MainTabbedArea } = createFactories(require("./components/main-tabbed-area")); const { MainTabbedArea } = createFactories(require("./components/main-tabbed-area"));
const json = document.getElementById("json"); 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. // Application state object.
let input = { let input = {
jsonText: json.textContent, jsonText: json.textContent,
jsonPretty: null, jsonPretty: null,
json: jsonData, headers: window.headers,
headers: JSON.parse(headers.textContent),
tabActive: 0, tabActive: 0,
prettified: false 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(); json.remove();
headers.remove();
/** /**
* Application actions/commands. This list implements all commands * Application actions/commands. This list implements all commands
@ -61,7 +61,7 @@ define(function (require, exports, module) {
theApp.setState({jsonText: input.jsonText}); theApp.setState({jsonText: input.jsonText});
} else { } else {
if (!input.jsonPretty) { if (!input.jsonPretty) {
input.jsonPretty = JSON.stringify(jsonData, null, " "); input.jsonPretty = JSON.stringify(input.json, null, " ");
} }
theApp.setState({jsonText: input.jsonPretty}); theApp.setState({jsonText: input.jsonPretty});
} }

View file

@ -96,6 +96,8 @@ exports.exportIntoContentScope = function (win, obj, defineAs) {
Cu.exportFunction(propValue, clone, { Cu.exportFunction(propValue, clone, {
defineAs: propName defineAs: propName
}); });
} else {
clone[propName] = Cu.cloneInto(propValue, win);
} }
} }
}; };

View file

@ -22,6 +22,7 @@
#include "nsIDOMWindow.h" #include "nsIDOMWindow.h"
#include "nsITabChild.h" #include "nsITabChild.h"
#include "nsIContent.h" #include "nsIContent.h"
#include "nsIImageLoadingContent.h"
#include "nsILoadContext.h" #include "nsILoadContext.h"
#include "nsCOMArray.h" #include "nsCOMArray.h"
#include "nsContentUtils.h" #include "nsContentUtils.h"
@ -145,6 +146,16 @@ nsContentPolicy::CheckPolicy(CPMethod policyMethod,
decision); decision);
if (NS_SUCCEEDED(rv) && NS_CP_REJECTED(*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 */ /* policy says no, no point continuing to check */
return NS_OK; return NS_OK;
} }
@ -193,6 +204,16 @@ nsContentPolicy::CheckPolicy(CPMethod policyMethod,
decision); decision);
if (NS_SUCCEEDED(rv) && NS_CP_REJECTED(*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 */ /* policy says no, no point continuing to check */
return NS_OK; return NS_OK;
} }

View file

@ -8478,12 +8478,9 @@ nsContentUtils::InternalContentPolicyTypeToExternalOrWorker(nsContentPolicyType
bool bool
nsContentUtils::IsPreloadType(nsContentPolicyType aType) nsContentUtils::IsPreloadType(nsContentPolicyType aType)
{ {
if (aType == nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD || return (aType == nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD ||
aType == nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD || aType == nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD ||
aType == nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD) { aType == nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD);
return true;
}
return false;
} }
nsresult nsresult

View file

@ -9222,19 +9222,23 @@ already_AddRefed<nsIURI>
nsDocument::ResolvePreloadImage(nsIURI *aBaseURI, nsDocument::ResolvePreloadImage(nsIURI *aBaseURI,
const nsAString& aSrcAttr, const nsAString& aSrcAttr,
const nsAString& aSrcsetAttr, const nsAString& aSrcsetAttr,
const nsAString& aSizesAttr) const nsAString& aSizesAttr,
bool *aIsImgSet)
{ {
nsString sourceURL; nsString sourceURL;
bool isImgSet;
if (mPreloadPictureDepth == 1 && !mPreloadPictureFoundSource.IsVoid()) { if (mPreloadPictureDepth == 1 && !mPreloadPictureFoundSource.IsVoid()) {
// We're in a <picture> element and found a URI from a source previous to // We're in a <picture> element and found a URI from a source previous to
// this image, use it. // this image, use it.
sourceURL = mPreloadPictureFoundSource; sourceURL = mPreloadPictureFoundSource;
isImgSet = true;
} else { } else {
// Otherwise try to use this <img> as a source // Otherwise try to use this <img> as a source
HTMLImageElement::SelectSourceForTagWithAttrs(this, false, aSrcAttr, HTMLImageElement::SelectSourceForTagWithAttrs(this, false, aSrcAttr,
aSrcsetAttr, aSizesAttr, aSrcsetAttr, aSizesAttr,
NullString(), NullString(), NullString(), NullString(),
sourceURL); sourceURL);
isImgSet = !aSrcsetAttr.IsEmpty();
} }
// Empty sources are not loaded by <img> (i.e. not resolved to the baseURI) // Empty sources are not loaded by <img> (i.e. not resolved to the baseURI)
@ -9252,6 +9256,8 @@ nsDocument::ResolvePreloadImage(nsIURI *aBaseURI,
return nullptr; return nullptr;
} }
*aIsImgSet = isImgSet;
// We don't clear mPreloadPictureFoundSource because subsequent <img> tags in // We don't clear mPreloadPictureFoundSource because subsequent <img> tags in
// this this <picture> share the same <sources> (though this is not valid per // this this <picture> share the same <sources> (though this is not valid per
// spec) // spec)
@ -9260,16 +9266,12 @@ nsDocument::ResolvePreloadImage(nsIURI *aBaseURI,
void void
nsDocument::MaybePreLoadImage(nsIURI* uri, const nsAString &aCrossOriginAttr, 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 // Early exit if the img is already present in the img-cache
// which indicates that the "real" load has already started and // which indicates that the "real" load has already started and
// that we shouldn't preload it. // that we shouldn't preload it.
int16_t blockingStatus; if (nsContentUtils::IsImageInCache(uri, static_cast<nsIDocument *>(this))) {
if (nsContentUtils::IsImageInCache(uri, static_cast<nsIDocument *>(this)) ||
!nsContentUtils::CanLoadImage(uri, static_cast<nsIDocument *>(this),
this, NodePrincipal(), &blockingStatus,
nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD)) {
return; return;
} }
@ -9288,6 +9290,10 @@ nsDocument::MaybePreLoadImage(nsIURI* uri, const nsAString &aCrossOriginAttr,
MOZ_CRASH("Unknown CORS mode!"); MOZ_CRASH("Unknown CORS mode!");
} }
nsContentPolicyType policyType =
aIsImgSet ? nsIContentPolicy::TYPE_IMAGESET :
nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD;
// Image not in cache - trigger preload // Image not in cache - trigger preload
RefPtr<imgRequestProxy> request; RefPtr<imgRequestProxy> request;
nsresult rv = nsresult rv =
@ -9301,7 +9307,7 @@ nsDocument::MaybePreLoadImage(nsIURI* uri, const nsAString &aCrossOriginAttr,
loadFlags, loadFlags,
NS_LITERAL_STRING("img"), NS_LITERAL_STRING("img"),
getter_AddRefs(request), getter_AddRefs(request),
nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD); policyType);
// Pin image-reference to avoid evicting it from the img-cache before // Pin image-reference to avoid evicting it from the img-cache before
// the "real" load occurs. Unpinned in DispatchContentLoadedEvents and // the "real" load occurs. Unpinned in DispatchContentLoadedEvents and

View file

@ -948,11 +948,13 @@ public:
ResolvePreloadImage(nsIURI *aBaseURI, ResolvePreloadImage(nsIURI *aBaseURI,
const nsAString& aSrcAttr, const nsAString& aSrcAttr,
const nsAString& aSrcsetAttr, const nsAString& aSrcsetAttr,
const nsAString& aSizesAttr) override; const nsAString& aSizesAttr,
bool *aIsImgSet) override;
virtual void MaybePreLoadImage(nsIURI* uri, virtual void MaybePreLoadImage(nsIURI* uri,
const nsAString &aCrossOriginAttr, const nsAString &aCrossOriginAttr,
ReferrerPolicy aReferrerPolicy) override; ReferrerPolicy aReferrerPolicy,
bool aIsImgSet) override;
virtual void ForgetImagePreload(nsIURI* aURI) override; virtual void ForgetImagePreload(nsIURI* aURI) override;
virtual void MaybePreconnect(nsIURI* uri, virtual void MaybePreconnect(nsIURI* uri,

View file

@ -2260,21 +2260,27 @@ public:
* nesting and possible sources, which are used to inform URL selection * nesting and possible sources, which are used to inform URL selection
* responsive <picture> or <img srcset> images. Unset attributes are expected * responsive <picture> or <img srcset> images. Unset attributes are expected
* to be marked void. * 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> virtual already_AddRefed<nsIURI>
ResolvePreloadImage(nsIURI *aBaseURI, ResolvePreloadImage(nsIURI *aBaseURI,
const nsAString& aSrcAttr, const nsAString& aSrcAttr,
const nsAString& aSrcsetAttr, 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 * Called by nsParser to preload images. Can be removed and code moved
* to nsPreloadURIs::PreloadURIs() in file nsParser.cpp whenever the * to nsPreloadURIs::PreloadURIs() in file nsParser.cpp whenever the
* parser-module is linked with gklayout-module. aCrossOriginAttr should * parser-module is linked with gklayout-module. aCrossOriginAttr should
* be a void string if the attr is not present. * 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, virtual void MaybePreLoadImage(nsIURI* uri,
const nsAString& aCrossOriginAttr, const nsAString& aCrossOriginAttr,
ReferrerPolicyEnum aReferrerPolicy) = 0; ReferrerPolicyEnum aReferrerPolicy,
bool aIsImgSet) = 0;
/** /**
* Called by images to forget an image preload when they start doing * Called by images to forget an image preload when they start doing

View file

@ -103,6 +103,15 @@ interface nsIImageLoadingContent : imgINotificationObserver
*/ */
imgIRequest getRequest(in long aRequestType); 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. * @return true if the current request's size is available.
*/ */

View file

@ -44,6 +44,7 @@
#include "mozAutoDocUpdate.h" #include "mozAutoDocUpdate.h"
#include "mozilla/AsyncEventDispatcher.h" #include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/EventStates.h" #include "mozilla/EventStates.h"
#include "mozilla/dom/Element.h" #include "mozilla/dom/Element.h"
#include "mozilla/dom/ImageTracker.h" #include "mozilla/dom/ImageTracker.h"
@ -94,7 +95,8 @@ nsImageLoadingContent::nsImageLoadingContent()
mNewRequestsWillNeedAnimationReset(false), mNewRequestsWillNeedAnimationReset(false),
mStateChangerDepth(0), mStateChangerDepth(0),
mCurrentRequestRegistered(false), mCurrentRequestRegistered(false),
mPendingRequestRegistered(false) mPendingRequestRegistered(false),
mIsStartingImageLoad(false)
{ {
if (!nsContentUtils::GetImgLoaderForChannel(nullptr, nullptr)) { if (!nsContentUtils::GetImgLoaderForChannel(nullptr, nullptr)) {
mLoadingEnabled = false; mLoadingEnabled = false;
@ -785,6 +787,11 @@ nsImageLoadingContent::LoadImage(nsIURI* aNewURI,
nsIDocument* aDocument, nsIDocument* aDocument,
nsLoadFlags aLoadFlags) 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 // 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 // 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. // 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. // URI equality check.
// //
// We skip the equality check if our current image was blocked, since in that // We skip the equality check if our current image was blocked, since in that
@ -844,23 +866,8 @@ nsImageLoadingContent::LoadImage(nsIURI* aNewURI,
"Principal mismatch?"); "Principal mismatch?");
#endif #endif
// Are we blocked?
int16_t cpDecision = nsIContentPolicy::REJECT_REQUEST;
nsContentPolicyType policyType = PolicyTypeForLoad(aImageLoadType); 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; nsLoadFlags loadFlags = aLoadFlags;
int32_t corsmode = GetCORSMode(); int32_t corsmode = GetCORSMode();
if (corsmode == CORS_ANONYMOUS) { if (corsmode == CORS_ANONYMOUS) {
@ -878,7 +885,6 @@ nsImageLoadingContent::LoadImage(nsIURI* aNewURI,
referrerPolicy = imgReferrerPolicy; referrerPolicy = imgReferrerPolicy;
} }
// Not blocked. Do the load.
RefPtr<imgRequestProxy>& req = PrepareNextRequest(aImageLoadType); RefPtr<imgRequestProxy>& req = PrepareNextRequest(aImageLoadType);
nsCOMPtr<nsIContent> content = nsCOMPtr<nsIContent> content =
do_QueryInterface(static_cast<nsIImageLoadingContent*>(this)); do_QueryInterface(static_cast<nsIImageLoadingContent*>(this));
@ -932,7 +938,6 @@ nsImageLoadingContent::LoadImage(nsIURI* aNewURI,
FireEvent(NS_LITERAL_STRING("error")); FireEvent(NS_LITERAL_STRING("error"));
FireEvent(NS_LITERAL_STRING("loadend")); FireEvent(NS_LITERAL_STRING("loadend"));
return NS_OK;
} }
return NS_OK; return NS_OK;
@ -1212,46 +1217,42 @@ nsImageLoadingContent::PrepareNextRequest(ImageLoadType aImageLoadType)
mMostRecentRequestChange = now; 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. // We only want to cancel the existing current request if size is not
return PreparePendingRequest(aImageLoadType); // 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 nsresult
nsImageLoadingContent::SetBlockedRequest(nsIURI* aURI, int16_t aContentDecision) nsImageLoadingContent::SetBlockedRequest(int16_t aContentDecision)
{ {
// If this is not calling from LoadImage, for example, from ServiceWorker,
// bail out.
if (!mIsStartingImageLoad) {
return NS_OK;
}
// Sanity // Sanity
MOZ_ASSERT(!NS_CP_ACCEPTED(aContentDecision), "Blocked but not?"); MOZ_ASSERT(!NS_CP_ACCEPTED(aContentDecision), "Blocked but not?");
// We do some slightly illogical stuff here to maintain consistency with // We should never have a pending request after we got blocked.
// old behavior that people probably depend on. Even in the case where the MOZ_ASSERT(!mPendingRequest, "mPendingRequest should be null.");
// 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)) {
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; 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>& RefPtr<imgRequestProxy>&
@ -1262,7 +1263,7 @@ nsImageLoadingContent::PrepareCurrentRequest(ImageLoadType aImageLoadType)
mImageBlockingStatus = nsIContentPolicy::ACCEPT; mImageBlockingStatus = nsIContentPolicy::ACCEPT;
// Get rid of anything that was there previously. // Get rid of anything that was there previously.
ClearCurrentRequest(NS_ERROR_IMAGE_SRC_CHANGED, ClearCurrentRequest(NS_BINDING_ABORTED,
Some(OnNonvisible::DISCARD_IMAGES)); Some(OnNonvisible::DISCARD_IMAGES));
if (mNewRequestsWillNeedAnimationReset) { if (mNewRequestsWillNeedAnimationReset) {
@ -1281,7 +1282,7 @@ RefPtr<imgRequestProxy>&
nsImageLoadingContent::PreparePendingRequest(ImageLoadType aImageLoadType) nsImageLoadingContent::PreparePendingRequest(ImageLoadType aImageLoadType)
{ {
// Get rid of anything that was there previously. // Get rid of anything that was there previously.
ClearPendingRequest(NS_ERROR_IMAGE_SRC_CHANGED, ClearPendingRequest(NS_BINDING_ABORTED,
Some(OnNonvisible::DISCARD_IMAGES)); Some(OnNonvisible::DISCARD_IMAGES));
if (mNewRequestsWillNeedAnimationReset) { if (mNewRequestsWillNeedAnimationReset) {

View file

@ -302,18 +302,11 @@ protected:
*/ */
RefPtr<imgRequestProxy>& PrepareNextRequest(ImageLoadType aImageLoadType); 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 * 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 * 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 * to get rid of one of the requests, you should call
* Clear*Request(NS_BINDING_ABORTED) instead, since it passes a more appropriate * Clear*Request(NS_BINDING_ABORTED) instead.
* aReason than Prepare*Request() does (NS_ERROR_IMAGE_SRC_CHANGED).
* *
* @param aImageLoadType The ImageLoadType for this request * @param aImageLoadType The ImageLoadType for this request
*/ */
@ -459,6 +452,14 @@ private:
// registered with the refresh driver. // registered with the refresh driver.
bool mCurrentRequestRegistered; bool mCurrentRequestRegistered;
bool mPendingRequestRegistered; 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__ #endif // nsImageLoadingContent_h__

View file

@ -10,6 +10,7 @@
#include "nsIStreamListener.h" #include "nsIStreamListener.h"
#include "nsCDefaultURIFixup.h" #include "nsCDefaultURIFixup.h"
#include "nsIURIFixup.h" #include "nsIURIFixup.h"
#include "nsIImageLoadingContent.h"
#include "mozilla/dom/Element.h" #include "mozilla/dom/Element.h"
#include "mozilla/dom/TabChild.h" #include "mozilla/dom/TabChild.h"
@ -801,6 +802,8 @@ nsContentSecurityManager::CheckChannel(nsIChannel* aChannel)
// within nsCorsListenerProxy // within nsCorsListenerProxy
rv = DoCheckLoadURIChecks(uri, loadInfo); rv = DoCheckLoadURIChecks(uri, loadInfo);
NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_SUCCESS(rv, rv);
// TODO: Bug 1371237
// consider calling SetBlockedRequest in nsContentSecurityManager::CheckChannel
} }
return NS_OK; return NS_OK;

View file

@ -1684,6 +1684,7 @@ imgLoader::ValidateRequestWithNewChannel(imgRequest* request,
rv = newChannel->AsyncOpen2(listener); rv = newChannel->AsyncOpen2(listener);
if (NS_WARN_IF(NS_FAILED(rv))) { if (NS_WARN_IF(NS_FAILED(rv))) {
req->CancelAndForgetObserver(rv);
return false; return false;
} }

View file

@ -907,6 +907,9 @@ nsCORSListenerProxy::UpdateChannel(nsIChannel* aChannel,
NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_SUCCESS(rv, rv);
} }
// TODO: Bug 1353683
// consider calling SetBlockedRequest in nsCORSListenerProxy::UpdateChannel
//
// Check that the uri is ok to load // Check that the uri is ok to load
rv = nsContentUtils::GetSecurityManager()-> rv = nsContentUtils::GetSecurityManager()->
CheckLoadURIWithPrincipal(mRequestingPrincipal, uri, CheckLoadURIWithPrincipal(mRequestingPrincipal, uri,

View file

@ -954,8 +954,9 @@ nsHtml5TreeOpExecutor::PreloadImage(const nsAString& aURL,
const nsAString& aImageReferrerPolicy) const nsAString& aImageReferrerPolicy)
{ {
nsCOMPtr<nsIURI> baseURI = BaseURIForPreload(); nsCOMPtr<nsIURI> baseURI = BaseURIForPreload();
bool isImgSet = false;
nsCOMPtr<nsIURI> uri = mDocument->ResolvePreloadImage(baseURI, aURL, aSrcset, nsCOMPtr<nsIURI> uri = mDocument->ResolvePreloadImage(baseURI, aURL, aSrcset,
aSizes); aSizes, &isImgSet);
if (uri && ShouldPreloadURI(uri)) { if (uri && ShouldPreloadURI(uri)) {
// use document wide referrer policy // use document wide referrer policy
mozilla::net::ReferrerPolicy referrerPolicy = mSpeculationReferrerPolicy; 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);
} }
} }