<?php
/**
* @package RSForm! Pro
* @copyright (C) 2007-2019 www.rsjoomla.com
* @license GPL, http://www.gnu.org/copyleft/gpl.html
*/

defined('_JEXEC') or die;

use Joomla\CMS\Language\Text;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Factory;

require_once JPATH_ADMINISTRATOR.'/components/com_rsform/helpers/field.php';

class RSFormProFieldFileUpload extends RSFormProField
{
	// backend preview
	public function getPreviewInput()
	{
		return '<input type="file" />';
	}

	// @desc Returns the full name of the name HTML tag (eg. form[textbox])
	public function getName()
	{
		$name = $this->namespace.'['.$this->name.']';

		if ($this->getProperty('MULTIPLE', false))
		{
			$name .= '[]';
		}

		return $name;
	}

	// functions used for rendering in front view
	public function getFormInput()
	{
		$multiple 		= $this->getProperty('MULTIPLE', false);
		$multipleplus 	= $this->getProperty('MULTIPLEPLUS', false);

		if ($multiple && $multipleplus)
		{
			$minFiles = (int) $this->getProperty('MINFILES', 1);

			// If we require a minimum number of files to be uploaded, let's show a separate input for each upload in order to help the user
			if ($minFiles > 1)
			{
				$html = str_repeat('<div class="rsfp-field-multiple-plus">' . $this->getFileInput() . '</div>', $minFiles);
			}
			else
			{
				$html = '<div class="rsfp-field-multiple-plus">' . $this->getFileInput() . '</div>';
			}

			return $html . $this->getButtonInput();
		}
		else
		{
			return $this->getFileInput();
		}
	}

	public function getButtonInput()
	{
		$button = '<button type="button"';

		if ($attr = $this->getButtonAttributes())
		{
			foreach ($attr as $key => $value)
			{
				$button .= $this->attributeToHtml($key, $value);
			}
		}

		$maxFiles = (int) $this->getProperty('MAXFILES', 0);
		if ($maxFiles > 0)
		{
			$button .= ' data-rsfp-maxfiles="' . $maxFiles . '"';
		}
		$minFiles = (int) $this->getProperty('MINFILES', 0);
		/*
		 * We have only one maximum file => disable
		 * We have a defined number of maximum files but we've set minimum files lower => disable
		 */
		if ($maxFiles === 1 || ($maxFiles > 0 && $minFiles >= $maxFiles))
		{
			$button .= ' disabled';
		}

		$button .= ' data-rsfp-formid="' . $this->formId . '"';

		$button .= ' onclick="RSFormPro.addMoreFiles(this);">' . Text::_('COM_RSFORM_FILE_ADD_PLUS') . '</button>';

		return $button;
	}

	public function getFileInput()
	{
		$name			= $this->getName();
		$id				= $this->getId();
		$attr			= $this->getAttributes();
		$multiple		= $this->getProperty('MULTIPLE', false);
		$multipleplus	= $this->getProperty('MULTIPLEPLUS', false);
		$type 			= 'file';
		$additional 	= '';
		
		// Start building the HTML input
		$html = '<input';
		// Parse Additional Attributes
		if ($attr) {
			foreach ($attr as $key => $values) {
				// @new feature - Some HTML attributes (type) can be overwritten
				// directly from the Additional Attributes area
				if ($key == 'type' && strlen($values)) {
					${$key} = $values;
					continue;
				}
				$additional .= $this->attributeToHtml($key, $values);
			}
		}
		// Set the type
		$html .= ' type="'.$this->escape($type).'"';
		// Name & id
		$html .= ' name="'.$this->escape($name).'"'.
				 ' id="'.$this->escape($id).'"';
		if ($multiple)
		{
			if ($multipleplus)
			{
				$html .= ' data-rsfp-skip-ajax="true"';
			}
			else
			{
				$html .= ' multiple';
			}
		}

		if ($this->getProperty('ACCEPTEDFILESIMAGES') && $this->getProperty('SHOWIMAGEPREVIEW'))
		{
			$html .= ' onchange="RSFormPro.loadImage(this);"';
		}

		// Additional HTML
		$html .= $additional;

		$html .= $this->addDataAttributes();

		// Close the tag
		$html .= ' />';
		
		return $html;
	}

	protected function getButtonAttributes()
	{
		return array('class' => 'rsfp-field-multiple-plus-button');
	}

	protected function addDataAttributes()
	{
		$html = '';

		if ($this->isRequired())
		{
			$html .= ' data-rsfp-required="true"';
		}

		if ($this->getProperty('MULTIPLE'))
		{
			$minFiles = (int) $this->getProperty('MINFILES', 1);
			$maxFiles = (int) $this->getProperty('MAXFILES', 0);

			if ($minFiles > 0)
			{
				$html .= ' data-rsfp-minfiles="' . $minFiles . '"';
			}
			if ($maxFiles > 0)
			{
				$html .= ' data-rsfp-maxfiles="' . $maxFiles . '"';
			}
		}

		if ($this->getProperty('ACCEPTEDFILESIMAGES'))
		{
			$acceptedExts = array('jpg', 'jpeg', 'png', 'gif');

			$newWidth = (int) $this->getProperty('THUMBSIZE', 220);
			if ($newWidth === 0 || ($newWidth > 0 && version_compare(PHP_VERSION, '7.3.0', '>=')))
			{
				$acceptedExts[] = 'webp';
			}
		}
		elseif ($exts = $this->getProperty('ACCEPTEDFILES'))
		{
			$acceptedExts = RSFormProHelper::explode($exts);
		}
		else
		{
			$acceptedExts = array();
		}

		if ($acceptedExts)
		{
			array_walk($acceptedExts, array($this, 'cleanExtension'));
			$html .= ' data-rsfp-exts="' . $this->escape(json_encode($acceptedExts)) . '"';
		}

		$size = (int) $this->getProperty('FILESIZE');

		if ($size)
		{
			$html .= ' data-rsfp-size="' . ($size * 1024) . '"';
		}

		$this->addScriptDeclaration('RSFormPro.Translations.add(' . $this->formId . ', ' . json_encode($this->name) . ', ' . json_encode('VALIDATIONMESSAGE') . ', ' . json_encode($this->getProperty('VALIDATIONMESSAGE')) . ');');
		$this->addCommonTranslations();

		return $html;
	}

	private function addCommonTranslations()
	{
		static $done;

		if (!$done)
		{
			$done = true;

			$messages = array(
				'COM_RSFORM_FILE_EXCEEDS_LIMIT' => Text::_('COM_RSFORM_FILE_EXCEEDS_LIMIT'),
				'COM_RSFORM_FILE_EXTENSION_NOT_ALLOWED' => Text::_('COM_RSFORM_FILE_EXTENSION_NOT_ALLOWED'),
				'COM_RSFORM_MINFILES_REQUIRED' => Text::_('COM_RSFORM_MINFILES_REQUIRED'),
				'COM_RSFORM_MAXFILES_REQUIRED' => Text::_('COM_RSFORM_MAXFILES_REQUIRED'),
			);

			$script = '';
			foreach ($messages as $key => $message)
			{
				$script .= 'RSFormPro.Translations.addCommonTranslation(' . json_encode($key) . ', ' . json_encode($message) . ');';
			}

			$this->addScriptDeclaration($script);
		}
	}

	public function cleanExtension(&$value, $key = null)
	{
		$value = strtolower(trim($value));
	}
	
	// @desc All upload fields should have a 'rsform-upload-box' class for easy styling
	public function getAttributes() {
		$attr = parent::getAttributes();
		if (strlen($attr['class'])) {
			$attr['class'] .= ' ';
		}
		$attr['class'] .= 'rsform-upload-box';
		
		return $attr;
	}

	// process the upload file after form validation
	public function processBeforeStore($submissionId, &$post, &$files, $addToDb = true)
	{
		if (!isset($files[$this->name]))
		{
			return false;
		}

		$allFiles = array();

		$actualFiles = $this->getProperty('MULTIPLE', false) ? $files[$this->name] : array($files[$this->name]);
		foreach ($actualFiles as $actualFile)
		{
			if ($actualFile['error'] != UPLOAD_ERR_OK)
			{
				continue;
			}

			$prefixProperty = $this->getProperty('PREFIX', '');
			$destination    = RSFormProHelper::getRelativeUploadPath($this->getProperty('DESTINATION', ''));
			$sanitize       = $this->getProperty('SANITIZEFILENAME', false);

			// Prefix
			$prefix = uniqid('') . '-';
			if (strlen(trim($prefixProperty)) > 0)
			{
				$prefix = $this->isCode($prefixProperty);
			}

			// Path
			$realpath = realpath($destination . DIRECTORY_SEPARATOR);
			if (substr($realpath, -1) != DIRECTORY_SEPARATOR)
			{
				$realpath .= DIRECTORY_SEPARATOR;
			}

			// Filename
			if ($sanitize)
			{
				$file = $realpath . $prefix . $this->sanitize($actualFile['name']);
			}
			else
			{
				$file = $realpath . $prefix . $actualFile['name'];
			}

			// Upload File
			if (File::upload($actualFile['tmp_name'], $file, false, (bool) RSFormProHelper::getConfig('allow_unsafe')))
			{
				if ($this->getProperty('ACCEPTEDFILESIMAGES', false))
				{
					if (function_exists('imagecreatefromstring'))
					{
						$newWidth = (int) $this->getProperty('THUMBSIZE', 220);

						if ($newWidth > 0)
						{
							$image = @imagecreatefromstring(file_get_contents($file));

							if ($image !== false)
							{
								$quality   = (int) $this->getProperty('THUMBQUALITY', 75);
								$extension = $this->getProperty('THUMBEXTENSION', 'jpg');

								// Try to rotate it, JPEG only
								$this->tryToRotate($image, $file);

								// If we're downsizing, IMG_BICUBIC produces better results
								if ($newWidth < imagesx($image))
								{
									$image = imagescale($image, $newWidth, -1, IMG_BICUBIC);
								}
								else
								{
									$image = imagescale($image, $newWidth);
								}

								if ($image !== false)
								{
									$thumbFile = File::stripExt($file) . '.' . $extension;

									// Delete old file, we no longer need it
									File::delete($file);

									if ($extension === 'png')
									{
										imagealphablending($image, false);
										imagesavealpha($image, true);
										imagepng($image, $thumbFile);
									}
									elseif ($extension === 'jpg')
									{
										imagejpeg($image, $thumbFile, $quality);
									}
									elseif ($extension === 'webp' && function_exists('imagewebp'))
									{
										imagewebp($image, $thumbFile, $quality);
									}

									$file = $thumbFile;

									unset($image);
								}
							}
						}
					}
					else
					{
						Factory::getApplication()->enqueueMessage('COM_RSFORM_CREATING_THUMBNAILS_FROM_IMAGES_REQUIRES_GD', 'warning');
					}
				}

				// Trigger Event - onBeforeStoreSubmissions
				Factory::getApplication()->triggerEvent('onRsformFrontendAfterFileUpload', array(array('formId' => $this->formId, 'submissionId' => $submissionId, 'fieldname' => $this->name, 'file' => $file, 'name' => $prefix . $actualFile['name'], 'addToDb' => $addToDb)));

				$allFiles[] = $file;
			}
		}

		if (!$allFiles)
		{
			return false;
		}

		$object = (object) array(
			'SubmissionId' 	=> $submissionId,
			'FormId'		=> $this->formId,
			'FieldName'		=> $this->name,
			'FieldValue'	=> implode("\n", $allFiles)
		);

		if ($addToDb)
		{
			Factory::getDbo()->insertObject('#__rsform_submission_values', $object, 'SubmissionValueId');
		}

		return $object;
	}

	protected function tryToRotate(&$image, $file)
	{
		if (!function_exists('exif_read_data') || !function_exists('exif_imagetype') || !function_exists('imagerotate'))
		{
			return false;
		}

		if (exif_imagetype($file) !== IMAGETYPE_JPEG)
		{
			return false;
		}

		$data = exif_read_data($file);

		if ($data === false || !isset($data['Orientation']) || $data['Orientation'] == 1)
		{
			return false;
		}

		switch ($data['Orientation'])
		{
			case 2:
				$image = $this->imageFlip($image, 2);
				break;

			case 3:
				$image = $this->imageFlip($image, 3);
				break;

			case 4:
				$image = $this->imageFlip($image, 3);
				$image = $this->imageFlip($image, 2);
				break;

			case 5:
				$image = imagerotate($image, 270, 0);
				$image = $this->imageFlip($image, 2);
				break;

			case 6:
				$image = imagerotate($image, 270, 0);
				break;

			case 7:
				$image = $this->imageFlip($image, 2);
				$image = imagerotate($image, 270, 0);
				break;

			case 8:
				$image = imagerotate($image, 90, 0);
				break;
		}

		return true;
	}

	protected function imageFlip($imgsrc, $mode)
	{
		$width  = imagesx($imgsrc);
		$height = imagesy($imgsrc);

		$src_x = 0;
		$src_y = 0;
		$src_width  = $width;
		$src_height = $height;

		switch ($mode)
		{
			case 1:
				$src_y = $height - 1;
				$src_height = -$height;
				break;

			case 2:
				$src_x = $width - 1;
				$src_width = -$width;
				break;

			case 3:
				$src_x = $width - 1;
				$src_y = $height - 1;
				$src_width 	= -$width;
				$src_height = -$height;
				break;

			default:
				return $imgsrc;
				break;
		}

		$imgdest = imagecreatetruecolor($width, $height);
		if (imagecopyresampled($imgdest, $imgsrc, 0, 0, $src_x, $src_y , $width, $height, $src_width, $src_height))
		{
			return $imgdest;
		}

		return $imgsrc;
	}

	protected function sanitize($string)
    {
        // Remove any '-' from the string since they will be used as concatenaters
        $str = str_replace('-', ' ', $string);

        // Transliterate on the current language
        $str = Factory::getLanguage()->transliterate($str);

        // Trim white spaces at beginning and end
        $str = trim($str);

        // Remove any duplicate whitespace, and ensure all characters are alphanumeric
        $str = preg_replace('/(\s|[^A-Za-z0-9\-\.])+/', '-', $str);

        // Trim dashes at beginning and end of alias
        $str = trim($str, '-');

        return $str;
    }

    public function md5(&$item, $key)
	{
		$item = md5($item);
	}

    public function removeHashedValues(&$form, $delete)
	{
		if (empty($form) || !is_array($form) || empty($delete) || !is_array($delete))
		{
			return false;
		}

		$hashes = $form;
		array_walk($hashes, array($this, 'md5'));

		foreach ($delete as $hashToDelete)
		{
			$position = array_search($hashToDelete, $hashes);

			if ($position !== false)
			{
				if (is_file($form[$position]))
				{
					File::delete($form[$position]);
				}
				unset($form[$position]);
			}
		}

		return true;
	}

	public function processValidation($validationType = 'form', $submissionId = 0)
	{
		$db 		= Factory::getDbo();
		$required 	= $this->isRequired();
		$multiple 	= $this->getProperty('MULTIPLE', false);
		$files 		= Factory::getApplication()->input->files->get('form', null, 'raw');

		if ($validationType == 'directory')
		{
			$query = $db->getQuery(true)
				->select($db->qn('FieldValue'))
				->from($db->qn('#__rsform_submission_values'))
				->where($db->qn('FieldName') . ' = ' . $db->q($this->name))
				->where($db->qn('SubmissionId') . ' = ' . $db->q($submissionId));

			if ($alreadyUploaded = $db->setQuery($query)->loadResult())
			{
				$alreadyUploaded = RSFormProHelper::explode($alreadyUploaded);
			}
			else
			{
				$alreadyUploaded = array();
			}

			$delete = Factory::getApplication()->input->post->get('delete', array(), 'array');
			if (!empty($delete[$this->name]))
			{
				$this->removeHashedValues($alreadyUploaded, $delete[$this->name]);
			}
		}

		try
		{
			// No $_FILES, but required
			if (!$files && $required)
			{
				return false;
			}

			// $_FILES exists but not for our own field
			if (!isset($files[$this->name]))
			{
				$actualFiles = array();
			}
			else
			{
				if ($multiple)
				{
					$actualFiles = $files[$this->name];
				}
				else
				{
					$actualFiles = array($files[$this->name]);
				}
			}

			// Since we can't rely on counting $_FILES we need to count each correct file
			$countFiles = 0;

			$allowImages = $this->getProperty('ACCEPTEDFILESIMAGES', false);
			$newWidth    = (int) $this->getProperty('THUMBSIZE', 220);

			foreach ($actualFiles as $actualFile)
			{
				$name 	= $actualFile['name'];
				$error 	= $actualFile['error'];
				$size	= $actualFile['size'];

				// File has been uploaded correctly to the server
				if ($error == UPLOAD_ERR_OK)
				{
					// Let's check if the extension is allowed
					$extParts 		= explode('.', $name);
					$ext 			= strtolower(end($extParts));
					$acceptedExts   = false;
					if ($allowImages)
					{
						$acceptedExts = array('jpg', 'jpeg', 'png', 'gif');
						if ($newWidth === 0 || ($newWidth > 0 && version_compare(PHP_VERSION, '7.3.0', '>=')))
						{
							$acceptedExts[] = 'webp';
						}
					}
					elseif ($exts = $this->getProperty('ACCEPTEDFILES'))
					{
						$acceptedExts = RSFormProHelper::explode($exts);
					}

					// Let's check only if accepted extensions are set
					if ($acceptedExts)
					{
						$accepted = false;

						foreach ($acceptedExts as $acceptedExt)
						{
							$acceptedExt = trim(strtolower($acceptedExt));
							if (strlen($acceptedExt) && $acceptedExt == $ext)
							{
								$accepted = true;
								break;
							}
						}

						if (!$accepted)
						{
							throw new Exception(Text::sprintf('COM_RSFORM_FILE_EXTENSION_NOT_ALLOWED', htmlspecialchars(basename($name), ENT_QUOTES, 'utf-8')));
						}

						if ($allowImages && function_exists('exif_imagetype'))
						{
							$imagetype = exif_imagetype($actualFile['tmp_name']);
							if ($imagetype === false && ($ext === 'webp' && version_compare(PHP_VERSION, '7.1.0', '>=')  || $ext !== 'webp'))
							{
								throw new Exception(Text::sprintf('COM_RSFORM_FILE_DOES_NOT_SEEM_TO_BE_AN_IMAGE', htmlspecialchars(basename($name), ENT_QUOTES, 'utf-8')));
							}
						}
					}

					$filesize = (int) $this->getProperty('FILESIZE');

					// Let's check if it's the correct size
					if ($size > 0 && $filesize > 0 && $size > $filesize * 1024)
					{
						throw new Exception(Text::sprintf('COM_RSFORM_FILE_EXCEEDS_LIMIT', htmlspecialchars(basename($name), ENT_QUOTES, 'utf-8'), $filesize));
					}

					$countFiles++;
				}
				elseif ($error != UPLOAD_ERR_NO_FILE)
				{
					// Parse the error message
					switch ($error)
					{
						default:
							// File has not been uploaded correctly
							throw new Exception(Text::_('RSFP_FILE_HAS_NOT_BEEN_UPLOADED_DUE_TO_AN_UNKNOWN_ERROR'));
							break;

						case UPLOAD_ERR_INI_SIZE:
							throw new Exception(Text::_('RSFP_UPLOAD_ERR_INI_SIZE'));
							break;

						case UPLOAD_ERR_FORM_SIZE:
							throw new Exception(Text::_('RSFP_UPLOAD_ERR_FORM_SIZE'));
							break;

						case UPLOAD_ERR_PARTIAL:
							throw new Exception(Text::_('RSFP_UPLOAD_ERR_PARTIAL'));
							break;

						case UPLOAD_ERR_NO_TMP_DIR:
							throw new Exception(Text::_('RSFP_UPLOAD_ERR_NO_TMP_DIR'));
							break;

						case UPLOAD_ERR_CANT_WRITE:
							throw new Exception(Text::_('RSFP_UPLOAD_ERR_CANT_WRITE'));
							break;

						case UPLOAD_ERR_EXTENSION:
							throw new Exception(Text::_('RSFP_UPLOAD_ERR_EXTENSION'));
							break;
					}
				}
			}

			if ($multiple)
			{
				$minFiles = $this->getProperty('MINFILES', 1);
				$maxFiles = $this->getProperty('MAXFILES', 0);

				if ($validationType == 'directory')
				{
					$countFiles += count($alreadyUploaded);
				}

				if ($required || $countFiles)
				{
					if ($minFiles > 0 && $countFiles < $minFiles)
					{
						throw new Exception(Text::sprintf('COM_RSFORM_MINFILES_REQUIRED', $minFiles));
					}

					if ($maxFiles > 0 && $countFiles > $maxFiles)
					{
						throw new Exception(Text::sprintf('COM_RSFORM_MAXFILES_REQUIRED', $maxFiles));
					}
				}
			}

			if ($required && $countFiles === 0 && empty($alreadyUploaded))
			{
				throw new Exception($this->getProperty('VALIDATIONMESSAGE'));
			}
		}
		catch (Exception $e)
		{
			$properties =& RSFormProHelper::getComponentProperties($this->componentId);
			$properties['VALIDATIONMESSAGE'] = $e->getMessage();

			return false;
		}

		return true;
	}
}