* @author Louis Chemineau * * @copyright Copyright (c) 2016, ownCloud GmbH. * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License, version 3, * along with this program. If not, see * */ namespace OCA\DAV\BundleUpload; use Exception; use Sabre\HTTP\RequestInterface; use Sabre\DAV\Exception\BadRequest; /** * This class is used to parse multipart/related HTTP message according to RFC http://www.rfc-archive.org/getrfc.php?rfc=2387 * This class requires a message to contain Content-length parameters, which is used in high performance reading of file contents. */ class MultipartContentsParser { /** * @var \Sabre\HTTP\RequestInterface */ // private $request; /** @var resource */ private $stream = null; /** @var string */ private $boundary = ""; private $lastBoundary = ""; /** * @var Bool */ // private $endDelimiterReached = false; /** * Constructor. */ public function __construct(RequestInterface $request) { $this->stream = $request->getBody(); if (gettype($this->stream) !== 'resource') { throw new BadRequest('Wrong body type'); } $this->boundary = '--'.$this->getBoundary($request->getHeader('Content-Type'))."\r\n"; $this->lastBoundary = '--'.$this->getBoundary($request->getHeader('Content-Type'))."--\r\n"; } /** * Parse the boundary from a Content-Type header * * @throws \Sabre\DAV\Exception\BadRequest */ private function getBoundary(string $contentType) { // Making sure the end node exists //TODO: add support for user creation if that is first sync. Currently user has to be created. // $this->userFilesHome = $this->request->getPath(); // $userFilesHomeNode = $this->server->tree->getNodeForPath($this->userFilesHome); // if (!($userFilesHomeNode instanceof FilesHome)){ // throw new Forbidden('URL endpoint has to be instance of \OCA\DAV\Files\FilesHome'); // } // $headers = array('Content-Type'); // foreach ($headers as $header) { // $value = $this->request->getHeader($header); // if ($value === null) { // throw new Forbidden(sprintf('%s header is needed', $header)); // } elseif (!is_int($value) && empty($value)) { // throw new Forbidden(sprintf('%s header must not be empty', $header)); // } // } // Validate content-type // Ex: Content-Type: "multipart/related; boundary=boundary_bf38b9b4b10a303a28ed075624db3978" [$mimeType, $boundary] = explode(';', $contentType); if (trim($mimeType) !== 'multipart/related') { throw new BadRequest('Content-Type must be multipart/related'); } // Validate boundary [$key, $value] = explode('=', $boundary); if (trim($key) !== 'boundary') { throw new BadRequest('Boundary is invalid'); } $value=trim($value); // Remove potential quotes around boundary value if (substr($value, 0, 1) == '"' && substr($value, -1) == '"') { $value = substr($value, 1, -1); } return $value; } /** * Get a line. * * If false is return, it's the end of file. * * @throws \Sabre\DAV\Exception\BadRequest */ // public function gets() { // $content = $this->getContent(); // if (!is_resource($content)) { // throw new BadRequest('Unable to get request content'); // } // return fgets($content); // } /** */ // public function getCursor() { // return ftell($this->getContent()); // } /** */ // public function getEndDelimiterReached() { // return $this->endDelimiterReached; // } /** * Return if end of file. */ public function eof() { return feof($this->stream); } /** * Seeks to offset of some file contentLength from the current cursor position in the * multipartContent. * * Return true on success and false on failure */ // public function multipartContentSeekToContentLength(int $contentLength) { // return (fseek($this->getContent(), $contentLength, SEEK_CUR) === 0 ? true : false); // } /** * Get request content. * * @throws \Sabre\DAV\Exception\BadRequest * * @return resource */ // public function getContent() { // if ($this->stream === null) { // // Pass body by reference, so other objects can have global access // $content = $this->request->getBody(); // if (!$this->stream) { // throw new BadRequest('Unable to get request content'); // } // if (gettype($this->stream) !== 'resource') { // throw new BadRequest('Wrong body type'); // } // $this->stream = $content; // } // return $this->stream; // } // public function getBoundary(string $boundary) { // return "\r\n--$boundary\r\n"; // } public function checkBoundary(string $boundary, string $line) { if ($line !== $boundary) { throw new Exception("Invalid boundary, is '$line', should be '$this->boundary'."); } return true; } public function lastBoundary() { $content = fread($this->stream, strlen($this->lastBoundary)); $result = fseek($this->stream, -strlen($this->lastBoundary), SEEK_CUR); if ($result === -1) { throw new Exception("Unknown error while seeking content"); } return $content === $this->lastBoundary; } /** * Return the next part of the request. * * @throws Exception */ public function readNextPart(int $length = 0) { $this->checkBoundary($this->boundary, fread($this->stream, strlen($this->boundary))); $headers = $this->readPartHeaders(); if ($length === 0 && isset($headers["content-length"])) { $length = $headers["content-length"]; } if ($length === 0) { throw new Exception("Part cannot be of length 0."); } $content = $this->readPartContent2($length); return [$headers, $content]; } /** * Return the next part of the request. * * @throws Exception */ public function readNextStream() { $this->checkBoundary($this->boundary, fread($this->stream, strlen($this->boundary))); $headers = $this->readPartHeaders(); return [$headers, $this->stream]; } /** * Return the headers of a part of the request. * * @throws \Sabre\DAV\Exception\BadRequest * @throws Exception */ public function readPartHeaders() { $headers = []; $blankLineCount = 0; while($blankLineCount < 1) { $line = fgets($this->stream); if ($line === false) { throw new Exception('An error appears while reading headers of a part'); } if ($line === "\r\n") { break; } try { [$key, $value] = explode(':', $line, 2); $headers[strtolower(trim($key))] = trim($value); } catch (Exception $e) { throw new BadRequest('An error appears while parsing headers of a part', $e); } } return $headers; } /** * Return the content of the current part of the stream. * * @throws \Sabre\DAV\Exception\BadRequest * @throws Exception */ public function readPartContent() { $line = ''; $content = ''; do { $content .= $line; if (feof($this->stream)) { throw new BadRequest("Unexpected EOF while reading stream."); } $line = fgets($this->stream); if ($line === false) { throw new Exception("Fail to read part's content."); } } while ($line !== $this->boundary); // We need to be before $boundary for the next parsing. $result = fseek($this->stream, -strlen($this->boundary), SEEK_CUR); if ($result === -1) { throw new Exception("Fail to seek upstream."); } // Remove the extra new line "\r\n" that is not part of the content return substr($content, 0, -2); } public function readPartContent2(int $length) { // Read stream until file's $length, EOF or $boundary is reached $content = stream_get_line($this->stream, $length); if ($content === false) { throw new Exception("Fail to read part's content."); } if (feof($this->stream)) { throw new Exception("Unexpected EOF while reading stream."); } stream_get_contents($this->stream, 2); return $content; } public function getContentPosition() { return ftell($this->stream); } public function getMetadata() { fseek($this->stream, 0); return $this->readNextPart(); } public function getContent(int $pos, int $length) { $previousPos = ftell($this->stream); $content = stream_get_contents($this->stream, $length, $pos); fseek($this->stream, $previousPos); return $content; } /** * Get a part of request separated by boundary $boundary. * * If this method returns an exception, it means whole request has to be abandoned, * Request part without correct headers might corrupt the message and parsing is impossible * * @throws \Exception */ // public function getPartHeaders(string $boundary) { // $delimiter = '--'.$boundary."\r\n"; // $endDelimiter = '--'.$boundary.'--'; // $boundaryCount = 0; // $content = ''; // $headers = null; // while (!$this->eof()) { // $line = $this->gets(); // if ($line === false) { // if ($boundaryCount == 0) { // // Empty part, ignore // break; // } // else{ // throw new \Exception('An error appears while reading and parsing header of content part using fgets'); // } // } // if ($boundaryCount == 0) { // if ($line != $delimiter) { // if ($this->getCursor() == strlen($line)) { // throw new \Exception('Expected boundary delimiter in content part - this is not a multipart request'); // } // elseif ($line == $endDelimiter || $line == $endDelimiter."\r\n") { // $this->endDelimiterReached = true; // break; // } // elseif ($line == "\r\n") { // continue; // } // } else { // continue; // } // // At this point we know, that first line was boundary // $boundaryCount++; // } // elseif ($boundaryCount == 1 && $line == "\r\n"){ // //header-end according to RFC // $content .= $line; // $headers = $this->readHeaders($content); // break; // } // elseif ($line == $endDelimiter || $line == $endDelimiter."\r\n") { // $this->endDelimiterReached = true; // break; // } // $content .= $line; // } // if ($this->eof()){ // $this->endDelimiterReached = true; // } // return $headers; // } /** * Read the contents from the current file pointer to the specified length * * @throws \Sabre\DAV\Exception\BadRequest */ // public function streamReadToString(int $length) { // if ($length<0) { // throw new BadRequest('Method streamRead cannot read contents with negative length'); // } // $source = $this->getContent(); // $bufChunkSize = 8192; // $count = $length; // $buf = ''; // while ($count!=0) { // $bufSize = (($count - $bufChunkSize)<0) ? $count : $bufChunkSize; // $buf .= fread($source, $bufSize); // $count -= $bufSize; // } // $bytesWritten = strlen($buf); // if ($length != $bytesWritten){ // throw new BadRequest('Method streamRead read '.$bytesWritten.' expected '.$length); // } // return $buf; // } /** * Read the contents from the current file pointer to the specified length and pass * * @param resource $target * * @throws \Sabre\DAV\Exception\BadRequest */ // public function streamReadToStream($target, int $length) { // if ($length<0) { // throw new BadRequest('Method streamRead cannot read contents with negative length'); // } // $source = $this->getContent(); // $bufChunkSize = 8192; // $count = $length; // $returnStatus = true; // while ($count!=0) { // $bufSize = (($count - $bufChunkSize)<0) ? $count : $bufChunkSize; // $buf = fread($source, $bufSize); // $bytesWritten = fwrite($target, $buf); // // note: strlen is expensive so only use it when necessary, // // on the last block // if ($bytesWritten === false // || ($bytesWritten < $bufSize) // ) { // // write error, could be disk full ? // $returnStatus = false; // break; // } // $count -= $bufSize; // } // return $returnStatus; // } /** * Get headers from content */ // public function readHeaders($content) { // $headers = null; // $headerLimitation = strpos($content, "\r\n\r\n"); // if ($headerLimitation === false) { // return null; // } // $headersContent = substr($content, 0, $headerLimitation); // $headersContent = trim($headersContent); // foreach (explode("\r\n", $headersContent) as $header) { // $parts = explode(':', $header, 2); // if (count($parts) != 2) { // //has incorrect header, abort // return null; // } // $headers[strtolower(trim($parts[0]))] = trim($parts[1]); // } // return $headers; // } }