Completed
Push — campaignadmin-error-check ( ad9c82 )
by Sam
10:29
created

Upload_Validator::clearErrors()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Assets;
4
5
use SilverStripe\Core\Config\Config;
6
use SilverStripe\Dev\SapphireTest;
7
8
class Upload_Validator
9
{
10
11
	/**
12
	 * Contains a list of the max file sizes shared by
13
	 * all upload fields. This is then duplicated into the
14
	 * "allowedMaxFileSize" instance property on construct.
15
	 *
16
	 * @config
17
	 * @var array
18
	 */
19
	private static $default_max_file_size = array();
20
21
	/**
22
	 * Information about the temporary file produced
23
	 * by the PHP-runtime.
24
	 *
25
	 * @var array
26
	 */
27
	protected $tmpFile;
28
29
	protected $errors = array();
30
31
	/**
32
	 * Restrict filesize for either all filetypes
33
	 * or a specific extension, with extension-name
34
	 * as array-key and the size-restriction in bytes as array-value.
35
	 *
36
	 * @var array
37
	 */
38
	public $allowedMaxFileSize = array();
39
40
	/**
41
	 * @var array Collection of extensions.
42
	 * Extension-names are treated case-insensitive.
43
	 *
44
	 * Example:
45
	 * <code>
46
	 *    array("jpg","GIF")
47
	 * </code>
48
	 */
49
	public $allowedExtensions = array();
50
51
	/**
52
	 * Return all errors that occurred while validating
53
	 * the temporary file.
54
	 *
55
	 * @return array
56
	 */
57
	public function getErrors()
58
	{
59
		return $this->errors;
60
	}
61
62
	/**
63
	 * Clear out all errors
64
	 */
65
	public function clearErrors()
66
	{
67
		$this->errors = array();
68
	}
69
70
	/**
71
	 * Set information about temporary file produced by PHP.
72
	 * @param array $tmpFile
73
	 */
74
	public function setTmpFile($tmpFile)
75
	{
76
		$this->tmpFile = $tmpFile;
77
	}
78
79
	/**
80
	 * Get maximum file size for all or specified file extension.
81
	 *
82
	 * @param string $ext
83
	 * @return int Filesize in bytes
84
	 */
85
	public function getAllowedMaxFileSize($ext = null)
86
	{
87
88
		// Check if there is any defined instance max file sizes
89
		if (empty($this->allowedMaxFileSize)) {
90
			// Set default max file sizes if there isn't
91
			$fileSize = Config::inst()->get('SilverStripe\\Assets\\Upload_Validator', 'default_max_file_size');
92
			if ($fileSize) {
93
				$this->setAllowedMaxFileSize($fileSize);
94
			} else {
95
				// When no default is present, use maximum set by PHP
96
				$maxUpload = File::ini2bytes(ini_get('upload_max_filesize'));
97
				$maxPost = File::ini2bytes(ini_get('post_max_size'));
98
				$this->setAllowedMaxFileSize(min($maxUpload, $maxPost));
99
			}
100
		}
101
102
		$ext = strtolower($ext);
103
		if ($ext) {
104
			if (isset($this->allowedMaxFileSize[$ext])) {
105
				return $this->allowedMaxFileSize[$ext];
106
			}
107
108
			$category = File::get_app_category($ext);
109
			if ($category && isset($this->allowedMaxFileSize['[' . $category . ']'])) {
110
				return $this->allowedMaxFileSize['[' . $category . ']'];
111
			}
112
		}
113
114
		return (isset($this->allowedMaxFileSize['*'])) ? $this->allowedMaxFileSize['*'] : false;
115
	}
116
117
	/**
118
	 * Set filesize maximums (in bytes or INI format).
119
	 * Automatically converts extensions to lowercase
120
	 * for easier matching.
121
	 *
122
	 * Example:
123
	 * <code>
124
	 * array('*' => 200, 'jpg' => 1000, '[doc]' => '5m')
125
	 * </code>
126
	 *
127
	 * @param array|int $rules
128
	 */
129
	public function setAllowedMaxFileSize($rules)
130
	{
131
		if (is_array($rules) && count($rules)) {
132
			// make sure all extensions are lowercase
133
			$rules = array_change_key_case($rules, CASE_LOWER);
134
			$finalRules = array();
135
136
			foreach ($rules as $rule => $value) {
137
				if (is_numeric($value)) {
138
					$tmpSize = $value;
139
				} else {
140
					$tmpSize = File::ini2bytes($value);
141
				}
142
143
				$finalRules[$rule] = (int)$tmpSize;
144
			}
145
146
			$this->allowedMaxFileSize = $finalRules;
147
		} elseif (is_string($rules)) {
148
			$this->allowedMaxFileSize['*'] = File::ini2bytes($rules);
149
		} elseif ((int)$rules > 0) {
150
			$this->allowedMaxFileSize['*'] = (int)$rules;
151
		}
152
	}
153
154
	/**
155
	 * @return array
156
	 */
157
	public function getAllowedExtensions()
158
	{
159
		return $this->allowedExtensions;
160
	}
161
162
	/**
163
	 * Limit allowed file extensions. Empty by default, allowing all extensions.
164
	 * To allow files without an extension, use an empty string.
165
	 * See {@link File::$allowed_extensions} to get a good standard set of
166
	 * extensions that are typically not harmful in a webserver context.
167
	 * See {@link setAllowedMaxFileSize()} to limit file size by extension.
168
	 *
169
	 * @param array $rules List of extensions
170
	 */
171
	public function setAllowedExtensions($rules)
172
	{
173
		if (!is_array($rules)) {
174
			return;
175
		}
176
177
		// make sure all rules are lowercase
178
		foreach ($rules as &$rule) {
179
			$rule = strtolower($rule);
180
		}
181
182
		$this->allowedExtensions = $rules;
183
	}
184
185
	/**
186
	 * Determines if the bytesize of an uploaded
187
	 * file is valid - can be defined on an
188
	 * extension-by-extension basis in {@link $allowedMaxFileSize}
189
	 *
190
	 * @return boolean
191
	 */
192
	public function isValidSize()
193
	{
194
		// If file was blocked via PHP for being excessive size, shortcut here
195
		switch ($this->tmpFile['error']) {
196
			case UPLOAD_ERR_INI_SIZE:
197
			case UPLOAD_ERR_FORM_SIZE:
198
				return false;
199
		}
200
		$pathInfo = pathinfo($this->tmpFile['name']);
201
		$extension = isset($pathInfo['extension']) ? strtolower($pathInfo['extension']) : null;
202
		$maxSize = $this->getAllowedMaxFileSize($extension);
203
		return (!$this->tmpFile['size'] || !$maxSize || (int)$this->tmpFile['size'] < $maxSize);
204
	}
205
206
	/**
207
	 * Determine if this file is valid but empty
208
	 *
209
	 * @return bool
210
	 */
211
	public function isFileEmpty() {
212
		// Don't check file size for errors
213
		if ($this->tmpFile['error'] !== UPLOAD_ERR_OK) {
214
			return false;
215
		}
216
		return empty($this->tmpFile['size']);
217
	}
218
219
	/**
220
	 * Determines if the temporary file has a valid extension
221
	 * An empty string in the validation map indicates files without an extension.
222
	 * @return boolean
223
	 */
224
	public function isValidExtension()
225
	{
226
		$pathInfo = pathinfo($this->tmpFile['name']);
227
228
		// Special case for filenames without an extension
229
		if (!isset($pathInfo['extension'])) {
230
			return in_array('', $this->allowedExtensions, true);
231
		} else {
232
			return (!count($this->allowedExtensions)
233
				|| in_array(strtolower($pathInfo['extension']), $this->allowedExtensions));
234
		}
235
	}
236
237
	/**
238
	 * Run through the rules for this validator checking against
239
	 * the temporary file set by {@link setTmpFile()} to see if
240
	 * the file is deemed valid or not.
241
	 *
242
	 * @return boolean
243
	 */
244
	public function validate()
245
	{
246
		// we don't validate for empty upload fields yet
247
		if (empty($this->tmpFile['name'])) {
248
			return true;
249
		}
250
251
		// Check file upload
252
		if (!$this->isValidUpload()) {
253
			$this->errors[] = _t('File.NOVALIDUPLOAD', 'File is not a valid upload');
254
			return false;
255
		}
256
257
		// Check file isn't empty
258
		if ($this->isFileEmpty()) {
259
			$this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.');
260
			return false;
261
		}
262
263
		// filesize validation
264 View Code Duplication
		if (!$this->isValidSize()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
265
			$pathInfo = pathinfo($this->tmpFile['name']);
266
			$ext = (isset($pathInfo['extension'])) ? $pathInfo['extension'] : '';
267
			$arg = File::format_size($this->getAllowedMaxFileSize($ext));
268
			$this->errors[] = _t(
269
				'File.TOOLARGE',
270
				'Filesize is too large, maximum {size} allowed',
271
				'Argument 1: Filesize (e.g. 1MB)',
272
				array('size' => $arg)
273
			);
274
			return false;
275
		}
276
277
		// extension validation
278 View Code Duplication
		if (!$this->isValidExtension()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
279
			$this->errors[] = _t(
280
				'File.INVALIDEXTENSION',
281
				'Extension is not allowed (valid: {extensions})',
282
				'Argument 1: Comma-separated list of valid extensions',
283
				array('extensions' => wordwrap(implode(', ', $this->allowedExtensions)))
284
			);
285
			return false;
286
		}
287
288
		return true;
289
	}
290
291
	/**
292
	 * Check that a valid file was given for upload (ignores file size)
293
	 *
294
	 * @return bool
295
	 */
296
	public function isValidUpload() {
297
		// Check file upload
298
		if ($this->tmpFile['error'] === UPLOAD_ERR_NO_FILE) {
299
			return false;
300
		}
301
302
		// Check if file is valid uploaded (with exception for unit testing)
303
		// Note that some "max file size" errors leave "temp_name" empty, so don't fail on this.
304
		$isRunningTests = (class_exists('SilverStripe\\Dev\\SapphireTest', false) && SapphireTest::is_running_test());
305
		if (!empty($this->tmpFile['tmp_name']) && !is_uploaded_file($this->tmpFile['tmp_name']) && !$isRunningTests) {
306
			return false;
307
		}
308
309
		return true;
310
	}
311
312
}
313