Path: blob/master/src/aphront/multipartparser/AphrontMultipartParser.php
12241 views
<?php12final class AphrontMultipartParser extends Phobject {34private $contentType;5private $boundary;67private $buffer;8private $body;9private $state;1011private $part;12private $parts;1314public function setContentType($content_type) {15$this->contentType = $content_type;16return $this;17}1819public function getContentType() {20return $this->contentType;21}2223public function beginParse() {24$content_type = $this->getContentType();25if ($content_type === null) {26throw new PhutilInvalidStateException('setContentType');27}2829if (!preg_match('(^multipart/form-data)', $content_type)) {30throw new Exception(31pht(32'Expected "multipart/form-data" content type when executing a '.33'multipart body read.'));34}3536$type_parts = preg_split('(\s*;\s*)', $content_type);37$boundary = null;38foreach ($type_parts as $type_part) {39$matches = null;40if (preg_match('(^boundary=(.*))', $type_part, $matches)) {41$boundary = $matches[1];42break;43}44}4546if ($boundary === null) {47throw new Exception(48pht('Received "multipart/form-data" request with no "boundary".'));49}5051$this->parts = array();52$this->part = null;5354$this->buffer = '';55$this->boundary = $boundary;5657// We're looking for a (usually empty) body before the first boundary.58$this->state = 'bodynewline';59}6061public function continueParse($bytes) {62$this->buffer .= $bytes;6364$continue = true;65while ($continue) {66switch ($this->state) {67case 'endboundary':68// We've just parsed a boundary. Next, we expect either "--" (which69// indicates we've reached the end of the parts) or "\r\n" (which70// indicates we should read the headers for the next part).7172if (strlen($this->buffer) < 2) {73// We don't have enough bytes yet, so wait for more.74$continue = false;75break;76}7778if (!strncmp($this->buffer, '--', 2)) {79// This is "--" after a boundary, so we're done. We'll read the80// rest of the body (the "epilogue") and discard it.81$this->buffer = substr($this->buffer, 2);82$this->state = 'epilogue';8384$this->part = null;85break;86}8788if (!strncmp($this->buffer, "\r\n", 2)) {89// This is "\r\n" after a boundary, so we're going to going to90// read the headers for a part.91$this->buffer = substr($this->buffer, 2);92$this->state = 'header';9394// Create the object to hold the part we're about to read.95$part = new AphrontMultipartPart();96$this->parts[] = $part;97$this->part = $part;98break;99}100101throw new Exception(102pht('Expected "\r\n" or "--" after multipart data boundary.'));103case 'header':104// We've just parsed a boundary, followed by "\r\n". We are going105// to read the headers for this part. They are in the form of HTTP106// headers and terminated by "\r\n". The section is terminated by107// a line with no header on it.108109if (strlen($this->buffer) < 2) {110// We don't have enough data to find a "\r\n", so wait for more.111$continue = false;112break;113}114115if (!strncmp("\r\n", $this->buffer, 2)) {116// This line immediately began "\r\n", so we're done with parsing117// headers. Start parsing the body.118$this->buffer = substr($this->buffer, 2);119$this->state = 'body';120break;121}122123// This is an actual header, so look for the end of it.124$header_len = strpos($this->buffer, "\r\n");125if ($header_len === false) {126// We don't have a full header yet, so wait for more data.127$continue = false;128break;129}130131$header_buf = substr($this->buffer, 0, $header_len);132$this->part->appendRawHeader($header_buf);133134$this->buffer = substr($this->buffer, $header_len + 2);135break;136case 'body':137// We've parsed a boundary and headers, and are parsing the data for138// this part. The data is terminated by "\r\n--", then the boundary.139140// We'll look for "\r\n", then switch to the "bodynewline" state if141// we find it.142143$marker = "\r";144$marker_pos = strpos($this->buffer, $marker);145146if ($marker_pos === false) {147// There's no "\r" anywhere in the buffer, so we can just read it148// as provided. Then, since we read all the data, we're done until149// we get more.150151// Note that if we're in the preamble, we won't have a "part"152// object and will just discard the data.153if ($this->part) {154$this->part->appendData($this->buffer);155}156$this->buffer = '';157$continue = false;158break;159}160161if ($marker_pos > 0) {162// If there are bytes before the "\r",163if ($this->part) {164$this->part->appendData(substr($this->buffer, 0, $marker_pos));165}166$this->buffer = substr($this->buffer, $marker_pos);167}168169$expect = "\r\n";170$expect_len = strlen($expect);171if (strlen($this->buffer) < $expect_len) {172// We don't have enough bytes yet to know if this is "\r\n"173// or not.174$continue = false;175break;176}177178if (strncmp($this->buffer, $expect, $expect_len)) {179// The next two bytes aren't "\r\n", so eat them and go looking180// for more newlines.181if ($this->part) {182$this->part->appendData(substr($this->buffer, 0, $expect_len));183}184$this->buffer = substr($this->buffer, $expect_len);185break;186}187188// Eat the "\r\n".189$this->buffer = substr($this->buffer, $expect_len);190$this->state = 'bodynewline';191break;192case 'bodynewline':193// We've parsed a newline in a body, or we just started parsing the194// request. In either case, we're looking for "--", then the boundary.195// If we find it, this section is done. If we don't, we consume the196// bytes and move on.197198$expect = '--'.$this->boundary;199$expect_len = strlen($expect);200201if (strlen($this->buffer) < $expect_len) {202// We don't have enough bytes yet, so wait for more.203$continue = false;204break;205}206207if (strncmp($this->buffer, $expect, $expect_len)) {208// This wasn't the boundary, so return to the "body" state and209// consume it. (But first, we need to append the "\r\n" which we210// ate earlier.)211if ($this->part) {212$this->part->appendData("\r\n");213}214$this->state = 'body';215break;216}217218// This is the boundary, so toss it and move on.219$this->buffer = substr($this->buffer, $expect_len);220$this->state = 'endboundary';221break;222case 'epilogue':223// We just discard any epilogue.224$this->buffer = '';225$continue = false;226break;227default:228throw new Exception(229pht(230'Unknown parser state "%s".\n',231$this->state));232}233}234}235236public function endParse() {237if ($this->state !== 'epilogue') {238throw new Exception(239pht(240'Expected "multipart/form-data" parse to end '.241'in state "epilogue".'));242}243244return $this->parts;245}246247248}249250251