1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\HttpFoundation\File;
13
14use Symfony\Component\HttpFoundation\File\Exception\FileException;
15use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
16use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser;
17
18/**
19 * A file uploaded through a form.
20 *
21 * @author Bernhard Schussek <bschussek@gmail.com>
22 * @author Florian Eckerstorfer <florian@eckerstorfer.org>
23 * @author Fabien Potencier <fabien@symfony.com>
24 */
25class UploadedFile extends File
26{
27    private $test;
28    private $originalName;
29    private $mimeType;
30    private $size;
31    private $error;
32
33    /**
34     * Accepts the information of the uploaded file as provided by the PHP global $_FILES.
35     *
36     * The file object is only created when the uploaded file is valid (i.e. when the
37     * isValid() method returns true). Otherwise the only methods that could be called
38     * on an UploadedFile instance are:
39     *
40     *   * getClientOriginalName,
41     *   * getClientMimeType,
42     *   * isValid,
43     *   * getError.
44     *
45     * Calling any other method on an non-valid instance will cause an unpredictable result.
46     *
47     * @param string      $path         The full temporary path to the file
48     * @param string      $originalName The original file name of the uploaded file
49     * @param string|null $mimeType     The type of the file as provided by PHP; null defaults to application/octet-stream
50     * @param int|null    $size         The file size provided by the uploader
51     * @param int|null    $error        The error constant of the upload (one of PHP's UPLOAD_ERR_XXX constants); null defaults to UPLOAD_ERR_OK
52     * @param bool        $test         Whether the test mode is active
53     *                                  Local files are used in test mode hence the code should not enforce HTTP uploads
54     *
55     * @throws FileException         If file_uploads is disabled
56     * @throws FileNotFoundException If the file does not exist
57     */
58    public function __construct($path, $originalName, $mimeType = null, $size = null, $error = null, $test = false)
59    {
60        $this->originalName = $this->getName($originalName);
61        $this->mimeType = $mimeType ?: 'application/octet-stream';
62        $this->size = $size;
63        $this->error = $error ?: UPLOAD_ERR_OK;
64        $this->test = (bool) $test;
65
66        parent::__construct($path, UPLOAD_ERR_OK === $this->error);
67    }
68
69    /**
70     * Returns the original file name.
71     *
72     * It is extracted from the request from which the file has been uploaded.
73     * Then it should not be considered as a safe value.
74     *
75     * @return string The original name
76     */
77    public function getClientOriginalName()
78    {
79        return $this->originalName;
80    }
81
82    /**
83     * Returns the original file extension.
84     *
85     * It is extracted from the original file name that was uploaded.
86     * Then it should not be considered as a safe value.
87     *
88     * @return string The extension
89     */
90    public function getClientOriginalExtension()
91    {
92        return pathinfo($this->originalName, PATHINFO_EXTENSION);
93    }
94
95    /**
96     * Returns the file mime type.
97     *
98     * The client mime type is extracted from the request from which the file
99     * was uploaded, so it should not be considered as a safe value.
100     *
101     * For a trusted mime type, use getMimeType() instead (which guesses the mime
102     * type based on the file content).
103     *
104     * @return string The mime type
105     *
106     * @see getMimeType()
107     */
108    public function getClientMimeType()
109    {
110        return $this->mimeType;
111    }
112
113    /**
114     * Returns the extension based on the client mime type.
115     *
116     * If the mime type is unknown, returns null.
117     *
118     * This method uses the mime type as guessed by getClientMimeType()
119     * to guess the file extension. As such, the extension returned
120     * by this method cannot be trusted.
121     *
122     * For a trusted extension, use guessExtension() instead (which guesses
123     * the extension based on the guessed mime type for the file).
124     *
125     * @return string|null The guessed extension or null if it cannot be guessed
126     *
127     * @see guessExtension()
128     * @see getClientMimeType()
129     */
130    public function guessClientExtension()
131    {
132        $type = $this->getClientMimeType();
133        $guesser = ExtensionGuesser::getInstance();
134
135        return $guesser->guess($type);
136    }
137
138    /**
139     * Returns the file size.
140     *
141     * It is extracted from the request from which the file has been uploaded.
142     * Then it should not be considered as a safe value.
143     *
144     * @return int|null The file size
145     */
146    public function getClientSize()
147    {
148        return $this->size;
149    }
150
151    /**
152     * Returns the upload error.
153     *
154     * If the upload was successful, the constant UPLOAD_ERR_OK is returned.
155     * Otherwise one of the other UPLOAD_ERR_XXX constants is returned.
156     *
157     * @return int The upload error
158     */
159    public function getError()
160    {
161        return $this->error;
162    }
163
164    /**
165     * Returns whether the file was uploaded successfully.
166     *
167     * @return bool True if the file has been uploaded with HTTP and no error occurred
168     */
169    public function isValid()
170    {
171        $isOk = UPLOAD_ERR_OK === $this->error;
172
173        return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname());
174    }
175
176    /**
177     * Moves the file to a new location.
178     *
179     * @param string $directory The destination folder
180     * @param string $name      The new file name
181     *
182     * @return File A File object representing the new file
183     *
184     * @throws FileException if, for any reason, the file could not have been moved
185     */
186    public function move($directory, $name = null)
187    {
188        if ($this->isValid()) {
189            if ($this->test) {
190                return parent::move($directory, $name);
191            }
192
193            $target = $this->getTargetFile($directory, $name);
194
195            set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
196            $moved = move_uploaded_file($this->getPathname(), $target);
197            restore_error_handler();
198            if (!$moved) {
199                throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error)));
200            }
201
202            @chmod($target, 0666 & ~umask());
203
204            return $target;
205        }
206
207        throw new FileException($this->getErrorMessage());
208    }
209
210    /**
211     * Returns the maximum size of an uploaded file as configured in php.ini.
212     *
213     * @return int The maximum size of an uploaded file in bytes
214     */
215    public static function getMaxFilesize()
216    {
217        $sizePostMax = self::parseFilesize(ini_get('post_max_size'));
218        $sizeUploadMax = self::parseFilesize(ini_get('upload_max_filesize'));
219
220        return min($sizePostMax ?: PHP_INT_MAX, $sizeUploadMax ?: PHP_INT_MAX);
221    }
222
223    /**
224     * Returns the given size from an ini value in bytes.
225     *
226     * @return int The given size in bytes
227     */
228    private static function parseFilesize($size)
229    {
230        if ('' === $size) {
231            return 0;
232        }
233
234        $size = strtolower($size);
235
236        $max = ltrim($size, '+');
237        if (0 === strpos($max, '0x')) {
238            $max = \intval($max, 16);
239        } elseif (0 === strpos($max, '0')) {
240            $max = \intval($max, 8);
241        } else {
242            $max = (int) $max;
243        }
244
245        switch (substr($size, -1)) {
246            case 't': $max *= 1024;
247            // no break
248            case 'g': $max *= 1024;
249            // no break
250            case 'm': $max *= 1024;
251            // no break
252            case 'k': $max *= 1024;
253        }
254
255        return $max;
256    }
257
258    /**
259     * Returns an informative upload error message.
260     *
261     * @return string The error message regarding the specified error code
262     */
263    public function getErrorMessage()
264    {
265        static $errors = [
266            UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).',
267            UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.',
268            UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.',
269            UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
270            UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.',
271            UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.',
272            UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.',
273        ];
274
275        $errorCode = $this->error;
276        $maxFilesize = UPLOAD_ERR_INI_SIZE === $errorCode ? self::getMaxFilesize() / 1024 : 0;
277        $message = isset($errors[$errorCode]) ? $errors[$errorCode] : 'The file "%s" was not uploaded due to an unknown error.';
278
279        return sprintf($message, $this->getClientOriginalName(), $maxFilesize);
280    }
281}
282