Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
File
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
8 / 8
29
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setAllowedTypes
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedTypes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasAllowedTypes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMaxSize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMaxSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasMaxSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validate
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
22
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Form\Element\Input;
16
17use Pop\Form\Element;
18
19/**
20 * Form file element class
21 *
22 * @category   Pop
23 * @package    Pop\Form
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    5.0.0
28 */
29
30class File extends Element\Input
31{
32
33    /**
34     * Allowed file extensions (lowercase, no leading dot). Empty = any extension allowed.
35     * @var array
36     */
37    protected array $allowedTypes = [];
38
39    /**
40     * Maximum allowed file size, in bytes. Null = no limit enforced here.
41     * @var ?int
42     */
43    protected ?int $maxSize = null;
44
45    /**
46     * Constructor
47     *
48     * Instantiate the file input form element
49     *
50     * @param  string  $name
51     * @param  ?string $value
52     * @param  ?string $indent
53     */
54    public function __construct(string $name, ?string $value = null, ?string $indent = null)
55    {
56        parent::__construct($name, 'file', $value, $indent);
57    }
58
59    /**
60     * Set the allowed file extensions (case-insensitive; leading dots are stripped)
61     *
62     * This checks the extension of the client-submitted filename only, not the file's
63     * actual content, so it is not a hard guarantee of file type (a client can rename any
64     * file). Pair it with real content validation at the storage layer if that matters.
65     *
66     * @param  array $extensions
67     * @return File
68     */
69    public function setAllowedTypes(array $extensions): File
70    {
71        $this->allowedTypes = array_map(
72            fn($extension) => strtolower(ltrim((string)$extension, '.')), $extensions
73        );
74        return $this;
75    }
76
77    /**
78     * Get the allowed file extensions
79     *
80     * @return array
81     */
82    public function getAllowedTypes(): array
83    {
84        return $this->allowedTypes;
85    }
86
87    /**
88     * Determine if the element has allowed file extensions set
89     *
90     * @return bool
91     */
92    public function hasAllowedTypes(): bool
93    {
94        return !empty($this->allowedTypes);
95    }
96
97    /**
98     * Set the maximum allowed file size, in bytes
99     *
100     * @param  int $bytes
101     * @return File
102     */
103    public function setMaxSize(int $bytes): File
104    {
105        $this->maxSize = $bytes;
106        return $this;
107    }
108
109    /**
110     * Get the maximum allowed file size, in bytes
111     *
112     * @return ?int
113     */
114    public function getMaxSize(): ?int
115    {
116        return $this->maxSize;
117    }
118
119    /**
120     * Determine if the element has a maximum file size set
121     *
122     * @return bool
123     */
124    public function hasMaxSize(): bool
125    {
126        return ($this->maxSize !== null);
127    }
128
129    /**
130     * Validate the form element object
131     *
132     * @param  array $formValues
133     * @return bool
134     */
135    public function validate(array $formValues = []): bool
136    {
137        if (($_FILES) && (isset($_FILES[$this->name]['name']))) {
138            $value = $_FILES[$this->name]['name'];
139            $size  = $_FILES[$this->name]['size'];
140        } else {
141            $value = null;
142            $size  = null;
143        }
144
145        // Check if the element is required
146        if (($this->required) && empty($value)) {
147            $this->errors[] = $this->getRequiredMessage();
148        }
149
150        // Check the file extension against the allowlist, if one is set
151        if (!empty($value) && $this->hasAllowedTypes()) {
152            $extension = strtolower((string)pathinfo((string)$value, PATHINFO_EXTENSION));
153            if (!in_array($extension, $this->allowedTypes, true)) {
154                $this->errors[] = 'The file type must be one of the following: ' .
155                    implode(', ', $this->allowedTypes) . '.';
156            }
157        }
158
159        // Check the file size against the max size, if one is set
160        if (($size !== null) && $this->hasMaxSize() && ((int)$size > $this->maxSize)) {
161            $this->errors[] = 'The file size must be less than or equal to ' .
162                \Pop\Utils\File::formatFileSize($this->maxSize) . '.';
163        }
164
165        // Check field validators
166        if (count($this->validators) > 0) {
167            foreach ($this->validators as $validator) {
168                if ($validator instanceof \Pop\Validator\ValidatorInterface) {
169                    $class =  get_class($validator);
170                    if (($size !== null) &&
171                        (('Pop\Validator\LessThanEqual' == $class) || ('Pop\Validator\GreaterThanEqual' == $class) ||
172                         ('Pop\Validator\LessThan' == $class) || ('Pop\Validator\GreaterThan' == $class))) {
173                        if (!$validator->evaluate($size)) {
174                            $this->errors[] = $validator->getMessage();
175                        }
176                    } else {
177                        if (!$validator->evaluate($value)) {
178                            $this->errors[] = $validator->getMessage();
179                        }
180                    }
181                } else if (is_callable($validator)) {
182                    $this->validateCallable($validator, $value, $formValues);
183                }
184            }
185        }
186
187        return (count($this->errors) == 0);
188    }
189
190}