<?php

/**
 * Core upload handler.
 *
 * @package XenForo_Upload
 */
class XenForo_Upload
{
	/**
	 * The user-supplied file name of the upload.
	 *
	 * @var string
	 */
	protected $_fileName = '';

	/**
	 * The extension from the user-supplied file name.
	 *
	 * @var string
	 */
	protected $_extension = '';

	/**
	 * Full path to the temporary file created by the upload.
	 *
	 * @var string
	 */
	protected $_tempFile = '';

	/**
	 * If the upload is an image, information about the image.
	 * If null, state is unknown; if false, not an image. Otherwise, array with keys:
	 * 	* from getimagesize()
	 * 	* width/height/type
	 *
	 * @var array|false|null
	 */
	protected $_imageInfo = null;

	/**
	 * True if errors have been checked.
	 *
	 * @var boolean
	 */
	protected $_errorsChecked = false;

	/**
	 * List of errors that occured in the upload.
	 *
	 * @var array
	 */
	protected $_errors = array();

	/**
	 * List of allowed attachment extensions.
	 *
	 * @var array
	 */
	protected $_allowedExtensions = array();

	/**
	 * Maximum attachment file size in bytes.
	 *
	 * @var integer
	 */
	protected $_maxFileSize = 0;

	/**
	 * Maximum attachment image width in pixels.
	 *
	 * @var integer
	 */
	protected $_maxWidth = 0;

	/**
	 * Maximum attachment image height in pixels.
	 *
	 * @var integer
	 */
	protected $_maxHeight = 0;

	/**
	 * If true, automatically resizes images that are
	 * too large.
	 *
	 * @var boolean
	 */
	protected $_autoResize = true;

	/**
	 * Constructor.
	 *
	 * @param string $fileName User-supplied file name
	 * @param string $tempFile Upload temporary file name; this can be an empty string to account for uploads that are too large
	 */
	public function __construct($fileName, $tempFile)
	{
		if ($tempFile && !file_exists($tempFile) && !is_readable($tempFile))
		{
			throw new XenForo_Exception('The temporary file for the upload cannot be found.');
		}

		$this->_fileName = $fileName;
		$this->_extension = XenForo_Helper_File::getFileExtension($fileName);
		$this->_tempFile = $tempFile;

		// TODO: clean up internal files on shut down (can't use destruct as files may still be used)
	}

	public function setFileName($fileName)
	{
		$this->_fileName = $fileName;
		$this->_extension = XenForo_Helper_File::getFileExtension($fileName);
	}

	/**
	 * Set the constraints for this upload. Possible keys:
	 * 	* extensions - array of allowed extensions
	 * 	* size - max file size in bytes
	 * 	* width - max image width in pixels
	 * 	* height - max image height in pixels
	 *
	 * @param array $constraints See above for format.
	 */
	public function setConstraints(array $constraints)
	{
		if ($this->_errorsChecked || $this->_imageInfo !== null)
		{
			throw new XenForo_Exception('Cannot set upload constraints after checking upload state.');
		}

		if (!empty($constraints['extensions']) && is_array($constraints['extensions']))
		{
			$this->_allowedExtensions = array_map('strtolower', $constraints['extensions']);
		}
		if (!empty($constraints['size']) && $constraints['size'] > 0)
		{
			$this->_maxFileSize = max(0, intval($constraints['size']));
		}
		if (!empty($constraints['width']) && $constraints['width'] > 0)
		{
			$this->_maxWidth = max(0, intval($constraints['width']));
		}
		if (!empty($constraints['height']) && $constraints['height'] > 0)
		{
			$this->_maxHeight = max(0, intval($constraints['height']));
		}
	}

	/**
	 * Returns true if the upload is valid.
	 *
	 * @return boolean
	 */
	public function isValid()
	{
		if (!$this->_errorsChecked)
		{
			$this->_checkForErrors();
		}

		return (count($this->_errors) == 0);
	}

	/**
	 * Returns true if the upload is a valid image.
	 *
	 * @return boolean
	 */
	public function isImage()
	{
		$this->_checkImageState();
		return ($this->_imageInfo ? true : false);
	}

	/**
	 * Checks the state of the upload to determine if it's
	 * a valid image.
	 */
	protected function _checkImageState()
	{
		if ($this->_imageInfo !== null)
		{
			return;
		}

		$this->_imageInfo = false; // default to not an image

		if (!$this->_tempFile)
		{
			return;
		}

		$imageInfo = @getimagesize($this->_tempFile);
		if (!$imageInfo)
		{
			if (in_array($this->_extension, array('gif', 'jpg', 'jpe', 'jpeg', 'png')))
			{
				$this->_errors['extension'] = new XenForo_Phrase('the_uploaded_file_was_not_an_image_as_expected');
			}
			return;
		}

		$imageInfo['width'] = $imageInfo[0];
		$imageInfo['height'] = $imageInfo[1];
		$imageInfo['type'] = $imageInfo[2];

		$type = $imageInfo['type'];
		$extensionMap = array(
			IMAGETYPE_GIF => array('gif'),
			IMAGETYPE_JPEG => array('jpg', 'jpeg', 'jpe'),
			IMAGETYPE_PNG => array('png')
		);
		if (!isset($extensionMap[$type]))
		{
			return; // only consider gif, jpeg, png to be images in this system
		}
		if (!in_array($this->_extension, $extensionMap[$type]))
		{
			$this->_errors['extension'] = new XenForo_Phrase('contents_of_uploaded_image_do_not_match_files_extension');
			return;
		}

		$fp = @fopen($this->_tempFile, 'rb');
		if ($fp)
		{
			$previous = '';
			while (!@feof($fp))
			{
				$content = fread($fp, 256000);
				$test = $previous . $content;
				$exists = (
					strpos($test, '<?php') !== false
					|| preg_match('/<script\s+language\s*=\s*(php|"php"|\'php\')\s*>/i', $test)
				);
				if ($exists) {
					@fclose($fp);
					$this->_errors['content'] = new XenForo_Phrase('uploaded_image_contains_invalid_content');
					return;
				}

				$previous = $content;
			}

			@fclose($fp);
		}

		$orientation = 0;
		if ($imageInfo['type'] == IMAGETYPE_JPEG && function_exists('exif_read_data'))
		{
			$exif = @exif_read_data($this->_tempFile, 'EXIF');
			if ($exif && !empty($exif['Orientation']) && $exif['Orientation'] > 1)
			{
				$orientation = $exif['Orientation'];
			}
		}
		$transformRequired = ($orientation > 1);

		$maxWidth = $this->_maxWidth;
		$maxHeight = $this->_maxHeight;

		if ($orientation >= 5 && $orientation <= 8)
		{
			// after rotation the X and Y coords will be reversed,
			// so flip the limits to reflect the "after" value
			$maxHeight = $this->_maxWidth;
			$maxWidth = $this->_maxHeight;
		}

		$resizeRequired = (
			($maxWidth && $imageInfo['width'] > $maxWidth)
			|| ($maxHeight && $imageInfo['height'] > $maxHeight)
		);

		if (XenForo_Image_Abstract::canResize($imageInfo['width'], $imageInfo['height'])
			&& ($resizeRequired || $transformRequired)
		)
		{
			$image = XenForo_Image_Abstract::createFromFile($this->_tempFile, $type);
			if ($image)
			{
				if ($resizeRequired)
				{
					$image->thumbnail($maxWidth ? $maxWidth : $maxHeight, $maxHeight);
				}
				if ($transformRequired)
				{
					$image->transformByExif($orientation);
				}

				$success = $image->output($type, $this->_tempFile);
				if ($success)
				{
					$imageInfo['width'] = $imageInfo[0] = $image->getWidth();
					$imageInfo['height'] = $imageInfo[1] = $image->getHeight();
				}
				else
				{
					if ($resizeRequired)
					{
						$this->_errors['dimensions'] = new XenForo_Phrase('uploaded_image_is_too_big');
					}
					// ok to ignore the EXIF transform failing
				}
			}
			else
			{
				// treat as non-image
				$imageInfo = false;
			}
		}
		else if ($resizeRequired)
		{
			$this->_errors['dimensions'] = new XenForo_Phrase('uploaded_image_is_too_big');
		}

		$this->_imageInfo = $imageInfo;
	}

	/**
	 * Checks for errors in the upload.
	 */
	protected function _checkForErrors()
	{
		$this->_checkImageState();

		if ($this->_allowedExtensions && !in_array($this->_extension, $this->_allowedExtensions))
		{
			$this->_errors['extension'] = new XenForo_Phrase('uploaded_file_does_not_have_an_allowed_extension');
		}

		if ($this->_tempFile && $this->_maxFileSize && filesize($this->_tempFile) > $this->_maxFileSize)
		{
			$this->_errors['fileSize'] = new XenForo_Phrase('uploaded_file_is_too_large');
		}

		if (!$this->_tempFile)
		{
			$this->_errors['fileSize'] = new XenForo_Phrase('uploaded_file_is_too_large_for_server_to_process');
		}

		$this->_errorsChecked = true;
	}

	/**
	 * Gets the user-supplied file name.
	 *
	 * @return string
	 */
	public function getFileName()
	{
		return $this->_fileName;
	}

	/**
	 * Gets the path to the temporary file.
	 *
	 * @return string
	 */
	public function getTempFile()
	{
		return $this->_tempFile;
	}

	/**
	 * Gets the errors for the upload.
	 *
	 * @return array
	 */
	public function getErrors()
	{
		return $this->_errors;
	}

	/**
	 * Gets the value of a specific image info field.
	 *
	 * @param string $field
	 *
	 * @return mixed|false Mixed scalar, or false if not an image or invalid field
	 */
	public function getImageInfoField($field)
	{
		$this->_checkImageState();
		if ($this->_imageInfo && isset($this->_imageInfo[$field]))
		{
			return $this->_imageInfo[$field];
		}
		else
		{
			return false;
		}
	}

	/**
	 * Gets the files that were uploaded into the specified form field (via HTTP POST).
	 *
	 * @param string $formField Name of the form field
	 * @param array|null $source Source array ($_FILES by default).
	 *
	 * @return array Format: [] => XenForo_Upload objects
	 */
	public static function getUploadedFiles($formField, array $source = null)
	{
		if ($source === null)
		{
			$source = $_FILES;
		}
		if (empty($source[$formField]))
		{
			return array();
		}

		$files = array();
		$field = $source[$formField];

		if (isset($field['name']))
		{
			if (is_array($field['name']))
			{
				foreach (array_keys($field['name']) AS $key)
				{
					if ($field['name'][$key])
					{
						$files[] = new XenForo_Upload($field['name'][$key], $field['tmp_name'][$key]);
					}
				}
			}
			else if ($field['name'])
			{
				$files[] = new XenForo_Upload($field['name'], $field['tmp_name']);
			}
		}

		return $files;
	}

	/**
	 * Gets the file that was uploaded into the specified form field (via HTTP POST).
	 *
	 * @param string $formField Name of the form field
	 * @param array|null $source Source array ($_FILES by default).
	 *
	 * @return XenForo_Upload (or false)
	 */
	public static function getUploadedFile($formField, array $source = null)
	{
		$files = XenForo_Upload::getUploadedFiles($formField, $source);
		return reset($files);
	}
}