Nav apraksta

XMLHttpRequest.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. // Copyright 2016 wkh237@github. All rights reserved.
  2. // Use of this source code is governed by a MIT-style license that can be
  3. // found in the LICENSE file.
  4. import RNFetchBlob from '../index.js'
  5. import XMLHttpRequestEventTarget from './XMLHttpRequestEventTarget.js'
  6. import Log from '../utils/log.js'
  7. import Blob from './Blob.js'
  8. import ProgressEvent from './ProgressEvent.js'
  9. import URIUtil from '../utils/uri'
  10. const log = new Log('XMLHttpRequest')
  11. log.disable()
  12. // log.level(3)
  13. const UNSENT = 0
  14. const OPENED = 1
  15. const HEADERS_RECEIVED = 2
  16. const LOADING = 3
  17. const DONE = 4
  18. export default class XMLHttpRequest extends XMLHttpRequestEventTarget{
  19. _onreadystatechange : () => void;
  20. upload : XMLHttpRequestEventTarget = new XMLHttpRequestEventTarget();
  21. static binaryContentTypes : Array<string> = [
  22. 'image/', 'video/', 'audio/'
  23. ];
  24. // readonly
  25. _readyState : number = UNSENT;
  26. _uriType : 'net' | 'file' = 'net';
  27. _response : any = '';
  28. _responseText : any = '';
  29. _responseHeaders : any = {};
  30. _responseType : '' | 'arraybuffer' | 'blob' | 'json' | 'text' = '';
  31. // TODO : not suppoted ATM
  32. _responseURL : null = '';
  33. _responseXML : null = '';
  34. _status : number = 0;
  35. _statusText : string = '';
  36. _timeout : number = 60000;
  37. _sendFlag : boolean = false;
  38. _uploadStarted : boolean = false;
  39. _increment : boolean = false;
  40. // RNFetchBlob compatible data structure
  41. _config : RNFetchBlobConfig = {};
  42. _url : any;
  43. _method : string;
  44. _headers: any = {
  45. 'Content-Type' : 'text/plain'
  46. };
  47. _cleanUp : () => void = null;
  48. _body: any;
  49. // RNFetchBlob promise object, which has `progress`, `uploadProgress`, and
  50. // `cancel` methods.
  51. _task: any;
  52. // constants
  53. get UNSENT() { return UNSENT }
  54. get OPENED() { return OPENED }
  55. get HEADERS_RECEIVED() { return HEADERS_RECEIVED }
  56. get LOADING() { return LOADING }
  57. get DONE() { return DONE }
  58. static get UNSENT() {
  59. return UNSENT
  60. }
  61. static get OPENED() {
  62. return OPENED
  63. }
  64. static get HEADERS_RECEIVED() {
  65. return HEADERS_RECEIVED
  66. }
  67. static get LOADING() {
  68. return LOADING
  69. }
  70. static get DONE() {
  71. return DONE
  72. }
  73. static setLog(level:number) {
  74. if(level === -1)
  75. log.disable()
  76. else
  77. log.level(level)
  78. }
  79. static addBinaryContentType(substr:string) {
  80. for(let i in XMLHttpRequest.binaryContentTypes) {
  81. if(new RegExp(substr,'i').test(XMLHttpRequest.binaryContentTypes[i])) {
  82. return
  83. }
  84. }
  85. XMLHttpRequest.binaryContentTypes.push(substr)
  86. }
  87. static removeBinaryContentType(val) {
  88. for(let i in XMLHttpRequest.binaryContentTypes) {
  89. if(new RegExp(substr,'i').test(XMLHttpRequest.binaryContentTypes[i])) {
  90. XMLHttpRequest.binaryContentTypes.splice(i,1)
  91. return
  92. }
  93. }
  94. }
  95. constructor() {
  96. log.verbose('XMLHttpRequest constructor called')
  97. super()
  98. }
  99. /**
  100. * XMLHttpRequest.open, always async, user and password not supported. When
  101. * this method invoked, headers should becomes empty again.
  102. * @param {string} method Request method
  103. * @param {string} url Request URL
  104. * @param {true} async Always async
  105. * @param {any} user NOT SUPPORTED
  106. * @param {any} password NOT SUPPORTED
  107. */
  108. open(method:string, url:string, async:true, user:any, password:any) {
  109. log.verbose('XMLHttpRequest open ', method, url, async, user, password)
  110. this._method = method
  111. this._url = url
  112. this._headers = {}
  113. this._increment = URIUtil.isJSONStreamURI(this._url)
  114. this._url = this._url.replace(/^JSONStream\:\/\//, '')
  115. this._dispatchReadStateChange(XMLHttpRequest.OPENED)
  116. }
  117. /**
  118. * Invoke this function to send HTTP request, and set body.
  119. * @param {any} body Body in RNfetchblob flavor
  120. */
  121. send(body) {
  122. this._body = body
  123. if(this._readyState !== XMLHttpRequest.OPENED)
  124. throw 'InvalidStateError : XMLHttpRequest is not opened yet.'
  125. let promise = Promise.resolve()
  126. this._sendFlag = true
  127. log.verbose('XMLHttpRequest send ', body)
  128. let {_method, _url, _headers } = this
  129. log.verbose('sending request with args', _method, _url, _headers, body)
  130. log.verbose(typeof body, body instanceof FormData)
  131. if(body instanceof Blob) {
  132. log.debug('sending blob body', body._blobCreated)
  133. promise = new Promise((resolve, reject) => {
  134. body.onCreated((blob) => {
  135. // when the blob is derived (not created by RN developer), the blob
  136. // will be released after XMLHttpRequest sent
  137. if(blob.isDerived) {
  138. this._cleanUp = () => {
  139. blob.close()
  140. }
  141. }
  142. log.debug('body created send request')
  143. body = RNFetchBlob.wrap(blob.getRNFetchBlobRef())
  144. resolve()
  145. })
  146. })
  147. }
  148. else if(typeof body === 'object') {
  149. body = JSON.stringify(body)
  150. promise = Promise.resolve()
  151. }
  152. else {
  153. body = body ? body.toString() : body
  154. promise = Promise.resolve()
  155. }
  156. promise.then(() => {
  157. log.debug('send request invoke', body)
  158. for(let h in _headers) {
  159. _headers[h] = _headers[h].toString()
  160. }
  161. this._task = RNFetchBlob
  162. .config({
  163. auto: true,
  164. timeout : this._timeout,
  165. increment : this._increment,
  166. binaryContentTypes : XMLHttpRequest.binaryContentTypes
  167. })
  168. .fetch(_method, _url, _headers, body)
  169. this._task
  170. .stateChange(this._headerReceived)
  171. .uploadProgress(this._uploadProgressEvent)
  172. .progress(this._progressEvent)
  173. .catch(this._onError)
  174. .then(this._onDone)
  175. })
  176. }
  177. overrideMimeType(mime:string) {
  178. log.verbose('XMLHttpRequest overrideMimeType', mime)
  179. this._headers['Content-Type'] = mime
  180. }
  181. setRequestHeader(name, value) {
  182. log.verbose('XMLHttpRequest set header', name, value)
  183. if(this._readyState !== OPENED || this._sendFlag) {
  184. throw `InvalidStateError : Calling setRequestHeader in wrong state ${this._readyState}`
  185. }
  186. // UNICODE SHOULD NOT PASS
  187. if(typeof name !== 'string' || /[^\u0000-\u00ff]/.test(name)) {
  188. throw 'TypeError : header field name should be a string'
  189. }
  190. //
  191. let invalidPatterns = [
  192. /[\(\)\>\<\@\,\:\\\/\[\]\?\=\}\{\s\ \u007f\;\t\0\v\r]/,
  193. /tt/
  194. ]
  195. for(let i in invalidPatterns) {
  196. if(invalidPatterns[i].test(name) || typeof name !== 'string') {
  197. throw `SyntaxError : Invalid header field name ${name}`
  198. }
  199. }
  200. this._headers[name] = value
  201. }
  202. abort() {
  203. log.verbose('XMLHttpRequest abort ')
  204. if(!this._task)
  205. return
  206. this._task.cancel((err) => {
  207. let e = {
  208. timeStamp : Date.now(),
  209. }
  210. if(this.onabort)
  211. this.onabort()
  212. if(err) {
  213. e.detail = err
  214. e.type = 'error'
  215. this.dispatchEvent('error', e)
  216. }
  217. else {
  218. e.type = 'abort'
  219. this.dispatchEvent('abort', e)
  220. }
  221. })
  222. }
  223. getResponseHeader(field:string):string | null {
  224. log.verbose('XMLHttpRequest get header', field, this._responseHeaders)
  225. if(!this._responseHeaders)
  226. return null
  227. return (this._responseHeaders[field] || this._responseHeaders[field.toLowerCase()]) || null
  228. }
  229. getAllResponseHeaders():string | null {
  230. log.verbose('XMLHttpRequest get all headers', this._responseHeaders)
  231. if(!this._responseHeaders)
  232. return ''
  233. let result = ''
  234. let respHeaders = this.responseHeaders
  235. for(let i in respHeaders) {
  236. result += `${i}: ${respHeaders[i]}${String.fromCharCode(0x0D,0x0A)}`
  237. }
  238. return result.substr(0, result.length-2)
  239. }
  240. _headerReceived = (e) => {
  241. log.debug('header received ', this._task.taskId, e)
  242. this.responseURL = this._url
  243. if(e.state === "2" && e.taskId === this._task.taskId) {
  244. this._responseHeaders = e.headers
  245. this._statusText = e.status
  246. this._status = Math.floor(e.status)
  247. this._dispatchReadStateChange(XMLHttpRequest.HEADERS_RECEIVED)
  248. }
  249. }
  250. _uploadProgressEvent = (send:number, total:number) => {
  251. if(!this._uploadStarted) {
  252. this.upload.dispatchEvent('loadstart')
  253. this._uploadStarted = true
  254. }
  255. if(send >= total)
  256. this.upload.dispatchEvent('load')
  257. this.upload.dispatchEvent('progress', new ProgressEvent(true, send, total))
  258. }
  259. _progressEvent = (send:number, total:number, chunk:string) => {
  260. log.verbose(this.readyState)
  261. if(this._readyState === XMLHttpRequest.HEADERS_RECEIVED)
  262. this._dispatchReadStateChange(XMLHttpRequest.LOADING)
  263. let lengthComputable = false
  264. if(total && total >= 0)
  265. lengthComputable = true
  266. let e = new ProgressEvent(lengthComputable, send, total)
  267. if(this._increment) {
  268. this._responseText += chunk
  269. }
  270. this.dispatchEvent('progress', e)
  271. }
  272. _onError = (err) => {
  273. let statusCode = Math.floor(this.status)
  274. if(statusCode >= 100 && statusCode !== 408) {
  275. return
  276. }
  277. log.debug('XMLHttpRequest error', err)
  278. this._statusText = err
  279. this._status = String(err).match(/\d+/)
  280. this._status = this._status ? Math.floor(this.status) : 404
  281. this._dispatchReadStateChange(XMLHttpRequest.DONE)
  282. if(err && String(err.message).match(/(timed\sout|timedout)/) || this._status == 408) {
  283. this.dispatchEvent('timeout')
  284. }
  285. this.dispatchEvent('loadend')
  286. this.dispatchEvent('error', {
  287. type : 'error',
  288. detail : err
  289. })
  290. this.clearEventListeners()
  291. }
  292. _onDone = (resp) => {
  293. log.debug('XMLHttpRequest done', this._url, resp, this)
  294. this._statusText = this._status
  295. let responseDataReady = () => {
  296. log.debug('request done state = 4')
  297. this.dispatchEvent('load')
  298. this.dispatchEvent('loadend')
  299. this._dispatchReadStateChange(XMLHttpRequest.DONE)
  300. this.clearEventListeners()
  301. }
  302. if(resp) {
  303. let info = resp.respInfo || {}
  304. log.debug(this._url, info, info.respType)
  305. switch(this._responseType) {
  306. case 'blob' :
  307. resp.blob().then((b) => {
  308. this._responseText = resp.text()
  309. this._response = b
  310. responseDataReady()
  311. })
  312. break;
  313. case 'arraybuffer':
  314. // TODO : to array buffer
  315. break
  316. case 'json':
  317. this._response = resp.json()
  318. this._responseText = resp.text()
  319. break
  320. default :
  321. this._responseText = resp.text()
  322. this._response = this.responseText
  323. responseDataReady()
  324. break;
  325. }
  326. }
  327. }
  328. _dispatchReadStateChange(state) {
  329. this._readyState = state
  330. if(typeof this._onreadystatechange === 'function')
  331. this._onreadystatechange()
  332. }
  333. set onreadystatechange(fn:() => void) {
  334. log.verbose('XMLHttpRequest set onreadystatechange', fn)
  335. this._onreadystatechange = fn
  336. }
  337. get onreadystatechange() {
  338. return this._onreadystatechange
  339. }
  340. get readyState() {
  341. log.verbose('get readyState', this._readyState)
  342. return this._readyState
  343. }
  344. get status() {
  345. log.verbose('get status', this._status)
  346. return this._status
  347. }
  348. get statusText() {
  349. log.verbose('get statusText', this._statusText)
  350. return this._statusText
  351. }
  352. get response() {
  353. log.verbose('get response', this._response)
  354. return this._response
  355. }
  356. get responseText() {
  357. log.verbose('get responseText', this._responseText)
  358. return this._responseText
  359. }
  360. get responseURL() {
  361. log.verbose('get responseURL', this._responseURL)
  362. return this._responseURL
  363. }
  364. get responseHeaders() {
  365. log.verbose('get responseHeaders', this._responseHeaders)
  366. return this._responseHeaders
  367. }
  368. set timeout(val) {
  369. this._timeout = val*1000
  370. log.verbose('set timeout', this._timeout)
  371. }
  372. get timeout() {
  373. log.verbose('get timeout', this._timeout)
  374. return this._timeout
  375. }
  376. set responseType(val) {
  377. log.verbose('set response type', this._responseType)
  378. this._responseType = val
  379. }
  380. get responseType() {
  381. log.verbose('get response type', this._responseType)
  382. return this._responseType
  383. }
  384. static get isRNFBPolyfill() {
  385. return true
  386. }
  387. }