Line data Source code
1 : // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 : // for details. All rights reserved. Use of this source code is governed by a
3 : // BSD-style license that can be found in the LICENSE file.
4 :
5 : import 'dart:async';
6 : import 'dart:convert';
7 : import 'dart:math';
8 :
9 : import 'base_request.dart';
10 : import 'boundary_characters.dart';
11 : import 'byte_stream.dart';
12 : import 'multipart_file.dart';
13 : import 'utils.dart';
14 :
15 : final _newlineRegExp = new RegExp(r"\r\n|\r|\n");
16 :
17 : /// A `multipart/form-data` request. Such a request has both string [fields],
18 : /// which function as normal form fields, and (potentially streamed) binary
19 : /// [files].
20 : ///
21 : /// This request automatically sets the Content-Type header to
22 : /// `multipart/form-data`. This value will override any value set by the user.
23 : ///
24 : /// var uri = Uri.parse("http://pub.dartlang.org/packages/create");
25 : /// var request = new http.MultipartRequest("POST", url);
26 : /// request.fields['user'] = 'nweiz@google.com';
27 : /// request.files.add(new http.MultipartFile.fromFile(
28 : /// 'package',
29 : /// new File('build/package.tar.gz'),
30 : /// contentType: new MediaType('application', 'x-tar'));
31 : /// request.send().then((response) {
32 : /// if (response.statusCode == 200) print("Uploaded!");
33 : /// });
34 : class MultipartRequest extends BaseRequest {
35 : /// The total length of the multipart boundaries used when building the
36 : /// request body. According to http://tools.ietf.org/html/rfc1341.html, this
37 : /// can't be longer than 70.
38 : static const int _BOUNDARY_LENGTH = 70;
39 :
40 : static final Random _random = new Random();
41 :
42 : /// The form fields to send for this request.
43 : final Map<String, String> fields;
44 :
45 : /// The private version of [files].
46 : final List<MultipartFile> _files;
47 :
48 : /// Creates a new [MultipartRequest].
49 : MultipartRequest(String method, Uri url)
50 0 : : fields = {},
51 0 : _files = <MultipartFile>[],
52 0 : super(method, url);
53 :
54 : /// The list of files to upload for this request.
55 0 : List<MultipartFile> get files => _files;
56 :
57 : /// The total length of the request body, in bytes. This is calculated from
58 : /// [fields] and [files] and cannot be set manually.
59 : int get contentLength {
60 : var length = 0;
61 :
62 0 : fields.forEach((name, value) {
63 0 : length += "--".length + _BOUNDARY_LENGTH + "\r\n".length +
64 0 : UTF8.encode(_headerForField(name, value)).length +
65 0 : UTF8.encode(value).length + "\r\n".length;
66 : });
67 :
68 0 : for (var file in _files) {
69 0 : length += "--".length + _BOUNDARY_LENGTH + "\r\n".length +
70 0 : UTF8.encode(_headerForFile(file)).length +
71 0 : file.length + "\r\n".length;
72 : }
73 :
74 0 : return length + "--".length + _BOUNDARY_LENGTH + "--\r\n".length;
75 : }
76 :
77 : void set contentLength(int value) {
78 0 : throw new UnsupportedError("Cannot set the contentLength property of "
79 : "multipart requests.");
80 : }
81 :
82 : /// Freezes all mutable fields and returns a single-subscription [ByteStream]
83 : /// that will emit the request body.
84 : ByteStream finalize() {
85 : // TODO(nweiz): freeze fields and files
86 0 : var boundary = _boundaryString();
87 0 : headers['content-type'] = 'multipart/form-data; boundary=$boundary';
88 0 : super.finalize();
89 :
90 0 : var controller = new StreamController<List<int>>(sync: true);
91 :
92 : void writeAscii(String string) {
93 0 : controller.add(UTF8.encode(string));
94 : }
95 :
96 0 : writeUtf8(String string) => controller.add(UTF8.encode(string));
97 0 : writeLine() => controller.add([13, 10]); // \r\n
98 :
99 0 : fields.forEach((name, value) {
100 0 : writeAscii('--$boundary\r\n');
101 0 : writeAscii(_headerForField(name, value));
102 0 : writeUtf8(value);
103 0 : writeLine();
104 : });
105 :
106 0 : Future.forEach(_files, (file) {
107 0 : writeAscii('--$boundary\r\n');
108 0 : writeAscii(_headerForFile(file));
109 0 : return writeStreamToSink(file.finalize(), controller)
110 0 : .then((_) => writeLine());
111 0 : }).then((_) {
112 : // TODO(nweiz): pass any errors propagated through this future on to
113 : // the stream. See issue 3657.
114 0 : writeAscii('--$boundary--\r\n');
115 0 : controller.close();
116 : });
117 :
118 0 : return new ByteStream(controller.stream);
119 : }
120 :
121 : /// Returns the header string for a field. The return value is guaranteed to
122 : /// contain only ASCII characters.
123 : String _headerForField(String name, String value) {
124 : var header =
125 0 : 'content-disposition: form-data; name="${_browserEncode(name)}"';
126 0 : if (!isPlainAscii(value)) {
127 : header = '$header\r\n'
128 : 'content-type: text/plain; charset=utf-8\r\n'
129 0 : 'content-transfer-encoding: binary';
130 : }
131 0 : return '$header\r\n\r\n';
132 : }
133 :
134 : /// Returns the header string for a file. The return value is guaranteed to
135 : /// contain only ASCII characters.
136 : String _headerForFile(MultipartFile file) {
137 0 : var header = 'content-type: ${file.contentType}\r\n'
138 0 : 'content-disposition: form-data; name="${_browserEncode(file.field)}"';
139 :
140 0 : if (file.filename != null) {
141 0 : header = '$header; filename="${_browserEncode(file.filename)}"';
142 : }
143 0 : return '$header\r\n\r\n';
144 : }
145 :
146 : /// Encode [value] in the same way browsers do.
147 : String _browserEncode(String value) {
148 : // http://tools.ietf.org/html/rfc2388 mandates some complex encodings for
149 : // field names and file names, but in practice user agents seem not to
150 : // follow this at all. Instead, they URL-encode `\r`, `\n`, and `\r\n` as
151 : // `\r\n`; URL-encode `"`; and do nothing else (even for `%` or non-ASCII
152 : // characters). We follow their behavior.
153 0 : return value.replaceAll(_newlineRegExp, "%0D%0A").replaceAll('"', "%22");
154 : }
155 :
156 : /// Returns a randomly-generated multipart boundary string
157 : String _boundaryString() {
158 : var prefix = "dart-http-boundary-";
159 0 : var list = new List<int>.generate(_BOUNDARY_LENGTH - prefix.length,
160 : (index) =>
161 0 : BOUNDARY_CHARACTERS[_random.nextInt(BOUNDARY_CHARACTERS.length)],
162 : growable: false);
163 0 : return "$prefix${new String.fromCharCodes(list)}";
164 : }
165 : }
|