originalName = $this->getName($originalName); $this->mimeType = $mimeType ?? 'application/octet-stream'; $this->error = $error ?? \UPLOAD_ERR_OK; $this->test = $test; parent::__construct($path); } /** * Returns the original file name. * * It is extracted from the request from which the file has been uploaded. * This should not be considered as a safe value to use for a file name on your servers. * * @return string The original name */ public function getClientOriginalName(): string { return $this->originalName; } /** * Returns the original file extension. * * It is extracted from the original file name that was uploaded. * This should not be considered as a safe value to use for a file name on your servers. * * @return string The extension */ public function getClientOriginalExtension(): string { return pathinfo($this->originalName, \PATHINFO_EXTENSION); } /** * Returns the file mime type. * * The client mime type is extracted from the request from which the file was uploaded, * so it should not be considered as a safe value. * * @return string The mime type */ public function getClientMimeType(): string { return $this->mimeType; } /** * Returns the extension based on the client mime type. * * If the mime type is unknown, returns null. * * This method uses a built-in list of mime type / extension pairs. * * @return string|null The guessed extension or null if it cannot be guessed */ public function guessClientExtension(): ?string { return self::mimeToExtension($this->mimeType); } /** * Returns the upload error. * * If the upload was successful, the constant UPLOAD_ERR_OK is returned. * Otherwise one of the other UPLOAD_ERR_XXX constants is returned. * * @return int The upload error */ public function getError(): int { return $this->error; } /** * Returns whether the file has been uploaded with HTTP and no error occurred. * * @return bool True if the file is valid, false otherwise */ public function isValid(): bool { $isOk = \UPLOAD_ERR_OK === $this->error; return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname()); } /** * Moves the file to a new location. * * @param string $directory The destination folder * @param string|null $name The new file name * * @return \SplFileInfo A SplFileInfo object for the new file * * @throws \RuntimeException if the file cannot be moved */ public function move(string $directory, ?string $name = null): \SplFileInfo { if ($this->isValid()) { if ($this->test) { return $this->doMove($directory, $name); } $target = $this->getTargetFile($directory, $name); if (!@move_uploaded_file($this->getPathname(), $target)) { $error = error_get_last(); throw new \RuntimeException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error['message'] ?? 'unknown error'))); } @chmod($target, 0666 & ~umask()); return new \SplFileInfo($target); } throw new \RuntimeException($this->getErrorMessage()); } /** * Returns the maximum size of an uploaded file as configured in php.ini. * * @return int|float The maximum size of an uploaded file in bytes (returns float on 32-bit for large values) */ public static function getMaxFilesize(): int|float { $sizePostMax = self::parseFilesize(\ini_get('post_max_size')); $sizeUploadMax = self::parseFilesize(\ini_get('upload_max_filesize')); return min($sizePostMax ?: \PHP_INT_MAX, $sizeUploadMax ?: \PHP_INT_MAX); } /** * Returns an informative upload error message. * * @return string The error message regarding the specified error code */ public function getErrorMessage(): string { return match ($this->error) { \UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive.', \UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.', \UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.', \UPLOAD_ERR_NO_FILE => 'No file was uploaded.', \UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.', \UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.', \UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.', default => 'The file "%s" was not uploaded due to an unknown error.', }; } /** * Returns locale independent base name of the given path. * * @param string $name The new file name * * @return string The base name */ protected function getName(string $name): string { $originalName = str_replace('\\', '/', $name); $pos = strrpos($originalName, '/'); $originalName = false === $pos ? $originalName : substr($originalName, $pos + 1); return $originalName; } protected function getTargetFile(string $directory, ?string $name = null): string { if (!is_dir($directory)) { if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) { throw new \RuntimeException(sprintf('Unable to create the "%s" directory.', $directory)); } } elseif (!is_writable($directory)) { throw new \RuntimeException(sprintf('Unable to write in the "%s" directory.', $directory)); } $target = rtrim($directory, '/\\') . \DIRECTORY_SEPARATOR . (null === $name ? $this->getBasename() : $this->getName($name)); return $target; } /** * Moves the file to a new location (used in test mode). */ protected function doMove(string $directory, ?string $name = null): \SplFileInfo { $target = $this->getTargetFile($directory, $name); if (!@rename($this->getPathname(), $target)) { $error = error_get_last(); throw new \RuntimeException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error['message'] ?? 'unknown error'))); } @chmod($target, 0666 & ~umask()); return new \SplFileInfo($target); } private static function parseFilesize(string $size): int|float { if ('' === $size) { return 0; } $size = strtolower($size); $max = ltrim($size, '+'); if (str_starts_with($max, '0x')) { $max = \intval($max, 16); } elseif (str_starts_with($max, '0')) { $max = \intval($max, 8); } else { $max = (int) $max; } switch (substr($size, -1)) { case 't': $max *= 1024; // no break case 'g': $max *= 1024; // no break case 'm': $max *= 1024; // no break case 'k': $max *= 1024; } return $max; } private static function mimeToExtension(string $mimeType): ?string { $map = [ 'application/pdf' => 'pdf', 'application/zip' => 'zip', 'application/json' => 'json', 'application/xml' => 'xml', 'application/octet-stream' => 'bin', 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', 'image/svg+xml' => 'svg', 'text/plain' => 'txt', 'text/html' => 'html', 'text/css' => 'css', 'text/javascript' => 'js', 'audio/mpeg' => 'mp3', 'audio/wav' => 'wav', 'video/mp4' => 'mp4', 'video/webm' => 'webm', ]; return $map[$mimeType] ?? null; } }