* @copyright 2002-16 by RalfBecker@outdoor-training.de * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage api * @version $Id$ */ use EGroupware\Api; /** * Business Object for eTemplates, extending the Storage Object */ class boetemplate extends soetemplate { /** * Widget types implemented directly by etemplate (no extensions) * * @var array intern name => label */ static public $types = array( 'label' => 'Label', // Label $cell['label'] is (to be translated) textual content 'text' => 'Text', // Textfield 1 Line (size = [length][,maxlength]) 'int' => 'Integer', // like text, but only numbers (size = [min][,max]) 'float' => 'Floating Point', // --------------- " -------------------------- 'textarea'=> 'Textarea', // Multiline Text Input (size = [rows][,cols]) 'htmlarea' => 'Formatted Text (HTML)', 'checkbox'=> 'Checkbox', 'radio' => 'Radiobutton', // Radiobutton (size = value if checked) 'button'=> 'Submitbutton', 'buttononly' => 'Button', // input type="button" 'hrule' => 'Horizontal Rule', 'template' => 'Template', // $cell['name'] contains template-name, $cell['size'] index into $content,$cname,$readonlys 'image' => 'Image', // label = url, name=link or method, help=alt or title 'progress' => 'Progressbar',// for BC, identical to image with percent value 'date' => '', // Datefield, size='' timestamp or size=format like 'm/d/Y' 'select'=> 'Selectbox', // Selectbox ($sel_options[$name] or $content[options-$name] is array with options) // if size > 1 then multiple selections, size lines showed 'html' => 'Html', // Raw html in $content[$cell['name']] 'file' => 'FileUpload', // show an input type='file', set the local name as ${name}_path 'vbox' => 'VBox', // a (vertical) box to contain widgets in rows, size = # of rows 'hbox' => 'HBox', // a (horizontal) box to contain widgets in cols, size = # of cols 'groupbox' => 'GroupBox', // a box with a label containing other elements to group them (html: fieldset) 'box' => 'Box', // just a container for widgets (html: div) 'grid' => 'Grid', // tabular widget containing rows with columns of widgets 'deck' => 'Deck', // a container of elements where only one is visible, size = # of elem. 'passwd' => 'Password', // a text of type password 'colorpicker' => 'Colorpicker', // input for a color (eg. #123456) 'hidden'=> 'Hidden input', // a hidden input eg. to submit javascript computed values back ); /** * Flag if form validation requires to loop * * @var boolean */ static public $loop = 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 Api\Etemplate\Request */ static public $request; /** * constructor of class * * Calls the constructor of soetemplate * * @param string|array $name name of etemplate or array with name and other keys * @param string|array $load_via name or array with keys of other etemplate to load in order to get $name */ function __construct($name='',$load_via='') { parent::__construct(); $tname = &$name; if (is_array($name)) { $tname = &$name['name']; } $tname = (strpos($tname,'.') === false && !empty($tname) ? (is_array($load_via) ? $load_via['name'] : $load_via).'.':'').$tname; if (empty($tname) || !$this->read($name,'','',0,'',$load_via)) { $this->init($name); } } /** * 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 $content the content-array in the context of the grid * @param int $row =null to be able to use $row or $row_content in value of checks * @param int $c =null to be able to use $row or $row_content in value of checks * @return boolean true if the row/col is disabled or false if not */ protected function check_disabled($disabled,$content,$row=null,$c=null) { if ($this->onclick_handler && !$this->no_onclick) { return false; // we have an onclick handler } 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 = $this->expand_name($val,$c,$row,'','',$content); $check_val = $this->expand_name($check_val,$c,$row,'','',$content); $result = count($vals) == 1 ? $val != '' : ($check_val[0] == '/' ? preg_match($check_val,$val) : $val == $check_val); if ($not) $result = !$result; //echo "

check_disabled: '".($not?'!':'')."$disabled' = '$val' ".(count($vals) == 1 ? '' : ($not?'!':'=')."= '$check_val'")." = ".($result?'True':'False')."

\n"; 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 sring $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 */ 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 = boetemplate::num2chrs($c-1); // $c-1 to get: 0:'@', 1:'A', ... $col_ = boetemplate::num2chrs($c_-1); $row_cont = $cont[$row]; $col_row_cont = $cont[$col.$row]; // 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)) { if (eval($code='$value = '.$matches[1].';') === false) { error_log(__METHOD__."(name='$name', c='$c', row=$row, c_='$c_', row_=$row_, ...) line ".__LINE__." ERROR parsing: $code"); error_log(function_backtrace()); } if (is_array($value) && $name[$pos_var-1] == "'") // arrays are only supported for ' { foreach($value as &$val) { $val = "'".str_replace(array("'",'"'),array('\\\'','"'),$val)."'"; } $value = '[ '.implode(', ',$value).' ]'; $name = str_replace("'".$matches[1]."'",$value,$name); } else { $value = str_replace(array("'",'"','[',']'),array('\\\'','"','[',']'),$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)) { if (eval($code='$value = '.$matches[3].';') === false) { error_log(__METHOD__."(name='$name', c='$c', row=$row, c_='$c_', row_=$row_, ...) line ".__LINE__." ERROR parsing: $code"); error_log(function_backtrace()); } 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); } } if (eval($code='$name = "'.str_replace(array('\\', '"'), array('\\\\', '\\"'), // fix non-fatal PHP Parse error, when $j was used eg. in onload str_replace(array('$j.', '$j('), array('jQuery.', 'jQuery('), $name)).'";') === false) { error_log(__METHOD__."(name='$name', c='$c', row=$row, c_='$c_', row_=$row_, ...) line ".__LINE__." ERROR parsing: $code"); error_log(function_backtrace()); } } if ($is_index_in_content) { if ($name[1] == '@') { $name = self::get_array(self::$request->content,substr($name,2)); } else { $name = self::get_array($cont,substr($name,1)); } } return $name; } /** * Checks if we have an row- or column autorepeat and sets the indexes for $content, etc. * * Autorepeat is important to allow a variable numer of rows or cols, eg. for a list. * The eTemplate has only one (have to be the last) row or column, which gets * automaticaly repeated as long as content is availible. To check this the content * has to be in an sub-array of content. The index / subscript into content is * determined by the content of size for templates or name for regular fields. * An autorepeat is defined by an index which contains variables to expand. * (vor variable expansion in names see expand_names). Usually I use the keys * $row: 0, 1, 2, 3, ... for only rows, $col: '@', 'A', 'B', 'C', ... for only cols or * $col$row: '@0','A0',... '@1','A1','B1',... '@2','A2','B2',... for both rows and cells. * In general everything expand_names can generate is ok - see there. * As you usually have col- and row-headers, data-cells start with '1' or 'A' !!! * * @param array $cell with data of cell: name, type, size, ... * @param int $c,$r col/row index starting from 0 * @param string &$idx returns the index in $content and $readonlys (NOT $sel_options !!!) * @param string &$idx_cname returns the basename for the form-name: is $idx if only one value * (no ',') is given in size (name (not template-fields) are always only one value) * @param boolean $check_col to check for col- or row-autorepeat * @return boolean true if cell is autorepeat (has index with vars / '$') or false otherwise */ function autorepeat_idx($cell,$c,$r,&$idx,&$idx_cname,$check_col=False,$cont=null) { $org_idx = $idx = $cell[ $cell['type'] == 'template' ? 'size' : 'name' ]; $idx = $this->expand_name($idx,$c,$r,'','',$cont); if (!($komma = strpos($idx,','))) { $idx_cname = $idx; } else { $idx_cname = substr($idx,1+$komma); $idx = substr($idx,0,$komma); } $Ok = False; $pat = $org_idx; while (!$Ok && ($pat = @strstr($pat,'$'))) { $pat = substr($pat,$pat[1] == '{' ? 2 : 1); if ($check_col) { $Ok = $pat[0] == 'c' && !(substr($pat,0,4) == 'cont' || substr($pat,0,2) == 'c_' || substr($pat,0,4) == 'col_'); } else { $Ok = $pat[0] == 'r' && !(substr($pat,0,2) == 'r_' || substr($pat,0,4) == 'row_'); } } if ($this->name && $this->name == $this->debug) { echo "$this->name ".($check_col ? 'col' : 'row')."-check: c=$c, r=$r, idx='$org_idx'='$idx' idx_cname='$idx_cname' ==> ".($Ok?'True':'False')."

\n"; } return $Ok; } /** * gets an attribute in a named cell * * @param string $name cell-name * @param string $attr attribute-name * @return mixed the attribute or False if named cell not found */ function &getElementAttribute($name,$attr) { return $this->setElementAttribute($name,$attr,NULL); } /** * set an attribute in a named cell if val is not NULL else return the attribute * * @param string $name cell-name * @param string $attr attribute-name * @param mixed $val if not NULL sets attribute else returns it * @return mixed number of changed cells or False, if none changed */ function &setElementAttribute($name,$attr,$val) { //echo "

set_cell_attribute(tpl->name=$this->name, name='$name', attr='$attr',val='$val')

\n"; $extra = array(false,$name,$attr,$val); $result =& $this->widget_tree_walk('set_cell_attribute_helper',$extra); if (is_null($val)) { return $result; } return $extra[0]; } /** * disables all cells with name == $name * * @param sting $name cell-name * @param boolean $disabled =true disable or enable a cell, default true=disable * @return mixed number of changed cells or False, if none changed */ function disableElement($name,$disabled=True) { return $this->set_cell_attribute($name,'disabled',$disabled); } /** * 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 * @deprecated use getElementAttribute($name, $attr) */ public function &get_cell_attribute($name,$attr) { return self::getElementAttribute($name, $attr); } /** * set an attribute in a named cell if val is not NULL else return the attribute * * @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 * @deprecated use setElementAttribute($name, $attr, $val) */ public function &set_cell_attribute($name,$attr,$val) { return self::setElementAttribute($name, $attr, $val); } /** * 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 * @deprecated use disableElement($name, $disabled=true) */ public function disable_cells($name,$disabled=True) { return self::disableElement($name, $disabled); } /** * set one or more attibutes for row $n * * @param int $n numerical row-number starting with 1 (!) * @param string $height percent or pixel or '' for no height * @param string $class name of css class (without the leading '.') or '' for no class * @param string $valign alignment (top,middle,bottom) or '' for none * @param boolean $disabled True or expression or False to disable or enable the row (Only the number 0 means dont change the attribute !!!) * @param string $path ='/0' default is the first widget in the tree of children * @return false if $path is no grid or array(height,class,valign,disabled) otherwise */ function set_row_attributes($n,$height=0,$class=0,$valign=0,$disabled=0,$path='/0') { $grid =& $this->get_widget_by_path($path); if (is_null($grid) || $grid['type'] != 'grid') return false; $grid_attr =& $grid['data'][0]; list($old_height,$old_disabled) = explode(',',$grid_attr["h$n"]); $disabled = $disabled !== 0 ? $disabled : $old_disabled; $grid_attr["h$n"] = ($height !== 0 ? $height : $old_height). ($disabled ? ','.$disabled : ''); list($old_class,$old_valign) = explode(',',$grid_attr["c$n"]); $valign = $valign !== 0 ? $valign : $old_valign; $grid_attr["c$n"] = ($class !== 0 ? $class : $old_class). ($valign ? ','.$valign : ''); list($height,$disabled) = explode(',',$grid_attr["h$n"]); list($class,$valign) = explode(',',$grid_attr["c$n"]); return array($height,$class,$valign,$disabled); } /** * disables row $n * * @param int $n numerical row-number starting with 1 (!) * @param boolean $enable =false can be used to re-enable a row if set to True * @param string $path ='/0' default is the first widget in the tree of children */ function disable_row($n,$enable=False,$path='/0') { $this->set_row_attributes($n,0,0,0,!$enable,$path); } /** * set one or more attibutes for column $c * * @param int|string $c numerical column-number starting with 0 (!), or the char-code starting with 'A' * @param string $width percent or pixel or '' for no height * @param mixed $disabled =0 True or expression or False to disable or enable the column (Only the number 0 means dont change the attribute !!!) * @param string $path ='/0' default is the first widget in the tree of children * @return false if $path specifies no grid or array(width,disabled) otherwise */ function set_column_attributes($c,$width=0,$disabled=0,$path='/0') { if (is_numeric($c)) { $c = $this->num2chrs($c); } $grid =& $this->get_widget_by_path($path); if (is_null($grid) || $grid['type'] != 'grid') return false; $grid_attr =& $grid['data'][0]; list($old_width,$old_disabled) = explode(',',$grid_attr[$c]); $disabled = $disabled !== 0 ? $disabled : $old_disabled; $grid_attr[$c] = ($width !== 0 ? $width : $old_width). ($disabled ? ','.$disabled : ''); //echo "set_column_attributes('$c',,'$path'): ".$grid_attr[$c]."

\n"; _debug_array($grid_attr); return explode(',',$grid_attr[$c]); } /** * disables column $c * * @param int|string $c numerical column-number starting with 0 (!), or the char-code starting with 'A' * @param boolean $enable can be used to re-enable a column if set to True * @param string $path ='/0' default is the first widget in the tree of children */ function disable_column($c,$enable=False,$path='/0') { $this->set_column_attributes($c,0,!$enable,$path); } /** * Cache for extension objects * * @var array */ static private $extensions = array(); /** * trys to load the Extension / Widget-class from the app or etemplate * * @param string $name name of the extension, the classname should be class.${name}_widget.inc.php * the $name might be "$name.$app" to give a app-name (default is the current app,or template-name) * @return string|boolean human readable name or false if not found/loadable */ function loadExtension($type) { list($class,$app) = explode('.',$type); $class .= '_widget'; if (!$app) $app = $GLOBALS['egw_info']['flags']['current_app']; if (!file_exists(EGW_SERVER_ROOT."/$app/inc/class.$class.inc.php")) { list($app) = explode('_',$type); } if (!file_exists(EGW_SERVER_ROOT."/$app/inc/class.$class.inc.php")) { list($app) = explode('.',$this->name); } if (!file_exists(EGW_SERVER_ROOT."/$app/inc/class.$class.inc.php")) { $app = 'etemplate'; } if (!file_exists(EGW_SERVER_ROOT."/$app/inc/class.$class.inc.php")) { //echo "

boetemplate::loadExtension($type) extension not found

\n"; return self::$extensions[$type] = False; } self::$extensions[$type] =& CreateObject($app.'.'.$class,$ui='html'); //echo "

boetemplate::loadExtension($type) extension found in App. $app

\n"; return self::$extensions[$type]->human_name; } /** * checks if extension is loaded (load it if it isnt) and optional checks if it has a given method * * @param string $name name of the extension, the classname should be class.${name}_widget.inc.php * the $name might be "$name.$app" to give a app-name (default is the current app,or template-name) * @param string $function 'pre_process', 'post_process' or 'render' * @return boolean true if the extension (incl. method) exists, else false */ protected function haveExtension($type,$function='') { return (self::$extensions[$type] || $this->loadExtension($type,$ui)) && ($function == '' || self::$extensions[$type]->public_functions[$function]); } /** * Check if we have a widget of type $type * * @param string $type * @return boolean true widget exists, false unknow widget type */ public function widgetExists($type) { if (isset(self::$types[$type])) return true; list($main,$sub) = explode('-',$type); if ($this->haveExtension($main) && $this->loadExtension($main) && ($extension = self::$extensions[$main])) { return $type == $main || is_array($extension->human_name) && isset($extension->human_name[$type]); } return false; } /** * executes the pre_process-function of the extension $cell[type] * * @param string $type type of the extension * @param string &$name form-name of this widget/field (used as a unique index into extension_data) * @param mixed &$value value of the extensions content(-array) * @param array &$cell table-cell on which the extension operates * @param array &$readonlys value of the extensions readonly-setting(-array) * @return mixed the return-value of the extensions preprocess function */ protected function extensionPreProcess($type,&$name,&$value,&$cell,&$readonlys) { if (!$this->haveExtension($type)) { return False; } // only supply extension data for non-readonly widgets or if it's already set // otherwise lists store >10k unnecessary data in each etemplate-session if (!($cell['readonly'] || $readonlys && !is_array($readonlys)) || isset(self::$request->extension_data[$name])) { $extension_data =& self::$request->extension_data[$name]; } return self::$extensions[$type]->pre_process($name,$value,$cell,$readonlys,$extension_data,$this); } /** * executes the post_process-function of the extension $cell[type] * * @param string $type name of the extension * @param string $name form-name of this widget/field (used as a unique index into extension_data) * @param mixed &$value returns the value of the extensions content(-array) * @param mixed $value_in unprocessed value, eg. as posted by the browser * @return boolean True if a value should be returned (default for no postprocess fkt.), else False */ protected function extensionPostProcess($type,$name,&$value,$value_in) { if (!$this->haveExtension($type,'post_process')) { return True; } return self::$extensions[$type]->post_process($name,$value, self::$request->extension_data[$name], self::$loop,$this,$value_in); } /** * executes the render-function of the extension $cell[type] * * @param string $type name of the extension * @param string $name form-name of this widget/field (used as a unique index into extension_data) * @param mixed &$value value of the extensions content(-array) * @param array &$cell table-cell on which the extension operates * @param array &$readonlys value of the extensions readonly-setting(-array) * @return mixed return-value of the render function */ protected function extensionRender($type,$name,&$value,&$cell,$readonly) { if (!$this->haveExtension($type,'render')) { return False; } return self::$extensions[$type]->render($cell,$name,$value,$readonly, self::$request->extension_data[$name],$this); } /** * checks if $idx is set in array $arr * * for one level of subindes identical to isset($arr[$idx]) * * @param array $arr array to check * @param string $idx may contain multiple subindex (eg.'x[y][z]') * @return boolean true if set, else false */ static function isset_array($arr,$idx) { if (count($idxs = explode('[', $idx, 2)) > 1) { $idxs = array_merge(array($idxs[0]), explode('][', substr($idxs[1],0,-1))); } $last_idx = array_pop($idxs); $pos = &$arr; foreach($idxs as $idx) { if (!is_array($pos)) { return False; } $pos = &$pos[$idx]; } // was return isset($pos[$last_idx]); // array_key_exists also returns true for keys with value null, which fixes some problems with autorepeating rows return is_array($pos) && array_key_exists($last_idx,$pos); } /** * sets $arr[$idx] = $val * * This works for non-trival indexes like 'a[b][c]' too: $arr['a']['b']['c'] = $val; * * @param array &$arr the array to search * @param string $idx the index, may contain sub-indices like a[b], see example below * @param mixed $val value to set */ static function set_array(&$arr,$idx,$val) { 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) && !is_null($pos)) return; // gives fatal error under PHP5.3 $pos = &$pos[$idx]; } $pos = $val; } /** * 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 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) { return False; } if($skip_empty && (!is_array($pos) || !isset($pos[$idx]))) return false; $pos = &$pos[$idx]; } return $pos; } /** * unsets $arr[$idx] * * This works for non-trival indexes like 'a[b][c]' too * unset_array($arr,'a[b]'); is equivalent to unset($arr['a']['b']); * * @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 */ static function unset_array(&$arr,$idx) { if (!is_array($arr)) { throw new egw_exception_assertion_failed(__METHOD__."(\$arr,'$idx') \$arr is no array!"); } if (count($idxs = explode('[', $idx, 2)) > 1) { $idxs = array_merge(array($idxs[0]), explode('][', substr($idxs[1],0,-1))); } $last_idx = array_pop($idxs); $pos = &$arr; foreach($idxs as $idx) { $pos = &$pos[$idx]; } unset($pos[$last_idx]); } /** * merges $old and $new, content of $new has precedence over $old * * THIS IS NOT THE SAME AS PHP's functions: * - array_merge, as it calls itself recursive for values which are arrays. * - array_merge_recursive accumulates values with the same index and $new does NOT overwrite $old * * @param array $old * @param array $new * @return array the merged array */ static function complete_array_merge($old,$new) { if (is_array($new)) { if (!is_array($old)) $old = (array) $old; foreach($new as $k => $v) { if (!is_array($v) || !isset($old[$k]) || // no array or a new array isset($v[0]) && !is_array($v[0]) && isset($v[count($v)-1])) // or no associative array, eg. selecting multiple accounts { $old[$k] = $v; } else { $old[$k] = self::complete_array_merge($old[$k],$v); } } } return $old; } /** * returns a reference to a widget in the widget's children tree spezified by a path * * The path get's generated by the widget_tree_walk() methode and consists of the keys of the children arrays. * For the 3. Column in the 2. row of a grid which is the only widget in the children-tree it is eg.: "/0/2C" * * @param string $path path in the widget tree * @param int $ancestor =0 0: widget itself, 1: parent, 2: grand-parent, ... * @return array referenz to the widget spezified or null, if it's not found */ function &get_widget_by_path($path,$ancestor=0) { //echo "

boetemplate::get_widget_by_path('$path',$ancestor)

\n"; $path_parts = explode('/',$path); while($ancestor-- > 0) { if (array_pop($path_parts) === '') return null; } $path = implode('/',$path_parts); if ($path == '/' || $path === '') return $this->children; if (count($path_parts) == 2) return $this->children[$path_parts[1]]; return $this->widget_tree_walk('get_widget_by_path_helper',$path); } /** * returns a reference to a widget in the widget's children tree spezified it's name * * It returns the first match! * * @param string $name name of the widget * @return array referenz to the widget spezified or null, if it's not found */ function &get_widget_by_name($name) { return $this->widget_tree_walk('get_widget_by_name_helper',$name); } /** * returns an array of references to widgets of the specified type * * @param type String * @return array */ function &get_widgets_by_type($type) { $extra = array( 'type' => $type, 'widgets' => array() ); $this->widget_tree_walk('get_widgets_by_type_helper', $extra); return $extra['widgets']; } /** * 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 */ 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; } /** * generated a file-name from an eTemplates, name, template(-set) and lang * * @param string|array $name name of template or array('name'=>$name,'template'=>$template,'lang'=>$lang) * @param string $template template-set * @param string $lang language to use * @return string */ private /* static */ function cache_name($name='',$template='default',$lang='default') { if (empty($name)) { $name = $this->name; $template = $this->template; $lang = $this->lang; } elseif (is_array($name)) { $template = $name['template']; $lang = $name['lang']; $name = $name['name']; } if (empty($template)) { $template = 'default'; } $cname = $template . '/' . $name . (!empty($lang) && $lang != 'default' ? '.' . $lang : ''); //echo "

".__METHOD__."('$name','$template','$lang') = '$cname'

"; return $cname; } /** * stores the etemplate in the cache in egw_info * * @param boetemplate $tpl =null required parameter for static use! * @param boolean $only_update_older =false true only update cache, if it contains an older template */ public /*static*/ function store_in_cache(boetemplate $tpl=null, $only_update_older=false) { if (is_null($tpl)) $tpl = $this; $cname = $tpl->cache_name(); $old = egw_cache::getInstance('etemplate',$cname); // only cache newest versions (currently cached one is older or same version) if (is_null($old) && !$only_update_older || !is_null($old) && version_compare($old['version'],$tpl->version,'<=')) { //error_log(__METHOD__."('$tpl->name','$tpl->template','$tpl->lang','$tpl->version') cached=".array2string($old).", modified=$tpl->modified, time()=".time()); egw_cache::setInstance('etemplate',$cname,$tpl->as_array(1)); } } /** * deletes the etemplate in the cache */ private function delete_in_cache() { //echo "

".__METHOD__."('$this->name','$this->template','$this->lang','$this->version')

\n"; egw_cache::unsetInstance('etemplate',$this->cache_name()); } static private $import_tested; /** * Test if new template-import necessary for app and does the import * * Get called on every read of a eTemplate, caches the result. * The timestamp of the last import for app gets written into the db. * * @param string|array $app app- or template-name * @return string translated message with number of templates imported */ static function test_import($app) // should be done from the setup-App { if (is_array($app)) $app = $app['name']; list($app) = explode('.',$app); if (is_null(self::$import_tested)) { self::$import_tested =& egw_cache::getSession('etemplate','import_tested'); if (is_null(self::$import_tested)) self::$import_tested = array(); } if (!$app || self::$import_tested[$app]) { return ''; // ensure test is done only once per session and app } self::$import_tested[$app] = True; // need to be done before new ... $path = EGW_SERVER_ROOT."/$app/setup/etemplates.inc.php"; if (($time = @filemtime($path))) { $templ = new boetemplate(array('name' => '.'.$app,'lang' => '##')); if ($templ->lang != '##' || $templ->version != $GLOBALS['egw_info']['apps'][$app]['version'] || $templ->modified < $time) // need to import { $templ->delete(); // delete old timestamp //echo "

".__METHOD__."($app) import necessary, as app-modified=$templ->modified < filemtime($path)=$time test-tpl=".array2string($templ->as_array(1))."

\n"; $ret = self::import_dump($app); $templ->modified = $time; // store new timestamp incl. app version $templ->save('.'.$app,'','##',0,$GLOBALS['egw_info']['apps'][$app]['version']); } } return $ret; } /** * Imports the dump-file /$app/setup/etempplates.inc.php unconditional (!) * * @param string $app app name * @return string translated message with number of templates imported */ static function import_dump($app) { $templ_version=0; include($path = EGW_SERVER_ROOT."/$app/setup/etemplates.inc.php"); $templ = new boetemplate($app); foreach($templ_data as $data) { if ((int)$templ_version < 1) // we need to stripslashes { $data['data'] = stripslashes($data['data']); } $templ->init($data); if (!$templ->modified) { $templ->modified = filemtime($path); } $templ->save(); $n++; } return lang("%1 new eTemplates imported for Application '%2'",$n,$app); } /** * reads the content of an eTemplate from the cache into the current object * * same as read but only via the cache * * @param string|array $name name of template or array('name'=>$name,'template'=>$template,'lang'=>$lang) * @param string $template template-set * @param string $lang language to use * @param int $group to use the template for * @param string $version of the template * @return boolean true if the eTemplate was found in the cache */ private function read_from_cache($name,$template='default',$lang='default',$group=0,$version='') { // check if new import necessary, currently on every request $msg = self::test_import($name); //if ($msg) echo "

".__METHOD__."($name,$template,$lang,$group,$version) self::test_import($name) returning $msg

\n"; if (is_array($name)) { $version = $name['version']; $lang = $name['lang']; $template = $name['template']; $name = $name['name']; } // templates starting with . (as the import timestamp) never use the cache if ($name[0] == '.') return false; $data = egw_cache::getInstance('etemplate',$cname=self::cache_name($name,$template,$lang)); //echo "

".__METHOD__.'('.array2string($name).",$template,$lang,$group,$version) egw_cache::getInstance('etemplate','$cname')=".array2string(array('name'=>$data['name'],'version'=>$data['version'],'modified'=>$data['modified']))."

\n"; if (!is_null($data) && (empty($version) || $data['version'] == $version)) { $this->init($data); return true; } return false; } /** * reads an eTemplate from the cache or database / filesystem (and updates the cache) * * reimplementation of soetemplate::read to use and/or update the cache * * We only update cache, if no version is given, as only that (= highest version) should be cached. * * @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 $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 * @param mixed $load_via name/array of keys of etemplate to load in order to get $name (only as second try!) * @return boolean True if a fitting template is found, else False */ function read($name,$template='default',$lang='default',$group=0,$version='',$load_via='') { if (is_array($name)) { $pname = &$name['name']; } else { $pname = &$name; } if (empty($pname)) { return False; } $parent = is_array($load_via) ? $load_via['name'] : $load_via; if (strstr($pname,'.') === False && !empty($parent)) { $pname = $parent . '.' . $pname; } if (!$this->read_from_cache($name,$template,$lang,$group,$version)) { if (!parent::read($name,$template,$lang,$group,$version)) { if ($load_via && (is_string($load_via) || !isset($load_via['tpls_in_file']) || $load_via['tpls_in_file'] > 1)) { parent::read($load_via); return $this->read_from_cache($name,$template,$lang,$group,$version); } return False; } if (!$version) $this->store_in_cache(); // only store default version (none given) } return True; } /** * saves eTemplate-object to db and update the cache * * reimplementation of soetemplate::save to update the cache * * We only update cache, if it contains an older (smaller version) template, * as allways updating might cache a version, with a smaller then the highest, version number. * * @param string $name name of the eTemplate or array with the values for all keys * @param string $template template-set or '' for the default one * @param string $lang language or '' for the default one * @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 * @return the number of affected rows, 1 should be ok, 0 somethings wrong */ function save($name='',$template='.',$lang='.',$group=0,$version='.') { if ($result = parent::save($name,$template,$lang,$group,$version)) { $this->store_in_cache(null, true); // true = only update older (smaller version) copies } return $result; } /** * Deletes the eTemplate from the db, object itself is unchanged * * reimplementation of soetemplate::delete to update the cache * * @return int number of affected rows, 1 should be ok, 0 somethings wrong */ function delete() { $this->delete_in_cache(); return parent::delete(); } /** * initialise our static vars */ static function _init_static() { } } boetemplate::_init_static(); if (!function_exists('set_cell_attribute_helper')) { function &set_cell_attribute_helper(&$widget,&$extra) { // extra = array(0=>n,1=>name,2=>attr,3=>value) if ($widget['name'] == $extra[1] || $widget['type'] == 'tab' && strpos($widget['name'], $extra[1].'=') === 0) { if (is_null($extra[3])) { $extra['__RETURN_NOW__'] = true; // wouldnt work otherwise, if attr is not yet set == null return $widget[$extra[2]]; } $widget[$extra[2]] = $extra[3]; ++$extra[0]; } } function &get_widget_by_name_helper(&$widget,$extra) { if ($widget['name'] == $extra) return $widget; } function &get_widget_by_path_helper(&$widget,$extra,$path) { //echo "

path_searched='$extra', widget-path($widget[type]:$widget[name])='$path'

\n"; if ($path == $extra) return $widget; } function &get_widgets_by_type_helper(&$widget, &$extra) { //echo '
get_widgets_by_type_helper(' . $widget['name'] . ',' . $extra['type'] . ')
'; if($widget['type'] == $extra['type']) { $extra['widgets'][] =& $widget; } } } // just in case someone still uses the old var if (!is_array($GLOBALS['egw_info']['flags']['etemplate'])) $GLOBALS['egw_info']['flags']['etemplate'] = array(); $GLOBALS['egw_info']['flags']['etemplate']['loop'] =& boetemplate::$loop;