UploadedFileTrait.php
PHP
Path: src/Http/Message/UploadedFileTrait.php
<?php
namespace mini\Http\Message;
use Psr\Http\Message\StreamInterface;
/**
* Value object representing a file uploaded through an HTTP request.
*
* Instances of this interface are considered immutable; all methods that
* might change state MUST be implemented such that they retain the internal
* state of the current instance and return an instance that contains the
* changed state.
*/
trait UploadedFileTrait {
protected mixed $stream = null;
protected ?string $path = null;
protected ?string $filename = null;
protected ?string $mediaType = null;
protected ?string $error = null;
protected ?int $filesize = null;
/**
* @param resource|string $source A stream resource or the path to the actual file in the file system
* @param string $filename The filename as provided by the uploader
* @param string $mediaType The mime type as provided by the uploader
* @param int $filesize The filesize of the uploaded file, if known
* @param int $error The error code as provided by the uploader {$see https://www.php.net/manual/en/features.file-upload.errors.php}
* @param bool $isUploadedFile Declare that the file is in fact an uploaded file, regardless of is_uploaded_file()
*/
protected function UploadedFileTrait(mixed $source, string $filename=null, string $mediaType=null, int $filesize=null, int $error=null, bool $isUploadedFile=false) {
if (is_resource($source)) {
$this->stream = $source;
} else {
$this->path = realpath($source);
if (!$this->path) {
throw new \InvalidArgumentException("File path '$source' does not exist");
}
}
$this->source = $source;
$this->name = $filename;
$this->mediaType = $mediaType;
$this->filesize = $filesize;
$this->error = $error ?? UPLOAD_ERR_OK;
$this->isUploadedFile = $isUploadedFile;
}
/**
* Retrieve a stream representing the uploaded file.
*
* This method MUST return a StreamInterface instance, representing the
* uploaded file. The purpose of this method is to allow utilizing native PHP
* stream functionality to manipulate the file upload, such as
* stream_copy_to_stream() (though the result will need to be decorated in a
* native PHP stream wrapper to work with such functions).
*
* If the moveTo() method has been called previously, this method MUST raise
* an exception.
*
* @return StreamInterface Stream representation of the uploaded file.
* @throws \RuntimeException in cases when no stream is available.
* @throws \RuntimeException in cases when no stream can be created.
*/
public function getStream(): StreamInterface {
$this->assertNoError();
if (is_resource($this->source)) {
return new Stream($this->source);
}
if (is_string($this->source) && file_exists($this->source)) {
return new Stream(fopen($this->source, 'rbn'));
}
throw new \RuntimeException("Uploaded file is unavailable");
}
/**
* Move the uploaded file to a new location.
*
* Use this method as an alternative to move_uploaded_file(). This method is
* guaranteed to work in both SAPI and non-SAPI environments.
* Implementations must determine which environment they are in, and use the
* appropriate method (move_uploaded_file(), rename(), or a stream
* operation) to perform the operation.
*
* $targetPath may be an absolute path, or a relative path. If it is a
* relative path, resolution should be the same as used by PHP's rename()
* function.
*
* The original file or stream MUST be removed on completion.
*
* If this method is called more than once, any subsequent calls MUST raise
* an exception.
*
* When used in an SAPI environment where $_FILES is populated, when writing
* files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
* used to ensure permissions and upload status are verified correctly.
*
* If you wish to move to a stream, use getStream(), as SAPI operations
* cannot guarantee writing to stream destinations.
*
* @see http://php.net/is_uploaded_file
* @see http://php.net/move_uploaded_file
* @param string $targetPath Path to which to move the uploaded file.
* @throws \InvalidArgumentException if the $targetPath specified is invalid.
* @throws \RuntimeException on any error during the move operation.
* @throws \RuntimeException on the second or subsequent call to the method.
*/
public function moveTo($targetPath): void {
$this->assertNoError();
if ($this->stream !== null) {
// we have a stream reference to the upload, so we'll read from that
if (!rewind($this->stream)) {
throw new \RuntimeException("Unable to rewind the incoming upload stream");
}
$fp = fopen($targetPath, "xn");
if (!$fp) {
throw new \InvalidArgumentException("Unable to create file '$targetPath'");
}
while (!feof($this->stream)) {
$chunk = fread($this->stream, 8192);
if ($chunk === false) {
unlink($targetPath);
throw new \RuntimeException("fread() failed on incoming upload stream");
}
$written = fwrite($fp, $chunk);
if (!is_int($written)) {
unlink($targetPath);
throw new \RuntimeException("fwrite() failed on the destination file");
}
}
fclose($fp);
fclose($this->stream);
return;
}
if ($this->path) {
if (!(isset($_FILES) && ($this->isUploadedFile || is_uploaded_file($this->path)))) {
throw new \RuntimeException("The file is not an uploaded file");
}
rename($this->path, $targetPath);
return;
}
throw new \RuntimeException("Unable to move uploaded file");
}
/**
* Retrieve the file size.
*
* Implementations SHOULD return the value stored in the "size" key of
* the file in the $_FILES array if available, as PHP calculates this based
* on the actual size transmitted.
*
* @return int|null The file size in bytes or null if unknown.
*/
public function getSize(): ?int {
if (is_int($this->filesize)) {
return $this->filesize;
}
if ($this->stream && is_resource($this->stream)) {
$stat = fstat($this->stream);
if ($stat && key_exists('size', $stat)) {
return $this->filesize = $stat['size'];
}
}
if ($this->path && file_exists($this->path)) {
return $this->filesize = filesize($this->path);
}
return null;
}
/**
* Retrieve the error associated with the uploaded file.
*
* The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
*
* If the file was uploaded successfully, this method MUST return
* UPLOAD_ERR_OK.
*
* Implementations SHOULD return the value stored in the "error" key of
* the file in the $_FILES array.
*
* @see http://php.net/manual/en/features.file-upload.errors.php
* @return int One of PHP's UPLOAD_ERR_XXX constants.
*/
public function getError(): int {
return $this->error;
}
/**
* Retrieve the filename sent by the client.
*
* Do not trust the value returned by this method. A client could send
* a malicious filename with the intention to corrupt or hack your
* application.
*
* Implementations SHOULD return the value stored in the "name" key of
* the file in the $_FILES array.
*
* @return string|null The filename sent by the client or null if none
* was provided.
*/
public function getClientFilename(): ?string {
return $this->filename;
}
/**
* Retrieve the media type sent by the client.
*
* Do not trust the value returned by this method. A client could send
* a malicious media type with the intention to corrupt or hack your
* application.
*
* Implementations SHOULD return the value stored in the "type" key of
* the file in the $_FILES array.
*
* @return string|null The media type sent by the client or null if none
* was provided.
*/
public function getClientMediaType(): ?string {
return $this->mediaType;
}
/**
* Check that no error condition exists which should prevent this operation
*/
protected function assertNoError(): void {
if ($this->error !== UPLOAD_ERR_OK) {
throw new \RuntimeException("Upload error code ".$this->error." prevented this operation");
}
}
}