Passed
Push — master ( 73ebf0...7f192b )
by Mihail
14:15
created

UploadedFile.php (6 issues)

1
<?php
2
3
/*
4
 * This file is part of the Koded package.
5
 *
6
 * (c) Mihail Binev <[email protected]>
7
 *
8
 * Please view the LICENSE distributed with this source code
9
 * for the full copyright and license information.
10
 *
11
 */
12
13
namespace Koded\Http;
14
15
use Koded\Exceptions\KodedException;
16
use Psr\Http\Message\{StreamInterface, UploadedFileInterface};
17
use function Koded\Stdlib\randomstring;
18
19
20
class UploadedFile implements UploadedFileInterface
21
{
22
    private ?string $file;
23
    private ?string $name;
24
    private ?string $type;
25
    private ?int $size;
26
    private int  $error;
27
    private bool $moved = false;
28
29
    public function __construct(array $uploadedFile)
30
    {
31
        $this->size  = $uploadedFile['size'] ?? null;
32
        $this->file  = $uploadedFile['tmp_name'] ?? null;
33
        $this->name  = $uploadedFile['name'] ?? randomstring(9);
34
        $this->error = (int)($uploadedFile['error'] ?? \UPLOAD_ERR_OK);
35
36
        // Create a file out of the stream
37
        if ($this->file instanceof StreamInterface) {
38
            $file = \sys_get_temp_dir() . '/' . $this->name;
39
            \file_put_contents($file, $this->file->getContents());
0 ignored issues
show
The method getContents() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

39
            \file_put_contents($file, $this->file->/** @scrutinizer ignore-call */ getContents());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
40
            $this->file = $file;
41
        } elseif (false === \is_string($this->file)) {
42
            throw UploadedFileException::fileNotSupported();
43 36
        } elseif (0 === \strlen($this->file)) {
0 ignored issues
show
$this->file of type null is incompatible with the type string expected by parameter $string of strlen(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

43
        } elseif (0 === \strlen(/** @scrutinizer ignore-type */ $this->file)) {
Loading history...
44
            throw UploadedFileException::filenameCannotBeEmpty();
45 36
        }
46 36
        // Never trust the provided mime type
47 36
        $this->type = $this->getClientMediaType();
48 36
    }
49
50
    public function getStream(): StreamInterface
51 36
    {
52 1
        if ($this->moved) {
53 1
            throw UploadedFileException::streamNotAvailable();
54 1
        }
55 35
        return new FileStream($this->file, 'w+b');
0 ignored issues
show
It seems like $this->file can also be of type null; however, parameter $filename of Koded\Http\FileStream::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

55
        return new FileStream(/** @scrutinizer ignore-type */ $this->file, 'w+b');
Loading history...
56 8
    }
57 34
58 1
    public function moveTo($targetPath)
59
    {
60
        $this->assertUploadError();
61
        $this->assertTargetPath($targetPath);
62 35
        // @codeCoverageIgnoreStart
63 35
        try {
64
            $this->moved = ('cli' === \php_sapi_name())
65
                ? \rename($this->file, $targetPath)
0 ignored issues
show
It seems like $this->file can also be of type null; however, parameter $from of rename() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

65
                ? \rename(/** @scrutinizer ignore-type */ $this->file, $targetPath)
Loading history...
66 4
                : \move_uploaded_file($this->file, $targetPath);
0 ignored issues
show
It seems like $this->file can also be of type null; however, parameter $from of move_uploaded_file() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

66
                : \move_uploaded_file(/** @scrutinizer ignore-type */ $this->file, $targetPath);
Loading history...
67
68 4
            @\unlink($this->file);
0 ignored issues
show
It seems like $this->file can also be of type null; however, parameter $filename of unlink() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

68
            @\unlink(/** @scrutinizer ignore-type */ $this->file);
Loading history...
69 2
        } catch (\Throwable $e) {
70
            throw new \RuntimeException($e->getMessage());
71
        }
72 2
        // @codeCoverageIgnoreEnd
73
    }
74
75
    public function getSize(): ?int
76 14
    {
77
        return $this->size;
78 14
    }
79 13
80
    public function getError(): int
81
    {
82
        return $this->error;
83
    }
84
85
    public function getClientFilename(): ?string
86
    {
87
        return $this->name;
88
    }
89
90
    public function getClientMediaType(): ?string
91
    {
92 6
        try {
93
            return @(new \finfo(\FILEINFO_MIME_TYPE))->file($this->file);
94
        } catch (\Throwable) {
95 2
            return $this->type;
96
        }
97 2
    }
98
99
    private function assertUploadError(): void
100
    {
101 2
        if ($this->error !== \UPLOAD_ERR_OK) {
102
            throw new UploadedFileException($this->error);
103 2
        }
104
    }
105
106
    private function assertTargetPath($targetPath): void
107 2
    {
108
        if ($this->moved) {
109 2
            throw UploadedFileException::fileAlreadyMoved();
110
        }
111
        if (false === \is_string($targetPath) || 0 === \mb_strlen($targetPath)) {
112
            throw UploadedFileException::targetPathIsInvalid();
113 35
        }
114
        if (false === \is_dir($dirname = \dirname($targetPath))) {
115
            @\mkdir($dirname, 0777, true);
116 35
        }
117 1
    }
118 1
}
119
120
121
class UploadedFileException extends KodedException
122
{
123 14
    protected array $messages = [
124
        \UPLOAD_ERR_INI_SIZE   => 'The uploaded file exceeds the "upload_max_filesize" directive in php.ini',
125 14
        \UPLOAD_ERR_FORM_SIZE  => 'The uploaded file exceeds the "MAX_FILE_SIZE" directive that was specified in the HTML form',
126 1
        \UPLOAD_ERR_PARTIAL    => 'The uploaded file was only partially uploaded',
127
        \UPLOAD_ERR_NO_FILE    => 'No file was uploaded',
128 13
        \UPLOAD_ERR_NO_TMP_DIR => 'The temporary directory to write to is missing',
129
        \UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
130
        \UPLOAD_ERR_EXTENSION  => 'A PHP extension stopped the file upload',
131 13
    ];
132
133 13
    public static function streamNotAvailable(): \RuntimeException
134 2
    {
135
        return new \RuntimeException('Stream is not available, because the file was previously moved');
136
    }
137 13
138 7
    public static function targetPathIsInvalid(): \InvalidArgumentException
139
    {
140
        return new \InvalidArgumentException('The provided path for moveTo operation is not valid');
141 6
    }
142 1
143
    public static function fileAlreadyMoved(): \RuntimeException
144 6
    {
145
        return new \RuntimeException('File is not available, because it was previously moved');
146
    }
147
148
    public static function fileNotSupported(): \InvalidArgumentException
149
    {
150
        return new \InvalidArgumentException('The uploaded file is not supported');
151
    }
152
153
    public static function filenameCannotBeEmpty(): \InvalidArgumentException
154
    {
155
        return new \InvalidArgumentException('Filename cannot be empty');
156
    }
157
}
158