mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
f86c05ba08
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@105540 467b73ca-7a2a-4603-9d3b-597d59a354a9
332 lines
8.5 KiB
PHP
332 lines
8.5 KiB
PHP
<?php
|
|
/**
|
|
* Manages uploads via HTML forms processed by PHP,
|
|
* uploads to Silverstripe's default upload directory,
|
|
* and either creates a new or uses an existing File-object
|
|
* for syncing with the database.
|
|
*
|
|
* @package sapphire
|
|
* @subpackage filesystem
|
|
*
|
|
* @todo Allow for non-database uploads
|
|
*/
|
|
class Upload extends Controller {
|
|
|
|
/**
|
|
* A File object
|
|
*
|
|
* @var File
|
|
*/
|
|
protected $file;
|
|
|
|
/**
|
|
* Information about the temporary file produced
|
|
* by the PHP-runtime.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $tmpFile;
|
|
|
|
/**
|
|
* Restrict filesize for either all filetypes
|
|
* or a specific extension, with extension-name
|
|
* as array-key and the size-restriction in bytes as array-value.
|
|
*
|
|
* @var array
|
|
*/
|
|
public $allowedMaxFileSize = array();
|
|
|
|
/**
|
|
* @var array Collection of extensions.
|
|
* Extension-names are treated case-insensitive.
|
|
*
|
|
* Example:
|
|
* <code>
|
|
* array("jpg","GIF")
|
|
* </code>
|
|
*/
|
|
public $allowedExtensions = array();
|
|
|
|
/**
|
|
* Processing errors that can be evaluated,
|
|
* e.g. by Form-validation.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $errors = array();
|
|
|
|
/**
|
|
* A foldername relative to /assets,
|
|
* where all uploaded files are stored by default.
|
|
*
|
|
* @var string
|
|
*/
|
|
public static $uploads_folder = "Uploads";
|
|
|
|
/**
|
|
* Save an file passed from a form post into this object.
|
|
*
|
|
* @param $tmpFile array Indexed array that PHP generated for every file it uploads.
|
|
* @param $folderPath string Folder path relative to /assets
|
|
* @return Boolean|string Either success or error-message.
|
|
*/
|
|
function load($tmpFile, $folderPath = false) {
|
|
$this->clearErrors();
|
|
|
|
if(!$folderPath) $folderPath = self::$uploads_folder;
|
|
|
|
if(!$this->file) $this->file = new File();
|
|
|
|
if(!is_array($tmpFile)) {
|
|
user_error("File::loadUploaded() Not passed an array. Most likely, the form hasn't got the right enctype", E_USER_ERROR);
|
|
}
|
|
|
|
if(!$tmpFile['size']) {
|
|
$this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.');
|
|
return false;
|
|
}
|
|
|
|
$valid = $this->validate($tmpFile);
|
|
if(!$valid) return false;
|
|
|
|
// @TODO This puts a HUGE limitation on files especially when lots
|
|
// have been uploaded.
|
|
$base = Director::baseFolder();
|
|
$parentFolder = Folder::findOrMake($folderPath);
|
|
|
|
// Create a folder for uploading.
|
|
if(!file_exists(ASSETS_PATH)){
|
|
mkdir(ASSETS_PATH, Filesystem::$folder_create_mask);
|
|
}
|
|
if(!file_exists(ASSETS_PATH . "/" . $folderPath)){
|
|
mkdir(ASSETS_PATH . "/" . $folderPath, Filesystem::$folder_create_mask);
|
|
}
|
|
|
|
// Generate default filename
|
|
$fileName = str_replace(' ', '-',$tmpFile['name']);
|
|
$fileName = ereg_replace('[^A-Za-z0-9+.-]+','',$fileName);
|
|
$fileName = ereg_replace('-+', '-',$fileName);
|
|
$fileName = basename($fileName);
|
|
|
|
$relativeFilePath = ASSETS_DIR . "/" . $folderPath . "/$fileName";
|
|
|
|
// if filename already exists, version the filename (e.g. test.gif to test1.gif)
|
|
while(file_exists("$base/$relativeFilePath")) {
|
|
$i = isset($i) ? ($i+1) : 2;
|
|
$oldFilePath = $relativeFilePath;
|
|
// make sure archives retain valid extensions
|
|
if(substr($relativeFilePath, strlen($relativeFilePath) - strlen('.tar.gz')) == '.tar.gz' ||
|
|
substr($relativeFilePath, strlen($relativeFilePath) - strlen('.tar.bz2')) == '.tar.bz2') {
|
|
$relativeFilePath = ereg_replace('[0-9]*(\.tar\.[^.]+$)',$i . '\\1', $relativeFilePath);
|
|
} else {
|
|
$relativeFilePath = ereg_replace('[0-9]*(\.[^.]+$)',$i . '\\1', $relativeFilePath);
|
|
}
|
|
if($oldFilePath == $relativeFilePath && $i > 2) user_error("Couldn't fix $relativeFilePath with $i tries", E_USER_ERROR);
|
|
}
|
|
|
|
if(file_exists($tmpFile['tmp_name']) && copy($tmpFile['tmp_name'], "$base/$relativeFilePath")) {
|
|
$this->file->ParentID = $parentFolder->ID;
|
|
// This is to prevent it from trying to rename the file
|
|
$this->file->Name = basename($relativeFilePath);
|
|
$this->file->write();
|
|
return true;
|
|
} else {
|
|
$this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load temporary PHP-upload into File-object.
|
|
*
|
|
* @param array $tmpFile
|
|
* @param File $file
|
|
* @return Boolean
|
|
*/
|
|
public function loadIntoFile($tmpFile, $file, $folderPath = false) {
|
|
$this->file = $file;
|
|
|
|
return $this->load($tmpFile, $folderPath);
|
|
}
|
|
|
|
/**
|
|
* Container for all validation on the file
|
|
* (e.g. size and extension restrictions).
|
|
* Is NOT connected to the {Validator} classes,
|
|
* please have a look at {FileField->validate()}
|
|
* for an example implementation of external validation.
|
|
*
|
|
* @param array $tmpFile
|
|
* @return boolean
|
|
*/
|
|
public function validate($tmpFile) {
|
|
// we don't validate for empty upload fields yet
|
|
if(!isset($tmpFile['name']) || empty($tmpFile['name'])) return true;
|
|
|
|
if(isset($tmpFile['tmp_name']) && !is_uploaded_file($tmpFile['tmp_name']) && !SapphireTest::is_running_test()) {
|
|
$this->errors[] = _t('File.NOVALIDUPLOAD', 'File is not a valid upload');
|
|
return false;
|
|
}
|
|
|
|
$pathInfo = pathinfo($tmpFile['name']);
|
|
// filesize validation
|
|
if(!$this->isValidSize($tmpFile)) {
|
|
$this->errors[] = sprintf(
|
|
_t(
|
|
'File.TOOLARGE',
|
|
'Filesize is too large, maximum %s allowed.',
|
|
PR_MEDIUM,
|
|
'Argument 1: Filesize (e.g. 1MB)'
|
|
),
|
|
File::format_size($this->getAllowedMaxFileSize($pathInfo['extension']))
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// extension validation
|
|
if(!$this->isValidExtension($tmpFile)) {
|
|
$this->errors[] = sprintf(
|
|
_t(
|
|
'File.INVALIDEXTENSION',
|
|
'Extension is not allowed (valid: %s)',
|
|
PR_MEDIUM,
|
|
'Argument 1: Comma-separated list of valid extensions'
|
|
),
|
|
implode(',',$this->allowedExtensions)
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get file-object, either generated from {load()},
|
|
* or manually set.
|
|
*
|
|
* @return File
|
|
*/
|
|
public function getFile() {
|
|
return $this->file;
|
|
}
|
|
|
|
/**
|
|
* Set a file-object (similiar to {loadIntoFile()})
|
|
*
|
|
* @param File $file
|
|
*/
|
|
public function setFile($file) {
|
|
$this->file = $file;
|
|
}
|
|
|
|
/**
|
|
* Get maximum file size for all or specified file extension.
|
|
*
|
|
* @param string $ext
|
|
* @return int Filesize in bytes
|
|
*/
|
|
public function getAllowedMaxFileSize($ext = null) {
|
|
$ext = strtolower($ext);
|
|
if(isset($ext) && isset($this->allowedMaxFileSize[$ext])) {
|
|
return $this->allowedMaxFileSize[$ext];
|
|
} else {
|
|
return (isset($this->allowedMaxFileSize['*'])) ? $this->allowedMaxFileSize['*'] : false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set filesize maximums (in bytes).
|
|
* Automatically converts extensions to lowercase
|
|
* for easier matching.
|
|
*
|
|
* Example:
|
|
* <code>
|
|
* array('*' => 200, 'jpg' => 1000)
|
|
* </code>
|
|
*
|
|
* @param array|int $rules
|
|
*/
|
|
public function setAllowedMaxFileSize($rules) {
|
|
if(is_array($rules) && count($rules)) {
|
|
// make sure all extensions are lowercase
|
|
$rules = array_change_key_case($rules, CASE_LOWER);
|
|
$this->allowedMaxFileSize = $rules;
|
|
} elseif((int)$rules > 0) {
|
|
$this->allowedMaxFileSize['*'] = (int)$rules;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function getAllowedExtensions() {
|
|
return $this->allowedExtensions;
|
|
}
|
|
|
|
/**
|
|
* @param array $rules
|
|
*/
|
|
public function setAllowedExtensions($rules) {
|
|
if(!is_array($rules)) return false;
|
|
|
|
// make sure all rules are lowercase
|
|
foreach($rules as &$rule) $rule = strtolower($rule);
|
|
|
|
$this->allowedExtensions = $rules;
|
|
}
|
|
|
|
/**
|
|
* Determines if the bytesize of an uploaded
|
|
* file is valid - can be defined on an
|
|
* extension-by-extension basis in {$allowedMaxFileSize}
|
|
*
|
|
* @param array $tmpFile
|
|
* @return boolean
|
|
*/
|
|
public function isValidSize($tmpFile) {
|
|
$pathInfo = pathinfo($tmpFile['name']);
|
|
$extension = isset($pathInfo['extension']) ? strtolower($pathInfo['extension']) : null;
|
|
$maxSize = $this->getAllowedMaxFileSize($extension);
|
|
return (!$tmpFile['size'] || !$maxSize || (int)$tmpFile['size'] < $maxSize);
|
|
}
|
|
|
|
/**
|
|
* Determines if the temporary file has a valid extension
|
|
*
|
|
* @param array $tmpFile
|
|
* @return boolean
|
|
*/
|
|
public function isValidExtension($tmpFile) {
|
|
$pathInfo = pathinfo($tmpFile['name']);
|
|
return (!count($this->allowedExtensions) || in_array(strtolower($pathInfo['extension']), $this->allowedExtensions));
|
|
}
|
|
|
|
|
|
/**
|
|
* Clear out all errors (mostly set by {loadUploaded()})
|
|
*/
|
|
public function clearErrors() {
|
|
$this->errors = array();
|
|
}
|
|
|
|
/**
|
|
* Determines wether previous operations caused an error.
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function isError() {
|
|
return (count($this->errors));
|
|
}
|
|
|
|
/**
|
|
* Return all errors that occurred while processing so far
|
|
* (mostly set by {loadUploaded()})
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getErrors() {
|
|
return $this->errors;
|
|
}
|
|
|
|
}
|
|
?>
|