egroupware/etemplate/inc/class.etemplate_widget.inc.php
Ralf Becker f203703e42 fixed changing of settings (eg. template set) did not work anymore reported by Christoph Kaulich
caused by always breaking reference from attribute values. Now reference is only broken, if attribute value needs expanding (because it contains @ or $)
2014-05-22 09:39:16 +00:00

1044 lines
34 KiB
PHP

<?php
/**
* EGroupware - eTemplate widget baseclass
*
* @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-14 by RalfBecker@outdoor-training.de
* @version $Id$
*/
/**
* 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
{
/**
* Widget type
*
* @var string
*/
public $type;
/**
* Widget id
*
* @var string
*/
public $id;
/**
* Widget attributes
*
* @var array
*/
public $attrs = array();
/**
* Children
*
* @var array
*/
protected $children = array();
/**
* (Array of) comma-separated list of legacy options to automatically replace when parsing with set_attrs
*
* @var string|array
*/
protected $legacy_options;
/**
* 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;
/**
* Namespaced content array, used when trying to initialize
*
* This is pretty much a global static variable, used when reading
* a template with the content set. This allows variable expansion
* in the constructor.
*
* @protected $cont
*/
static protected $cont = null;
/**
* Constructor
*
* @param string|XMLReader $xml string with xml or XMLReader positioned on the element to construct
* @throws egw_exception_wrong_parameter
*/
public function __construct($xml)
{
$reader = self::get_reader($xml);
$this->type = $reader->name;
$depth = $reader->depth;
$this->id = $reader->getAttribute('id');
// Update content?
if(self::$cont == null)
self::$cont = is_array(self::$request->content) ? self::$request->content : array();
if($this->id && is_array(self::$cont[$this->id]))
{
$old_cont = self::$cont;
self::$cont = self::$cont[$this->id];
}
// read all attributes
$this->set_attrs($reader);
while($reader->read() && $reader->depth > $depth)
{
if ($reader->nodeType == XMLReader::ELEMENT && $reader->depth > $depth)
{
$this->children[] = self::factory($reader->name, $reader, $reader->getAttribute('id'));
}
}
// Reset content as we leave
if($old_cont) {
self::$cont = $old_cont;
}
}
/**
* Get XMLReader for given xml string
*
* @param string|XMLReader $xml string with xml or XMLReader positioned on an element
* @throws egw_exception_wrong_parameter
*/
protected static function get_reader($xml)
{
if (is_a($xml, 'XMLReader'))
{
$reader = $xml;
}
else
{
$reader = new XMLReader();
if (!$reader->XML($xml))
{
throw new egw_exception_wrong_parameter("Can't parse xml:\n$xml");
}
}
return $reader;
}
/**
* Parse and set extra attributes from xml in template object
*
* Returns a cloned template object, if any attribute needs to be set.
* This is necessary as templates can be used multiple time, so we can not alter the cached template!
*
* @param string|XMLReader $xml
* @param boolean $cloned=true true: object does NOT need to be cloned, false: to set attribute, set them in cloned object
* @return etemplate_widget_template current object or clone, if any attribute was set
*/
public function set_attrs($xml, $cloned=true)
{
$reader = self::get_reader($xml);
// check if we have to split legacy options (can be by type)
$legacy_options = $this->legacy_options;
if (is_array($legacy_options))
{
if (!($type = $reader->getAttribute('type')))
{
$type = $this->type;
}
$legacy_options = $legacy_options[$type];
}
// read and set all attributes
$template = $this;
while($reader->moveToNextAttribute())
{
if ($reader->name != 'id' && $template->attr[$reader->name] != $reader->value)
{
if (!$cloned)
{
$template = clone($this);
$cloned = true; // only clone it once, otherwise we loose attributes!
}
// $reader->value is an object and therefore assigned by reference
// this is important to not loose content when validating dynamic generated tabs as in settings!
$template->attrs[$reader->name] = $value = $reader->value;
// expand attributes values, otherwise eg. validation can not use attrs referencing to content
if ($value[0] == '@' || strpos($value, '$') !== false)
{
$value = self::expand_name($value, null, null, null, null,
isset(self::$cont) ? self::$cont : self::$request->content);
}
// split legacy options
if ($legacy_options && $reader->name == 'options')
{
$legacy_options = explode(',', $legacy_options);
foreach(self::csv_split($value, count($legacy_options)) as $n => $val)
{
if ($legacy_options[$n] && (string)$val !== '') $template->attrs[$legacy_options[$n]] = $val;
}
}
}
}
// Add in anything in the modification array
if(is_array(self::$request->modifications[$this->id]))
{
$this->attrs = array_merge($this->attrs,self::$request->modifications[$this->id]);
}
return $template;
}
/**
* Split a $delimiter-separated options string, which can contain parts with delimiters enclosed in $enclosure
*
* Examples:
* - csv_split('"1,2,3",2,3') === array('1,2,3','2','3')
* - csv_split('1,2,3',2) === array('1','2,3')
* - csv_split('"1,2,3",2,3',2) === array('1,2,3','2,3')
* - csv_split('"a""b,c",d') === array('a"b,c','d') // to escape enclosures double them!
*
* @param string $str
* @param int $num=null in how many parts to split maximal, parts over this number end up (unseparated) in the last part
* @param string $delimiter=','
* @param string $enclosure='"'
* @return array
*/
public static function csv_split($str,$num=null,$delimiter=',',$enclosure='"')
{
if (strpos($str,$enclosure) === false)
{
return is_null($num) ? explode($delimiter,$str) : explode($delimiter,$str,$num); // no need to run this more expensive code
}
$parts = explode($delimiter,$str);
for($n = 0; isset($parts[$n]); ++$n)
{
$part =& $parts[$n];
if ($part[0] === $enclosure)
{
while (isset($parts[$n+1]) && substr($part,-1) !== $enclosure)
{
$part .= $delimiter.$parts[++$n];
unset($parts[$n]);
}
$part = substr(str_replace($enclosure.$enclosure,$enclosure,$part),1,-1);
}
}
$parts = array_values($parts); // renumber the parts (in case we had to concat them)
if ($num > 0 && count($parts) > $num)
{
$parts[$num-1] = implode($delimiter,array_slice($parts,$num-1,count($parts)-$num+1));
$parts = array_slice($parts,0,$num);
}
return $parts;
}
/**
* 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)
{
if (!is_subclass_of($class, __CLASS__))
{
throw new egw_exception_wrong_parameter(__METHOD__."('$class', ".array2string($widgets).") $class is no subclass of ".__CLASS__.'!');
}
foreach((array)$widgets as $widget)
{
self::$widget_registry[$widget] = $class;
}
}
/**
* Factory method to construct all widgets
*
* @param string $type
* @param string|XMLReader $xml
* @param string $id=null
*/
public static function factory($type, $xml, $id=null)
{
$class_name =& self::$widget_registry[$type];
if (!isset($class_name))
{
list($basetype) = explode('-',$type);
if (!class_exists($class_name = 'etemplate_widget_'.str_replace('-','_',$type)) &&
!class_exists($class_name = 'etemplate_widget_'.$basetype) &&
// widgets supplied by application in class ${app}_widget_etemplate or ${app}_${subtype}_widget_etemplate
!(isset($GLOBALS['egw_info']['apps'][$basetype]) &&
(class_exists($class_name = str_replace('-','_',$type).'_etemplate_widget') ||
class_exists($class_name = $basetype.'_etemplate_widget'))))
{
// Try for base type, it's probably better than the root
if(self::$widget_registry[$basetype] && self::$widget_registry[$basetype] != $class_name)
{
$class_name = self::$widget_registry[$basetype];
}
else
{
// Fall back to widget class, we can not ignore it, as the widget may contain other widgets
$class_name = 'etemplate_widget';
}
}
}
if(!$xml)
{
if (empty($type)) $type = 'widget';
$xml = "<$type id='$id'/>";
}
//error_log(__METHOD__."('$type', ..., '$id') using $class_name");
// currently only overlays can contain templates, other widgets can only reference to templates via id
if ($type == 'template' && $id && ($template = etemplate_widget_template::instance($id)))
{
// references can set different attributes like: class, span, content (namespace)
return $template->set_attrs($xml, false); // false = need to clone template, if attributs are set!
}
return new $class_name($xml);
}
/**
* Iterate over children to find the one with the given id and optional type
*
* @param string $id
* @param string $type=null
* @return etemplate_widget or NULL
*/
public function getElementById($id, $type=null)
{
foreach($this->children as $child)
{
if ($child->id === $id && (is_null($type) || $child->type === $type))
{
return $child;
}
if (($element = $child->getElementById($id, $type)))
{
return $element;
}
}
return null;
}
/**
* Iterate over children to find the one with the given type
*
* @param string $type
* @return etemplate_widget or NULL
*/
public function getElementsByType($type)
{
$elements = array();
foreach($this->children as $child)
{
if ($child->type === $type)
{
$elements[] = $child;
}
$elements += $child->getElementsByType($type, $subclass_ok);
}
return $elements;
}
/**
* Run a given method on all children
*
* Default implementation only calls method on itself and run on all children
*
* @param string $method_name
* @param array $params=array('') parameter(s) first parameter has to be the cname, second $expand!
* @param boolean $respect_disabled=false false (default): ignore disabled, true: method is NOT run for disabled widgets AND their children
*/
public function run($method_name, $params=array(''), $respect_disabled=false)
{
// maintain $expand array name-expansion
$cname = $params[0];
$expand =& $params[1];
if ($expand['cname'] && $expand['cname'] !== $cname)
{
$expand['cont'] =& self::get_array(self::$request->content, $cname);
$expand['cname'] = $cname;
}
if ($respect_disabled && ($disabled = $this->attrs['disabled'] && self::check_disabled($this->attrs['disabled'], $expand)))
{
error_log(__METHOD__."('$method_name', ".array2string($params).', '.array2string($respect_disabled).") $this disabled='{$this->attrs['disabled']}'=".array2string($disabled).": NOT running");
return;
}
if (method_exists($this, $method_name))
{
// Some parameter checking to avoid fatal errors
$call = true;
$method = new ReflectionMethod($this, $method_name);
foreach($method->getParameters() as $index => $param)
{
if(!$param->isOptional() && !array_key_exists($index,$params))
{
error_log("Missing required parameter {$param->getPosition()}: {$param->getName()}");
$call = false;
}
if($param->isArray() && !is_array($params[$index]))
{
error_log("$method_name expects an array for {$param->getPosition()}: {$param->getName()}");
$params[$index] = (array)$params[$index];
}
}
if($call) call_user_func_array(array($this, $method_name), $params);
}
foreach($this->children as $child)
{
// If type has something that can be expanded, we need to expand it so the correct method is run
$this->expand_widget($child, $expand);
$child->run($method_name, $params, $respect_disabled);
}
}
/**
* If a widget's type is expandable, we need to expand it to make sure we have
* the right class before running the method on it
*
* @param etemplate_widget $child Widget to check & expand if needed
* @param Array $expand Expansion array
*/
protected function expand_widget(etemplate_widget &$child, Array &$expand)
{
if(strpos($child->attrs['type'], '@') !== false || strpos($child->attrs['type'], '$') !== false)
{
$type = self::expand_name($child->attrs['type'],$expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']);
$id = self::expand_name($child->id,$expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']);
$attrs = $child->attrs;
unset($attrs['type']);
$expanded_child = self::factory($type, false,$id);
$expanded_child->id = $id;
$expanded_child->type = $type;
$expanded_child->attrs = $attrs + array('type' => $type);
$child = $expanded_child;
}
}
/**
* Checks if a grid row or column is disabled
*
* Expression: [!][@]val[=[@]check]
* Parts in square brackets are optional, a ! negates the expression, @val evaluates to $content['val']
* if no =check is given all set non-empty and non-zero strings are true (standard php behavior)
*
* @param string $disabled expression to check, eg. "!@var" for !$content['var']
* @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
* @return boolean true if the row/col is disabled or false if not
*/
protected static function check_disabled($disabled, array $expand)
{
if ($not = $disabled[0] == '!')
{
$disabled = substr($disabled,1);
}
list($val,$check_val) = $vals = explode('=',$disabled);
// use expand_name to be able to use @ or $
$val = self::expand_name($val,$expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']);
$check_val = self::expand_name($check_val,$expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']);
$result = count($vals) == 1 ? $val != '' : ($check_val[0] == '/' ? preg_match($check_val,$val) : $val == $check_val);
if ($not) $result = !$result;
//error_log(__METHOD__."('".($not?'!':'')."$disabled' = '$val' ".(count($vals) == 1 ? '' : ($not?'!':'=')."= '$check_val'")." = ".($result?'True':'False'));
return $result;
}
/**
* Regular expression matching a PHP variable in a string, eg.
*
* "replies[$row][reply_message]" should only match $row
* "delete[$row_cont[path]]" should match $row_cont[path]
*/
const PHP_VAR_PREG = '\$[A-Za-z0-9_]+(\[[A-Za-z0-9_]+\])*';
/**
* allows a few variables (eg. row-number) to be used in field-names
*
* This is mainly used for autorepeat, but other use is possible.
* You need to be aware of the rules PHP uses to expand vars in strings, a name
* of "Row$row[length]" will expand to 'Row' as $row is scalar, you need to use
* "Row${row}[length]" instead. Only one indirection is allowd in a string by php !!!
* Out of that reason we have now the variable $row_cont, which is $cont[$row] too.
* Attention !!!
* Using only number as index in field-names causes a lot trouble, as depending
* on the variable type (which php determines itself) you used filling and later
* accessing the array it can by the index or the key of an array element.
* To make it short and clear, use "Row$row" or "$col$row" not "$row" or "$row$col" !!!
*
* @param string $name the name to expand
* @param int $c is the column index starting with 0 (if you have row-headers, data-cells start at 1)
* @param int $row is the row number starting with 0 (if you have col-headers, data-cells start at 1)
* @param int $c_ is the value of the previous template-inclusion,
* eg. the column-headers in the eTemplate-editor are templates itself,
* to show the column-name in the header you can not use $col as it will
* be constant as it is always the same col in the header-template,
* what you want is the value of the previous template-inclusion.
* @param int $row_ is the value of the previous template-inclusion,
* @param array $cont content of the template, you might use it to generate button-names with id values in it:
* "del[$cont[id]]" expands to "del[123]" if $cont = array('id' => 123)
* @return string the expanded name
*/
protected static function expand_name($name,$c,$row,$c_='',$row_='',$cont='')
{
$is_index_in_content = $name[0] == '@';
if (($pos_var=strpos($name,'$')) !== false)
{
if (!$cont)
{
$cont = array();
}
if (!is_numeric($c)) $c = boetemplate::chrs2num($c);
$col = self::num2chrs($c-1); // $c-1 to get: 0:'@', 1:'A', ...
$col_ = self::num2chrs($c_-1);
$row_cont = $cont[$row];
$col_row_cont = $cont[$col.$row];
/* RB: dont think any of this is needed in eTemplate2, as this escaping probably needs to be done on clientside anyway
// check if name is enclosed in single quotes as argument eg. to an event handler or
// variable name is contained in quotes and curly brackets, eg. "'{$cont[nm][path]}'" or
// used as name for a button like "delete[$row_cont[something]]" --> quote contained quotes (' or ")
if (in_array($name[$pos_var-1],array('[',"'",'{')) && preg_match('/[\'\[]{?('.self::PHP_VAR_PREG.')}?[\'\]]+/',$name,$matches))
{
eval('$value = '.$matches[1].';');
if (is_array($value) && $name[$pos_var-1] == "'") // arrays are only supported for '
{
foreach($value as &$val)
{
$val = "'".str_replace(array("'",'"','[',']'),array('\\\'','&quot;','&#x5B;','&#x5D;'),$val)."'";
}
$value = '&#x5B; '.implode(', ',$value).' &#x5D;';
$name = str_replace("'".$matches[1]."'",$value,$name);
}
else
{
$value = str_replace(array("'",'"','[',']'),array('\\\'','&quot;','&#x5B;','&#x5D;'),$value);
$name = str_replace(array('{'.$matches[1].'}',$matches[1]),$value,$name);
}
}
// check if name is assigned in an url --> urlendcode contained & as %26, as egw::link explodes it on &
if ($name[$pos_var-1] == '=' && preg_match('/[&?]([A-Za-z0-9_]+(\[[A-Za-z0-9_]+\])*)=('.self::PHP_VAR_PREG.')/',$name,$matches))
{
eval('$value = '.$matches[3].';');
if (is_array($value)) // works only reasonable, if get-parameter uses array notation, eg. &file[]=$cont[filenames]
{
foreach($value as &$val)
{
$val = str_replace('&',urlencode('&'),$val);
}
$name = str_replace($matches[3],implode('&'.$matches[1].'=',$value),$name);
}
else
{
$value = str_replace('&',urlencode('&'),$value);
$name = str_replace($matches[3],$value,$name);
}
}*/
eval('$name = "'.str_replace('"','\\"',$name).'";');
}
if ($is_index_in_content)
{
if ($name[1] == '@' && is_array(self::$request->content))
{
$name = self::get_array(self::$request->content,substr($name,2));
}
elseif(is_array($cont))
{
$name = self::get_array($cont,substr($name,1));
}
else
{
// Content not set expands to ''
$name = '';
}
}
// RB: not sure why this business with entity encoding for square brakets, it messes up validation
//$name = str_replace(array('[',']'),array('&#x5B;','&#x5D;'),$name);
return $name;
}
/**
* generates column-names from index: 'A', 'B', ..., 'AA', 'AB', ..., 'ZZ' (not more!)
*
* @param int $num numerical index to generate name from 1 => 'A'
* @return string the name
*/
static function num2chrs($num)
{
$min = ord('A');
$max = ord('Z') - $min + 1;
if ($num >= $max)
{
$chrs = chr(($num / $max) + $min - 1);
}
$chrs .= chr(($num % $max) + $min);
return $chrs;
}
/**
* Convert object to string
*
* @return string
*/
public function __toString()
{
return '['.get_class($this).'] ' .
$this->type.($this->attrs['type'] && $this->attrs['type'] != $this->type ? '('.$this->attrs['type'].')' : '').'#'.$this->id;
}
/**
* When cloning a widget, we also clone children
*/
public function __clone()
{
foreach($this->children as $child_num => $child) {
$this->children[$child_num] = clone $child;
}
}
/**
* Convert widget (incl. children) to xml
*
* @param string $indent=''
* @return string
*/
public function toXml($indent='')
{
echo "$indent<$this->type";
if ($this->id) echo ' id="'.htmlspecialchars($this->id).'"';
foreach($this->attrs as $name => $value)
{
if ($name == 'options' && $this->legacy_options && (!is_array($this->legacy_options) ||
isset($this->legacy_options[$this->attrs['type'] ? $this->attrs['type'] : $this->type])))
{
continue; // do NOT output already converted legacy options
}
echo ' '.$name.'="'.htmlspecialchars($value).'"';
}
echo ' php-class="'.get_class($this).'"';
if ($this->children)
{
echo ">\n";
foreach($this->children as $child)
{
$child->toXml($indent."\t");
}
echo "$indent</$this->type>\n";
}
else
{
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
* @param array $expand=null values for keys 'c', 'row', 'c_', 'row_', 'cont'
* @return string complete form-name
*/
static function form_name($cname,$name,array $expand=null)
{
if ($expand && !empty($name))
{
$name = self::expand_name($name, $expand['c'], $expand['row'], $expand['c_'], $expand['row_'], $expand['cont']);
}
if (count($name_parts = explode('[', $name, 2)) > 1)
{
$name_parts = array_merge(array($name_parts[0]), explode('][', substr($name_parts[1],0,-1)));
}
if (!empty($cname))
{
array_unshift($name_parts,$cname);
}
$form_name = array_shift($name_parts);
if (count($name_parts))
{
// RB: not sure why this business with entity encoding for square brakets, it messes up validation
//$form_name .= '&#x5B;'.implode('&#x5D;&#x5B;',$name_parts).'&#x5D;';
$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 null 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
// Make sure none of these are left
$idx = str_replace(array('&#x5B;','&#x5D;'),array('[',']'),$idx);
// Handle things expecting arrays - ends in []
if(substr($idx,-2) == "[]")
{
$idx = substr($idx,0,-2);
}
if (count($idxs = explode('[', $idx, 2)) > 1)
{
$idxs = array_merge(array($idxs[0]), explode('][', substr($idxs[1],0,-1)));
}
$pos = &$arr;
foreach($idxs as $idx)
{
if (!is_array($pos) && (!$reference_into || $reference_into && isset($pos)))
{
//if ($reference_into) error_log(__METHOD__."(".(strlen($s=array2string($arr))>512?substr($s,0,512).'...':$s).", '$idx', ".array2string($reference_into).", ".array2string($skip_empty).") ".function_backtrace());
return null;
}
if($skip_empty && (!is_array($pos) || !isset($pos[$idx]))) return null;
$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
*
* @param string $cname=''
* @param string $form_name=null form_name, to not calculate him again
* @return boolean
*/
public function is_readonly($cname='', $form_name=null)
{
if (!isset($form_name))
{
$expand = array(
'cont' => self::get_array(self::$request->content, $cname),
);
$form_name = self::form_name($cname, $this->id, $expand);
}
$readonly = $this->attrs['readonly'] || self::$request->readonlys[$form_name] ||
self::get_array(self::$request->readonlys,$form_name) === true ||
isset(self::$request->readonlys['__ALL__']) && (
// Exceptions to all
self::$request->readonlys[$form_name] !== false &&
self::get_array(self::$request->readonlys,$form_name) !== false
);
//error_log(__METHOD__."('$cname') this->id='$this->id' --> form_name='$form_name': attrs[readonly]=".array2string($this->attrs['readonly']).", readonlys['$form_name']=".array2string(self::$request->readonlys[$form_name]).", readonlys[$form_name]=".array2string(self::get_array(self::$request->readonlys,$form_name)).", readonlys['__ALL__']=".array2string(self::$request->readonlys['__ALL__'])." 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|boolean $error error-message already translated or false to reset all existing error for given name
* @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)
{
// not yet used: if (is_null($cname)) $cname = self::$name_vars;
error_log(__METHOD__."('$name','$error','$cname') ".function_backtrace());
if ($cname) $name = self::form_name($cname,$name);
if ($error === false)
{
unset(self::$validation_errors[$name]);
}
else
{
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='')
{
// not yet used: 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;
}
/**
* Returns reference to an attribute in a named cell
*
* Currently we always return a reference to an not set value, unless it was set before.
* We do not return a reference to the actual cell, as it get's contructed on client-side!
*
* @param string $name cell-name
* @param string $attr attribute-name
* @return mixed reference to attribute, usually NULL
*/
public function &getElementAttribute($name, $attr)
{
//error_log(__METHOD__."('$name', '$attr')");
return self::$request->modifications[$name][$attr];
}
/**
* Set an attribute in a named cell if val is not NULL else return the attribute
*
* Can be called static, in which case it only sets modifications.
*
* @param string $name cell-name
* @param string $attr attribute-name
* @param mixed $val if not NULL sets attribute else returns it
* @return reference to attribute
*/
public static function &setElementAttribute($name,$attr,$val)
{
//error_log(__METHOD__."('$name', '$attr', ...) request=".get_class(self::$request).", response=".get_class(self::$response).function_backtrace());
$ref =& self::$request->modifications[$name][$attr];
if(self::$request && self::$response && (!isset($this) || $val != $this->attrs[$attr]))
{
// In an AJAX response - automatically add
self::$response->generic('assign',array(
'etemplate_exec_id' => self::$request->id(),
'id' => $name,
'key' => $attr,
'value' => $val
));
// Don't delete it
self::$request->data_modified = true;
//error_log(__METHOD__."('$name', '$attr', ...) ".function_backtrace());
}
if (isset($this)) $this->attrs[$attr] = $val;
if (!is_null($val)) $ref = $val;
//error_log(__METHOD__."('$name', '$attr', ".array2string($val).')');
return $ref;
}
/**
* disables all cells with name == $name
*
* @param sting $name cell-name
* @param boolean $disabled=true disable or enable a cell, default true=disable
* @return reference to attribute
*/
public function disableElement($name,$disabled=True)
{
return self::setElementAttribute($name, 'disabled', $disabled);
}
}
/**
* *box widgets having an own namespace
*/
class etemplate_widget_box extends etemplate_widget
{
/**
* (Array of) comma-separated list of legacy options to automatically replace when parsing with set_attrs
*
* @var string|array
*/
protected $legacy_options = array(
'box' => ',cellpadding,cellspacing,keep',
'hbox' => 'cellpadding,cellspacing,keep',
'vbox' => 'cellpadding,cellspacing,keep',
'groupbox' => 'cellpadding,cellspacing,keep',
);
/**
* Run a given method on all children
*
* Reimplemented because grids and boxes can have an own namespace
*
* @param string $method_name
* @param array $params=array('') parameter(s) first parameter has to be cname!
* @param boolean $respect_disabled=false false (default): ignore disabled, true: method is NOT run for disabled widgets AND their children
*/
public function run($method_name, $params=array(''), $respect_disabled=false)
{
$cname =& $params[0];
$expand =& $params[1];
$old_cname = $params[0];
$old_expand = $params[1];
if ($this->id) $cname = self::form_name($cname, $this->id, $params[1]);
if ($expand['cname'] !== $cname && $cname)
{
$expand['cont'] =& self::get_array(self::$request->content, $cname);
$expand['cname'] = $cname;
}
if ($respect_disabled && ($disabled = $this->attrs['disabled'] && self::check_disabled($this->attrs['disabled'], $expand)))
{
error_log(__METHOD__."('$method_name', ".array2string($params).', '.array2string($respect_disabled).") $this disabled='{$this->attrs['disabled']}'=".array2string($disabled).": NOT running");
return;
}
if (method_exists($this, $method_name))
{
call_user_func_array(array($this, $method_name), $params);
}
// Expand children
for($n = 0; ; ++$n)
{
if (isset($this->children[$n]))
{
$child =& $this->children[$n];
// If type has something that can be expanded, we need to expand it so the correct method is run
$this->expand_widget($child, $expand);
}
// check if we need to autorepeat last row ($child)
elseif (isset($child) && $child->type == 'box' && $this->need_autorepeat($child, $cname, $expand))
{
// Set row for repeating
$expand['row'] = $n;
// not breaking repeats last row/column ($child)
}
else
{
break;
}
//error_log('Running ' . $method_name . ' on child ' . $n . '(' . $child . ') ['.$expand['row'] . ','.$expand['c'] . ']');
$disabled = $child->run($method_name, $params, $respect_disabled, $columns_disabled) === false;
}
$params[0] = $old_cname;
$params[1] = $old_expand;
return true;
}
/**
* Check if a box child needs autorepeating, because still content left
*
* We only check passed widget and direct children.
*
* @param string $cname
* @param array $expand
*/
private function need_autorepeat(etemplate_widget &$widget, $cname, array $expand)
{
foreach(array($widget) + $widget->children as $check_widget)
{
$pat = $check_widget->id;
while(($pat = strstr($pat, '$')))
{
$pat = substr($pat,$pat[1] == '{' ? 2 : 1);
$Ok = $pat[0] == 'r' && !(substr($pat,0,2) == 'r_' ||
substr($pat,0,4) == 'row_' && substr($pat,0,8) != 'row_cont');
if ($Ok && ($fname=self::form_name($cname, $check_widget->id, $expand)) &&
// need to break if fname ends in [] as get_array() will ignore it and returns whole array
// for an id like "run[$row_cont[appname]]"
substr($fname, -2) != '[]' &&
($value = self::get_array(self::$request->content, $fname)) !== null) // null = not found (can be false!)
{
//error_log(__METHOD__."($widget,$cname) $this autorepeating row $expand[row] because of $check_widget->id = '$fname' is ".array2string($value));
return true;
}
}
}
return false;
}
}
// register class for layout widgets, which can have an own namespace
etemplate_widget::registerWidget('etemplate_widget_box', array('box', 'hbox', 'vbox', 'groupbox'));
/**
* Description widget
*
* Reimplemented to set legacy options
*/
class etemplate_widget_description extends etemplate_widget
{
/**
* (Array of) comma-separated list of legacy options to automatically replace when parsing with set_attrs
*
* @var string|array
*/
protected $legacy_options = 'bold-italic,link,activate_links,label_for,link_target,link_popup_size,link_title';
}