// Copyright 2016 wkh237@github. All rights reserved. // Use of this source code is governed by a MIT-style license that can be // found in the LICENSE file. import RNFetchBlob from '../index.js' import XMLHttpRequestEventTarget from './XMLHttpRequestEventTarget.js' import Log from '../utils/log.js' import Blob from './Blob.js' import ProgressEvent from './ProgressEvent.js' import URIUtil from '../utils/uri' const log = new Log('XMLHttpRequest') log.disable() // log.level(3) const UNSENT = 0 const OPENED = 1 const HEADERS_RECEIVED = 2 const LOADING = 3 const DONE = 4 export default class XMLHttpRequest extends XMLHttpRequestEventTarget{ _onreadystatechange : () => void; upload : XMLHttpRequestEventTarget = new XMLHttpRequestEventTarget(); static binaryContentTypes : Array = [ 'image/', 'video/', 'audio/' ]; // readonly _readyState : number = UNSENT; _uriType : 'net' | 'file' = 'net'; _response : any = ''; _responseText : any = ''; _responseHeaders : any = {}; _responseType : '' | 'arraybuffer' | 'blob' | 'json' | 'text' = ''; // TODO : not suppoted ATM _responseURL : null = ''; _responseXML : null = ''; _status : number = 0; _statusText : string = ''; _timeout : number = 60000; _sendFlag : boolean = false; _uploadStarted : boolean = false; _increment : boolean = false; // RNFetchBlob compatible data structure _config : RNFetchBlobConfig = {}; _url : any; _method : string; _headers: any = { 'Content-Type' : 'text/plain' }; _cleanUp : () => void = null; _body: any; // RNFetchBlob promise object, which has `progress`, `uploadProgress`, and // `cancel` methods. _task: any; // constants get UNSENT() { return UNSENT } get OPENED() { return OPENED } get HEADERS_RECEIVED() { return HEADERS_RECEIVED } get LOADING() { return LOADING } get DONE() { return DONE } static get UNSENT() { return UNSENT } static get OPENED() { return OPENED } static get HEADERS_RECEIVED() { return HEADERS_RECEIVED } static get LOADING() { return LOADING } static get DONE() { return DONE } static setLog(level:number) { if(level === -1) log.disable() else log.level(level) } static addBinaryContentType(substr:string) { for(let i in XMLHttpRequest.binaryContentTypes) { if(new RegExp(substr,'i').test(XMLHttpRequest.binaryContentTypes[i])) { return } } XMLHttpRequest.binaryContentTypes.push(substr) } static removeBinaryContentType(val) { for(let i in XMLHttpRequest.binaryContentTypes) { if(new RegExp(substr,'i').test(XMLHttpRequest.binaryContentTypes[i])) { XMLHttpRequest.binaryContentTypes.splice(i,1) return } } } constructor() { log.verbose('XMLHttpRequest constructor called') super() } /** * XMLHttpRequest.open, always async, user and password not supported. When * this method invoked, headers should becomes empty again. * @param {string} method Request method * @param {string} url Request URL * @param {true} async Always async * @param {any} user NOT SUPPORTED * @param {any} password NOT SUPPORTED */ open(method:string, url:string, async:true, user:any, password:any) { log.verbose('XMLHttpRequest open ', method, url, async, user, password) this._method = method this._url = url this._headers = {} this._increment = URIUtil.isJSONStreamURI(this._url) this._url = this._url.replace(/^JSONStream\:\/\//, '') this._dispatchReadStateChange(XMLHttpRequest.OPENED) } /** * Invoke this function to send HTTP request, and set body. * @param {any} body Body in RNfetchblob flavor */ send(body) { this._body = body if(this._readyState !== XMLHttpRequest.OPENED) throw 'InvalidStateError : XMLHttpRequest is not opened yet.' let promise = Promise.resolve() this._sendFlag = true log.verbose('XMLHttpRequest send ', body) let {_method, _url, _headers } = this log.verbose('sending request with args', _method, _url, _headers, body) log.verbose(typeof body, body instanceof FormData) if(body instanceof Blob) { log.debug('sending blob body', body._blobCreated) promise = new Promise((resolve, reject) => { body.onCreated((blob) => { // when the blob is derived (not created by RN developer), the blob // will be released after XMLHttpRequest sent if(blob.isDerived) { this._cleanUp = () => { blob.close() } } log.debug('body created send request') body = RNFetchBlob.wrap(blob.getRNFetchBlobRef()) resolve() }) }) } else if(typeof body === 'object') { body = JSON.stringify(body) promise = Promise.resolve() } else { body = body ? body.toString() : body promise = Promise.resolve() } promise.then(() => { log.debug('send request invoke', body) for(let h in _headers) { _headers[h] = _headers[h].toString() } this._task = RNFetchBlob .config({ auto: true, timeout : this._timeout, increment : this._increment, binaryContentTypes : XMLHttpRequest.binaryContentTypes }) .fetch(_method, _url, _headers, body) this._task .stateChange(this._headerReceived) .uploadProgress(this._uploadProgressEvent) .progress(this._progressEvent) .catch(this._onError) .then(this._onDone) }) } overrideMimeType(mime:string) { log.verbose('XMLHttpRequest overrideMimeType', mime) this._headers['Content-Type'] = mime } setRequestHeader(name, value) { log.verbose('XMLHttpRequest set header', name, value) if(this._readyState !== OPENED || this._sendFlag) { throw `InvalidStateError : Calling setRequestHeader in wrong state ${this._readyState}` } // UNICODE SHOULD NOT PASS if(typeof name !== 'string' || /[^\u0000-\u00ff]/.test(name)) { throw 'TypeError : header field name should be a string' } // let invalidPatterns = [ /[\(\)\>\<\@\,\:\\\/\[\]\?\=\}\{\s\ \u007f\;\t\0\v\r]/, /tt/ ] for(let pattern of invalidPatterns) { if(pattern.test(name) || typeof name !== 'string') { throw `SyntaxError : Invalid header field name ${name}` } } this._headers[name] = value } abort() { log.verbose('XMLHttpRequest abort ') if(!this._task) return this._task.cancel((err) => { let e = { timeStamp : Date.now(), } if(this.onabort) this.onabort() if(err) { e.detail = err e.type = 'error' this.dispatchEvent('error', e) } else { e.type = 'abort' this.dispatchEvent('abort', e) } }) } getResponseHeader(field:string):string | null { log.verbose('XMLHttpRequest get header', field, this._responseHeaders) if(!this._responseHeaders) return null return (this._responseHeaders[field] || this._responseHeaders[field.toLowerCase()]) || null } getAllResponseHeaders():string | null { log.verbose('XMLHttpRequest get all headers', this._responseHeaders) if(!this._responseHeaders) return '' let result = '' let respHeaders = this.responseHeaders for(let i in respHeaders) { result += `${i}: ${respHeaders[i]}${String.fromCharCode(0x0D,0x0A)}` } return result.substr(0, result.length-2) } _headerReceived = (e) => { log.debug('header received ', this._task.taskId, e) this.responseURL = this._url if(e.state === "2" && e.taskId === this._task.taskId) { this._responseHeaders = e.headers this._statusText = e.status this._status = Math.floor(e.status) this._dispatchReadStateChange(XMLHttpRequest.HEADERS_RECEIVED) } } _uploadProgressEvent = (send:number, total:number) => { if(!this._uploadStarted) { this.upload.dispatchEvent('loadstart') this._uploadStarted = true } if(send >= total) this.upload.dispatchEvent('load') this.upload.dispatchEvent('progress', new ProgressEvent(true, send, total)) } _progressEvent = (send:number, total:number, chunk:string) => { log.verbose(this.readyState) if(this._readyState === XMLHttpRequest.HEADERS_RECEIVED) this._dispatchReadStateChange(XMLHttpRequest.LOADING) let lengthComputable = false if(total && total >= 0) lengthComputable = true let e = new ProgressEvent(lengthComputable, send, total) if(this._increment) { this._responseText += chunk } this.dispatchEvent('progress', e) } _onError = (err) => { let statusCode = Math.floor(this.status) if(statusCode >= 100 && statusCode !== 408) { return } log.debug('XMLHttpRequest error', err) this._statusText = err this._status = String(err).match(/\d+/) this._status = this._status ? Math.floor(this.status) : 404 this._dispatchReadStateChange(XMLHttpRequest.DONE) if(err && String(err.message).match(/(timed\sout|timedout)/) || this._status == 408) { this.dispatchEvent('timeout') } this.dispatchEvent('loadend') this.dispatchEvent('error', { type : 'error', detail : err }) this.clearEventListeners() } _onDone = (resp) => { log.debug('XMLHttpRequest done', this._url, resp, this) this._statusText = this._status let responseDataReady = () => { log.debug('request done state = 4') this.dispatchEvent('load') this.dispatchEvent('loadend') this._dispatchReadStateChange(XMLHttpRequest.DONE) this.clearEventListeners() } if(resp) { let info = resp.respInfo || {} log.debug(this._url, info, info.respType) switch(this._responseType) { case 'blob' : resp.blob().then((b) => { this._responseText = resp.text() this._response = b responseDataReady() }) break; case 'arraybuffer': // TODO : to array buffer break case 'json': this._response = resp.json() this._responseText = resp.text() break default : this._responseText = resp.text() this._response = this.responseText responseDataReady() break; } } } _dispatchReadStateChange(state) { this._readyState = state if(typeof this._onreadystatechange === 'function') this._onreadystatechange() } set onreadystatechange(fn:() => void) { log.verbose('XMLHttpRequest set onreadystatechange', fn) this._onreadystatechange = fn } get onreadystatechange() { return this._onreadystatechange } get readyState() { log.verbose('get readyState', this._readyState) return this._readyState } get status() { log.verbose('get status', this._status) return this._status } get statusText() { log.verbose('get statusText', this._statusText) return this._statusText } get response() { log.verbose('get response', this._response) return this._response } get responseText() { log.verbose('get responseText', this._responseText) return this._responseText } get responseURL() { log.verbose('get responseURL', this._responseURL) return this._responseURL } get responseHeaders() { log.verbose('get responseHeaders', this._responseHeaders) return this._responseHeaders } set timeout(val) { this._timeout = val*1000 log.verbose('set timeout', this._timeout) } get timeout() { log.verbose('get timeout', this._timeout) return this._timeout } set responseType(val) { log.verbose('set response type', this._responseType) this._responseType = val } get responseType() { log.verbose('get response type', this._responseType) return this._responseType } static get isRNFBPolyfill() { return true } }