depage-forms v1.4.1
html forms made easy
Loading...
Searching...
No Matches
File.php
Go to the documentation of this file.
1<?php
2
10
11namespace Depage\HtmlForm\Elements;
12
13use Depage\HtmlForm\Abstracts;
14
20class File extends Abstracts\Input
21{
22 public const UPLOAD_ERR_FILE_EXTENSION = 1000;
23
24 protected $value = [];
25
29 protected $maxNum;
30
34 protected $maxSize;
35
40
50 protected function setDefaults(): void
51 {
52 parent::setDefaults();
53
54 // textClass elements have values of type string
55 $this->defaults['maxNum'] = 1;
56 $this->defaults['maxSize'] = false;
57 $this->defaults['allowedExtensions'] = "";
58 }
59
65 public function __toString(): string
66 {
67 if ($this->maxSize !== false) {
68 $maxInput = "<input type=\"hidden\" name=\"MAX_FILE_SIZE\" value=\"{$this->maxSize}\" />";
69 } else {
70 $maxInput = "";
71 }
72
73 $value = $this->htmlValue();
74 $inputAttributes = $this->htmlInputAttributes();
75 $marker = $this->htmlMarker();
76 $label = $this->htmlLabel();
77 $list = $this->htmlList();
78 $wrapperAttributes = $this->htmlWrapperAttributes();
81
82 return "<p {$wrapperAttributes}>" .
83 "<label>" .
84 "<span class=\"depage-label\">{$label}{$marker}</span>" .
85 $maxInput .
86 "<input name=\"{$this->name}[]\" type=\"{$this->type}\"{$inputAttributes}>" .
87 $list .
88 "</label>" .
91 "</p>\n";
92 }
93
98 protected function htmlInputAttributes(): string
99 {
100 $attributes = parent::htmlInputAttributes();
101
102 if ($this->maxNum > 1) {
103 $attributes .= " multiple=\"multiple\"";
104 }
105 if (!empty($this->allowedExtensions)) {
106 $attributes .= " accept=\"" . htmlentities($this->allowedExtensions) . "\"";
107 }
108
109 return $attributes;
110 }
111
117 protected function typeCastValue(): void
118 {
119 $this->value = (array) $this->value;
120 }
121
127 public function handleUploadedFiles(?array $files = null): array
128 {
129 if (!is_array($files)) {
130 $files = [];
131 }
132 $extRegex = "";
133 if (!empty($this->allowedExtensions)) {
134 $extRegex = str_replace([" ", ",", "."], ["", "|", "\."], $this->allowedExtensions);
135 }
136 if (isset($_FILES[$this->name])) {
137 foreach ($_FILES[$this->name]["error"] as $key => $error) {
138 if (!empty($extRegex) && !preg_match("/.*(" . $extRegex . ")$/i", $_FILES[$this->name]["name"][$key])) {
139 $error = self::UPLOAD_ERR_FILE_EXTENSION;
140 }
141 if ($error == UPLOAD_ERR_OK) {
142 $uploadName = $_FILES[$this->name]["tmp_name"][$key];
143 $safeFilename = $this->sanitizeFilename($_FILES[$this->name]["name"][$key]);
144
145 // Validate MIME type
146 if (function_exists('finfo_open') && $this->isAllowedMimeType($uploadName, $safeFilename)) {
147 $uploadName = $uploadName; // ok
148 } else {
149 $this->log("htmlform: Uploaded file has disallowed MIME type.");
150 $error = self::UPLOAD_ERR_FILE_EXTENSION;
151 }
152
153 if ($error == UPLOAD_ERR_OK) {
154 $tmpName = sys_get_temp_dir() . '/htmlforms/' . session_id() . '_' . uniqid('depage-form-upload-', true);
155 $success = move_uploaded_file($uploadName, $tmpName);
156 if (!$success) {
157 $this->log("htmlform: Failed to move uploaded file to secure temp location.");
158 $error = UPLOAD_ERR_CANT_WRITE;
159 }
160 }
161
162 if ($error == UPLOAD_ERR_OK) {
163 if ($this->maxNum > 1) {
164 $files[] = [
165 'name' => $safeFilename,
166 'tmp_name' => $tmpName,
167 ];
168
169 } else {
170 $files[0] = [
171 'name' => $safeFilename,
172 'tmp_name' => $tmpName,
173 ];
174 }
175 } else {
176 if (isset($tmpName) && file_exists($tmpName)) {
177 unlink($tmpName);
178 }
179 }
180 } else {
181 $errorMsgs = [
182 UPLOAD_ERR_INI_SIZE => "The uploaded file exceeds the upload_max_filesize directive in php.ini.",
183 UPLOAD_ERR_FORM_SIZE => "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.",
184 UPLOAD_ERR_PARTIAL => "The uploaded file was only partially uploaded.",
185 UPLOAD_ERR_NO_FILE => "No file was uploaded.",
186 UPLOAD_ERR_NO_TMP_DIR => "Missing a temporary folder.",
187 UPLOAD_ERR_CANT_WRITE => "Failed to write file to disk.",
188 UPLOAD_ERR_EXTENSION => "A PHP extension stopped the file upload. PHP does not provide a way to ascertain which extension caused the file upload to stop.",
189 self::UPLOAD_ERR_FILE_EXTENSION => "The uploaded file has an unallowed extension.", // @todo add error message to form
190 ];
191 $this->log("htmlform: " . $errorMsgs[$error]);
192 // TODO can't send array here
193 // $this->log($_FILES[$this->name]);
194 }
195 }
196 }
197
198 // truncate files at max
199 $this->value = array_slice($files, - $this->maxNum, $this->maxNum);
200
201 return $this->value;
202 }
203
210 protected function sanitizeFilename(string $filename): string
211 {
212 // Use basename to strip any path components
213 $filename = basename($filename);
214
215 // Remove null bytes and non-printable characters
216 $filename = preg_replace('/[\x00-\x1f\x7f]/', '', $filename);
217
218 // Only allow safe filename characters, then trim to prevent overlong names
219 $filename = preg_replace('/[^a-zA-Z0-9\.\-_]/', '_', $filename);
220 $filename = substr($filename, 0, 255);
221
222 // Prevent hidden files and dangerous extensions
223 $filename = ltrim($filename, '.');
224
225 // Final safety: only allow safe characters
226 $filename = preg_replace('/[^a-zA-Z0-9\.\-_]/', '', $filename);
227
228 return !empty($filename) ? $filename : 'uploaded_file';
229 }
230
236 protected function getAllowedMimeTypes(): array
237 {
238 // Map common extensions to MIME types
239 $extensionMap = [
240 'jpg' => ['image/jpeg'],
241 'jpeg' => ['image/jpeg'],
242 'png' => ['image/png'],
243 'gif' => ['image/gif'],
244 'bmp' => ['image/bmp'],
245 'webp' => ['image/webp'],
246 'tiff' => ['image/tiff'],
247 'svg' => ['image/svg+xml'],
248 'pdf' => ['application/pdf'],
249 'doc' => ['application/msword'],
250 'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
251 'xls' => ['application/vnd.ms-excel'],
252 'xlsx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
253 'csv' => ['text/csv'],
254 'txt' => ['text/plain'],
255 'rtf' => ['application/rtf'],
256 'odt' => ['application/vnd.oasis.opendocument.text'],
257 'ods' => ['application/vnd.oasis.opendocument.spreadsheet'],
258 'zip' => ['application/zip'],
259 'gz' => ['application/gzip'],
260 'tar' => ['application/x-tar'],
261 'xml' => ['application/xml', 'text/xml'],
262 'json' => ['application/json'],
263 'mp3' => ['audio/mpeg'],
264 'mp4' => ['video/mp4'],
265 'mpeg' => ['video/mpeg'],
266 'ogg' => ['application/ogg', 'audio/ogg'],
267 'ogv' => ['video/ogg'],
268 'webm' => ['video/webm'],
269 'aac' => ['audio/aac'],
270 'wav' => ['audio/wav'],
271 'eps' => ['application/postscript'],
272 'ps' => ['application/postscript'],
273 ];
274
275 $allowedMimes = [];
276 if (!empty($this->allowedExtensions)) {
277 $extensions = array_map('trim', explode(',', $this->allowedExtensions));
278 foreach ($extensions as $ext) {
279 $ext = strtolower($ext);
280 if (isset($extensionMap[$ext])) {
281 $allowedMimes = array_merge($allowedMimes, $extensionMap[$ext]);
282 }
283 }
284 }
285
286 return $allowedMimes;
287 }
288
296 protected function isAllowedMimeType(string $uploadPath, string $safeFilename): bool
297 {
298 $allowedMimes = $this->getAllowedMimeTypes();
299
300 // If no extensions are specified, allow all (backwards compatible)
301 if (empty($allowedMimes)) {
302 return true;
303 }
304
305 // Use finfo for reliable MIME detection
306 if (function_exists('finfo_open')) {
307 $finfo = finfo_open(FILEINFO_MIME_TYPE);
308 $mimeType = finfo_file($finfo, $uploadPath);
309 finfo_close($finfo);
310 } else {
311 // Fallback: attempt basic detection via file extension
312 $ext = strtolower(pathinfo($safeFilename, PATHINFO_EXTENSION));
313 $extMap = [
314 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png',
315 'gif' => 'image/gif', 'bmp' => 'image/bmp', 'webp' => 'image/webp',
316 'pdf' => 'application/pdf', 'zip' => 'application/zip',
317 ];
318 $mimeType = $extMap[$ext] ?? 'application/octet-stream';
319 }
320
321 return in_array($mimeType, $allowedMimes);
322 }
323
329 public function clearValue(): void
330 {
331 $this->clearUploadedFiles();
332
333 $this->value = [];
334 }
335
339 public function clearUploadedFiles(): void
340 {
341 if (count($this->value)) {
342 foreach ($this->value as $file) {
343 if (file_exists($file['tmp_name'])) {
344 unlink($file['tmp_name']);
345 }
346 }
347 }
348 }
349}
350
351/* vim:set ft=php sw=4 sts=4 fdm=marker et : */
log(string $argument, ?string $type=null)
error & warning logger
Definition Element.php:236
htmlList(?array $options=null, array|string|null $value=null)
Renders HTML datalist.
Definition Element.php:291
input element base class
Definition Input.php:22
$label
Input element - HTML label.
Definition Input.php:37
$errorMessage
Message that gets displayed in case of invalid input.
Definition Input.php:218
htmlHelpMessage()
Returns HTML-rendered helpMessage.
Definition Input.php:685
htmlWrapperAttributes()
Returns string of HTML attributes for element wrapper paragraph.
Definition Input.php:636
htmlMarker()
Returns elements' required-indicator.
Definition Input.php:566
$helpMessage
Extra help message.
Definition Input.php:230
htmlValue()
Returns HTML-rendered element value.
Definition Input.php:656
$marker
Input element - HTML marker text that marks required fields.
Definition Input.php:92
htmlErrorMessage()
Returns HTML-rendered error message.
Definition Input.php:666
HTML file input type.
Definition File.php:21
htmlInputAttributes()
renders text element specific HTML attributes
Definition File.php:98
$maxSize
HTML maxSize attribute.
Definition File.php:34
clearValue()
resets the value to en empty array and cleans uploaded files
Definition File.php:329
typeCastValue()
Converts value to element specific type.
Definition File.php:117
$maxNum
HTML maxNum attribute.
Definition File.php:29
getAllowedMimeTypes()
Returns the list of allowed MIME types based on allowedExtensions.
Definition File.php:236
handleUploadedFiles(?array $files=null)
saves uploaded files
Definition File.php:127
__toString()
Renders element to HTML.
Definition File.php:65
clearUploadedFiles()
cleans uploaded files when session is cleared
Definition File.php:339
$allowedExtensions
HTML allowedExtensions attribute.
Definition File.php:39
isAllowedMimeType(string $uploadPath, string $safeFilename)
Checks if a file's MIME type is allowed.
Definition File.php:296
sanitizeFilename(string $filename)
Sanitizes a filename to prevent directory traversal and injection.
Definition File.php:210
setDefaults()
collects initial values across subclasses
Definition File.php:50