279 lines
8.1 KiB
PHP
279 lines
8.1 KiB
PHP
<?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
|
|
*/
|
|
protected string $file;
|
|
|
|
/**
|
|
* The client-provided file name.
|
|
*/
|
|
protected ?string $name;
|
|
|
|
/**
|
|
* The client-provided media type of the file.
|
|
*/
|
|
protected ?string $type;
|
|
|
|
protected ?int $size;
|
|
|
|
/**
|
|
* A valid PHP UPLOAD_ERR_xxx code for the file upload.
|
|
*/
|
|
protected int $error = UPLOAD_ERR_OK;
|
|
|
|
/**
|
|
* Indicates if the upload is from a SAPI environment.
|
|
*/
|
|
protected bool $sapi = false;
|
|
|
|
/**
|
|
* @var StreamInterface|null
|
|
*/
|
|
protected $stream;
|
|
|
|
/**
|
|
* Indicates if the uploaded file has already been moved.
|
|
*/
|
|
protected bool $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}
|
|
* @return StreamInterface
|
|
*/
|
|
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 self::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] = self::parseUploadedFiles($uploadedFile);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
$parsed[$field] = [];
|
|
if (!is_array($uploadedFile['error'])) {
|
|
$parsed[$field] = new static(
|
|
$uploadedFile['tmp_name'],
|
|
$uploadedFile['name'] ?? null,
|
|
$uploadedFile['type'] ?? null,
|
|
$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] = self::parseUploadedFiles($subArray);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $parsed;
|
|
}
|
|
}
|