<?php /** * Slim Framework (https://slimframework.com) * * @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License) */ declare(strict_types=1); namespace Slim\Psr7; use InvalidArgumentException; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UploadedFileInterface; use RuntimeException; use Slim\Psr7\Factory\StreamFactory; use function copy; use function dirname; use function is_array; use function is_string; use function is_uploaded_file; use function is_writable; use function move_uploaded_file; use function rename; use function sprintf; use function strpos; use function unlink; use const UPLOAD_ERR_OK; class UploadedFile implements UploadedFileInterface { /** * The client-provided full path to the file * * @var string */ protected $file; /** * The client-provided file name. * * @var string|null */ protected $name; /** * The client-provided media type of the file. * * @var string|null */ protected $type; /** * @var int|null */ protected $size; /** * A valid PHP UPLOAD_ERR_xxx code for the file upload. * * @var int */ protected $error = UPLOAD_ERR_OK; /** * Indicates if the upload is from a SAPI environment. * * @var bool */ protected $sapi = false; /** * @var StreamInterface|null */ protected $stream; /** * Indicates if the uploaded file has already been moved. * * @var bool */ protected $moved = false; /** * @param string|StreamInterface $fileNameOrStream The full path to the uploaded file provided by the client, * or a StreamInterface instance. * @param string|null $name The file name. * @param string|null $type The file media type. * @param int|null $size The file size in bytes. * @param int $error The UPLOAD_ERR_XXX code representing the status of the upload. * @param bool $sapi Indicates if the upload is in a SAPI environment. */ final public function __construct( $fileNameOrStream, ?string $name = null, ?string $type = null, ?int $size = null, int $error = UPLOAD_ERR_OK, bool $sapi = false ) { if ($fileNameOrStream instanceof StreamInterface) { $file = $fileNameOrStream->getMetadata('uri'); if (!is_string($file)) { throw new InvalidArgumentException('No URI associated with the stream.'); } $this->file = $file; $this->stream = $fileNameOrStream; } elseif (is_string($fileNameOrStream)) { $this->file = $fileNameOrStream; } else { throw new InvalidArgumentException( 'Please provide a string (full path to the uploaded file) or an instance of StreamInterface.' ); } $this->name = $name; $this->type = $type; $this->size = $size; $this->error = $error; $this->sapi = $sapi; } /** * {@inheritdoc} */ public function getStream() { if ($this->moved) { throw new RuntimeException(sprintf('Uploaded file %s has already been moved', $this->name)); } if (!$this->stream) { $this->stream = (new StreamFactory())->createStreamFromFile($this->file); } return $this->stream; } /** * {@inheritdoc} */ public function moveTo($targetPath): void { if ($this->moved) { throw new RuntimeException('Uploaded file already moved'); } $targetIsStream = strpos($targetPath, '://') > 0; if (!$targetIsStream && !is_writable(dirname($targetPath))) { throw new InvalidArgumentException('Upload target path is not writable'); } if ($targetIsStream) { if (!copy($this->file, $targetPath)) { throw new RuntimeException(sprintf('Error moving uploaded file %s to %s', $this->name, $targetPath)); } if (!unlink($this->file)) { throw new RuntimeException(sprintf('Error removing uploaded file %s', $this->name)); } } elseif ($this->sapi) { if (!is_uploaded_file($this->file)) { throw new RuntimeException(sprintf('%s is not a valid uploaded file', $this->file)); } if (!move_uploaded_file($this->file, $targetPath)) { throw new RuntimeException(sprintf('Error moving uploaded file %s to %s', $this->name, $targetPath)); } } else { if (!rename($this->file, $targetPath)) { throw new RuntimeException(sprintf('Error moving uploaded file %s to %s', $this->name, $targetPath)); } } $this->moved = true; } /** * {@inheritdoc} */ public function getError(): int { return $this->error; } /** * {@inheritdoc} */ public function getClientFilename(): ?string { return $this->name; } /** * {@inheritdoc} */ public function getClientMediaType(): ?string { return $this->type; } /** * {@inheritdoc} */ public function getSize(): ?int { return $this->size; } /** * Returns the client-provided full path to the file * * @internal This method is not part of the PSR-7 standard * * @return string */ public function getFilePath(): string { return $this->file; } /** * Create a normalized tree of UploadedFile instances from the Environment. * * @internal This method is not part of the PSR-7 standard. * * @param array $globals The global server variables. * * @return array A normalized tree of UploadedFile instances or null if none are provided. */ public static function createFromGlobals(array $globals): array { if (isset($globals['slim.files']) && is_array($globals['slim.files'])) { return $globals['slim.files']; } if (!empty($_FILES)) { return static::parseUploadedFiles($_FILES); } return []; } /** * Parse a non-normalized, i.e. $_FILES superglobal, tree of uploaded file data. * * @internal This method is not part of the PSR-7 standard. * * @param array $uploadedFiles The non-normalized tree of uploaded file data. * * @return array A normalized tree of UploadedFile instances. */ private static function parseUploadedFiles(array $uploadedFiles): array { $parsed = []; foreach ($uploadedFiles as $field => $uploadedFile) { if (!isset($uploadedFile['error'])) { if (is_array($uploadedFile)) { $parsed[$field] = static::parseUploadedFiles($uploadedFile); } continue; } $parsed[$field] = []; if (!is_array($uploadedFile['error'])) { $parsed[$field] = new static( $uploadedFile['tmp_name'], isset($uploadedFile['name']) ? $uploadedFile['name'] : null, isset($uploadedFile['type']) ? $uploadedFile['type'] : null, isset($uploadedFile['size']) ? $uploadedFile['size'] : null, $uploadedFile['error'], true ); } else { $subArray = []; foreach ($uploadedFile['error'] as $fileIdx => $error) { // Normalize sub array and re-parse to move the input's key name up a level $subArray[$fileIdx]['name'] = $uploadedFile['name'][$fileIdx]; $subArray[$fileIdx]['type'] = $uploadedFile['type'][$fileIdx]; $subArray[$fileIdx]['tmp_name'] = $uploadedFile['tmp_name'][$fileIdx]; $subArray[$fileIdx]['error'] = $uploadedFile['error'][$fileIdx]; $subArray[$fileIdx]['size'] = $uploadedFile['size'][$fileIdx]; $parsed[$field] = static::parseUploadedFiles($subArray); } } } return $parsed; } }