Implement form validation for file uploads

This commit is contained in:
Eike Foken
2011-08-30 20:14:20 +02:00
parent 121b47718a
commit dbd95d7b55
3 changed files with 550 additions and 104 deletions

View File

@@ -33,6 +33,7 @@ class Projects extends CI_Controller {
*/ */
public function __construct() { public function __construct() {
parent::__construct(); parent::__construct();
$this->load->library('form_validation');
$this->load->model('project'); $this->load->model('project');
$this->load->model('trial'); $this->load->model('trial');
} }
@@ -51,58 +52,8 @@ class Projects extends CI_Controller {
* Allows users to create a new project. * Allows users to create a new project.
*/ */
public function create() { public function create() {
$this->load->library('upload');
$this->load->library('form_validation');
$this->form_validation->set_error_delimiters('<span class="error">', '</span>');
$config = array(
array(
'field' => 'name',
'label' => 'Projektname',
'rules' => 'trim|required|min_length[3]|max_length[100]|xss_clean',
),
array(
'field' => 'description',
'label' => 'Beschreibung',
'rules' => 'trim|required|xss_clean',
),
array(
'field' => 'defaultmodel',
'label' => '3D-Modell',
),
array(
'field' => 'defaultconfig',
'label' => 'Standard-Konfiguration',
),
);
$this->form_validation->set_rules($config);
if (count($_POST) > 0) {
$config = array();
$config['upload_path'] = '/tmp';
$config['allowed_types'] = 'txt';
$config['max_size'] = '1024';
$config['file_name'] = 'defaultmodel';
$this->upload->initialize($config);
$modelUploaded = $this->upload->do_upload('defaultmodel');
$modelData = $this->upload->data();
$config['file_name'] = 'defaultconfig';
$this->upload->initialize($config);
$configUploaded = $this->upload->do_upload('defaultconfig');
$configData = $this->upload->data();
}
// run form validation // run form validation
if ($this->form_validation->run() == false) { if ($this->form_validation->run() === true) {
$data['model']['success'] = isset($modelUploaded) && $modelUploaded ? true : false;
$data['config']['success'] = isset($configUploaded) && $configUploaded ? true: false;
$this->load->view('projects/new', $data);
} else {
$data = array( $data = array(
'name' => $this->input->post('name'), 'name' => $this->input->post('name'),
'description' => $this->input->post('description'), 'description' => $this->input->post('description'),
@@ -117,11 +68,27 @@ class Projects extends CI_Controller {
$projectPath = FCPATH . 'uploads/' . $data['project_id'] . '/'; $projectPath = FCPATH . 'uploads/' . $data['project_id'] . '/';
mkdirs($projectPath); mkdirs($projectPath);
if ($modelUploaded) { $config = array(
copy($modelData['full_path'], $projectpath . $modelData['file_name']); 'upload_path' => $projectPath,
'allowed_types' => '*',
);
$this->load->library('upload', $config);
if (!empty($_FILES['defaultmodel']['tmp_name'])) {
$config['file_name'] = 'defaultmodel';
$this->upload->initialize($config);
if (!$this->upload->do_upload('defaultmodel')) {
$this->messages->add(_('The default model could not be uploaded.'), 'error');
}
}
if (!empty($_FILES['defaultconfig']['tmp_name'])) {
$config['file_name'] = 'defaultconfig';
$this->upload->initialize($config);
if (!$this->upload->do_upload('defaultconfig')) {
$this->messages->add(_('The default config could not be uploaded.'), 'error');
} }
if ($configUploaded) {
copy($configData['full_path'], $projectpath . $configData['file_name']);
} }
$this->messages->add($projectpath, 'notice'); $this->messages->add($projectpath, 'notice');
@@ -131,6 +98,10 @@ class Projects extends CI_Controller {
$this->load->view('projects/new'); $this->load->view('projects/new');
} }
} }
$data = array();
$this->load->view('projects/new', $data);
} }
/** /**

View File

@@ -3,12 +3,24 @@
/** /**
* Extends CI's form validation class. * Extends CI's form validation class.
* *
* Supported rules are:
* unique
* file_required
* file_allowed_type[type]
* file_disallowed_type[type]
* file_size_min[size]
* file_size_max[size]
* file_image_mindim[x,y]
* file_image_maxdim[x,y]
*
* @author Eike Foken <kontakt@eikefoken.de> * @author Eike Foken <kontakt@eikefoken.de>
*/ */
class MY_Form_validation extends CI_Form_validation { class MY_Form_validation extends CI_Form_validation {
/** /**
* Calls the parent constructor. * Calls the parent constructor.
*
* @param array $rules
*/ */
public function __construct($rules = array()) { public function __construct($rules = array()) {
parent::__construct($rules); parent::__construct($rules);
@@ -17,16 +29,197 @@ class MY_Form_validation extends CI_Form_validation {
$this->set_error_delimiters('<p class="error">', '</p>'); $this->set_error_delimiters('<p class="error">', '</p>');
} }
/**
* Sets rules.
*
* @see CI_Form_validation::set_rules()
*/
public function set_rules($field, $label = '', $rules = '') {
// this will prevent the form_validation from working
if (count($_POST) === 0 && count($_FILES) > 0) {
// add a dummy $_POST
$_POST['DUMMY_ITEM'] = '';
parent::set_rules($field, $label, $rules);
unset($_POST['DUMMY_ITEM']);
} else {
// we are safe just run as is
parent::set_rules($field, $label, $rules);
}
}
/**
* Runs the validator.
*
* @see CI_Form_validation::run()
*/
public function run($group = '') {
$rc = false;
log_message('debug', "Called MY_Form_validation::run()");
// does it have a file only form?
if (count($_POST) === 0 && count($_FILES) > 0) {
// add a dummy $_POST
$_POST['DUMMY_ITEM'] = '';
$rc = parent::run($group);
unset($_POST['DUMMY_ITEM']);
} else {
// we are safe just run as is
$rc = parent::run($group);
}
return $rc;
}
/**
* Executes the validation routines.
*
* @see CI_Form_validation::_execute()
*/
function _execute($row, $rules, $postdata = null, $cycles = 0) {
log_message('debug', "Called MY_Form_validation::_execute() " . $row['field']);
if (isset($_FILES[$row['field']])) {
// it is a file so process as a file
log_message('debug', "Processing as a file");
$postdata = $_FILES[$row['field']];
// before doing anything check for errors
if ($postdata['error'] !== UPLOAD_ERR_OK && $postdata['error'] !== UPLOAD_ERR_NO_FILE) {
$this->_error_array[$row['field']] = $this->getUploadError($postdata['error']);
return false;
}
$_in_array = false;
// if the field is blank, but NOT required, no further tests are necessary
$callback = false;
if (!in_array('file_required', $rules) && $postdata['error'] === UPLOAD_ERR_NO_FILE) {
// before we bail out, does the rule contain a callback?
if (preg_match("/(callback_\w+)/", implode(' ', $rules), $match)) {
$callback = true;
$rules = array('1' => $match[1]);
} else {
return;
}
}
// partly copied from the original class
foreach ($rules as $rule) {
// is the rule a callback?
$callback = false;
if (substr($rule, 0, 9) == 'callback_') {
$rule = substr($rule, 9);
$callback = true;
}
// rules can contain a parameter: max_length[5]
$param = false;
if (preg_match("/(.*?)\[(.*?)\]/", $rule, $match)) {
$rule = $match[1];
$param = $match[2];
}
// call the function that corresponds to the rule
if ($callback === true) {
if (!method_exists($this->CI, $rule)) {
continue;
}
// run the function and grab the result
$result = $this->CI->$rule($postdata, $param);
// re-assign the result to the master data array
if ($_in_array == true) {
$this->_field_data[$row['field']]['postdata'][$cycles] = (is_bool($result)) ? $postdata : $result;
} else {
$this->_field_data[$row['field']]['postdata'] = (is_bool($result)) ? $postdata : $result;
}
// if the field isn't required and we just processed a callback we'll move on...
if (!in_array('file_required', $rules, true) && $result !== false) {
return;
}
} else {
if (!method_exists($this, $rule)) {
/*
* If our own wrapper function doesn't exist we see if a native
* PHP function does. Users can use any native PHP function call
* that has one param.
*/
if (function_exists($rule)) {
$result = $rule($postdata);
if ($_in_array == true) {
$this->_field_data[$row['field']]['postdata'][$cycles] = (is_bool($result)) ? $postdata : $result;
} else {
$this->_field_data[$row['field']]['postdata'] = (is_bool($result)) ? $postdata : $result;
}
}
continue;
}
$result = $this->$rule($postdata, $param);
if ($_in_array == true) {
$this->_field_data[$row['field']]['postdata'][$cycles] = (is_bool($result)) ? $postdata : $result;
} else {
$this->_field_data[$row['field']]['postdata'] = (is_bool($result)) ? $postdata : $result;
}
}
// TODO The following line needs testing!!! Not sure if it will work
// put back the tested values back into $_FILES
//$_FILES[$row['field']] = $this->_field_data[$row['field']]['postdata'];
// did the rule test negatively? If so, grab the error.
if ($result === false) {
if (!isset($this->_error_messages[$rule])) {
if (false === ($line = $this->CI->lang->line($rule))) {
$line = 'Unable to access an error message corresponding to your field name.';
}
} else {
$line = $this->_error_messages[$rule];
}
/*
* Is the parameter we are inserting into the error message the name
* of another field? If so we need to grab it's "field label".
*/
if (isset($this->_field_data[$param]) && isset($this->_field_data[$param]['label'])) {
$param = $this->_field_data[$param]['label'];
}
// build the error message
$message = sprintf($line, $this->_translate_fieldname($row['label']), $param);
// save the error message
$this->_field_data[$row['field']]['error'] = $message;
if (!isset($this->_error_array[$row['field']])) {
$this->_error_array[$row['field']] = $message;
}
return;
}
}
} else {
log_message('debug', "Called parent::_execute()");
parent::_execute($row, $rules, $postdata,$cycles);
}
}
/** /**
* Checks if a username or email is unique. * Checks if a username or email is unique.
* *
* @param string $value * @param string $value
* @param string $params * @param string $params
*/ */
function unique($value, $params) { public function unique($value, $params) {
$CI =& get_instance(); $CI =& get_instance();
$CI->form_validation->set_message('unique', 'The %s is already being used.'); $this->set_message('unique', _('The %s is already being used.'));
list($table, $field) = explode(".", $params, 2); list($table, $field) = explode(".", $params, 2);
@@ -39,6 +232,290 @@ class MY_Form_validation extends CI_Form_validation {
} }
} }
/**
* Returns the upload error as a human-readable string.
*
* @param integer $errorCode
* @return string
*/
private function getUploadError($errorCode) {
switch ($errorCode) {
case UPLOAD_ERR_INI_SIZE:
$error = $this->setError('upload_file_exceeds_limit');
break;
case UPLOAD_ERR_FORM_SIZE:
$error = $this->setError('upload_file_exceeds_form_limit');
break;
case UPLOAD_ERR_PARTIAL:
$error = $this->setError('upload_file_partial');
break;
case UPLOAD_ERR_NO_FILE:
$error = $this->setError('upload_no_file_selected');
break;
case UPLOAD_ERR_NO_TMP_DIR:
$error = $this->setError('upload_no_temp_directory');
break;
case UPLOAD_ERR_CANT_WRITE:
$error = $this->setError('upload_unable_to_write_file');
break;
case UPLOAD_ERR_EXTENSION:
$error = $this->setError('upload_stopped_by_extension');
break;
default:
$error = _('Unknown upload error');
}
return $error;
}
/**
* Returns the error message of choice.
*
* The function will use $msg if it cannot find the error in the language file.
*
* @param string $msg the error message
*/
private function setError($msg) {
$CI =& get_instance();
$CI->lang->load('upload');
$msg = ($CI->lang->line($msg) == false) ? $msg : $CI->lang->line($msg);
log_message('error', $msg);
return $msg;
}
/**
* Converts a given string in format of ###AA to the number of bits it is assigning.
*
* @see http://codeigniter.com/forums/viewthread/123816/P20
*
* @param string $sValue
* @return integer number of bits
*/
private function letToBit($sValue) {
// split value from name
if (!preg_match('/([0-9]+)([ptgmkb]{1,2}|)/ui', $sValue, $aMatches)) {
// invalid input
return false;
}
if (empty($aMatches[2])) {
// no name? Enter default value
$aMatches[2] = 'KB';
}
if (strlen($aMatches[2]) == 1) {
// shorted name? Full name
$aMatches[2] .= 'B';
}
$iBit = (substr($aMatches[2], -1) == 'B') ? 1024 : 1000;
// calculate bits
switch (strtoupper(substr($aMatches[2], 0, 1))) {
case 'P':
$aMatches[1] *= $iBit;
case 'T':
$aMatches[1] *= $iBit;
case 'G':
$aMatches[1] *= $iBit;
case 'M':
$aMatches[1] *= $iBit;
case 'K':
$aMatches[1] *= $iBit;
break;
}
// return the value in bits
return $aMatches[1];
}
/**
* Checks if a required file is uploaded.
*
* @param mixed $file
*/
public function file_required($file) {
if ($file['size'] === 0) {
$this->set_message('file_required', _('Uploading a file for %s is required.'));
return false;
}
return true;
}
/**
* Checks if a file is within expected file size limit.
*
* @param mixed $file
* @param mixed $maxSize
*/
public function file_size_max($file, $maxSize) {
if ($file['size'] > $this->letToBit($maxSize)) {
$this->set_message('file_size_max', sprintf(_('The selected file is too big. (Maximum allowed is %s)', $maxSize)));
return false;
}
return true;
}
/**
* Checks if a file is bigger than minimum size.
*
* @param mixed $file
* @param mixed $minSize
*/
public function file_size_min($file, $minSize) {
if ($file['size'] < $this->letToBit($minSize)) {
$this->set_message('file_size_min', sprintf(_('The selected file is too small. (Minimum allowed is %s)'), $minSize));
return false;
}
return true;
}
/**
* Checks the file extension for valid file types.
*
* @param mixed $file
* @param mixed $type
*/
public function file_allowed_type($file, $type) {
// is type of format a,b,c,d? -> convert to array
$exts = explode(',', $type);
// is $type array? run self recursively
if (count($exts) > 1) {
foreach ($exts as $v) {
$rc = $this->file_allowed_type($file,$v);
if ($rc === true) {
return true;
}
}
}
// is type a group type? image, application, code... -> load proper array
$extGroups = array();
$extGroups['image'] = array('jpg', 'jpeg', 'gif', 'png');
$extGroups['config'] = array('calc', 'par');
$extGroups['application'] = array('exe', 'dll', 'so', 'cgi');
$extGroups['php_code'] = array('php', 'php4', 'php5', 'inc', 'phtml');
$extGroups['compressed'] = array('zip', 'gzip', 'tar', 'gz');
if (array_key_exists($exts[0], $extGroups)) {
$exts = $extGroups[$exts[0]];
}
// get file ext
$file_ext = strrchr($file['name'], '.');
$file_ext = substr(strtolower($file_ext), 1);
if (!in_array($file_ext, $exts)) {
$this->set_message('file_allowed_type', sprintf(_('The selected files type should be %s.'), $type));
return false;
} else {
return true;
}
}
/**
*
* @param mixed $file
* @param mixed $type
* @return boolean
*/
public function file_disallowed_type($file, $type) {
$rc = $this->file_allowed_type($file, $type);
if (!$rc) {
$this->set_message('file_disallowed_type', sprintf(_('The selected file cannot be %s.'), $type));
}
return $rc;
}
/**
* Returns FALSE if image is bigger than the given dimensions.
*
* @param mixed $file
* @param array $dim
* @return boolean
*/
public function file_image_maxdim($file, $dim) {
log_message('debug', "MY_Form_validation::file_image_maxdim() " . $dim);
$dim = explode(',', $dim);
if (count($dim) !== 2) {
// bad size given
$this->set_message('file_image_maxdim', "%s has invalid rule expected similar to 150,300 .");
return false;
}
log_message('debug', 'MY_Form_validation::file_image_maxdim() ' . $dim[0] . ' ' . $dim[1]);
// get image size
$d = $this->getImageDimensions($file['tmp_name']);
log_message('debug', $d[0] . ' ' . $d[1]);
if (!$d) {
$this->set_message('file_image_maxdim', "%s dimensions was not detected.");
return false;
}
if ($d[0] < $dim[0] && $d[1] < $dim[1]) {
return true;
}
$this->set_message('file_image_maxdim', "%s image size is too big.");
return false;
}
/**
* Returns FALSE is the image is smaller than the given dimension.
*
* @param mixed $file
* @param array $dim
* @return boolean
*/
public function file_image_mindim($file, $dim) {
$dim = explode(',', $dim);
if (count($dim) !== 2) {
// bad size given
$this->set_message('file_image_mindim', "%s has invalid rule expected similar to 150,300 .");
return false;
}
// get image size
$d = $this->getImageDimensions($file['tmp_name']);
if (!$d) {
$this->set_message('file_image_mindim', "%s dimensions was not detected.");
return false;
}
log_message('debug', $d[0] . ' ' . $d[1]);
if ($d[0] > $dim[0] && $d[1] > $dim[1]) {
return true;
}
$this->set_message('file_image_mindim', "%s image size is too big.");
return false;
}
/**
* Attempts to determine the image dimension.
*
* @param mixed $filename The path to the image file
* @return mixed
*/
private function getImageDimensions($filename) {
log_message('debug', $filename);
if (function_exists('getimagesize')) {
$d = @getimagesize($filename);
return $d;
}
return false;
}
} }
/* End of file MY_Form_validation.php */ /* End of file MY_Form_validation.php */

View File

@@ -1,53 +1,51 @@
<?php $this->load->view('header'); ?> <?php $this->load->view('header');?>
<div id="content"> <div id="content">
<div class="title"> <div class="title">
<h2><?= _('Create a new project') ?></h2> <h2><?=_('Create a new project');?></h2>
</div> </div>
<div class="box"> <div class="box">
<form method="post" name="createproject" action="<?=site_url('projects/create');?>" enctype="multipart/form-data">
<h3><?=_('Required information');?></h3>
<form method="post" name="createproject" action="<?=site_url('projects/create')?>" enctype="multipart/form-data">
<h3><?= _('Required information') ?></h3>
<ul> <ul>
<li> <li>
<h4><?= _('Project name') ?> <span class="req">*</span></h4> <?=form_label(_('Project name'), 'name');?> <span class="req">*</span>
<div> <div>
<input type="text" name="name" class="short text" tabindex="1" value="<?=set_value('name') == '' ? $this->input->post('name') : set_value('name');?>"> <input type="text" name="name" id="name" class="short text" value="<?=set_value('name');?>" />
<?=form_error('name')?> <?=form_error('name');?>
</div> </div>
</li> </li>
<li> <li>
<h4><?= _('Description') ?></h4> <?=form_label(_('Description'), 'description');?> <span class="req">*</span>
<label class="note"><?= _('A description is useful if you want to share this project with co-workers.') ?></label>
<div> <div>
<textarea name="description" rows="6" cols="60" tabindex="2" class="textarea"><?=set_value('description') == '' ? $this->input->post('description') : set_value('description');?></textarea> <textarea name="description" id="description" rows="6" cols="60" class="textarea"><?=set_value('description');?></textarea>
<?=form_error('description')?> <?=form_error('description');?>
</div> </div>
<label class="note"><?= _('A description is useful if you want to share this project with co-workers.') ?></label>
</li> </li>
</ul> </ul>
<h3><?= _('Optional information') ?></h3> <h3><?=_('Optional information');?></h3>
<ul> <ul>
<li> <li>
<h4><?= _('3D model') ?></h4> <?=form_label(_('3D model'), 'defaultmodel');?>
<label class="note"><?= _('Upload a 3D model that is used as a default for new trials. <br/>This model can be changed for every trial.')?></label>
<div> <div>
<input type="file" class="file" name="defaultmodel" tabindex="3" value="<?=set_value('defaultmodel')?>"> <input type="file" name="defaultmodel" id="defaultmodel" class="file" />
<?=$model['success'] ? '' : $this->upload->display_errors('<span class="error">', '</span>');?> <?=form_error('defaultmodel');?>
</div> </div>
<label class="note"><?=_('Upload a 3D model that is used as a default for new trials.<br/>This model can be changed for every trial.');?></label>
</li> </li>
<li> <li>
<h4><?= _('Default configuration') ?> <span class="req">*</span></h4> <?=form_label(_('Default configuration'), 'defaultconfig');?>
<label class="note"><?= _('Upload a configuration that is used as a default for new trials. <br/>This configuration can be changed for every trial.')?></label>
<div> <div>
<input type="file" class="file" name="defaultconfig" tabindex="4" value="<?=set_value('defaultconfig')?>"> <input type="file" name="defaultconfig" id="defaultconfig" class="file" />
<?=$config['success'] ? '' : $this->upload->display_errors('<span class="error">', '</span>');?> <?=form_error('defaultconfig');?>
</div> </div>
<label class="note"><?=_('Upload a configuration that is used as a default for new trials.<br/>This configuration can be changed for every trial.');?></label>
</li> </li>
<li> <li>
<a href="#" onclick="document.forms.createproject.submit()" class="button"><?= _('Save') ?></a> <a href="#" onclick="document.forms.createproject.submit()" class="button"><?=_('Save');?></a>
</li> </li>
</ul> </ul>
</form> </form>
@@ -55,4 +53,4 @@
</div> </div>
<?php $this->load->view('footer'); ?> <?php $this->load->view('footer');?>