Provide missing default attachment list for Files transactions
[phabricator.git] / src / aphront / multipartparser / AphrontMultipartParser.php
blobdc7d6df9eb786412b3fae5694e0e13f4efe6e8ca
1 <?php
3 final class AphrontMultipartParser extends Phobject {
5 private $contentType;
6 private $boundary;
8 private $buffer;
9 private $body;
10 private $state;
12 private $part;
13 private $parts;
15 public function setContentType($content_type) {
16 $this->contentType = $content_type;
17 return $this;
20 public function getContentType() {
21 return $this->contentType;
24 public function beginParse() {
25 $content_type = $this->getContentType();
26 if ($content_type === null) {
27 throw new PhutilInvalidStateException('setContentType');
30 if (!preg_match('(^multipart/form-data)', $content_type)) {
31 throw new Exception(
32 pht(
33 'Expected "multipart/form-data" content type when executing a '.
34 'multipart body read.'));
37 $type_parts = preg_split('(\s*;\s*)', $content_type);
38 $boundary = null;
39 foreach ($type_parts as $type_part) {
40 $matches = null;
41 if (preg_match('(^boundary=(.*))', $type_part, $matches)) {
42 $boundary = $matches[1];
43 break;
47 if ($boundary === null) {
48 throw new Exception(
49 pht('Received "multipart/form-data" request with no "boundary".'));
52 $this->parts = array();
53 $this->part = null;
55 $this->buffer = '';
56 $this->boundary = $boundary;
58 // We're looking for a (usually empty) body before the first boundary.
59 $this->state = 'bodynewline';
62 public function continueParse($bytes) {
63 $this->buffer .= $bytes;
65 $continue = true;
66 while ($continue) {
67 switch ($this->state) {
68 case 'endboundary':
69 // We've just parsed a boundary. Next, we expect either "--" (which
70 // indicates we've reached the end of the parts) or "\r\n" (which
71 // indicates we should read the headers for the next part).
73 if (strlen($this->buffer) < 2) {
74 // We don't have enough bytes yet, so wait for more.
75 $continue = false;
76 break;
79 if (!strncmp($this->buffer, '--', 2)) {
80 // This is "--" after a boundary, so we're done. We'll read the
81 // rest of the body (the "epilogue") and discard it.
82 $this->buffer = substr($this->buffer, 2);
83 $this->state = 'epilogue';
85 $this->part = null;
86 break;
89 if (!strncmp($this->buffer, "\r\n", 2)) {
90 // This is "\r\n" after a boundary, so we're going to going to
91 // read the headers for a part.
92 $this->buffer = substr($this->buffer, 2);
93 $this->state = 'header';
95 // Create the object to hold the part we're about to read.
96 $part = new AphrontMultipartPart();
97 $this->parts[] = $part;
98 $this->part = $part;
99 break;
102 throw new Exception(
103 pht('Expected "\r\n" or "--" after multipart data boundary.'));
104 case 'header':
105 // We've just parsed a boundary, followed by "\r\n". We are going
106 // to read the headers for this part. They are in the form of HTTP
107 // headers and terminated by "\r\n". The section is terminated by
108 // a line with no header on it.
110 if (strlen($this->buffer) < 2) {
111 // We don't have enough data to find a "\r\n", so wait for more.
112 $continue = false;
113 break;
116 if (!strncmp("\r\n", $this->buffer, 2)) {
117 // This line immediately began "\r\n", so we're done with parsing
118 // headers. Start parsing the body.
119 $this->buffer = substr($this->buffer, 2);
120 $this->state = 'body';
121 break;
124 // This is an actual header, so look for the end of it.
125 $header_len = strpos($this->buffer, "\r\n");
126 if ($header_len === false) {
127 // We don't have a full header yet, so wait for more data.
128 $continue = false;
129 break;
132 $header_buf = substr($this->buffer, 0, $header_len);
133 $this->part->appendRawHeader($header_buf);
135 $this->buffer = substr($this->buffer, $header_len + 2);
136 break;
137 case 'body':
138 // We've parsed a boundary and headers, and are parsing the data for
139 // this part. The data is terminated by "\r\n--", then the boundary.
141 // We'll look for "\r\n", then switch to the "bodynewline" state if
142 // we find it.
144 $marker = "\r";
145 $marker_pos = strpos($this->buffer, $marker);
147 if ($marker_pos === false) {
148 // There's no "\r" anywhere in the buffer, so we can just read it
149 // as provided. Then, since we read all the data, we're done until
150 // we get more.
152 // Note that if we're in the preamble, we won't have a "part"
153 // object and will just discard the data.
154 if ($this->part) {
155 $this->part->appendData($this->buffer);
157 $this->buffer = '';
158 $continue = false;
159 break;
162 if ($marker_pos > 0) {
163 // If there are bytes before the "\r",
164 if ($this->part) {
165 $this->part->appendData(substr($this->buffer, 0, $marker_pos));
167 $this->buffer = substr($this->buffer, $marker_pos);
170 $expect = "\r\n";
171 $expect_len = strlen($expect);
172 if (strlen($this->buffer) < $expect_len) {
173 // We don't have enough bytes yet to know if this is "\r\n"
174 // or not.
175 $continue = false;
176 break;
179 if (strncmp($this->buffer, $expect, $expect_len)) {
180 // The next two bytes aren't "\r\n", so eat them and go looking
181 // for more newlines.
182 if ($this->part) {
183 $this->part->appendData(substr($this->buffer, 0, $expect_len));
185 $this->buffer = substr($this->buffer, $expect_len);
186 break;
189 // Eat the "\r\n".
190 $this->buffer = substr($this->buffer, $expect_len);
191 $this->state = 'bodynewline';
192 break;
193 case 'bodynewline':
194 // We've parsed a newline in a body, or we just started parsing the
195 // request. In either case, we're looking for "--", then the boundary.
196 // If we find it, this section is done. If we don't, we consume the
197 // bytes and move on.
199 $expect = '--'.$this->boundary;
200 $expect_len = strlen($expect);
202 if (strlen($this->buffer) < $expect_len) {
203 // We don't have enough bytes yet, so wait for more.
204 $continue = false;
205 break;
208 if (strncmp($this->buffer, $expect, $expect_len)) {
209 // This wasn't the boundary, so return to the "body" state and
210 // consume it. (But first, we need to append the "\r\n" which we
211 // ate earlier.)
212 if ($this->part) {
213 $this->part->appendData("\r\n");
215 $this->state = 'body';
216 break;
219 // This is the boundary, so toss it and move on.
220 $this->buffer = substr($this->buffer, $expect_len);
221 $this->state = 'endboundary';
222 break;
223 case 'epilogue':
224 // We just discard any epilogue.
225 $this->buffer = '';
226 $continue = false;
227 break;
228 default:
229 throw new Exception(
230 pht(
231 'Unknown parser state "%s".\n',
232 $this->state));
237 public function endParse() {
238 if ($this->state !== 'epilogue') {
239 throw new Exception(
240 pht(
241 'Expected "multipart/form-data" parse to end '.
242 'in state "epilogue".'));
245 return $this->parts;