Newer
Older
framework / system / Files / File.php
@lonnieezell lonnieezell on 1 May 2020 6 KB Release v4.0.3
<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 * Copyright (c) 2019-2020 CodeIgniter Foundation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2019-2020 CodeIgniter Foundation
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 4.0.0
 * @filesource
 */

namespace CodeIgniter\Files;

use CodeIgniter\Files\Exceptions\FileException;
use CodeIgniter\Files\Exceptions\FileNotFoundException;
use SplFileInfo;

/**
 * Wrapper for PHP's built-in SplFileInfo, with goodies.
 *
 * @package CodeIgniter\Files
 */
class File extends SplFileInfo
{

	/**
	 * The files size in bytes
	 *
	 * @var float
	 */
	protected $size;

	//--------------------------------------------------------------------

	/**
	 * Run our SplFileInfo constructor with an optional verification
	 * that the path is really a file.
	 *
	 * @param string  $path
	 * @param boolean $checkFile
	 */
	public function __construct(string $path, bool $checkFile = false)
	{
		if ($checkFile && ! is_file($path))
		{
			throw FileNotFoundException::forFileNotFound($path);
		}

		parent::__construct($path);
	}

	//--------------------------------------------------------------------

	/**
	 * Retrieve the file size.
	 *
	 * Implementations SHOULD return the value stored in the "size" key of
	 * the file in the $_FILES array if available, as PHP calculates this based
	 * on the actual size transmitted.
	 *
	 * @return integer The file size in bytes
	 */
	public function getSize()
	{
		if (is_null($this->size))
		{
			$this->size = parent::getSize();
		}

		return $this->size;
	}

	/**
	 * Retrieve the file size by unit.
	 *
	 * @param string $unit
	 *
	 * @return integer|string
	 */
	public function getSizeByUnit(string $unit = 'b')
	{
		switch (strtolower($unit))
		{
			case 'kb':
				return number_format($this->getSize() / 1024, 3);
			case 'mb':
				return number_format(($this->getSize() / 1024) / 1024, 3);
			default:
				return $this->getSize();
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Attempts to determine the file extension based on the trusted
	 * getType() method. If the mime type is unknown, will return null.
	 *
	 * @return string|null
	 */
	public function guessExtension(): ?string
	{
		return \Config\Mimes::guessExtensionFromType($this->getMimeType());
	}

	//--------------------------------------------------------------------

	/**
	 * Retrieve the media type of the file. SHOULD not use information from
	 * the $_FILES array, but should use other methods to more accurately
	 * determine the type of file, like finfo, or mime_content_type().
	 *
	 * @return string|null The media type we determined it to be.
	 */
	public function getMimeType(): string
	{
		if (! function_exists('finfo_open'))
		{
			return $this->originalMimeType ?? 'application/octet-stream';
		}

		$finfo    = finfo_open(FILEINFO_MIME_TYPE);
		$mimeType = finfo_file($finfo, $this->getRealPath());
		finfo_close($finfo);
		return $mimeType;
	}

	//--------------------------------------------------------------------

	/**
	 * Generates a random names based on a simple hash and the time, with
	 * the correct file extension attached.
	 *
	 * @return string
	 */
	public function getRandomName(): string
	{
		$extension = $this->getExtension();
		$extension = empty($extension) ? '' : '.' . $extension;
		return time() . '_' . bin2hex(random_bytes(10)) . $extension;
	}

	//--------------------------------------------------------------------

	/**
	 * Moves a file to a new location.
	 *
	 * @param string      $targetPath
	 * @param string|null $name
	 * @param boolean     $overwrite
	 *
	 * @return \CodeIgniter\Files\File
	 */
	public function move(string $targetPath, string $name = null, bool $overwrite = false)
	{
		$targetPath  = rtrim($targetPath, '/') . '/';
		$name        = $name ?? $this->getBaseName();
		$destination = $overwrite ? $targetPath . $name : $this->getDestination($targetPath . $name);

		$oldName = empty($this->getRealPath()) ? $this->getPath() : $this->getRealPath();

		if (! @rename($oldName, $destination))
		{
			$error = error_get_last();
			throw FileException::forUnableToMove($this->getBasename(), $targetPath, strip_tags($error['message']));
		}

		@chmod($targetPath, 0777 & ~umask());

		return new File($destination);
	}

	//--------------------------------------------------------------------

	/**
	 * Returns the destination path for the move operation where overwriting is not expected.
	 *
	 * First, it checks whether the delimiter is present in the filename, if it is, then it checks whether the
	 * last element is an integer as there may be cases that the delimiter may be present in the filename.
	 * For the all other cases, it appends an integer starting from zero before the file's extension.
	 *
	 * @param string  $destination
	 * @param string  $delimiter
	 * @param integer $i
	 *
	 * @return string
	 */
	public function getDestination(string $destination, string $delimiter = '_', int $i = 0): string
	{
		while (is_file($destination))
		{
			$info      = pathinfo($destination);
			$extension = isset($info['extension']) ? '.' . $info['extension'] : '';
			if (strpos($info['filename'], $delimiter) !== false)
			{
				$parts = explode($delimiter, $info['filename']);
				if (is_numeric(end($parts)))
				{
					$i = end($parts);
					array_pop($parts);
					array_push($parts, ++ $i);
					$destination = $info['dirname'] . '/' . implode($delimiter, $parts) . $extension;
				}
				else
				{
					$destination = $info['dirname'] . '/' . $info['filename'] . $delimiter . ++ $i . $extension;
				}
			}
			else
			{
				$destination = $info['dirname'] . '/' . $info['filename'] . $delimiter . ++ $i . $extension;
			}
		}
		return $destination;
	}

	//--------------------------------------------------------------------
}