* @copyright 2002-13 by RalfBecker@outdoor-training.de * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage tools * @version $Id$ */ /** * template editor of the eTemplate package */ class editor { var $debug; /** * eTemplate we edit * * @var etemplate */ var $etemplate; /** * editor eTemplate * * @var etemplate */ var $editor; var $aligns = array( '' => 'Left', 'right' => 'Right', 'center' => 'Center', ); var $valigns = array( '' => 'Middle', 'top' => 'Top', 'bottom' => 'Bottom', 'baseline' => 'Baseline', ); var $parts = array( '' => 'Body', 'header' => 'Header', 'footer' => 'Footer', ); var $edit_menu = array( 'delete' => 'delete', 'cut' => 'cut', 'copy' => 'copy', 'paste' => 'paste', 'swap' => 'swap', ); var $row_menu = array( 'row_delete' => 'delete this row', 'row_insert_above' => 'insert a row above', 'row_insert_below' => 'insert a row below', 'row_swap_next' => 'swap with next row', ); var $column_menu = array( 'column_delete' => 'delete this column', 'column_insert_before' => 'insert a column before', 'column_insert_behind' => 'insert a column behind', 'column_swap_next' => 'swap with next column', ); var $box_menu = array( 'box_insert_before' => 'insert a widget before', 'box_insert_behind' => 'insert a widget behind', 'box_swap_next' => 'swap widget with next one', ); var $options = array( 'width', 'height', 'border', 'class', 'spacing', 'padding', 'overflow' ); var $overflows = array( '' => 'visible', 'hidden' => 'hidden', 'scroll' => 'scroll', 'auto' => 'auto' ); var $onclick_types = array( '' => 'nothing', 'confirm' => 'confirm', 'popup' => 'popup', 'custom' => 'custom', ); var $onchange_types = array( '' => 'nothing', 'submit' => 'submit form', 'custom' => 'custom', ); var $extensions = ''; var $public_functions = array ( 'edit' => True, 'widget' => True, 'styles' => True, ); function __construct() { $this->etemplate = new etemplate(); $this->extensions = $GLOBALS['egw']->session->appsession('extensions','etemplate'); } function export_xml(&$xml,&$xml_label) { $name = $this->etemplate->name; $template = $this->etemplate->template != '' ? $this->etemplate->template : 'default'; list($app) = explode('.',$name); if (!is_object($this->etemplate->xul_io)) { $this->etemplate->xul_io = new xul_io(); } $xml = $this->etemplate->xul_io->export($this->etemplate); $dir = EGW_SERVER_ROOT . "/$app/templates/$template"; if (($create_it = !is_dir($dir))) { $dir = EGW_SERVER_ROOT . "/$app/templates"; } if (!is_writeable($dir)) { // if dir is not writable, download file html::content_header($name.'.xet','application/xml',bytes($xml)); echo $xml; common::egw_exit(); //return lang("Error: webserver is not allowed to write into '%1' !!!",$dir); } if ($create_it) { mkdir($dir .= "/$template"); } $file = $dir . '/' . substr($name,strlen($app)+1); if ($this->etemplate->lang) { $file .= '.' . $this->etemplate->lang; } $old_file = $file . '.old.xet'; $file .= '.xet'; if (file_exists($file)) { if (file_exists($old_file)) { unlink($old_file); } rename($file,$old_file); } if (!($f = fopen($xml_label=$file,'w'))) { return 0; } if (!is_object($this->etemplate->xul_io)) { $this->etemplate->xul_io = new xul_io(); } $xml = $this->etemplate->xul_io->export($this->etemplate); fwrite($f,$xml); fclose($f); return lang("eTemplate '%1' written to '%2'",$name,$file); } function import_xml($file,&$xml) { if ($file == 'none' || $file == '' || !($f = fopen($file,'r'))) { return lang('no filename given or selected via Browse...')."file='$file'"; } $xml = fread ($f, filesize ($file)); fclose($f); if (!is_object($this->etemplate->xul_io)) { $this->etemplate->xul_io = new xul_io(); } $imported = $this->etemplate->xul_io->import($this->etemplate,$xml); $this->etemplate->modified = @filemtime($f); $this->etemplate->modified_set = 'xul-import'; if (is_array($imported)) { if (count($imported) == 1) { $imported = lang("eTemplate '%1' imported, use Save to put it in the database",$this->etemplate->name); } else { $imported = lang('File contains more than one eTemplate, last one is shown !!!'); } } return $imported; } function list_result($cont='',$msg='') { if ($this->debug) { echo "

etemplate.editor.list_result: cont="; _debug_array($cont); } if (!$cont || !is_array($cont)) { return $this->edit('error'); } if (!isset($cont['result']) || isset($cont['search'])) { $cont['result'] = $this->etemplate->search($cont); } $result = $cont['result']; if (isset($cont['delete'])) { list($delete) = each($cont['delete']); $this->etemplate->init($result[$delete-1]); if ($this->etemplate->delete()) { $msg = lang('Template deleted'); unset($result[$delete-1]); $result = array_values($result); } else { $msg = lang('Error: Template not found !!!'); } } if (isset($cont['delete_selected'])) { foreach($cont['selected'] as $row => $sel) { if ($sel) { $this->etemplate->init($result[$row-1]); if ($this->etemplate->delete()) { unset($result[$row-1]); ++$n; } } } if ($n) { $msg = lang('%1 eTemplates deleted',$n); } unset($cont['selected']); unset($cont['delete_selected']); $result = array_values($result); } if (isset($cont['read'])) { list($read) = each($cont['read']); $this->etemplate->read($result[$read-1]); $this->edit(); return; } if (!$msg) { $msg = lang('%1 eTemplates found',count($result)); } unset($cont['result']); if (!isset($cont['name'])) { $cont += $this->etemplate->as_array(); } $content = $cont + array('msg' => $msg); reset($result); for ($row=1; list(,$param) = each($result); ++$row) { $content[$row] = $param; } $list_result = new etemplate('etemplate.editor.list_result'); $GLOBALS['egw_info']['flags']['app_header'] = lang('Editable Templates - Search'); $list_result->exec('etemplate.editor.list_result',$content,'','',array( 'result' => $result, ),''); } /** * new eTemplate editor, which edits widgets in a popup * * @param array $content content from the process_exec call * @param string $msg message to show */ function edit($content=null,$msg = '') { if ($this->debug) { echo "

etemplate.editor.edit: content="; _debug_array($content); } if (!is_array($content)) $content = array(); $preserv = array(); if ($content['import_xml']) { $msg .= $this->import_xml($content['file']['tmp_name'],$xml); //$this->etemplate->echo_tmpl(); $xml_label = $content['file']['name']; $preserv['import'] = $this->etemplate->as_array(1); } elseif (is_array($content['import']) && !$content['read']) // imported not yet saved tmpl { $this->etemplate->init($content['import']); $preserv['import'] = $content['import']; } if ($content['save']) { if (!is_array($content['import'])) $this->etemplate->read($content['old_keys']); if (!$this->etemplate->modified_set || !$this->etemplate->modified) { $this->etemplate->modified = time(); } $ok = $this->etemplate->save(trim($content['name']),trim($content['template']),trim($content['lang']),(int) $content['group'],trim($content['version'])); $msg = $ok ? lang('Template saved') : lang('Error: while saving !!!'); if ($ok) unset($preserv['import']); } elseif (!$content['import_xml'] && (isset($_GET['name']) || isset($content['name'])) && !$content['restore']) { if ($_GET['name']) { foreach(boetemplate::$db_key_cols as $var) { $content[$var] = $_GET[$var]; } } if ($content['version'] != '') { $save_version = $content['version']; unset($content['version']); $this->etemplate->read($content); $newest_version = $this->etemplate->version; $content['version'] = $save_version; } if (!$this->etemplate->read($content)) { if (isset($content['name'])) { $version_backup = $content['version']; $content['version'] = ''; // trying it without version if ($this->etemplate->read($content)) { $msg = lang('only an other Version found !!!'); } else { $result = $this->etemplate->search($content); if (count($result) > 1) { return $this->list_result(array('result' => $result)); } elseif (!count($result) || !$this->etemplate->read($result[0])) { $msg = lang('Error: Template not found !!!'); $this->etemplate->version = $content['version'] = $version_backup; } elseif ($content['name'] == $result[0]['name']) { $msg = lang('only an other Version found !!!'); } } } else { $msg = lang('Error: Template not found !!!'); } } elseif ($newest_version != '' && $this->etemplate->version != $newest_version) { $link = $this->etemplate->as_array(-1); $link['menuaction'] = 'etemplate.editor.edit'; $link['version'] = $newest_version; $msg = lang("newer version '%1' exists !!!",html::a_href($newest_version,$link)); } } if (!is_array($this->extensions)) { if (($extensions = $this->scan_for_extensions())) { $msg .= lang('Extensions loaded:') . ' ' . $extensions."\n"; $msg_ext_loaded = True; } } list($app) = explode('.',$this->etemplate->name?$this->etemplate->name:$content['name']); if ($app && $app != 'etemplate') { translation::add_app($app); // load translations for app if (($extensions = $this->scan_for_extensions($app))) { $msg .= (!$msg_ext_loaded?lang('Extensions loaded:').' ':', ') . $extensions."\n"; } } if (!$msg && $content['delete']) { if (!$content['version'] && $this->etemplate->version) { $this->etemplate->version = ''; // else the newest would get deleted and not the one without version } $ok = $this->etemplate->delete(); $msg = $ok ? lang('Template deleted') : lang('Error: Template not found !!!'); $preserv['import'] = $this->etemplate->as_array(1); // that way the content can be saved again } elseif ($content['restore']) { if (empty($app) || !@is_dir(EGW_SERVER_ROOT.'/'.$app)) { $msg .= lang('Application name needed to restore eTemplates!'); } elseif (!@file_exists(EGW_SERVER_ROOT.'/'.($file=$app.'/setup/etemplates.inc.php'))) { $msg .= lang('Application has no eTemplates (no file %1) to restore!',$file); } else { $msg .= $this->etemplate->import_dump($app); } } elseif ($content['dump']) { if (empty($app) || !@is_dir(EGW_SERVER_ROOT.'/'.$app)) { $msg .= lang('Application name needed to write a langfile or dump the eTemplates !!!'); } else { $msg .= $this->etemplate->dump4setup($app); } } elseif ($content['langfile']) { if (empty($app) || !@is_dir(EGW_SERVER_ROOT.'/'.$app)) { $msg = lang('Application name needed to write a langfile or dump the eTemplates !!!'); } else { $additional = array(); if ($app == 'etemplate') { $additional = boetemplate::$types + $this->extensions + $this->aligns + $this->valigns + $this->edit_menu + $this->box_menu + $this->row_menu + $this->column_menu + $this->onclick_types + $this->onchange_types; } else // try to call the writeLangFile function of the app's ui-layer { foreach(array('ui'.$name,'ui',$name,'bo'.$name) as $class) { if (file_exists(EGW_INCLUDE_ROOT.'/'.$name.'/inc/class.'.$class.'.inc.php') && ($ui =& CreateObject($name.'.'.$class)) && is_object($ui)) { break; } } if (is_object($ui) && @$ui->public_functions['writeLangFile']) { $msg = "$class::writeLangFile: ".$ui->writeLangFile(); } unset($ui); } //if (empty($msg)) { $msg = $this->etemplate->writeLangFile($app,'en',$additional); } } } elseif ($content['export_xml']) { $msg .= $this->export_xml($xml,$xml_label); } $new_content = $this->etemplate->as_array() + array( 'msg' => $msg, 'xml_label' => $xml_label, 'xml' => $xml ? '

'.html::htmlspecialchars($xml)."
\n" : '', ); $editor = new etemplate('etemplate.editor.new'); if (isset($content['values']) && !isset($content['vals'])) { $r = 1; foreach((array)$content['cont'] as $key => $val) { $vals["@$r"] = $key; $vals["A$r"] = is_array($val) ? serialize($val).'#SeR#' : $val; ++$r; } $editor->data[$editor->rows]['A']['name'] = 'etemplate.editor.values'; $editor->data[$editor->rows]['A']['size'] = 'vals'; $new_content['vals'] = $vals; } else { // set onclick handler $this->etemplate->onclick_handler = "edit_widget('%p');"; // setting the javascript via the content, allows looping too $new_content['onclick'] = ' '; if ($app != 'etemplate' && file_exists(EGW_SERVER_ROOT.'/'.$app.'/templates/default/app.css')) { $new_content['onclick'] .= html::style('@import url('.$GLOBALS['egw_info']['server']['webserver_url'].'/'.$app.'/templates/default/app.css);'); } // check if application of template has a app.js file --> load it if (file_exists(EGW_SERVER_ROOT.'/'.$app.'/js/app.js')) { egw_framework::validate_file('.','app',$app,false); } $editor->data[$editor->rows]['A']['obj'] = &$this->etemplate; $vals = $content['vals']; $olds = $content['olds']; for ($r = 1; isset($vals["A$r"]); ++$r) { $new_content['cont'][$olds["@$r"]] = substr($vals["A$r"],-5)=='#SeR#' ? unserialize(substr($vals["A$r"],0,-5)) : $vals["A$r"]; } } // check if bo class of $app exists and has a labels method --> add labels to content to display them in the editor if(class_exists($app.'_bo') && method_exists($app.'_bo','labels')) { $new_content['labels'] = ExecMethod($acm=$app.'.'.$app.'_bo.labels'); $new_content['cont']['labels'] =& $new_content['labels']; //echo $acm; _debug_array($new_content['labels']); } $preserv['olds'] = $vals; $preserv['old_keys'] = $this->etemplate->as_array(-1); // in case we do a save as $GLOBALS['egw_info']['flags']['app_header'] = lang('Editable Templates - Show Template'); $editor->exec('etemplate.editor.edit',$new_content,array(),'',$preserv,0,'/^cont/'); } /** * initialises the children arrays for the new widget type, converts boxes <--> grids * * @internal * @param array &$widget reference to the new widget data * @param array $old the old widget data */ function change_widget_type(&$widget,$old) { //echo "

editor::change_widget_type($widget[type]=$old[type])

\n"; $old_type = $old['type']; $old_had_children = isset(boetemplate::$widgets_with_children[$old_type]); if (!isset(boetemplate::$widgets_with_children[$widget['type']]) || ($old_type == 'grid') == ($widget['type'] == 'grid')) { if (boetemplate::$widgets_with_children[$widget['type']] == 'box') // box { if ((int) $widget['size'] < 1) // min. 1 child { list(,$options) = explode(',',$widget['size'],2); $widget['size'] = '1'.($options ? ','.$options : ''); } // create the needed cells, if they dont exist for ($n = 1; $n <= (int) $widget['size']; ++$n) { if (!is_array($widget[$n])) $widget[$n] = $n == 1 ? $old : boetemplate::empty_cell(); } } return; // no change necessary, eg. between different box-types } switch (boetemplate::$widgets_with_children[$widget['type']]) { case 'grid': $widget['data'] = array(array()); $widget['cols'] = $widget['rows'] = 0; if ($old_had_children) // box --> grid: hbox --> 1 row, other boxes --> 1 column { list($num) = explode(',',$old['size']); for ($n = 1; is_array($old[$n]) && $n <= $num; ++$n) { $new_line = null; if ($old_type != 'hbox') boetemplate::add_child($widget,$new_line); boetemplate::add_child($widget,$old[$n]); unset($widget[$n]); } $widget['size'] = ''; } else // 1 row with 1 column/child { boetemplate::add_child($widget,$cell=boetemplate::empty_cell()); } break; case 'box': $widget['size'] = 0; if ($old_type == 'grid') { if (preg_match('/,(vertical|horizontal)/',$widget['size'],$matches)) { $orient = $matches[1]; } else { $orient = $widget['type'] == 'hbox' ? 'horizontal' : 'vertical'; } if ($orient == 'horizontal') // ==> use first row { $row =& $old['data'][1]; for ($n = 0; $n < $old['cols']; ++$n) { $cell =& $row[boetemplate::num2chrs($n)]; boetemplate::add_child($widget,$cell); list($span) = (int)explode(',',$cell['span']); if ($span == 'all') break; while ($span-- > 1) ++$n; } } else // vertical ==> use 1 column { for ($n = 1; $n <= $old['rows']; ++$n) { boetemplate::add_child($widget,$old['data'][$n][boetemplate::num2chrs(0)]); } } } if (!$widget['size']) // minimum one child { boetemplate::add_child($widget,boetemplate::empty_cell()); } break; } //_debug_array($widget); } /** * returns array with path => type pairs for each parent of $path * * @param string $path path to the widget not the parent! * @return array */ function path_components($path) { $path_parts = explode('/',$path); array_pop($path_parts); // removed the widget itself array_shift($path_parts); // removed the leading empty string $components = array(); $part_path = ''; foreach($path_parts as $part) { $part_path .= '/'.$part; $parent =& $this->etemplate->get_widget_by_path($part_path); $components[$part_path] = $parent['type']; } return $components; } /** * returns array with path => type pairs for each parent of $path * * @param array $parent the parent * @param string $child_id id of child * @param string $parent_path path of the parent * @return array with keys left, right, up and down and their pathes set (if applicable) */ function parent_navigation($parent,$parent_path,$child_id,$widget) { if ($parent['type'] == 'grid' && preg_match('/^([0-9]+)([A-Z]+)$/',$child_id,$matches)) { list(,$r,$c) = $matches; // find the column-number (base 0) for $c (A, B, C, ...) for($col = 0; boetemplate::num2chrs($col) != $c && $col < 100; ++$col) ; if ($col > 0) $left = $parent_path.'/'.$r.boetemplate::num2chrs($col-1); if ($col < $parent['cols']-1) $right = $parent_path.'/'.$r.boetemplate::num2chrs($col+1); if ($r > 1) $up = $parent_path.'/'.($r-1).$c; if ($r < $parent['rows']) $down = $parent_path.'/'.($r+1).$c; } elseif ($parent['type']) // any box { if ($child_id > 1) $previous = $parent_path.'/'.($child_id-1); if ($child_id < (int) $parent['size']) $next = $parent_path.'/'.($child_id+1); } else // template { if ($child_id > 0) $previous = '/'.($child_id-1); if ($child_id < count($this->etemplate->children)-1) $next = '/'.($child_id+1); } if ($widget['type'] == 'grid') { $in = $parent_path.'/'.$child_id.'/1A'; } elseif (isset(boetemplate::$widgets_with_children[$widget['type']]) && $widget['type'] != 'template') { if ($widget['type']) // box { $in = $parent_path.'/'.$child_id.'/1'; } else { $in = '/0'; } } $navi = array(); foreach(array('left'=>'←','up'=>' ↑ ','down'=>' ↓ ', 'right'=>'→','previous'=>'←↑','next'=>'↓→','in'=>'×') as $var => $dir) { if ($$var) $navi[$$var] = $dir; } return $navi; } /** * functions of the edit-menu: paste, swap, cut, delete, copy * * @internal * @param string &$action row_delete, row_insert_above, row_insert_below, row_swap, row_prefs * @param array &$parent referece to the parent * @param array &$content reference to the content-array * @param string $child_id id of a cell * @return string msg to display */ function edit_actions(&$action,&$parent,&$content,$child_id) { switch ($action) { case 'paste': case 'swap': $clipboard = $GLOBALS['egw']->session->appsession('clipboard','etemplate'); if (!is_array($clipboard)) { return lang('nothing in clipboard to paste !!!'); } if ($action == 'swap') { $GLOBALS['egw']->session->appsession('clipboard','etemplate',$content['cell']); } $content['cell'] = $clipboard; break; case 'copy': case 'cut': $GLOBALS['egw']->session->appsession('clipboard','etemplate',$content['cell']); if ($action != 'cut') { return lang('widget copied into clipboard'); } // fall-through case 'delete': if ($parent['type'] != 'grid') { // delete widget from parent if ($parent['type']) // box { list($num,$options) = explode(',',$parent['size'],2); if ($num <= 1) // cant delete last child --> only empty it { $parent[$num=1] = boetemplate::empty_cell(); } else { for($n = $child_id; $n < $num; ++$n) { $parent[$n] = $parent[1+$n]; } unset($parent[$num--]); } $parent['size'] = $num . ($options ? ','.$options : ''); } else // template itself { if (count($this->etemplate->children) <= 1) // cant delete last child { $this->etemplate->children[0] = boetemplate::empty_cell(); } else { unset($parent[$child_id]); $this->etemplate->children = array_values($this->etemplate->children); } } $action = 'save-no-merge'; } else { $content['cell'] = boetemplate::empty_cell(); return lang('cant delete a single widget from a grid !!!'); } break; } return ''; } /** * functions of the box-menu: insert-before, -behind und swap * * @internal * @param string &$action row_delete, row_insert_above, row_insert_below, row_swap, row_prefs * @param array &$parent referece to the parent * @param array &$content reference to the content-array * @param string &$child_id id of a cell, may change to the next cell if inserting behind * @param string $parent_path path of parent * @return string msg to display */ function box_actions(&$action,&$parent,&$content,&$child_id,$parent_path) { switch ($action) { case 'box_insert_before': case 'box_insert_behind': $n = $child_id + (int)($action == 'box_insert_behind'); if (!$parent['type']) // template { $num = count($parent)-1; // 0..count()-1 } else // boxes { list($num,$options) = explode(',',$parent['size'],2); } for($i = $num; $i >= $n; --$i) { $parent[1+$i] = $parent[$i]; } $parent[$n] = $content['cell'] = boetemplate::empty_cell(); $child_id = $n; if ($parent['type']) $parent['size'] = (1+$num) . ($options ? ','.$options : ''); break; case 'box_swap_next': if (!$parent['type']) // template { $num = count($parent)-1; // 0..count()-1 } else // boxes { list($num) = explode(',',$parent['size'],2); } if ($child_id == $num) // if on last cell, swap with the one before { $this->swap($parent[$child_id],$parent[$child_id-1]); --$child_id; } else { $this->swap($parent[$child_id],$parent[$child_id+1]); ++$child_id; } break; } $action = 'apply-no-merge'; return ''; } /** * functions of the row-menu: insert, deleting & swaping of rows * * @internal * @param string &$action row_delete, row_insert_above, row_insert_below, row_swap_next, row_prefs * @param array &$grid grid * @param string $child_id id of a cell * @return string msg to display */ function row_actions(&$action,&$grid,$child_id) { $data =& $grid['data']; $rows =& $grid['rows']; $cols =& $grid['cols']; $opts =& $data[0]; if (preg_match('/^([0-9]+)([A-Z]+)$/',$child_id,$matches)) list(,$r,$c) = $matches; if (!$c || !$r || $r > $rows) return "wrong child_id='$child_id' => r='$r', c='$c'"; switch($action) { case 'row_swap_next': if ($r > $rows-1) { if ($r != $rows) return lang('no row to swap with !!!'); --$r; // in last row swap with row above } $this->swap($data[$r],$data[1+$r]); $this->swap($opts['c'.$r],$opts['c'.(1+$r)]); $this->swap($opts['h'.$r],$opts['h'.(1+$r)]); break; case 'row_delete': if ($rows <= 1) // one row only => delete whole grid { return lang('cant delete the only row in a grid !!!'); // todo: delete whole grid instead } for($i = $r; $i < $rows; ++$i) { $opts['c'.$i] = $opts['c'.(1+$i)]; $opts['h'.$i] = $opts['h'.(1+$i)]; $data[$i] = $data[1+$i]; } unset($opts['c'.$rows]); unset($opts['h'.$rows]); unset($data[$rows--]); break; case 'row_insert_above': --$r; // fall-through case 'row_insert_below': //echo "row_insert_below($r) rows=$rows, cols=$cols"; _debug_array($grid); // move height + class options of rows for($i = $rows; $i > $r; --$i) { echo ($i+1)."=$i
\n"; $data[1+$i] = $data[$i]; $opts['c'.(1+$i)] = $opts['c'.$i]; $opts['h'.(1+$i)] = $opts['h'.$i]; } for($i = 0; $i < $cols; ++$i) { echo (1+$r).":$i=".boetemplate::num2chrs($i)."=empty_cell()
\n"; $data[1+$r][boetemplate::num2chrs($i)] = boetemplate::empty_cell(); } $opts['c'.(1+$r)] = $opts['h'.(1+$r)] = ''; ++$rows; //_debug_array($grid); return ''; break; } $action = 'save-no-merge'; return ''; } /** * functions of the column-menu: insert, deleting & swaping of columns * * @internal * @param string &$action column_delete, column_insert_before, column_insert_behind, column_swap_next, column_prefs * @param array &$grid grid * @param string $child_id id of a cell * @return string msg to display */ function column_actions(&$action,&$grid,$child_id) { $data =& $grid['data']; $rows =& $grid['rows']; $cols =& $grid['cols']; $opts =& $data[0]; if (preg_match('/^([0-9]+)([A-Z]+)$/',$child_id,$matches)) list(,$r,$c) = $matches; // find the column-number (base 0) for $c (A, B, C, ...) for($col = 0; boetemplate::num2chrs($col) != $c && $col < 100; ++$col) ; if (!$c || !$r || $r > $rows || $col >= $cols) return "wrong child_id='$child_id' => r='$r', c='$c', col=$col"; switch($action) { case 'column_swap_next': if ($col >= $cols-1) { if ($col != $cols-1) return lang('no column to swap with !!!'); $c = boetemplate::num2chrs(--$col); // in last column swap with the one before } $c_next = boetemplate::num2chrs(1+$col); for($row = 1; $row <= $rows; ++$row) { $this->swap($data[$row][$c],$data[$row][$c_next]); } $this->swap($opts[$c],$opts[$c_next]); //_debug_array($grid); return ''; break; case 'column_insert_behind': ++$col; case 'column_insert_before': //echo "

column_insert_before: col=$col

\n"; // $col is where the new column data goes for ($row = 1; $row <= $rows; ++$row) { for ($i = $cols; $i > $col; --$i) { $data[$row][boetemplate::num2chrs($i)] = $data[$row][boetemplate::num2chrs($i-1)]; } $data[$row][boetemplate::num2chrs($col)] = boetemplate::empty_cell(); } for ($i = $cols; $i > $col; --$i) { $opts[boetemplate::num2chrs($i)] = $opts[boetemplate::num2chrs($i-1)]; } unset($opts[boetemplate::num2chrs($col)]); ++$cols; //_debug_array($grid); return ''; break; case 'column_delete': if ($cols <= 1) { return lang('cant delete the only column of a grid !!!'); // todo: delete whole grid instead } for ($row = 1; $row <= $rows; ++$row) { for ($i = $col; $i < $cols-1; ++$i) { $data[$row][boetemplate::num2chrs($i)] = $data[$row][boetemplate::num2chrs($i+1)]; } unset($data[$row][boetemplate::num2chrs($cols-1)]); } for ($i = $col; $i < $cols-1; ++$i) { $opts[boetemplate::num2chrs($i)] = $opts[boetemplate::num2chrs($i+1)]; } unset($opts[boetemplate::num2chrs(--$cols)]); break; } $action = 'save-no-merge'; return ''; } /** * converts onclick selectbox and onclick text to one javascript call * * @param array &$widget reference into the widget-tree * @param array &$cell_content cell array in content * @param boolean $widget2content=true copy from widget to content or other direction */ function fix_set_onclick(&$widget,&$cell_content,$widget2content=true) { if ($widget2content) { if (preg_match('/^return confirm\(["\']{1}?(.*)["\']{1}\);?$/',$widget['onclick'],$matches)) { $cell_content['onclick'] = $matches[1]; $cell_content['onclick_type'] = 'confirm'; } elseif (preg_match('/^window.open\(egw::link\(\'\/index.php\',\'([^\']+)\'\)(\+values2url\(.*\))?,\'([^\']+)\',\'dependent=yes,width=([0-9]+),height=([0-9]+),scrollbars=yes,status=yes\'\); return false;$/',$widget['onclick'],$matches)) { $cell_content['onclick'] = $matches[1].($matches[2] ? str_replace('+values2url(this.form,','&values2url(',$matches[2]) : ''); if ($matches[3] != '_blank') { $cell_content['onclick'] .= ','.$matches[3]; } if ($matches[4] != '600') { $cell_content['onclick'] .= ($matches[3]=='_blank' ? ',':'').','.$matches[4]; } if ($matches[5] != '450') { $cell_content['onclick'] .= ($matches[4]=='600' ? ','.($matches[3]=='_blank' ? ',':'') : ''). ','.$matches[5]; } $cell_content['onclick_type'] = 'popup'; } else { $cell_content['onclick_type'] = !$widget['onclick'] ? '' : 'custom'; } } else // content --> widget { if (preg_match('/^return confirm\(["\']{1}?(.*)["\']{1}\);?$/',$cell_content['onclick'],$matches) || $cell_content['onclick_type'] == 'confirm' && $cell_content['onclick']) { $cell_content['onclick_type'] = 'confirm'; $cell_content['onclick'] = is_array($matches) && $matches[1] ? $matches[1] : $cell_content['onclick']; $widget['onclick'] = "return confirm('".$cell_content['onclick']."');"; } elseif ($cell_content['onclick_type'] == 'popup' && $cell_content['onclick']) { // eg: menuaction=calendar.uiforms.freetimesearch&values2url('start,end,participants'),ft_search,700,500 if (($values2url = preg_match('/&values2url\((\'[^\']+\')\)/',$cell_content['onclick'],$matches))) { $values2url = $matches[1]; $onclick = str_replace('&values2url('.$values2url.')','',$cell_content['onclick']); } list($get,$target,$width,$height) = explode(',',$values2url ? $onclick : $cell_content['onclick']); if (!$target) $target = '_blank'; if (!$width) $width = 600; if (!$height) $height = 450; $widget['onclick'] = "window.open(egw::link('/index.php','$get')".($values2url ? "+values2url(this.form,$values2url)" : ''). ",'$target','dependent=yes,width=$width,height=$height,scrollbars=yes,status=yes'); return false;"; } elseif ($cell_content['onclick']) { $wiget['onclick'] = $cell_content['onclick']; $cell_content['onclick_type'] = 'custom'; } else { $cell_content['onclick_type'] = ''; } unset($widget['onclick_type']); } //echo "

editor::fix_set_onclick(,,widget2content=".(int)$widget2content.") widget="; _debug_array($widget); echo "content="; _debug_array($cell_content); } /** * converts onchange selectbox and onchange text to one javascript call * * @param array &$widget reference into the widget-tree * @param array &$cell_content cell array in content * @param boolean $widget2content=true copy from widget to content or other direction */ function fix_set_onchange(&$widget,&$cell_content,$widget2content=true) { if ($widget2content) { if (!$widget['onchange']) { $cell_content['onchange_type'] = $cell_content['onchange'] = ''; } elseif ($widget['onchange'] == 1 || $widget['onchange'] == 'this.form.submit();') { $cell_content['onchange'] = ''; $cell_content['onchange_type'] = 'submit'; } else { $cell_content['onchange_type'] = 'custom'; } } else // content --> widget { if ($cell_content['onchange_type'] == 'submit' || $cell_content['onchange'] == 'this.form.submit();') { $widget['onchange'] = 1; } elseif(!$cell_content['onchange']) { $widget['onchange'] = 0; } unset($widget['onchange_type']); } //echo "

editor::fix_set_onchange(,,widget2content=".(int)$widget2content.") widget="; _debug_array($widget); echo "content="; _debug_array($cell_content); } /** * edit dialog for a widget * * @param array $content the submitted content of the etemplate::exec function, default null * @param string $msg msg to display, default '' */ function widget($content=null,$msg='') { if (is_array($content)) { $path = $content['goto'] ? $content['goto'] : ($content['goto2'] ? $content['goto2'] : $content['path']); $Ok = $this->etemplate->read($content['name'],$content['template'],$content['lang'],0,$content['goto'] || $content['goto2'] ? $content['version'] : $content['old_version']); // build size from options array, if applicable if (is_array($content['cell']['options'])) { $size = ''; for ($n = max(array_keys($content['cell']['options'])); $n >= 0; --$n) { if (strlen($content['cell']['options'][$n]) || strlen($size)) { $size = $content['cell']['options'][$n].(strlen($size) ? ','.$size : ''); } } $content['cell']['size'] = $size; } } else { //echo "

".($_GET['path']).":

\n"; list($name,$version,$path) = explode(':',$_GET['path'],3); // :: $Ok = $this->etemplate->read(array( 'name' => $name, 'version' => $version, )); } if (!$Ok && !$content['cancel']) { $msg .= lang('Error: Template not found !!!'); } $path_parts = explode('/',$path); $child_id = array_pop($path_parts); $parent_path = implode('/',$path_parts); //echo "

path='$path': child_id='$child_id', parent_path='$parent_path'

\n"; $parent =& $this->etemplate->get_widget_by_path($parent_path); if (is_array($content)) { foreach(array('save','apply','cancel','goto','goto2','edit_menu','box_menu','row_menu','column_menu') as $n => $name) { if (($action = $content[$name] ? ($n < 5 ? $name : $content[$name]) : false)) break; $name = ''; } unset($content[$name]); //echo "

name='$name', parent-type='$parent[type]', action='$action'

\n"; if (($name == 'row_menu' || $name == 'column_menu') && $parent['type'] != 'grid' || $name == 'box_menu' && $parent['type'] == 'grid') { $msg .= lang("parent is a '%1' !!!",lang($parent['type'] ? $parent['type'] : 'template')); $action = false; } switch($name) { case 'edit_menu': $msg .= $this->edit_actions($action,$parent,$content,$child_id); break; case 'box_menu': $msg .= $this->box_actions($action,$parent,$content,$child_id,$parent_path); break; case 'row_menu': $msg .= $this->row_actions($action,$parent,$child_id); break; case 'column_menu': $msg .= $this->column_actions($action,$parent,$child_id); break; case '': // reload, eg. by changing the type $widget = $content['cell']; break; default: // all menu's are (only) working on the parent, referencing widget is unnecessary // and gives unexpected results, if parent is changed (eg. content gets copied) $widget =& $this->etemplate->get_widget_by_path($path); break; } switch ($action) { case 'goto': case 'goto2': $content['cell'] = $widget; $this->fix_set_onclick($widget,$content['cell'],true); $this->fix_set_onchange($widget,$content['cell'],true); break; case '': case 'save': case 'apply': // initialise the children arrays if type is changed to a widget with children //echo "

$content[path]: $widget[type] --> ".$content['cell']['type']."

\n"; if (isset(boetemplate::$widgets_with_children[$content['cell']['type']])) { $this->change_widget_type($content['cell'],$widget); } if (!$action) break; // save+apply only $widget = $content['cell']; if ($content['cell']['onclick_type'] || $content['cell']['onclick']) { $this->fix_set_onclick($widget,$content['cell'],false); } if ($content['cell']['onchange_type'] || $content['cell']['onchange']) { $this->fix_set_onchange($widget,$content['cell'],false); } // row- and column-attr for a grid if ($parent['type'] == 'grid' && preg_match('/^([0-9]+)([A-Z]+)$/',$child_id,$matches)) { list(,$row,$col) = $matches; $parent['data'][0]['h'.$row] = $content['grid_row']['height']. ($content['grid_row']['disabled']||$content['grid_row']['part']?','.$content['grid_row']['disabled']:''). ($content['grid_row']['part']?','.$content['grid_row']['part']:''); $parent['data'][0]['c'.$row] = $content['grid_row']['class']. ($content['grid_row']['valign']?','.$content['grid_row']['valign']:''); $parent['data'][0][$col] = $content['grid_column']['width']. ($content['grid_column']['disabled']?','.$content['grid_column']['disabled']:''); } // fall-through case 'save-no-merge': case 'apply-no-merge': //$this->etemplate->echo_tmpl(); $ok = $this->etemplate->save($content); $msg .= $ok ? lang('Template saved') : lang('Error: while saving !!!'); // if necessary fix the version of our opener if ($content['opener']['name'] == $content['name'] && $content['opener']['template'] == $content['template'] && $content['opener']['group'] == $content['group'] && $content['opener']['lang'] == $content['lang']) { $content['opener']['version'] = $content['version']; } $js = "opener.location.href='".$GLOBALS['egw']->link('/index.php',array( 'menuaction' => 'etemplate.editor.edit', )+$content['opener'])."';"; if ($action == 'apply' || $action == 'apply-no-merge') break; // fall through case 'cancel': $js .= 'window.close();'; echo "\n"; common::egw_exit(); break; } if ($js) { $content['java_script'] = ""; } } else { $widget =& $this->etemplate->get_widget_by_path($path); $content = $this->etemplate->as_array(-1); $content['cell'] = $widget; $this->fix_set_onclick($widget,$content['cell'],true); $this->fix_set_onchange($widget,$content['cell'],true); foreach(boetemplate::$db_key_cols as $var) { if (isset($_GET[$var])) { $content['opener'][$var] = $_GET[$var]; } } } unset($content['cell']['obj']); // just in case it contains a template-object if ($parent['type'] == 'grid' && preg_match('/^([0-9]+)([A-Z]+)$/',$child_id,$matches)) { list(,$row,$col) = $matches; $grid_row =& $content['grid_row']; list($grid_row['height'],$grid_row['disabled'],$grid_row['part']) = explode(',',$parent['data'][0]['h'.$row]); list($grid_row['class'],$grid_row['valign']) = explode(',',$parent['data'][0]['c'.$row]); $grid_column =& $content['grid_column']; list($grid_column['width'],$grid_column['disabled']) = explode(',',$parent['data'][0][$col]); //echo "

grid_row($row)=".print_r($grid_row,true).", grid_column($col)=".print_r($grid_column,true)."

\n"; list(,,$previous_part) = explode(',',$parent['data'][0]['h'.($row-1)]); list(,,$next_part) = explode(',',$parent['data'][0]['h'.($row+1)]); $allowed_parts = $this->get_allowed_parts($row,$previous_part,$next_part); //echo "

$row: previous=$previous_part, current={$grid_row['part']}, next=$next_part".(!isset($allowed_parts[$grid_row['part']])?': current part is NOT allowed!!!':'')."

\n"; _debug_array($allowed_parts); } else { unset($content['grid_row']); unset($content['grid_column']); } $content['path'] = ($parent_path!='/'?$parent_path:'').'/'.$child_id; $content['msg'] = $msg; $content['goto'] = $this->path_components($content['path']); $content['goto2'] = $this->parent_navigation($parent,$parent_path,$child_id,$widget); $content['cell']['options'] = explode(',',$content['cell']['size']); $editor = new etemplate('etemplate.editor.widget'); $type_tmpl = new etemplate; list($ext_type) = explode('-',$widget['type']); // allow to read template of app-specific widgets from their app: eg. "infolog-value" --> "infolog.widget.infolog-value" if (isset($GLOBALS['egw_info']['apps'][$ext_type]) && $type_tmpl->read($ext_type.'.widget.'.$widget['type']) || $type_tmpl->read('etemplate.editor.widget.'.$widget['type']) || $type_tmpl->read('etemplate.editor.widget.'.$ext_type)) { $editor->set_cell_attribute('etemplate.editor.widget.generic','obj',$type_tmpl); } /* Not a known type, use a generic attribute thing to at least allow working with the attributes provided */ if ($widget['type'] && !boetemplate::$types[$widget['type']] && !$this->extensions[$widget['type']]) { $grid =& $editor->get_widget_by_name('etemplate.editor.widget.generic'); $grid['type'] = 'grid'; $grid['name'] = 'cell'; $grid['data'] = array(array()); $grid['data'][] = array('A'=>boetemplate::empty_cell('label','',array('label' => 'Type')), 'B' => boetemplate::empty_cell('select','type')); $attributes = array('type'=>$widget_type,'name'=>''); if(is_array($widget)) { $attributes += $widget; } foreach($attributes as $attr_name => $attr_value) { if(is_array($attr_value) || in_array($attr_value, array('type','options'))) { continue; } else { $attr = boetemplate::empty_cell('text',$attr_name,array()); } $grid['data'][] = array('A' => boetemplate::empty_cell('label','',array('label' => $attr_name)),'B'=>$attr); //boetemplate::add_child($grid,$attr); unset($attr); } } if ($parent['type'] == 'grid') { $editor->disable_cells('box_menu'); } else { $editor->disable_cells('row_menu'); $editor->disable_cells('column_menu'); } $preserv = $this->etemplate->as_array()+array( 'path' => $content['path'], 'type' => $content['type'], 'old_version' => $this->etemplate->version, 'opener' => $content['opener'], 'cell' => $content['cell'], 'goto' => $content['goto'], ); unset($preserv['cell']['options']); // otherwise we never know if content is returned via options array or size $GLOBALS['egw_info']['flags']['java_script'] = "\n"; $GLOBALS['egw_info']['flags']['app_header'] = lang('Editable Templates - Editor'); $editor->exec('etemplate.editor.widget',$content,array( 'type' => array_merge(array($widget['type'] => $widget['type'] . ' (?)'), boetemplate::$types,$this->extensions ), 'align' => &$this->aligns, 'valign' => &$this->valigns, 'part' => $allowed_parts, 'edit_menu' => &$this->edit_menu, 'box_menu' => &$this->box_menu, 'row_menu' => &$this->row_menu, 'column_menu'=> &$this->column_menu, 'onclick_type'=>&$this->onclick_types, 'onchange_type'=>&$this->onchange_types, 'options[6]' => &$this->overflows, ),'',$preserv,2); } /** * Get the allowed tables-part for a table rows based on row-number, previous and next part * * @param int $row (1-based!) * @param string $previous_part ''=body, 'h'=header, 'f'=footer * @param string $next_part see above * @return array */ private function get_allowed_parts($row,$previous_part,$next_part) { $allowed_parts = $this->parts; switch((string)$previous_part) { case 'footer': unset($allowed_parts['header']); break; case '': if ($row > 1) $allowed_parts = array('' => $allowed_parts['']); break; } switch($next_part) { case 'header': $allowed_parts = array('header' => $allowed_parts['header']); break; case 'footer': unset($allowed_parts['']); break; } return $allowed_parts; } /** * edit dialog for the styles of a templat or app * * @param array $content the submitted content of the etemplate::exec function, default null * @param string $msg msg to display, default '' */ function styles($content=null,$msg='') { if (!is_array($content)) { foreach(boetemplate::$db_key_cols as $var) { if (isset($_GET[$var])) $content[$var] = $_GET[$var]; } } //_debug_array($content); // security check for content[from] if ($content['from'] && !preg_match('/^[A-Za-z0-9_-]+\/templates\/[A-Za-z0-9_-]+\/app.css$/',$content['from'])) { $content['from'] = ''; // someone tried to trick us reading a file we are not suppost to read } if (!$this->etemplate->read($content)) { $msg .= lang('Error: Template not found !!!'); } if ($content['save'] || $content['apply']) { if ($content['from']) { $path = EGW_SERVER_ROOT.'/'.$content['from']; if (is_writable(dirname($path)) && file_exists($path)) { rename($path,str_replace('.css','.old.css',$path)); } if (file_exists($path) && !is_writable(dirname($path))) { $msg .= lang("Error: webserver is not allowed to write into '%1' !!!",dirname($path)); } else { $fp = fopen($path,'w'); if (!$fp || !fwrite($fp,$content['styles'])) { $msg .= lang('Error: while saving !!!'); } else { $msg .= lang("File writen",$path); } @fclose($fp); } } else // the templates own embeded styles; { $this->etemplate->style = $content['styles']; $ok = $this->etemplate->save(); $msg = $ok ? lang('Template saved') : lang('Error: while saving !!!'); } $js = "opener.location.href='".$GLOBALS['egw']->link('/index.php',array( 'menuaction' => 'etemplate.editor.edit', )+$this->etemplate->as_array(-1))."';"; } if ($content['save'] || $content['cancel']) { $js .= 'window.close();'; echo "\n"; common::egw_exit(); } $content = array( 'from' => $content['from'], 'java_script' => $js ? '' : '', 'msg' => $msg ); $tmpl = new etemplate('etemplate.editor.styles'); if ($content['from']) { $path = EGW_SERVER_ROOT.'/'.$content['from']; $content['styles'] = file_exists($path) && is_readable($path) ? implode('',file($path)) : ''; if (!is_writable(dirname($path)) && (!file_exists($path) || !is_writable($path))) { $tmpl->set_cell_attribute('styles','readonly',true); } } else { $content['styles'] = $this->etemplate->style; } // generate list of style-sources $keys = $this->etemplate->as_array(-1); unset($keys['group']); $sources[''] = lang('eTemplate').': '.implode(':',$keys); list($app) = explode('.',$this->etemplate->name); $app_templates = @opendir(EGW_SERVER_ROOT.'/'.$app.'/templates'); while (($template = @readdir($app_templates)) !== false) { $dir = EGW_SERVER_ROOT.'/'.$app.'/templates/'.$template; if ($template[0] == '.' || $template == 'CVS' || !is_dir($dir.'/images')) continue; // not a template-dir $exists = file_exists($dir.'/app.css'); $writable = is_writable($dir) || $exists && is_writable($dir.'/app.css'); if (!$exists && !$writable) continue; // nothing to show $rel_path = $app.'/templates/'.$template.'/app.css'; $sources[$rel_path] = lang('file').': '.$rel_path.($exists && !$writable ? ' ('.lang('readonly').')' : ''); } $GLOBALS['egw_info']['flags']['java_script'] = "\n"; $GLOBALS['egw_info']['flags']['app_header'] = lang('etemplate').' - '.lang('CSS-styles'); $tmpl->exec('etemplate.editor.styles',$content,array('from'=>$sources),'',$keys,2); } /** * search the inc-dirs of etemplate and the app whichs template is edited for extensions / custom widgets * * extensions are class-files in $app/inc/class.${name}_widget.inc.php * the extensions found will be saved in a class-var and in the session * * @param string $app='etemplate' app to scan * @return string comma delimited list of new found extensions */ function scan_for_extensions($app='etemplate') { if (!is_array($this->extensions)) $this->extensions = array(); if (isset($this->extensions['**loaded**'][$app])) return ''; // already loaded $labels = array(); $dir = @opendir(EGW_SERVER_ROOT.'/'.$app.'/inc'); while ($dir && ($file = readdir($dir))) { // ignore et2 base widget matching name-schema of old eTemplate if ($file == 'class.etemplate_widget.inc.php') continue; if (preg_match('/class\\.([a-zA-Z0-9_]*)_widget.inc.php/',$file,$regs) && ($regs[1] != 'xslt' || $this->etemplate->xslt) && ($ext = $this->etemplate->loadExtension($regs[1].'.'.$app,$this->etemplate))) { if (is_array($ext)) { $this->extensions += $ext; $labels += $ext; } else { $this->extensions[$regs[1]] = $ext; $labels[] = $ext; } } } // store the information in the session, our constructor loads it from there $GLOBALS['egw']->session->appsession('extensions','etemplate',$this->extensions); $apps_loaded = $GLOBALS['egw']->session->appsession('apps_loaded','etemplate'); $apps_loaded[$app] = true; $GLOBALS['egw']->session->appsession('apps_loaded','etemplate',$apps_loaded); //_debug_array($this->extensions); _debug_array($apps_loaded); return implode(', ',$labels); } /** * swap the values of $a and $b * * @param mixed &$a * @param mixed &$b */ function swap(&$a,&$b) { $h = $a; $a = $b; $b = $h; } }