serverside validation for textbox and button

This commit is contained in:
Ralf Becker 2011-08-18 21:56:37 +00:00
parent d095250d85
commit a488b67f99
5 changed files with 460 additions and 128 deletions

View File

@ -18,7 +18,7 @@
*
* @ToDo supported customized templates stored in DB, currently we only support xet files stored in filesystem
*/
class etemplate_new
class etemplate_new extends etemplate_widget_template
{
/**
* Are we running as sitemgr module or not
@ -27,22 +27,6 @@ class etemplate_new
*/
public $sitemgr=false;
/**
* Request object of the currecntly created request
*
* It's a static variable as etemplates can contain further etemplates (rendered by a different object)
*
* @var etemplate_request
*/
static protected $request;
/**
* JSON response object, if we run via a JSON request
*
* @var egw_json_response
*/
static protected $response;
/**
* constructor of etemplate class, reads an eTemplate if $name is given
*
@ -114,7 +98,7 @@ class etemplate_new
self::$request->ignore_validation = $ignore_validation;
self::$request->app_header = $GLOBALS['egw_info']['flags']['app_header'];
if (self::$request->output_mode == -1) self::$request->output_mode = 0;
self::$request->template = $this->rel_path;
self::$request->template = $this->as_array();
$data = array(
'etemplate_exec_id' => self::$request->id(),
@ -134,6 +118,8 @@ class etemplate_new
}
else // first call
{
// missing dependency, thought egw:uses jquery.jquery.tools does NOT work, maybe we should rename it to jquery-tools
egw_framework::validate_file('jquery','jquery.tools.min');
egw_framework::validate_file('.','etemplate2','etemplate');
egw_framework::includeCSS('/etemplate/js/test/test.css');
@ -165,26 +151,22 @@ class etemplate_new
self::$response = egw_json_response::get();
// todo: validate content
$content = self::validate($content);
if (!($template = self::instance(self::$request->template['name'], self::$request->template['template_set'],
self::$request->template['version'], self::$request->template['load_via'])))
{
throw new egw_exception_wrong_parameter('Can NOT read template '.array2string(self::$request->template));
}
$validated = array();
$template->validate($content, $validated);
if (self::validation_errors(self::$request->ignore_validation))
{
error_log(__METHOD__."(,".array2string($content).') validation_errors='.array2string(self::$validation_errors));
self::$response->generic('et2_validation_error', self::$validation_errors);
exit;
}
error_log(__METHOD__."(,".array2string($content).') validated='.array2string($validated));
// merge with preserve and call our callback
return ExecMethod(self::$request->method, self::complete_array_merge(self::$request->preserv, $content));
}
/**
* Validate the content
*
* ** Test stub! ** First field always fails.
* @todo Fix this for proper server side validation
*/
static public function validate($content) {
self::$response->generic('et2_validation_error', array(
// Validation errors are field_name: message
key($content)=> "First field fails serverside. '".current($content)."'")
);
return $content;
return ExecMethod(self::$request->method, self::complete_array_merge(self::$request->preserv, $validated));
}
/**
@ -194,11 +176,16 @@ class etemplate_new
*/
public $rel_path;
public $name;
public $template_set;
public $version;
public $laod_via;
/**
* Reads an eTemplate from filesystem or DB (not yet supported)
*
* @param string $name name of the eTemplate or array with the values for all keys
* @param string $template template-set, '' loads the prefered template of the user, 'default' loads the default one '' in the db
* @param string $template_set template-set, '' loads the prefered template of the user, 'default' loads the default one '' in the db
* @param string $lang language, '' loads the pref. lang of the user, 'default' loads the default one '' in the db
* @param int $group id of the (primary) group of the user or 0 for none, not used at the moment !!!
* @param string $version version of the eTemplate
@ -207,79 +194,27 @@ class etemplate_new
*
* @ToDo supported customized templates stored in DB
*/
public function read($name,$template='default',$lang='default',$group=0,$version='',$load_via='')
public function read($name,$template_set='default',$lang='default',$group=0,$version='',$load_via='')
{
list($app, $tpl_name) = explode('.', $name, 2);
$this->rel_path = self::relPath($this->name=$name, $this->template_set=$template_set,
$this->version=$version, $this->laod_via = $load_via);
$this->rel_path = '/'.$app.'/templates/'.$template.'/'.$tpl_name.'.xet';
if (!file_exists(EGW_SERVER_ROOT.$this->rel_path) && $template !== 'default')
{
$this->rel_path = '/'.$app.'/templates/default/'.$tpl_name.'.xet';
}
if (!file_exists(EGW_SERVER_ROOT.$this->rel_path))
{
$this->rel_path = null;
error_log(__METHOD__."('$name',...,'$load_via') returning FALSE");
return false;
}
//error_log(__METHOD__."('$name',...,'$load_via') this->rel_path=$this->rel_path returning TRUE");
return true;
return (boolean)$this->real_path;
}
/**
* Validation errors from process_show and the extensions, should be set via etemplate::set_validation_error
* Get template data as array
*
* @public array form_name => message pairs
* @return array
*/
static protected $validation_errors = array();
/**
* Sets a validation error, to be displayed in the next exec
*
* @param string $name (complete) name of the widget causing the error
* @param string $error error-message already translated
* @param string $cname=null set it to '', if the name is already a form-name, defaults to self::$name_vars
*/
public static function set_validation_error($name,$error,$cname=null)
public function as_array()
{
if (is_null($cname)) $cname = self::$name_vars;
//echo "<p>etemplate::set_validation_error('$name','$error','$cname');</p>\n";
if ($cname) $name = self::form_name($cname,$name);
if (self::$validation_errors[$name])
{
self::$validation_errors[$name] .= ', ';
}
self::$validation_errors[$name] .= $error;
}
/**
* Check if we have not ignored validation errors
*
* @param string $ignore_validation='' if not empty regular expression for validation-errors to ignore
* @param string $cname=null name-prefix, which need to be ignored, default self::$name_vars
* @return boolean true if there are not ignored validation errors, false otherwise
*/
public static function validation_errors($ignore_validation='',$cname='')
{
// if (is_null($cname)) $cname = self::$name_vars;
//echo "<p>uietemplate::validation_errors('$ignore_validation','$cname') validation_error="; _debug_array(self::$validation_errors);
if (!$ignore_validation) return count(self::$validation_errors) > 0;
foreach(self::$validation_errors as $name => $error)
{
if ($cname) $name = preg_replace('/^'.$cname.'\[([^\]]+)\](.*)$/','\\1\\2',$name);
// treat $ignoare_validation only as regular expression, if it starts with a slash
if ($ignore_validation[0] == '/' && !preg_match($ignore_validation,$name) ||
$ignore_validation[0] != '/' && $ignore_validation != $name)
{
//echo "<p>uietemplate::validation_errors('$ignore_validation','$cname') name='$name' ($error) not ignored!!!</p>\n";
return true;
}
//echo "<p>uietemplate::validation_errors('$ignore_validation','$cname') name='$name' ($error) ignored</p>\n";
}
return false;
return array(
'name' => $this->name,
'template_set' => $this->template_set,
'version' => $this->version,
'load_via' => $this->load_via,
);
}
/**

View File

@ -11,8 +11,13 @@
* @version $Id$
*/
// include only widgets which can't be autoloaded (or contain sub-widgets which cant)
require_once EGW_INCLUDE_ROOT.'/etemplate/inc/class.etemplate_widget_textbox.inc.php';
/**
* eTemplate widget baseclass
*
* @todo text content, eg. the styles of a template are not parsed, thought they are not used here either
*/
class etemplate_widget
{
@ -44,6 +49,22 @@ class etemplate_widget
*/
protected $children = array();
/**
* Request object of the currently created request
*
* It's a static variable as etemplates can contain further etemplates (rendered by a different object)
*
* @var etemplate_request
*/
static protected $request;
/**
* JSON response object, if we run via a JSON request
*
* @var egw_json_response
*/
static protected $response;
/**
* Constructor
*
@ -89,6 +110,30 @@ class etemplate_widget
}
}
/**
* Registry of classes implementing widgets
*
* @var array
*/
static protected $widget_registry = array();
/**
* Register a given class for certain widgets
*
* Registration is only needed if widget (base-)name is not autoloadable,
* eg. class etemplate_widget_template does NOT need to be registered.
*
* @param string $class
* @param string|array $widgets
*/
public static function registerWidget($class, $widgets)
{
foreach((array)$widgets as $widget)
{
self::$widget_registry[$widget] = $class;
}
}
/**
* Factory method to construct all widgets
*
@ -98,22 +143,20 @@ class etemplate_widget
*/
public static function factory($type, $xml, $id=null)
{
static $type2class_name = array();
$class_name =& $type2class_name[$type];
$class_name =& self::$widget_registry[$type];
if (!isset($class_name))
{
list($basetype) = explode('-',$type);
if (!class_exists($class_name = 'etemplate_'.str_replace('-','_',$type).'_widget') &&
!class_exists($class_name = 'etemplate_'.str_replace('-','_',$basetype).'_widget'))
if (!class_exists($class_name = 'etemplate_widget_'.str_replace('-','_',$type)) &&
!class_exists($class_name = 'etemplate_widget_'.str_replace('-','_',$basetype)))
{
// default to widget class, we can not ignore it, as the widget may contain other widgets
$class_name = 'etemplate_widget';
}
}
// currently only overlays can contain templates, other widgets can only reference to templates via id
if ($type == 'template' && $id && ($template = etemplate_template_widget::read($id)))
if ($type == 'template' && $id && ($template = etemplate_widget_template::instance($id)))
{
return $template;
}
@ -144,15 +187,23 @@ class etemplate_widget
}
/**
* Validate input of a widget
* Validate input
*
* Default implementation only calls validate on it's children
*
* @param array $content
* @param array &$validated=array() validated content
* @param string $cname='' current namespace
* @return mixed
* @return boolean true if no validation error, false otherwise
*/
public function validate(array $content, $cname = '')
public function validate(array $content, &$validated=array(), $cname = '')
{
$ok = true;
foreach($this->children as $child)
{
$ok = $child->validate($content, $validated, $cname) && $ok;
}
return $ok;
}
/**
@ -179,6 +230,8 @@ class etemplate_widget
{
echo ' '.$name.'="'.htmlspecialchars($value).'"';
}
echo ' php-class="'.get_class($this).'"';
if ($this->children)
{
echo ">\n";
@ -193,4 +246,163 @@ class etemplate_widget
echo " />\n";
}
}
/**
* build the name of a form-element from a basename and name
*
* name and basename can contain sub-indices in square bracets, eg. basename="base[basesub1][basesub2]"
* and name = "name[sub]" gives "base[basesub1][basesub2][name][sub]"
*
* @param string $cname basename
* @param string $name name
* @return string complete form-name
*/
static function form_name($cname,$name)
{
$name_parts = explode('[',str_replace(']','',$name));
if (!empty($cname))
{
array_unshift($name_parts,$cname);
}
$form_name = array_shift($name_parts);
if (count($name_parts))
{
$form_name .= '['.implode('][',$name_parts).']';
}
return $form_name;
}
/**
* return a reference to $arr[$idx]
*
* This works for non-trival indexes like 'a[b][c]' too: it returns &$arr[a][b][c]
* $sub = get_array($arr,'a[b]'); $sub = 'c'; is equivalent to $arr['a']['b'] = 'c';
*
* @param array $arr the array to search, referenz as a referenz gets returned
* @param string $idx the index, may contain sub-indices like a[b], see example below
* @param boolean $reference_into default False, if True none-existing sub-arrays/-indices get created to be returned as referenz, else False is returned
* @param bool $skip_empty returns false if $idx is not present in $arr
* @return mixed reference to $arr[$idx] or false if $idx is not set and not $reference_into
*/
static function &get_array(&$arr,$idx,$reference_into=False,$skip_empty=False)
{
if (!is_array($arr))
{
throw new egw_exception_assertion_failed(__METHOD__."(\$arr,'$idx',$reference_into,$skip_empty) \$arr is no array!");
}
if (is_object($idx)) return false; // given an error in php5.2
$idxs = explode('[',str_replace(']','',$idx));
$pos = &$arr;
foreach($idxs as $idx)
{
if (!is_array($pos) && !$reference_into)
{
return False;
}
if($skip_empty && (!is_array($pos) || !isset($pos[$idx]))) return false;
$pos = &$pos[$idx];
}
return $pos;
}
/**
* Checks if a widget is readonly:
* - readonly attribute set
* - $readonlys[__ALL__] set and $readonlys[$form_name] !== false
* - $readonlys[$form_name] evaluates to true
*
* @return boolean
*/
public function is_readonly($cname='')
{
$form_name = self::form_name($cname, $this->id);
$readonly = $this->attrs['readonly'] || self::$request->readonlys[$form_name] ||
isset(self::$request->readonlys['__ALL__']) && self::$request->readonlys[$form_name] !== false;
error_log(__METHOD__."('$cname') this->id='$this->id' --> form_name='$form_name' returning ".array2string($readonly));
return $readonly;
}
/**
* Validation errors from process_show and the extensions, should be set via etemplate::set_validation_error
*
* @public array form_name => message pairs
*/
static protected $validation_errors = array();
/**
* Sets a validation error, to be displayed in the next exec
*
* @param string $name (complete) name of the widget causing the error
* @param string $error error-message already translated
* @param string $cname=null set it to '', if the name is already a form-name, defaults to self::$name_vars
*/
public static function set_validation_error($name,$error,$cname=null)
{
if (is_null($cname)) $cname = self::$name_vars;
//echo "<p>etemplate::set_validation_error('$name','$error','$cname');</p>\n";
if ($cname) $name = self::form_name($cname,$name);
if (self::$validation_errors[$name])
{
self::$validation_errors[$name] .= ', ';
}
self::$validation_errors[$name] .= $error;
}
/**
* Check if we have not ignored validation errors
*
* @param string $ignore_validation='' if not empty regular expression for validation-errors to ignore
* @param string $cname=null name-prefix, which need to be ignored, default self::$name_vars
* @return boolean true if there are not ignored validation errors, false otherwise
*/
public static function validation_errors($ignore_validation='',$cname='')
{
// if (is_null($cname)) $cname = self::$name_vars;
//echo "<p>uietemplate::validation_errors('$ignore_validation','$cname') validation_error="; _debug_array(self::$validation_errors);
if (!$ignore_validation) return count(self::$validation_errors) > 0;
foreach(self::$validation_errors as $name => $error)
{
if ($cname) $name = preg_replace('/^'.$cname.'\[([^\]]+)\](.*)$/','\\1\\2',$name);
// treat $ignoare_validation only as regular expression, if it starts with a slash
if ($ignore_validation[0] == '/' && !preg_match($ignore_validation,$name) ||
$ignore_validation[0] != '/' && $ignore_validation != $name)
{
//echo "<p>uietemplate::validation_errors('$ignore_validation','$cname') name='$name' ($error) not ignored!!!</p>\n";
return true;
}
//echo "<p>uietemplate::validation_errors('$ignore_validation','$cname') name='$name' ($error) ignored</p>\n";
}
return false;
}
}
/**
* Named widget having an own namespace: grid, *box
*/
class etemplate_widget_named extends etemplate_widget
{
/**
* Validate input
*
* Reimplemented because grids can have an own namespace
*
* @param array $content
* @param array &$validated=array() validated content
* @param string $cname='' current namespace
* @return boolean true if no validation error, false otherwise
*/
public function validate(array $content, &$validated=array(), $cname = '')
{
if ($this->id) $cname = self::form_name($cname, $this->id);
return parent::validate($content, $validated, $cname);
}
}
// register class for layout widgets, which can have an own namespace
etemplate_widget::registerWidget('etemplate_widget_named', array('grid', 'box', 'hbox', 'vbox', 'groupbox'));

View File

@ -0,0 +1,50 @@
<?php
/**
* EGroupware - eTemplate serverside button widget
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link http://www.egroupware.org
* @author Ralf Becker <RalfBecker@outdoor-training.de>
* @copyright 2002-11 by RalfBecker@outdoor-training.de
* @version $Id$
*/
/**
* eTemplate button widget
*/
class etemplate_widget_button extends etemplate_widget
{
/**
* Validate input
*
* Readonly buttons can NOT be pressed
*
* @param array $content
* @param array &$validated=array() validated content
* @param string $cname='' current namespace
* @return boolean true if no validation error, false otherwise
*/
public function validate(array $content, &$validated=array(), $cname = '')
{
$form_name = self::form_name($cname, $this->id);
if (self::get_array($content, $form_name) && !$this->is_readonly($cname))
{
$valid =& self::get_array($validated, $form_name, true);
$valid = 'pressed'; // that's what it was in old etemplate
// recored pressed button globally, was in the template object before, not sure self::$request is the right place ...
if ($this->type == 'cancel' || $form_name == 'cancel' || substr($form_name,-10) == '[cancel]')
{
self::$request->canceled = true;
}
else
{
self::$request->button_pressed = true;
}
}
return parent::validate($content, $validated, $cname);
}
}

View File

@ -11,21 +11,21 @@
* @version $Id$
*/
/* testwise to have autoloading
// allow to call direct for tests (see end of class)
if (!isset($GLOBALS['egw_info']))
{
$GLOBALS['egw_info'] = array(
'flags' => array(
'currentapp' => 'login',
),
)
);
include_once '../../header.inc.php';
}
*/
/**
* eTemplate widget baseclass
*/
class etemplate_template_widget extends etemplate_widget
class etemplate_widget_template extends etemplate_widget
{
/**
* Cache of already read templates
@ -35,23 +35,23 @@ class etemplate_template_widget extends etemplate_widget
protected static $cache = array();
/**
* Read a templates specified by name, template(-set) and version
* Get instance of template specified by name, template(-set) and version
*
* @param string $name
* @param string $template_set='default'
* @param string $version=''
* @param string $load_via='' use given template to load $name
* @todo Reading customized templates from database
* @return etemplate_template_widget|boolean false if not found
* @return etemplate_widget_template|boolean false if not found
*/
public static function read($name, $template_set='default', $version='', $load_via='')
public static function instance($name, $template_set='default', $version='', $load_via='')
{
$start = microtime(true);
if (isset(self::$cache[$name]) || !($path = self::relPath($name, $template_set, $version)))
{
if ((!$path || self::read($load_via, $template_set)) && isset(self::$cache[$name]))
{
error_log(__METHOD__."('$name', '$template_set', '$version', '$load_via') read from cache");
//error_log(__METHOD__."('$name', '$template_set', '$version', '$load_via') read from cache");
return self::$cache[$name];
}
error_log(__METHOD__."('$name', '$template_set', '$version', '$load_via') template NOT found!");
@ -64,14 +64,14 @@ class etemplate_template_widget extends etemplate_widget
{
if ($reader->nodeType == XMLReader::ELEMENT && $reader->name == 'template')
{
$template = new etemplate_template_widget($reader);
$template = new etemplate_widget_template($reader);
//echo $template->id; _debug_array($template);
self::$cache[$template->id] = $template;
if ($template->id == $name)
{
error_log(__METHOD__."('$name', '$template_set', '$version', '$load_via') read in ".round(1000.0*(microtime(true)-$start),2)." ms");
//error_log(__METHOD__."('$name', '$template_set', '$version', '$load_via') read in ".round(1000.0*(microtime(true)-$start),2)." ms");
return $template;
}
}
@ -104,9 +104,28 @@ class etemplate_template_widget extends etemplate_widget
}
return false;
}
/**
* Validate input
*
* Reimplemented because templates can have an own namespace specified in options, NOT id!
*
* @param array $content
* @param array &$validated=array() validated content
* @param string $cname='' current namespace
* @return boolean true if no validation error, false otherwise
*/
public function validate(array $content, &$validated=array(), $cname = '')
{
if ($this->attrs['options']) $cname = self::form_name($cname, $this->attrs['options']);
return parent::validate($content, $validated, $cname);
}
}
/*
header('Content-Type: text/xml');
$template = etemplate_template_widget::read('timesheet.edit');
$template->toXml();
*/
if ($GLOBALS['egw_info']['flags']['currentapp'] == 'login')
{
$template = etemplate_widget_template::instance('timesheet.edit');
header('Content-Type: text/xml');
echo $template->toXml();
}

View File

@ -0,0 +1,116 @@
<?php
/**
* EGroupware - eTemplate serverside textbox widget
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link http://www.egroupware.org
* @author Ralf Becker <RalfBecker@outdoor-training.de>
* @copyright 2002-11 by RalfBecker@outdoor-training.de
* @version $Id$
*/
/**
* eTemplate textbox widget with following sub-types:
* - textbox optional multiline="true" and rows="123"
* - int
* - float
* - hidden
* - colorpicker
* sub-types are either passed to constructor or set via 'type' attribute!
*/
class etemplate_widget_textbox extends etemplate_widget
{
/**
* Validate input
*
* Following attributes get checked:
* - needed: value must NOT be empty
* - min, max: int and float widget only
* - maxlength: maximum length of string (longer strings get truncated to allowed size)
* - preg: perl regular expression incl. delimiters (set by default for int, float and colorpicker)
* - int and float get casted to their type
*
* @param array $content
* @param array &$validated=array() validated content
* @param string $cname='' current namespace
* @return boolean true if no validation error, false otherwise
*/
public function validate(array $content, &$validated=array(), $cname = '')
{
$ok = true;
$type = isset($this->attrs['type']) ? $this->attrs['type'] : $this->type;
if (!$this->is_readonly($cname))
{
if (!isset($this->attrs['preg']))
{
switch($type)
{
case 'int':
$this->attrs['preg'] = '/^-?[0-9]*$/';
break;
case 'float':
$this->attrs['preg'] = '/^-?[0-9]*[,.]?[0-9]*$/';
break;
case 'colorpicker':
$this->attrs['preg'] = '/^(#[0-9a-f]{6}|)$/i';
break;
}
}
$form_name = self::form_name($cname, $this->id);
$value = self::get_array($content, $form_name);
$valid =& self::get_array($validated, $form_name, true);
if ((string)$value === '' && $this->attrs['needed'])
{
self::set_validation_error($form_name,lang('Field must not be empty !!!'),'');
$ok = false;
}
if ((int) $this->attrs['maxlength'] > 0 && strlen($value) > (int) $this->attrs['maxlength'])
{
$value = substr($value,0,(int) $this->attrs['maxlength']);
}
if ($this->attrs['preg'] && !preg_match($this->attrs['preg'],$value))
{
switch($type)
{
case 'int':
self::set_validation_error($form_name,lang("'%1' is not a valid integer !!!",$value),'');
break;
case 'float':
self::set_validation_error($form_name,lang("'%1' is not a valid floatingpoint number !!!",$value),'');
break;
default:
self::set_validation_error($form_name,lang("'%1' has an invalid format !!!",$value)/*." !preg_match('$this->attrs[preg]', '$value')"*/,'');
break;
}
$ok = false;
}
elseif ($type == 'int' || $type == 'float') // cast int and float and check range
{
if ((string)$value !== '' || $this->attrs['needed']) // empty values are Ok if needed is not set
{
$value = $type == 'int' ? (int) $value : (float) str_replace(',','.',$value); // allow for german (and maybe other) format
if (!empty($this->attrs['min']) && $value < $this->attrs['min'])
{
self::set_validation_error($form_name,lang("Value has to be at least '%1' !!!",$this->attrs['min']),'');
$value = $type == 'int' ? (int) $this->attrs['min'] : (float) $this->attrs['min'];
$ok = false;
}
if (!empty($this->attrs['max']) && $value > $this->attrs['max'])
{
self::set_validation_error($form_name,lang("Value has to be at maximum '%1' !!!",$this->attrs['max']),'');
$value = $type == 'int' ? (int) $this->attrs['max'] : (float) $this->attrs['max'];
$ok = false;
}
}
}
$valid = $value;
}
return parent::validate($content, $validated, $cname) && $ok;
}
}
etemplate_widget::registerWidget('etemplate_widget_textbox', array('textbox','int','float','passwd','hidden','colorpicker'));