@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at upstream/main 249 lines 7.9 kB view raw
1<?php 2 3final class AphrontMultipartParser extends Phobject { 4 5 private $contentType; 6 private $boundary; 7 8 private $buffer; 9 private $body; 10 private $state; 11 12 private $part; 13 private $parts; 14 15 public function setContentType($content_type) { 16 $this->contentType = $content_type; 17 return $this; 18 } 19 20 public function getContentType() { 21 return $this->contentType; 22 } 23 24 public function beginParse() { 25 $content_type = $this->getContentType(); 26 if ($content_type === null) { 27 throw new PhutilInvalidStateException('setContentType'); 28 } 29 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.')); 35 } 36 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; 44 } 45 } 46 47 if ($boundary === null) { 48 throw new Exception( 49 pht('Received "multipart/form-data" request with no "boundary".')); 50 } 51 52 $this->parts = array(); 53 $this->part = null; 54 55 $this->buffer = ''; 56 $this->boundary = $boundary; 57 58 // We're looking for a (usually empty) body before the first boundary. 59 $this->state = 'bodynewline'; 60 } 61 62 public function continueParse($bytes) { 63 $this->buffer .= $bytes; 64 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). 72 73 if (strlen($this->buffer) < 2) { 74 // We don't have enough bytes yet, so wait for more. 75 $continue = false; 76 break; 77 } 78 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'; 84 85 $this->part = null; 86 break; 87 } 88 89 if (!strncmp($this->buffer, "\r\n", 2)) { 90 // This is "\r\n" after a boundary, so we're going to read the 91 // headers for a part. 92 $this->buffer = substr($this->buffer, 2); 93 $this->state = 'header'; 94 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; 100 } 101 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. 109 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; 114 } 115 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; 122 } 123 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; 130 } 131 132 $header_buf = substr($this->buffer, 0, $header_len); 133 $this->part->appendRawHeader($header_buf); 134 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. 140 141 // We'll look for "\r\n", then switch to the "bodynewline" state if 142 // we find it. 143 144 $marker = "\r"; 145 $marker_pos = strpos($this->buffer, $marker); 146 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. 151 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); 156 } 157 $this->buffer = ''; 158 $continue = false; 159 break; 160 } 161 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)); 166 } 167 $this->buffer = substr($this->buffer, $marker_pos); 168 } 169 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; 177 } 178 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)); 184 } 185 $this->buffer = substr($this->buffer, $expect_len); 186 break; 187 } 188 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. 198 199 $expect = '--'.$this->boundary; 200 $expect_len = strlen($expect); 201 202 if (strlen($this->buffer) < $expect_len) { 203 // We don't have enough bytes yet, so wait for more. 204 $continue = false; 205 break; 206 } 207 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"); 214 } 215 $this->state = 'body'; 216 break; 217 } 218 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)); 233 } 234 } 235 } 236 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".')); 243 } 244 245 return $this->parts; 246 } 247 248 249}