<?php
namespace JExtstore\Component\JChat\Site\Model;
/** 
 * @package JCHAT::ATTACHMENTS::components::com_jchat
 * @subpackage models
 * @author Joomla! Extensions Store
 * @Copyright (C) 2015 - Joomla! Extensions Store
 * @license GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html   
 */
defined( '_JEXEC' ) or die( 'Restricted access' );
use Joomla\CMS\Language\Text;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Filter\InputFilter;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Filesystem\File;
use JExtstore\Component\JChat\Administrator\Framework\Model as JChatModel;
use JExtstore\Component\JChat\Administrator\Framework\Helpers\Users as JChatHelpersUsers;
use JExtstore\Component\JChat\Administrator\Framework\Thumb\Factory as JChatThumbFactory;

/**
 * Here the entity is the file attachment message on stream
 * 
 * @package JCHAT::ATTACHMENTS::components::com_jchat
 * @subpackage models
 * @since 1.0
 */ 
class AttachmentsModel extends JChatModel {
	/**
	 * @access private
	 * @var int
	 */
	private $cacheFolder;
	
	/**
	 * User object
	 *
	 * @access private
	 * @var int
	 */
	private $myUser;
	
	/**
	 * User object
	 *
	 * @access private
	 * @var int
	 */
	private $lastInsertId;
	
	/**
	 * @access private
	 * @param string $originalFilename
	 * @param string $thumbFilename
	 * @param string $fileExtension
	 * @return boolean
	 */
	private function resizeImage($originalFilename, $thumbFilename, $fileExtension) {
		$thumb = JChatThumbFactory::create($originalFilename);
		$resizeWidth = $this->componentParams->get('resize_attachments_images_maxwidth', 800);
		
		$defaultOptions = array (
				'resizeUp'				=> false,
				'jpegQuality'			=> $this->componentParams->get('resize_attachments_images_quality', 80),
				'correctPermissions'	=> false,
				'preserveAlpha'			=> true,
				'alphaMaskColor'		=> array (255, 255, 255),
				'preserveTransparency'	=> false,
				'transparencyMaskColor'	=> array (0, 0, 0)
		);
		$thumb->setOptions($defaultOptions);
		
		$thumb->resize($resizeWidth);
		
		$thumb->save($thumbFilename, $fileExtension);
	}
	
	/**
	 * Generate file name hash
	 * 
	 * @access private
	 * @param string $filename
	 * @param string $userid
	 * @return string 
	 */
	private function generaHash($filename, $messageId) { 
		$filenameStripped = File::stripExt($filename); 
		$fileExtension = File::getExt($filename);
		 
		$hash = md5($filenameStripped . $messageId);
		return $hash . '.' . $fileExtension;
	}
	
	/**
	 * Store file attachment message on database as a special record
	 * 
	 * @access private
	 * @param string $filename
	 * @return boolean 
	 */
	private function storeDBMessage($filename) { 
		if ($this->getState('to', null) && !empty($filename)) {
			// Get user reference
			$to = $this->getState('to', null);
			$tologged = $this->getState('tologged', null);

			// Valid target user session id?
			if($to == -1 && $tologged) {
				$sessionSql =  "SELECT" .
							   "\n " . $this->dbInstance->quoteName('session_id') .
							   "\n FROM #__session" .
							   "\n WHERE" .
							   "\n " . $this->dbInstance->quoteName('userid') . " = " . (int)$tologged .
							   "\n ORDER BY " . $this->dbInstance->quoteName('time') . " DESC" .
							   "\n LIMIT 1";
				$this->dbInstance->setQuery($sessionSql);
				$sessionIDReceiver = $this->dbInstance->loadResult();
				$to = $sessionIDReceiver ? $sessionIDReceiver : -1;
			}
			
			// Get users actual names
			$actualNames = JChatHelpersUsers::getActualNames ( $this->getState('from'), $this->getState('to'), $this->componentParams );
			
			$unixTimeStamp = time();
			$sql = "INSERT INTO #__jchat (" .
					$this->dbInstance->quoteName('from') . ',' .
					$this->dbInstance->quoteName('to') . ',' .
					$this->dbInstance->quoteName('fromuser') . ',' .
					$this->dbInstance->quoteName('touser') . ',' .
					$this->dbInstance->quoteName('message') . ',' .
					$this->dbInstance->quoteName('sent') . ',' .
					$this->dbInstance->quoteName('read') . ',' .
					$this->dbInstance->quoteName('type') . ',' .
					$this->dbInstance->quoteName('status') . ',' .
					$this->dbInstance->quoteName('actualfrom') . ',' .
					$this->dbInstance->quoteName('actualto') . ',' .
					$this->dbInstance->quoteName('ipaddress') . ') VALUES( ' . 
					$this->dbInstance->quote($this->getState('from')). ", ".
					$this->dbInstance->quote($to). ",".
					$this->dbInstance->quote($this->myUser->id). ", ".
					$this->dbInstance->quote($tologged). ",".
					$this->dbInstance->quote($filename) . ",".
					$this->dbInstance->quote($unixTimeStamp) . ",".
					"0" . "," .
					$this->dbInstance->quote('file') . "," .
					"0" . "," .
					$this->dbInstance->quote($actualNames['fromActualName']) . ", ".
					$this->dbInstance->quote($actualNames['toActualName']) . ", ".
					$this->dbInstance->quote($_SERVER['REMOTE_ADDR']) . 
					")";
		    $this->dbInstance->setQuery($sql);
			if(!$this->dbInstance->execute()){
				return false;
			} 
			
			if (empty($this->sessionName['jchat_user_'.$this->getState('to')])) {
				$this->sessionName['jchat_user_'.$this->getState('to')] = array();
			}
			
			$lastInsertId = $this->dbInstance->insertid();
			$this->lastInsertId = $lastInsertId;
			$insertTime = HTMLHelper::_('date', $unixTimeStamp, Text::_('DATE_FORMAT_LC2'));
			$this->sessionName['jchat_user_'.$this->getState('to')][$lastInsertId] = array(
																					"id" => $this->dbInstance->insertid(),
																				  	"from" => $this->getState('to'),
																				  	"message" => $filename,
																				  	"type" => 'file',
																				  	"status" => 0,
																				  	"time" => $insertTime,
																				  	"self" => 1,
																				  	"old" => 1) ;
		}
		return true;
	}

	/**
	 * Store file attachment message on database as a special record
	 * to all participants to the conference after retrieving the list of other peers involved
	 *
	 * @access private
	 * @param string $filename
	 * @return boolean
	 */
	private function storeDBConferenceMessage($filename) {
		if (!empty($filename)) {

			// Get this peer session ID identifier
			$thisPeer = $this->getState('from');

			if($this->getState('isLiveStreaming')) {
				// Valid target user session id?
				$otherConfPeersSql = "SELECT" .
									 "\n status.sessionid AS peer2, sess.userid" .
									 "\n FROM #__jchat_sessionstatus AS status" .
									 "\n LEFT JOIN #__session AS sess" .
									 "\n ON status.sessionid = sess.session_id" .
									 "\n WHERE" .
									 "\n status.livestreaming_hash = " . $this->dbInstance->quote($thisPeer) .
									 "\n AND status.sessionid != " . $this->dbInstance->quote($thisPeer);
			} else {
				// Valid target user session id?
				$otherConfPeersSql = "SELECT" .
									 "\n conf.peer2, sess.userid" .
									 "\n FROM #__jchat_webrtc_conference AS conf" .
									 "\n LEFT JOIN #__session AS sess" .
									 "\n ON conf.peer2 = sess.session_id" .
									 "\n WHERE" .
									 "\n conf.peer1 = " . $this->dbInstance->quote($thisPeer);
			}
			$this->dbInstance->setQuery($otherConfPeersSql);
			$otherConfPeers = $this->dbInstance->loadObjectList();

			if(is_array($otherConfPeers) && count($otherConfPeers)) {
				foreach ($otherConfPeers as $otherConfPeer) {
					// Get users actual names
					$actualNames = JChatHelpersUsers::getActualNames ( $thisPeer, $otherConfPeer->peer2, $this->componentParams );

					$unixTimeStamp = time();
					$sql = "INSERT INTO #__jchat (" .
							$this->dbInstance->quoteName('from') . ',' .
							$this->dbInstance->quoteName('to') . ',' .
							$this->dbInstance->quoteName('fromuser') . ',' .
							$this->dbInstance->quoteName('touser') . ',' .
							$this->dbInstance->quoteName('message') . ',' .
							$this->dbInstance->quoteName('sent') . ',' .
							$this->dbInstance->quoteName('read') . ',' .
							$this->dbInstance->quoteName('type') . ',' .
							$this->dbInstance->quoteName('status') . ',' .
							$this->dbInstance->quoteName('actualfrom') . ',' .
							$this->dbInstance->quoteName('actualto') . ',' .
							$this->dbInstance->quoteName('ipaddress') . ') VALUES( ' .
							$this->dbInstance->quote($thisPeer). ", ".
							$this->dbInstance->quote($otherConfPeer->peer2). ",".
							$this->dbInstance->quote($this->myUser->id). ", ".
							$this->dbInstance->quote($otherConfPeer->userid). ",".
							$this->dbInstance->quote($filename) . ",".
							$this->dbInstance->quote($unixTimeStamp) . ",".
							"0" . "," .
							$this->dbInstance->quote('file') . "," .
							"0" . "," .
							$this->dbInstance->quote($actualNames['fromActualName']) . ", ".
							$this->dbInstance->quote($actualNames['toActualName']) . ", ".
							$this->dbInstance->quote($_SERVER['REMOTE_ADDR']) .
							")";
							$this->dbInstance->setQuery($sql);
							if(!$this->dbInstance->execute()){
								return false;
							}

							if (empty($this->sessionName['jchat_user_'.$otherConfPeer->peer2])) {
								$this->sessionName['jchat_user_'.$otherConfPeer->peer2] = array();
							}

							$lastInsertId = $this->dbInstance->insertid();
							$this->lastInsertId = $lastInsertId;
							$insertTime = HTMLHelper::_('date', $unixTimeStamp, Text::_('DATE_FORMAT_LC2'));
							$this->sessionName['jchat_user_'.$otherConfPeer->peer2][$lastInsertId] = array(
																	"id" => $this->dbInstance->insertid(),
																	"from" => $otherConfPeer->peer2,
																	"message" => $filename,
																	"type" => 'file',
																	"status" => 0,
																	"time" => $insertTime,
																	"self" => 1,
																	"old" => 1) ;
				}
			}
		}
		return true;
	}

	/**
	 * Read file by chunks to send to output buffer
	 * 
	 * @access private
	 * @param string $nomefile
	 * @return boolean
	 */
	private function readFileChunked($filePath) {
		$chunksize = 1 * (1024 * 1024); // how many bytes per chunk
		$buffer = '';
		$cnt = 0;
		$handle = fopen ( $filePath, 'rb' );
		if ($handle === false) {
			return false;
		}
		while ( ! feof ( $handle ) ) {
			$buffer = fread ( $handle, $chunksize );
			echo $buffer;
			@ob_flush ();
			flush ();
		}
		$status = fclose ( $handle );
		return $status;
	}
	
	/**
	 * Detect mime type for streamed file
	 * 
	 * @access private
	 * @param $filename
	 * @return string Il mime type trovato a fronte del lookup nella tabella
	 */
	private function detectMimeType($filename) {
		global $mosConfig_absolute_path;
		include_once JPATH_COMPONENT_ADMINISTRATOR . '/Framework/Helpers/mime.mapping.php';
		
		$filename = strtolower ( $filename );
		$exts = preg_split ( "#[/\\.]#i", $filename );
		$n = count ( $exts ) - 1;
		$fileExtension = $exts [$n];
		
		foreach ( $mime_extension_map as $extension => $mime ) {
			if ($extension === $fileExtension)
				return $mime;
		}
		return 'application/octet-stream';
	}

	/**
	 * Store uploaded file to cache folder,
	 * fully manage error messages and ask for database insert
	 *
	 * @access public
	 * @param bool $updateNulls
	 * @return mixed
	 */
	public function storeEntity($updateNulls = false) {
		$tmpFile = $this->requestFilesName['newfile']['tmp_name'];
		$tmpFileName = $this->requestFilesName['newfile']['name'];
		
		if(!$tmpFile || !$tmpFileName) {
			$msg = Text::_('COM_JCHAT_NOFILE_SELECTED');
			$this->setError($msg);
			return;
		}
		
		$tmpFileSize = $this->requestFilesName['newfile']['size'];
		$allowedFileSize = $this->componentParams->get('maxfilesize', 2) * 1024 * 1024; // MB->Bytes
		if($tmpFileSize > $allowedFileSize) {
			$msg = Text::_('COM_JCHAT_SIZE_ERROR') .' Max ' . $this->componentParams->get('maxfilesize', 2) . 'MB.';
			$this->setError($msg);
			return;
		}
		
		$disallowedExtensions = explode(',', $this->componentParams->get('disallowed_extensions', 'exe,bat,pif')); 
		$tmpFileExtension = @array_pop(explode('.', $tmpFileName));
		if(in_array(strtolower($tmpFileExtension), $disallowedExtensions)) {
			$msg = Text::_('COM_JCHAT_EXT_ERROR') . $this->componentParams->get('disallowed_extensions', 'exe,bat,pif');
			$this->setError($msg);
			return;
		}
				
		if(!is_dir($this->cacheFolder)) {
			Folder::create($this->cacheFolder);
		}
		
		if(!is_writable($this->cacheFolder)) {
			try {
				if(!chmod($this->cacheFolder, 0775)) {
					throw new \Exception( Text::_('COM_JCHAT_DIR_WRITABLE'));
				}
			} catch(\Exception $e) {
				$msg = $e->getMessage();
				$this->setError($msg);
				return;
			}
		}
		 
		if(!move_uploaded_file($tmpFile, $this->cacheFolder . $tmpFileName)) {
			$msg =  Text::_('COM_JCHAT_UPLOAD_ERROR');
			$this->setError($msg);
			return;
		}
	 
		// Store the DB message file
		$filter = InputFilter::getInstance();
		if($this->getState('receiver', null) === 'conference') {
			// Set the message to be received by conference joined users
			if(!$this->storeDBConferenceMessage($filter->clean($tmpFileName))) {
				$msg =  Text::_('COM_JCHAT_SENDMSGFILE_ERROR');
				$this->setError($msg);
				return;
			}
			$this->lastInsertId = 'conference';
		} else {
			if(!$this->storeDBMessage($filter->clean($tmpFileName))) {
				$msg =  Text::_('COM_JCHAT_SENDMSGFILE_ERROR');
				$this->setError($msg);
				return;
			}
		}
		
		// Hash and store the file on file system
		$hashedFileName = $this->generaHash($tmpFileName, $this->lastInsertId);
		if(file_exists($this->cacheFolder . $hashedFileName)) {
			unlink($this->cacheFolder . $hashedFileName);
		}

		// Check if there is an image file uploaded and if the option to resize images is active
		if($this->componentParams->get('resize_attachments_images', 0) && in_array(strtolower($tmpFileExtension), array('jpg', 'jpeg', 'gif'))) {
			$this->resizeImage($this->cacheFolder . $tmpFileName, $this->cacheFolder . $hashedFileName, $tmpFileExtension);
			unlink($this->cacheFolder . $tmpFileName);
		} else {
			if(!rename($this->cacheFolder . $tmpFileName, $this->cacheFolder . $hashedFileName)) {
				$msg = Text::_('COM_JCHAT_RENAME_ERROR');
				$this->setError($msg);
				return;
			}
		}

		$msg = Text::_('COM_JCHAT_SUCCESS_FILEUPLOAD');
		$this->setState('result', $msg);
	}

	/**
	 * Download uploaded file message
	 * 
	 * @access public
	 * @return void
	 */
	public function loadEntity($ids = null) { 
		$idMessage = $this->getState('idMessage');
		$idUserConversation = $this->getState('from');
		 
		try {
			$query = "SELECT #__jchat.from, #__jchat.message FROM #__jchat WHERE id = " . (int)$idMessage;
			$this->dbInstance->setQuery($query);
			$resultInfo = $this->dbInstance->loadObject();
			if(!$resultInfo) {
				$conversationArray = $this->sessionName['jchat_user_' . $idUserConversation];
				foreach ($conversationArray as $message) {
					if($message['id'] == $idMessage) {
						$resultInfo = new \stdClass();
						$resultInfo->from = $message['from'];
						$resultInfo->message = $message['message'];
						break;
					} 
				}
				if(!$resultInfo) {
					throw new \Exception('COM_JCHAT_ERROR_NOTFOUND_FILE');
				}
			}
			$fileName = $this->generaHash($resultInfo->message, $idMessage);
			$filePath = $this->cacheFolder . $fileName;
			
			if(!file_exists($filePath)) {
				// Check if there is a conference shared file, fallback here before throwing an error
				$fileName = $this->generaHash($resultInfo->message, 'conference');
				$filePath = $this->cacheFolder . $fileName;
				if(!file_exists($filePath)) {
					throw new \Exception('COM_JCHAT_ERROR_DELETED_FILE');
				}
			}
		} catch (\Exception $e) {
			// JS inject
			$appNonce = $this->app->get('csp_nonce', null);
			$nonce = $appNonce ? ' nonce="' . $appNonce . '"' : '';
			echo '<script' . $nonce . '>alert("' . Text::_($e->getMessage()) . '");window.history.go(-1);</script>';
			exit();
		} 
		
		$fsize = @filesize ( $filePath );
		$mod_date = date ( 'r', filemtime ( $filePath ) ); 
		$cont_dis = 'attachment';
		$mimeType = $this->detectMimeType ( $fileName );
		
		// required for IE, otherwise Content-disposition is ignored
		if (ini_get ( 'zlib.output_compression' )) {
			ini_set ( 'zlib.output_compression', 'Off' );
		}
		header ( "Pragma: public" );
		header ( "Cache-Control: must-revalidate, post-check=0, pre-check=0" );
		header ( "Expires: 0" );
		header ( "Content-Transfer-Encoding: binary" );
		header ( 'Content-Disposition:' . $cont_dis . ';' . ' filename="' . $resultInfo->message . '";' . ' modification-date="' . $mod_date . '";' . ' size=' . $fsize . ';' ); //RFC2183
		header ( "Content-Type: " . $mimeType ); // MIME type
		header ( "Content-Length: " . $fsize );
		if (! ini_get ( 'safe_mode' )) { // set_time_limit doesn't work in safe mode
			@set_time_limit ( 0 );
		}
		// No encoding - we aren't using compression... (RFC1945)
		//header("Content-Encoding: none");
		//header("Vary: none");
		$downloadStatus = $this->readFileChunked ( $filePath );
		
		// Al raggiungimento dell'effettivo download si aggiorna lo status update
		if($downloadStatus) {
			$query = "UPDATE #__jchat SET status=1 WHERE id = " . (int)$idMessage;
			$this->dbInstance->setQuery($query); 
			$this->dbInstance->execute();
		} 
		exit();
	}

	/**
	 * Class constructor
	 * 
	 * @access public
	 * @return Object &
	 */
	public function __construct($config = array(), MVCFactoryInterface $factory = null) {
		$this->getComponentParams();
		
		$this->cacheFolder = JPATH_SITE . '/components/com_jchat/cache/';
		$this->myUser = Factory::getApplication()->getIdentity();
		
		parent::__construct( $config, $factory );
	}
} 
?>