Line data Source code
1 : import 'dart:async'; 2 : import 'dart:convert'; 3 : import 'dart:math'; 4 : 5 : import 'multipart_file.dart'; 6 : import 'options.dart'; 7 : import 'utils.dart'; 8 : 9 : /// A class to create readable "multipart/form-data" streams. 10 : /// It can be used to submit forms and file uploads to http server. 11 : class FormData { 12 : static const String _BOUNDARY_PRE_TAG = '--dio-boundary-'; 13 : static const _BOUNDARY_LENGTH = _BOUNDARY_PRE_TAG.length + 10; 14 : 15 : late String _boundary; 16 : 17 : /// The boundary of FormData, it consists of a constant prefix and a random 18 : /// postfix to assure the the boundary unpredictable and unique, each FormData 19 : /// instance will be different. 20 2 : String get boundary => _boundary; 21 : 22 : final _newlineRegExp = RegExp(r'\r\n|\r|\n'); 23 : 24 : /// The form fields to send for this request. 25 : final fields = <MapEntry<String, String>>[]; 26 : 27 : /// The [files]. 28 : final files = <MapEntry<String, MultipartFile>>[]; 29 : 30 : /// Whether [finalize] has been called. 31 2 : bool get isFinalized => _isFinalized; 32 : bool _isFinalized = false; 33 : 34 1 : FormData() { 35 1 : _init(); 36 : } 37 : 38 : /// Create FormData instance with a Map. 39 1 : FormData.fromMap( 40 : Map<String, dynamic> map, [ 41 : ListFormat collectionFormat = ListFormat.multi, 42 : ]) { 43 1 : _init(); 44 1 : encodeMap( 45 : map, 46 1 : (key, value) { 47 : if (value == null) return null; 48 1 : if (value is MultipartFile) { 49 3 : files.add(MapEntry(key, value)); 50 : } else { 51 4 : fields.add(MapEntry(key, value.toString())); 52 : } 53 : return null; 54 : }, 55 : listFormat: collectionFormat, 56 : encode: false, 57 : ); 58 : } 59 : 60 1 : void _init() { 61 : // Assure the boundary unpredictable and unique 62 1 : var random = Random(); 63 2 : _boundary = _BOUNDARY_PRE_TAG + 64 3 : random.nextInt(4294967296).toString().padLeft(10, '0'); 65 : } 66 : 67 : /// Returns the header string for a field. The return value is guaranteed to 68 : /// contain only ASCII characters. 69 1 : String _headerForField(String name, String value) { 70 : var header = 71 2 : 'content-disposition: form-data; name="${_browserEncode(name)}"'; 72 1 : if (!isPlainAscii(value)) { 73 0 : header = '$header\r\n' 74 : 'content-type: text/plain; charset=utf-8\r\n' 75 : 'content-transfer-encoding: binary'; 76 : } 77 1 : return '$header\r\n\r\n'; 78 : } 79 : 80 : /// Returns the header string for a file. The return value is guaranteed to 81 : /// contain only ASCII characters. 82 1 : String _headerForFile(MapEntry<String, MultipartFile> entry) { 83 1 : var file = entry.value; 84 : var header = 85 3 : 'content-disposition: form-data; name="${_browserEncode(entry.key)}"'; 86 1 : if (file.filename != null) { 87 3 : header = '$header; filename="${_browserEncode(file.filename)}"'; 88 : } 89 1 : header = '$header\r\n' 90 1 : 'content-type: ${file.contentType}'; 91 1 : if (file.headers != null) { 92 : // append additional headers 93 3 : file.headers!.forEach((key, values) { 94 2 : values.forEach((value) { 95 1 : header = '$header\r\n' 96 : '$key: $value'; 97 : }); 98 : }); 99 : } 100 1 : return '$header\r\n\r\n'; 101 : } 102 : 103 : /// Encode [value] in the same way browsers do. 104 1 : String? _browserEncode(String? value) { 105 : // http://tools.ietf.org/html/rfc2388 mandates some complex encodings for 106 : // field names and file names, but in practice user agents seem not to 107 : // follow this at all. Instead, they URL-encode `\r`, `\n`, and `\r\n` as 108 : // `\r\n`; URL-encode `"`; and do nothing else (even for `%` or non-ASCII 109 : // characters). We follow their behavior. 110 : if (value == null) { 111 : return null; 112 : } 113 3 : return value.replaceAll(_newlineRegExp, '%0D%0A').replaceAll('"', '%22'); 114 : } 115 : 116 : /// The total length of the request body, in bytes. This is calculated from 117 : /// [fields] and [files] and cannot be set manually. 118 1 : int get length { 119 : var length = 0; 120 3 : fields.forEach((entry) { 121 3 : length += '--'.length + 122 1 : _BOUNDARY_LENGTH + 123 2 : '\r\n'.length + 124 6 : utf8.encode(_headerForField(entry.key, entry.value)).length + 125 4 : utf8.encode(entry.value).length + 126 1 : '\r\n'.length; 127 : }); 128 : 129 2 : for (var file in files) { 130 3 : length += '--'.length + 131 1 : _BOUNDARY_LENGTH + 132 2 : '\r\n'.length + 133 4 : utf8.encode(_headerForFile(file)).length + 134 3 : file.value.length + 135 1 : '\r\n'.length; 136 : } 137 : 138 5 : return length + '--'.length + _BOUNDARY_LENGTH + '--\r\n'.length; 139 : } 140 : 141 1 : Stream<List<int>> finalize() { 142 1 : if (isFinalized) { 143 1 : throw StateError("Can't finalize a finalized MultipartFile."); 144 : } 145 1 : _isFinalized = true; 146 1 : var controller = StreamController<List<int>>(sync: false); 147 1 : void writeAscii(String string) { 148 2 : controller.add(utf8.encode(string)); 149 : } 150 : 151 3 : void writeUtf8(String string) => controller.add(utf8.encode(string)); 152 3 : void writeLine() => controller.add([13, 10]); // \r\n 153 : 154 3 : fields.forEach((entry) { 155 2 : writeAscii('--$boundary\r\n'); 156 3 : writeAscii(_headerForField(entry.key, entry.value)); 157 1 : writeUtf8(entry.value); 158 : writeLine(); 159 : }); 160 : 161 3 : Future.forEach<MapEntry<String, MultipartFile>>(files, (file) { 162 2 : writeAscii('--$boundary\r\n'); 163 1 : writeAscii(_headerForFile(file)); 164 3 : return writeStreamToSink(file.value.finalize(), controller) 165 2 : .then((_) => writeLine()); 166 2 : }).then((_) { 167 2 : writeAscii('--$boundary--\r\n'); 168 1 : controller.close(); 169 : }); 170 1 : return controller.stream; 171 : } 172 : 173 : ///Transform the entire FormData contents as a list of bytes asynchronously. 174 1 : Future<List<int>> readAsBytes() { 175 7 : return Future(() => finalize().reduce((a, b) => [...a, ...b])); 176 : } 177 : }