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\Validator\AbstractValidator;
13use Zend\Validator\Exception;
14
15/**
16 * Validator for the maximum size of a file up to a max of 2GB
17 *
18 */
19class Upload extends AbstractValidator
20{
21    /**
22     * @const string Error constants
23     */
24    const INI_SIZE       = 'fileUploadErrorIniSize';
25    const FORM_SIZE      = 'fileUploadErrorFormSize';
26    const PARTIAL        = 'fileUploadErrorPartial';
27    const NO_FILE        = 'fileUploadErrorNoFile';
28    const NO_TMP_DIR     = 'fileUploadErrorNoTmpDir';
29    const CANT_WRITE     = 'fileUploadErrorCantWrite';
30    const EXTENSION      = 'fileUploadErrorExtension';
31    const ATTACK         = 'fileUploadErrorAttack';
32    const FILE_NOT_FOUND = 'fileUploadErrorFileNotFound';
33    const UNKNOWN        = 'fileUploadErrorUnknown';
34
35    /**
36     * @var array Error message templates
37     */
38    protected $messageTemplates = array(
39        self::INI_SIZE       => "File '%value%' exceeds the defined ini size",
40        self::FORM_SIZE      => "File '%value%' exceeds the defined form size",
41        self::PARTIAL        => "File '%value%' was only partially uploaded",
42        self::NO_FILE        => "File '%value%' was not uploaded",
43        self::NO_TMP_DIR     => "No temporary directory was found for file '%value%'",
44        self::CANT_WRITE     => "File '%value%' can't be written",
45        self::EXTENSION      => "A PHP extension returned an error while uploading the file '%value%'",
46        self::ATTACK         => "File '%value%' was illegally uploaded. This could be a possible attack",
47        self::FILE_NOT_FOUND => "File '%value%' was not found",
48        self::UNKNOWN        => "Unknown error while uploading file '%value%'"
49    );
50
51    protected $options = array(
52        'files' => array(),
53    );
54
55    /**
56     * Sets validator options
57     *
58     * The array $files must be given in syntax of Zend\File\Transfer\Transfer to be checked
59     * If no files are given the $_FILES array will be used automatically.
60     * NOTE: This validator will only work with HTTP POST uploads!
61     *
62     * @param  array|\Traversable $options Array of files in syntax of \Zend\File\Transfer\Transfer
63     */
64    public function __construct($options = array())
65    {
66        if (is_array($options) && !array_key_exists('files', $options)) {
67            $options = array('files' => $options);
68        }
69
70        parent::__construct($options);
71    }
72
73    /**
74     * Returns the array of set files
75     *
76     * @param  string $file (Optional) The file to return in detail
77     * @return array
78     * @throws Exception\InvalidArgumentException If file is not found
79     */
80    public function getFiles($file = null)
81    {
82        if ($file !== null) {
83            $return = array();
84            foreach ($this->options['files'] as $name => $content) {
85                if ($name === $file) {
86                    $return[$file] = $this->options['files'][$name];
87                }
88
89                if ($content['name'] === $file) {
90                    $return[$name] = $this->options['files'][$name];
91                }
92            }
93
94            if (count($return) === 0) {
95                throw new Exception\InvalidArgumentException("The file '$file' was not found");
96            }
97
98            return $return;
99        }
100
101        return $this->options['files'];
102    }
103
104    /**
105     * Sets the files to be checked
106     *
107     * @param  array $files The files to check in syntax of \Zend\File\Transfer\Transfer
108     * @return Upload Provides a fluent interface
109     */
110    public function setFiles($files = array())
111    {
112        if (count($files) === 0) {
113            $this->options['files'] = $_FILES;
114        } else {
115            $this->options['files'] = $files;
116        }
117
118        if ($this->options['files'] === null) {
119            $this->options['files'] = array();
120        }
121
122        foreach ($this->options['files'] as $file => $content) {
123            if (!isset($content['error'])) {
124                unset($this->options['files'][$file]);
125            }
126        }
127
128        return $this;
129    }
130
131    /**
132     * Returns true if and only if the file was uploaded without errors
133     *
134     * @param  string $value Single file to check for upload errors, when giving null the $_FILES array
135     *                       from initialization will be used
136     * @param  mixed  $file
137     * @return bool
138     */
139    public function isValid($value, $file = null)
140    {
141        $files = array();
142        $this->setValue($value);
143        if (array_key_exists($value, $this->getFiles())) {
144            $files = array_merge($files, $this->getFiles($value));
145        } else {
146            foreach ($this->getFiles() as $file => $content) {
147                if (isset($content['name']) && ($content['name'] === $value)) {
148                    $files = array_merge($files, $this->getFiles($file));
149                }
150
151                if (isset($content['tmp_name']) && ($content['tmp_name'] === $value)) {
152                    $files = array_merge($files, $this->getFiles($file));
153                }
154            }
155        }
156
157        if (empty($files)) {
158            return $this->throwError($file, self::FILE_NOT_FOUND);
159        }
160
161        foreach ($files as $file => $content) {
162            $this->value = $file;
163            switch ($content['error']) {
164                case 0:
165                    if (!is_uploaded_file($content['tmp_name'])) {
166                        $this->throwError($content, self::ATTACK);
167                    }
168                    break;
169
170                case 1:
171                    $this->throwError($content, self::INI_SIZE);
172                    break;
173
174                case 2:
175                    $this->throwError($content, self::FORM_SIZE);
176                    break;
177
178                case 3:
179                    $this->throwError($content, self::PARTIAL);
180                    break;
181
182                case 4:
183                    $this->throwError($content, self::NO_FILE);
184                    break;
185
186                case 6:
187                    $this->throwError($content, self::NO_TMP_DIR);
188                    break;
189
190                case 7:
191                    $this->throwError($content, self::CANT_WRITE);
192                    break;
193
194                case 8:
195                    $this->throwError($content, self::EXTENSION);
196                    break;
197
198                default:
199                    $this->throwError($content, self::UNKNOWN);
200                    break;
201            }
202        }
203
204        if (count($this->getMessages()) > 0) {
205            return false;
206        }
207
208        return true;
209    }
210
211    /**
212     * Throws an error of the given type
213     *
214     * @param  string $file
215     * @param  string $errorType
216     * @return false
217     */
218    protected function throwError($file, $errorType)
219    {
220        if ($file !== null) {
221            if (is_array($file)) {
222                if (array_key_exists('name', $file)) {
223                    $this->value = $file['name'];
224                }
225            } elseif (is_string($file)) {
226                $this->value = $file;
227            }
228        }
229
230        $this->error($errorType);
231        return false;
232    }
233}
234