Skip to content

Commit

Permalink
fix `The onreadystatechange property of XMLHttpRequest.prototype rede…
Browse files Browse the repository at this point in the history
…fined by Angular` (close DevExpress#1283) (DevExpress#1287)

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

* fix

* fix review's issues

* some fix

* refactoring

* refactoring

* fix review's issues

* refactoring
  • Loading branch information
LavrovArtem authored and AndreyBelym committed Feb 28, 2019
1 parent 65e5dfb commit 04c7255
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 43 deletions.
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();
};

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();
});

0 comments on commit 04c7255

Please sign in to comment.