Browse Source

Fix unicode response issue

Fetch polyfill wip #70
Ben Hsieh 8 years ago
parent
commit
1e1a6b0d39

+ 8
- 8
src/index.js View File

112
   let taskId = getUUID()
112
   let taskId = getUUID()
113
   let options = this || {}
113
   let options = this || {}
114
   let subscription, subscriptionUpload, stateEvent
114
   let subscription, subscriptionUpload, stateEvent
115
+  let respInfo = {}
115
 
116
 
116
   let promise = new Promise((resolve, reject) => {
117
   let promise = new Promise((resolve, reject) => {
117
     let [method, url, headers, body] = [...args]
118
     let [method, url, headers, body] = [...args]
131
     })
132
     })
132
 
133
 
133
     stateEvent = emitter.addListener('RNFetchBlobState', (e) => {
134
     stateEvent = emitter.addListener('RNFetchBlobState', (e) => {
135
+      respInfo = e
134
       if(e.taskId === taskId && promise.onStateChange) {
136
       if(e.taskId === taskId && promise.onStateChange) {
135
         promise.onStateChange(e)
137
         promise.onStateChange(e)
136
       }
138
       }
144
 
146
 
145
     let req = RNFetchBlob[nativeMethodName]
147
     let req = RNFetchBlob[nativeMethodName]
146
 
148
 
147
-    req(options, taskId, method, url, headers || {}, body, (err, info, data) => {
149
+    req(options, taskId, method, url, headers || {}, body, (err, unsused, data) => {
148
 
150
 
149
       // task done, remove event listener
151
       // task done, remove event listener
150
       subscription.remove()
152
       subscription.remove()
151
       subscriptionUpload.remove()
153
       subscriptionUpload.remove()
152
       stateEvent.remove()
154
       stateEvent.remove()
153
-      info = info ? info : {}
155
+
154
       if(err)
156
       if(err)
155
         reject(new Error(err, info))
157
         reject(new Error(err, info))
156
       else {
158
       else {
157
         let rnfbEncode = 'base64'
159
         let rnfbEncode = 'base64'
158
         // response data is saved to storage
160
         // response data is saved to storage
159
         if(options.path || options.fileCache || options.addAndroidDownloads
161
         if(options.path || options.fileCache || options.addAndroidDownloads
160
-          || options.key || options.auto && info.respType === 'blob') {
162
+          || options.key || options.auto && respInfo.respType === 'blob') {
161
           rnfbEncode = 'path'
163
           rnfbEncode = 'path'
162
           if(options.session)
164
           if(options.session)
163
             session(options.session).add(data)
165
             session(options.session).add(data)
164
         }
166
         }
165
-        info = info || {}
166
-        info.rnfbEncode = rnfbEncode
167
-        resolve(new FetchBlobResponse(taskId, info, data))
167
+        respInfo.rnfbEncode = rnfbEncode
168
+        resolve(new FetchBlobResponse(taskId, respInfo, data))
168
       }
169
       }
169
 
170
 
170
     })
171
     })
240
           try {
241
           try {
241
             let b = new polyfill.Blob(this.data, 'application/octet-stream;BASE64')
242
             let b = new polyfill.Blob(this.data, 'application/octet-stream;BASE64')
242
             b.onCreated(() => {
243
             b.onCreated(() => {
243
-              console.log('####', b)
244
               resolve(b)
244
               resolve(b)
245
             })
245
             })
246
           } catch(err) {
246
           } catch(err) {
254
      * @return {string} Decoded base64 string.
254
      * @return {string} Decoded base64 string.
255
      */
255
      */
256
     this.text = ():string => {
256
     this.text = ():string => {
257
-      return base64.decode(this.data)
257
+      return decodeURIComponent(base64.decode(this.data))
258
     }
258
     }
259
     /**
259
     /**
260
      * Convert result to JSON object.
260
      * Convert result to JSON object.

+ 1
- 0
src/ios/RNFetchBlobConst.h View File

29
 extern NSString *const CONFIG_TRUSTY;
29
 extern NSString *const CONFIG_TRUSTY;
30
 extern NSString *const CONFIG_INDICATOR;
30
 extern NSString *const CONFIG_INDICATOR;
31
 extern NSString *const CONFIG_KEY;
31
 extern NSString *const CONFIG_KEY;
32
+extern NSString *const CONFIG_EXTRA_BLOB_CTYPE;
32
 
33
 
33
 // fs events
34
 // fs events
34
 extern NSString *const FS_EVENT_DATA;
35
 extern NSString *const FS_EVENT_DATA;

+ 1
- 0
src/ios/RNFetchBlobConst.m View File

20
 extern NSString *const CONFIG_TRUSTY = @"trusty";
20
 extern NSString *const CONFIG_TRUSTY = @"trusty";
21
 extern NSString *const CONFIG_INDICATOR = @"indicator";
21
 extern NSString *const CONFIG_INDICATOR = @"indicator";
22
 extern NSString *const CONFIG_KEY = @"key";
22
 extern NSString *const CONFIG_KEY = @"key";
23
+extern NSString *const CONFIG_EXTRA_BLOB_CTYPE = @"binaryContentTypes";
23
 
24
 
24
 extern NSString *const EVENT_STATE_CHANGE = @"RNFetchBlobState";
25
 extern NSString *const EVENT_STATE_CHANGE = @"RNFetchBlobState";
25
 extern NSString *const MSG_EVENT = @"RNFetchBlobMessage";
26
 extern NSString *const MSG_EVENT = @"RNFetchBlobMessage";

+ 8
- 3
src/ios/RNFetchBlobFS.m View File

234
             return;
234
             return;
235
         }
235
         }
236
         else {
236
         else {
237
-            content = [data dataUsingEncoding:NSISOLatin1StringEncoding];
237
+            content = [data dataUsingEncoding:NSUTF8StringEncoding];
238
         }
238
         }
239
         if(append == YES) {
239
         if(append == YES) {
240
             [fileHandle seekToEndOfFile];
240
             [fileHandle seekToEndOfFile];
333
                 onComplete(fileContent);
333
                 onComplete(fileContent);
334
             
334
             
335
             if([[encoding lowercaseString] isEqualToString:@"utf8"]) {
335
             if([[encoding lowercaseString] isEqualToString:@"utf8"]) {
336
-                if(resolve != nil)
337
-                    resolve([[NSString alloc] initWithData:fileContent encoding:NSUTF8StringEncoding]);
336
+                if(resolve != nil) {
337
+                    NSString * utf8 = [[NSString alloc] initWithData:fileContent encoding:NSUTF8StringEncoding];
338
+                    if(utf8 == nil)
339
+                        resolve([[NSString alloc] initWithData:fileContent encoding:NSISOLatin1StringEncoding]);
340
+                    else
341
+                        resolve(utf8);
342
+                }
338
             }
343
             }
339
             else if ([[encoding lowercaseString] isEqualToString:@"base64"]) {
344
             else if ([[encoding lowercaseString] isEqualToString:@"base64"]) {
340
                 if(resolve != nil)
345
                 if(resolve != nil)

+ 24
- 4
src/ios/RNFetchBlobNetwork.m View File

201
                                lowercaseString];
201
                                lowercaseString];
202
         if([headers valueForKey:@"Content-Type"] != nil)
202
         if([headers valueForKey:@"Content-Type"] != nil)
203
         {
203
         {
204
-            if([respType containsString:@"text/"])
204
+            NSArray * extraBlobCTypes = [options objectForKey:CONFIG_EXTRA_BLOB_CTYPE];
205
+            // If extra blob content type is not empty, check if response type matches
206
+            if( extraBlobCTypes !=  nil) {
207
+                for(NSString * substr in extraBlobCTypes)
208
+                {
209
+                    if([[respType lowercaseString] containsString:[substr lowercaseString]])
210
+                    {
211
+                        respType = @"blob";
212
+                        respFile = YES;
213
+                        destPath = [RNFetchBlobFS getTempPath:taskId withExtension:nil];
214
+                        break;
215
+                    }
216
+                }
217
+            }
218
+            else if([respType containsString:@"text/"])
205
             {
219
             {
206
                 respType = @"text";
220
                 respType = @"text";
207
             }
221
             }
315
     }
329
     }
316
     // base64 response
330
     // base64 response
317
     else {
331
     else {
318
-        NSString * res = [[NSString alloc] initWithData:respData encoding:NSUTF8StringEncoding];
332
+        NSString * utf8 = [[[NSString alloc] initWithData:respData encoding:NSUTF8StringEncoding] stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
333
+        NSString * base64 = @"";
334
+        if(utf8 != nil)
335
+            base64 = [[utf8 dataUsingEncoding:NSUTF8StringEncoding] base64EncodedStringWithOptions:0];
336
+        else
337
+            base64 = [respData base64EncodedStringWithOptions:0];
319
         callback(@[error == nil ? [NSNull null] : [error localizedDescription],
338
         callback(@[error == nil ? [NSNull null] : [error localizedDescription],
320
                    respInfo == nil ? [NSNull null] : respInfo,
339
                    respInfo == nil ? [NSNull null] : respInfo,
321
-                   [respData base64EncodedStringWithOptions:0]
322
-                   ]);
340
+                   base64
341
+                ]);
342
+        
323
     }
343
     }
324
     
344
     
325
     [taskTable removeObjectForKey:taskId];
345
     [taskTable removeObjectForKey:taskId];

+ 89
- 0
src/polyfill/Fetch.js View File

1
+import RNFetchBlob from '../index.js'
2
+import Log from '../utils/log.js'
3
+import fs from '../fs'
4
+import unicode from '../utils/unicode'
5
+
6
+const log = new Log('FetchPolyfill')
7
+
8
+log.level(3)
9
+
10
+
11
+export default class Fetch {
12
+
13
+  provider:RNFetchBlobFetch;
14
+
15
+  constructor(config:RNFetchBlobConfig) {
16
+    this.provider = new RNFetchBlobFetch(config)
17
+  }
18
+
19
+}
20
+
21
+class RNFetchBlobFetch {
22
+
23
+  constructor(config:RNFetchBlobConfig) {
24
+    this._fetch = (url, options) => {
25
+      let bodyUsed = false
26
+      options.headers = options.headers || {}
27
+      options['Content-Type'] = options.headers['Content-Type'] || options.headers['content-type']
28
+      options['content-type'] = options.headers['Content-Type'] || options.headers['content-type']
29
+      return RNFetchBlob.config(config)
30
+        .fetch(options.method, url, options.headers, options.body)
31
+        .then((resp) => {
32
+          let info = resp.info()
33
+          return Promise.resolve({
34
+            headers : info.headers,
35
+            ok : info.status >= 200 && info.status <= 299,
36
+            status : info.status,
37
+            type : 'basic',
38
+            bodyUsed,
39
+            arrayBuffer : () => {
40
+              log.verbose('to arrayBuffer', info)
41
+
42
+            },
43
+            text : () => {
44
+              log.verbose('to text', resp, info)
45
+              switch (info.rnfbEncode) {
46
+                case 'base64':
47
+                  let result = unicode(resp.text())
48
+                  return Promise.resolve(result)
49
+                  break
50
+                case 'path':
51
+                  return resp.readFile('utf8').then((data) => {
52
+                    data = unicode(data)
53
+                    return Promise.resolve(data)
54
+                  })
55
+                  break
56
+                case '':
57
+                default:
58
+                  return Promise.resolve(resp.data)
59
+                  break
60
+              }
61
+            },
62
+            json : () => {
63
+              log.verbose('to json', resp, info)
64
+              switch (info.rnfbEncode) {
65
+                case 'base64':
66
+                  return Promise.resolve(resp.json())
67
+                case 'path':
68
+                  return resp.readFile('utf8').then((data) => {
69
+                    return Promise.resolve(JSON.parse(data))
70
+                  })
71
+                case '':
72
+                default:
73
+                  return Promise.resolve(JSON.parse(resp.data))
74
+              }
75
+            },
76
+            formData : () => {
77
+              log.verbose('to formData', resp, info)
78
+
79
+            }
80
+          })
81
+        })
82
+    }
83
+  }
84
+
85
+  get fetch() {
86
+    return this._fetch
87
+  }
88
+
89
+}

+ 91
- 0
src/polyfill/FileReader.js View File

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
+
5
+import RNFetchBlob from '../index.js'
6
+import ProgressEvent from './ProgressEvent.js'
7
+import EventTarget from './EventTarget'
8
+import Blob from './Blob'
9
+import Log from '../utils/log.js'
10
+import fs from '../fs'
11
+
12
+const log = new Log('FileReader')
13
+
14
+log.level(3)
15
+
16
+export default class FileReader extends EventTarget {
17
+
18
+  static get EMPTY(){
19
+    return 0
20
+  }
21
+  static get LOADING(){
22
+    return 1
23
+  }
24
+  static get DONE(){
25
+    return 2
26
+  }
27
+
28
+  // properties
29
+  _readState:number = 0;
30
+  _result:any;
31
+  _error:any;
32
+
33
+  get isRNFBPolyFill(){ return true }
34
+
35
+  // event handlers
36
+  onloadstart:(e:Event) => void;
37
+  onprogress:(e:Event) => void;
38
+  onload:(e:Event) => void;
39
+  onabort:(e:Event) => void;
40
+  onerror:(e:Event) => void;
41
+  onloadend:(e:Event) => void;
42
+
43
+  constructor() {
44
+    super()
45
+    log.verbose('file reader const')
46
+    this._result = null
47
+  }
48
+
49
+  abort() {
50
+    log.verbose('abort', b, label)
51
+  }
52
+
53
+  readAsArrayBuffer(b:Blob) {
54
+    log.verbose('readAsArrayBuffer', b, label)
55
+  }
56
+
57
+  readAsBinaryString(b:Blob) {
58
+    log.verbose('readAsBinaryString', b, label)
59
+  }
60
+
61
+  readAsText(b:Blob, label:?string) {
62
+    log.verbose('readAsText', b, label)
63
+  }
64
+
65
+  readAsDataURL(b:Blob) {
66
+    log.verbose('readAsDataURL', b, label)
67
+  }
68
+
69
+  dispatchEvent(event, e) {
70
+    log.verbose('dispatch event', event, e)
71
+    super.dispatchEvent(event, e)
72
+    if(typeof this[`on${event}`] === 'function') {
73
+      this[`on${event}`](e)
74
+    }
75
+  }
76
+
77
+  // private methods
78
+
79
+  // getters and setters
80
+
81
+  get readState() {
82
+    return this._readyState
83
+  }
84
+
85
+  get result() {
86
+    return this._result
87
+  }
88
+
89
+
90
+
91
+}

+ 32
- 4
src/polyfill/XMLHttpRequest.js View File

10
 
10
 
11
 const log = new Log('XMLHttpRequest')
11
 const log = new Log('XMLHttpRequest')
12
 
12
 
13
-log.disable()
13
+log.level(0)
14
 
14
 
15
 const UNSENT = 0
15
 const UNSENT = 0
16
 const OPENED = 1
16
 const OPENED = 1
23
   _onreadystatechange : () => void;
23
   _onreadystatechange : () => void;
24
 
24
 
25
   upload : XMLHttpRequestEventTarget = new XMLHttpRequestEventTarget();
25
   upload : XMLHttpRequestEventTarget = new XMLHttpRequestEventTarget();
26
+  static binaryContentTypes : Array<string> = [];
26
 
27
 
27
   // readonly
28
   // readonly
28
   _readyState : number = UNSENT;
29
   _readyState : number = UNSENT;
79
     return DONE
80
     return DONE
80
   }
81
   }
81
 
82
 
83
+  static addBinaryContentType(substr:string) {
84
+    for(let i in XMLHttpRequest.binaryContentTypes) {
85
+      if(new RegExp(substr,'i').test(XMLHttpRequest.binaryContentTypes[i])) {
86
+        return
87
+      }
88
+    }
89
+    XMLHttpRequest.binaryContentTypes.push(substr)
90
+
91
+  }
92
+
93
+  static removeBinaryContentType(val) {
94
+    for(let i in XMLHttpRequest.binaryContentTypes) {
95
+      if(new RegExp(substr,'i').test(XMLHttpRequest.binaryContentTypes[i])) {
96
+        XMLHttpRequest.binaryContentTypes.splice(i,1)
97
+        return
98
+      }
99
+    }
100
+  }
101
+
82
   constructor() {
102
   constructor() {
83
     super()
103
     super()
84
     log.verbose('XMLHttpRequest constructor called')
104
     log.verbose('XMLHttpRequest constructor called')
127
       body = body ? body.toString() : body
147
       body = body ? body.toString() : body
128
 
148
 
129
     this._task = RNFetchBlob
149
     this._task = RNFetchBlob
130
-                  .config({ auto: true, timeout : this._timeout })
150
+                  .config({
151
+                    auto: true,
152
+                    timeout : this._timeout,
153
+                    binaryContentTypes : XMLHttpRequest.binaryContentTypes
154
+                  })
131
                   .fetch(_method, _url, _headers, body)
155
                   .fetch(_method, _url, _headers, body)
132
-    this.dispatchEvent('load')
133
     this._task
156
     this._task
134
         .stateChange(this._headerReceived.bind(this))
157
         .stateChange(this._headerReceived.bind(this))
135
         .uploadProgress(this._uploadProgressEvent.bind(this))
158
         .uploadProgress(this._uploadProgressEvent.bind(this))
281
           this._response = this.responseText
304
           this._response = this.responseText
282
         break;
305
         break;
283
       }
306
       }
284
-      this.dispatchEvent('loadend')
285
       this.dispatchEvent('load')
307
       this.dispatchEvent('load')
308
+      this.dispatchEvent('loadend')
286
       this._dispatchReadStateChange(XMLHttpRequest.DONE)
309
       this._dispatchReadStateChange(XMLHttpRequest.DONE)
287
     }
310
     }
288
     this.clearEventListeners()
311
     this.clearEventListeners()
348
     return this._timeout
371
     return this._timeout
349
   }
372
   }
350
 
373
 
374
+  set responseType(val) {
375
+    log.verbose('set response type', this._responseType)
376
+    this._responseType = val
377
+  }
378
+
351
   get responseType() {
379
   get responseType() {
352
     log.verbose('get response type', this._responseType)
380
     log.verbose('get response type', this._responseType)
353
     return this._responseType
381
     return this._responseType

+ 3
- 1
src/polyfill/index.js View File

3
 import XMLHttpRequest from './XMLHttpRequest.js'
3
 import XMLHttpRequest from './XMLHttpRequest.js'
4
 import ProgressEvent from './ProgressEvent'
4
 import ProgressEvent from './ProgressEvent'
5
 import Event from './Event'
5
 import Event from './Event'
6
+import FileReader from './FileReader'
7
+import Fetch from './Fetch'
6
 
8
 
7
 export default {
9
 export default {
8
-  Blob, File, XMLHttpRequest, ProgressEvent, Event
10
+  Blob, File, XMLHttpRequest, ProgressEvent, Event, FileReader, Fetch
9
 }
11
 }

+ 7
- 0
src/utils/unicode.js View File

1
+export default function(x) {
2
+  var r = /\\u([\d\w]{4})/gi
3
+  x = x.replace(r, function (match, grp) {
4
+    return String.fromCharCode(parseInt(grp, 16))
5
+  })
6
+  return unescape(x)
7
+}

+ 4
- 0
test-server/server.js View File

63
   next();
63
   next();
64
 })
64
 })
65
 
65
 
66
+app.get('/unicode', (req, res) => {
67
+  res.send({ data:'你好!'})
68
+})
69
+
66
 app.all('/echo', (req, res) => {
70
 app.all('/echo', (req, res) => {
67
   var body = ''
71
   var body = ''
68
   req.on('data', (chunk) => {
72
   req.on('data', (chunk) => {

+ 58
- 0
test/test-0.8.2.js View File

1
+import RNTest from './react-native-testkit/'
2
+import React from 'react'
3
+import RNFetchBlob from 'react-native-fetch-blob'
4
+
5
+import {
6
+  StyleSheet,
7
+  Text,
8
+  View,
9
+  ScrollView,
10
+  Platform,
11
+  Dimensions,
12
+  Image,
13
+} from 'react-native';
14
+
15
+window.XMLHttpRequest = RNFetchBlob.polyfill.XMLHttpRequest
16
+window.Blob = Blob
17
+window.fetch = new RNFetchBlob.polyfill.Fetch({
18
+  auto : true,
19
+  binaryContentTypes : ['image/', 'video/', 'audio/']
20
+}).provider.fetch
21
+
22
+const fs = RNFetchBlob.fs
23
+const { Assert, Comparer, Info, prop } = RNTest
24
+const describe = RNTest.config({
25
+  group : '0.8.2',
26
+  run : true,
27
+  expand : true,
28
+  timeout : 20000,
29
+})
30
+const { TEST_SERVER_URL, TEST_SERVER_URL_SSL, FILENAME, DROPBOX_TOKEN, styles } = prop()
31
+const dirs = RNFetchBlob.fs.dirs
32
+
33
+let prefix = ((Platform.OS === 'android') ? 'file://' : '')
34
+
35
+describe('unicode file access', (report, done) => {
36
+  let path = dirs.DocumentDir + '/chinese.tmp'
37
+  fs.writeFile(path, '你好!', 'utf8')
38
+    .then(() => fs.readFile(path, 'utf8'))
39
+    .then((data) => {
40
+      console.log(data)
41
+      done()
42
+    })
43
+})
44
+
45
+describe('whatwg-fetch - GET should work correctly', (report, done) => {
46
+  console.log(fetch)
47
+  fetch(`${TEST_SERVER_URL}/unicode`, {
48
+    method : 'GET'
49
+  })
50
+  .then((res) => {
51
+    console.log('fetch resp',res)
52
+    return res.text()
53
+  })
54
+  .then((blob) => {
55
+    console.log(blob)
56
+    done()
57
+  })
58
+})

+ 5
- 4
test/test-fs.js View File

97
               d += chunk
97
               d += chunk
98
             })
98
             })
99
             stream.onEnd(() => {
99
             stream.onEnd(() => {
100
-              report(<Assert
101
-                key="base64 content test"
102
-                expect={raw}
103
-                actual={d}/>)
100
+              report(
101
+                <Assert
102
+                  key="base64 content test"
103
+                  expect={raw}
104
+                  actual={d}/>)
104
                 done()
105
                 done()
105
               })
106
               })
106
         })
107
         })

+ 5
- 4
test/test-init.js View File

18
 // test environment variables
18
 // test environment variables
19
 
19
 
20
 prop('FILENAME', `${Platform.OS}-0.8.0-${Date.now()}.png`)
20
 prop('FILENAME', `${Platform.OS}-0.8.0-${Date.now()}.png`)
21
-prop('TEST_SERVER_URL', 'http://192.168.0.11:8123')
22
-prop('TEST_SERVER_URL_SSL', 'https://192.168.0.11:8124')
21
+prop('TEST_SERVER_URL', 'http://192.168.16.70:8123')
22
+prop('TEST_SERVER_URL_SSL', 'https://192.168.16.70:8124')
23
 prop('DROPBOX_TOKEN', 'fsXcpmKPrHgAAAAAAAAAoXZhcXYWdgLpQMan6Tb_bzJ237DXhgQSev12hA-gUXt4')
23
 prop('DROPBOX_TOKEN', 'fsXcpmKPrHgAAAAAAAAAoXZhcXYWdgLpQMan6Tb_bzJ237DXhgQSev12hA-gUXt4')
24
 prop('styles', {
24
 prop('styles', {
25
   image : {
25
   image : {
64
 // require('./test-0.6.0')
64
 // require('./test-0.6.0')
65
 // require('./test-0.6.2')
65
 // require('./test-0.6.2')
66
 // require('./test-0.6.3')
66
 // require('./test-0.6.3')
67
-require('./test-0.7.0')
67
+// require('./test-0.7.0')
68
 // require('./test-0.8.0')
68
 // require('./test-0.8.0')
69
+require('./test-0.8.2')
69
 // require('./test-fs')
70
 // require('./test-fs')
70
 // require('./test-xmlhttp')
71
 // require('./test-xmlhttp')
71
 // require('./test-blob')
72
 // require('./test-blob')
72
-require('./test-firebase')
73
+// require('./test-firebase')
73
 // require('./test-android')
74
 // require('./test-android')
74
 // require('./benchmark')
75
 // require('./benchmark')