Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix The onreadystatechange property of XMLHttpRequest.prototype redefined by Angular (close #1283) #1287

Merged
merged 8 commits into from
Sep 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 41 additions & 41 deletions src/client/sandbox/xhr.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as JSON from '../json';

const IS_OPENED_XHR = 'hammerhead|xhr|is-opened-xhr';
const REMOVE_SET_COOKIE_HH_HEADER = new RegExp(`${ reEscape(XHR_HEADERS.setCookie) }:[^\n]*\n`, 'gi');
const XHR_READY_STATES = ['UNSENT', 'OPENED', 'HEADERS_RECEIVED', 'LOADING', 'DONE'];

export default class XhrSandbox extends SandboxBase {
constructor (cookieSandbox) {
Expand Down Expand Up @@ -54,10 +55,18 @@ export default class XhrSandbox extends SandboxBase {
attach (window) {
super.attach(window);

const xhrSandbox = this;
const xmlHttpRequestProto = window.XMLHttpRequest.prototype;
const xhrSandbox = this;
const xmlHttpRequestProto = window.XMLHttpRequest.prototype;
const xmlHttpRequestToString = nativeMethods.XMLHttpRequest.toString();

const syncCookieWithClient = function () {
const emitXhrCompletedEventIfNecessary = function () {
if (this.readyState === this.DONE) {
xhrSandbox.emit(xhrSandbox.XHR_COMPLETED_EVENT, { xhr: this });
nativeMethods.xhrRemoveEventListener.call(this, 'readystatechange', emitXhrCompletedEventIfNecessary);
}
};

const syncCookieWithClientIfNecessary = function () {
if (this.readyState < this.HEADERS_RECEIVED)
return;

Expand All @@ -70,9 +79,34 @@ export default class XhrSandbox extends SandboxBase {
xhrSandbox.cookieSandbox.setCookie(window.document, cookie);
}

nativeMethods.xhrRemoveEventListener.call(this, 'readystatechange', syncCookieWithClient);
nativeMethods.xhrRemoveEventListener.call(this, 'readystatechange', syncCookieWithClientIfNecessary);
};

const xmlHttpRequestWrapper = function () {
const xhr = new nativeMethods.XMLHttpRequest();

nativeMethods.xhrAddEventListener.call(xhr, 'readystatechange', emitXhrCompletedEventIfNecessary);
nativeMethods.xhrAddEventListener.call(xhr, 'readystatechange', syncCookieWithClientIfNecessary);

return xhr;
};

for (const readyState of XHR_READY_STATES) {
nativeMethods.objectDefineProperty.call(window.Object, xmlHttpRequestWrapper, readyState, {
value: XMLHttpRequest[readyState],
enumerable: true
});
}

window.XMLHttpRequest = xmlHttpRequestWrapper;
xmlHttpRequestWrapper.prototype = xmlHttpRequestProto;
xmlHttpRequestWrapper.toString = () => xmlHttpRequestToString;

// NOTE: We cannot just assign constructor property in OS X 10.11 safari 9.0
nativeMethods.objectDefineProperty.call(window.Object, xmlHttpRequestProto, 'constructor', {
value: xmlHttpRequestWrapper
});

xmlHttpRequestProto.abort = function () {
nativeMethods.xhrAbort.apply(this, arguments);
xhrSandbox.emit(xhrSandbox.XHR_ERROR_EVENT, {
Expand All @@ -94,52 +128,17 @@ export default class XhrSandbox extends SandboxBase {
if (typeof arguments[1] === 'string')
arguments[1] = getProxyUrl(arguments[1]);

nativeMethods.xhrAddEventListener.call(this, 'readystatechange', syncCookieWithClient);
nativeMethods.xhrOpen.apply(this, arguments);
};

xmlHttpRequestProto.send = function () {
const xhr = this;

xhrSandbox.emit(xhrSandbox.BEFORE_XHR_SEND_EVENT, { xhr });

const orscHandler = () => {
if (this.readyState === 4)
xhrSandbox.emit(xhrSandbox.XHR_COMPLETED_EVENT, { xhr });
};

// NOTE: If we're using the sync mode or if the response is in cache,
// we need to raise the callback manually.
if (this.readyState === 4)
orscHandler();
else {
// NOTE: Get out of the current execution tick and then proxy onreadystatechange,
// because jQuery assigns a handler after the send() method was called.
nativeMethods.setTimeout.call(xhrSandbox.window, () => {
// NOTE: If the state is already changed, we just call the handler without proxying
// onreadystatechange.
if (this.readyState === 4)
orscHandler();

else if (typeof this.onreadystatechange === 'function') {
const originalHandler = this.onreadystatechange;

this.onreadystatechange = progress => {
orscHandler();
originalHandler.call(this, progress);
};
}
else
this.addEventListener('readystatechange', orscHandler, false);
}, 0);
}
xhrSandbox.emit(xhrSandbox.BEFORE_XHR_SEND_EVENT, { xhr: this });

// NOTE: Add the XHR request mark, so that a proxy can recognize a request as a XHR request. As all
// requests are passed to the proxy, we need to perform Same Origin Policy compliance checks on the
// server side. So, we pass the CORS support flag to inform the proxy that it can analyze the
// Access-Control_Allow_Origin flag and skip "preflight" requests.
nativeMethods.xhrSetRequestHeader.call(this, XHR_HEADERS.requestMarker, 'true');

nativeMethods.xhrSetRequestHeader.call(this, XHR_HEADERS.origin, getOriginHeader());

if (xhrSandbox.corsSupported)
Expand All @@ -151,7 +150,8 @@ export default class XhrSandbox extends SandboxBase {
nativeMethods.xhrSend.apply(this, arguments);

// NOTE: For xhr with the sync mode
syncCookieWithClient.call(this);
emitXhrCompletedEventIfNecessary.call(this);
syncCookieWithClientIfNecessary.call(this);
};

xmlHttpRequestProto.getResponseHeader = function (name) {
Expand Down
45 changes: 43 additions & 2 deletions test/client/fixtures/sandbox/xhr-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var settings = hammerhead.get('./settings');
var iframeSandbox = hammerhead.sandbox.iframe;
var browserUtils = hammerhead.utils.browser;
var nativeMethods = hammerhead.nativeMethods;
var xhrSandbox = hammerhead.sandbox.xhr;

QUnit.testStart(function () {
iframeSandbox.on(iframeSandbox.RUN_TASK_SCRIPT_EVENT, initIframeTestHandler);
Expand Down Expand Up @@ -43,13 +44,16 @@ test('redirect requests to proxy', function () {
});

test('createNativeXHR', function () {
window.XMLHttpRequest = function () {};
var storedXMLHttpRequest = window.XMLHttpRequest;

window.XMLHttpRequest = function () {
};

var xhr = XhrSandbox.createNativeXHR();

ok(xhr instanceof nativeMethods.XMLHttpRequest);

window.XMLHttpRequest = nativeMethods.XMLHttpRequest;
window.XMLHttpRequest = storedXMLHttpRequest;

var isWrappedFunctionRE = /return 'function is wrapped'/;

Expand All @@ -69,6 +73,19 @@ test('createNativeXHR', function () {
}
});

test('toString, instanceof, constructor and static properties', function () {
var xhr = new XMLHttpRequest();

strictEqual(XMLHttpRequest.toString(), nativeMethods.XMLHttpRequest.toString());
ok(xhr instanceof XMLHttpRequest);
strictEqual(XMLHttpRequest.prototype.constructor, XMLHttpRequest);
strictEqual(XMLHttpRequest.UNSENT, nativeMethods.XMLHttpRequest.UNSENT);
strictEqual(XMLHttpRequest.OPENED, nativeMethods.XMLHttpRequest.OPENED);
strictEqual(XMLHttpRequest.HEADERS_RECEIVED, nativeMethods.XMLHttpRequest.HEADERS_RECEIVED);
strictEqual(XMLHttpRequest.LOADING, nativeMethods.XMLHttpRequest.LOADING);
strictEqual(XMLHttpRequest.DONE, nativeMethods.XMLHttpRequest.DONE);
});

module('regression');

asyncTest('unexpected text modifying during typing text in the search input on the http://www.google.co.uk (B238528)', function () {
Expand Down Expand Up @@ -201,3 +218,27 @@ asyncTest('authorization headers by client should be processed (GH-1016)', funct
});
xhr.send();
});

asyncTest('"XHR_COMPLETED_EVENT" should be raised when xhr is prevented (GH-1283)', function () {
var xhr = new XMLHttpRequest();
var timeoutId = null;
var testDone = function (eventObj) {
clearTimeout(timeoutId);
ok(!!eventObj);
start();
};

var readyStateChangeHandler = function (e) {
if (this.readyState === this.DONE)
e.stopImmediatePropagation();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add ok(false, 'outer handler is called before XHR_COMPLETED_EVENT');

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this handler will be called in all cases

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

};

xhr.addEventListener('readystatechange', readyStateChangeHandler, true);
xhr.onreadystatechange = readyStateChangeHandler;

xhrSandbox.on(xhrSandbox.XHR_COMPLETED_EVENT, testDone);
timeoutId = setTimeout(testDone, 2000);

xhr.open('GET', '/xhr-test/', true);
xhr.send();
});