<?php
/**
* @version	$Id: upload_formatter.php 16596 2017-07-26 15:17:37Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2011 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

defined('FULL_PATH') or die('restricted access!');

class kUploadFormatter extends kFormatter
{
	var $DestinationPath;

	var $FullPath;

	/**
	 * File helper reference
	 *
	 * @var FileHelper
	 */
	var $fileHelper = NULL;

	/**
	 * Uploaded files, that are ordered as required (both live & temp images in one list)
	 *
	 * @var Array
	 * @access protected
	 */
	protected $sorting = Array ();

	/**
	 * Creates formatter instance
	 *
	 * @access public
	 */
	public function __construct()
	{
		parent::__construct();

		$this->fileHelper = $this->Application->recallObject('FileHelper');

		if ( $this->DestinationPath ) {
			$this->FullPath = FULL_PATH . $this->DestinationPath;
		}
	}

	/**
	 * Sets field option defaults.
	 *
	 * @param string  $field_name    Field nae.
	 * @param array   $field_options Field options.
	 * @param kDBBase $object        Object.
	 *
	 * @return void
	 */
	public function PrepareOptions($field_name, &$field_options, &$object)
	{
		if ( !$this->DestinationPath && !isset($field_options['upload_dir']) ) {
			$base_path = $this->Application->getUnitOption($object->Prefix, 'BasePath');
			$field_options['upload_dir'] = WRITEBALE_BASE . '/' . basename($base_path) . '/';
		}

		if ( !isset($field_options['max_size']) ) {
			$field_options['max_size'] = MAX_UPLOAD_SIZE;
		}
	}

	/**
	 * Processes file uploads from form
	 *
	 * @param mixed $value
	 * @param string $field_name
	 * @param kDBItem $object
	 * @return mixed
	 * @access public
	 */
	public function Parse($value, $field_name, &$object)
	{
		$value = $this->Application->HttpQuery->unescapeRequestVariable($value);
		$options = $object->GetFieldOptions($field_name);

		if ( getArrayValue($options, 'upload_dir') ) {
			$this->DestinationPath = $options['upload_dir'];
			$this->FullPath = FULL_PATH . $this->DestinationPath;
		}

		if ( is_array($value) && isset($value['tmp_ids']) ) {
			$ret = $this->_processFlashUploader($value, $field_name, $object);
		}
		else {
			$ret = $this->_processRegularUploader($value, $field_name, $object);
		}

		if ( getArrayValue($options, 'upload_dir') ) {
			$this->DestinationPath = null;
			$this->FullPath = null;
		}

		return $ret;
	}

	/**
	 * Handles uploaded files, provided by Flash uploader
	 *
	 * @param Array|string $value
	 * @param string $field_name
	 * @param kDBItem $object
	 * @return string
	 * @access protected
	 */
	protected function _processFlashUploader($value, $field_name, $object)
	{
		$options = $object->GetFieldOptions($field_name);
		$this->sorting = isset($value['order']) ? explode('|', $value['order']) : Array ();

		if ( $value['tmp_deleted'] ) {
			$n_upload = Array ();
			$deleted = explode('|', $value['tmp_deleted']);
			$upload = explode('|', $value['upload']);

			foreach ($upload as $name) {
				if ( in_array($name, $deleted) ) {
					continue;
				}

				$n_upload[] = $name;
			}

			$value['upload'] = implode('|', $n_upload);
		}

		if ( !$value['tmp_ids'] ) {
			// no pending files -> return already uploaded files
			return $this->_sortFiles($value['upload']);
		}

		$swf_uploaded_ids = explode('|', $value['tmp_ids']);
		$swf_uploaded_names = explode('|', $value['tmp_names']);
		$existing = $value['upload'] ? explode('|', $value['upload']) : Array ();

		$fret = Array ();
		$max_files = $this->_getMaxFiles($options);
		$pending_actions = $object->getPendingActions();
		$files_to_delete = $this->_getFilesToDelete($object);

		for ($i = 0; $i < min($max_files, count($swf_uploaded_ids)); $i++) {
			// don't delete uploaded file, when it's name matches delete file name
			$real_name = $this->_getRealFilename($swf_uploaded_names[$i], $options, $object, $files_to_delete);
			$file_name = $this->FullPath . $real_name;

			$tmp_file = WRITEABLE . '/tmp/' . $swf_uploaded_ids[$i] . '_' . $swf_uploaded_names[$i];
			rename($tmp_file, $file_name);

			@chmod($file_name, 0666);
			$fret[] = getArrayValue($options, 'upload_dir') ? $real_name : $this->DestinationPath . $real_name;

			$pending_actions[] = Array (
				'action' => 'make_live', 'id' => $object->GetID(), 'field' => $field_name, 'file' => $file_name
			);

			$this->_renameFileInSorting($swf_uploaded_names[$i], $real_name);
		}

		$object->setPendingActions($pending_actions);

		return $this->_sortFiles(array_merge($existing, $fret));
	}

	/**
	 * Returns files, scheduled for deleting
	 *
	 * @param kDBItem $object
	 * @return Array
	 * @access protected
	 */
	protected function _getFilesToDelete($object)
	{
		$ret = Array ();

		foreach ($object->getPendingActions() as $data) {
			if ( $data['action'] == 'delete' ) {
				$ret[] = $data['file'];
			}
		}

		return $ret;
	}

	/**
	 * Handles regular file upload
	 *
	 * @param string|Array $value
	 * @param string $field_name
	 * @param kDBItem $object
	 * @return string
	 * @access protected
	 */
	protected function _processRegularUploader($value, $field_name, $object)
	{
		$ret = !is_array($value) ? $value : '';
		$options = $object->GetFieldOptions($field_name);

		if ( getArrayValue($value, 'upload') && getArrayValue($value, 'error') == UPLOAD_ERR_NO_FILE ) {
			// file was not uploaded this time, but was uploaded before, then use previously uploaded file (from db)
			return getArrayValue($value, 'upload');
		}

		if ( is_array($value) && count($value) > 1 && $value['size'] ) {
			if ( is_array($value) && (int)$value['error'] === UPLOAD_ERR_OK ) {
				// we can get mime type based on file content and don't use one, provided by the client
//				$value['type'] = kUtil::mimeContentType($value['tmp_name']);

				if ( getArrayValue($options, 'file_types') && !$this->extensionMatch($value['name'], $options['file_types']) ) {
					// match by file extensions
					$error_params = Array (
						'file_name' => $value['name'],
						'file_types' => $options['file_types'],
					);

					$object->SetError($field_name, 'bad_file_format', 'la_error_InvalidFileFormat', $error_params);
				}
				elseif ( getArrayValue($options, 'allowed_types') && !in_array($value['type'], $options['allowed_types']) ) {
					// match by mime type provided by web-browser
					$error_params = Array (
						'file_type' => $value['type'],
						'allowed_types' => $options['allowed_types'],
					);

					$object->SetError($field_name, 'bad_file_format', 'la_error_InvalidFileFormat', $error_params);
				}
				elseif ( $value['size'] > $options['max_size'] ) {
					$object->SetError($field_name, 'bad_file_size', 'la_error_FileTooLarge');
				}
				elseif ( !is_writable($this->FullPath) ) {
					$object->SetError($field_name, 'cant_save_file', 'la_error_cant_save_file');
				}
				else {
					$real_name = $this->_getRealFilename($value['name'], $options, $object);
					$file_name = $this->FullPath . $real_name;

					$moved = move_uploaded_file($value['tmp_name'], $file_name);
					$storage_format = isset($options['storage_format']) ? $options['storage_format'] : false;

					if ( $storage_format ) {
						/** @var kUploadHelper $upload_helper */
						$upload_helper = $this->Application->recallObject('kUploadHelper');
						$moved = $upload_helper->resizeUploadedFile($file_name, $storage_format);
					}

					if ( !$moved ) {
						$object->SetError($field_name, 'cant_save_file', 'la_error_cant_save_file');
					}
					else {
						@chmod($file_name, 0666);

						if ( getArrayValue($options, 'size_field') ) {
							$object->SetDBField($options['size_field'], $value['size']);
						}

						if ( getArrayValue($options, 'orig_name_field') ) {
							$object->SetDBField($options['orig_name_field'], $value['name']);
						}

						if ( getArrayValue($options, 'content_type_field') ) {
							$object->SetDBField($options['content_type_field'], $value['type']);
						}

						$ret = getArrayValue($options, 'upload_dir') ? $real_name : $this->DestinationPath . $real_name;

						// delete previous file, when new file is uploaded under same field
						/*$previous_file = isset($value['upload']) ? $value['upload'] : false;
						if ( $previous_file && file_exists($this->FullPath . $previous_file) ) {
							unlink($this->FullPath . $previous_file);
						}*/
					}
				}
			}
			else {
				$object->SetError($field_name, 'cant_save_file', 'la_error_cant_save_file');
			}
		}

		if ( (count($value) > 1) && $value['error'] && ($value['error'] != UPLOAD_ERR_NO_FILE) ) {
			$object->SetError($field_name, 'cant_save_file', 'la_error_cant_save_file', $value);
		}

		return $ret;
	}

	/**
	 * Checks, that given file name has on of provided file extensions
	 *
	 * @param string $filename
	 * @param string $file_types
	 * @return bool
	 * @access protected
	 */
	protected function extensionMatch($filename, $file_types)
	{
		if ( preg_match_all('/\*\.(.*?)(;|$)/', $file_types, $regs) ) {
			$file_extension = mb_strtolower(pathinfo($filename, PATHINFO_EXTENSION));
			$file_extensions = array_map('mb_strtolower', $regs[1]);

			return in_array($file_extension, $file_extensions);
		}

		return true;
	}

	/**
	 * Resorts uploaded files according to given file order
	 *
	 * @param Array|string $files
	 * @return string
	 * @access protected
	 */
	protected function _sortFiles($files)
	{
		if ( !is_array($files) ) {
			$files = explode('|', $files);
		}

		$sorted_files = array_intersect($this->sorting, $files); // removes deleted files from sorting
		$new_files = array_diff($files, $sorted_files); // files, that weren't sorted - add to the end

		return implode('|', array_merge($sorted_files, $new_files));
	}

	/**
	 * Returns maximal allowed file count per field
	 *
	 * @param Array $options
	 * @return int
	 * @access protected
	 */
	protected function _getMaxFiles($options)
	{
		if ( !isset($options['multiple']) ) {
			return 1;
		}

		return $options['multiple'] == false ? 1 : $options['multiple'];
	}

	/**
	 * Returns final filename after applying storage-engine specific naming
	 *
	 * @param string $file_name
	 * @param Array $options
	 * @param kDBItem $object
	 * @param Array $files_to_delete
	 * @return string
	 * @access protected
	 */
	protected function _getRealFilename($file_name, $options, $object, $files_to_delete = Array ())
	{
		$real_name = $this->getStorageEngineFile($file_name, $options, $object->Prefix);
		$real_name = $this->getStorageEngineFolder($real_name, $options) . $real_name;

		return $this->fileHelper->ensureUniqueFilename($this->FullPath, $real_name, $files_to_delete);
	}

	/**
	 * Renames file in sorting list
	 *
	 * @param string $old_name
	 * @param string $new_name
	 * @return void
	 * @access protected
	 */
	protected function _renameFileInSorting($old_name, $new_name)
	{
		$index = array_search($old_name, $this->sorting);

		if ( $index !== false ) {
			$this->sorting[$index] = $new_name;
		}
	}

	function getSingleFormat($format)
	{
		$single_mapping = Array (
			'file_raw_urls' => 'raw_url',
			'file_display_names' => 'display_name',
			'file_urls' => 'full_url',
			'file_paths' => 'full_path',
			'file_sizes' => 'file_size',
			'files_resized' => 'resize',
			'img_sizes' => 'img_size',
			'wms' => 'wm',
		);

		return $single_mapping[$format];
	}

	/**
	 * Return formatted file url,path or size (or same for multiple files)
	 *
	 * @param string $value
	 * @param string $field_name
	 * @param kDBItem|kDBList $object
	 * @param string $format
	 * @return string
	 */
	function Format($value, $field_name, &$object, $format = NULL)
	{
		if ( is_null($value) ) {
			return '';
		}

		$options = $object->GetFieldOptions($field_name);

		if ( !isset($format) ) {
			$format = isset($options['format']) ? $options['format'] : false;
		}

		if ( $format && preg_match('/(file_raw_urls|file_display_names|file_urls|file_paths|file_names|file_sizes|img_sizes|files_resized|wms)(.*)/', $format, $regs) ) {
			if ( !$value || $format == 'file_names' ) {
				// storage format matches display format OR no value
				return $value;
			}

			$ret = Array ();
			$files = explode('|', $value);
			$format = $this->getSingleFormat($regs[1]) . $regs[2];

			foreach ($files as $a_file) {
				$ret[] = $this->GetFormatted($a_file, $field_name, $object, $format);
			}

			return implode('|', $ret);
		}

		$tc_value = $this->TypeCast($value, $options);

		if ( ($tc_value === false) || ($tc_value != $value) ) {
			// for leaving badly formatted date on the form
			return $value;
		}

		return $this->GetFormatted($tc_value, $field_name, $object, $format);
	}

	/**
	 * Return formatted file url,path or size
	 *
	 * @param string $value
	 * @param string $field_name
	 * @param kDBItem $object
	 * @param string $format
	 * @return string
	 */
	function GetFormatted($value, $field_name, &$object, $format = NULL)
	{
		if ( !$format ) {
			return $value;
		}

		$options = $object->GetFieldOptions($field_name);
		$upload_dir = isset($options['include_path']) && $options['include_path'] ? '' : $this->getUploadDir($options);
		$file_path = strlen($value) ? FULL_PATH . str_replace('/', DIRECTORY_SEPARATOR, $upload_dir) . $value : '';

		if ( preg_match('/resize:([\d]*)x([\d]*)/', $format, $regs) ) {
			/** @var ImageHelper $image_helper */
			$image_helper = $this->Application->recallObject('ImageHelper');

			try {
				return $image_helper->ResizeImage($file_path, $format);
			}
			catch ( RuntimeException $e ) {
				// error, during image resize -> return empty string
				return '';
			}
		}
		elseif ( !strlen($file_path) || !file_exists($file_path) ) {
			// file doesn't exist OR not uploaded
			return '';
		}

		switch ($format) {
			case 'display_name':
				return kUtil::removeTempExtension($value);
				break;

			case 'raw_url':
				return $this->fileHelper->pathToUrl($file_path);
				break;

			case 'full_url':
				$direct_links = isset($options['direct_links']) ? $options['direct_links'] : true;

				if ( $direct_links ) {
					return $this->fileHelper->pathToUrl($file_path);
				}
				else {
					$url_params = Array (
						'pass' => 'm,'.$object->Prefix,
						$object->Prefix . '_event' => 'OnViewFile',
						'file' => $value, 'field' => $field_name
					);

					return $this->Application->HREF('', '', $url_params);
				}
				break;

			case 'full_path':
				return $file_path;
				break;

			case 'file_size':
				return filesize($file_path);
				break;

			case 'img_size':
				/** @var ImageHelper $image_helper */
				$image_helper = $this->Application->recallObject('ImageHelper');

				$image_info = $image_helper->getImageInfo($file_path);
				return $image_info ? $image_info[3] : '';
				break;
		}

		return sprintf($format, $value);
	}

	/**
	 * Creates & returns folder, based on storage engine specified in field options
	 *
	 * @param string $file_name
	 * @param array $options
	 * @return string
	 * @access protected
	 * @throws Exception
	 */
	protected function getStorageEngineFolder($file_name, $options)
	{
		$storage_engine = (string)getArrayValue($options, 'storage_engine');

		if ( !$storage_engine ) {
			return '';
		}

		switch ($storage_engine) {
			case StorageEngine::HASH:
				$folder_path = kUtil::getHashPathForLevel($file_name);
				break;

			case StorageEngine::TIMESTAMP:
				$folder_path = adodb_date('Y-m/d/');
				break;

			default:
				throw new Exception('Unknown storage engine "<strong>' . $storage_engine . '</strong>".');
				break;
		}

		return $folder_path;
	}

	/**
	 * Applies prefix & suffix to uploaded filename, based on storage engine in field options
	 *
	 * @param string $name
	 * @param array $options
	 * @param string $unit_prefix
	 * @return string
	 * @access protected
	 */
	protected function getStorageEngineFile($name, $options, $unit_prefix)
	{
		$prefix = $this->getStorageEngineFilePart(getArrayValue($options, 'filename_prefix'), $unit_prefix);
		$suffix = $this->getStorageEngineFilePart(getArrayValue($options, 'filename_suffix'), $unit_prefix);

		$parts = pathinfo($name);

		return ($prefix ? $prefix . '_' : '') . $parts['filename'] . ($suffix ? '_' . $suffix : '') . '.' . $parts['extension'];
	}

	/**
	 * Creates prefix/suffix to join with uploaded file
	 *
	 * Added "u" before user_id to keep this value after FileHelper::ensureUniqueFilename method call
	 *
	 * @param string $option
	 * @param string $unit_prefix
	 * @return string
	 * @access protected
	 */
	protected function getStorageEngineFilePart($option, $unit_prefix)
	{
		$replace_from = Array (
			StorageEngine::PS_DATE_TIME, StorageEngine::PS_PREFIX, StorageEngine::PS_USER
		);

		$replace_to = Array (
			adodb_date('Ymd-His'), $unit_prefix, 'u' . $this->Application->RecallVar('user_id')
		);

		return str_replace($replace_from, $replace_to, $option);
	}

	public function getUploadDir($options)
	{
		return isset($options['upload_dir']) ? $options['upload_dir'] : $this->DestinationPath;
	}
}


class kPictureFormatter extends kUploadFormatter
{
	public function __construct()
	{
		$this->NakeLookupPath = IMAGES_PATH; // used ?
		$this->DestinationPath = kUtil::constOn('ADMIN') ? IMAGES_PENDING_PATH : IMAGES_PATH;

		parent::__construct();
	}

	/**
	 * Return formatted file url,path or size
	 *
	 * @param string $value
	 * @param string $field_name
	 * @param kDBItem $object
	 * @param string $format
	 * @return string
	 */
	function GetFormatted($value, $field_name, &$object, $format = NULL)
	{
		if ( $format == 'img_size' ) {
			$options = $object->GetFieldOptions($field_name);
			$img_path = FULL_PATH . '/' . $this->getUploadDir($options) . $value;
			$image_info = getimagesize($img_path);

			return ' ' . $image_info[3];
		}

		return parent::GetFormatted($value, $field_name, $object, $format);
	}
}
