Skip to content

Instantly share code, notes, and snippets.

@thekid
Created December 13, 2019 22:48
Show Gist options
  • Select an option

  • Save thekid/e1d6a171ced83f801c398093c061a423 to your computer and use it in GitHub Desktop.

Select an option

Save thekid/e1d6a171ced83f801c398093c061a423 to your computer and use it in GitHub Desktop.
File Uploads
<?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));
}
}
}
<?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;
}
}
<?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