/** * @fileoverview Utilities for mixed-content in Web Platform Tests. * @author burnik@google.com (Kristijan Burnik) * Disclaimer: Some methods of other authors are annotated in the corresponding * method's JSDoc. */ function timeoutPromise(t, ms) { return new Promise(resolve => { t.step_timeout(resolve, ms); }); } /** * Normalizes the target port for use in a URL. For default ports, this is the * empty string (omitted port), otherwise it's a colon followed by the port * number. Ports 80, 443 and an empty string are regarded as default ports. * @param {number} targetPort The port to use * @return {string} The port portion for using as part of a URL. */ function getNormalizedPort(targetPort) { return ([80, 443, ""].indexOf(targetPort) >= 0) ? "" : ":" + targetPort; } /** * Creates a GUID. * See: https://en.wikipedia.org/wiki/Globally_unique_identifier * Original author: broofa (http://www.broofa.com/) * Sourced from: http://stackoverflow.com/a/2117523/4949715 * @return {string} A pseudo-random GUID. */ function guid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } /** * Initiates a new XHR via GET. * @param {string} url The endpoint URL for the XHR. * @param {string} responseType Optional - how should the response be parsed. * Default is "json". * See: https://xhr.spec.whatwg.org/#dom-xmlhttprequest-responsetype * @return {Promise} A promise wrapping the success and error events. */ function xhrRequest(url, responseType) { return new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = responseType || "json"; xhr.addEventListener("error", function() { reject(Error("Network Error")); }); xhr.addEventListener("load", function() { if (xhr.status != 200) reject(Error(xhr.statusText)); else resolve(xhr.response); }); xhr.send(); }); } /** * Sets attributes on a given DOM element. * @param {DOMElement} element The element on which to set the attributes. * @param {object} An object with keys (serving as attribute names) and values. */ function setAttributes(el, attrs) { attrs = attrs || {} for (var attr in attrs) el.setAttribute(attr, attrs[attr]); } /** * Binds to success and error events of an object wrapping them into a promise * available through {@code element.eventPromise}. The success event * resolves and error event rejects. * This method adds event listeners, and then removes all the added listeners * when one of listened event is fired. * @param {object} element An object supporting events on which to bind the * promise. * @param {string} resolveEventName [="load"] The event name to bind resolve to. * @param {string} rejectEventName [="error"] The event name to bind reject to. */ function bindEvents(element, resolveEventName, rejectEventName) { element.eventPromise = bindEvents2(element, resolveEventName, element, rejectEventName); } // Returns a promise wrapping success and error events of objects. // This is a variant of bindEvents that can accept separate objects for each // events and two events to reject, and doesn't set `eventPromise`. // // When `resolveObject`'s `resolveEventName` event (default: "load") is // fired, the promise is resolved with the event. // // When `rejectObject`'s `rejectEventName` event (default: "error") or // `rejectObject2`'s `rejectEventName2` event (default: "error") is // fired, the promise is rejected. // // `rejectObject2` is optional. function bindEvents2(resolveObject, resolveEventName, rejectObject, rejectEventName, rejectObject2, rejectEventName2) { return new Promise(function(resolve, reject) { const actualResolveEventName = resolveEventName || "load"; const actualRejectEventName = rejectEventName || "error"; const actualRejectEventName2 = rejectEventName2 || "error"; const resolveHandler = function(event) { cleanup(); resolve(event); }; const rejectHandler = function(event) { // Chromium starts propagating errors from worker.onerror to // window.onerror. This handles the uncaught exceptions in tests. event.preventDefault(); cleanup(); reject(event); }; const cleanup = function() { resolveObject.removeEventListener(actualResolveEventName, resolveHandler); rejectObject.removeEventListener(actualRejectEventName, rejectHandler); if (rejectObject2) { rejectObject2.removeEventListener(actualRejectEventName2, rejectHandler); } }; resolveObject.addEventListener(actualResolveEventName, resolveHandler); rejectObject.addEventListener(actualRejectEventName, rejectHandler); if (rejectObject2) { rejectObject2.addEventListener(actualRejectEventName2, rejectHandler); } }); } /** * Creates a new DOM element. * @param {string} tagName The type of the DOM element. * @param {object} attrs A JSON with attributes to apply to the element. * @param {DOMElement} parent Optional - an existing DOM element to append to * If not provided, the returned element will remain orphaned. * @param {boolean} doBindEvents Optional - Whether to bind to load and error * events and provide the promise wrapping the events via the element's * {@code eventPromise} property. Default value evaluates to false. * @return {DOMElement} The newly created DOM element. */ function createElement(tagName, attrs, parentNode, doBindEvents) { var element = document.createElement(tagName); if (doBindEvents) bindEvents(element); // We set the attributes after binding to events to catch any // event-triggering attribute changes. E.g. form submission. // // But be careful with images: unlike other elements they will start the load // as soon as the attr is set, even if not in the document yet, and sometimes // complete it synchronously, so the append doesn't have the effect we want. // So for images, we want to set the attrs after appending, whereas for other // elements we want to do it before appending. var isImg = (tagName == "img"); if (!isImg) setAttributes(element, attrs); if (parentNode) parentNode.appendChild(element); if (isImg) setAttributes(element, attrs); return element; } function createRequestViaElement(tagName, attrs, parentNode) { return createElement(tagName, attrs, parentNode, true).eventPromise; } /** * Creates a new empty iframe and appends it to {@code document.body} . * @param {string} name The name and ID of the new iframe. * @param {boolean} doBindEvents Whether to bind load and error events. * @return {DOMElement} The newly created iframe. */ function createHelperIframe(name, doBindEvents) { return createElement("iframe", {"name": name, "id": name}, document.body, doBindEvents); } /** * requestVia*() functions return promises that are resolved on successful * requests with objects of the same "type", i.e. objects that contains * the same sets of keys that are fixed within one category of tests (e.g. * within wpt/referrer-policy tests). * wrapResult() (that should be defined outside this file) is used to convert * the response bodies of subresources into the expected result objects in some * cases, and in other cases the result objects are constructed more directly. * TODO(https://crbug.com/906850): Clean up the semantics around this, e.g. * use (or not use) wrapResult() consistently, unify the arguments, etc. */ /** * Creates a new iframe, binds load and error events, sets the src attribute and * appends it to {@code document.body} . * @param {string} url The src for the iframe. * @return {Promise} The promise for success/error events. */ function requestViaIframe(url, additionalAttributes) { const iframe = createElement( "iframe", Object.assign({"src": url}, additionalAttributes), document.body, false); return bindEvents2(window, "message", iframe, "error", window, "error") .then(event => { assert_equals(event.source, iframe.contentWindow); return event.data; }); } /** * Creates a new image, binds load and error events, sets the src attribute and * appends it to {@code document.body} . * @param {string} url The src for the image. * @return {Promise} The promise for success/error events. */ function requestViaImage(url) { return createRequestViaElement("img", {"src": url}, document.body); } // Helpers for requestViaImageForReferrerPolicy(). function loadImageInWindow(src, attributes, w) { return new Promise((resolve, reject) => { var image = new w.Image(); image.crossOrigin = "Anonymous"; image.onload = function() { resolve(image); }; // Extend element with attributes. (E.g. "referrerPolicy" or "rel") if (attributes) { for (var attr in attributes) { image[attr] = attributes[attr]; } } image.src = src; w.document.body.appendChild(image) }); } function extractImageData(img) { var canvas = document.createElement("canvas"); var context = canvas.getContext('2d'); context.drawImage(img, 0, 0); var imgData = context.getImageData(0, 0, img.clientWidth, img.clientHeight); return imgData.data; } function decodeImageData(rgba) { let decodedBytes = new Uint8ClampedArray(rgba.length); let decodedLength = 0; for (var i = 0; i + 12 <= rgba.length; i += 12) { // A single byte is encoded in three pixels. 8 pixel octets (among // 9 octets = 3 pixels * 3 channels) are used to encode 8 bits, // the most significant bit first, where `0` and `255` in pixel values // represent `0` and `1` in bits, respectively. // This encoding is used to avoid errors due to different color spaces. const bits = []; for (let j = 0; j < 3; ++j) { bits.push(rgba[i + j * 4 + 0]); bits.push(rgba[i + j * 4 + 1]); bits.push(rgba[i + j * 4 + 2]); // rgba[i + j * 4 + 3]: Skip alpha channel. } // The last one element is not used. bits.pop(); // Decode a single byte. let byte = 0; for (let j = 0; j < 8; ++j) { byte <<= 1; if (bits[j] >= 128) byte |= 1; } // Zero is the string terminator. if (byte == 0) break; decodedBytes[decodedLength++] = byte; } // Remove trailing nulls from data. decodedBytes = decodedBytes.subarray(0, decodedLength); var string_data = (new TextDecoder("ascii")).decode(decodedBytes); return JSON.parse(string_data); } // A variant of requestViaImage for referrer policy tests. // This tests many patterns of