3 final class AphrontMultipartParser
extends Phobject
{
15 public function setContentType($content_type) {
16 $this->contentType
= $content_type;
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)) {
33 'Expected "multipart/form-data" content type when executing a '.
34 'multipart body read.'));
37 $type_parts = preg_split('(\s*;\s*)', $content_type);
39 foreach ($type_parts as $type_part) {
41 if (preg_match('(^boundary=(.*))', $type_part, $matches)) {
42 $boundary = $matches[1];
47 if ($boundary === null) {
49 pht('Received "multipart/form-data" request with no "boundary".'));
52 $this->parts
= array();
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;
67 switch ($this->state
) {
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.
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';
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;
103 pht('Expected "\r\n" or "--" after multipart data boundary.'));
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.
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';
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.
132 $header_buf = substr($this->buffer
, 0, $header_len);
133 $this->part
->appendRawHeader($header_buf);
135 $this->buffer
= substr($this->buffer
, $header_len +
2);
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
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
152 // Note that if we're in the preamble, we won't have a "part"
153 // object and will just discard the data.
155 $this->part
->appendData($this->buffer
);
162 if ($marker_pos > 0) {
163 // If there are bytes before the "\r",
165 $this->part
->appendData(substr($this->buffer
, 0, $marker_pos));
167 $this->buffer
= substr($this->buffer
, $marker_pos);
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"
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.
183 $this->part
->appendData(substr($this->buffer
, 0, $expect_len));
185 $this->buffer
= substr($this->buffer
, $expect_len);
190 $this->buffer
= substr($this->buffer
, $expect_len);
191 $this->state
= '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.
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
213 $this->part
->appendData("\r\n");
215 $this->state
= 'body';
219 // This is the boundary, so toss it and move on.
220 $this->buffer
= substr($this->buffer
, $expect_len);
221 $this->state
= 'endboundary';
224 // We just discard any epilogue.
231 'Unknown parser state "%s".\n',
237 public function endParse() {
238 if ($this->state
!== 'epilogue') {
241 'Expected "multipart/form-data" parse to end '.
242 'in state "epilogue".'));