egroupware/etemplate/inc/class.xul_io.inc.php

813 lines
22 KiB
PHP

<?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);
}
}
}
}