<?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;
}
}
?>