<?php /** * EGroupware - eTemplates - XUL/XML Import & Export * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @link http://www.egroupware.org * @author Ralf Becker <RalfBecker@outdoor-training.de> * @copyright 2002-11 by RalfBecker@outdoor-training.de * @version $Id$ */ if (!function_exists('var2xml')) { if (file_exists(EGW_API_INC.'class.xmltool.inc.php')) { include_once(EGW_API_INC.'class.xmltool.inc.php'); } else { include_once('class.xmltool.inc.php'); } } /** * XUL/XML Import & Export for eTemplates * * used only internaly */ class xul_io { /** * translate attr, common to all widgets * * @var array */ var $attr2xul = array( 'name' => 'id', 'help' => 'statustext', 'span' => 'span,class', 'type' => '', // this is the widget-name => dont write as attr 'disabled' => 'disabled=true', 'readonly' => 'readonly=true', 'size' => 'options' ); /** * translate widget-names and widget-spec. attr., not set ones are identical * * @var array */ var $widget2xul = array( 'label' => array( '.name' => 'description', 'label' => 'value', 'size' => 'font_style,href,activate_links,for,extra_link_target,extra_link_popup,extra_link_title', ), 'text' => array( '.name' => 'textbox', 'size' => 'size,maxlength,validator' ), 'textarea' => array( '.name' => 'textbox', '.set' => 'multiline=true', 'size' => 'rows,cols' ), 'integer' => array( '.name' => 'textbox', '.set' => 'type=integer', 'size' => 'min,max,size,precision,step' ), 'int' => array( '.name' => 'textbox', '.set' => 'type=integer', 'size' => 'min,max,size,precision,step' ), 'float' => array( '.name' => 'textbox', '.set' => 'type=float', 'size' => 'min,max,size,precision,step' ), 'select' => array( '.name' => 'menulist,menupopup', ), 'select-multi' => array( // multiselection, if size > 0 '.name' => 'listbox', 'size' => 'rows,options' ), 'template' => array( '.name' => 'template', 'size' => 'content' ), 'image' => array( '.name' => 'image', 'name' => 'src', 'size' => 'href,extra_link_target,imagemap,extra_link_popup,id', ), 'progres' => array( '.name' => 'progress', 'size' => 'href,extra_link_target,,extra_link_popup', ), 'tab' => array( '.name' => 'tabbox,tabs,tabpanels' ), 'button' => array( '.name' => 'button', 'size' => 'image,ro_image' ), 'htmlarea' => array( 'size' => 'mode,height,width,expand_toolbar,base_href', ), 'nextmatch' => array( 'size' => 'template,hide_header,header_left,header_right', ), ); /** * translate xul-widget names to our internal ones, not set ones are identical * * @var array */ var $xul2widget = array( 'menulist' => 'select', 'listbox' => 'select', 'menupopup' => 'select', 'description' => 'label' ); /** * explicit whitelist for certain attributes and widget types */ var $attr_whitelist = array( 'rows' => array('textbox'), 'cols' => array('textbox'), ); /** * Keys of currently processed template on export, to resolve relative names * * @var array */ var $load_via; /** * sets an attribute in the xml object representing a widget * * @param object &$widget widget to set the attribute in * @param string $attr comma delimited attr = default-value pairs, eg. "type=int,min=0" * @param array $val array with values to set */ function set_attributes(&$widget,$attr,$val) { if ($attr != '' && !is_numeric($attr)) { $attrs = explode(',',$attr); if (count($attrs)) { $vals = count($attrs) > 1 ? explode(',',$val,count($attrs)) : array($val); foreach($attrs as $n => $attr) { if (($val = $vals[$n]) != '') { list($attr,$set) = explode('=',$attr); $widget->set_attribute($attr,$set != '' ? $set : $val); } } } } } /** * add a widget to a parent * * @param object &$parent parten to add the widget * @param array $cell widget to add * @param array &$embeded_too already embeded eTemplates * @return object reference (!) the the xml object representing the widget, so other children can be added */ function &add_widget(&$parent,$cell,&$embeded_too) { // sort attributes, to stop xet files from changing because of changed attribute order ksort($cell, SORT_STRING); $type = $cell['type']; if (is_array($type)) { list(,$type) = each($type); } if (!$type) $cell['type'] = $type = 'unknown'; if (substr($type,0,6) == 'select') { $type = $cell['size'] > 1 ? 'select-multi' : 'select'; } $widgetattr2xul = isset($this->widget2xul[$type]) ? $this->widget2xul[$type] : array(); $type = isset($widgetattr2xul['.name']) ? $widgetattr2xul['.name'] : $type; list($type,$child,$child2) = explode(',',$type); $widget = new xmlnode($type); $attr_widget = &$widget; if ($child) { $child = new xmlnode($child); if ($type != 'tabbox') $attr_widget = &$child; } if ($child2) { $child2 = new xmlnode($child2); } if (isset($widgetattr2xul['.set'])) // set default-attr for type { $attrs = explode(',',$widgetattr2xul['.set']); foreach($attrs as $attr) { list($attr,$val) = explode('=',$attr); $widget->set_attribute($attr,$val); } } switch ($type) { case 'nextmatch': $tpls = $cell['size'] = explode(',', $cell['size']); // template,hide_header,header_left,header_right unset($tpls[1]); // hide_header is no template foreach($tpls as $n => $tpl) { if (empty($tpl)) continue; $embeded = new boetemplate($tpl,$this->load_via); if ($embeded_too) { $this->add_etempl($embeded,$embeded_too); } $cell['size'][$n] = $embeded->name; unset($embeded); } $cell['size'] = implode(',', $cell['size']); break; case 'tabbox': $labels = explode('|',$cell['label']); unset($cell['label']); $helps = explode('|',$cell['help']); unset($cell['help']); if (strpos($tab_names=$cell['name'],'=') !== false) { list($cell['name'],$tab_names) = explode('=',$cell['name']); } $names = explode('|',$tab_names); for ($n = 0; $n < count($labels); ++$n) { $tab = new xmlnode('tab'); $tab->set_attribute('id',$names[$n]); $tab->set_attribute('label',$labels[$n]); if ($helps[$n]) $tab->set_attribute('statustext',$helps[$n]); $child->add_node($tab); $embeded = new boetemplate($names[$n],$this->load_via); if ($embeded_too) { $this->add_etempl($embeded,$embeded_too); } $template = new xmlnode('template'); $template->set_attribute('id',$embeded->name); $child2->add_node($template); unset($embeded); unset($template); } break; case 'menulist': // id,options belongs to the 'menupopup' child if ($cell['span']) { list($span, $class) = explode(',', $cell['span']); if (!empty($span)) $this->set_attributes($widget, 'span', $span); if (!empty($class)) { $cell['span'] = ','.$class; } else { unset($cell['span']); } } // fall-through case 'listbox': if ($cell['type'] != 'select') // one of the sub-types { $attr_widget->set_attribute('type',$cell['type']); } break; case 'groupbox': if ($cell['label']) { $caption = new xmlnode('caption'); $caption->set_attribute('label',$cell['label']); $widget->add_node($caption); unset($cell['label']); } // fall-through case 'split': case 'vbox': case 'hbox': case 'box': case 'deck': list($anz,$orient,$options) = explode(',',$cell['size'],3); for ($n = 1; $n <= $anz; ++$n) { $this->add_widget($widget,$cell[$n],$embeded_too); unset($cell[$n]); } // no sure where the data key gets set, but it gives a warning in xml serialization (empty array) unset($cell['data']); if (!empty($orient)) $cell['orient'] = $orient; $cell['size'] = $options; break; case 'template': if ($cell['name'][0] != '@' && $embeded_too) { $templ = new boetemplate(); if ($templ->read(boetemplate::expand_name($cell['name'],0,0),'default','default',0,'',$this->load_via)) { $this->add_etempl($templ,$embeded_too); } unset($templ); } break; case 'grid': $this->add_grid($parent,$cell,$embeded_too); return; // grid is already added } foreach($cell as $attr => $val) { if (is_array($val)) // correct old buggy etemplates { list(,$val) = each($val); } if (isset($widgetattr2xul[$attr])) { $attr = $widgetattr2xul[$attr]; } elseif (isset($this->attr2xul[$attr])) { $attr = $this->attr2xul[$attr]; } // check if attribute has an explicit whitelist and widget type is in it if (isset($this->attr_whitelist[$attr]) && !in_array($type, $this->attr_whitelist[$attr])) { continue; } $this->set_attributes($attr_widget,$attr,$val); } if ($child) { $widget->add_node($child); } if ($child2) { $widget->add_node($child2); } $parent->add_node($widget); } /** * add a grid to $parent (xml object) * * @param object &$parent where to add the grid * @param array $grid grid to add * @param array &embeded_too array with already embeded eTemplates */ function add_grid(&$parent,$grid,&$embeded_too) { $xul_grid = new xmlnode('grid'); $this->set_attributes($xul_grid,'width,height,border,class,spacing,padding,overflow',$grid['size']); $this->set_attributes($xul_grid,'id',$grid['name']); $xul_columns = new xmlnode('columns'); $xul_rows = new xmlnode('rows'); reset($grid['data']); list(,$opts) = each ($grid['data']); // read over options-row while (list($r,$row) = each ($grid['data'])) { $xul_row = new xmlnode('row'); $this->set_attributes($xul_row,'class,valign',$opts["c$r"]); $this->set_attributes($xul_row,'height,disabled,part',$opts["h$r"]); $spanned = 0; foreach($row as $c => $cell) { if ($r == '1') // write columns only once in the first row { $xul_column = new xmlnode('column'); $this->set_attributes($xul_column,'width,disabled',$opts[$c]); $xul_columns->add_node($xul_column); } if ($spanned-- > 1) { continue; // spanned cells are not written } $this->add_widget($xul_row,$cell,$embeded_too); $spanned = $cell['span'] == 'all' ? 999 : $cell['span']; } $xul_rows->add_node($xul_row); } $xul_grid->add_node($xul_columns); $xul_grid->add_node($xul_rows); $parent->add_node($xul_grid); } /** * add / embed an eTemplate into the global $xul_overlay object (used by export) * * @param boetemplate &$etempl eTemplate to embed * @param array &embeded_too array with already embeded templates */ function add_etempl(boetemplate $etempl,&$embeded_too) { if (is_array($embeded_too)) { if (isset($embeded_too[$etempl->name])) { return; // allready embeded } } else { $embeded_too = array(); } $embeded_too[$etempl->name] = True; $template = new xmlnode('template'); $template->set_attribute('id',$etempl->name); $template->set_attribute('template',$etempl->template); $template->set_attribute('lang',$etempl->lang); $template->set_attribute('group',$etempl->group); $template->set_attribute('version',$etempl->version); $backup_load_via = $this->load_via; $this->load_via = $etempl->as_array(-1); foreach($etempl->children as $child) { $this->add_widget($template,$child,$embeded_too); } $this->load_via = $backup_load_via; if ($etempl->style != '') { $styles = new xmlnode('styles'); $styles->set_value(str_replace("\r",'',$etempl->style)); $template->add_node($styles); } $this->xul_overlay->add_node($template); } /** * create an XML representation of an eTemplate * * @param etemplate $etempl eTemplate object to export * @return string the XML */ function export($etempl) { if ($this->debug) { echo "<p>etempl->data = "; _debug_array($etempl->data); } $doc = new xmldoc(); $doc->add_comment('$'.'Id$'); $this->xul_overlay = new xmlnode('overlay'); // global for all add_etempl calls $embeded_too = True; $this->add_etempl($etempl,$embeded_too); $doc->add_root($this->xul_overlay); $xml = $doc->export_xml(); if ($this->debug) { echo "<pre>\n" . htmlentities($xml) . "\n</pre>\n"; } return $xml; } /** * create an eTemplate from it's XML representation * * @param object &$etempl eTemplate object to set * @param string $data the XML * @return array/string array with names of imported templates or error-message */ function import(&$etempl,$data) { if ($this->debug) { echo "<pre>\n" . htmlentities($data) . "\n</pre><p>\n"; } $parser = xml_parser_create(); xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0); xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1); $vals = $index = ''; $ok = xml_parse_into_struct($parser, $data, $vals, $index); if (!$ok || !is_array($vals)) { $err = 'Error Line '.xml_get_current_line_number($parser).', Column '.xml_get_current_column_number($parser). ': '.xml_error_string(xml_get_error_code($parser)); } xml_parser_free($parser); if ($err != '') { return $err; } $parents = array(); $parent = null; foreach($vals as $n => $node) { if ($this->debug) { echo "<h1>$n</h1><pre>".print_r($node,true)."</pre>"; } $type = $node['type']; $tag = $node['tag']; $attr = is_array($node['attributes']) ? $node['attributes'] : array(); if ($attr['id']) { $attr['name'] = $attr['id']; unset($attr['id']); } if (isset($attr['options']) && $attr['options'] != '') { $attr['size'] = $attr['options']; unset($attr['options']); } if ($tag != 'textbox' && !isset($attr['type'])) { $attr['type'] = $this->xul2widget[$tag] ? $this->xul2widget[$tag] : $tag; } if ($this->debug) { echo "<p>$node[level]: $tag/$type: value='$node[value]' attr=\n"; _debug_array($attr); } switch ($tag) { case 'overlay': break; case 'template': case 'grid': if ($type != 'open' && is_array($tab_attr)) // templates/grids in a tabpanel { $tab_names[] = $attr['name']; break; } if ($tag == 'template' && $type != 'complete' && $node['level'] > 2) // level 1 is the overlay { return "Can't import nested $tag's !!!"; } switch ($type) { case 'close': if (!count($parents)) // templ import complet => save it { unset($parent); $parents = array(); $etempl->fix_old_template_format(); // set the depricated compat vars // save tmpl to the cache, as the file may contain more then one tmpl $cname = ($etempl->template == '' ? 'default' : $etempl->template).'/'.$etempl->name. ($etempl->lang == '' ? '' : '.'.$etempl->lang); boetemplate::store_in_cache($etempl); if ($this->debug) { $etempl->echo_tmpl(); } $imported[] = $etempl->name; } else { // poping the last used parent from the end of the parents array (array_pop does not work with references) $parent = &$parents[count($parents)-1]; unset($parents[count($parents)-1]); } break; case 'open': if (($is_root = is_null($parent))) // starting a new templ { $etempl->init($attr); $etempl->children = array(); // init adds one grid by default $parent = &$etempl; // parent is the template-object itself! } if ($tag == 'grid') { $size = ''; foreach(array('overflow','padding','spacing','class','border','height','width') as $opt) { $size = $attr[$opt] . ($size != '' ? ",$size" : ''); } $grid = array( // empty grid 'type' => 'grid', 'name' => $attr['name'], 'data' => array(), 'cols' => 0, 'rows' => 0, 'size' => $size, ); soetemplate::add_child($parent,$grid); $parents[count($parents)] = &$parent; $parent = &$grid; unset($grid); } break; case 'complete': // reference to an other template $attr['type'] = 'template'; // might be grid in old xet-files soetemplate::add_child($parent,$attr); unset($attr); break; } break; case 'columns': case 'rows': break; case 'column': if ($type != 'complete') { return 'place widgets in <row> and not in <column> !!!'; } $parent['data'][0][$etempl->num2chrs($parent['cols']++)] = $attr['width'] . ($attr['disabled'] ? ','.$attr['disabled'] : ''); break; case 'row': if ($type != 'open') { break; } $nul = null; soetemplate::add_child($parent,$nul); // null = new row $parent['data'][0]['c'.$parent['rows']] = $attr['class'] . ($attr['valign'] ? ','.$attr['valign'] : ''); $parent['data'][0]['h'.$parent['rows']] = $attr['height'] . ($attr['disabled']||$attr['part'] ? ','.$attr['disabled'] : ''). ($attr['part'] ? ','.$attr['part'] : ''); break; case 'styles': $etempl->style = trim($node['value']); break; case 'tabbox': if ($type == 'open') { $tab_labels = $tab_helps = $tab_names = array(); $tab_attr = $attr; } else { $tab_attr['type'] = 'tab'; $tab_attr['label'] = implode('|',$tab_labels); $tab_attr['name'] = implode('|',$tab_names); $tab_attr['help'] = implode('|',$tab_helps); $tab_attr['span'] .= $tab_attr['class'] ? ','.$tab_attr['class'] : ''; unset($tab_attr['class']); soetemplate::add_child($parent,$tab_attr); unset($tab_attr); } break; case 'tabs': case 'tabpanels': break; case 'tab': if ($type != 'close') { $tab_labels[] = $attr['label']; $tab_helps[] = $attr['statustext']; } break; case 'menupopup': if (is_array($menulist_attr)) { $attr['help'] = $attr['statustext']; unset($attr['statustext']); unset($menulist_attr['type']); $menulist_attr += $attr; } break; case 'menulist': if ($type == 'open') { $menulist_attr = $attr; } else { soetemplate::add_child($parent,$menulist_attr); unset($menulist_attr); } break; case 'split': case 'vbox': case 'hbox': case 'deck': case 'groupbox': case 'box': if ($type != 'close') // open or complete { $attr['size'] = '0'.($attr['orient'] || $attr['size'] ? ','.$attr['orient']. ($attr['size'] ? ','.$attr['size'] : '') : ''); $attr['span'] .= $attr['class'] ? ','.$attr['class'] : ''; unset($attr['class']); soetemplate::add_child($parent,$attr); $parents[count($parents)] = &$parent; // $parents[] does not always the same - strange $parent = &$attr; unset($attr); } if ($type != 'open') // close or complete { // poping the last used parent from the end of the parents array (array_pop does not work with references) $parent = &$parents[count($parents)-1]; unset($parents[count($parents)-1]); } break; case 'caption': // caption of (group)box if ($parent['type'] == 'groupbox') { $parent['label'] = $attr['label']; } break; // the following labels create automaticaly a child-entry in their parent case 'textbox': if ($attr['multiline']) { unset($attr['multiline']); $attr['type'] = 'textarea'; $attr['size'] = $attr['rows'] . ($attr['cols'] ? ','.$attr['cols'] : ''); unset($attr['cols']); unset($attr['rows']); } elseif ($attr['type']) // integer,float { $attr['size'] = $attr['min'] . ($attr['max'] ? ','.$attr['max'] : ($attr['size'] ? ',':'')) . ','.$attr['size']; unset($attr['min']); unset($attr['max']); } else // input { $attr['type'] = 'text'; $attr['size'] .= $attr['maxlength']!='' ? ','.$attr['maxlength'] : ''; unset($attr['maxlength']); } // fall-through default: switch ($tag) { case 'description': case 'label': $attr['label'] = $attr['value']; unset($attr['value']); break; case 'template': $attr['size'] = $attr['content']; unset($attr['content']); break; case 'image': $attr['name'] = $attr['src']; unset($attr['src']); $this->set_legacy_options($tag, $attr); break; case 'listbox': $attr['size'] = preg_replace('/,*$/','',$attr['rows'].','.$attr['size']); unset($attr['rows']); break; case 'button': if ($attr['image'] || $attr['ro_image']) { $attr['size'] = $attr['image'] . ($attr['ro_image'] ? ','.$attr['ro_image'] : ''); unset($attr['image']); unset($attr['ro_image']); } break; case 'nextmatch': $this->set_legacy_options($tag, $attr); break; } $attr['help'] = $attr['statustext']; unset($attr['statustext']); $attr['span'] .= $attr['class'] ? ','.$attr['class'] : ''; unset($attr['class']); if ($type == 'close') { break; } soetemplate::add_child($parent,$attr); unset($attr); break; } if ($this->debug) { echo "<b>parent</b><pre>".print_r($parent,true)."</pre>"; echo "<b>parents</b><pre>".print_r($parents,true)."</pre>"; echo "<b>children</b><pre>".print_r($etempl->children,true)."</pre>"; } } return $imported; } /** * re-assemble legacy options in "size" attribute * * @param string $tag * @param array &$attr */ function set_legacy_options($tag, &$attr) { // re-assemble legacy options in "size" attribute if (empty($attr['size']) && $this->widget2xul[$tag]['size']) { foreach(explode(',', $this->widget2xul[$tag]['size']) as $l_attr) { $attr['size'] .= ($attr['size'] ? ',' : '').$attr[$l_attr]; unset($attr[$l_attr]); } while(substr($attr['size'], -1) == ',') { $attr['size'] = substr($attr['size'], 0, -1); } } } }