Skip to content

Instantly share code, notes, and snippets.

@romac
Created June 8, 2010 09:26
Show Gist options
  • Save romac/429809 to your computer and use it in GitHub Desktop.
Save romac/429809 to your computer and use it in GitHub Desktop.
RRFileUploader
<?php
/*
* Copyright (c) 2010 Romain Ruetschi <[email protected]>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Source file containing class RRFileUploader.
*
* @package RRFileUploader
* @license http://opensource.org/licenses/mit-license.php MIT License
* @author Romain Ruetschi <[email protected]>
* @version 0.1
* @see RRFileUploader
*/
/**
* Class RRFileUploader.
*
* @todo Description for class RRFileUploader.
*
* @package RRFileUploader
* @license http://opensource.org/licenses/mit-license.php MIT license
* @author Romain Ruetschi <[email protected]>
*/
class RRFileUploader
{
/**
* Valid Mime Types
*
* @var string
*/
protected $_validMimeTypes = array();
/**
* Valid Extensions
*
* @var string
*/
protected $_validExtensions = array();
/**
* The path to the directory where the files will be moved.
*
* @var string
*/
protected $_destinationPath = '';
/**
* Non Moved Files
*
* @var array
*/
protected $_nonMovedFiles = array();
/**
* Error Messages
*
* @var array
*/
protected $_errorMessages = array(
UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the "upload_max_filesize" directive in php.ini',
UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the "MAX_FILE_SIZE" directive that was specified in the HTML form',
UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload.'
);
/**
* Create a new RRFileUploader instance.
*
* @param string $destinationPath The path to the directory where the files will be moved.
* @param array $validMimeTypes The valid MIME types.
* @param array $validExtensions The valid extensions.
* @author Romain Ruetschi <[email protected]>
*/
public function __construct
(
$destinationPath,
array $validMimeTypes = array(),
array $validExtensions = array()
)
{
if( is_array( $destinationPath ) )
{
$this->setOptions( $destinationPath );
}
else
{
$this->setDestinationPath( $destinationPath );
if( $validMimeTypes )
{
$this->setValidMimeTypes( $validMimeTypes );
}
if( $validExtensions )
{
$this->setValidExtensions( $validExtensions );
}
}
}
/**
* Set all options at once.
* You must supply an array to this function in which
* each key represents a property, associated
* with the value to set to this property.
* Then the setter corresponding to this property will be called.
*
* @example
* <code>
* $uploader->setOptions( array(
* 'destinationPath' => __DIR__,
* 'validMimeTypes' => array( 'image/jpeg' )
* ) );
* </code>
*
* Will call: $uploader->setDestinationPath( __DIR__ );
* and: $uploader->setValidMimeTypes( array( 'image/jpeg' ) );
*
* @param array $options The options to set.
* @return RRFileUploader
* @author Romain Ruetschi <[email protected]>
*/
public function setOptions( array $options )
{
foreach( $options as $property => $value )
{
$methodName = 'set' . ucfirst( $property );
if( method_exists( $this, $methodName ) )
{
$this->$methodName( $value );
}
}
return $this;
}
/**
* Move all the uploaded files present in the superglobal $_FILES array.
*
* @see moveUploadedFile()
* @return array An array of all the uploaded files new filenames.
* @author Romain Ruetschi <[email protected]>
*/
public function moveAllUploadedFiles()
{
$normalizedFilesArray = $this->_normalizeFilesArray( $_FILES );
$uploadedFiles = $this->_nonMovedFiles = array();
foreach( $normalizedFilesArray as $file )
{
try
{
$uploadedFiles[] = $this->moveUploadedFile( $file );
}
catch( Exception $e )
{
$this->_nonMovedFiles[] = array(
'error' => $e,
'file' => $file
);
}
}
return $uploadedFiles;
}
/**
* Move an uploaded file to a directory.
*
* @see _normalizeFilesArray()
* @throws Exception If the file cannot be moved successfully or is not valid.
* @param array $file An entry of the normalized $_FILES array.
* @return string The new path to the moved file.
* @author Romain Ruetschi <[email protected]>
*/
function moveUploadedFile( array $file )
{
$errors = array();
if( $file[ 'error' ] !== UPLOAD_ERR_OK )
{
$this->_throwException(
$this->_getFileUploadErrorMessage( $file[ 'error' ] )
);
}
if( $file[ 'type' ] && !$this->_isValidMimeType( $file[ 'type' ] ) )
{
$this->_throwException(
'Invalid MIME type.'
);
}
if( !$this->_hasValidExtension( $file[ 'name' ] ) )
{
$this->_throwException(
'Invalid extension.'
);
}
$filePathName = $this->getDestinationPath() . $file[ 'name' ];
if( file_exists( $filePathName ) )
{
$filePathName = $this->_findFreeFileName( $file );
}
if( !move_uploaded_file( $file[ 'tmp_name' ], $filePathName ) )
{
$this->_throwException(
'Moving ' . $file[ 'tmp_name' ] .
' to ' . $filePathName . ' failed. ' .
'Please check if the webserver has write permission in "' .
$this->getDestinationPath() . '".'
);
}
return $filePathName;
}
/**
* If the destination file already exists this method try to find
* a new destination filename which is not used in the destination directory.
* It does so by appending _1, _2, _3, etc. right before the extension.
*
* @param string $file The normalized file array.
* @return string The new filename.
* @author Romain Ruetschi <[email protected]>
*/
protected function _findFreeFileName( array $file )
{
$folder = $this->getDestinationPath();
$fileName = pathinfo( $file[ 'name' ], PATHINFO_FILENAME );
$fileExt = pathinfo( $file[ 'name' ], PATHINFO_EXTENSION );
$filePathName = $folder . $fileName . '.' . $fileExt;
$i = 0;
while( file_exists( $filePathName ) )
{
$filePathName = $folder . $fileName . '_' . ++$i . '.' . $fileExt;
}
return $filePathName;
}
/**
* Return an array of the valid MIME types.
*
* @return array
* @author Romain Ruetschi <[email protected]>
*/
public function getValidMimeTypes()
{
return $this->_validMimeTypes;
}
/**
* Set the valid MIME types.
*
* @param array $validMimeTypes The valid MIME types.
* @return RRFileUploader
* @author Romain Ruetschi <[email protected]>
*/
public function setValidMimeTypes( array $validMimeTypes )
{
$this->_validMimeTypes = $validMimeTypes;
return $this;
}
/**
* Return an array of the valid file extensions.
*
* @return array
* @author Romain Ruetschi <[email protected]>
*/
public function getValidExtensions()
{
return $this->_validExtensions;
}
/**
* Set the valid file extensions.
*
* @param array $validExtensions THe valid file extensions.
* @return RRFileUploader
* @author Romain Ruetschi <[email protected]>
*/
public function setValidExtensions( array $validExtensions )
{
$this->_validExtensions = $validExtensions;
return $this;
}
/**
* Get the destination path.
*
* @return string The destination path.
* @author Romain Ruetschi <[email protected]>
*/
public function getDestinationPath()
{
return $this->_destinationPath;
}
/**
* Set the destination path.
*
* @see _normalizePath()
* @param string $destinationPath The destination path.
* @return RRFileUploader
* @author Romain Ruetschi <[email protected]>
*/
public function setDestinationPath( $destinationPath )
{
if(
!is_string( $destinationPath )
||
(
is_object( $destinationPath )
&&
method_exists( $destinationPath, '__toString' )
)
)
{
throw new InvalidArgumentException(
'$destinationPath must be either a string or an object ' .
'with a __toString method. ' .
ucfirst( gettype( $destinationPath ) ) . ' supplied.'
);
}
$this->_destinationPath = $this->_normalizePath( $destinationPath );
return $this;
}
/**
* Return an array of all the non-moved files.
* Each item in this array it a normalized $_FILES item.
*
* @see _normalizeFilesArray()
* @return array.
* @author Romain Ruetschi <[email protected]>
*/
public function getNonMovedFiles()
{
return $this->_nonMovedFiles;
}
/**
* Check if some files were not moved.
*
* @return boolean
* @author Romain Ruetschi <[email protected]>
*/
public function hasNonMovedFiles()
{
return count( $this->_nonMovedFiles > 0 );
}
/**
* Check if the supplied MIME type is a valid one.
*
* @param string $type The MIME type to check.
* @return boolean
* @author Romain Ruetschi <[email protected]>
*/
protected function _isValidMimeType( $type )
{
return in_array( $type, $this->_validMimeTypes, TRUE );
}
/**
* Check if the supplied file has a valid extension.
*
* @param string $name The extension to check
* @return boolean
* @author Romain Ruetschi <[email protected]>
*/
protected function _hasValidExtension( $name )
{
$extension = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) );
return in_array( $extension, $this->_validExtensions, TRUE );
}
/**
* Get the error message corresponding to an error code.
*
* @param string $errorCode The error code.
* @return string The corresponding error message.
* @author Romain Ruetschi <[email protected]>
*/
protected function _getFileUploadErrorMessage( $errorCode )
{
if( isset( $this->_errorMessages[ $errorCode ] ) )
{
return $this->_errorMessages[ $errorCode ];
}
return 'Unknow error.';
}
/**
* Normalize a path according to the OS PHP is running on.
* This method also append a directory separator at the end of the
* path.
*
* @param string $path The path to normalize.
* @return string The normalized path.
* @author Romain Ruetschi <[email protected]>
*/
protected function _normalizePath( $path )
{
$path = trim( $path );
$path = strtr( $path, '\\/', DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR );
$path = rtrim( $path, DIRECTORY_SEPARATOR )
. DIRECTORY_SEPARATOR;
return $path;
}
/**
* Normalize a $_FILES-like array.
* Instead of having a key at the first level for each property of
* the uploaded files, this function returns an array with an entry
* per uploaded file which correponds to an array with all the property
* in it.
*
* @param array $files A $_FILES-like array.
* @return array The normalized array.
* @author Romain Ruetschi <[email protected]>
*/
protected function _normalizeFilesArray( array $files )
{
if( !$files )
{
return array();
}
$normalized = array();
$properties = array_keys( $files );
foreach( $files[ 'error' ] as $key => $error )
{
if( $error === UPLOAD_ERR_NO_FILE )
{
continue;
}
$normalized[ $key ] = array();
foreach( $properties as $property )
{
$normalized[ $key ][ $property ] = $files[ $property ][ $key ];
}
}
return $normalized;
}
/**
* Throw a pre-formatted exception.
*
* @param string $reason The reason of the failure.
* @return void
* @author Romain Ruetschi <[email protected]>
* @throws Exception
*/
protected function _throwException( $reason )
{
throw new Exception(
'Unable to move the supplied file. Reason: ' . $reason
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment