1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\Validator\File;
11
12use Zend\Stdlib\ErrorHandler;
13use Zend\Validator\AbstractValidator;
14use Zend\Validator\Exception;
15
16/**
17 * Validator for the maximum size of a file up to a max of 2GB
18 */
19class Size extends AbstractValidator
20{
21    /**
22     * @const string Error constants
23     */
24    const TOO_BIG   = 'fileSizeTooBig';
25    const TOO_SMALL = 'fileSizeTooSmall';
26    const NOT_FOUND = 'fileSizeNotFound';
27
28    /**
29     * @var array Error message templates
30     */
31    protected $messageTemplates = array(
32        self::TOO_BIG   => "Maximum allowed size for file is '%max%' but '%size%' detected",
33        self::TOO_SMALL => "Minimum expected size for file is '%min%' but '%size%' detected",
34        self::NOT_FOUND => "File is not readable or does not exist",
35    );
36
37    /**
38     * @var array Error message template variables
39     */
40    protected $messageVariables = array(
41        'min'  => array('options' => 'min'),
42        'max'  => array('options' => 'max'),
43        'size' => 'size',
44    );
45
46    /**
47     * Detected size
48     *
49     * @var int
50     */
51    protected $size;
52
53    /**
54     * Options for this validator
55     *
56     * @var array
57     */
58    protected $options = array(
59        'min'           => null, // Minimum file size, if null there is no minimum
60        'max'           => null, // Maximum file size, if null there is no maximum
61        'useByteString' => true, // Use byte string?
62    );
63
64    /**
65     * Sets validator options
66     *
67     * If $options is an integer, it will be used as maximum file size
68     * As Array is accepts the following keys:
69     * 'min': Minimum file size
70     * 'max': Maximum file size
71     * 'useByteString': Use bytestring or real size for messages
72     *
73     * @param  int|array|\Traversable $options Options for the adapter
74     */
75    public function __construct($options = null)
76    {
77        if (is_string($options) || is_numeric($options)) {
78            $options = array('max' => $options);
79        }
80
81        if (1 < func_num_args()) {
82            $argv = func_get_args();
83            array_shift($argv);
84            $options['max'] = array_shift($argv);
85            if (!empty($argv)) {
86                $options['useByteString'] = array_shift($argv);
87            }
88        }
89
90        parent::__construct($options);
91    }
92
93    /**
94     * Should messages return bytes as integer or as string in SI notation
95     *
96     * @param  bool $byteString Use bytestring ?
97     * @return int
98     */
99    public function useByteString($byteString = true)
100    {
101        $this->options['useByteString'] = (bool) $byteString;
102        return $this;
103    }
104
105    /**
106     * Will bytestring be used?
107     *
108     * @return bool
109     */
110    public function getByteString()
111    {
112        return $this->options['useByteString'];
113    }
114
115    /**
116     * Returns the minimum file size
117     *
118     * @param  bool $raw Whether or not to force return of the raw value (defaults off)
119     * @return int|string
120     */
121    public function getMin($raw = false)
122    {
123        $min = $this->options['min'];
124        if (!$raw && $this->getByteString()) {
125            $min = $this->toByteString($min);
126        }
127
128        return $min;
129    }
130
131    /**
132     * Sets the minimum file size
133     *
134     * File size can be an integer or a byte string
135     * This includes 'B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'
136     * For example: 2000, 2MB, 0.2GB
137     *
138     * @param  int|string $min The minimum file size
139     * @return Size Provides a fluent interface
140     * @throws Exception\InvalidArgumentException When min is greater than max
141     */
142    public function setMin($min)
143    {
144        if (!is_string($min) and !is_numeric($min)) {
145            throw new Exception\InvalidArgumentException('Invalid options to validator provided');
146        }
147
148        $min = (int) $this->fromByteString($min);
149        $max = $this->getMax(true);
150        if (($max !== null) && ($min > $max)) {
151            throw new Exception\InvalidArgumentException(
152                "The minimum must be less than or equal to the maximum file size, but $min > $max"
153            );
154        }
155
156        $this->options['min'] = $min;
157        return $this;
158    }
159
160    /**
161     * Returns the maximum file size
162     *
163     * @param  bool $raw Whether or not to force return of the raw value (defaults off)
164     * @return int|string
165     */
166    public function getMax($raw = false)
167    {
168        $max = $this->options['max'];
169        if (!$raw && $this->getByteString()) {
170            $max = $this->toByteString($max);
171        }
172
173        return $max;
174    }
175
176    /**
177     * Sets the maximum file size
178     *
179     * File size can be an integer or a byte string
180     * This includes 'B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'
181     * For example: 2000, 2MB, 0.2GB
182     *
183     * @param  int|string $max The maximum file size
184     * @return Size Provides a fluent interface
185     * @throws Exception\InvalidArgumentException When max is smaller than min
186     */
187    public function setMax($max)
188    {
189        if (!is_string($max) && !is_numeric($max)) {
190            throw new Exception\InvalidArgumentException('Invalid options to validator provided');
191        }
192
193        $max = (int) $this->fromByteString($max);
194        $min = $this->getMin(true);
195        if (($min !== null) && ($max < $min)) {
196            throw new Exception\InvalidArgumentException(
197                "The maximum must be greater than or equal to the minimum file size, but $max < $min"
198            );
199        }
200
201        $this->options['max'] = $max;
202        return $this;
203    }
204
205    /**
206     * Retrieve current detected file size
207     *
208     * @return int
209     */
210    protected function getSize()
211    {
212        return $this->size;
213    }
214
215    /**
216     * Set current size
217     *
218     * @param  int $size
219     * @return Size
220     */
221    protected function setSize($size)
222    {
223        $this->size = $size;
224        return $this;
225    }
226
227    /**
228     * Returns true if and only if the file size of $value is at least min and
229     * not bigger than max (when max is not null).
230     *
231     * @param  string|array $value File to check for size
232     * @param  array        $file  File data from \Zend\File\Transfer\Transfer (optional)
233     * @return bool
234     */
235    public function isValid($value, $file = null)
236    {
237        if (is_string($value) && is_array($file)) {
238            // Legacy Zend\Transfer API support
239            $filename = $file['name'];
240            $file     = $file['tmp_name'];
241        } elseif (is_array($value)) {
242            if (!isset($value['tmp_name']) || !isset($value['name'])) {
243                throw new Exception\InvalidArgumentException(
244                    'Value array must be in $_FILES format'
245                );
246            }
247            $file     = $value['tmp_name'];
248            $filename = $value['name'];
249        } else {
250            $file     = $value;
251            $filename = basename($file);
252        }
253        $this->setValue($filename);
254
255        // Is file readable ?
256        if (empty($file) || false === stream_resolve_include_path($file)) {
257            $this->error(self::NOT_FOUND);
258            return false;
259        }
260
261        // limited to 4GB files
262        ErrorHandler::start();
263        $size = sprintf("%u", filesize($file));
264        ErrorHandler::stop();
265        $this->size = $size;
266
267        // Check to see if it's smaller than min size
268        $min = $this->getMin(true);
269        $max = $this->getMax(true);
270        if (($min !== null) && ($size < $min)) {
271            if ($this->getByteString()) {
272                $this->options['min'] = $this->toByteString($min);
273                $this->size          = $this->toByteString($size);
274                $this->error(self::TOO_SMALL);
275                $this->options['min'] = $min;
276                $this->size          = $size;
277            } else {
278                $this->error(self::TOO_SMALL);
279            }
280        }
281
282        // Check to see if it's larger than max size
283        if (($max !== null) && ($max < $size)) {
284            if ($this->getByteString()) {
285                $this->options['max'] = $this->toByteString($max);
286                $this->size          = $this->toByteString($size);
287                $this->error(self::TOO_BIG);
288                $this->options['max'] = $max;
289                $this->size          = $size;
290            } else {
291                $this->error(self::TOO_BIG);
292            }
293        }
294
295        if (count($this->getMessages()) > 0) {
296            return false;
297        }
298
299        return true;
300    }
301
302    /**
303     * Returns the formatted size
304     *
305     * @param  int $size
306     * @return string
307     */
308    protected function toByteString($size)
309    {
310        $sizes = array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
311        for ($i=0; $size >= 1024 && $i < 9; $i++) {
312            $size /= 1024;
313        }
314
315        return round($size, 2) . $sizes[$i];
316    }
317
318    /**
319     * Returns the unformatted size
320     *
321     * @param  string $size
322     * @return int
323     */
324    protected function fromByteString($size)
325    {
326        if (is_numeric($size)) {
327            return (int) $size;
328        }
329
330        $type  = trim(substr($size, -2, 1));
331
332        $value = substr($size, 0, -1);
333        if (!is_numeric($value)) {
334            $value = substr($value, 0, -1);
335        }
336
337        switch (strtoupper($type)) {
338            case 'Y':
339                $value *= (1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024);
340                break;
341            case 'Z':
342                $value *= (1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024);
343                break;
344            case 'E':
345                $value *= (1024 * 1024 * 1024 * 1024 * 1024 * 1024);
346                break;
347            case 'P':
348                $value *= (1024 * 1024 * 1024 * 1024 * 1024);
349                break;
350            case 'T':
351                $value *= (1024 * 1024 * 1024 * 1024);
352                break;
353            case 'G':
354                $value *= (1024 * 1024 * 1024);
355                break;
356            case 'M':
357                $value *= (1024 * 1024);
358                break;
359            case 'K':
360                $value *= 1024;
361                break;
362            default:
363                break;
364        }
365
366        return $value;
367    }
368}
369