PluginProbe
Site Reviews / trunk
Site Reviews vtrunk
8.3.1 8.3.0 8.2.2 8.2.1 8.2.0 8.1.0 8.0.13 8.0.12 8.0.11 trunk 1.2.2 2.17.1 3.5.4 4.7.0 5.25.1 6.11.8 7.0.10 7.0.11 7.0.12 7.0.13 7.0.14 7.0.15 7.0.16 7.0.17 7.0.18 All 54 releases
site-reviews / plugin / UploadedFile.php

UploadedFile.php in Site Reviews trunk, at plugin/UploadedFile.php

206 lines 7.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 (string) 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 /* translators: %1$s: file name, %2$d: upload limit in KiB */
96 \UPLOAD_ERR_INI_SIZE => _x('The file "%1$s" exceeds the upload_max_filesize ini directive (limit is %2$d KiB).', 'file error (admin-text)', 'site-reviews'),
97 /* translators: %s: file name */
98 \UPLOAD_ERR_FORM_SIZE => _x('The file "%s" exceeds the upload limit defined in your form.', 'file error (admin-text)', 'site-reviews'),
99 /* translators: %s: file name */
100 \UPLOAD_ERR_PARTIAL => _x('The file "%s" was only partially uploaded.', 'file error (admin-text)', 'site-reviews'),
101 \UPLOAD_ERR_NO_FILE => _x('No file was uploaded.', 'file error (admin-text)', 'site-reviews'),
102 /* translators: %s: file name */
103 \UPLOAD_ERR_CANT_WRITE => _x('The file "%s" could not be written on disk.', 'file error (admin-text)', 'site-reviews'),
104 \UPLOAD_ERR_NO_TMP_DIR => _x('File could not be uploaded: missing temporary directory.', 'file error (admin-text)', 'site-reviews'),
105 \UPLOAD_ERR_EXTENSION => _x('File upload was stopped by a PHP extension.', 'file error (admin-text)', 'site-reviews'),
106 ];
107 $errorCode = $this->error;
108 $maxFilesize = \UPLOAD_ERR_INI_SIZE === $errorCode ? wp_max_upload_size() / 1024 : 0;
109 $message = $errors[$errorCode] ?? 'The file "%s" was not uploaded due to an unknown error.';
110 return sprintf($message, $this->getClientOriginalName(), $maxFilesize);
111 }
112
113 public function getExtensionFromMimeType(): string
114 {
115 $mimetypes = wp_parse_args(get_allowed_mime_types(), [
116 'json' => 'application/json',
117 ]);
118 $extensions = explode('|', array_search($this->getMimeType(), $mimetypes, true));
119 return $extensions[0] ?? $this->getExtension() ?: $this->getClientOriginalExtension();
120 }
121
122 /**
123 * This should not be considered a safe value.
124 * 1. Examine the file content for the MIME type.
125 * 2. Fallback to mime_content_type if Fileinfo fails or isn't available.
126 * 3. Fallback to client-provided MIME type.
127 */
128 public function getMimeType(): string
129 {
130 if (extension_loaded('fileinfo')) {
131 $finfo = new \finfo(\FILEINFO_MIME_TYPE);
132 if ($mimeType = $finfo->file($this->getPathname())) {
133 return $mimeType;
134 }
135 }
136 if (function_exists('mime_content_type')) {
137 if ($mimeType = mime_content_type($this->getPathname())) {
138 return $mimeType;
139 }
140 }
141 return $this->getClientMimeType();
142 }
143
144 /**
145 * Checks against the file mime type extracted from the file upload request.
146 * This should not be considered a safe check.
147 */
148 public function hasMimeType(string $mimeType): bool
149 {
150 $detectedMimeType = $this->getMimeType();
151 $csvMimeTypes = [
152 'application/csv',
153 'application/vnd.ms-excel',
154 'text/csv',
155 ];
156 if ('text/csv' === $mimeType && in_array($detectedMimeType, $csvMimeTypes)) {
157 return 'csv' === ($this->getExtension() ?: $this->getClientOriginalExtension());
158 }
159 $inconclusiveMimeTypes = [
160 'application/octet-stream',
161 'application/x-empty',
162 'text/plain',
163 ];
164 if (in_array($detectedMimeType, $inconclusiveMimeTypes)) {
165 return true;
166 }
167 return $mimeType === $detectedMimeType;
168 }
169
170 /**
171 * Returns whether the file has been uploaded with HTTP and no error occurred.
172 */
173 public function isValid(): bool
174 {
175 $isOk = \UPLOAD_ERR_OK === $this->error;
176 return $isOk && is_uploaded_file($this->getPathname());
177 }
178
179 /**
180 * @throws FileException
181 * @return \Generator<string>
182 */
183 public function streamContent(int $chunkSize = 8192): \Generator
184 {
185 $file = new \SplFileObject($this->getPathname(), 'r');
186 while (!$file->eof()) {
187 $chunk = $file->fread($chunkSize); // 8KB by default
188 if ($chunk === false) {
189 throw new FileException(sprintf('Could not stream the contents of "%s".', $this->getPathname()));
190 }
191 yield $chunk;
192 }
193 }
194
195 /**
196 * Returns locale independent base name of the given path.
197 */
198 protected function getName(string $name): string
199 {
200 $originalName = str_replace('\\', '/', $name);
201 $pos = strrpos($originalName, '/');
202 $originalName = false === $pos ? $originalName : substr($originalName, $pos + 1);
203 return $originalName;
204 }
205 }
206