Created
December 13, 2019 22:48
-
-
Save thekid/e1d6a171ced83f801c398093c061a423 to your computer and use it in GitHub Desktop.
File Uploads
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php namespace web; | |
| use lang\FormatException; | |
| /** @see https://tools.ietf.org/html/rfc7578 */ | |
| class MultiPart { | |
| private $request, $boundary; | |
| private $buffer= ''; | |
| public function __construct($request) { | |
| $this->request= $request; | |
| $type= new ContentType($request->header('Content-Type')); | |
| $this->boundary= $type->matches('multipart/*') ? $type->param('boundary') : null; | |
| } | |
| /** @return bool */ | |
| public function present() { return null !== $this->boundary; } | |
| private function line($in) { | |
| if (null === $this->buffer) return null; | |
| // Read until CRLF or EOF, whichever comes first. | |
| while (false === ($p= strpos($this->buffer, "\r\n"))) { | |
| if (null === ($chunk= $in->read(4096))) { | |
| $line= $this->buffer; | |
| $this->buffer= null; | |
| return $line; | |
| } | |
| $this->buffer.= $chunk; | |
| } | |
| $line= substr($this->buffer, 0, $p); | |
| $this->buffer= substr($this->buffer, $p + 2); | |
| return $line; | |
| } | |
| private function bytes($in) { | |
| if (null === $this->buffer) return; | |
| $delimiter= "\r\n--".$this->boundary; | |
| do { | |
| // Return chunks as long as no CR is encountered | |
| while (false === ($p= strpos($this->buffer, "\r"))) { | |
| yield $this->buffer; | |
| $this->buffer= $in->read(8192); | |
| } | |
| // Return eveything up until CR | |
| yield substr($this->buffer, 0, $p); | |
| $this->buffer= substr($this->buffer, $p); | |
| // Ensure we have enough in our buffer by reading another chunk | |
| if (strlen($this->buffer) < strlen($delimiter)) { | |
| $this->buffer.= $in->read(8192); | |
| } | |
| // Now check if the CR is followed by "\n--" and a boundary | |
| if (0 === strncmp($this->buffer, $delimiter, strlen($delimiter))) { | |
| $this->buffer= substr($this->buffer, 2); | |
| break; | |
| } | |
| // Not a boundary, yield the CR we found, and continue | |
| yield "\r"; | |
| $this->buffer= substr($this->buffer, 1); | |
| } while ($in->available()); | |
| } | |
| /** @return iterable */ | |
| public function parts() { | |
| if (null === $this->boundary) return; // Edge case: No parts present | |
| $next= '--'.$this->boundary; | |
| $last= '--'.$this->boundary.'--'; | |
| $in= $this->request->stream(); | |
| while ($last !== ($boundary= $this->line($in))) { | |
| if ($next !== $boundary) { | |
| throw new FormatException('Expected '."\n".$next.', have '."\n".$boundary); | |
| } | |
| $headers= []; | |
| while ($line= $this->line($in)) { | |
| sscanf($line, "%[^:]: %[^\r]", $name, $value); | |
| $headers[$name]= $value; | |
| } | |
| yield new Part($headers, $this->bytes($in)); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php namespace web; | |
| use lang\FormatException; | |
| /** | |
| * Parameterized header | |
| */ | |
| class Parameterized { | |
| protected $value; | |
| protected $params= []; | |
| private $lookup= []; | |
| /** | |
| * Creates a content type instance; either from its string representation | |
| * when passed one argument, or from a media type and parameters. | |
| * | |
| * @param string $input | |
| * @param [:string] $params | |
| * @throws lang.FormatException | |
| */ | |
| public function __construct($input, $params= null) { | |
| if (null !== $params) { | |
| $this->value= $input; | |
| foreach ($params as $name => $value) { | |
| $this->params[$name]= $value; | |
| $this->lookup[strtolower($name)]= $name; | |
| } | |
| } else if (false === ($offset= strpos($input, ';'))) { | |
| $this->value= $input; | |
| } else { | |
| $this->value= substr($input, 0, $offset); | |
| $offset++; | |
| while (false !== ($p= strpos($input, '=', $offset))) { | |
| $name= ltrim(substr($input, $offset, $p - $offset), '; '); | |
| if ('"' === $input[$p + 1]) { | |
| $offset= $p + 2; | |
| do { | |
| if (false === ($offset= strpos($input, '"', $offset))) { | |
| throw new FormatException('Unclosed string in parameter "'.$name.'"'); | |
| } | |
| } while ('\\' === $input[$offset++ - 1]); | |
| $value= strtr(substr($input, $p + 2, $offset - $p - 3), ['\"' => '"']); | |
| } else { | |
| $value= substr($input, $p + 1, strcspn($input, ';', $p) - 1); | |
| $offset= $p + strlen($value) + 1; | |
| } | |
| $this->params[$name]= $value; | |
| $this->lookup[strtolower($name)]= $name; | |
| } | |
| } | |
| } | |
| /** @return string */ | |
| public function value() { return $this->value; } | |
| /** @return [:string] */ | |
| public function params() { return $this->params; } | |
| /** | |
| * Gets a parameter by a given name | |
| * | |
| * @param string $name | |
| * @param string $default | |
| * @return string | |
| */ | |
| public function param($name, $default= null) { | |
| $name= strtolower($name); | |
| return isset($this->lookup[$name]) ? $this->params[$this->lookup[$name]] : $default; | |
| } | |
| /** @return string */ | |
| public function toString() { | |
| $result= $this->value; | |
| foreach ($this->params as $name => $value) { | |
| $result.= '; '.$name.'='.(strlen($value) === strcspn($value, '()<>@,;:\\".[]') | |
| ? $value | |
| : '"'.strtr($value, ['"' => '\"']).'"' | |
| ); | |
| } | |
| return $result; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php namespace web; | |
| use io\streams\InputStream; | |
| use lang\IllegalStateException; | |
| class Part implements InputStream { | |
| private $headers, $bytes; | |
| private $disposition= null; | |
| public function __construct($headers, $bytes) { | |
| $this->headers= $headers; | |
| $this->bytes= $bytes; | |
| } | |
| private function disposition() { | |
| if (null === $this->disposition) { | |
| $this->disposition= new Parameterized($this->headers['Content-Disposition']); | |
| } | |
| return $this->disposition; | |
| } | |
| /** @return [:string] */ | |
| public function headers() { return $this->headers; } | |
| /** @return string */ | |
| public function name() { return $this->disposition()->param('name'); } | |
| /** @return bool */ | |
| public function isFile() { return null !== $this->disposition()->param('filename'); } | |
| /** | |
| * Returns the file name | |
| * | |
| * @param bool $raw | |
| * @return string | |
| * @throws lang.IllegalStateException | |
| */ | |
| public function filename($raw= false) { | |
| if (null === ($filename= $this->disposition()->param('filename', null))) { | |
| throw new IllegalStateException('Part does not have a filename'); | |
| } | |
| return $raw ? $filename : strtr($filename, ['?' => '_', '*' => '_', '../' => '', '..\\' => '']); | |
| } | |
| public function available() { | |
| return (int)$this->bytes->valid(); | |
| } | |
| public function read($limit= 8192) { | |
| if ($this->bytes->valid()) { | |
| $bytes= $this->bytes->current(); | |
| $this->bytes->next(); | |
| return $bytes; | |
| } else { | |
| return null; // EOF | |
| } | |
| } | |
| public function value() { | |
| $value= ''; | |
| foreach ($this->bytes as $chunk) { | |
| $value.= $chunk; | |
| } | |
| return $value; | |
| } | |
| public function close() { | |
| // NOOP | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment