From 8084572c4dcebd12983e28d07bb0a33e021d9580 Mon Sep 17 00:00:00 2001
From: OJ Kwon <kwon.ohjoong@gmail.com>
Date: Thu, 9 Jun 2016 10:39:38 -0700
Subject: [PATCH] fix(AjaxObservable): support withCredentials for CORS request

- AjaxRequest now support setting withCredentials flag

closes #1732, #1711
---
 spec/helpers/ajax-helper.ts          |  1 +
 spec/observables/dom/ajax-spec.ts    | 93 ++++++++++++++++++++++++++++
 spec/support/default.opts            |  2 +-
 src/observable/dom/AjaxObservable.ts | 50 +++++++++++----
 4 files changed, 132 insertions(+), 14 deletions(-)

diff --git a/spec/helpers/ajax-helper.ts b/spec/helpers/ajax-helper.ts
index 3d32f5d01a..244a6e345d 100644
--- a/spec/helpers/ajax-helper.ts
+++ b/spec/helpers/ajax-helper.ts
@@ -133,6 +133,7 @@ export class MockXMLHttpRequest {
   method: any;
   data: any;
   requestHeaders: any = {};
+  withCredentials: boolean = false;
 
   constructor() {
     this.previousRequest = MockXMLHttpRequest.recentRequest;
diff --git a/spec/observables/dom/ajax-spec.ts b/spec/observables/dom/ajax-spec.ts
index 5782b983ed..2849c5b5a4 100644
--- a/spec/observables/dom/ajax-spec.ts
+++ b/spec/observables/dom/ajax-spec.ts
@@ -1,4 +1,5 @@
 import {expect} from 'chai';
+import * as sinon from 'sinon';
 import * as Rx from '../../../dist/cjs/Rx';
 import {root} from '../../../dist/cjs/util/root';
 import {MockXMLHttpRequest} from '../../helpers/ajax-helper';
@@ -10,18 +11,110 @@ describe('Observable.ajax', () => {
   let gXHR: XMLHttpRequest;
   let rXHR: XMLHttpRequest;
 
+  let sandbox: sinon.SinonSandbox;
+
   beforeEach(() => {
+    sandbox = sinon.sandbox.create();
     gXHR = global.XMLHttpRequest;
     rXHR = root.XMLHttpRequest;
+
     global.XMLHttpRequest = MockXMLHttpRequest;
     root.XMLHttpRequest = MockXMLHttpRequest;
   });
 
   afterEach(() => {
+    sandbox.restore();
     MockXMLHttpRequest.clearRequest();
 
     global.XMLHttpRequest = gXHR;
     root.XMLHttpRequest = rXHR;
+
+    root.XDomainRequest = null;
+    root.ActiveXObject = null;
+  });
+
+  it('should create default XMLHttpRequest for non CORS', () => {
+    const obj: Rx.AjaxRequest = {
+      url: '/',
+      method: ''
+    };
+
+    Rx.Observable.ajax(obj).subscribe();
+    expect(MockXMLHttpRequest.mostRecent.withCredentials).to.be.false;
+  });
+
+  it('should try to create AXObject for XHR in old version of IE', () => {
+    const axObjectStub = sandbox.stub();
+    axObjectStub.returns(sinon.stub(new MockXMLHttpRequest()));
+    root.ActiveXObject = axObjectStub;
+    root.XMLHttpRequest = null;
+
+    const obj: Rx.AjaxRequest = {
+      url: '/',
+      method: ''
+    };
+
+    Rx.Observable.ajax(obj).subscribe();
+    expect(axObjectStub).to.have.been.called;
+  });
+
+  it('should throw if not able to create XMLHttpRequest', () => {
+    root.XMLHttpRequest = null;
+    root.ActiveXObject = null;
+
+    const obj: Rx.AjaxRequest = {
+      url: '/',
+      method: ''
+    };
+
+    expect(() => {
+      Rx.Observable.ajax(obj).subscribe();
+    }).to.throw();
+  });
+
+  it('should create XMLHttpRequest for CORS', () => {
+    const obj: Rx.AjaxRequest = {
+      url: '/',
+      method: '',
+      crossDomain: true,
+      withCredentials: true
+    };
+
+    Rx.Observable.ajax(obj).subscribe();
+    expect(MockXMLHttpRequest.mostRecent.withCredentials).to.be.true;
+  });
+
+  it('should try to create XDomainRequest for CORS if XMLHttpRequest is not available', () => {
+    const xDomainStub = sandbox.stub();
+    xDomainStub.returns(sinon.stub(new MockXMLHttpRequest()));
+    root.XDomainRequest = xDomainStub;
+    root.XMLHttpRequest = null;
+
+    const obj: Rx.AjaxRequest = {
+      url: '/',
+      method: '',
+      crossDomain: true,
+      withCredentials: true
+    };
+
+    Rx.Observable.ajax(obj).subscribe();
+    expect(xDomainStub).to.have.been.called;
+  });
+
+  it('should throw if not able to create CORS request', () => {
+    root.XMLHttpRequest = null;
+    root.XDomainRequest = null;
+
+    const obj: Rx.AjaxRequest = {
+      url: '/',
+      method: '',
+      crossDomain: true,
+      withCredentials: true
+    };
+
+    expect(() => {
+      Rx.Observable.ajax(obj).subscribe();
+    }).to.throw();
   });
 
   it('should set headers', () => {
diff --git a/spec/support/default.opts b/spec/support/default.opts
index 287be14820..04406230fa 100644
--- a/spec/support/default.opts
+++ b/spec/support/default.opts
@@ -8,7 +8,7 @@
 --bail
 --full-trace
 --check-leaks
---globals WebSocket,FormData
+--globals WebSocket,FormData,XDomainRequest,ActiveXObject
 
 --recursive
 --timeout 5000
diff --git a/src/observable/dom/AjaxObservable.ts b/src/observable/dom/AjaxObservable.ts
index 7cd7922da8..87da9f89f1 100644
--- a/src/observable/dom/AjaxObservable.ts
+++ b/src/observable/dom/AjaxObservable.ts
@@ -16,25 +16,48 @@ export interface AjaxRequest {
   password?: string;
   hasContent?: boolean;
   crossDomain?: boolean;
+  withCredentials?: boolean;
   createXHR?: () => XMLHttpRequest;
   progressSubscriber?: Subscriber<any>;
   resultSelector?: <T>(response: AjaxResponse) => T;
   responseType?: string;
 }
 
-function createXHRDefault(): XMLHttpRequest {
-  let xhr = new root.XMLHttpRequest();
-  if (this.crossDomain) {
+function getCORSRequest(): XMLHttpRequest {
+  if (root.XMLHttpRequest) {
+    const xhr = new root.XMLHttpRequest();
     if ('withCredentials' in xhr) {
-      xhr.withCredentials = true;
-      return xhr;
-    } else if (!!root.XDomainRequest) {
-      return new root.XDomainRequest();
-    } else {
-      throw new Error('CORS is not supported by your browser');
+      xhr.withCredentials = !!this.withCredentials;
     }
-  } else {
     return xhr;
+  } else if (!!root.XDomainRequest) {
+    return new root.XDomainRequest();
+  } else {
+    throw new Error('CORS is not supported by your browser');
+  }
+}
+
+function getXMLHttpRequest(): XMLHttpRequest {
+  if (root.XMLHttpRequest) {
+    return new root.XMLHttpRequest();
+  } else {
+    let progId: string;
+    try {
+      const progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'];
+      for (let i = 0; i < 3; i++) {
+        try {
+          progId = progIds[i];
+          if (new root.ActiveXObject(progId)) {
+            break;
+          }
+        } catch (e) {
+          //suppress exceptions
+        }
+      }
+      return new root.ActiveXObject(progId);
+    } catch (e) {
+      throw new Error('XMLHttpRequest is not supported by your browser');
+    }
   }
 }
 
@@ -104,8 +127,6 @@ export class AjaxObservable<T> extends Observable<T> {
    * @name ajax
    * @owner Observable
   */
-  static _create_stub(): void { return null; }
-
   static create: AjaxCreationMethod = (() => {
     const create: any = (urlOrRequest: string | AjaxRequest) => {
       return new AjaxObservable(urlOrRequest);
@@ -127,8 +148,11 @@ export class AjaxObservable<T> extends Observable<T> {
 
     const request: AjaxRequest = {
       async: true,
-      createXHR: createXHRDefault,
+      createXHR: function() {
+        return this.crossDomain ? getCORSRequest.call(this) : getXMLHttpRequest();
+      },
       crossDomain: false,
+      withCredentials: false,
       headers: {},
       method: 'GET',
       responseType: 'json',