artur-graniszewski/ZEUS-for-PHP

View on GitHub
src/Zeus/ServerService/Http/Message/Helper/FileUpload.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace Zeus\ServerService\Http\Message\Helper;

use Zend\Http\Header\GenericHeader;
use Zend\Http\Response;
use Zeus\ServerService\Http\Message\Request;

trait FileUpload
{
    /** @var mixed[] */
    protected $currentFormDataInfo = [];

    /** @var mixed[] */
    protected $requestFilesInfo = [];

    /** @var bool */
    protected $formDataHeadersReceived = false;

    /** @var bool */
    protected $formDataReceived = false;

    /**
     * @param Request $request
     * @param string $boundaryLine
     * @param int $pos
     * @return string|null
     */
    protected function checkBodyHeaders(Request $request, $boundaryLine, $pos)
    {
        // initialize data block
        if (!isset($this->currentFormDataInfo['tmp_name'])) {
            // file not opened yet, check the boundary
            if (substr($request->getContent(), 0, $pos) !== $boundaryLine) {
                throw new \InvalidArgumentException("Boundary missing in multipart data", Response::STATUS_CODE_400);
            }

            $tmpFileName = tempnam(sys_get_temp_dir(), 'php_upload_');
            $file = fopen($tmpFileName, 'w+');
            $tmpFileName = stream_get_meta_data($file)['uri'];
            $this->currentFormDataInfo['handle'] = $file;
            $this->currentFormDataInfo['tmp_name'] = $tmpFileName;
            $this->currentFormDataInfo['type'] = 'text/plain';
            $this->currentFormDataInfo['error'] = UPLOAD_ERR_OK;
            $request->setContent(substr($request->getContent(), $pos + 2));

            return null;
        }

        // check headers
        // is this the last header?
        if ($pos === 0) {
            $this->formDataHeadersReceived = true;
            $request->setContent(substr($request->getContent(), 2));

            return null;
        }

        $headerLine = substr($request->getContent(), 0, $pos);

        // check the header...
        $header = GenericHeader::fromString($headerLine);
        $headerFieldName = strtolower($header->getFieldName());
        switch ($headerFieldName) {
            case 'content-type':
                $this->currentFormDataInfo['type'] = $header->getFieldValue();
                break;

            case 'content-disposition':
                $headerValue = $header->getFieldValue();
                $headerParts = explode(';', $headerValue);

                foreach ($headerParts as $part) {
                    $this->checkContentDisposition($part);
                }
                break;

            default:
                // @todo: validate other headers
                break;
        }

        return $headerLine;
    }

    /**
     * @param string $headerPart
     * @return $this
     */
    protected function checkContentDisposition($headerPart)
    {
        $part = trim($headerPart);
        if ($part === 'form-data') {

            return $this;
        }

        if (preg_match('~^filename="([^"]+)"$~i', $part, $matches)) {
            $this->currentFormDataInfo['name'] = $matches[1];

            return $this;
        }

        if (preg_match('~^name="([^"]+)"$~i', $part, $matches)) {
            $this->currentFormDataInfo['form_name'] = $matches[1];

            return $this;
        }

        throw new \InvalidArgumentException("Unknown content-disposition parameter: $part", Response::STATUS_CODE_400);
    }

    /**
     * @param Request $request
     * @return $this
     */
    protected function parseRequestFileData(Request $request)
    {
        $body = $request->getContent();
        $boundaryLine = $this->getMultipartDataBoundary($request);

        if (!$body || $this->formDataReceived || !$boundaryLine) {
            return $this;
        }

        $boundaryClosingLine = $boundaryLine . '--';

        while (false !== ($pos = strpos($request->getContent(), "\r\n"))) {
            if (!$this->formDataHeadersReceived) {
                $headerLine = $this->checkBodyHeaders($request, $boundaryLine, $pos);

                if (!$headerLine) {
                    continue;
                }

                $request->setContent(substr($request->getContent(), strlen($headerLine) + 2));

                continue;
            }

            if ($this->formDataHeadersReceived) {
                $bodyLine = substr($request->getContent(), 0, $pos);
                // check if there's a boundary in the buffer
                if ($bodyLine === $boundaryLine || $bodyLine === $boundaryClosingLine) {
                    $this->registerUploadedFile($request);

                    if ($bodyLine !== $boundaryClosingLine) {
                        continue;
                    }

                    $this->formDataReceived = true;

                    return $this;
                }

                fwrite($this->currentFormDataInfo['handle'], $bodyLine);
                $request->setContent(substr($request->getContent(), $pos + 2));

                continue;
            }
        }

        if (!$this->formDataHeadersReceived) {
            // headers are incomplete, fetch more data...
            return $this;
        }

        // check if there's a boundary in the buffer
        if ($request->getContent() === $boundaryLine . '--') {
            $this->registerUploadedFile($request);

            $request->setContent('');

            $this->formDataReceived = true;
            // @todo: validate if ending boundary line is exactly at the end of a request
            return $this;
        }

        // no new line found, check if its a buffer that can be sent to disk (or if it may contain part of the boundary)
        $body = $request->getContent();
        if ($body !== substr($boundaryLine, 0, strlen($body)) && $body !== substr($boundaryClosingLine, 0, strlen($body))) {
            if (substr($body, -1) === "\r") {
                // the new line at the end of a buffer may preceed the boundary string, don't write anything yet
                return $this;
            }
            fwrite($this->currentFormDataInfo['handle'], $body);
            $request->setContent('');
        }

        return $this;

        // there may be a boundary hidden here, fetch more data
    }

    /**
     * @param Request $request
     * @return $this
     */
    protected function registerUploadedFile(Request $request)
    {
        $this->formDataHeadersReceived = false;
        $this->currentFormDataInfo['size'] = filesize($this->currentFormDataInfo['tmp_name']);

        fclose($this->currentFormDataInfo['handle']);
        unset($this->currentFormDataInfo['handle']);

        if (!isset($this->currentFormDataInfo['name'])) {
            // its not a file, just a POST variable
            // for now, read it into memory
            // @todo: handle big data (stream from file to variable?)
            $request->getPost()->set($this->currentFormDataInfo['form_name'], file_get_contents($this->currentFormDataInfo['tmp_name']));
            unlink($this->currentFormDataInfo['tmp_name']);
            $this->currentFormDataInfo = [];

            return $this;
        }

        $this->requestFilesInfo[$this->currentFormDataInfo['form_name']][] = $this->currentFormDataInfo;
        $this->currentFormDataInfo = [];

        return $this;
    }

    /**
     * @param Request $request
     * @return $this
     */
    protected function mapUploadedFiles(Request $request)
    {
        $request->getFiles()->fromArray($this->requestFilesInfo);
        $this->requestFilesInfo = [];
        $this->formDataHeadersReceived = false;

        return $this;
    }

    /**
     * @return $this
     */
    protected function deleteTemporaryFiles()
    {
        foreach ($this->requestFilesInfo as $formData) {
            foreach ($formData as $file) {
                if (file_exists($file['tmp_name'])) {
                    unlink($file['tmp_name']);
                }
            }
        }

        return $this;
    }

    /**
     * @param Request $request
     * @return string
     */
    protected function getMultipartDataBoundary(Request $request)
    {
        $contentType = $request->getHeaderOverview('Content-Type', true);

        if (preg_match('~^multipart/form-data; boundary=([^\r\n]+)$~i', $contentType, $matches)) {
            return '--' . $matches[1];
        }

        // @todo: validate the above header
    }
}