<?php
/**
 * Do not use or reference this directly from your client-side code.
 * Instead, this should be required via the endpoint.php or endpoint-cors.php
 * file(s).
 */
class UploadHandler {
	public $allowedExtensions = array();
	public $sizeLimit = null;
	public $inputName = 'qqfile';
	public $chunksFolder = 'chunks';
	public $chunksCleanupProbability = 0.001; // Once in 1000 requests on avg
	public $chunksExpireIn = 604800; // One week
	protected $uploadName;

	/**
		* Get the original filename
		*/
	public function getName()
	{
			if( isset( $_REQUEST['qqfilename'] ) )
					return $_REQUEST['qqfilename'];
			if( isset( $_FILES[$this->inputName] ) )
					return $_FILES[$this->inputName]['name'];
	}

	public function getInitialFiles()
	{
		$initialFiles = array();
		for( $i = 0; $i < 5000; $i++ )
		{
			array_push( $initialFiles, array(
				"name" => "name" + $i,
				uuid => "uuid" + $i,
				thumbnailUrl => "/test/dev/handlers/vendor/fineuploader/php-traditional-server/fu.png" ) );
		}

		return $initialFiles;
	}

	/**
		* Get the name of the uploaded file
		*/
	public function getUploadName()
	{
			return $this->uploadName;
	}

	public function combineChunks( $name = null )
	{
		$uuid = $_POST['qquuid'];

		if( $name === null )
		{
			$name = $this->getName();
		}

		$targetFolder = $this->chunksFolder.DIRECTORY_SEPARATOR.$uuid;
		$totalParts = isset( $_REQUEST['qqtotalparts'] ) ? ( int ) $_REQUEST['qqtotalparts'] : 1;
		$targetPath = join( DIRECTORY_SEPARATOR, array( $uuid, $name ) );
		$this->uploadName = $name;

		if ( ! file_exists( $targetPath ) )
		{
			mkdir( dirname( $targetPath ), 0777, true );
		}

		$target = fopen( $targetPath, 'wb' );
		for( $i = 0; $i < $totalParts; $i++ )
		{
			$chunk = fopen( $targetFolder.DIRECTORY_SEPARATOR.$i, "rb" );
			stream_copy_to_stream( $chunk, $target );
			fclose ($chunk );
		}

		// Success
		fclose( $target );

		$target = fopen( $targetPath, 'rb' );
		fseek( $temp, 0, SEEK_SET );
		$contents = '';

		load_funcs( 'tools/model/_system.funcs.php' );
		$memory_limit = system_check_memory_limit();

		while( ! feof( $target ) )
		{
			if( $memory_limit != -1 && ( $memory_limit - memory_get_usage( true ) ) < 8192 )
			{ // Don't try to load the next portion of image into the memory because it would cause 'Allowed memory size exhausted' error
				fclose( $temp );
				return array( 'error' => T_('Out of memory.') );
			}
			$contents .= fread( $target, 8192 );
		}

		fclose( $target );

		// Delete chunks
		for( $i = 0; $i < $totalParts; $i++ )
		{
			unlink( $targetFolder.DIRECTORY_SEPARATOR.$i );
		}
		rmdir( $targetFolder );
		if ( ! is_null( $this->sizeLimit ) && filesize( $targetPath ) > $this->sizeLimit )
		{
			unlink( $targetPath );
			http_response_code( 413 );
			return array( "success" => false, "uuid" => $uuid, "preventRetry" => true );
		}

		// Delete combined chunks
		unlink( $target );

		return array( "success" => true, "contents" => $contents, "uuid" => $uuid );
	}

	/**
		* Process the upload.
		* @param string $name Overwrites the name of the file.
		*/
	public function handleUpload( $name = null )
	{
		if( is_writable( $this->chunksFolder ) && 1 == mt_rand( 1, 1 / $this->chunksCleanupProbability ) )
		{
			// Run garbage collection
			$this->cleanupChunks();
		}

		// Check that the max upload size specified in class configuration does not
		// exceed size allowed by server config
		if( $this->toBytes( ini_get( 'post_max_size' ) ) < $this->sizeLimit || $this->toBytes( ini_get( 'upload_max_filesize') ) < $this->sizeLimit )
		{
			$neededRequestSize = max( 1, $this->sizeLimit / 1024 / 1024 ) . 'M';
			return array( 'error' => sprintf( /* NO TRANS for sysadmins */ 'Server error. Increase post_max_size and upload_max_filesize to %s', $neededRequestSize ) );
		}

		$type = $_SERVER['CONTENT_TYPE'];
		if( isset( $_SERVER['HTTP_CONTENT_TYPE'] ) )
		{
			$type = $_SERVER['HTTP_CONTENT_TYPE'];
		}

		if( !isset( $type ) )
		{
			return array( 'error' => T_('No file was uploaded.') );
		}
		elseif ( strpos( strtolower( $type ), 'multipart/' ) !== 0 )
		{
			return array( 'error' => /* NO TRANS for sysadmins */ 'Server error. Not a multipart request. Please set forceMultipart to default value (true).' );
		}

		// Check for wrong uploading file:
		if( ! isset( $_FILES[$this->inputName]['tmp_name'] ) ||
		    ! is_uploaded_file( $_FILES[$this->inputName]['tmp_name'] ) )
		{	// Deny uploading of wrong file:
			return array( 'error' => 'Invalid '.$this->inputName.' file!' );
		}

		// Get size and name
		$file = $_FILES[$this->inputName];
		$size = $file['size'];
		if( isset( $_REQUEST['qqtotalfilesize'] ) )
		{
			$size = $_REQUEST['qqtotalfilesize'];
		}

		if ( $name === null )
		{
			$name = $this->getName();
		}

		// check file error
		if( $file['error'] )
		{
			return array( 'error' => sprintf( T_('Upload Error #%s'), $file['error'] ) );
		}

		// Validate name
		if($name === null || $name === '')
		{
			return array( 'error' => T_('File name empty.') );
		}
		// Validate file size
		if( $size == 0 )
		{
			return array( 'error' => T_('File is empty.') );
		}
		if ( ! is_null( $this->sizeLimit ) && $size > $this->sizeLimit )
		{
			return array( 'error' => T_('File is too large.'), 'preventRetry' => true );
		}

		// Validate file extension
		$pathinfo = pathinfo( $name );
		$ext = isset( $pathinfo['extension'] ) ? $pathinfo['extension'] : '';
		if( $this->allowedExtensions && ! in_array( strtolower( $ext ), array_map( "strtolower", $this->allowedExtensions ) ) )
		{
			$these = implode( ', ', $this->allowedExtensions );
			return array( 'error' => sprintf( T_('File has an invalid extension, it should be one of %s.'), $these ) );
		}

		if( empty( $_REQUEST['qquuid'] ) )
		{	// Param qquuid must be passed:
			return array( 'error' => 'Parameter "qquuid" is required!' );
		}

		// Save a chunk
		$totalParts = isset( $_REQUEST['qqtotalparts'] ) ? ( int ) $_REQUEST['qqtotalparts'] : 1;
		$uuid = $_REQUEST['qquuid'];
		if( $totalParts > 1 )
		{ # chunked upload
			$chunksFolder = $this->chunksFolder;
			$partIndex = ( int ) $_REQUEST['qqpartindex'];
			if( ! is_writable( $chunksFolder ) )
			{
				return array( 'error' => /* NO TRANS for sysadmins */ 'Server error. Chunks directory isn\'t writable or executable.' );
			}
			$targetFolder = $this->chunksFolder.DIRECTORY_SEPARATOR.$uuid;
			if( ! file_exists( $targetFolder ) )
			{
				mkdir( $targetFolder, 0777, true );
			}
			$target = $targetFolder.'/'.$partIndex;
			$success = move_uploaded_file( $file['tmp_name'], $target );

			return array( "success" => true, "uuid" => $uuid );
		}
		else
		{ # non-chunked upload
			$temp = fopen( $file['tmp_name'], "rb" );
			fseek( $temp, 0, SEEK_SET );
			$contents = '';

			load_funcs( 'tools/model/_system.funcs.php' );
			$memory_limit = system_check_memory_limit();

			while( ! feof( $temp ) )
			{
				if( $memory_limit != -1 && ( $memory_limit - memory_get_usage( true ) ) < 8192 )
				{ // Don't try to load the next portion of image into the memory because it would cause 'Allowed memory size exhausted' error
					fclose( $temp );
					return array( 'error' => T_('Out of memory.') );
				}
				$contents .= fread( $temp, 8192 );
			}

			fclose( $temp );

			return array( 'success' => true, 'contents' => $contents, 'uuid' => $uuid );
		}
	}

	/**
		* Process a delete.
		* @param string $uploadDirectory Target directory.
		* @params string $name Overwrites the name of the file.
		*
		*/
	public function handleDelete( $uploadDirectory, $name = null )
	{
		if( $this->isInaccessible( $uploadDirectory ) )
		{
			return array( 'error' => /* NO TRANS for sysadmins */ 'Server error. Uploads directory isn\'t writable'. ( ( ! $this->isWindows() ) ? ' or executable.' : '.' ) );
		}
		$targetFolder = $uploadDirectory;
		$url = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
		$tokens = explode( '/', $url );
		$uuid = $tokens[sizeof( $tokens ) - 1];
		$target = join( DIRECTORY_SEPARATOR, array( $targetFolder, $uuid ) );

		if( is_dir( $target ) )
		{
			$this->removeDir( $target );
			return array( 'success' => true, 'uuid' => $uuid );
		}
		else
		{
			return array(
				'success' => false,
				'error' => T_('File not found! Unable to delete. ').$url,
				'path' => $uuid
			);
		}
	}

	/**
		* Returns a path to use with this upload. Check that the name does not exist,
		* and appends a suffix otherwise.
		* @param string $uploadDirectory Target directory
		* @param string $filename The name of the file to use.
		*/
	protected function getUniqueTargetPath($uploadDirectory, $filename)
	{
		// Allow only one process at the time to get a unique file name, otherwise
		// if multiple people would upload a file with the same name at the same time
		// only the latest would be saved.
		if( function_exists( 'sem_acquire' ) )
		{
			$lock = sem_get( ftok( __FILE__, 'u' ) );
			sem_acquire( $lock );
		}
		$pathinfo = pathinfo( $filename );
		$base = $pathinfo['filename'];
		$ext = isset( $pathinfo['extension'] ) ? $pathinfo['extension'] : '';
		$ext = $ext == '' ? $ext : '.' . $ext;
		$unique = $base;
		$suffix = 0;

		// Get unique file name for the file, by appending random suffix.
		while( file_exists( $uploadDirectory . DIRECTORY_SEPARATOR . $unique . $ext ) )
		{
			$suffix += rand( 1, 999 );
			$unique = $base.'-'.$suffix;
		}
		$result =  $uploadDirectory . DIRECTORY_SEPARATOR . $unique . $ext;

		// Create an empty target file
		if( ! touch( $result ) )
		{
			// Failed
			$result = false;
		}
		if( function_exists( 'sem_acquire' ) )
		{
			sem_release( $lock );
		}

		return $result;
	}

	/**
		* Deletes all file parts in the chunks folder for files uploaded
		* more than chunksExpireIn seconds ago
		*/
	protected function cleanupChunks()
	{
		foreach( scandir( $this->chunksFolder ) as $item )
		{
			if( $item == "." || $item == ".." )
			{
				continue;
			}
			$path = $this->chunksFolder.DIRECTORY_SEPARATOR.$item;
			if( ! is_dir( $path ) )
			{
				continue;
			}
			if( time() - filemtime( $path ) > $this->chunksExpireIn )
			{
				$this->removeDir( $path );
			}
		}
	}

	/**
		* Removes a directory and all files contained inside
		* @param string $dir
		*/
	protected function removeDir( $dir )
	{
		foreach( scandir( $dir ) as $item )
		{
			if( $item == "." || $item == ".." )
			{
				continue;
			}

			if( is_dir( $item ) )
			{
				$this->removeDir( $item );
			}
			else
			{
				unlink( join( DIRECTORY_SEPARATOR, array( $dir, $item ) ) );
			}
		}
		rmdir( $dir );
	}

	/**
		* Converts a given size with units to bytes.
		* @param string $str
		*/
	protected function toBytes( $str )
	{
		$str = trim( $str );
		$val = intval( $str );
		$last = strtolower( $str[strlen( $str ) - 1] );
		switch( $last )
		{
			case 'g': $val *= 1024;
			case 'm': $val *= 1024;
			case 'k': $val *= 1024;
		}
		return $val;
	}

	/**
		* Determines whether a directory can be accessed.
		*
		* is_executable() is not reliable on Windows prior PHP 5.0.0
		*  (http://www.php.net/manual/en/function.is-executable.php)
		* The following tests if the current OS is Windows and if so, merely
		* checks if the folder is writable;
		* otherwise, it checks additionally for executable status (like before).
		*
		* @param string $directory The target directory to test access
		*/
	protected function isInaccessible( $directory )
	{
		$isWin = $this->isWindows();
		$folderInaccessible = ( $isWin ) ? ! is_writable( $directory ) : ( ! is_writable( $directory ) && ! is_executable( $directory ) );
		return $folderInaccessible;
	}

	/**
		* Determines is the OS is Windows or not
		*
		* @return boolean
		*/
	protected function isWindows()
	{
		$isWin = (strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN' );
		return $isWin;
	}
}
?>