Browse Source

Fix unicode response issue

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

+ 8
- 8
src/index.js View File

@@ -112,6 +112,7 @@ function fetch(...args:any):Promise {
112 112
   let taskId = getUUID()
113 113
   let options = this || {}
114 114
   let subscription, subscriptionUpload, stateEvent
115
+  let respInfo = {}
115 116
 
116 117
   let promise = new Promise((resolve, reject) => {
117 118
     let [method, url, headers, body] = [...args]
@@ -131,6 +132,7 @@ function fetch(...args:any):Promise {
131 132
     })
132 133
 
133 134
     stateEvent = emitter.addListener('RNFetchBlobState', (e) => {
135
+      respInfo = e
134 136
       if(e.taskId === taskId && promise.onStateChange) {
135 137
         promise.onStateChange(e)
136 138
       }
@@ -144,27 +146,26 @@ function fetch(...args:any):Promise {
144 146
 
145 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 151
       // task done, remove event listener
150 152
       subscription.remove()
151 153
       subscriptionUpload.remove()
152 154
       stateEvent.remove()
153
-      info = info ? info : {}
155
+
154 156
       if(err)
155 157
         reject(new Error(err, info))
156 158
       else {
157 159
         let rnfbEncode = 'base64'
158 160
         // response data is saved to storage
159 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 163
           rnfbEncode = 'path'
162 164
           if(options.session)
163 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,7 +241,6 @@ class FetchBlobResponse {
240 241
           try {
241 242
             let b = new polyfill.Blob(this.data, 'application/octet-stream;BASE64')
242 243
             b.onCreated(() => {
243
-              console.log('####', b)
244 244
               resolve(b)
245 245
             })
246 246
           } catch(err) {
@@ -254,7 +254,7 @@ class FetchBlobResponse {
254 254
      * @return {string} Decoded base64 string.
255 255
      */
256 256
     this.text = ():string => {
257
-      return base64.decode(this.data)
257
+      return decodeURIComponent(base64.decode(this.data))
258 258
     }
259 259
     /**
260 260
      * Convert result to JSON object.

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

@@ -29,6 +29,7 @@ extern NSString *const CONFIG_FILE_EXT;
29 29
 extern NSString *const CONFIG_TRUSTY;
30 30
 extern NSString *const CONFIG_INDICATOR;
31 31
 extern NSString *const CONFIG_KEY;
32
+extern NSString *const CONFIG_EXTRA_BLOB_CTYPE;
32 33
 
33 34
 // fs events
34 35
 extern NSString *const FS_EVENT_DATA;

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

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

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

@@ -234,7 +234,7 @@ NSMutableDictionary *fileStreams = nil;
234 234
             return;
235 235
         }
236 236
         else {
237
-            content = [data dataUsingEncoding:NSISOLatin1StringEncoding];
237
+            content = [data dataUsingEncoding:NSUTF8StringEncoding];
238 238
         }
239 239
         if(append == YES) {
240 240
             [fileHandle seekToEndOfFile];
@@ -333,8 +333,13 @@ NSMutableDictionary *fileStreams = nil;
333 333
                 onComplete(fileContent);
334 334
             
335 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 344
             else if ([[encoding lowercaseString] isEqualToString:@"base64"]) {
340 345
                 if(resolve != nil)

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

@@ -201,7 +201,21 @@ NSOperationQueue *taskQueue;
201 201
                                lowercaseString];
202 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 220
                 respType = @"text";
207 221
             }
@@ -315,11 +329,17 @@ NSOperationQueue *taskQueue;
315 329
     }
316 330
     // base64 response
317 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 338
         callback(@[error == nil ? [NSNull null] : [error localizedDescription],
320 339
                    respInfo == nil ? [NSNull null] : respInfo,
321
-                   [respData base64EncodedStringWithOptions:0]
322
-                   ]);
340
+                   base64
341
+                ]);
342
+        
323 343
     }
324 344
     
325 345
     [taskTable removeObjectForKey:taskId];

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

@@ -0,0 +1,89 @@
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

@@ -0,0 +1,91 @@
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,7 +10,7 @@ import ProgressEvent from './ProgressEvent.js'
10 10
 
11 11
 const log = new Log('XMLHttpRequest')
12 12
 
13
-log.disable()
13
+log.level(0)
14 14
 
15 15
 const UNSENT = 0
16 16
 const OPENED = 1
@@ -23,6 +23,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget{
23 23
   _onreadystatechange : () => void;
24 24
 
25 25
   upload : XMLHttpRequestEventTarget = new XMLHttpRequestEventTarget();
26
+  static binaryContentTypes : Array<string> = [];
26 27
 
27 28
   // readonly
28 29
   _readyState : number = UNSENT;
@@ -79,6 +80,25 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget{
79 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 102
   constructor() {
83 103
     super()
84 104
     log.verbose('XMLHttpRequest constructor called')
@@ -127,9 +147,12 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget{
127 147
       body = body ? body.toString() : body
128 148
 
129 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 155
                   .fetch(_method, _url, _headers, body)
132
-    this.dispatchEvent('load')
133 156
     this._task
134 157
         .stateChange(this._headerReceived.bind(this))
135 158
         .uploadProgress(this._uploadProgressEvent.bind(this))
@@ -281,8 +304,8 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget{
281 304
           this._response = this.responseText
282 305
         break;
283 306
       }
284
-      this.dispatchEvent('loadend')
285 307
       this.dispatchEvent('load')
308
+      this.dispatchEvent('loadend')
286 309
       this._dispatchReadStateChange(XMLHttpRequest.DONE)
287 310
     }
288 311
     this.clearEventListeners()
@@ -348,6 +371,11 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget{
348 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 379
   get responseType() {
352 380
     log.verbose('get response type', this._responseType)
353 381
     return this._responseType

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

@@ -3,7 +3,9 @@ import File from './File.js'
3 3
 import XMLHttpRequest from './XMLHttpRequest.js'
4 4
 import ProgressEvent from './ProgressEvent'
5 5
 import Event from './Event'
6
+import FileReader from './FileReader'
7
+import Fetch from './Fetch'
6 8
 
7 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

@@ -0,0 +1,7 @@
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,6 +63,10 @@ app.use(function(req, res, next) {
63 63
   next();
64 64
 })
65 65
 
66
+app.get('/unicode', (req, res) => {
67
+  res.send({ data:'你好!'})
68
+})
69
+
66 70
 app.all('/echo', (req, res) => {
67 71
   var body = ''
68 72
   req.on('data', (chunk) => {

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

@@ -0,0 +1,58 @@
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,10 +97,11 @@ describe('create file API test', (report, done) => {
97 97
               d += chunk
98 98
             })
99 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 105
                 done()
105 106
               })
106 107
         })

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

@@ -18,8 +18,8 @@ const { Assert, Comparer, Info, prop } = RNTest
18 18
 // test environment variables
19 19
 
20 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 23
 prop('DROPBOX_TOKEN', 'fsXcpmKPrHgAAAAAAAAAoXZhcXYWdgLpQMan6Tb_bzJ237DXhgQSev12hA-gUXt4')
24 24
 prop('styles', {
25 25
   image : {
@@ -64,11 +64,12 @@ describe('GET image from server', (report, done) => {
64 64
 // require('./test-0.6.0')
65 65
 // require('./test-0.6.2')
66 66
 // require('./test-0.6.3')
67
-require('./test-0.7.0')
67
+// require('./test-0.7.0')
68 68
 // require('./test-0.8.0')
69
+require('./test-0.8.2')
69 70
 // require('./test-fs')
70 71
 // require('./test-xmlhttp')
71 72
 // require('./test-blob')
72
-require('./test-firebase')
73
+// require('./test-firebase')
73 74
 // require('./test-android')
74 75
 // require('./benchmark')