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 image size of an image file
18 */
19class ImageSize extends AbstractValidator
20{
21    /**
22     * @const string Error constants
23     */
24    const WIDTH_TOO_BIG    = 'fileImageSizeWidthTooBig';
25    const WIDTH_TOO_SMALL  = 'fileImageSizeWidthTooSmall';
26    const HEIGHT_TOO_BIG   = 'fileImageSizeHeightTooBig';
27    const HEIGHT_TOO_SMALL = 'fileImageSizeHeightTooSmall';
28    const NOT_DETECTED     = 'fileImageSizeNotDetected';
29    const NOT_READABLE     = 'fileImageSizeNotReadable';
30
31    /**
32     * @var array Error message template
33     */
34    protected $messageTemplates = array(
35        self::WIDTH_TOO_BIG    => "Maximum allowed width for image should be '%maxwidth%' but '%width%' detected",
36        self::WIDTH_TOO_SMALL  => "Minimum expected width for image should be '%minwidth%' but '%width%' detected",
37        self::HEIGHT_TOO_BIG   => "Maximum allowed height for image should be '%maxheight%' but '%height%' detected",
38        self::HEIGHT_TOO_SMALL => "Minimum expected height for image should be '%minheight%' but '%height%' detected",
39        self::NOT_DETECTED     => "The size of image could not be detected",
40        self::NOT_READABLE     => "File is not readable or does not exist",
41    );
42
43    /**
44     * @var array Error message template variables
45     */
46    protected $messageVariables = array(
47        'minwidth'  => array('options' => 'minWidth'),
48        'maxwidth'  => array('options' => 'maxWidth'),
49        'minheight' => array('options' => 'minHeight'),
50        'maxheight' => array('options' => 'maxHeight'),
51        'width'     => 'width',
52        'height'    => 'height'
53    );
54
55    /**
56     * Detected width
57     *
58     * @var int
59     */
60    protected $width;
61
62    /**
63     * Detected height
64     *
65     * @var int
66     */
67    protected $height;
68
69    /**
70     * Options for this validator
71     *
72     * @var array
73     */
74    protected $options = array(
75        'minWidth'  => null,  // Minimum image width
76        'maxWidth'  => null,  // Maximum image width
77        'minHeight' => null,  // Minimum image height
78        'maxHeight' => null,  // Maximum image height
79    );
80
81    /**
82     * Sets validator options
83     *
84     * Accepts the following option keys:
85     * - minheight
86     * - minwidth
87     * - maxheight
88     * - maxwidth
89     *
90     * @param  array|\Traversable $options
91     */
92    public function __construct($options = null)
93    {
94        if (1 < func_num_args()) {
95            if (!is_array($options)) {
96                $options = array('minWidth' => $options);
97            }
98
99            $argv = func_get_args();
100            array_shift($argv);
101            $options['minHeight'] = array_shift($argv);
102            if (!empty($argv)) {
103                $options['maxWidth'] = array_shift($argv);
104                if (!empty($argv)) {
105                    $options['maxHeight'] = array_shift($argv);
106                }
107            }
108        }
109
110        parent::__construct($options);
111    }
112
113    /**
114     * Returns the minimum allowed width
115     *
116     * @return int
117     */
118    public function getMinWidth()
119    {
120        return $this->options['minWidth'];
121    }
122
123    /**
124     * Sets the minimum allowed width
125     *
126     * @param  int $minWidth
127     * @return ImageSize Provides a fluid interface
128     * @throws Exception\InvalidArgumentException When minwidth is greater than maxwidth
129     */
130    public function setMinWidth($minWidth)
131    {
132        if (($this->getMaxWidth() !== null) && ($minWidth > $this->getMaxWidth())) {
133            throw new Exception\InvalidArgumentException(
134                "The minimum image width must be less than or equal to the "
135                . " maximum image width, but {$minWidth} > {$this->getMaxWidth()}"
136            );
137        }
138
139        $this->options['minWidth']  = (int) $minWidth;
140        return $this;
141    }
142
143    /**
144     * Returns the maximum allowed width
145     *
146     * @return int
147     */
148    public function getMaxWidth()
149    {
150        return $this->options['maxWidth'];
151    }
152
153    /**
154     * Sets the maximum allowed width
155     *
156     * @param  int $maxWidth
157     * @return ImageSize Provides a fluid interface
158     * @throws Exception\InvalidArgumentException When maxwidth is less than minwidth
159     */
160    public function setMaxWidth($maxWidth)
161    {
162        if (($this->getMinWidth() !== null) && ($maxWidth < $this->getMinWidth())) {
163            throw new Exception\InvalidArgumentException(
164                "The maximum image width must be greater than or equal to the "
165                . "minimum image width, but {$maxWidth} < {$this->getMinWidth()}"
166            );
167        }
168
169        $this->options['maxWidth']  = (int) $maxWidth;
170        return $this;
171    }
172
173    /**
174     * Returns the minimum allowed height
175     *
176     * @return int
177     */
178    public function getMinHeight()
179    {
180        return $this->options['minHeight'];
181    }
182
183    /**
184     * Sets the minimum allowed height
185     *
186     * @param  int $minHeight
187     * @return ImageSize Provides a fluid interface
188     * @throws Exception\InvalidArgumentException When minheight is greater than maxheight
189     */
190    public function setMinHeight($minHeight)
191    {
192        if (($this->getMaxHeight() !== null) && ($minHeight > $this->getMaxHeight())) {
193            throw new Exception\InvalidArgumentException(
194                "The minimum image height must be less than or equal to the "
195                . " maximum image height, but {$minHeight} > {$this->getMaxHeight()}"
196            );
197        }
198
199        $this->options['minHeight']  = (int) $minHeight;
200        return $this;
201    }
202
203    /**
204     * Returns the maximum allowed height
205     *
206     * @return int
207     */
208    public function getMaxHeight()
209    {
210        return $this->options['maxHeight'];
211    }
212
213    /**
214     * Sets the maximum allowed height
215     *
216     * @param  int $maxHeight
217     * @return ImageSize Provides a fluid interface
218     * @throws Exception\InvalidArgumentException When maxheight is less than minheight
219     */
220    public function setMaxHeight($maxHeight)
221    {
222        if (($this->getMinHeight() !== null) && ($maxHeight < $this->getMinHeight())) {
223            throw new Exception\InvalidArgumentException(
224                "The maximum image height must be greater than or equal to the "
225                . "minimum image height, but {$maxHeight} < {$this->getMinHeight()}"
226            );
227        }
228
229        $this->options['maxHeight']  = (int) $maxHeight;
230        return $this;
231    }
232
233    /**
234     * Returns the set minimum image sizes
235     *
236     * @return array
237     */
238    public function getImageMin()
239    {
240        return array('minWidth' => $this->getMinWidth(), 'minHeight' => $this->getMinHeight());
241    }
242
243    /**
244     * Returns the set maximum image sizes
245     *
246     * @return array
247     */
248    public function getImageMax()
249    {
250        return array('maxWidth' => $this->getMaxWidth(), 'maxHeight' => $this->getMaxHeight());
251    }
252
253    /**
254     * Returns the set image width sizes
255     *
256     * @return array
257     */
258    public function getImageWidth()
259    {
260        return array('minWidth' => $this->getMinWidth(), 'maxWidth' => $this->getMaxWidth());
261    }
262
263    /**
264     * Returns the set image height sizes
265     *
266     * @return array
267     */
268    public function getImageHeight()
269    {
270        return array('minHeight' => $this->getMinHeight(), 'maxHeight' => $this->getMaxHeight());
271    }
272
273    /**
274     * Sets the minimum image size
275     *
276     * @param  array $options                 The minimum image dimensions
277     * @return ImageSize Provides a fluent interface
278     */
279    public function setImageMin($options)
280    {
281        $this->setOptions($options);
282        return $this;
283    }
284
285    /**
286     * Sets the maximum image size
287     *
288     * @param  array|\Traversable $options The maximum image dimensions
289     * @return ImageSize Provides a fluent interface
290     */
291    public function setImageMax($options)
292    {
293        $this->setOptions($options);
294        return $this;
295    }
296
297    /**
298     * Sets the minimum and maximum image width
299     *
300     * @param  array $options               The image width dimensions
301     * @return ImageSize Provides a fluent interface
302     */
303    public function setImageWidth($options)
304    {
305        $this->setImageMin($options);
306        $this->setImageMax($options);
307
308        return $this;
309    }
310
311    /**
312     * Sets the minimum and maximum image height
313     *
314     * @param  array $options               The image height dimensions
315     * @return ImageSize Provides a fluent interface
316     */
317    public function setImageHeight($options)
318    {
319        $this->setImageMin($options);
320        $this->setImageMax($options);
321
322        return $this;
323    }
324
325    /**
326     * Returns true if and only if the image size of $value is at least min and
327     * not bigger than max
328     *
329     * @param  string|array $value Real file to check for image size
330     * @param  array        $file  File data from \Zend\File\Transfer\Transfer (optional)
331     * @return bool
332     */
333    public function isValid($value, $file = null)
334    {
335        if (is_string($value) && is_array($file)) {
336            // Legacy Zend\Transfer API support
337            $filename = $file['name'];
338            $file     = $file['tmp_name'];
339        } elseif (is_array($value)) {
340            if (!isset($value['tmp_name']) || !isset($value['name'])) {
341                throw new Exception\InvalidArgumentException(
342                    'Value array must be in $_FILES format'
343                );
344            }
345            $file     = $value['tmp_name'];
346            $filename = $value['name'];
347        } else {
348            $file     = $value;
349            $filename = basename($file);
350        }
351        $this->setValue($filename);
352
353        // Is file readable ?
354        if (empty($file) || false === stream_resolve_include_path($file)) {
355            $this->error(self::NOT_READABLE);
356            return false;
357        }
358
359        ErrorHandler::start();
360        $size = getimagesize($file);
361        ErrorHandler::stop();
362
363        if (empty($size) || ($size[0] === 0) || ($size[1] === 0)) {
364            $this->error(self::NOT_DETECTED);
365            return false;
366        }
367
368        $this->width  = $size[0];
369        $this->height = $size[1];
370        if ($this->width < $this->getMinWidth()) {
371            $this->error(self::WIDTH_TOO_SMALL);
372        }
373
374        if (($this->getMaxWidth() !== null) && ($this->getMaxWidth() < $this->width)) {
375            $this->error(self::WIDTH_TOO_BIG);
376        }
377
378        if ($this->height < $this->getMinHeight()) {
379            $this->error(self::HEIGHT_TOO_SMALL);
380        }
381
382        if (($this->getMaxHeight() !== null) && ($this->getMaxHeight() < $this->height)) {
383            $this->error(self::HEIGHT_TOO_BIG);
384        }
385
386        if (count($this->getMessages()) > 0) {
387            return false;
388        }
389
390        return true;
391    }
392}
393