<?php
/**
* @version	$Id$
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2012 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.
*/

class kUploadHelper extends kHelper
{

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

	/**
	 * Creates kUploadHelper instance.
	 */
	public function __construct()
	{
		parent::__construct();

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

		// 5 minutes execution time
		@set_time_limit(5 * 60);
	}

	/**
	 * Handles the upload.
	 *
	 * @param kEvent $event Event.
	 *
	 * @return string
	 * @throws kUploaderException When upload could not be handled properly.
	 */
	public function handle(kEvent $event)
	{
		$this->disableBrowserCache();

//		Uncomment this one to fake upload time
//		sleep(5);

		if ( !$this->Application->HttpQuery->Post ) {
			// Variables {field, id, flashsid} are always submitted through POST!
			// When file size is larger, then "upload_max_filesize" (in php.ini),
			// then these variables also are not submitted.
			throw new kUploaderException('File size exceeds allowed limit.', 413);
		}

		if ( !$this->checkPermissions($event) ) {
			// 403 Forbidden
			throw new kUploaderException('You don\'t have permissions to upload.', 403);
		}

		$value = $this->Application->GetVar('file');

		if ( !$value || ($value['error'] != UPLOAD_ERR_OK) ) {
			// 413 Request Entity Too Large (file uploads disabled OR uploaded file was
			// too large for web server to accept, see "upload_max_filesize" in php.ini)
			throw new kUploaderException('File size exceeds allowed limit.', 413);
		}

		$value = $this->Application->unescapeRequestVariable($value);

		$tmp_path = WRITEABLE . '/tmp/';
		$filename = $this->getUploadedFilename() . '.tmp';
		$id = $this->Application->GetVar('id');

		if ( $id ) {
			$filename = $id . '_' . $filename;
		}

		if ( !is_writable($tmp_path) ) {
			// 500 Internal Server Error
			// check both temp and live upload directory
			throw new kUploaderException('Write permissions not set on the server, please contact server administrator.', 500);
		}

		$filename = $this->fileHelper->ensureUniqueFilename($tmp_path, $filename);
		$storage_format = $this->getStorageFormat($this->Application->GetVar('field'), $event);

		$file_path = $tmp_path . $filename;
		$actual_file_path = $this->moveUploadedFile($file_path);

		if ( $storage_format && $file_path == $actual_file_path ) {
			$this->resizeUploadedFile($file_path, $storage_format);
		}

		$this->deleteTempFiles($tmp_path);

		$thumbs_path = preg_replace('/^' . preg_quote(FULL_PATH, '/') . '/', '', $tmp_path, 1);
		$thumbs_path = FULL_PATH . THUMBS_PATH . $thumbs_path;

		if ( file_exists($thumbs_path) ) {
			$this->deleteTempFiles($thumbs_path);
		}

		return preg_replace('/^' . preg_quote($id, '/') . '_/', '', $filename);
	}

	/**
	 * Resizes uploaded file.
	 *
	 * @param string $file_path File path.
	 * @param string $format    Format.
	 *
	 * @return boolean
	 */
	public function resizeUploadedFile($file_path, $format)
	{
		/** @var ImageHelper $image_helper */
		$image_helper = $this->Application->recallObject('ImageHelper');

		// Add extension, so that "ImageHelper::ResizeImage" can work.
		$resize_file_path = tempnam(sys_get_temp_dir(), 'uploaded_') . '.jpg';

		if ( rename($file_path, $resize_file_path) === false ) {
			return false;
		}

		$resized_file_path = $this->fileHelper->urlToPath(
			$image_helper->ResizeImage($resize_file_path, $format)
		);

		return rename($resized_file_path, $file_path);
	}

	/**
	 * Sends headers to ensure, that response is never cached.
	 *
	 * @return void
	 */
	protected function disableBrowserCache()
	{
		header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
		header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
		header('Cache-Control: no-store, no-cache, must-revalidate');
		header('Cache-Control: post-check=0, pre-check=0', false);
		header('Pragma: no-cache');
	}

	/**
	 * Checks, that flash uploader is allowed to perform upload
	 *
	 * @param kEvent $event
	 * @return bool
	 */
	protected function checkPermissions(kEvent $event)
	{
		// Flash uploader does NOT send correct cookies, so we need to make our own check
		$cookie_name = 'adm_' . $this->Application->ConfigValue('SessionCookieName');
		$this->Application->HttpQuery->Cookie['cookies_on'] = 1;
		$this->Application->HttpQuery->Cookie[$cookie_name] = $this->Application->GetVar('flashsid');

		// this prevents session from auto-expiring when KeepSessionOnBrowserClose & FireFox is used
		$this->Application->HttpQuery->Cookie[$cookie_name . '_live'] = $this->Application->GetVar('flashsid');

		/** @var Session $admin_session */
		$admin_session = $this->Application->recallObject('Session.admin');

		if ( $this->Application->permissionCheckingDisabled($admin_session->RecallVar('user_id')) ) {
			return true;
		}

		// copy some data from given session to current session
		$backup_user_id = $this->Application->RecallVar('user_id');
		$this->Application->StoreVar('user_id', $admin_session->RecallVar('user_id'));

		$backup_user_groups = $this->Application->RecallVar('UserGroups');
		$this->Application->StoreVar('UserGroups', $admin_session->RecallVar('UserGroups'));

		// check permissions using event, that have "add|edit" rule
		$check_event = new kEvent($event->getPrefixSpecial() . ':OnProcessSelected');
		$check_event->setEventParam('top_prefix', $this->Application->GetTopmostPrefix($event->Prefix, true));

		/** @var kEventHandler $event_handler */
		$event_handler = $this->Application->recallObject($event->Prefix . '_EventHandler');
		$allowed_to_upload = $event_handler->CheckPermission($check_event);

		// restore changed data, so nothing gets saved to database
		$this->Application->StoreVar('user_id', $backup_user_id);
		$this->Application->StoreVar('UserGroups', $backup_user_groups);

		return $allowed_to_upload;
	}

	/**
	 * Returns uploaded filename.
	 *
	 * @return string
	 */
	protected function getUploadedFilename()
	{
		if ( isset($_REQUEST['name']) ) {
			$file_name = $_REQUEST['name'];
		}
		elseif ( !empty($_FILES) ) {
			$file_name = $_FILES['file']['name'];
		}
		else {
			$file_name = uniqid('file_');
		}

		return $file_name;
	}

	/**
	 * Gets storage format for a given field.
	 *
	 * @param string $field_name
	 * @param kEvent $event
	 * @return bool
	 */
	protected function getStorageFormat($field_name, kEvent $event)
	{
		$fields = $this->Application->getUnitOption($event->Prefix, 'Fields');
		$virtual_fields = $this->Application->getUnitOption($event->Prefix, 'VirtualFields');
		$field_options = array_key_exists($field_name, $fields) ? $fields[$field_name] : $virtual_fields[$field_name];

		return isset($field_options['storage_format']) ? $field_options['storage_format'] : false;
	}

	/**
	 * Moves uploaded file to given location.
	 *
	 * @param string $file_path File path.
	 *
	 * @return string
	 * @throws kUploaderException When upload could not be handled properly.
	 */
	protected function moveUploadedFile($file_path)
	{
		// Chunking might be enabled.
		$chunk = (int)$this->Application->GetVar('chunk', 0);
		$chunks = (int)$this->Application->GetVar('chunks', 0);
		$actual_file_path = $file_path . '.part';

		// Open temp file.
		if ( !$out = @fopen($actual_file_path, $chunks ? 'ab' : 'wb') ) {
			throw new kUploaderException('Failed to open output stream.', 102);
		}

		if ( !empty($_FILES) ) {
			if ( $_FILES['file']['error'] || !is_uploaded_file($_FILES['file']['tmp_name']) ) {
				throw new kUploaderException('Failed to move uploaded file.', 103);
			}

			// Read binary input stream and append it to temp file.
			if ( !$in = @fopen($_FILES['file']['tmp_name'], 'rb') ) {
				throw new kUploaderException('Failed to open input stream.', 101);
			}
		}
		else {
			if ( !$in = @fopen('php://input', 'rb') ) {
				throw new kUploaderException('Failed to open input stream.', 101);
			}
		}

		while ( $buff = fread($in, 4096) ) {
			fwrite($out, $buff);
		}

		@fclose($out);
		@fclose($in);

		// Check if file has been uploaded.
		if ( !$chunks || $chunk == $chunks - 1 ) {
			// Strip the temp .part suffix off.
			rename($actual_file_path, $file_path);
			$actual_file_path = $file_path;
		}

		return $actual_file_path;
	}

	/**
	 * Delete temporary files, that won't be used for sure
	 *
	 * @param string $path
	 * @return void
	 */
	protected function deleteTempFiles($path)
	{
		$files = glob($path . '*.*');
		$max_file_date = strtotime('-1 day');

		foreach ( $files as $file ) {
			if ( filemtime($file) < $max_file_date ) {
				unlink($file);
			}
		}
	}

	/**
	 * Prepares object for operations with file on given field.
	 *
	 * @param kEvent $event Event.
	 * @param string $field Field.
	 *
	 * @return kDBItem
	 */
	public function prepareUploadedFile(kEvent $event, $field)
	{
		/** @var kDBItem $object */
		$object = $event->getObject(Array ('skip_autoload' => true));

		$filename = $this->getSafeFilename();

		if ( !$filename ) {
			$object->SetDBField($field, '');

			return $object;
		}

		// set current uploaded file
		if ( $this->Application->GetVar('tmp') ) {
			$options = $object->GetFieldOptions($field);
			$options['upload_dir'] = WRITEBALE_BASE . '/tmp/';
			unset($options['include_path']);
			$object->SetFieldOptions($field, $options);

			$filename = $this->Application->GetVar('id') . '_' . $filename;
		}

		$object->SetDBField($field, $filename);

		return $object;
	}

	/**
	 * Returns safe version of filename specified in url
	 *
	 * @return bool|string
	 * @access protected
	 */
	protected function getSafeFilename()
	{
		$filename = $this->Application->GetVar('file');
		$filename = $this->Application->unescapeRequestVariable($filename);

		if ( (strpos($filename, '../') !== false) || (trim($filename) !== $filename) ) {
			// when relative paths or special chars are found template names from url, then it's hacking attempt
			return false;
		}

		return $filename;
	}
}


class kUploaderException extends Exception
{

}
