Test Failed
Push — develop ( 8521e7...4e95f0 )
by Paul
15:47
created

UploadedFile::streamContent()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 6
c 1
b 0
f 1
dl 0
loc 9
ccs 0
cts 0
cp 0
rs 10
cc 3
nc 3
nop 1
crap 12
1
<?php
2
3
namespace GeminiLabs\SiteReviews;
4
5
use GeminiLabs\SiteReviews\Defaults\UploadedFileDefaults;
6
use GeminiLabs\SiteReviews\Exceptions\FileException;
7
use GeminiLabs\SiteReviews\Exceptions\FileNotFoundException;
8
9
class UploadedFile extends \SplFileInfo
10
{
11
    private int $error;
12
    private string $mimeType;
13
    private string $originalName;
14
    private int $size;
15
16
    /**
17
     * @throws FileNotFoundException
18
     */
19
    public function __construct(array $filedata)
20
    {
21
        $data = glsr(UploadedFileDefaults::class)->restrict($filedata);
22
        $this->error = $data['error'] ?: \UPLOAD_ERR_OK;
23
        $this->mimeType = $data['type'] ?: 'application/octet-stream';
24
        $this->originalName = $this->getName($data['name']);
25
        $this->size = $data['size'];
26
        if (\UPLOAD_ERR_OK === $this->error && !is_file($data['tmp_name'])) {
27
            throw new FileNotFoundException($data['tmp_name']);
28
        }
29
        parent::__construct($data['tmp_name']);
30
    }
31
32
    /**
33
     * Returns the file mime type extracted from the file upload request.
34
     * This should not be considered as a safe value.
35
     */
36
    public function getClientMimeType(): string
37
    {
38
        return $this->mimeType;
39
    }
40
41
    /**
42
     * Returns the original file name extracted from the file upload request.
43
     * This should not be considered as a safe value to use for a file name on your servers.
44
     */
45
    public function getClientOriginalName(): string
46
    {
47
        return $this->originalName;
48
    }
49
50
    /**
51
     * Returns the original file extension extracted from the file upload request.
52
     * This should not be considered as a safe value to use for a file name on your servers.
53
     */
54
    public function getClientOriginalExtension(): string
55
    {
56
        return pathinfo($this->originalName, \PATHINFO_EXTENSION);
57
    }
58
59
    /**
60
     * Returns the file size extracted from the file upload request.
61
     * This should not be considered as a safe value.
62
     */
63
    public function getClientSize(): int
64
    {
65
        return $this->size;
66
    }
67
68
    /**
69
     * @throws FileException
70
     */
71
    public function getContent(): string
72
    {
73
        $content = '';
74
        foreach ($this->streamContent() as $chunk) {
75
            $content .= $chunk;
76
        }
77
        return $content;
78
    }
79
80
    /**
81
     * If the upload was successful, the constant UPLOAD_ERR_OK is returned.
82
     * Otherwise one of the other UPLOAD_ERR_XXX constants is returned.
83
     */
84
    public function getError(): int
85
    {
86
        return $this->error;
87
    }
88
89
    public function getErrorMessage(): string
90
    {
91
        if (\UPLOAD_ERR_OK === $this->error) {
92
            return '';
93
        }
94
        $errors = [
95
            \UPLOAD_ERR_INI_SIZE => _x('The file "%s" exceeds the upload_max_filesize ini directive (limit is %d KiB).', 'file error (admin-text)', 'site-reviews'),
96
            \UPLOAD_ERR_FORM_SIZE => _x('The file "%s" exceeds the upload limit defined in your form.', 'file error (admin-text)', 'site-reviews'),
97
            \UPLOAD_ERR_PARTIAL => _x('The file "%s" was only partially uploaded.', 'file error (admin-text)', 'site-reviews'),
98
            \UPLOAD_ERR_NO_FILE => _x('No file was uploaded.', 'file error (admin-text)', 'site-reviews'),
99
            \UPLOAD_ERR_CANT_WRITE => _x('The file "%s" could not be written on disk.', 'file error (admin-text)', 'site-reviews'),
100
            \UPLOAD_ERR_NO_TMP_DIR => _x('File could not be uploaded: missing temporary directory.', 'file error (admin-text)', 'site-reviews'),
101
            \UPLOAD_ERR_EXTENSION => _x('File upload was stopped by a PHP extension.', 'file error (admin-text)', 'site-reviews'),
102
        ];
103
        $errorCode = $this->error;
104
        $maxFilesize = \UPLOAD_ERR_INI_SIZE === $errorCode ? wp_max_upload_size() / 1024 : 0;
105
        $message = $errors[$errorCode] ?? 'The file "%s" was not uploaded due to an unknown error.';
106
        return sprintf($message, $this->getClientOriginalName(), $maxFilesize);
107
    }
108
109
    public function getExtensionFromMimeType(): string
110
    {
111
        $mimetypes = wp_parse_args(get_allowed_mime_types(), [
112
            'json' => 'application/json',
113
        ]);
114
        $extensions = explode('|', array_search($this->getMimeType(), $mimetypes, true));
115
        return $extensions[0] ?? $this->getExtension() ?: $this->getClientOriginalExtension();
116
    }
117
118
    /**
119
     * This should not be considered a safe value.
120
     * 1. Examine the file content for the MIME type.
121
     * 2. Fallback to mime_content_type if Fileinfo fails or isn't available.
122
     * 3. Fallback to client-provided MIME type.
123
     */
124
    public function getMimeType(): string
125
    {
126
        if (extension_loaded('fileinfo')) {
127
            $finfo = new \finfo(\FILEINFO_MIME_TYPE);
128
            if ($mimeType = $finfo->file($this->getPathname())) {
129
                return $mimeType;
130
            }
131
        }
132
        if (function_exists('mime_content_type')) {
133
            if ($mimeType = mime_content_type($this->getPathname())) {
134
                return $mimeType;
135
            }
136
        }
137
        return $this->getClientMimeType();
138
    }
139
140
    /**
141
     * Checks against the file mime type extracted from the file upload request.
142
     * This should not be considered a safe check.
143
     */
144
    public function hasMimeType(string $mimeType): bool
145
    {
146
        $detectedMimeType = $this->getMimeType();
147
        $csvMimeTypes = [
148
            'application/csv',
149
            'application/vnd.ms-excel',
150
            'text/csv',
151
        ];
152
        if ('text/csv' === $mimeType && in_array($detectedMimeType, $csvMimeTypes)) {
153
            return 'csv' === ($this->getExtension() ?: $this->getClientOriginalExtension());
154
        }
155
        $inconclusiveMimeTypes = [
156
            'application/octet-stream',
157
            'application/x-empty',
158
            'text/plain',
159
        ];
160
        if (in_array($detectedMimeType, $inconclusiveMimeTypes)) {
161
            return true;
162
        }
163
        return $mimeType === $detectedMimeType;
164
    }
165
166
    /**
167
     * Returns whether the file has been uploaded with HTTP and no error occurred.
168
     */
169
    public function isValid(): bool
170
    {
171
        $isOk = \UPLOAD_ERR_OK === $this->error;
172
        return $isOk && is_uploaded_file($this->getPathname());
173
    }
174
175
    /**
176
     * @throws FileException
177
     * @return \Generator<string>
178
     */
179
    public function streamContent(int $chunkSize = 8192): \Generator
180
    {
181
        $file = new \SplFileObject($this->getPathname(), 'r');
182
        while (!$file->eof()) {
183
            $chunk = $file->fread($chunkSize); // 8KB by default
184
            if ($chunk === false) {
185
                throw new FileException(sprintf('Could not stream the contents of "%s".', $this->getPathname()));
186
            }
187
            yield $chunk;
188
        }
189
    }
190
191
    /**
192
     * Returns locale independent base name of the given path.
193
     */
194
    protected function getName(string $name): string
195
    {
196
        $originalName = str_replace('\\', '/', $name);
197
        $pos = strrpos($originalName, '/');
198
        $originalName = false === $pos ? $originalName : substr($originalName, $pos + 1);
199
        return $originalName;
200
    }
201
}
202