diff --git a/etemplate/inc/class.etemplate.inc.php b/etemplate/inc/class.etemplate.inc.php index 6f62da8e92..db1490a426 100644 --- a/etemplate/inc/class.etemplate.inc.php +++ b/etemplate/inc/class.etemplate.inc.php @@ -1,70 +1,167 @@ - * @copyright 2002-13 by RalfBecker@outdoor-training.de - * @version $Id$ - */ +* EGroupware - EditableTemplates - HTML User Interface +* +* @link http://www.egroupware.org +* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License +* @author Ralf Becker +* @copyright 2002-14 by RalfBecker@outdoor-training.de +* @package etemplate +* @subpackage api +* @version $Id$ +*/ /** - * New eTemplate serverside contains: - * - main server methods like read, exec - * - - * - * Not longer available methods: - * - set_(row|column)_attributes modifies template on run-time, was only used internally by etemplate itself - * - disable_(row|column) dto. - * - * @ToDo supported customized templates stored in DB, currently we only support xet files stored in filesystem - */ -class etemplate_new extends etemplate_widget_template +* creates dialogs / HTML-forms from eTemplate descriptions +* +* Usage example: +* +* $tmpl = new etemplate('app.template.name'); +* $tmpl->exec('app.class.callback',$content_to_show); +* +* This creates a form from the eTemplate 'app.template.name' and takes care that +* the method / public function 'callback' in class 'class' of 'app' gets called +* if the user submits the form. For the complete param's see the description of exec. +* +* etemplate or uietemplate extends boetemplate, all vars and public functions are inherited +*/ +class etemplate extends boetemplate { /** - * Are we running as sitemgr module or not - * - * @public boolean - */ + * integer debug-level or template-name or cell-type or '' = off + * 1=calls to show and process_show, 2=content after process_show, + * 3=calls to show_cell and process_show_cell + * + * @public int/string + */ + public $debug; + /** + * Inner width of browser window + * + * @public int + */ + public $innerWidth; + /** + * Reference to the content-param of the last call to show, for extensions to use + * + * @public array + */ + public $content; + /** + * Reference to the sel_options-param of the last call to show, for extensions to use + * + * @public array + */ + public $sel_options; + /** + * Name of the form of the currently processed etemplate + * + * @public string + */ + static $name_form='eTemplate'; + /** + * Used form-names in this request + * + * @public array + */ + static $name_forms=array(); + /** + * Basename of the variables (content) in $_POST and id's, usually 'exec', + * if there's not more then one eTemplate on the page (then it will be exec, exec2, exec3, ... + * + * @public string + */ + static $name_vars='exec'; + /** + * Are we running as sitemgr module or not + * + * @public boolean + */ public $sitemgr=false; - /** - * Tell egw framework it's ok to call this - */ - public $public_functions = array( - 'process_exec' => true - ); - - /** - * constructor of etemplate class, reads an eTemplate if $name is given + * Javascript to be called, when a widget get's double-clicked (used only by the editor) + * A '%p' gets replace with the colon ':' separated template-name, -version and path of the clicked widget. * - * @param string $name of etemplate or array with name and other keys - * @param string|array $load_via with keys of other etemplate to load in order to get $name + * @public string */ + public $onclick_handler; + /** + * Does template processes onclick itself, or forwards it to a proxy + * + * @var boolean + */ + public $no_onclick = false; + /** + * handler to call for onclick + * + * @var string + */ + public $onclick_proxy; + + /** + * Extra options for forms, eg. enctype="multipart/form-data" + * + * @public string + */ + static protected $form_options = ''; + + /** + * Validation errors from process_show and the extensions, should be set via self::set_validation_error + * + * @public array form_name => message pairs + */ + static protected $validation_errors = array(); + + /** + * Flag if exec() is called as part of a hook, replaces the 1.6 and earlier $GLOBALS['egw_info']['etemplate']['hooked'] global variable + * + * @var boolean + */ + static public $hooked; + + /** + * The following 3 static vars are used to allow eTemplate apps to hook into each other + * + * @var mixed + */ + static private $previous_content; + static private $hook_content; + static private $hook_app; + + /** + * constructor of etemplate class, reads an eTemplate if $name is given + * + * @param string $name of etemplate or array with name and other keys + * @param string/array $load_via with keys of other etemplate to load in order to get $name + */ function __construct($name='',$load_via='') { - // we do NOT call parent consturctor, as we only want to enherit it's (static) methods - if (false) parent::__construct ($name); // satisfy IDE, as we dont call parent constructor + // tell framework old eTemplate apps needs eval and inline javascript :( + egw_framework::csp_script_src_attrs(array('unsafe-eval', 'unsafe-inline')); + + parent::__construct($name,$load_via); $this->sitemgr = isset($GLOBALS['Common_BO']) && is_object($GLOBALS['Common_BO']); - if ($name) $this->read($name,$template='default','default',0,'',$load_via); - - // generate new etemplate request object, if not already existing - if(!isset(self::$request)) self::$request = etemplate_request::read(); + if (($this->innerWidth = (int) $_POST['innerWidth'])) + { + $GLOBALS['egw']->session->appsession('innerWidth','etemplate',$this->innerWidth); + } + elseif (!($this->innerWidth = (int) $GLOBALS['egw']->session->appsession('innerWidth','etemplate'))) + { + $this->innerWidth = 1018; // default width for an assumed screen-resolution of 1024x768 + } + //echo "

_POST[innerWidth]='$_POST[innerWidth]', innerWidth=$this->innerWidth

\n"; } /** - * Abstracts a html-location-header call - * - * In other UI's than html this needs to call the methode, defined by menuaction or - * open a browser-window for any other links. - * - * @param string|array $params url or array with get-params incl. menuaction - */ + * Abstracts a html-location-header call + * + * In other UI's than html this needs to call the methode, defined by menuaction or + * open a browser-window for any other links. + * + * @param string/array $params url or array with get-params incl. menuaction + */ static function location($params='') { egw::redirect_link(is_array($params) ? '/index.php' : $params, @@ -72,438 +169,408 @@ class etemplate_new extends etemplate_widget_template } /** - * Generates a Dialog from an eTemplate - abstract the UI-layer - * - * This is the only function an application should use, all other are INTERNAL and - * do NOT abstract the UI-layer, because they return HTML. - * Generates a webpage with a form from the template and puts process_exec in the - * form as submit-url to call process_show for the template before it - * ExecuteMethod's the given $method of the caller. - * - * @param string $method Methode (e.g. 'etemplate.editor.edit') to be called if form is submitted - * @param array $content with content to fill the input-fields of template, eg. the text-field - * with name 'name' gets its content from $content['name'] - * @param $sel_options array or arrays with the options for each select-field, keys are the - * field-names, eg. array('name' => array(1 => 'one',2 => 'two')) set the - * options for field 'name'. ($content['options-name'] is possible too !!!) - * @param array $readonlys with field-names as keys for fields with should be readonly - * (eg. to implement ACL grants on field-level or to remove buttons not applicable) - * @param array $preserv with vars which should be transported to the $method-call (eg. an id) array('id' => $id) sets $_POST['id'] for the $method-call - * @param int $output_mode - * 0 = echo incl. navbar - * 1 = return html - * -1 = first time return html, after use 0 (echo html incl. navbar), eg. for home - * 2 = echo without navbar (eg. for popups) - * 3 = return eGW independent html site - * @param string $ignore_validation if not empty regular expression for validation-errors to ignore - * @param array $changes change made in the last call if looping, only used internaly by process_exec - * @return string html for $output_mode == 1, else nothing - */ - function exec($method,array $content,array $sel_options=null,array $readonlys=null,array $preserv=null,$output_mode=0,$ignore_validation='',array $changes=null) + * Generats a Dialog from an eTemplate - abstract the UI-layer + * + * This is the only function an application should use, all other are INTERNAL and + * do NOT abstract the UI-layer, because they return HTML. + * Generates a webpage with a form from the template and puts process_exec in the + * form as submit-url to call process_show for the template before it + * ExecuteMethod's the given $method of the caller. + * + * @param string $method Methode (e.g. 'etemplate.editor.edit') to be called if form is submitted + * @param array $content with content to fill the input-fields of template, eg. the text-field + * with name 'name' gets its content from $content['name'] + * @param $sel_options array or arrays with the options for each select-field, keys are the + * field-names, eg. array('name' => array(1 => 'one',2 => 'two')) set the + * options for field 'name'. ($content['options-name'] is possible too !!!) + * @param array $readonlys with field-names as keys for fields with should be readonly + * (eg. to implement ACL grants on field-level or to remove buttons not applicable) + * @param array $preserv with vars which should be transported to the $method-call (eg. an id) array('id' => $id) sets $_POST['id'] for the $method-call + * @param int $output_mode + * 0 = echo incl. navbar + * 1 = return html + * -1 = first time return html, after use 0 (echo html incl. navbar), eg. for home + * 2 = echo without navbar (eg. for popups) + * 3 = return eGW independent html site + * @param string $ignore_validation if not empty regular expression for validation-errors to ignore + * @param array $changes change made in the last call if looping, only used internaly by process_exec + * @return string html for $output_mode == 1, else nothing + */ + function exec($method,$content,$sel_options='',$readonlys='',$preserv='',$output_mode=0,$ignore_validation='',$changes='') { - // Include the etemplate2 javascript code - egw_framework::validate_file('.', 'etemplate2', 'etemplate'); - - if (!$this->rel_path) throw new egw_exception_assertion_failed("No (valid) template '$this->name' found!"); + if (!$sel_options) + { + $sel_options = array(); + } + if (!$readonlys) + { + $readonlys = array(); + } + if (!$preserv) + { + $preserv = array(); + } + if (!$changes) + { + $changes = array(); + } + if (isset($content['app_header'])) + { + $GLOBALS['egw_info']['flags']['app_header'] = $content['app_header']; + } + if ($GLOBALS['egw_info']['flags']['currentapp'] != 'etemplate') + { + translation::add_app('etemplate'); // some extensions have own texts + } + // use different form-names to allows multiple eTemplates in one page, eg. addressbook-view + self::$name_form = 'eTemplate'; + if (in_array(self::$name_form,self::$name_forms)) + { + self::$name_form .= 1+count(self::$name_forms); + self::$name_vars .= 1+count(self::$name_forms); + } + self::$name_forms[] = self::$name_form; + self::$request = etemplate_request::read(); self::$request->output_mode = $output_mode; // let extensions "know" they are run eg. in a popup - self::$request->content = self::$cont = $content; + self::$request->readonlys = $readonlys; + self::$request->content = $content; self::$request->changes = $changes; - self::$request->sel_options = $sel_options ? $sel_options : array(); - self::$request->readonlys = $readonlys ? $readonlys : array(); - self::$request->preserv = $preserv ? $preserv : array(); + self::$request->sel_options = $sel_options; + self::$request->preserv = $preserv; self::$request->method = $method; self::$request->ignore_validation = $ignore_validation; - if (self::$request->output_mode == -1) self::$request->output_mode = 0; - self::$request->template = $this->as_array(); + self::$request->name_vars = self::$name_vars; - if (empty($this->name)) throw new egw_exception_assertion_failed("Template name is not set '$this->name' !"); - // instanciate template to fill self::$request->sel_options for select-* widgets - // not sure if we want to handle it this way, thought otherwise we will have a few ajax request for each dialog fetching predefined selectboxes - $template = etemplate_widget_template::instance($this->name, $this->template_set, $this->version, $this->laod_via); - $template->run('beforeSendToClient', array('', array('cont'=>$content))); + // tell html5 form validation NOT to validate + if ($ignore_validation) self::$form_options .= ' novalidate="novalidate"'; - // some apps (eg. InfoLog) set app_header only in get_rows depending on filter settings + if((int) $output_mode == 3) + { + self::$styles_included[$this->name] = True; + return "\n" + ."\n\n".html::style($this->style)."\n\n" + ."\n".$this->show($content)."\n\n"; + } + + $html = $this->show(self::complete_array_merge($content,$changes),$sel_options,$readonlys,self::$name_vars); + + self::$request->java_script_from_flags = $GLOBALS['egw_info']['flags']['java_script']; + self::$request->java_script_body_tags = array( + 'onload' => egw_framework::set_onload(), + 'onunload' => egw_framework::set_onunload(), + 'onresize' => egw_framework::set_onresize(), + ); + self::$request->java_script_files = egw_framework::js_files(); + self::$request->include_xajax = $GLOBALS['egw_info']['flags']['include_xajax']; + + // check if application of template has a app.js file --> load it + list($app) = explode('.',$this->name); + if (file_exists(EGW_SERVER_ROOT.'/'.$app.'/js/app.js')) + { + egw_framework::validate_file('.','app',$app,false); + } + + if (!$this->sitemgr) + { + $hooked = isset(self::$previous_content) || !isset($GLOBALS['egw']->template) ? + self::$previous_content : $GLOBALS['egw']->template->get_var('phpgw_body'); + } + self::$request->hooked = $hooked ? $hooked : self::$hook_content; + self::$request->hook_app = $hooked ? $GLOBALS['egw_info']['flags']['currentapp'] : self::$hook_app; self::$request->app_header = $GLOBALS['egw_info']['flags']['app_header']; + if (self::$request->output_mode == -1) self::$request->output_mode = 0; + self::$request->template = $this->as_array(2); - // compile required translations translations - translation::add_app('etemplate'); - $currentapp = $GLOBALS['egw_info']['flags']['currentapp']; - $langRequire = array('common' => array(), 'etemplate' => array()); // keep that order - foreach(translation::$loaded_apps as $l_app => $lang) + $html = html::form(html::input('etemplate_exec_id',self::$request->id(),'hidden',' id="etemplate_exec_id"').$html. + html::input_hidden(array( + 'submit_button' => '', + 'innerWidth' => '', + ),'',false),array(),$this->sitemgr ? '' : '/etemplate/process_exec.php?menuaction='.$method, + '',self::$name_form,self::$form_options. + // dont set the width of popups! + ($output_mode != 0 ? '' : ' onsubmit="this.innerWidth.value=window.innerWidth ? window.innerWidth : document.body.clientWidth;"')); + //echo "to_process="; _debug_array(self::$request->to_process); + + egw_framework::validate_file('/etemplate/js/etemplate.js'); + + //echo '

'.__METHOD__."($method,...) etemplate[hooked]=".(int)self::$hooked.", etemplate[hook_app]='".self::$hook_app."', isset(etemplate[content])=".(int)isset(self::$previous_content)."

\n"; + if (!$this->sitemgr) { - if (!in_array($l_app, array($currentapp, 'custom'))) + // support the old global var, in case old apps like 1.6 infolog use it + if (isset($GLOBALS['egw_info']['etemplate']['hooked'])) self::$hooked = $GLOBALS['egw_info']['etemplate']['hooked']; + + if (!@self::$hooked && (int) $output_mode != 1 && (int) $output_mode != -1) // not just returning the html { - $langRequire[$l_app] = array('app' => $l_app, 'lang' => $lang, 'etag' => translation::etag($l_app, $lang)); - } - } - foreach(array($currentapp, 'custom') as $l_app) - { - if (isset(translation::$loaded_apps[$l_app])) - { - $langRequire[$l_app] = array('app' => $l_app, 'lang' => translation::$loaded_apps[$l_app], 'etag' => translation::etag($l_app, translation::$loaded_apps[$l_app])); - } - } - - $data = array( - 'etemplate_exec_id' => self::$request->id(), - 'app_header' => self::$request->app_header, - 'content' => self::$request->content, - 'sel_options' => self::$request->sel_options, - 'readonlys' => self::$request->readonlys, - 'modifications' => self::$request->modifications, - 'validation_errors' => self::$validation_errors, - 'langRequire' => array_values($langRequire), - 'currentapp' => $currentapp, - ); - - // Info required to load the etemplate client-side - $dom_id = str_replace('.','-',$this->dom_id); - $load_array = array( - 'name' => $this->name, - 'url' => etemplate_widget_template::rel2url($this->rel_path), - 'data' => $data, - 'DOMNodeID' => $dom_id, - ); - if (self::$response) // call is within an ajax event / form submit - { - //error_log("Ajax " . __LINE__); - self::$response->generic('et2_load', $load_array+egw_framework::get_extra()); - egw_framework::clear_extra(); // to not send/set it twice for multiple etemplates (eg. CRM view) - } - else // first call - { - // missing dependency, thought egw:uses jquery.jquery.tools does NOT work, maybe we should rename it to jquery-tools - // egw_framework::validate_file('jquery','jquery.tools.min'); - - // Include the jQuery-UI CSS - many more complex widgets use it - $theme = 'redmond'; - egw_framework::includeCSS("/phpgwapi/js/jquery/jquery-ui/$theme/jquery-ui-1.10.3.custom.css"); - // Load our CSS after jQuery-UI, so we can override it - egw_framework::includeCSS('/etemplate/templates/default/etemplate2.css'); - - // check if application of template has a app.js file --> load it - list($app) = explode('.',$this->name); - if (file_exists(EGW_SERVER_ROOT.'/'.$app.'/js/app.js')) - { - egw_framework::validate_file('.','app',$app,false); - } - - // check if we are in an ajax-exec call from jdots template (or future other tabbed templates) - if (isset($GLOBALS['egw']->framework->response)) - { - $content = '
'; - // add server-side page-generation times - if($GLOBALS['egw_info']['user']['preferences']['common']['show_generation_time']) + if ($GLOBALS['egw_info']['flags']['currentapp'] != 'etemplate') { - $vars = $GLOBALS['egw']->framework->_get_footer(); - $content .= "\n".$vars['page_generation_time']; + egw_framework::includeCSS('etemplate', 'app'); } - $GLOBALS['egw']->framework->response->generic("data", array($content)); - $GLOBALS['egw']->framework->response->generic('et2_load',$load_array+egw_framework::get_extra()); - egw_framework::clear_extra(); // to not send/set it twice for multiple etemplates (eg. CRM view) - self::$request = null; - return; } - echo $GLOBALS['egw']->framework->header(); - if ($output_mode != 2) + // saving the etemplate content for other hooked etemplate apps (atm. infolog hooked into addressbook) + self::$previous_content =& $html; + } + //echo '

'.__METHOD__."($method,...) after show: sitemgr=$this->sitemgr, hooked=".(int)$hooked.", output_mode=$output_mode

\n"; + + if($output_mode == 2) + { + $html .= ' +'."\n"; + } + + if (!$this->sitemgr && (int) $output_mode != 1 && (int) $output_mode != -1) // NOT returning html + { + if (!@self::$hooked) { - parse_navbar(); - } - else // mark popups as such, by enclosing everything in div#popupMainDiv - { - echo '
'."\n"; - } - echo '
'; - - if ($output_mode == 2) - { - echo "\n
\n"; - echo $GLOBALS['egw']->framework->footer(); - } - ob_flush(); - - // Send any accumulated json responses - after flush to avoid sending the buffer as a response - if(egw_json_response::isJSONResponse()) - { - $response = egw_json_response::get(); - echo ''; - } - } - self::$request = null; - } - - /** - * Process via Ajax submitted content - */ - static public function ajax_process_content($etemplate_exec_id, array $content) - { - //error_log(__METHOD__."(".array2string($etemplate_exec_id).', '.array2string($content).")"); - - self::$request = etemplate_request::read($etemplate_exec_id); - //error_log('request='.array2string(self::$request)); - - self::$response = egw_json_response::get(); - - if (!($template = self::instance(self::$request->template['name'], self::$request->template['template_set'], - self::$request->template['version'], self::$request->template['load_via']))) - { - throw new egw_exception_wrong_parameter('Can NOT read template '.array2string(self::$request->template)); - } - - // Set current app for validation - list($app) = explode('.',self::$request->method); - if(!$app) list($app) = explode('::',self::$request->method); - if($app) - { - translation::add_app($app); - $GLOBALS['egw_info']['flags']['currentapp'] = $app; - } - $validated = array(); - $expand = array( - 'cont' => &self::$request->content, - ); - $template->run('validate', array('', $expand, $content, &$validated), true); // $respect_disabled=true: do NOT validate disabled widgets and children - - if (self::validation_errors(self::$request->ignore_validation)) - { - error_log(__METHOD__."(,".array2string($content).') validation_errors='.array2string(self::$validation_errors)); - self::$response->generic('et2_validation_error', self::$validation_errors); - exit; - } - - // tell request call to remove request, if it is not modified eg. by call to exec in callback - self::$request->remove_if_not_modified(); - - //error_log(__METHOD__."(,".array2string($content).')'); - //error_log(' validated='.array2string($validated)); - $content = ExecMethod(self::$request->method, self::complete_array_merge(self::$request->preserv, $validated)); - - if (isset($GLOBALS['egw_info']['flags']['java_script'])) - { - // Strip out any script tags - $GLOBALS['egw_info']['flags']['java_script'] = preg_replace(array('/(]*>)([^<]*)/is','/<\/script>/'),array('$2',''),$GLOBALS['egw_info']['flags']['java_script']); - self::$response->script($GLOBALS['egw_info']['flags']['java_script']); - //error_log($app .' added javascript to $GLOBALS[egw_info][flags][java_script] - use egw_json_response->script() instead.'); - } - - return $content; - } - - /** - * Notify server that eT session/request is no longer needed, because user closed window - * - * @param string $_exec_id - */ - static public function ajax_destroy_session($_exec_id) - { - //error_log(__METHOD__."('$_exec_id')"); - if (($request = etemplate_request::read($_exec_id))) - { - $request->remove_if_not_modified(); - unset($request); - } - } - - /** - * Process via POST submitted content - */ - static public function process_exec() - { - if (get_magic_quotes_gpc()) $_POST['value'] = stripslashes($_POST['value']); - $content = json_decode($_POST['value'],true); - if($content == null && $_POST['exec']) - { - // Old etemplate submit - error_log("Old etemplate submitted"); - return ExecMethod('etemplate.etemplate_old.process_exec'); - } - error_log(__METHOD__."(".array2string($content).")"); - - self::$request = etemplate_request::read($_POST['etemplate_exec_id']); - - if (!($template = self::instance(self::$request->template['name'], self::$request->template['template_set'], - self::$request->template['version'], self::$request->template['load_via']))) - { - throw new egw_exception_wrong_parameter('Can NOT read template '.array2string(self::$request->template)); - } - $validated = array(); - $expand = array( - 'cont' => &self::$request->content, - ); - $template->run('validate', array('', $expand, $content, &$validated), true); // $respect_disabled=true: do NOT validate disabled widgets and children - if (self::validation_errors(self::$request->ignore_validation)) - { - error_log(__METHOD__."(,".array2string($content).') validation_errors='.array2string(self::$validation_errors)); - exit; - } - error_log(__METHOD__."(,".array2string($content).')'); - error_log(' validated='.array2string($validated)); - - // tell request call to remove request, if it is not modified eg. by call to exec in callback - self::$request->remove_if_not_modified(); - - return ExecMethod(self::$request->method, self::complete_array_merge(self::$request->preserv, $validated)); - } - - /** - * Path of template relative to EGW_SERVER_ROOT - * - * @var string - */ - public $rel_path; - - public $name; - public $template_set; - public $version; - public $laod_via; - - /** - * - * @var string If the template needs a div named other than the template name, this is it - */ - protected $dom_id; - - /** - * Reads an eTemplate from filesystem or DB (not yet supported) - * - * @param string $name name of the eTemplate or array with the values for all keys - * @param string $template_set=null default try template-set from user and if not found "default" - * @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 - * - * @ToDo supported customized templates stored in DB - */ - public function read($name,$template_set=null,$lang='default',$group=0,$version='',$load_via='') - { - unset($lang); unset($group); // not used, but in old signature - $this->rel_path = self::relPath($this->name=$name, $this->template_set=$template_set, - $this->version=$version, $this->laod_via = $load_via); - //error_log(__METHOD__."('$name', '$template_set', '$lang', $group, '$version', '$load_via') rel_path=".array2string($this->rel_path)); - - $this->dom_id = $name; - - return (boolean)$this->rel_path; - } - - /** - * Set the DOM ID for the etemplate div. If not set, it will be generated from the template name. - * - * @param string $new_id - */ - public function set_dom_id($new_id) - { - $this->dom_id = $new_id; - } - /** - * Get template data as array - * - * @return array - */ - public function as_array() - { - return array( - 'name' => $this->name, - 'template_set' => $this->template_set, - 'version' => $this->version, - 'load_via' => $this->load_via, - ); - } - - /** - * 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); - } - - /** - * 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 - */ - public 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 - is_array($v) && count($v) == 0 && is_array($old[$k])) // Empty array replacing non-empty + if((int) $output_mode != 2) { - $old[$k] = $v; + // jdots needs sidebox/navbar data in header call, therefore enable navbar BEFORE calling header + $GLOBALS['egw_info']['flags']['nonavbar'] = false; + echo $GLOBALS['egw']->framework->header(); + echo $GLOBALS['egw']->framework->navbar(); } else { - $old[$k] = self::complete_array_merge($old[$k],$v); + echo $GLOBALS['egw']->framework->header(); + echo '
'."\n"; + if ($GLOBALS['egw_info']['user']['apps']['manual']) // adding a manual icon to every popup + { + $manual = new etemplate_old('etemplate.popup.manual'); + echo $manual->show(array()); + unset($manual); + echo ''."\n"; + echo '
'.html::image('phpgwapi','ajax-loader') . '
'; + } } } + echo self::$hook_content.$html; + + if (!self::$hooked && (!isset($_GET['menuaction']) || strpos($_SERVER['PHP_SELF'],'process_exec.php') !== false)) + { + if((int) $output_mode == 2) + { + echo "
\n"; + } + common::egw_footer(); + } + } + if ($this->sitemgr || (int) $output_mode == 1 || (int) $output_mode == -1) // return html + { + return $html; } - return $old; } /** - * Debug callback just outputting content - * - * @param array $content=null - */ - public function debug(array $content=null) + * Check if we have not ignored validation errors + * + * @param string $ignore_validation='' if not empty regular expression for validation-errors to ignore + * @param string $cname=null name-prefix, which need to be ignored, default self::$name_vars + * @return boolean true if there are not ignored validation errors, false otherwise + */ + static function validation_errors($ignore_validation='',$cname=null) { - common::egw_header(); - _debug_array($content); - common::egw_footer(); + if (is_null($cname)) $cname = self::$name_vars; + //echo "

uiself::validation_errors('$ignore_validation','$cname') validation_error="; _debug_array(self::$validation_errors); + if (!$ignore_validation) return count(self::$validation_errors) > 0; + + foreach(self::$validation_errors as $name => $error) + { + if (!self::ignore_validation_match($name, $ignore_validation)) + { + //echo "

uiself::validation_errors('$ignore_validation','$cname') name='$name' ($error) not ignored!!!

\n"; + return true; + } + //echo "

uiself::validation_errors('$ignore_validation','$cname') name='$name' ($error) ignored

\n"; + } + return false; + } + + /** + * Check if given form-name matches ai ignore-validation rule + * + * @param string $ignore_validation='' if not empty regular expression for validation-errors to ignore + * @param string $cname=null name-prefix, which need to be ignored, default self::$name_vars + * @param string $cname + * @return boolean + */ + static function ignore_validation_match($form_name, $ignore_validation, $cname=null) + { + if (is_null($cname)) $cname = self::$name_vars; + if ($cname) $form_name = preg_replace('/^'.$cname.'\[([^\]]+)\](.*)$/','\\1\\2', $form_name); + + return empty($ignore_validation) || + $ignore_validation[0] == '/' && preg_match($ignore_validation, $form_name) || + $ignore_validation[0] != '/' && $ignore_validation == $form_name; + } + + /** + * Makes the necessary adjustments to _POST before it calls the app's method + * + * This function is only to submit forms to, create with exec. + * All eTemplates / forms executed with exec are submited to this function + * via /etemplate/process_exec.php?menuaction=. We cant use the global index.php as + * it would set some constants to etemplate instead of the calling app. + * process_exec then calls process_show for the eTemplate (to adjust the content of the _POST) and + * ExecMethod's the given callback from the app with the content of the form as first argument. + * + * @return mixed false if no sessiondata and $this->sitemgr, else the returnvalue of exec of the method-calls + */ + function process_exec($etemplate_exec_id = null, $submit_button = null, $exec = null, $type = 'regular' ) + { + if(!$etemplate_exec_id) $etemplate_exec_id = $_POST['etemplate_exec_id']; + if(!$submit_button) $submit_button = $_POST['submit_button']; + if(!$exec) $exec = $_POST; + + //echo "process_exec: _POST ="; _debug_array($_POST); + if (!$etemplate_exec_id || !(self::$request = etemplate_request::read($etemplate_exec_id))) + { + if ($this->sitemgr) return false; + //echo "uitemplate::process_exec() id='$_POST[etemplate_exec_id]' invalid session-data !!!"; _debug_array($_SESSION); + // this prevents an empty screen, if the sessiondata gets lost somehow + $redirect = array( + 'menuaction' => $_GET['menuaction'], + ); + if (!$_POST && $_SERVER['REQUEST_METHOD'] == 'POST') + { + $redirect['post_empty'] = 1; + // check if we have a failed upload, because user tried to uploaded a file + // bigger then php.ini setting post_max_size + // in that case the webserver calls PHP with $_POST === array() + if (substr($_SERVER['CONTENT_TYPE'],0,19) == 'multipart/form-data' && + $_SERVER['CONTENT_LENGTH'] > self::km2int(ini_get('post_max_size'))) + { + $redirect['failed_upload'] = 1; + $redirect['msg'] = lang('Error uploading file!')."\n".self::max_upload_size_message(); + } + } else if ($_SERVER['REQUEST_METHOD'] == 'GET') { + // Pass along any parameters + $redirect = $_GET; + } + $this->location($redirect); + } + self::$name_vars = self::$request->name_vars; + if (isset($submit_button) && !empty($submit_button)) + { + self::set_array($exec,$submit_button,'pressed'); + } + $content = $exec[self::$name_vars]; + if (!is_array($content)) + { + $content = array(); + } + $this->init(self::$request->template); + //echo "process_exec($this->name) content ="; _debug_array($content); + if ($GLOBALS['egw_info']['flags']['currentapp'] != 'etemplate') + { + $GLOBALS['egw']->translation->add_app('etemplate'); // some extensions have own texts + } + $this->process_show($content,self::$request->to_process,self::$name_vars,$type); + + self::$loop |= !$this->canceled && $this->button_pressed && + $this->validation_errors(self::$request->ignore_validation); // set by process_show + + // If a tab has an error on it, change to that tab + foreach(self::$validation_errors as $form_name => $msg) + { + $name = $this->template_name($form_name); + if (!$this->get_widget_by_name($name)) + { + foreach($this->get_widgets_by_type('tab') as $widget) + { + $tab_name = $tabs = $widget['name']; + if (strpos($tabs,'=') !== false) list($tab_name,$tabs) = explode('=',$tabs,2); + foreach(explode('|',$tabs) as $tab) + { + if (strpos('.',$tab) === false) $tab = $this->name.'.'.$tab; + $tab_tpl = new etemplate_old($tab); + if ($tab_tpl->get_widget_by_name($name)) + { + $content[$tab_name] = $tab; + break 3; + } + elseif($name[0] == '#') + { + foreach($tab_tpl->get_widgets_by_type('customfields') as $cf_widget) + { + if (empty($cf_widget['name'])) + { + $content[$tab_name] = $tab; + break 4; + } + } + } + } + } + // widget NOT found --> as last resort add valdation message to $content['msg'] + $content['msg'] .= ($content['msg'] ? "\n" : '').$msg; + } + } + + //echo "process_exec($this->name) process_show(content) ="; _debug_array($content); + //echo "process_exec($this->name) session_data[changes] ="; _debug_array(self::$request->changes); + $content = self::complete_array_merge(self::$request->changes,$content); + //echo "process_exec($this->name) merge(changes,content) ="; _debug_array($content); + + if (self::$loop && $type == 'regular') // only loop for regular (not ajax_submit) requests + { + if (self::$request->hooked != '') // set previous phpgw_body if we are called as hook + { + self::$hook_content = self::$request->hooked; + $GLOBALS['egw_info']['flags']['currentapp'] = self::$hook_app = self::$request->hook_app; + + // Prevent previous content form names from being used again in exec() + preg_match_all('/
hooked, $used); + if(is_array($used[1]) && count($used[1])) + { + self::$name_forms += $used[1]; + } + } + if(self::$request->include_xajax) $GLOBALS['egw_info']['flags']['include_xajax'] = true; + + if (!empty(self::$request->app_header)) + { + $GLOBALS['egw_info']['flags']['app_header'] = self::$request->app_header; + } + + $GLOBALS['egw_info']['flags']['java_script'] .= self::$request->java_script_from_flags; + if (!empty(self::$request->java_script_body_tags)) + { + foreach (self::$request->java_script_body_tags as $tag => $code) + { + call_user_func('egw_framework::set_'.$tag,$code); + } + } + if (is_array(self::$request->java_script_files)) + { + $files = egw_framework::js_files(); + if (is_array($files)) + { + $files = array_unique(array_merge($files,self::$request->java_script_files)); + } + else + { + $files = self::$request->java_script_files; + } + egw_framework::js_files($files); + } + + //echo "

process_exec($this->name): loop is set, content=

\n"; _debug_array(self::complete_array_merge(self::$request->content,$content)); + return $this->exec(self::$request->method,self::complete_array_merge(self::$request->content,$content), + self::$request->sel_options,self::$request->readonlys,self::$request->preserv, + self::$request->output_mode,self::$request->ignore_validation,$content); + } + else + { + //echo "

process_exec($this->name): calling ".($type == 'regular' ? self::$request->method : $_GET['menuaction'])."

\n"; + return ExecMethod($type == 'regular' ? self::$request->method : $_GET['menuaction'], + self::complete_array_merge(self::$request->preserv,$content)); + } } /** @@ -525,28 +592,6 @@ class etemplate_new extends etemplate_widget_template " (php.ini: upload_max_filesize=$upload_max_filesize, post_max_size=$post_max_size)"; } - /** - * Format a number according to user prefs with decimal and thousands separator (later only for readonly) - * - * @param int|float|string $number - * @param int $num_decimal_places=2 - * @param boolean $readonly=true - * @return string - */ - static public function number_format($number,$num_decimal_places=2,$readonly=true) - { - static $dec_separator=null,$thousands_separator=null; - if (is_null($dec_separator)) - { - $dec_separator = $GLOBALS['egw_info']['user']['preferences']['common']['number_format'][0]; - if (empty($dec_separator)) $dec_separator = '.'; - $thousands_separator = $GLOBALS['egw_info']['user']['preferences']['common']['number_format'][1]; - } - if ((string)$number === '') return ''; - - return number_format(str_replace(' ','',$number),$num_decimal_places,$dec_separator,$readonly ? $thousands_separator : ''); - } - /** * Convert numbers like '32M' or '512k' to integers * @@ -571,9 +616,40 @@ class etemplate_new extends etemplate_widget_template } /** - * creates HTML from an eTemplate + * process the values transfered with the javascript function values2url * - * Compatibility function for calendar, which uses etemplate::show to generate html --> use etemplate_old class + * The returned array contains the preserved values overwritten (only!) with the variables named in values2url + * + * @return array/boolean content array or false on error + */ + function process_values2url() + { + //echo "process_exec: _GET ="; _debug_array($_GET); + if (!$_GET['etemplate_exec_id'] || !($request = etemplate_request::read($_GET['etemplate_exec_id']))) + { + return false; + } + self::$name_vars = $request->name_vars; + + $content = $_GET[self::$name_vars]; + if (!is_array($content)) + { + $content = array(); + } + $this->process_show($content,$request->to_process,self::$name_vars); + + return self::complete_array_merge($request->preserv,$content); + } + + /** + * Flag if the styles of a certain template are already included + * + * @var array template-name => boolean + */ + static private $styles_included = array(); + + /** + * creates HTML from an eTemplate * * This is done by calling show_cell for each cell in the form. show_cell itself * calls show recursivly for each included eTemplate. @@ -581,6 +657,7 @@ class etemplate_new extends etemplate_widget_template * This is intended as internal function and should NOT be called by new app's direct, * as it deals with HTML and is so UI-dependent, use exec instead. * + * @internal * @param array $content with content for the cells, keys are the names given in the cells/form elements * @param array $sel_options with options for the selectboxes, keys are the name of the selectbox * @param array $readonlys with names of cells/form-elements to be not allowed to change @@ -593,53 +670,1847 @@ class etemplate_new extends etemplate_widget_template */ function show($content,$sel_options='',$readonlys='',$cname='',$show_c=0,$show_row=0) { - $etemplate_old = new etemplate_old($this->name, $this->laod_via); + if (!$sel_options) + { + $sel_options = array(); + } + // make it globaly availible for show_cell and show_grid, or extensions + $this->sel_options =& $sel_options; - return $etemplate_old->show($content,$sel_options,$readonlys,$cname,$show_c,$show_row); + if (!$readonlys) + { + $readonlys = array(); + } + if (++$this->already_showed > 1) return ''; // prefens infinit self-inclusion + + if (is_int($this->debug) && $this->debug >= 1 || $this->name && $this->debug == $this->name) + { + echo "

etemplate.show($this->name): $cname =\n"; _debug_array($content); + echo "readonlys="; _debug_array($readonlys); + } + if (!is_array($content)) + { + $content = array(); // happens if incl. template has no content + } + // make the content availible as class-public for extensions + $this->content =& $content; + + $html = "\n\n\n

name)."\">\n\n"; + if (!self::$styles_included[$this->name]) + { + self::$styles_included[$this->name] = True; + $html .= html::style($this->style)."\n\n"; + } + $path = '/'; + foreach ($this->children as $n => $child) + { + $h = $this->show_cell($child,$content,$readonlys,$cname,$show_c,$show_row,$nul,$class,$path.$n); + $html .= $class || $child['align'] ? html::div($h,html::formatOptions(array( + $class, + $child['align'], + ),'class,align')) : $h; + } + return $html."\n
\n\n\n"; } /** - * Avoid fatal error from addressbook + * Get the color of a category + * + * For multiple cats, the first with a color is used + * + * @param int/string $cats multiple comma-separated cat_id's + * @return string + */ + static function cats2color($cats) + { + static $cat2color; + + // ACL check + $cats = $GLOBALS['egw']->categories->check_list(EGW_ACL_READ,$cats); + + if (!$cats) return null; + + if (isset($cat2color[$cats])) + { + return $cat2color[$cats]; + } + + foreach(explode(',',$cats) as $cat) + { + if (isset($cat2color[$cat])) + { + return $cat2color[$cat]; + } + $data = categories::id2name($cat,'data'); + + if (is_array($data) && ($color = $data['color'])) + { + //echo "

cats2color('$cats')=$color

\n"; + return $cat2color[$cats] = $cat2color[$cat] = $color; + } + } + return null; + } + + /** + * creates HTML from an eTemplate + * + * This is done by calling show_cell for each cell in the form. show_cell itself + * calls show recursivly for each included eTemplate. + * You can use it in the UI-layer of an app, just make shure to call process_show !!! + * This is intended as internal function and should NOT be called by new app's direct, + * as it deals with HTML and is so UI-dependent, use exec instead. + * + * @internal + * @param array $grid representing a grid + * @param array $content with content for the cells, keys are the names given in the cells/form elements + * @param array $readonlys with names of cells/form-elements to be not allowed to change + * This is to facilitate complex ACL's which denies access on field-level !!! + * @param string $cname basename of names for form-elements, means index in $_POST + * eg. $cname='cont', element-name = 'name' returned content in $_POST['cont']['name'] + * @param string $show_c name/index for name expansion + * @param string $show_row name/index for name expansion + * @param string $path path in the widget tree + * @return string the generated HTML + */ + private function show_grid(&$grid,$content,$readonlys='',$cname='',$show_c=0,$show_row=0,$path='') + { + if (!$readonlys) + { + $readonlys = array(); + } + if (is_int($this->debug) && $this->debug >= 2 || $grid['name'] && $this->debug == $grid['name'] || + $this->name && $this->debug == $this->name) + { + echo "

etemplate.show_grid($grid[name]): $cname =\n"; _debug_array($content); + } + if (!is_array($content)) + { + $content = array(); // happens if incl. template has no content + } + $content += array( // for var-expansion in names in show_cell + '.c' => $show_c, + '.col' => $this->num2chrs($show_c-1), + '.row' => $show_row + ); + $rows = array(); + + $data = &$grid['data']; + reset($data); + if (isset($data[0])) + { + list(,$opts) = each($data); + } + else + { + $opts = array(); + } + $row_id = $content['_row_id']; + + $max_cols = $grid['cols']; + for ($r = 0; $row = 1+$r /*list($row,$cols) = each($data)*/; ++$r) + { + if (!(list($r_key) = each($data))) // no further row + { + if (!(($this->autorepeat_idx($cols['A'],0,$r,$idx,$idx_cname,false,$content) && $idx_cname) || + (in_array($cols['A']['type'], array('vbox','hbox','box')) && $this->autorepeat_idx($cols['A'][1],0,$r,$idx,$idx_cname,false,$content) && $idx_cname) || + ($this->autorepeat_idx($cols['B'],1,$r,$idx,$idx_cname,false,$content) && $idx_cname)) || + !$this->isset_array($content,$idx_cname)) + { + break; // no auto-row-repeat + } + } + else + { + $cols = &$data[$r_key]; + $part = ''; // '' = body-prefix + list($height,$disabled,$part) = explode(',',$opts["h$row"]); + $class = $opts["c$row"]; + } + if ($disabled != '' && $this->check_disabled($disabled,$content,$r)) + { + continue; // row is disabled + } + if ($part) $row = $part[0].$row; // add part prefix + $rows[".$row"] .= html::formatOptions($height,'height'); + list($cl) = explode(',',$class); + if ($cl == '@' || $cl && strpos($cl,'$') !== false) + { + $cl = $this->expand_name($cl,0,$r,$content['.c'],$content['.row'],$content); + if (!$cl || preg_match('/(^| )([0-9,]+)( |$)/',$cl,$matches)) + { + if (($color = $this->cats2color($matches[2]))) + { + $rows[".$row"] .= ' style="background-color: '.$color.';"'; + } + $cl = str_replace($matches[2],'row',$cl); + } + } + if ($cl == 'nmr' || substr($cl,0,3) == 'row') // allow to have further classes behind row + { + $cl = 'row_'.($nmr_alternate++ & 1 ? 'off' : 'on').substr($cl,3); // alternate color + } + $cl = isset(self::$class_conf[$cl]) ? self::$class_conf[$cl] : $cl; + $rows[".$row"] .= html::formatOptions($cl,'class'); + $rows[".$row"] .= html::formatOptions($class,',valign'); + // set row-id, if requested + if ($row_id && isset($content[$r][$row_id])) + { + $rows[".$row"] .= ' id="'.htmlspecialchars($content[$r][$row_id]).'"'; + } + reset ($cols); + $row_data = array(); + for ($c = 0; True /*list($col,$cell) = each($cols)*/; ++$c) + { + $col = $this->num2chrs($c); + if (!(list($c_key) = each($cols))) // no further cols + { + // only check if the max. column-number reached so far is exeeded + // otherwise the rows have a differen number of cells and it saved a lot checks + if ($c >= $max_cols) + { + if (!$this->autorepeat_idx($cell,$c,$r,$idx,$idx_cname,True,$content) || + !$this->isset_array($content,$idx)) + { + break; // no auto-col-repeat + } + $max_cols = $c+1; + } + } + else + { + $cell = $cols[$c_key]; + list($col_width,$col_disabled) = explode(',',$opts[$col]); + + if (!$cell['height']) // if not set, cell-height = height of row + { + $cell['height'] = $height; + } + if (!$cell['width']) // if not set, cell-width = width of column or table + { + list($col_span) = explode(',',$cell['span']); + if ($col_span == 'all' && !$c) + { + list($cell['width']) = explode(',',$this->size); + } + else + { + $cell['width'] = $col_width; + } + } + } + if ($col_disabled != '' && $this->check_disabled($col_disabled,$content,$r,$c)) + { + continue; // col is disabled + } + $align_was = $cell['align']; + $row_data[$col] = $this->show_cell($cell,$content,$readonlys,$cname,$c,$r,$span,$cl,$path.'/'.$r_key.$c_key); + + if ($row_data[$col] == '' && $this->rows == 1) + { + unset($row_data[$col]); // omit empty/disabled cells if only one row + continue; + } + if (strlen($cell['onclick']) > 1) + { + $onclick = $cell['onclick']; + if (strpos($onclick,'$') !== false || $onclick[0] == '@') + { + $onclick = $this->expand_name($onclick,$c,$r,$content['.c'],$content['.row'],$content); + } + $row_data[".$col"] .= ' onclick="'.$this->js_pseudo_funcs($onclick,$cname).'"' .self::get_id('',$cell['name'],$cell['id']); + } + $colspan = $span == 'all' ? $grid['cols']-$c : 0+$span; + if ($colspan > 1) + { + $row_data[".$col"] .= " colspan=\"$colspan\""; + for ($i = 1; $i < $colspan; ++$i,++$c) + { + each($cols); // skip next cell(s) + } + } + else + { + list($width,$disable) = explode(',',$opts[$col]); + if ($width) // width only once for a non colspan cell + { + $row_data[".$col"] .= " width=\"$width\""; + $opts[$col] = "0,$disable"; + } + } + $row_data[".$col"] .= html::formatOptions($cell['align']?$cell['align']:($align_was?$align_was:'left'),'align'); + // allow to set further attributes in the tablecell, beside the class + if (is_array($cl)) + { + foreach($cl as $attr => $val) + { + if ($attr != 'class' && $val) + { + $row_data['.'.$col] .= ' '.$attr.'="'.$val.'"'; + } + } + $cl = $cl['class']; + } + $cl = $this->expand_name(isset(self::$class_conf[$cl]) ? self::$class_conf[$cl] : $cl, + $c,$r,$show_c,$show_row,$content); + // else the class is set twice, in the table and the table-cell, which is not good for borders + if ($cl && $cell['type'] != 'template' && $cell['type'] != 'grid') + { + $row_data[".$col"] .= html::formatOptions($cl,'class'); + } + } + $rows[$row] = $row_data; + } + if (!$rows) return ''; + + list($width,$height,,,,,$overflow) = $options = explode(',',$grid['size']); + if ($overflow && $height) + { + $options[1] = ''; // set height in div only + } + $html = html::table($rows,html::formatOptions($options,'width,height,border,class,cellspacing,cellpadding'). + html::formatOptions($grid['span'],',class'). + html::formatOptions($grid['name']?self::form_name($cname,$grid['name']):'','id')); + + if (!empty($overflow)) + { + if (is_numeric($height)) $height .= 'px'; + if (is_numeric($width)) $width .= 'px'; + if ($width == '100%') $overflow .= '; overflow-x: hidden'; // no horizontal scrollbar + $div_style=' style="'.($width?"width: $width; ":'').($height ? "height: $height; ":'')."overflow: $overflow;\""; + $html = html::div($html,$div_style); + } + + // initialise egw_actions for nextmatch widget, if egwGridView_grid CSS class set + if ($options[3] == 'egwGridView_grid') + { + $html .= nextmatch_widget::init_egw_actions($content['_actions'], $content['action_links'], $this->name); + } + + return "\n\n\n$html\n\n"; + } + + /** + * build the name of a form-element from a basename and name + * + * name and basename can contain sub-indices in square bracets, eg. basename="base[basesub1][basesub2]" + * and name = "name[sub]" gives "base[basesub1][basesub2][name][sub]" + * + * @param string $cname basename + * @param string $name name + * @return string complete form-name + */ + static function form_name($cname,$name) + { + if(is_object($name)) return ''; + + $name_parts = explode('[',str_replace(']','',$name)); + if (!empty($cname)) + { + array_unshift($name_parts,$cname); + } + $form_name = array_shift($name_parts); + if (count($name_parts)) + { + $form_name .= '['.implode('][',$name_parts).']'; + } + return $form_name; + } + + /** + * strip the prefix of a form-element from a form_name + * This function removes the prefix of form_name(). It takes a name like base[basesub1][basesub2][name][sub] + * and gives basesub1[basesub2][name][sub] + * + * @param string form_name + * @return string name without prefix + */ + static private function template_name($form_name) + { + $parts = explode('[',str_replace(']','',$form_name)); + + array_shift($parts); // remove exec + + $name = array_shift($parts); + + if ($parts) $name .= '['.implode('][',$parts).']'; + + return $name; + } + + static private $class_conf = array('nmh' => 'th','nmr0' => 'row_on','nmr1' => 'row_off'); + + /** + * generates HTML for one widget (input-field / cell) + * + * calls show to generate included eTemplates. Again only an INTERMAL function. + * + * @internal + * @param array $cell with data of the cell: name, type, ... + * @param array $content with content for the cells, keys are the names given in the cells/form elements + * @param array $readonlys with names of cells/form-elements to be not allowed to change + * This is to facilitate complex ACL's which denies access on field-level !!! + * @param string $cname basename of names for form-elements, means index in $_POST + * eg. $cname='cont', element-name = 'name' returned content in $_POST['cont']['name'] + * @param string $show_c name/index for name expansion + * @param string $show_row name/index for name expansion + * @param string &$span on return number of cells to span or 'all' for the rest (only used for grids) + * @param string &$class on return the css class of the cell, to be set in the tag + * @param string $path path in the widget tree + * @return string the generated HTML + */ + private function show_cell(&$cell,$content,$readonlys,$cname,$show_c,$show_row,&$span,&$class,$path='') + { + if ($this->debug && (is_int($this->debug) && $this->debug >= 3 || $this->debug == $cell['type'])) + { + echo "

etemplate.show_cell($this->name,name='${cell['name']}',type='${cell['type']}',cname='$cname',...,'$path')

\n"; + } + list($span) = explode(',',$cell['span']); // evtl. overriten later for type template + + if ($cell['name'][0] == '@' && $cell['type'] != 'template') + { + $cell['name'] = $this->get_array($content,$this->expand_name(substr($cell['name'],1), + $show_c,$show_row,$content['.c'],$content['.row'],$content)); + } + $name = $this->expand_name($cell['name'],$show_c,$show_row,$content['.c'],$content['.row'],$content); + // allow names like "tabs=one|two|three", which will be equal to just "tabs" + // eg. for tabs to use a name independent of the tabs contained + if (is_string($name) && strpos($name,'=') !== false) + { + list($name) = explode('=',$name); + } + $form_name = self::form_name($cname,$name); + $value =& $this->get_array($content,$name); + $old_value = $value; // remember value to be able to restore it + $options = ''; + if ($readonly = $cell['readonly'] && $readonlys[$name] !== false || // allow to overwrite readonly settings of a cell + @$readonlys[$name] && !is_array($readonlys[$name]) || $readonlys['__ALL__'] && (!is_string($name) || $readonlys[$name] !== false) || + !empty($name) && is_string($name) && ($p = strrpos($name,'[')) !== false && ($parent=substr($name,0,$p)) && $readonlys[$parent]) // allow also set parent readonly (instead each child) + { + $options .= ' readonly="readonly"'; + } + if ((int) $cell['tabindex']) $options .= ' tabindex="'.(int)$cell['tabindex'].'"'; + if ($cell['accesskey']) $options .= ' accesskey="'.html::htmlspecialchars($cell['accesskey']).'"'; + + if (is_string($cell['size']) && (strchr($cell['size'],'$') || $cell['size'][0] == '@')) // expand cell['size'] for the button-disabled-check now + { + $cell['size'] = $this->expand_name($cell['size'],$show_c,$show_row,$content['.c'],$content['.row'],$content); + } + if ($cell['type'] == 'template' && !$this->check_disabled($cell['disabled'], $content)) + { + // template is NOT disabled (eg. in editor) + } + elseif ($cell['disabled'] && $readonlys[$name] !== false || $readonly && in_array($cell['type'],array('button','buttononly','image','progress')) && strpos($cell['size'],',') === false) + { + if ($this->rows == 1) + { + return ''; // if only one row omit cell + } + $cell = $this->empty_cell('label','',array('span' => $cell['span'])); // show nothing (keep the css class!) + $name = $value = ''; + } + $extra_label = True; + + if (strchr($cell['onchange'],'$') || $cell['onchange'][0] == '@') + { + $cell['onchange'] = $this->expand_name($cell['onchange'],$show_c,$show_row,$content['.c'],$content['.row'],$content); + } + if ($cell['type'][0] == '@') + { + $cell['type'] = $this->expand_name($t=$cell['type'],$show_c,$show_row,$content['.c'],$content['.row'],$content); + } + // the while loop allows to build extensions from other extensions + // please note: only the first extension's post_process function is called !!! + list($type,$sub_type) = explode('-',$cell['type']); + while ((!self::$types[$cell['type']] || !empty($sub_type)) && $this->haveExtension($type,'pre_process')) + { + //echo "

pre_process($cell[name]/$cell[type])

\n"; + if (is_string($cell['size']) && (strchr($cell['size'],'$') || $cell['size'][0] == '@')) + { + $cell['size'] = $this->expand_name($cell['size'],$show_c,$show_row,$content['.c'],$content['.row'],$content); + } + if (strchr($cell['onchange'],'$') || $cell['onchange'][0] == '@') + { + $cell['onchange'] = $this->expand_name($cell['onchange'],$show_c,$show_row,$content['.c'],$content['.row'],$content); + } + if (!$ext_type) $ext_type = $type; + // if readonlys[__ALL__] is set, also set readonlys[$name] (extensions can mark themselfs as 'noReadonlysALL', eg. tab-widget!) + if ($readonlys['__ALL__'] && !$this->haveExtension($type,'noReadonlysALL')) + { + $readonlys[$name] = true; + } + $extra_label = $this->extensionPreProcess($type,$form_name,$value,$cell,$readonlys[$name]); + + $readonly = $cell['readonly'] !== false && ($readonly || $cell['readonly']); // might be set or unset (===false) by extension + + self::set_array($content,$name,$value); + + if ($cell['type'] == $type.'-'.$sub_type) break; // stop if no further type-change + + list($type,$sub_type) = explode('-',$cell['type']); + } + list(,$class) = explode(',',$cell['span']); // might be set by extension + if (strchr($class,'$') || $class[0] == '@') + { + $class = $this->expand_name($class,$show_c,$show_row,$content['.c'],$content['.row'],$content); + } + if ($cell['needed'] && !in_array($cell['type'],array('button','buttononly'))) + { + $class .= ' inputRequired'; + } + $cell_options = $cell['size']; + if (strchr($cell_options,'$') || $cell_options[0] == '@') + { + $cell_options = $this->expand_name($cell_options,$show_c,$show_row,$content['.c'],$content['.row'],$content); + } + $label = $cell['label']; + if (strchr($label,'$') || $label[0] == '@') + { + $label = $this->expand_name($label,$show_c,$show_row,$content['.c'],$content['.row'],$content); + } + $help = $cell['help']; + if (strchr($help,'$') || $help[0] == '@') + { + $no_lang_on_help = true; + $help = $this->expand_name($help,$show_c,$show_row,$content['.c'],$content['.row'],$content); + } + $blur = $cell['blur'][0] == '@' ? $this->get_array($content,substr($cell['blur'],1)) : + (strlen($cell['blur']) <= 1 ? $cell['blur'] : lang($cell['blur'])); + + if ($blur) + { + if ((string)$value === '') + { + $value = $blur; + } + $onFocus .= "if(this.value=='".addslashes(html::htmlspecialchars($blur))."') this.value='';"; + $onBlur .= "if(this.value=='') this.value='".addslashes(html::htmlspecialchars($blur))."';"; + } + if ($help) + { + if ((int)$cell['no_lang'] < 2 && !$no_lang_on_help) + { + if (($use_tooltip_for_help = $help[0] == '|')) $help = substr($help,1); + $help = lang($help); + } + if (substr($help,0,5) == 'call:') + { + $options .= ' onMouseOver="'.html::htmlspecialchars(substr($help,5)).'"'; + } + elseif (($use_tooltip_for_help = $use_tooltip_for_help || strpos($help,'<') !== false && strip_tags($help) != $help)) // helptext is html => use a tooltip + { + $options .= html::tooltip($help); + } + else // "regular" help-text in the statusline + { + $onFocus .= "self.status='".addslashes(html::htmlspecialchars($help))."'; return true;"; + $onBlur .= "self.status=''; return true;"; + if (in_array($cell['type'],array('button','buttononly','file'))) // for button additionally when mouse over button + { + $options .= " onMouseOver=\"self.status='".addslashes(html::htmlspecialchars($help))."'; return true;\""; + $options .= " onMouseOut=\"self.status=''; return true;\""; + } + } + } + if ($onBlur) + { + $options .= " onFocus=\"$onFocus\" onBlur=\"$onBlur\""; + } + if ($cell['onchange'] && !($cell['type'] == 'button' || $cell['type'] == 'buttononly')) + { + $onchange = $cell['onchange'] == '1' ? 'this.form.submit();' : $this->js_pseudo_funcs($cell['onchange'],$cname); + // rewriting onchange for checkboxes for IE to an onclick + if ($cell['type'] == 'checkbox' && html::$user_agent == 'msie') + { + $options .= ' onClick="'.$onchange.'; return true;"'; + } + else + { + $options .= ' onChange="'.$onchange.'"'; + } + } + + if ($form_name != '') + { + $options = self::get_id($form_name,$cell['name'],$cell['id']).' '.$options; + } + switch ($type) + { + case 'label': // size: [b[old]][i[talic]],[link],[activate_links],[label_for],[link_target],[link_popup_size],[link_title] + if (is_array($value)) break; + if($cell_options) + { + list($style,$extra_link,$activate_links,$label_for,$extra_link_target,$extra_link_popup,$extra_link_title) = self::csv_split($cell_options,7); + } + else + { + $style = $cell['font_style']; + $extra_link = $cell['href']; + $activate_links = $cell['activate_links']; + $label_for = $cell['for']; + $extra_link_target = $cell['extra_link_target']; + $extra_link_popup = $cell['extra_link_popup']; + $extra_link_title = $cell['extra_link_title']; + } + $value = strlen($value) > 1 && !$cell['no_lang'] ? lang($value) : $value; + $value = nl2br(html::htmlspecialchars($value)); + if ($activate_links) $value = html::activate_links($value); + if ($value != '' && $style && strpos($style,'b')!==false) $value = html::bold($value); + if ($value != '' && $style && strpos($style,'i')!==false) $value = html::italic($value); + // if the label has a name, use it as id in a span, to allow addressing it via javascript + $html .= ($name ? '' : '').$value.($name ? '' : ''); + if ($help) + { + $class = array( + 'class' => $class, + ); + list($class['onmouseover'],$class['onmouseout']) = html::tooltip($help, False, False, true); + } + break; + case 'html': // size: [link],[link_target],[link_popup_size],[link_title],[activate_links] + list($extra_link,$extra_link_target,$extra_link_popup,$extra_link_title,$activate_links) = explode(',',$cell_options); + if ($activate_links) $value = html::activate_links($value); + $html .= $value; + break; + case 'int': // size: [min],[max],[len],[precision/sprint format],[step] + case 'integer': + case 'float': + list($min,$max,$cell_options,$pre,$step) = explode(',',$cell_options); + // a few html5 options + if ((string)$min !== '') $options .= ' min="'.htmlspecialchars($min).'"'; + if ((string)$max !== '') $options .= ' max="'.htmlspecialchars($max).'"'; + // default step="any" for float, as not setting it limits value to integer as step defaults to 1 in html5! + if (is_numeric($step) || $type == 'float') + { + $options .= ' step="'.(is_numeric($step)?$step:'any').'"'; + } + if ($cell_options == '' && !$readonly) + { + $cell_options = $cell['type'] != 'float' ? 5 : 8; + } + // html5 input type=nummeric seems to ignore size, setting a width instead + $options .= ' style="width: '.(3+abs($cell_options)).'ex"'; + if (($type == 'float' || !is_numeric($pre)) && $value && $pre) + { + $value = is_numeric($pre) ? self::number_format($value,$pre,$readonly) : sprintf($pre,$value); + } + $cell_options .= ',,'.($cell['type'] != 'float' ? '/^-?[0-9]*$/' : '"/^-?[0-9]*[,.]?[0-9]*$/"').',number'; + // fall-through + case 'hidden': + case 'passwd': + case 'text': // size: [length][,maxLength[,preg[,html5type]]] + case 'textbox': + $cell_opts = $c = self::csv_split($cell_options); // allows to enclose preg in quote to allow comma + // fix preg, in case it contains a comma (html5type is only letters and always last option!) + if (count($cell_opts) > 3 && ($cell_opts2 = explode(',',$cell_options)) && $cell_opts2[2][0] != '"') + { + $html5type = array_pop($cell_opts); + $cell_opts = explode(',',$cell_options,3); + if (preg_match('/^[a-z]+$/i',$html5type)) + { + $cell_opts[2] = substr($cell_opts[2],0,-strlen($html5type)-1); + if ($cell_opts[2][0] == ',') $cell_opts[2] = ''; // happens in link-entry under some condition + $cell_opts[] = $html5type; + } + } + if ($readonly && (int)$cell_opts[0] >= 0) + { + $html .= strlen($value) ? html::bold(html::htmlspecialchars($value)) : ''; + } + else + { + if ($cell_opts[0] < 0) + { + $cell_opts[0] = abs($cell_opts[0]); + $options .= ' readonly="readonly"'; + } + // only add html5 required attribute, if validation is NOT ignored + if ($cell['needed'] && self::ignore_validation_match($form_name, self::$request->ignore_validation, $cname)) + { + $required = ' required'; + } + $html .= html::input($form_name,$value,$type == 'passwd' ? 'password' : ($type == 'hidden' ? 'hidden' : $cell_opts[3]), + $options.html::formatOptions($cell_opts,'SIZE,MAXLENGTH'). + $required.($type == 'passwd'?' autocomplete="off"':'')); + + if (!$readonly) + { + self::$request->set_to_process($form_name,$cell['type'],array( + 'maxlength' => $cell_opts[1], + 'needed' => $cell['needed'], + 'preg' => $cell_opts[2], + 'min' => $min, // int and float only + 'max' => $max, + )); + } + } + unset($cell_opts); + break; + case 'textarea': // Multiline Text Input, size: [rows][,cols] + if ($readonly && !$cell_options) + { + $html .= '
'.nl2br(html::htmlspecialchars($value))."
\n"; + } + else + { + // if textarea is readonly, but form_name is already used by an other widget, dont use it + // browser would only send the content of the readonly (and therefore unchanged) field + if ($readonly && self::$request->isset_to_process($form_name)) $form_name = ''; + $html .= html::textarea($form_name,$value, + $options.html::formatOptions($cell_options,'ROWS,COLS').($cell['needed']?' required="required"':'')); + } + if (!$readonly) + { + self::$request->set_to_process($form_name,$cell['type'],array( + 'needed' => $cell['needed'], + )); + } + break; + case 'htmlarea': // Multiline formatted Text Input, size: {simple|extended|advanced},height,width,toolbar-expanded,upload-path + list($mode,$height,$width,$toolbar,$baseref,$convertnl) = explode(',',$cell_options); + + if ($convertnl) + { + $value = nl2br(html::htmlspecialchars($value)); + } + if (!$readonly) + { + $height = $height ? $height : '400px'; + $width = $width ? $width : '100%'; + $fckoptions = array( + 'toolbar_expanded' => $toolbar, + ); + // html::fckEditor runs everything through html::purify + $html .= html::fckEditor($form_name,$value,$mode,$fckoptions,$height,$width,$baseref); + + self::$request->set_to_process($form_name,$cell['type'],array( + 'needed' => $cell['needed'], + 'mode' => $mode, // need mode to not run purify for $mode=='ascii' + )); + } + else + { + $html .= html::div(html::purify(html::activate_links($value)),'style="overflow: auto; width='. $width. '; height='. $height. '"'); + } + break; + case 'checkbox': + $set_val = 1; $unset_val = 0; + if (!empty($cell_options)) + { + list($set_val,$unset_val,$ro_true,$ro_false) = self::csv_split($cell_options); + if (!$set_val && !$unset_val) $set_val = 1; + $value = $value == $set_val; + } + if (($multiple = substr($cell['name'],-2) == '[]') && !$readonly) + { + $readonly = $readonlys[substr($cell['name'],0,-1).$set_val.']']; + } + if ($readonly) + { + if (count(explode(',',$cell_options)) < 3) + { + $ro_true = 'x'; + $ro_false = ''; + } + if (!$value && $ro_false == 'disable') return ''; + + $html .= $value ? html::bold($ro_true) : $ro_false; + } + else + { + if ($value) $options .= ' checked="checked"'; + + if ($multiple) + { + // add the set_val to the id to make it unique + $options = str_replace('id="'.$form_name,'id="'.substr($form_name,0,-2)."[$set_val]",$options); + } + $html .= html::input($form_name,$set_val,'checkbox',$options); + + if ($multiple) $form_name = self::form_name($cname,substr($cell['name'],0,-2)); + + if (!self::$request->isset_to_process($form_name)) + { + self::$request->set_to_process($form_name,$cell['type'],array( + 'unset_value' => $unset_val, + 'multiple' => $multiple, + 'needed' => $cell['needed'], + )); + } + self::$request->set_to_process_attribute($form_name,'values',$set_val,true); + if (!$multiple) unset($set_val); // otherwise it will be added to the label + } + break; + case 'radio': // size: value if checked, readonly set, readonly unset + list($set_val,$ro_true,$ro_false) = self::csv_split($cell_options); + $set_val = $this->expand_name($set_val,$show_c,$show_row,$content['.c'],$content['.row'],$content); + + if ($value == $set_val) + { + $options .= ' checked="checked"'; + } + // add the set_val to the id to make it unique + $options = str_replace('id="'.$form_name,'id="'.$form_name."[$set_val]",$options); + + if ($readonly) + { + if (!$ro_true && !$ro_false) $ro_true = 'x'; + $html .= $value == $set_val ? html::bold($ro_true) : $ro_false; + } + else + { + $html .= html::input($form_name,$set_val,'RADIO',$options); + self::$request->set_to_process($form_name,$cell['type'],array( + 'needed' => $cell['needed'], + )); + } + break; + case 'button': + case 'buttononly': + case 'cancel': // cancel button + if ($name == 'cancel' || stripos($name,'[cancel]') !== false) $type = 'cancel'; + list($app) = explode('.',$this->name); + list($img,$ro_img) = explode(',',$cell_options); + if ($img[0] != '/' && strpos($img,'/') !== false && count($img_parts = explode('/',$img)) == 2) + { + list($app,$img) = $img_parts; // allow to specify app in image name (eg. img='addressbook/navbar') + } + $title = strlen($label) <= 1 || $cell['no_lang'] ? $label : lang($label); + if ($cell['onclick'] && + ($onclick = $this->expand_name($cell['onclick'],$show_c,$show_row,$content['.c'],$content['.row'],$content))) + { + $onclick = $this->js_pseudo_funcs($onclick,$cname); + } + unset($cell['onclick']); // otherwise the grid will handle it + if (($cell['onchange'] != '' || $img && !$readonly) && !$cell['needed']) // use a link instead of a button + { + $onclick = ($onclick ? preg_replace('/^return(.*);$/','if (\\1) ',$onclick) : ''). + (((string)$cell['onchange'] === '1' || $img) ? + 'return submitit('.self::$name_form.",'".$form_name."');" : $cell['onchange']).'; return false;'; + + if (!html::$netscape4 && substr($img,-1) == '%' && is_numeric($percent = substr($img,0,-1))) + { + $html .= html::progressbar($percent,$title,'onclick="'.$onclick.'" '.$options); + } + else + { + $html .= '' . + ($img ? html::image($app,$img,$title,'border="0"') : $title) . ''; + } + } + else + { + if (!empty($img)) + { + $options .= ' title="'.$title.'"'; + } + if ($cell['onchange'] && $cell['onchange'] != 1) + { + $onclick = ($onclick ? preg_replace('/^return(.*);$/','if (\\1) ',$onclick) : '').$cell['onchange']; + } + if ($type == 'cancel') $options .= ' novalidate="novalidate"'; // tell html5 form validation NOT to validate + $html .= !$readonly ? html::submit_button($form_name,$label,$onclick, + strlen($label) <= 1 || $cell['no_lang'],$options,$img,$app,$type == 'buttononly' ? 'button' : 'submit') : + html::image($app,$ro_img,'',$options); + } + $extra_label = False; + if (!$readonly && $type != 'buttononly') // input button, are never submitted back! + { + self::$request->set_to_process($form_name,$type); + } + break; + case 'hrule': + $html .= html::hr($cell_options); + break; + case 'grid': + if ($readonly && !$readonlys['__ALL__']) + { + if (!is_array($readonlys)) $readonlys = array(); + $set_readonlys_all = $readonlys['__ALL__'] = True; + } + if ($name != '') + { + $cname .= $cname == '' ? $name : '['.str_replace('[','][',str_replace(']','',$name)).']'; + } + $html .= $this->show_grid($cell,$name ? $value : $content,$readonlys+(array)$readonlys[$name],$cname,$show_c,$show_row,$path); + if ($set_readonlys_all) unset($readonlys['__ALL__']); + break; + case 'template': // size: index in content-array (if not full content is past further on) + if (is_object($cell['name'])) + { + $cell['obj'] = &$cell['name']; + unset($cell['name']); + $cell['name'] = 'was Object'; + echo "

Object in Name in tpl '$this->name': "; _debug_array($grid); + } + $obj_read = 'already loaded'; + if (is_array($cell['obj'])) + { + $obj = new etemplate_old(); + $obj->init($cell['obj']); + $cell['obj'] =& $obj; + unset($obj); + } + if (!is_object($cell['obj'])) + { + if ($cell['name'][0] == '@') + { + $cell['obj'] = $this->get_array($content,substr($cell['name'],1)); + $obj_read = is_object($cell['obj']) ? 'obj from content' : 'obj read, obj-name from content'; + if (!is_object($cell['obj'])) + { + $cell['obj'] = new etemplate_old($cell['obj'],$this->as_array()); + } + } + else + { $obj_read = 'obj read'; + $cell['obj'] = new etemplate_old($name,$this->as_array()); + } + } + if (is_int($this->debug) && $this->debug >= 3 || $this->debug == $cell['type']) + { + echo "

show_cell::template(tpl=$this->name,name=$cell[name]): $obj_read, readonly=$readonly

\n"; + } + if ($this->autorepeat_idx($cell,$show_c,$show_row,$idx,$idx_cname,false,$content) || $cell_options != '') + { + if ($span == '' && isset($content[$idx]['span'])) + { // this allows a colspan in autorepeated cells like the editor + list($span) = explode(',',$content[$idx]['span']); + if ($span == 'all') + { + $span = 1 + $content['cols'] - $show_c; + } + } + $readonlys = $this->get_array($readonlys,$idx); + $content = $this->get_array($content,$idx); + if ($idx_cname != '') + { + $cname .= $cname == '' ? $idx_cname : '['.str_replace('[','][',str_replace(']','',$idx_cname)).']'; + } + //echo "

show_cell-autorepeat($name,$show_c,$show_row,cname='$cname',idx='$idx',idx_cname='$idx_cname',span='$span'): content ="; _debug_array($content); + } + if ($readonly && !$readonlys['__ALL__']) + { + if (!is_array($readonlys)) $readonlys = array(); + $set_readonlys_all = $readonlys['__ALL__'] = True; + } + // propagate our onclick handler to embeded templates, if they dont have their own + if (!isset($cell['obj']->onclick_handler)) $cell['obj']->onclick_handler = $this->onclick_handler; + if ($cell['obj']->no_onclick) + { + $cell['obj']->onclick_proxy = $this->onclick_proxy ? $this->onclick_proxy : $this->name.':'.$this->version.':'.$path; + } + // propagate the CSS class to the template + if ($class) + { + $grid_size = array_pad(explode(',',$cell['obj']->size),4,''); + $grid_size[3] = ($grid_size[3] ? $grid_size[3].' ' : '') . $class; + $cell['obj']->size = implode(',',$grid_size); + } + $html = $cell['obj']->show($content,$this->sel_options,$readonlys,$cname,$show_c,$show_row); + + if ($set_readonlys_all) unset($readonlys['__ALL__']); + break; + case 'select': // size:[linesOnMultiselect|emptyLabel,extraStyleMulitselect, [,]{5} enhance] + $sels = array(); + list($multiple,$extraStyleMultiselect) = explode(',',$cell_options,2); + + // Allow widget to specify using enhanced select or not + $c_options = explode(',',$cell_options); + if(array_key_exists('enhance', $cell)) + { + $enhance = $cell['enhance']; + } + else if (count($c_options) >= 8) + { + // 8 or more optionsu - #7 is enhance flag + $enhance = ($c_options[7] == '1' || $c_options[7] == 'true'); + } + + if (!empty($multiple) && 0+$multiple <= 0) + { + $sels[''] = $multiple < 0 ? 'all' : $multiple; + // extra-option: no_lang=0 gets translated later and no_lang=1 gets translated too (now), only no_lang>1 gets not translated + if ((int)$cell['no_lang'] == 1) + { + $sels[''] = substr($sels[''],-3) == '...' ? lang(substr($sels[''],0,-3)).'...' : lang($sels['']); + } + $multiple = 0; + } + $sels += $this->_sel_options($cell,$name,$content); + if ($multiple && !is_array($value)) $value = explode(',',$value); + if ($readonly || $cell['noprint']) + { + foreach($multiple || is_array($value) ? $value : array($value) as $val) + { + if (is_array($sels[$val])) + { + $option_label = $sels[$val]['label']; + $option_title = $sels[$val]['title']; + } + else + { + $option_label = ($sels[$val]?$sels[$val]:$val); + $option_title = ''; + } + if (!$cell['no_lang']) $option_label = lang($option_label); + + if ($html) $html .= "
\n"; + + if ($option_title) + { + $html .= ''.html::htmlspecialchars($option_label).''; + } + else + { + $html .= html::htmlspecialchars($option_label); + } + } + } + if (!$readonly) + { + if ($cell['noprint']) + { + $html = ''.$html.''; + $options .= ' class="noPrint"'; + } + if ($multiple && is_numeric($multiple)) // eg. "3+" would give a regular multiselectbox + { + $html .= html::checkbox_multiselect($form_name.($multiple > 1 ? '[]' : ''),$value,$sels, + $cell['no_lang'],$options,$multiple,$multiple[0]!=='0', + $extraStyleMultiselect,$enhance); + } + else + { + $html .= html::select($form_name.($multiple > 1 ? '[]' : ''),$value,$sels, + $cell['no_lang'],$options,$multiple,$enhance); + } + if (!self::$request->isset_to_process($form_name)) + { + // fix for optgroup's + $options=array(); + foreach($sels as $key => $val) + { + # we want the key anyway, even if this allowes more values than wanted (the name/key of the optgroup if there is one, + # the keys of the arrays in case you have key/value pair(s) as value for the value of your option ). + $options[$key]=$key; + if (is_array($val)) + { + foreach(array_keys($val) as $key2) + { + $options[$key2]=$key2; + } + } + } + self::$request->set_to_process($form_name,$cell['type'],array( + 'needed' => $cell['needed'], + 'allowed' => array_keys($options), + 'multiple'=> $multiple, + )); + } + } + break; + case 'image': // size: [link],[link_target],[imagemap],[link_popup],[id] + case 'progress': + if (is_string($value) && $value !== '' && + ($is_progress = substr($value,-1) == '%' && is_numeric(substr($value,0,-1))) !== ($type == 'progress')) + { + error_log("Please use correct widget-type '".($is_progress?'progress':'image')."' in eTemplate '$this->name'!"); + } + $image = $value != '' ? $value : $name; + if (is_string($image)) list($app,$img) = explode('/',$image,2); + if (!$app || !$img || !is_dir(EGW_SERVER_ROOT.'/'.$app) || strpos($img,'/')!==false) + { + $img = $image; + list($app) = explode('.',$this->name); + } + if (!$readonly) + { + list($extra_link,$extra_link_target,$imagemap,$extra_link_popup,$id) = self::csv_split($cell['size']); + } + $html .= html::image($app,$img,strlen($label) > 1 && !$cell['no_lang'] ? lang($label) : $label, + 'border="0"'.($imagemap?' usemap="#'.html::htmlspecialchars($imagemap).'"':''). + ($id || $value ? self::get_id($name,$cell['name'],$id) : '')); + $extra_label = False; + break; + case 'file': // size: size of the filename field + if (!$readonly) + { + if ((int) $cell_options) $options .= ' size="'.(int)$cell_options.'"'; + if (substr($name,-2) == '[]') + { + self::$form_options .= ' enctype="multipart/form-data"'; + if (strpos($options,'onChange="') !== false) + { + $options = preg_replace('/onChange="([^"]+)"/i','onChange="\\1; if (!this.multiple) add_upload(this);"',$options); + } + else + { + $options .= ' onChange="if (!this.multiple) add_upload(this);"'; + } + $options .= ' multiple="multiple"'; // allow html5 browsers to select more then one file + } + else + { + $html .= html::input_hidden($path_name = str_replace($name,$name.'_path',$form_name),'.'); + self::$form_options = " enctype=\"multipart/form-data\" onsubmit=\"set_element2(this,'$path_name','$form_name')\""; + } + $html .= html::input($form_name,'','file',$options); + self::$request->set_to_process($form_name,$cell['type'],array( + 'needed' => $cell['needed'], + )); + } + break; + case 'split': + // Render this et2 widget as a box + $orient = $cell['orientation']; + case 'vbox': + case 'hbox': + case 'groupbox': + case 'box': // size: num,orient,cellpadding,cellspacing,keep + $rows = array(); + $box_row = 1; + $box_col = 'A'; + $box_anz = 0; + list($num,$orient,,,$keep_empty) = explode(',',$cell_options); + if (!$orient) $orient = $type == 'hbox' ? 'horizontal' : ($type == 'box' ? false : 'vertical'); + for ($n = 1; $n <= (int) $num; ++$n) + { + $child = $cell[$n]; // first param is a var_param now! + $h = $this->show_cell($child,$content,$readonlys,$cname,$show_c,$show_row,$nul,$cl,$path.'/'.$n); + if ($h != '' && $h != ' ' || $keep_empty) + { + if ($orient != 'horizontal') + { + $box_row = $n; + } + else + { + $box_col = $this->num2chrs($n); + } + if (!$orient) + { + $html .= $cl ? html::div($h," class=\"$cl\"") : $h; + } + else + { + $rows[$box_row][$box_col] = $html = $h; + } + $box_anz++; + if ($cell[$n]['align']) + { + $rows[$box_row]['.'.$box_col] = html::formatOptions($child['align'],'align'); + $sub_cell_has_align = true; + } + if (strlen($child['onclick']) > 1) + { + $rows[$box_row]['.'.$box_col] .= ' onclick="'.$this->js_pseudo_funcs($child['onclick'],$cname).'"'. + self::get_id('',$child['name'],$child['id']); + } + // allow to set further attributes in the tablecell, beside the class + if (is_array($cl)) + { + foreach($cl as $attr => $val) + { + if ($attr != 'class' && $val) + { + $rows[$box_row]['.'.$box_col] .= ' '.$attr.'="'.$val.'"'; + } + } + $cl = $cl['class']; + } + $box_item_class = $this->expand_name(isset(self::$class_conf[$cl]) ? self::$class_conf[$cl] : $cl, + $show_c,$show_row,$content['.c'],$content['.row'],$content); + $rows[$box_row]['.'.$box_col] .= html::formatOptions($box_item_class,'class'); + } + } + if ($box_anz > 1 && $orient) // a single cell is NOT placed into a table + { + $html = html::table($rows,html::formatOptions($cell_options,',,cellpadding,cellspacing'). + ($type != 'groupbox' ? html::formatOptions($class,'class'). + ($cell['name'] ? self::get_id($form_name,$cell['name'],$cell['id']) : '') : ''). + ($cell['align'] && $orient != 'horizontal' || $sub_cell_has_align ? ' width="100%"' : '')); // alignment only works if table has full width + if ($type != 'groupbox') $class = ''; // otherwise we create an extra div + } + // put the class of the box-cell, into the the class of this cell + elseif ($box_item_class && $box_anz == 1) + { + $class = ($class ? $class . ' ' : '') . $box_item_class; + // if we have onclick or tooltip, add it to an extra div around single cell + if (!empty($rows[$box_row]['.'.$box_col])) $html = html::div($html, $rows[$box_row]['.'.$box_col]); + } + if ($type == 'groupbox') + { + if (strlen($label) > 1 && $cell['label'] == $label) + { + $label = lang($label); + } + $html = html::fieldset($html,$label,self::get_id($form_name,$cell['name'],$cell['id']). + ($class ? ' class="'.$class.'"' : '')); + $class = ''; // otherwise we create an extra div + } + elseif (!$orient) + { + if (strpos($html,'class="'.$class)) $class = ''; // dont add class a 2. time + $html = html::div($html,html::formatOptions(array( + $cell['height'], + $cell['width'], + $class, + ),'height,width,class').self::get_id($form_name,$cell['name'],$cell['id'])). ($html ? '' : ''); + $class = ''; // otherwise we create an extra div + } + if ($box_anz > 1) // small docu in the html-source + { + $html = "\n\n\n\n".$html."\n\n\n\n"; + } + // we need noPrint on td + if (strpos($cell['span'], 'noPrint')) $class .= ' noPrint'; + $extra_label = False; + break; + case 'deck': + for ($n = 1; $n <= $cell_options && (empty($value) || $value != $cell[$n]['name']); ++$n) ; + if ($n > $cell_options) + { + $value = $cell[1]['name']; + } + if ($s_width = $cell['width']) + { + $s_width = "width: $s_width".(substr($s_width,-1) != '%' ? 'px' : '').';'; + } + if ($s_height = $cell['height']) + { + $s_height = "height: $s_height".(substr($s_height,-1) != '%' ? 'px' : '').';'; + } + $html = html::input_hidden($form_name,$value); + self::$request->set_to_process($form_name,$cell['type']); + + for ($n = 1; $n <= $cell_options; ++$n) + { + $child = $cell[$n]; // first param is a var_param now! + $html .= html::div($this->show_cell($child,$content,$readonlys,$cname,$show_c, + $show_row,$nul,$cl,$path.'/'.$n),html::formatOptions(array( + 'display: '.($value == $child['name'] ? 'inline' : 'none').';', + $child['name'] + ),'style,id')); + } + break; + case 'colorpicker': + if ($readonly) + { + $html = $value; + } + else + { + $html = html::inputColor($form_name,$value,$cell['help']); + + self::$request->set_to_process($form_name,$cell['type'],array( + 'maxlength' => 7, + 'needed' => $cell['needed'], + 'preg' => '/^(#[0-9a-f]{6}|)$/i', + )); + } + break; + default: + if ($ext_type && $this->haveExtension($ext_type,'render')) + { + $html .= $this->extensionRender($ext_type,$form_name,$value,$cell,$readonly); + } + else + { + $html .= "unknown type '$cell[type]'"; + } + break; + } + // extension-processing need to be after all other and only with diff. name + if ($ext_type && !$readonly && $this->haveExtension($ext_type,'post_process')) + { // unset it first, if it is already set, to be after the other widgets of the ext. + $to_process = self::$request->get_to_process($form_name); + self::$request->unset_to_process($form_name); + self::$request->set_to_process($form_name,'ext-'.$ext_type,$to_process); + } + // restoring value, as it is a reference into content + // some widgets change it and rely on it being a reference + // using same name for multiple widgets breaks, if we dont restore it now + $value = $old_value; + // save blur-value to strip it in process_exec + if (!empty($blur) && self::$request->isset_to_process($form_name)) + { + self::$request->set_to_process_attribute($form_name,'blur',$blur); + } + if ($extra_label && ($label != '' || $html == '')) + { + if (strlen($label) > 1 && !($cell['no_lang'] && $cell['label'] != $label || (int)$cell['no_lang'] == 2)) + { + $label = lang($label); + } + $accesskey = false; + if (($accesskey = $label && strpos($label,'&')!==false) && $accesskey[1] != ' ' && $form_name != '' && + (($pos = strpos($accesskey,';')) === false || $pos > 5)) + { + $label = str_replace('&'.$accesskey[1],''.$accesskey[1].'',$label); + $accesskey = $accesskey[1]; + } + if ($label && !$readonly && ($accesskey || $label_for || $type != 'label' && $cell['name'])) + { + if ($label_for) // if label_for starts with a '#', it is already an id - no need to create default id from it + { + $label_for = $label_for[0] == '#' ? substr($label_for,1) : self::form_name($cname,$label_for); + } + else + { + $label_for = $form_name.($set_val?"[$set_val]":''); + } + $label = html::label($label,$label_for,$accesskey); + } + if ($type == 'radio' || $type == 'checkbox' || $label && strpos($label,'%s')!==false) // default for radio is label after the button + { + $html = strpos($label,'%s')!==false ? str_replace('%s',$html,$label) : $html.' '.$label; + } + elseif (($html = $label . ' ' . $html) == ' ') + { + $html = ' '; + } + } + if ($extra_link && (($extra_link = $this->expand_name($extra_link,$show_c,$show_row,$content['.c'],$content['.row'],$content)))) + { + $options = $help ? ' onmouseover="self.status=\''.addslashes(html::htmlspecialchars($help)).'\'; return true;"' . + ' onmouseout="self.status=\'\'; return true;"' : ''; + + if ($extra_link_target && (($extra_link_target = $this->expand_name($extra_link_target,$show_c,$show_row,$content['.c'],$content['.row'],$content)))) + { + $options .= ' target="'.addslashes($extra_link_target).'"'; + } + if ($extra_link_popup && (($extra_link_popup = $this->expand_name($extra_link_popup,$show_c,$show_row,$content['.c'],$content['.row'],$content)))) + { + list($w,$h) = explode('x',$extra_link_popup); + $options .= ' onclick="window.open(this,this.target,\'width='.(int)$w.',height='.(int)$h.',location=no,menubar=no,toolbar=no,scrollbars=yes,status=yes\'); return false;"'; + } + if ($extra_link_title) + { + $options .= ' title="'.addslashes($extra_link_title).'"'; + } + return html::a_href($html,$extra_link,'',$options); + } + // if necessary show validation-error behind field + if (isset(self::$validation_errors[$form_name])) + { + $html .= ' '.self::$validation_errors[$form_name].''; + } + // generate an extra div, if we have an onclick handler and NO children or it's an extension + //echo "

$this->name($this->onclick_handler:$this->no_onclick:$this->onclick_proxy): $cell[type]/$cell[name]

\n"; + if ($this->onclick_handler && !isset(self::$widgets_with_children[$cell['type']])) + { + $handler = str_replace('%p',$this->no_onclick ? $this->onclick_proxy : $this->name.':'.$this->version.':'.$path, + $this->onclick_handler); + if ($type == 'button' || $type == 'buttononly' || !$label) // add something to click on + { + $html = (substr($html,-1) == "\n" ? substr($html,0,-1) : $html).' '; + } + return html::div($html,' ondblclick="'.$handler.'"','clickWidgetToEdit'); + } + return $html; + } + + /** + * Return id="..." attribute, using the following order to determine the id: + * - $id if not empty + * - $name if starting with a hash (#), without the hash of cause + * - $form_name otherwise + * + * This is necessary to not break backward compatibility: if you want to specify + * a certain id, you can use now "#something" as name to get id="something", + * otherwise the $form_name "exec[something]" is used. + * (If no id is directly supplied internally.) + * + * @param string $form_name + * @param string $name=null + * @param string $id=null + * @return string ' id="..."' or '' if no id found */ - public static function cats2color($cats) + static public function get_id($form_name,$name=null,$id=null) { - trigger_error('Calling etemplate_old', E_USER_NOTICE); - return etemplate_old::cats2color($cats); + if (empty($id)) + { + if ($name[0] == '#') + { + $id = substr($name,1); + } + else + { + $id = $form_name; + } + } + return !empty($id) ? ' id="'.str_replace('"','"',$id).'"' : ''; } -} -// default etemplate class has to be defined by either extending etemplate_new or etemplate_old -class etemplate extends etemplate_old {} -// Try to discover all widgets, as names don't always match tags (eg: listbox is in menupopup) -$files = scandir(EGW_INCLUDE_ROOT . '/etemplate/inc'); -foreach($files as $filename) -{ - if(strpos($filename, 'class.etemplate_widget') === 0) + /** + * Format a number according to user prefs with decimal and thousands separator (later only for readonly) + * + * HTML5 input type=number requires a float value with a dot, not comma! + * Chrome 22 and Safari 6 shows no value if a comma is used, + * while FF 16, IE 9 and 10 have no support for input type=number :-( + * --> use . as decimal separator for browser supporting html5 input type=number + * + * @param int|float|string $number + * @param int $num_decimal_places=2 + * @param boolean $readonly=true + * @return string + */ + static public function number_format($number,$num_decimal_places=2,$readonly=true) { - try + static $dec_separator,$thousands_separator; + if (is_null($dec_separator)) { - include_once($filename); + $dec_separator = $GLOBALS['egw_info']['user']['preferences']['common']['number_format'][0]; + if (empty($dec_separator)) $dec_separator = '.'; + $thousands_separator = $GLOBALS['egw_info']['user']['preferences']['common']['number_format'][1]; } - catch(Exception $e) - { - error_log($e->getMessage()); - } - } -} + if ((string)$number === '') return ''; -// Use hook to load custom widgets from other apps -$widgets = $GLOBALS['egw']->hooks->process('etemplate2_register_widgets'); -foreach($widgets as $app => $list) -{ - foreach($list as $class) + $ret = number_format(str_replace(' ','',$number), $num_decimal_places, + // need to use '.' as decimal separator for all browser supporting html5 input type=number + $dec_sep_used=$readonly || !in_array(html::$user_agent, array('chrome', 'safari', 'opera')) ? + $dec_separator : '.', + $readonly ? $thousands_separator : ''); + //error_log(__METHOD__."($number, $num_decimal_places, $readonly) html::user_agent=".html::$user_agent.", dec_sep='$dec_separator' --> '$dec_sep_used', thousands_sep='$thousands_separator' returning '$ret'"); + return $ret; + } + + /** + * Retrive options for selectboxes and similar widgets (eg. the tree) + * + * @param array $cell + * @param string $name + * @param array $content=array(); + * @return array + */ + function _sel_options($cell,$name,$content=array()) { - try + $sels = array(); + + if (!empty($cell['sel_options'])) { - __autoload($class); + if (!is_array($cell['sel_options'])) + { + $opts = explode(',',$cell['sel_options']); + while (list(,$opt) = each($opts)) + { + list($k,$v) = explode('=',$opt); + $sels[$k] = $v; + } + } + else + { + $sels += $cell['sel_options']; + } } - catch(Exception $e) + $explode_needed = true; + if (($options = $this->sel_options[$name]) && is_array($options) || + ($options = self::get_array($this->sel_options, $name)) && is_array($options)) { - error_log($e->getMessage()); + if(count($options) == 2 && $options['label'] && $options['title']) + { + // In too far - need to back up + } + else + { + $sels += $options; + $explode_needed = false; + } + } + if($explode_needed) + { + $name_parts = explode('[',str_replace(']','',$name)); + if (count($name_parts)) + { + $org_name = $name_parts[count($name_parts)-1]; + if (isset($this->sel_options[$org_name]) && is_array($this->sel_options[$org_name])) + { + $sels += $this->sel_options[$org_name]; + } + elseif (isset($this->sel_options[$name_parts[0]]) && is_array($this->sel_options[$name_parts[0]])) + { + $sels += $this->sel_options[$name_parts[0]]; + } + } + } + if (isset($content["options-$name"])) + { + $sels += $content["options-$name"]; + } + //error_log(__METHOD__."(, '$name') returning ".array2string($sels)); + return $sels; + } + + /** + * Resolve javascript pseudo functions in onclick or onchange: + * - egw::link('$l','$p') calls $egw->link($l,$p) + * - form::name('name') returns expanded name/id taking into account the name at that point of the template hierarchy + * - egw::lang('Message ...') translate the message + * - confirm('message') translates 'message' and adds a '?' if not present + * - window.open() replaces it with egw_openWindowCentered2() + * - xajax_doXMLHTTP('etemplate. replace ajax calls in widgets with special handler not requiring etemplate run rights + * + * @param string $on onclick, onchange, ... action + * @param string $cname name-prefix / name-space + * @return string + */ + function js_pseudo_funcs($on,$cname) + { + if (strpos($on,'::') !== false) // avoid the expensive regular expresions, for performance reasons + { + if (preg_match_all("/egw::link\\('([^']+)','(.+?)'(?:,'(.+?)')?\\)/",$on,$matches)) // the ? alters the expression to shortest match + { + foreach(array_keys($matches[1]) as $n) // this way we can correctly parse ' in the 2. argument + { + $url = $GLOBALS['egw']->link($matches[1][$n],$matches[2][$n],$matches[3][$n]); + $on = str_replace($matches[0][$n],'\''.addslashes($url).'\'',$on); + } + } + + if (preg_match_all("/form::name\\('([^']+)'\\)/",$on,$matches)) + { + foreach($matches[1] as $n => $matche_name) + { + $matches[1][$n] = '\''.self::form_name($cname,$matche_name).'\''; + } + $on = str_replace($matches[0],$matches[1],$on); + } + // we need to search ungready (shortest possible match), to avoid catching to much + if (preg_match_all('/egw::lang\(["\']{1}(.*)["\']{1}\)/U',$on,$matches)) { + foreach($matches[1] as $n => $string) { + $str = lang($string); + $on = str_replace($matches[0][$n],'\''.addslashes($str).'\'',$on); + } + } + + // inserts the styles of a named template + if (preg_match('/template::styles\(["\']{1}(.*)["\']{1}\)/U',$on,$matches)) + { + $tpl = $matches[1] == $this->name ? $this : new etemplate_old($matches[1]); + $on = str_replace($matches[0],"''",$on); + } + } + + // translate messages in confirm() + if (strpos($on,'confirm(') !== false && preg_match('/confirm\(["\']{1}(.*)["\']{1}\)/U',$on,$matches)) + { + $question = lang($matches[1]).(substr($matches[1],-1) != '?' ? '?' : ''); // add ? if not there, saves extra phrase + $on = str_replace($matches[0],'confirm(\''.str_replace("'","\\'",$question).'\')',$on); + } + + // replace window.open() with EGw's egw_openWindowCentered2() + if (strpos($on,'window.open(') !== false && preg_match("/window.open\('(.*)','(.*)','dependent=yes,width=([^,]*),height=([^,]*),scrollbars=yes,status=(.*)'\)/",$on,$matches)) + { + $on = str_replace($matches[0], "egw_openWindowCentered2('$matches[1]', '$matches[2]', $matches[3], $matches[4], '$matches[5]')", $on); + } + + // replace xajax calls to code in widgets, with the "etemplate" handler, + // this allows to call widgets with the current app, otherwise everyone would need etemplate run rights + if (strpos($on,"xajax_doXMLHTTP('etemplate.") !== false) + { + $on = preg_replace("/^xajax_doXMLHTTP\('etemplate\.([a-z]+_widget\.[a-zA-Z0-9_]+)\'/",'xajax_doXMLHTTP(\''.$GLOBALS['egw_info']['flags']['currentapp'].'.\\1.etemplate\'',$on); + } + + return $on; + } + + /** + * applies stripslashes recursivly on each element of an array + * + * @param array &$var + * @return array + */ + static function array_stripslashes($var) + { + if (!is_array($var)) + { + return stripslashes($var); + } + foreach($var as $key => $val) + { + $var[$key] = is_array($val) ? self::array_stripslashes($val) : stripslashes($val); + } + return $var; + } + + /** + * makes necessary adjustments on $_POST after a eTemplate / form gots submitted + * + * This is only an internal function, dont call it direct use only exec + * Process_show uses a list of input-fields/widgets generated by show. + * + * @internal + * @param array $content $_POST[$cname], on return the adjusted content + * @param array $to_process list of widgets/form-fields to process + * @param string $cname='' basename of our returnt content (same as in call to show) + * @param string $_type='regular' type of request + * @return array with validation errors + */ + function process_show(&$content,$to_process,$cname='',$_type='regular') + { + if (!isset($content) || !is_array($content) || !is_array($to_process)) + { + return; + } + if (is_int($this->debug) && $this->debug >= 1 || $this->debug == $this->name && $this->name) + { + echo "

process_show($this->name) cname='$cname' start: content ="; _debug_array($content); + } + $content_in = $cname ? array($cname => $content) : $content; + $content = array(); + if (get_magic_quotes_gpc()) + { + $content_in = self::array_stripslashes($content_in); + } + self::$validation_errors = array(); + $this->canceled = $this->button_pressed = False; + + foreach($to_process as $form_name => $type) + { + if (is_array($type)) + { + $attr = $type; + $type = $attr['type']; + } + else + { + $attr = array(); + } + $form_name = str_replace(array('[',']'), array('[',']'), $form_name); + $value = self::get_array($content_in,$form_name,True,$GLOBALS['egw_info']['flags']['currentapp'] == 'etemplate' ? false : true ); + // The comment below does only aplay to normal posts, not for xajax. Files are not supported anyway by xajax atm. + // not checked checboxes are not returned in HTML and file is in $_FILES and not in $content_in + if($value === false && $_type == 'xajaxResponse' /*!in_array($type,array('checkbox','file'))*/) continue; + + if (isset($attr['blur']) && $attr['blur'] == $value) + { + $value = ''; // blur-values is equal to emtpy + } + //echo "

process_show($this->name) loop was ".self::$loop.", $type: $form_name = ".array2string($value)."

\n"; + if (is_string($type)) list($type,$sub) = explode('-',$type); + switch ($type) + { + case 'ext': + $_cont = &self::get_array($content,$form_name,True); + if (!$this->extensionPostProcess($sub,$form_name,$_cont,$value)) + { + //echo "\n

unsetting content[$form_name] !!!

\n"; + $this->unset_array($content,$form_name); + } + // this else should NOT be unnecessary as $_cont is a reference to the index + // $form_name of $content, but under some circumstances a set/changed $_cont + // does not result in a change in $content -- RalfBecker 2004/09/18 + // seems to depend on the number of (not existing) dimensions of the array -- -- RalfBecker 2005/04/06 + elseif (!self::isset_array($content,$form_name)) + { + //echo "

setting content[$form_name]='$_cont' because is was unset !!!

\n"; + self::set_array($content,$form_name,$_cont); + } + if ($_cont === '' && $attr['needed'] && !$attr['blur']) + { + self::set_validation_error($form_name,lang('Field must not be empty !!!'),''); + } + break; + case 'htmlarea': + if ($attr['mode'] !== 'ascii') + { + self::set_array($content,$form_name,html::purify($value)); + break; + } + // fall-throught for mode 'ascii', which is identical to textarea + case 'int': + case 'integer': + case 'float': + case 'passwd': + case 'text': + case 'textbox': + case 'hidden': + case 'textarea': + case 'colorpicker': + if ((string)$value === '' && $attr['needed'] && !$attr['blur']) + { + self::set_validation_error($form_name,lang('Field must not be empty !!!'),''); + } + if ((int) $attr['maxlength'] > 0 && mb_strlen($value) > (int) $attr['maxlength']) + { + $value = mb_substr($value,0,(int) $attr['maxlength']); + } + if ($attr['preg'] && !preg_match($attr['preg'],$value)) + { + switch($type) + { + case 'int': + case 'integer': + self::set_validation_error($form_name,lang("'%1' is not a valid integer !!!",$value),''); + break; + case 'float': + self::set_validation_error($form_name,lang("'%1' is not a valid floatingpoint number !!!",$value),''); + break; + default: + self::set_validation_error($form_name,lang("'%1' has an invalid format !!!",$value)/*." !preg_match('$attr[preg]', '$value')"*/,''); + break; + } + } + elseif (in_array($type, array('int', 'integer', 'float'))) // cast int and float and check range + { + if ((string)$value !== '' || $attr['needed']) // empty values are Ok if needed is not set + { + $value = $type != 'float' ? (int) $value : (float) str_replace(',','.',$value); // allow for german (and maybe other) format + + if (!empty($attr['min']) && $value < $attr['min']) + { + self::set_validation_error($form_name,lang("Value has to be at least '%1' !!!",$attr['min']),''); + $value = $type != 'float' ? (int) $attr['min'] : (float) $attr['min']; + } + if (!empty($attr['max']) && $value > $attr['max']) + { + self::set_validation_error($form_name,lang("Value has to be at maximum '%1' !!!",$attr['max']),''); + $value = $type != 'float' ? (int) $attr['max'] : (float) $attr['max']; + } + } + } + self::set_array($content,$form_name,$value); + break; + case 'cancel': // cancel button ==> dont care for validation errors + if ($value) + { + $this->canceled = True; + self::set_array($content,$form_name,$value); + } + break; + case 'button': + if ($value) + { + $this->button_pressed = True; + self::set_array($content,$form_name,$value); + } + break; + case 'select': + if ($attr['allowed']) // only check for $value is allowed, if allowed values are set + { + foreach(is_array($value) ? $value : array($value) as $val) + { + if (!($attr['multiple'] && !$val) && !in_array($val,$attr['allowed'])) + { + self::set_validation_error($form_name,lang("'%1' is NOT allowed ('%2')!",$val,implode("','",$attr['allowed'])),''); + $value = ''; + break; + } + } + } + if (is_array($value)) $value = implode(',',$value); + if ($value === '' && $attr['needed']) + { + self::set_validation_error($form_name,lang('Field must not be empty !!!',$value),''); + } + self::set_array($content,$form_name,$value); + break; + case 'checkbox': + if (!$value && $attr['needed']) + { + self::set_validation_error($form_name,lang('Field must not be empty !!!',$value),''); + } + if ($value === false) + { + self::set_array($content,$form_name,$attr['multiple'] ? array() : $attr['unset_value']); // need to be reported too + } + else + { + $value = array_intersect(is_array($value) ? $value : array($value),$attr['values']); // return only allowed values + self::set_array($content,$form_name,$attr['multiple'] ? $value : $value[0]); + } + break; + case 'file': + if (($multiple = substr($form_name,-2) == '[]')) + { + $form_name = substr($form_name,0,-2); + } + $parts = explode('[',str_replace(']','',$form_name)); + $name = array_shift($parts); + $index = count($parts) ? '['.implode('][',$parts).']' : ''; + $value = array(); + for($i=0; $i < 100; ++$i) + { + $file = array(); + foreach(array('tmp_name','type','size','name','error') as $part) + { + if (!is_array($_FILES[$name])) break 2; // happens eg. in forms set via xajax, which do not upload files + $file[$part] = $this->get_array($_FILES[$name],$part.$index.($multiple ? "[$i]" : '')); + } + if (!$multiple) $file['path'] = $this->get_array($content_in,substr($form_name,0,-1).'_path]'); + $file['ip'] = $_SERVER['REMOTE_ADDR']; + // check if we have an upload error + if ($file['error'] && $file['name'] !== '' && !$file['size']) // ignore empty upload boxes + { + self::set_validation_error($form_name.($multiple?'[]':''), + lang('Error uploading file!')."\n".self::max_upload_size_message(),''); + } + if ((string)$file['name'] === '' || $file['tmp_name'] && function_exists('is_uploaded_file') && !is_uploaded_file($file['tmp_name'])) + { + if ($multiple && ($file['name'] === '' || $file['error'])) + { + continue; // ignore empty upload box + } + break; + } + if (!$multiple) + { + $value = $file; + break; + } + $value[] = $file; + } + //echo $form_name; _debug_array($value); + // fall-throught + default: + if ($attr['needed'] && !$value) + { + self::set_validation_error($form_name,lang('Field must not be empty !!!',$value),''); + } + self::set_array($content,$form_name,$value); + break; + } + } + if ($cname) + { + $content = $content[$cname]; + } + if (is_int($this->debug) && $this->debug >= 2 || $this->debug == $this->name && $this->name) + { + echo "

process_show($this->name) end: content ="; _debug_array($content); + if (count(self::$validation_errors)) + { + echo "

validation_errors = "; _debug_array(self::$validation_errors); + } + } + return self::$validation_errors; + } + + /** + * Sets a validation error, to be displayed in the next exec + * + * @param string $name (complete) name of the widget causing the error + * @param string|boolean $error error-message already translated or false to reset all existing error for given name + * @param string $cname=null set it to '', if the name is already a form-name, defaults to self::$name_vars + */ + static function set_validation_error($name,$error,$cname=null) + { + if (is_null($cname)) $cname = self::$name_vars; + //echo "

self::set_validation_error('$name','$error','$cname');

\n"; + if ($cname) $name = self::form_name($cname,$name); + + if ($error === false) + { + unset(self::$validation_errors[$name]); + } + else + { + if (self::$validation_errors[$name]) + { + self::$validation_errors[$name] .= ', '; + } + self::$validation_errors[$name] .= $error; } } } diff --git a/etemplate/inc/class.etemplate_new.inc.php b/etemplate/inc/class.etemplate_new.inc.php new file mode 100644 index 0000000000..aed16d11c1 --- /dev/null +++ b/etemplate/inc/class.etemplate_new.inc.php @@ -0,0 +1,606 @@ + + * @copyright 2002-13 by RalfBecker@outdoor-training.de + * @version $Id$ + */ + +/** + * New eTemplate serverside contains: + * - main server methods like read, exec + * - + * + * Not longer available methods: + * - set_(row|column)_attributes modifies template on run-time, was only used internally by etemplate itself + * - disable_(row|column) dto. + * + * @ToDo supported customized templates stored in DB, currently we only support xet files stored in filesystem + */ +class etemplate_new extends etemplate_widget_template +{ + /** + * Are we running as sitemgr module or not + * + * @public boolean + */ + public $sitemgr=false; + + /** + * Tell egw framework it's ok to call this + */ + public $public_functions = array( + 'process_exec' => true + ); + + /** + * constructor of etemplate class, reads an eTemplate if $name is given + * + * @param string $name of etemplate or array with name and other keys + * @param string|array $load_via with keys of other etemplate to load in order to get $name + */ + function __construct($name='',$load_via='') + { + // we do NOT call parent consturctor, as we only want to enherit it's (static) methods + if (false) parent::__construct ($name); // satisfy IDE, as we dont call parent constructor + + $this->sitemgr = isset($GLOBALS['Common_BO']) && is_object($GLOBALS['Common_BO']); + + if ($name) $this->read($name,$template='default','default',0,'',$load_via); + + // generate new etemplate request object, if not already existing + if(!isset(self::$request)) self::$request = etemplate_request::read(); + } + + /** + * Abstracts a html-location-header call + * + * In other UI's than html this needs to call the methode, defined by menuaction or + * open a browser-window for any other links. + * + * @param string|array $params url or array with get-params incl. menuaction + */ + static function location($params='') + { + egw::redirect_link(is_array($params) ? '/index.php' : $params, + is_array($params) ? $params : ''); + } + + /** + * Generates a Dialog from an eTemplate - abstract the UI-layer + * + * This is the only function an application should use, all other are INTERNAL and + * do NOT abstract the UI-layer, because they return HTML. + * Generates a webpage with a form from the template and puts process_exec in the + * form as submit-url to call process_show for the template before it + * ExecuteMethod's the given $method of the caller. + * + * @param string $method Methode (e.g. 'etemplate.editor.edit') to be called if form is submitted + * @param array $content with content to fill the input-fields of template, eg. the text-field + * with name 'name' gets its content from $content['name'] + * @param $sel_options array or arrays with the options for each select-field, keys are the + * field-names, eg. array('name' => array(1 => 'one',2 => 'two')) set the + * options for field 'name'. ($content['options-name'] is possible too !!!) + * @param array $readonlys with field-names as keys for fields with should be readonly + * (eg. to implement ACL grants on field-level or to remove buttons not applicable) + * @param array $preserv with vars which should be transported to the $method-call (eg. an id) array('id' => $id) sets $_POST['id'] for the $method-call + * @param int $output_mode + * 0 = echo incl. navbar + * 1 = return html + * -1 = first time return html, after use 0 (echo html incl. navbar), eg. for home + * 2 = echo without navbar (eg. for popups) + * 3 = return eGW independent html site + * @param string $ignore_validation if not empty regular expression for validation-errors to ignore + * @param array $changes change made in the last call if looping, only used internaly by process_exec + * @return string html for $output_mode == 1, else nothing + */ + function exec($method,array $content,array $sel_options=null,array $readonlys=null,array $preserv=null,$output_mode=0,$ignore_validation='',array $changes=null) + { + // Include the etemplate2 javascript code + egw_framework::validate_file('.', 'etemplate2', 'etemplate'); + + if (!$this->rel_path) throw new egw_exception_assertion_failed("No (valid) template '$this->name' found!"); + + self::$request->output_mode = $output_mode; // let extensions "know" they are run eg. in a popup + self::$request->content = self::$cont = $content; + self::$request->changes = $changes; + self::$request->sel_options = $sel_options ? $sel_options : array(); + self::$request->readonlys = $readonlys ? $readonlys : array(); + self::$request->preserv = $preserv ? $preserv : array(); + self::$request->method = $method; + self::$request->ignore_validation = $ignore_validation; + if (self::$request->output_mode == -1) self::$request->output_mode = 0; + self::$request->template = $this->as_array(); + + if (empty($this->name)) throw new egw_exception_assertion_failed("Template name is not set '$this->name' !"); + // instanciate template to fill self::$request->sel_options for select-* widgets + // not sure if we want to handle it this way, thought otherwise we will have a few ajax request for each dialog fetching predefined selectboxes + $template = etemplate_widget_template::instance($this->name, $this->template_set, $this->version, $this->laod_via); + $template->run('beforeSendToClient', array('', array('cont'=>$content))); + + // some apps (eg. InfoLog) set app_header only in get_rows depending on filter settings + self::$request->app_header = $GLOBALS['egw_info']['flags']['app_header']; + + // compile required translations translations + translation::add_app('etemplate'); + $currentapp = $GLOBALS['egw_info']['flags']['currentapp']; + $langRequire = array('common' => array(), 'etemplate' => array()); // keep that order + foreach(translation::$loaded_apps as $l_app => $lang) + { + if (!in_array($l_app, array($currentapp, 'custom'))) + { + $langRequire[$l_app] = array('app' => $l_app, 'lang' => $lang, 'etag' => translation::etag($l_app, $lang)); + } + } + foreach(array($currentapp, 'custom') as $l_app) + { + if (isset(translation::$loaded_apps[$l_app])) + { + $langRequire[$l_app] = array('app' => $l_app, 'lang' => translation::$loaded_apps[$l_app], 'etag' => translation::etag($l_app, translation::$loaded_apps[$l_app])); + } + } + + $data = array( + 'etemplate_exec_id' => self::$request->id(), + 'app_header' => self::$request->app_header, + 'content' => self::$request->content, + 'sel_options' => self::$request->sel_options, + 'readonlys' => self::$request->readonlys, + 'modifications' => self::$request->modifications, + 'validation_errors' => self::$validation_errors, + 'langRequire' => array_values($langRequire), + 'currentapp' => $currentapp, + ); + + // Info required to load the etemplate client-side + $dom_id = str_replace('.','-',$this->dom_id); + $load_array = array( + 'name' => $this->name, + 'url' => etemplate_widget_template::rel2url($this->rel_path), + 'data' => $data, + 'DOMNodeID' => $dom_id, + ); + if (self::$response) // call is within an ajax event / form submit + { + //error_log("Ajax " . __LINE__); + self::$response->generic('et2_load', $load_array+egw_framework::get_extra()); + egw_framework::clear_extra(); // to not send/set it twice for multiple etemplates (eg. CRM view) + } + else // first call + { + // missing dependency, thought egw:uses jquery.jquery.tools does NOT work, maybe we should rename it to jquery-tools + // egw_framework::validate_file('jquery','jquery.tools.min'); + + // Include the jQuery-UI CSS - many more complex widgets use it + $theme = 'redmond'; + egw_framework::includeCSS("/phpgwapi/js/jquery/jquery-ui/$theme/jquery-ui-1.10.3.custom.css"); + // Load our CSS after jQuery-UI, so we can override it + egw_framework::includeCSS('/etemplate/templates/default/etemplate2.css'); + + // check if application of template has a app.js file --> load it + list($app) = explode('.',$this->name); + if (file_exists(EGW_SERVER_ROOT.'/'.$app.'/js/app.js')) + { + egw_framework::validate_file('.','app',$app,false); + } + + // check if we are in an ajax-exec call from jdots template (or future other tabbed templates) + if (isset($GLOBALS['egw']->framework->response)) + { + $content = '
'; + // add server-side page-generation times + if($GLOBALS['egw_info']['user']['preferences']['common']['show_generation_time']) + { + $vars = $GLOBALS['egw']->framework->_get_footer(); + $content .= "\n".$vars['page_generation_time']; + } + $GLOBALS['egw']->framework->response->generic("data", array($content)); + $GLOBALS['egw']->framework->response->generic('et2_load',$load_array+egw_framework::get_extra()); + egw_framework::clear_extra(); // to not send/set it twice for multiple etemplates (eg. CRM view) + self::$request = null; + return; + } + echo $GLOBALS['egw']->framework->header(); + if ($output_mode != 2) + { + parse_navbar(); + } + else // mark popups as such, by enclosing everything in div#popupMainDiv + { + echo '
'."\n"; + } + echo '
'; + + if ($output_mode == 2) + { + echo "\n
\n"; + echo $GLOBALS['egw']->framework->footer(); + } + ob_flush(); + + // Send any accumulated json responses - after flush to avoid sending the buffer as a response + if(egw_json_response::isJSONResponse()) + { + $response = egw_json_response::get(); + echo ''; + } + } + self::$request = null; + } + + /** + * Process via Ajax submitted content + */ + static public function ajax_process_content($etemplate_exec_id, array $content) + { + //error_log(__METHOD__."(".array2string($etemplate_exec_id).', '.array2string($content).")"); + + self::$request = etemplate_request::read($etemplate_exec_id); + //error_log('request='.array2string(self::$request)); + + self::$response = egw_json_response::get(); + + if (!($template = self::instance(self::$request->template['name'], self::$request->template['template_set'], + self::$request->template['version'], self::$request->template['load_via']))) + { + throw new egw_exception_wrong_parameter('Can NOT read template '.array2string(self::$request->template)); + } + + // Set current app for validation + list($app) = explode('.',self::$request->method); + if(!$app) list($app) = explode('::',self::$request->method); + if($app) + { + translation::add_app($app); + $GLOBALS['egw_info']['flags']['currentapp'] = $app; + } + $validated = array(); + $expand = array( + 'cont' => &self::$request->content, + ); + $template->run('validate', array('', $expand, $content, &$validated), true); // $respect_disabled=true: do NOT validate disabled widgets and children + + if (self::validation_errors(self::$request->ignore_validation)) + { + error_log(__METHOD__."(,".array2string($content).') validation_errors='.array2string(self::$validation_errors)); + self::$response->generic('et2_validation_error', self::$validation_errors); + exit; + } + + // tell request call to remove request, if it is not modified eg. by call to exec in callback + self::$request->remove_if_not_modified(); + + //error_log(__METHOD__."(,".array2string($content).')'); + //error_log(' validated='.array2string($validated)); + $content = ExecMethod(self::$request->method, self::complete_array_merge(self::$request->preserv, $validated)); + + if (isset($GLOBALS['egw_info']['flags']['java_script'])) + { + // Strip out any script tags + $GLOBALS['egw_info']['flags']['java_script'] = preg_replace(array('/(]*>)([^<]*)/is','/<\/script>/'),array('$2',''),$GLOBALS['egw_info']['flags']['java_script']); + self::$response->script($GLOBALS['egw_info']['flags']['java_script']); + //error_log($app .' added javascript to $GLOBALS[egw_info][flags][java_script] - use egw_json_response->script() instead.'); + } + + return $content; + } + + /** + * Notify server that eT session/request is no longer needed, because user closed window + * + * @param string $_exec_id + */ + static public function ajax_destroy_session($_exec_id) + { + //error_log(__METHOD__."('$_exec_id')"); + if (($request = etemplate_request::read($_exec_id))) + { + $request->remove_if_not_modified(); + unset($request); + } + } + + /** + * Process via POST submitted content + */ + static public function process_exec() + { + if (get_magic_quotes_gpc()) $_POST['value'] = stripslashes($_POST['value']); + $content = json_decode($_POST['value'],true); + if($content == null && $_POST['exec']) + { + // Old etemplate submit + error_log("Old etemplate submitted"); + return ExecMethod('etemplate.etemplate_old.process_exec'); + } + error_log(__METHOD__."(".array2string($content).")"); + + self::$request = etemplate_request::read($_POST['etemplate_exec_id']); + + if (!($template = self::instance(self::$request->template['name'], self::$request->template['template_set'], + self::$request->template['version'], self::$request->template['load_via']))) + { + throw new egw_exception_wrong_parameter('Can NOT read template '.array2string(self::$request->template)); + } + $validated = array(); + $expand = array( + 'cont' => &self::$request->content, + ); + $template->run('validate', array('', $expand, $content, &$validated), true); // $respect_disabled=true: do NOT validate disabled widgets and children + if (self::validation_errors(self::$request->ignore_validation)) + { + error_log(__METHOD__."(,".array2string($content).') validation_errors='.array2string(self::$validation_errors)); + exit; + } + error_log(__METHOD__."(,".array2string($content).')'); + error_log(' validated='.array2string($validated)); + + // tell request call to remove request, if it is not modified eg. by call to exec in callback + self::$request->remove_if_not_modified(); + + return ExecMethod(self::$request->method, self::complete_array_merge(self::$request->preserv, $validated)); + } + + /** + * Path of template relative to EGW_SERVER_ROOT + * + * @var string + */ + public $rel_path; + + public $name; + public $template_set; + public $version; + public $laod_via; + + /** + * + * @var string If the template needs a div named other than the template name, this is it + */ + protected $dom_id; + + /** + * Reads an eTemplate from filesystem or DB (not yet supported) + * + * @param string $name name of the eTemplate or array with the values for all keys + * @param string $template_set=null default try template-set from user and if not found "default" + * @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 + * + * @ToDo supported customized templates stored in DB + */ + public function read($name,$template_set=null,$lang='default',$group=0,$version='',$load_via='') + { + unset($lang); unset($group); // not used, but in old signature + $this->rel_path = self::relPath($this->name=$name, $this->template_set=$template_set, + $this->version=$version, $this->laod_via = $load_via); + //error_log(__METHOD__."('$name', '$template_set', '$lang', $group, '$version', '$load_via') rel_path=".array2string($this->rel_path)); + + $this->dom_id = $name; + + return (boolean)$this->rel_path; + } + + /** + * Set the DOM ID for the etemplate div. If not set, it will be generated from the template name. + * + * @param string $new_id + */ + public function set_dom_id($new_id) + { + $this->dom_id = $new_id; + } + /** + * Get template data as array + * + * @return array + */ + public function as_array() + { + return array( + 'name' => $this->name, + 'template_set' => $this->template_set, + 'version' => $this->version, + 'load_via' => $this->load_via, + ); + } + + /** + * 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); + } + + /** + * 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 + */ + public 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 + is_array($v) && count($v) == 0 && is_array($old[$k])) // Empty array replacing non-empty + { + $old[$k] = $v; + } + else + { + $old[$k] = self::complete_array_merge($old[$k],$v); + } + } + } + return $old; + } + + /** + * Debug callback just outputting content + * + * @param array $content=null + */ + public function debug(array $content=null) + { + common::egw_header(); + _debug_array($content); + common::egw_footer(); + } + + /** + * Message containing the max Upload size from the current php.ini settings + * + * We have to take the smaler one of upload_max_filesize AND post_max_size-2800 into account. + * memory_limit does NOT matter any more, because of the stream-interface of the vfs. + * + * @param int &$max_upload=null on return max. upload size in byte + * @return string + */ + static function max_upload_size_message(&$max_upload=null) + { + $upload_max_filesize = ini_get('upload_max_filesize'); + $post_max_size = ini_get('post_max_size'); + $max_upload = min(self::km2int($upload_max_filesize),self::km2int($post_max_size)-2800); + + return lang('Maximum size for uploads').': '.egw_vfs::hsize($max_upload). + " (php.ini: upload_max_filesize=$upload_max_filesize, post_max_size=$post_max_size)"; + } + + /** + * Format a number according to user prefs with decimal and thousands separator (later only for readonly) + * + * @param int|float|string $number + * @param int $num_decimal_places=2 + * @param boolean $readonly=true + * @return string + */ + static public function number_format($number,$num_decimal_places=2,$readonly=true) + { + static $dec_separator=null,$thousands_separator=null; + if (is_null($dec_separator)) + { + $dec_separator = $GLOBALS['egw_info']['user']['preferences']['common']['number_format'][0]; + if (empty($dec_separator)) $dec_separator = '.'; + $thousands_separator = $GLOBALS['egw_info']['user']['preferences']['common']['number_format'][1]; + } + if ((string)$number === '') return ''; + + return number_format(str_replace(' ','',$number),$num_decimal_places,$dec_separator,$readonly ? $thousands_separator : ''); + } + + /** + * Convert numbers like '32M' or '512k' to integers + * + * @param string $size + * @return int + */ + private static function km2int($size) + { + if (!is_numeric($size)) + { + switch(strtolower(substr($size,-1))) + { + case 'm': + $size = 1024*1024*(int)$size; + break; + case 'k': + $size = 1024*(int)$size; + break; + } + } + return (int)$size; + } +} + +// Try to discover all widgets, as names don't always match tags (eg: listbox is in menupopup) +$files = scandir(EGW_INCLUDE_ROOT . '/etemplate/inc'); +foreach($files as $filename) +{ + if(strpos($filename, 'class.etemplate_widget') === 0) + { + try + { + include_once($filename); + } + catch(Exception $e) + { + error_log($e->getMessage()); + } + } +} + +// Use hook to load custom widgets from other apps +$widgets = $GLOBALS['egw']->hooks->process('etemplate2_register_widgets'); +foreach($widgets as $app => $list) +{ + foreach($list as $class) + { + try + { + __autoload($class); + } + catch(Exception $e) + { + error_log($e->getMessage()); + } + } +} diff --git a/etemplate/inc/class.etemplate_old.inc.php b/etemplate/inc/class.etemplate_old.inc.php deleted file mode 100644 index fd05b7b74d..0000000000 --- a/etemplate/inc/class.etemplate_old.inc.php +++ /dev/null @@ -1,2516 +0,0 @@ - -* @copyright 2002-14 by RalfBecker@outdoor-training.de -* @package etemplate -* @subpackage api -* @version $Id$ -*/ - -/** -* creates dialogs / HTML-forms from eTemplate descriptions -* -* Usage example: -* -* $tmpl = new etemplate('app.template.name'); -* $tmpl->exec('app.class.callback',$content_to_show); -* -* This creates a form from the eTemplate 'app.template.name' and takes care that -* the method / public function 'callback' in class 'class' of 'app' gets called -* if the user submits the form. For the complete param's see the description of exec. -* -* etemplate or uietemplate extends boetemplate, all vars and public functions are inherited -*/ -class etemplate_old extends boetemplate -{ - /** - * integer debug-level or template-name or cell-type or '' = off - * 1=calls to show and process_show, 2=content after process_show, - * 3=calls to show_cell and process_show_cell - * - * @public int/string - */ - public $debug; - /** - * Inner width of browser window - * - * @public int - */ - public $innerWidth; - /** - * Reference to the content-param of the last call to show, for extensions to use - * - * @public array - */ - public $content; - /** - * Reference to the sel_options-param of the last call to show, for extensions to use - * - * @public array - */ - public $sel_options; - /** - * Name of the form of the currently processed etemplate - * - * @public string - */ - static $name_form='eTemplate'; - /** - * Used form-names in this request - * - * @public array - */ - static $name_forms=array(); - /** - * Basename of the variables (content) in $_POST and id's, usually 'exec', - * if there's not more then one eTemplate on the page (then it will be exec, exec2, exec3, ... - * - * @public string - */ - static $name_vars='exec'; - /** - * Are we running as sitemgr module or not - * - * @public boolean - */ - public $sitemgr=false; - /** - * Javascript to be called, when a widget get's double-clicked (used only by the editor) - * A '%p' gets replace with the colon ':' separated template-name, -version and path of the clicked widget. - * - * @public string - */ - public $onclick_handler; - /** - * Does template processes onclick itself, or forwards it to a proxy - * - * @var boolean - */ - public $no_onclick = false; - /** - * handler to call for onclick - * - * @var string - */ - public $onclick_proxy; - - /** - * Extra options for forms, eg. enctype="multipart/form-data" - * - * @public string - */ - static protected $form_options = ''; - - /** - * Validation errors from process_show and the extensions, should be set via self::set_validation_error - * - * @public array form_name => message pairs - */ - static protected $validation_errors = array(); - - /** - * Flag if exec() is called as part of a hook, replaces the 1.6 and earlier $GLOBALS['egw_info']['etemplate']['hooked'] global variable - * - * @var boolean - */ - static public $hooked; - - /** - * The following 3 static vars are used to allow eTemplate apps to hook into each other - * - * @var mixed - */ - static private $previous_content; - static private $hook_content; - static private $hook_app; - - /** - * constructor of etemplate class, reads an eTemplate if $name is given - * - * @param string $name of etemplate or array with name and other keys - * @param string/array $load_via with keys of other etemplate to load in order to get $name - */ - function __construct($name='',$load_via='') - { - // tell framework old eTemplate apps needs eval and inline javascript :( - egw_framework::csp_script_src_attrs(array('unsafe-eval', 'unsafe-inline')); - - parent::__construct($name,$load_via); - - $this->sitemgr = isset($GLOBALS['Common_BO']) && is_object($GLOBALS['Common_BO']); - - if (($this->innerWidth = (int) $_POST['innerWidth'])) - { - $GLOBALS['egw']->session->appsession('innerWidth','etemplate',$this->innerWidth); - } - elseif (!($this->innerWidth = (int) $GLOBALS['egw']->session->appsession('innerWidth','etemplate'))) - { - $this->innerWidth = 1018; // default width for an assumed screen-resolution of 1024x768 - } - //echo "

_POST[innerWidth]='$_POST[innerWidth]', innerWidth=$this->innerWidth

\n"; - } - - /** - * Abstracts a html-location-header call - * - * In other UI's than html this needs to call the methode, defined by menuaction or - * open a browser-window for any other links. - * - * @param string/array $params url or array with get-params incl. menuaction - */ - static function location($params='') - { - egw::redirect_link(is_array($params) ? '/index.php' : $params, - is_array($params) ? $params : ''); - } - - /** - * Generats a Dialog from an eTemplate - abstract the UI-layer - * - * This is the only function an application should use, all other are INTERNAL and - * do NOT abstract the UI-layer, because they return HTML. - * Generates a webpage with a form from the template and puts process_exec in the - * form as submit-url to call process_show for the template before it - * ExecuteMethod's the given $method of the caller. - * - * @param string $method Methode (e.g. 'etemplate.editor.edit') to be called if form is submitted - * @param array $content with content to fill the input-fields of template, eg. the text-field - * with name 'name' gets its content from $content['name'] - * @param $sel_options array or arrays with the options for each select-field, keys are the - * field-names, eg. array('name' => array(1 => 'one',2 => 'two')) set the - * options for field 'name'. ($content['options-name'] is possible too !!!) - * @param array $readonlys with field-names as keys for fields with should be readonly - * (eg. to implement ACL grants on field-level or to remove buttons not applicable) - * @param array $preserv with vars which should be transported to the $method-call (eg. an id) array('id' => $id) sets $_POST['id'] for the $method-call - * @param int $output_mode - * 0 = echo incl. navbar - * 1 = return html - * -1 = first time return html, after use 0 (echo html incl. navbar), eg. for home - * 2 = echo without navbar (eg. for popups) - * 3 = return eGW independent html site - * @param string $ignore_validation if not empty regular expression for validation-errors to ignore - * @param array $changes change made in the last call if looping, only used internaly by process_exec - * @return string html for $output_mode == 1, else nothing - */ - function exec($method,$content,$sel_options='',$readonlys='',$preserv='',$output_mode=0,$ignore_validation='',$changes='') - { - if (!$sel_options) - { - $sel_options = array(); - } - if (!$readonlys) - { - $readonlys = array(); - } - if (!$preserv) - { - $preserv = array(); - } - if (!$changes) - { - $changes = array(); - } - if (isset($content['app_header'])) - { - $GLOBALS['egw_info']['flags']['app_header'] = $content['app_header']; - } - if ($GLOBALS['egw_info']['flags']['currentapp'] != 'etemplate') - { - translation::add_app('etemplate'); // some extensions have own texts - } - // use different form-names to allows multiple eTemplates in one page, eg. addressbook-view - self::$name_form = 'eTemplate'; - if (in_array(self::$name_form,self::$name_forms)) - { - self::$name_form .= 1+count(self::$name_forms); - self::$name_vars .= 1+count(self::$name_forms); - } - self::$name_forms[] = self::$name_form; - - self::$request = etemplate_request::read(); - self::$request->output_mode = $output_mode; // let extensions "know" they are run eg. in a popup - self::$request->readonlys = $readonlys; - self::$request->content = $content; - self::$request->changes = $changes; - self::$request->sel_options = $sel_options; - self::$request->preserv = $preserv; - self::$request->method = $method; - self::$request->ignore_validation = $ignore_validation; - self::$request->name_vars = self::$name_vars; - - // tell html5 form validation NOT to validate - if ($ignore_validation) self::$form_options .= ' novalidate="novalidate"'; - - if((int) $output_mode == 3) - { - self::$styles_included[$this->name] = True; - return "\n" - ."\n\n".html::style($this->style)."\n\n" - ."\n".$this->show($content)."\n\n"; - } - - $html = $this->show(self::complete_array_merge($content,$changes),$sel_options,$readonlys,self::$name_vars); - - self::$request->java_script_from_flags = $GLOBALS['egw_info']['flags']['java_script']; - self::$request->java_script_body_tags = array( - 'onload' => egw_framework::set_onload(), - 'onunload' => egw_framework::set_onunload(), - 'onresize' => egw_framework::set_onresize(), - ); - self::$request->java_script_files = egw_framework::js_files(); - self::$request->include_xajax = $GLOBALS['egw_info']['flags']['include_xajax']; - - // check if application of template has a app.js file --> load it - list($app) = explode('.',$this->name); - if (file_exists(EGW_SERVER_ROOT.'/'.$app.'/js/app.js')) - { - egw_framework::validate_file('.','app',$app,false); - } - - if (!$this->sitemgr) - { - $hooked = isset(self::$previous_content) || !isset($GLOBALS['egw']->template) ? - self::$previous_content : $GLOBALS['egw']->template->get_var('phpgw_body'); - } - self::$request->hooked = $hooked ? $hooked : self::$hook_content; - self::$request->hook_app = $hooked ? $GLOBALS['egw_info']['flags']['currentapp'] : self::$hook_app; - self::$request->app_header = $GLOBALS['egw_info']['flags']['app_header']; - if (self::$request->output_mode == -1) self::$request->output_mode = 0; - self::$request->template = $this->as_array(2); - - $html = html::form(html::input('etemplate_exec_id',self::$request->id(),'hidden',' id="etemplate_exec_id"').$html. - html::input_hidden(array( - 'submit_button' => '', - 'innerWidth' => '', - ),'',false),array(),$this->sitemgr ? '' : '/etemplate/process_exec.php?menuaction='.$method, - '',self::$name_form,self::$form_options. - // dont set the width of popups! - ($output_mode != 0 ? '' : ' onsubmit="this.innerWidth.value=window.innerWidth ? window.innerWidth : document.body.clientWidth;"')); - //echo "to_process="; _debug_array(self::$request->to_process); - - egw_framework::validate_file('/etemplate/js/etemplate.js'); - - //echo '

'.__METHOD__."($method,...) etemplate[hooked]=".(int)self::$hooked.", etemplate[hook_app]='".self::$hook_app."', isset(etemplate[content])=".(int)isset(self::$previous_content)."

\n"; - if (!$this->sitemgr) - { - // support the old global var, in case old apps like 1.6 infolog use it - if (isset($GLOBALS['egw_info']['etemplate']['hooked'])) self::$hooked = $GLOBALS['egw_info']['etemplate']['hooked']; - - if (!@self::$hooked && (int) $output_mode != 1 && (int) $output_mode != -1) // not just returning the html - { - if ($GLOBALS['egw_info']['flags']['currentapp'] != 'etemplate') - { - egw_framework::includeCSS('etemplate', 'app'); - } - } - // saving the etemplate content for other hooked etemplate apps (atm. infolog hooked into addressbook) - self::$previous_content =& $html; - } - //echo '

'.__METHOD__."($method,...) after show: sitemgr=$this->sitemgr, hooked=".(int)$hooked.", output_mode=$output_mode

\n"; - - if($output_mode == 2) - { - $html .= ' -'."\n"; - } - - if (!$this->sitemgr && (int) $output_mode != 1 && (int) $output_mode != -1) // NOT returning html - { - if (!@self::$hooked) - { - if((int) $output_mode != 2) - { - // jdots needs sidebox/navbar data in header call, therefore enable navbar BEFORE calling header - $GLOBALS['egw_info']['flags']['nonavbar'] = false; - echo $GLOBALS['egw']->framework->header(); - echo $GLOBALS['egw']->framework->navbar(); - } - else - { - echo $GLOBALS['egw']->framework->header(); - echo '
'."\n"; - if ($GLOBALS['egw_info']['user']['apps']['manual']) // adding a manual icon to every popup - { - $manual = new etemplate_old('etemplate.popup.manual'); - echo $manual->show(array()); - unset($manual); - echo ''."\n"; - echo '
'.html::image('phpgwapi','ajax-loader') . '
'; - } - } - } - echo self::$hook_content.$html; - - if (!self::$hooked && (!isset($_GET['menuaction']) || strpos($_SERVER['PHP_SELF'],'process_exec.php') !== false)) - { - if((int) $output_mode == 2) - { - echo "
\n"; - } - common::egw_footer(); - } - } - if ($this->sitemgr || (int) $output_mode == 1 || (int) $output_mode == -1) // return html - { - return $html; - } - } - - /** - * Check if we have not ignored validation errors - * - * @param string $ignore_validation='' if not empty regular expression for validation-errors to ignore - * @param string $cname=null name-prefix, which need to be ignored, default self::$name_vars - * @return boolean true if there are not ignored validation errors, false otherwise - */ - static function validation_errors($ignore_validation='',$cname=null) - { - if (is_null($cname)) $cname = self::$name_vars; - //echo "

uiself::validation_errors('$ignore_validation','$cname') validation_error="; _debug_array(self::$validation_errors); - if (!$ignore_validation) return count(self::$validation_errors) > 0; - - foreach(self::$validation_errors as $name => $error) - { - if (!self::ignore_validation_match($name, $ignore_validation)) - { - //echo "

uiself::validation_errors('$ignore_validation','$cname') name='$name' ($error) not ignored!!!

\n"; - return true; - } - //echo "

uiself::validation_errors('$ignore_validation','$cname') name='$name' ($error) ignored

\n"; - } - return false; - } - - /** - * Check if given form-name matches ai ignore-validation rule - * - * @param string $ignore_validation='' if not empty regular expression for validation-errors to ignore - * @param string $cname=null name-prefix, which need to be ignored, default self::$name_vars - * @param string $cname - * @return boolean - */ - static function ignore_validation_match($form_name, $ignore_validation, $cname=null) - { - if (is_null($cname)) $cname = self::$name_vars; - if ($cname) $form_name = preg_replace('/^'.$cname.'\[([^\]]+)\](.*)$/','\\1\\2', $form_name); - - return empty($ignore_validation) || - $ignore_validation[0] == '/' && preg_match($ignore_validation, $form_name) || - $ignore_validation[0] != '/' && $ignore_validation == $form_name; - } - - /** - * Makes the necessary adjustments to _POST before it calls the app's method - * - * This function is only to submit forms to, create with exec. - * All eTemplates / forms executed with exec are submited to this function - * via /etemplate/process_exec.php?menuaction=. We cant use the global index.php as - * it would set some constants to etemplate instead of the calling app. - * process_exec then calls process_show for the eTemplate (to adjust the content of the _POST) and - * ExecMethod's the given callback from the app with the content of the form as first argument. - * - * @return mixed false if no sessiondata and $this->sitemgr, else the returnvalue of exec of the method-calls - */ - function process_exec($etemplate_exec_id = null, $submit_button = null, $exec = null, $type = 'regular' ) - { - if(!$etemplate_exec_id) $etemplate_exec_id = $_POST['etemplate_exec_id']; - if(!$submit_button) $submit_button = $_POST['submit_button']; - if(!$exec) $exec = $_POST; - - //echo "process_exec: _POST ="; _debug_array($_POST); - if (!$etemplate_exec_id || !(self::$request = etemplate_request::read($etemplate_exec_id))) - { - if ($this->sitemgr) return false; - //echo "uitemplate::process_exec() id='$_POST[etemplate_exec_id]' invalid session-data !!!"; _debug_array($_SESSION); - // this prevents an empty screen, if the sessiondata gets lost somehow - $redirect = array( - 'menuaction' => $_GET['menuaction'], - ); - if (!$_POST && $_SERVER['REQUEST_METHOD'] == 'POST') - { - $redirect['post_empty'] = 1; - // check if we have a failed upload, because user tried to uploaded a file - // bigger then php.ini setting post_max_size - // in that case the webserver calls PHP with $_POST === array() - if (substr($_SERVER['CONTENT_TYPE'],0,19) == 'multipart/form-data' && - $_SERVER['CONTENT_LENGTH'] > self::km2int(ini_get('post_max_size'))) - { - $redirect['failed_upload'] = 1; - $redirect['msg'] = lang('Error uploading file!')."\n".self::max_upload_size_message(); - } - } else if ($_SERVER['REQUEST_METHOD'] == 'GET') { - // Pass along any parameters - $redirect = $_GET; - } - $this->location($redirect); - } - self::$name_vars = self::$request->name_vars; - if (isset($submit_button) && !empty($submit_button)) - { - self::set_array($exec,$submit_button,'pressed'); - } - $content = $exec[self::$name_vars]; - if (!is_array($content)) - { - $content = array(); - } - $this->init(self::$request->template); - //echo "process_exec($this->name) content ="; _debug_array($content); - if ($GLOBALS['egw_info']['flags']['currentapp'] != 'etemplate') - { - $GLOBALS['egw']->translation->add_app('etemplate'); // some extensions have own texts - } - $this->process_show($content,self::$request->to_process,self::$name_vars,$type); - - self::$loop |= !$this->canceled && $this->button_pressed && - $this->validation_errors(self::$request->ignore_validation); // set by process_show - - // If a tab has an error on it, change to that tab - foreach(self::$validation_errors as $form_name => $msg) - { - $name = $this->template_name($form_name); - if (!$this->get_widget_by_name($name)) - { - foreach($this->get_widgets_by_type('tab') as $widget) - { - $tab_name = $tabs = $widget['name']; - if (strpos($tabs,'=') !== false) list($tab_name,$tabs) = explode('=',$tabs,2); - foreach(explode('|',$tabs) as $tab) - { - if (strpos('.',$tab) === false) $tab = $this->name.'.'.$tab; - $tab_tpl = new etemplate_old($tab); - if ($tab_tpl->get_widget_by_name($name)) - { - $content[$tab_name] = $tab; - break 3; - } - elseif($name[0] == '#') - { - foreach($tab_tpl->get_widgets_by_type('customfields') as $cf_widget) - { - if (empty($cf_widget['name'])) - { - $content[$tab_name] = $tab; - break 4; - } - } - } - } - } - // widget NOT found --> as last resort add valdation message to $content['msg'] - $content['msg'] .= ($content['msg'] ? "\n" : '').$msg; - } - } - - //echo "process_exec($this->name) process_show(content) ="; _debug_array($content); - //echo "process_exec($this->name) session_data[changes] ="; _debug_array(self::$request->changes); - $content = self::complete_array_merge(self::$request->changes,$content); - //echo "process_exec($this->name) merge(changes,content) ="; _debug_array($content); - - if (self::$loop && $type == 'regular') // only loop for regular (not ajax_submit) requests - { - if (self::$request->hooked != '') // set previous phpgw_body if we are called as hook - { - self::$hook_content = self::$request->hooked; - $GLOBALS['egw_info']['flags']['currentapp'] = self::$hook_app = self::$request->hook_app; - - // Prevent previous content form names from being used again in exec() - preg_match_all('/hooked, $used); - if(is_array($used[1]) && count($used[1])) - { - self::$name_forms += $used[1]; - } - } - if(self::$request->include_xajax) $GLOBALS['egw_info']['flags']['include_xajax'] = true; - - if (!empty(self::$request->app_header)) - { - $GLOBALS['egw_info']['flags']['app_header'] = self::$request->app_header; - } - - $GLOBALS['egw_info']['flags']['java_script'] .= self::$request->java_script_from_flags; - if (!empty(self::$request->java_script_body_tags)) - { - foreach (self::$request->java_script_body_tags as $tag => $code) - { - call_user_func('egw_framework::set_'.$tag,$code); - } - } - if (is_array(self::$request->java_script_files)) - { - $files = egw_framework::js_files(); - if (is_array($files)) - { - $files = array_unique(array_merge($files,self::$request->java_script_files)); - } - else - { - $files = self::$request->java_script_files; - } - egw_framework::js_files($files); - } - - //echo "

process_exec($this->name): loop is set, content=

\n"; _debug_array(self::complete_array_merge(self::$request->content,$content)); - return $this->exec(self::$request->method,self::complete_array_merge(self::$request->content,$content), - self::$request->sel_options,self::$request->readonlys,self::$request->preserv, - self::$request->output_mode,self::$request->ignore_validation,$content); - } - else - { - //echo "

process_exec($this->name): calling ".($type == 'regular' ? self::$request->method : $_GET['menuaction'])."

\n"; - return ExecMethod($type == 'regular' ? self::$request->method : $_GET['menuaction'], - self::complete_array_merge(self::$request->preserv,$content)); - } - } - - /** - * Message containing the max Upload size from the current php.ini settings - * - * We have to take the smaler one of upload_max_filesize AND post_max_size-2800 into account. - * memory_limit does NOT matter any more, because of the stream-interface of the vfs. - * - * @param int &$max_upload=null on return max. upload size in byte - * @return string - */ - static function max_upload_size_message(&$max_upload=null) - { - $upload_max_filesize = ini_get('upload_max_filesize'); - $post_max_size = ini_get('post_max_size'); - $max_upload = min(self::km2int($upload_max_filesize),self::km2int($post_max_size)-2800); - - return lang('Maximum size for uploads').': '.egw_vfs::hsize($max_upload). - " (php.ini: upload_max_filesize=$upload_max_filesize, post_max_size=$post_max_size)"; - } - - /** - * Convert numbers like '32M' or '512k' to integers - * - * @param string $size - * @return int - */ - private static function km2int($size) - { - if (!is_numeric($size)) - { - switch(strtolower(substr($size,-1))) - { - case 'm': - $size = 1024*1024*(int)$size; - break; - case 'k': - $size = 1024*(int)$size; - break; - } - } - return (int)$size; - } - - /** - * process the values transfered with the javascript function values2url - * - * The returned array contains the preserved values overwritten (only!) with the variables named in values2url - * - * @return array/boolean content array or false on error - */ - function process_values2url() - { - //echo "process_exec: _GET ="; _debug_array($_GET); - if (!$_GET['etemplate_exec_id'] || !($request = etemplate_request::read($_GET['etemplate_exec_id']))) - { - return false; - } - self::$name_vars = $request->name_vars; - - $content = $_GET[self::$name_vars]; - if (!is_array($content)) - { - $content = array(); - } - $this->process_show($content,$request->to_process,self::$name_vars); - - return self::complete_array_merge($request->preserv,$content); - } - - /** - * Flag if the styles of a certain template are already included - * - * @var array template-name => boolean - */ - static private $styles_included = array(); - - /** - * creates HTML from an eTemplate - * - * This is done by calling show_cell for each cell in the form. show_cell itself - * calls show recursivly for each included eTemplate. - * You could use it in the UI-layer of an app, just make shure to call process_show !!! - * This is intended as internal function and should NOT be called by new app's direct, - * as it deals with HTML and is so UI-dependent, use exec instead. - * - * @internal - * @param array $content with content for the cells, keys are the names given in the cells/form elements - * @param array $sel_options with options for the selectboxes, keys are the name of the selectbox - * @param array $readonlys with names of cells/form-elements to be not allowed to change - * This is to facilitate complex ACL's which denies access on field-level !!! - * @param string $cname basename of names for form-elements, means index in $_POST - * eg. $cname='cont', element-name = 'name' returned content in $_POST['cont']['name'] - * @param string $show_c name/index for name expansion - * @param string $show_row name/index for name expansion - * @return string the generated HTML - */ - function show($content,$sel_options='',$readonlys='',$cname='',$show_c=0,$show_row=0) - { - if (!$sel_options) - { - $sel_options = array(); - } - // make it globaly availible for show_cell and show_grid, or extensions - $this->sel_options =& $sel_options; - - if (!$readonlys) - { - $readonlys = array(); - } - if (++$this->already_showed > 1) return ''; // prefens infinit self-inclusion - - if (is_int($this->debug) && $this->debug >= 1 || $this->name && $this->debug == $this->name) - { - echo "

etemplate.show($this->name): $cname =\n"; _debug_array($content); - echo "readonlys="; _debug_array($readonlys); - } - if (!is_array($content)) - { - $content = array(); // happens if incl. template has no content - } - // make the content availible as class-public for extensions - $this->content =& $content; - - $html = "\n\n\n

name)."\">\n\n"; - if (!self::$styles_included[$this->name]) - { - self::$styles_included[$this->name] = True; - $html .= html::style($this->style)."\n\n"; - } - $path = '/'; - foreach ($this->children as $n => $child) - { - $h = $this->show_cell($child,$content,$readonlys,$cname,$show_c,$show_row,$nul,$class,$path.$n); - $html .= $class || $child['align'] ? html::div($h,html::formatOptions(array( - $class, - $child['align'], - ),'class,align')) : $h; - } - return $html."\n
\n\n\n"; - } - - /** - * Get the color of a category - * - * For multiple cats, the first with a color is used - * - * @param int/string $cats multiple comma-separated cat_id's - * @return string - */ - static function cats2color($cats) - { - static $cat2color; - - // ACL check - $cats = $GLOBALS['egw']->categories->check_list(EGW_ACL_READ,$cats); - - if (!$cats) return null; - - if (isset($cat2color[$cats])) - { - return $cat2color[$cats]; - } - - foreach(explode(',',$cats) as $cat) - { - if (isset($cat2color[$cat])) - { - return $cat2color[$cat]; - } - $data = categories::id2name($cat,'data'); - - if (is_array($data) && ($color = $data['color'])) - { - //echo "

cats2color('$cats')=$color

\n"; - return $cat2color[$cats] = $cat2color[$cat] = $color; - } - } - return null; - } - - /** - * creates HTML from an eTemplate - * - * This is done by calling show_cell for each cell in the form. show_cell itself - * calls show recursivly for each included eTemplate. - * You can use it in the UI-layer of an app, just make shure to call process_show !!! - * This is intended as internal function and should NOT be called by new app's direct, - * as it deals with HTML and is so UI-dependent, use exec instead. - * - * @internal - * @param array $grid representing a grid - * @param array $content with content for the cells, keys are the names given in the cells/form elements - * @param array $readonlys with names of cells/form-elements to be not allowed to change - * This is to facilitate complex ACL's which denies access on field-level !!! - * @param string $cname basename of names for form-elements, means index in $_POST - * eg. $cname='cont', element-name = 'name' returned content in $_POST['cont']['name'] - * @param string $show_c name/index for name expansion - * @param string $show_row name/index for name expansion - * @param string $path path in the widget tree - * @return string the generated HTML - */ - private function show_grid(&$grid,$content,$readonlys='',$cname='',$show_c=0,$show_row=0,$path='') - { - if (!$readonlys) - { - $readonlys = array(); - } - if (is_int($this->debug) && $this->debug >= 2 || $grid['name'] && $this->debug == $grid['name'] || - $this->name && $this->debug == $this->name) - { - echo "

etemplate.show_grid($grid[name]): $cname =\n"; _debug_array($content); - } - if (!is_array($content)) - { - $content = array(); // happens if incl. template has no content - } - $content += array( // for var-expansion in names in show_cell - '.c' => $show_c, - '.col' => $this->num2chrs($show_c-1), - '.row' => $show_row - ); - $rows = array(); - - $data = &$grid['data']; - reset($data); - if (isset($data[0])) - { - list(,$opts) = each($data); - } - else - { - $opts = array(); - } - $row_id = $content['_row_id']; - - $max_cols = $grid['cols']; - for ($r = 0; $row = 1+$r /*list($row,$cols) = each($data)*/; ++$r) - { - if (!(list($r_key) = each($data))) // no further row - { - if (!(($this->autorepeat_idx($cols['A'],0,$r,$idx,$idx_cname,false,$content) && $idx_cname) || - (in_array($cols['A']['type'], array('vbox','hbox','box')) && $this->autorepeat_idx($cols['A'][1],0,$r,$idx,$idx_cname,false,$content) && $idx_cname) || - ($this->autorepeat_idx($cols['B'],1,$r,$idx,$idx_cname,false,$content) && $idx_cname)) || - !$this->isset_array($content,$idx_cname)) - { - break; // no auto-row-repeat - } - } - else - { - $cols = &$data[$r_key]; - $part = ''; // '' = body-prefix - list($height,$disabled,$part) = explode(',',$opts["h$row"]); - $class = $opts["c$row"]; - } - if ($disabled != '' && $this->check_disabled($disabled,$content,$r)) - { - continue; // row is disabled - } - if ($part) $row = $part[0].$row; // add part prefix - $rows[".$row"] .= html::formatOptions($height,'height'); - list($cl) = explode(',',$class); - if ($cl == '@' || $cl && strpos($cl,'$') !== false) - { - $cl = $this->expand_name($cl,0,$r,$content['.c'],$content['.row'],$content); - if (!$cl || preg_match('/(^| )([0-9,]+)( |$)/',$cl,$matches)) - { - if (($color = $this->cats2color($matches[2]))) - { - $rows[".$row"] .= ' style="background-color: '.$color.';"'; - } - $cl = str_replace($matches[2],'row',$cl); - } - } - if ($cl == 'nmr' || substr($cl,0,3) == 'row') // allow to have further classes behind row - { - $cl = 'row_'.($nmr_alternate++ & 1 ? 'off' : 'on').substr($cl,3); // alternate color - } - $cl = isset(self::$class_conf[$cl]) ? self::$class_conf[$cl] : $cl; - $rows[".$row"] .= html::formatOptions($cl,'class'); - $rows[".$row"] .= html::formatOptions($class,',valign'); - // set row-id, if requested - if ($row_id && isset($content[$r][$row_id])) - { - $rows[".$row"] .= ' id="'.htmlspecialchars($content[$r][$row_id]).'"'; - } - reset ($cols); - $row_data = array(); - for ($c = 0; True /*list($col,$cell) = each($cols)*/; ++$c) - { - $col = $this->num2chrs($c); - if (!(list($c_key) = each($cols))) // no further cols - { - // only check if the max. column-number reached so far is exeeded - // otherwise the rows have a differen number of cells and it saved a lot checks - if ($c >= $max_cols) - { - if (!$this->autorepeat_idx($cell,$c,$r,$idx,$idx_cname,True,$content) || - !$this->isset_array($content,$idx)) - { - break; // no auto-col-repeat - } - $max_cols = $c+1; - } - } - else - { - $cell = $cols[$c_key]; - list($col_width,$col_disabled) = explode(',',$opts[$col]); - - if (!$cell['height']) // if not set, cell-height = height of row - { - $cell['height'] = $height; - } - if (!$cell['width']) // if not set, cell-width = width of column or table - { - list($col_span) = explode(',',$cell['span']); - if ($col_span == 'all' && !$c) - { - list($cell['width']) = explode(',',$this->size); - } - else - { - $cell['width'] = $col_width; - } - } - } - if ($col_disabled != '' && $this->check_disabled($col_disabled,$content,$r,$c)) - { - continue; // col is disabled - } - $align_was = $cell['align']; - $row_data[$col] = $this->show_cell($cell,$content,$readonlys,$cname,$c,$r,$span,$cl,$path.'/'.$r_key.$c_key); - - if ($row_data[$col] == '' && $this->rows == 1) - { - unset($row_data[$col]); // omit empty/disabled cells if only one row - continue; - } - if (strlen($cell['onclick']) > 1) - { - $onclick = $cell['onclick']; - if (strpos($onclick,'$') !== false || $onclick[0] == '@') - { - $onclick = $this->expand_name($onclick,$c,$r,$content['.c'],$content['.row'],$content); - } - $row_data[".$col"] .= ' onclick="'.$this->js_pseudo_funcs($onclick,$cname).'"' .self::get_id('',$cell['name'],$cell['id']); - } - $colspan = $span == 'all' ? $grid['cols']-$c : 0+$span; - if ($colspan > 1) - { - $row_data[".$col"] .= " colspan=\"$colspan\""; - for ($i = 1; $i < $colspan; ++$i,++$c) - { - each($cols); // skip next cell(s) - } - } - else - { - list($width,$disable) = explode(',',$opts[$col]); - if ($width) // width only once for a non colspan cell - { - $row_data[".$col"] .= " width=\"$width\""; - $opts[$col] = "0,$disable"; - } - } - $row_data[".$col"] .= html::formatOptions($cell['align']?$cell['align']:($align_was?$align_was:'left'),'align'); - // allow to set further attributes in the tablecell, beside the class - if (is_array($cl)) - { - foreach($cl as $attr => $val) - { - if ($attr != 'class' && $val) - { - $row_data['.'.$col] .= ' '.$attr.'="'.$val.'"'; - } - } - $cl = $cl['class']; - } - $cl = $this->expand_name(isset(self::$class_conf[$cl]) ? self::$class_conf[$cl] : $cl, - $c,$r,$show_c,$show_row,$content); - // else the class is set twice, in the table and the table-cell, which is not good for borders - if ($cl && $cell['type'] != 'template' && $cell['type'] != 'grid') - { - $row_data[".$col"] .= html::formatOptions($cl,'class'); - } - } - $rows[$row] = $row_data; - } - if (!$rows) return ''; - - list($width,$height,,,,,$overflow) = $options = explode(',',$grid['size']); - if ($overflow && $height) - { - $options[1] = ''; // set height in div only - } - $html = html::table($rows,html::formatOptions($options,'width,height,border,class,cellspacing,cellpadding'). - html::formatOptions($grid['span'],',class'). - html::formatOptions($grid['name']?self::form_name($cname,$grid['name']):'','id')); - - if (!empty($overflow)) - { - if (is_numeric($height)) $height .= 'px'; - if (is_numeric($width)) $width .= 'px'; - if ($width == '100%') $overflow .= '; overflow-x: hidden'; // no horizontal scrollbar - $div_style=' style="'.($width?"width: $width; ":'').($height ? "height: $height; ":'')."overflow: $overflow;\""; - $html = html::div($html,$div_style); - } - - // initialise egw_actions for nextmatch widget, if egwGridView_grid CSS class set - if ($options[3] == 'egwGridView_grid') - { - $html .= nextmatch_widget::init_egw_actions($content['_actions'], $content['action_links'], $this->name); - } - - return "\n\n\n$html\n\n"; - } - - /** - * build the name of a form-element from a basename and name - * - * name and basename can contain sub-indices in square bracets, eg. basename="base[basesub1][basesub2]" - * and name = "name[sub]" gives "base[basesub1][basesub2][name][sub]" - * - * @param string $cname basename - * @param string $name name - * @return string complete form-name - */ - static function form_name($cname,$name) - { - if(is_object($name)) return ''; - - $name_parts = explode('[',str_replace(']','',$name)); - if (!empty($cname)) - { - array_unshift($name_parts,$cname); - } - $form_name = array_shift($name_parts); - if (count($name_parts)) - { - $form_name .= '['.implode('][',$name_parts).']'; - } - return $form_name; - } - - /** - * strip the prefix of a form-element from a form_name - * This function removes the prefix of form_name(). It takes a name like base[basesub1][basesub2][name][sub] - * and gives basesub1[basesub2][name][sub] - * - * @param string form_name - * @return string name without prefix - */ - static private function template_name($form_name) - { - $parts = explode('[',str_replace(']','',$form_name)); - - array_shift($parts); // remove exec - - $name = array_shift($parts); - - if ($parts) $name .= '['.implode('][',$parts).']'; - - return $name; - } - - static private $class_conf = array('nmh' => 'th','nmr0' => 'row_on','nmr1' => 'row_off'); - - /** - * generates HTML for one widget (input-field / cell) - * - * calls show to generate included eTemplates. Again only an INTERMAL function. - * - * @internal - * @param array $cell with data of the cell: name, type, ... - * @param array $content with content for the cells, keys are the names given in the cells/form elements - * @param array $readonlys with names of cells/form-elements to be not allowed to change - * This is to facilitate complex ACL's which denies access on field-level !!! - * @param string $cname basename of names for form-elements, means index in $_POST - * eg. $cname='cont', element-name = 'name' returned content in $_POST['cont']['name'] - * @param string $show_c name/index for name expansion - * @param string $show_row name/index for name expansion - * @param string &$span on return number of cells to span or 'all' for the rest (only used for grids) - * @param string &$class on return the css class of the cell, to be set in the tag - * @param string $path path in the widget tree - * @return string the generated HTML - */ - private function show_cell(&$cell,$content,$readonlys,$cname,$show_c,$show_row,&$span,&$class,$path='') - { - if ($this->debug && (is_int($this->debug) && $this->debug >= 3 || $this->debug == $cell['type'])) - { - echo "

etemplate.show_cell($this->name,name='${cell['name']}',type='${cell['type']}',cname='$cname',...,'$path')

\n"; - } - list($span) = explode(',',$cell['span']); // evtl. overriten later for type template - - if ($cell['name'][0] == '@' && $cell['type'] != 'template') - { - $cell['name'] = $this->get_array($content,$this->expand_name(substr($cell['name'],1), - $show_c,$show_row,$content['.c'],$content['.row'],$content)); - } - $name = $this->expand_name($cell['name'],$show_c,$show_row,$content['.c'],$content['.row'],$content); - // allow names like "tabs=one|two|three", which will be equal to just "tabs" - // eg. for tabs to use a name independent of the tabs contained - if (is_string($name) && strpos($name,'=') !== false) - { - list($name) = explode('=',$name); - } - $form_name = self::form_name($cname,$name); - $value =& $this->get_array($content,$name); - $old_value = $value; // remember value to be able to restore it - $options = ''; - if ($readonly = $cell['readonly'] && $readonlys[$name] !== false || // allow to overwrite readonly settings of a cell - @$readonlys[$name] && !is_array($readonlys[$name]) || $readonlys['__ALL__'] && (!is_string($name) || $readonlys[$name] !== false) || - !empty($name) && is_string($name) && ($p = strrpos($name,'[')) !== false && ($parent=substr($name,0,$p)) && $readonlys[$parent]) // allow also set parent readonly (instead each child) - { - $options .= ' readonly="readonly"'; - } - if ((int) $cell['tabindex']) $options .= ' tabindex="'.(int)$cell['tabindex'].'"'; - if ($cell['accesskey']) $options .= ' accesskey="'.html::htmlspecialchars($cell['accesskey']).'"'; - - if (is_string($cell['size']) && (strchr($cell['size'],'$') || $cell['size'][0] == '@')) // expand cell['size'] for the button-disabled-check now - { - $cell['size'] = $this->expand_name($cell['size'],$show_c,$show_row,$content['.c'],$content['.row'],$content); - } - if ($cell['type'] == 'template' && !$this->check_disabled($cell['disabled'], $content)) - { - // template is NOT disabled (eg. in editor) - } - elseif ($cell['disabled'] && $readonlys[$name] !== false || $readonly && in_array($cell['type'],array('button','buttononly','image','progress')) && strpos($cell['size'],',') === false) - { - if ($this->rows == 1) - { - return ''; // if only one row omit cell - } - $cell = $this->empty_cell('label','',array('span' => $cell['span'])); // show nothing (keep the css class!) - $name = $value = ''; - } - $extra_label = True; - - if (strchr($cell['onchange'],'$') || $cell['onchange'][0] == '@') - { - $cell['onchange'] = $this->expand_name($cell['onchange'],$show_c,$show_row,$content['.c'],$content['.row'],$content); - } - if ($cell['type'][0] == '@') - { - $cell['type'] = $this->expand_name($t=$cell['type'],$show_c,$show_row,$content['.c'],$content['.row'],$content); - } - // the while loop allows to build extensions from other extensions - // please note: only the first extension's post_process function is called !!! - list($type,$sub_type) = explode('-',$cell['type']); - while ((!self::$types[$cell['type']] || !empty($sub_type)) && $this->haveExtension($type,'pre_process')) - { - //echo "

pre_process($cell[name]/$cell[type])

\n"; - if (is_string($cell['size']) && (strchr($cell['size'],'$') || $cell['size'][0] == '@')) - { - $cell['size'] = $this->expand_name($cell['size'],$show_c,$show_row,$content['.c'],$content['.row'],$content); - } - if (strchr($cell['onchange'],'$') || $cell['onchange'][0] == '@') - { - $cell['onchange'] = $this->expand_name($cell['onchange'],$show_c,$show_row,$content['.c'],$content['.row'],$content); - } - if (!$ext_type) $ext_type = $type; - // if readonlys[__ALL__] is set, also set readonlys[$name] (extensions can mark themselfs as 'noReadonlysALL', eg. tab-widget!) - if ($readonlys['__ALL__'] && !$this->haveExtension($type,'noReadonlysALL')) - { - $readonlys[$name] = true; - } - $extra_label = $this->extensionPreProcess($type,$form_name,$value,$cell,$readonlys[$name]); - - $readonly = $cell['readonly'] !== false && ($readonly || $cell['readonly']); // might be set or unset (===false) by extension - - self::set_array($content,$name,$value); - - if ($cell['type'] == $type.'-'.$sub_type) break; // stop if no further type-change - - list($type,$sub_type) = explode('-',$cell['type']); - } - list(,$class) = explode(',',$cell['span']); // might be set by extension - if (strchr($class,'$') || $class[0] == '@') - { - $class = $this->expand_name($class,$show_c,$show_row,$content['.c'],$content['.row'],$content); - } - if ($cell['needed'] && !in_array($cell['type'],array('button','buttononly'))) - { - $class .= ' inputRequired'; - } - $cell_options = $cell['size']; - if (strchr($cell_options,'$') || $cell_options[0] == '@') - { - $cell_options = $this->expand_name($cell_options,$show_c,$show_row,$content['.c'],$content['.row'],$content); - } - $label = $cell['label']; - if (strchr($label,'$') || $label[0] == '@') - { - $label = $this->expand_name($label,$show_c,$show_row,$content['.c'],$content['.row'],$content); - } - $help = $cell['help']; - if (strchr($help,'$') || $help[0] == '@') - { - $no_lang_on_help = true; - $help = $this->expand_name($help,$show_c,$show_row,$content['.c'],$content['.row'],$content); - } - $blur = $cell['blur'][0] == '@' ? $this->get_array($content,substr($cell['blur'],1)) : - (strlen($cell['blur']) <= 1 ? $cell['blur'] : lang($cell['blur'])); - - if ($blur) - { - if ((string)$value === '') - { - $value = $blur; - } - $onFocus .= "if(this.value=='".addslashes(html::htmlspecialchars($blur))."') this.value='';"; - $onBlur .= "if(this.value=='') this.value='".addslashes(html::htmlspecialchars($blur))."';"; - } - if ($help) - { - if ((int)$cell['no_lang'] < 2 && !$no_lang_on_help) - { - if (($use_tooltip_for_help = $help[0] == '|')) $help = substr($help,1); - $help = lang($help); - } - if (substr($help,0,5) == 'call:') - { - $options .= ' onMouseOver="'.html::htmlspecialchars(substr($help,5)).'"'; - } - elseif (($use_tooltip_for_help = $use_tooltip_for_help || strpos($help,'<') !== false && strip_tags($help) != $help)) // helptext is html => use a tooltip - { - $options .= html::tooltip($help); - } - else // "regular" help-text in the statusline - { - $onFocus .= "self.status='".addslashes(html::htmlspecialchars($help))."'; return true;"; - $onBlur .= "self.status=''; return true;"; - if (in_array($cell['type'],array('button','buttononly','file'))) // for button additionally when mouse over button - { - $options .= " onMouseOver=\"self.status='".addslashes(html::htmlspecialchars($help))."'; return true;\""; - $options .= " onMouseOut=\"self.status=''; return true;\""; - } - } - } - if ($onBlur) - { - $options .= " onFocus=\"$onFocus\" onBlur=\"$onBlur\""; - } - if ($cell['onchange'] && !($cell['type'] == 'button' || $cell['type'] == 'buttononly')) - { - $onchange = $cell['onchange'] == '1' ? 'this.form.submit();' : $this->js_pseudo_funcs($cell['onchange'],$cname); - // rewriting onchange for checkboxes for IE to an onclick - if ($cell['type'] == 'checkbox' && html::$user_agent == 'msie') - { - $options .= ' onClick="'.$onchange.'; return true;"'; - } - else - { - $options .= ' onChange="'.$onchange.'"'; - } - } - - if ($form_name != '') - { - $options = self::get_id($form_name,$cell['name'],$cell['id']).' '.$options; - } - switch ($type) - { - case 'label': // size: [b[old]][i[talic]],[link],[activate_links],[label_for],[link_target],[link_popup_size],[link_title] - if (is_array($value)) break; - if($cell_options) - { - list($style,$extra_link,$activate_links,$label_for,$extra_link_target,$extra_link_popup,$extra_link_title) = self::csv_split($cell_options,7); - } - else - { - $style = $cell['font_style']; - $extra_link = $cell['href']; - $activate_links = $cell['activate_links']; - $label_for = $cell['for']; - $extra_link_target = $cell['extra_link_target']; - $extra_link_popup = $cell['extra_link_popup']; - $extra_link_title = $cell['extra_link_title']; - } - $value = strlen($value) > 1 && !$cell['no_lang'] ? lang($value) : $value; - $value = nl2br(html::htmlspecialchars($value)); - if ($activate_links) $value = html::activate_links($value); - if ($value != '' && $style && strpos($style,'b')!==false) $value = html::bold($value); - if ($value != '' && $style && strpos($style,'i')!==false) $value = html::italic($value); - // if the label has a name, use it as id in a span, to allow addressing it via javascript - $html .= ($name ? '' : '').$value.($name ? '' : ''); - if ($help) - { - $class = array( - 'class' => $class, - ); - list($class['onmouseover'],$class['onmouseout']) = html::tooltip($help, False, False, true); - } - break; - case 'html': // size: [link],[link_target],[link_popup_size],[link_title],[activate_links] - list($extra_link,$extra_link_target,$extra_link_popup,$extra_link_title,$activate_links) = explode(',',$cell_options); - if ($activate_links) $value = html::activate_links($value); - $html .= $value; - break; - case 'int': // size: [min],[max],[len],[precision/sprint format],[step] - case 'integer': - case 'float': - list($min,$max,$cell_options,$pre,$step) = explode(',',$cell_options); - // a few html5 options - if ((string)$min !== '') $options .= ' min="'.htmlspecialchars($min).'"'; - if ((string)$max !== '') $options .= ' max="'.htmlspecialchars($max).'"'; - // default step="any" for float, as not setting it limits value to integer as step defaults to 1 in html5! - if (is_numeric($step) || $type == 'float') - { - $options .= ' step="'.(is_numeric($step)?$step:'any').'"'; - } - if ($cell_options == '' && !$readonly) - { - $cell_options = $cell['type'] != 'float' ? 5 : 8; - } - // html5 input type=nummeric seems to ignore size, setting a width instead - $options .= ' style="width: '.(3+abs($cell_options)).'ex"'; - if (($type == 'float' || !is_numeric($pre)) && $value && $pre) - { - $value = is_numeric($pre) ? self::number_format($value,$pre,$readonly) : sprintf($pre,$value); - } - $cell_options .= ',,'.($cell['type'] != 'float' ? '/^-?[0-9]*$/' : '"/^-?[0-9]*[,.]?[0-9]*$/"').',number'; - // fall-through - case 'hidden': - case 'passwd': - case 'text': // size: [length][,maxLength[,preg[,html5type]]] - case 'textbox': - $cell_opts = $c = self::csv_split($cell_options); // allows to enclose preg in quote to allow comma - // fix preg, in case it contains a comma (html5type is only letters and always last option!) - if (count($cell_opts) > 3 && ($cell_opts2 = explode(',',$cell_options)) && $cell_opts2[2][0] != '"') - { - $html5type = array_pop($cell_opts); - $cell_opts = explode(',',$cell_options,3); - if (preg_match('/^[a-z]+$/i',$html5type)) - { - $cell_opts[2] = substr($cell_opts[2],0,-strlen($html5type)-1); - if ($cell_opts[2][0] == ',') $cell_opts[2] = ''; // happens in link-entry under some condition - $cell_opts[] = $html5type; - } - } - if ($readonly && (int)$cell_opts[0] >= 0) - { - $html .= strlen($value) ? html::bold(html::htmlspecialchars($value)) : ''; - } - else - { - if ($cell_opts[0] < 0) - { - $cell_opts[0] = abs($cell_opts[0]); - $options .= ' readonly="readonly"'; - } - // only add html5 required attribute, if validation is NOT ignored - if ($cell['needed'] && self::ignore_validation_match($form_name, self::$request->ignore_validation, $cname)) - { - $required = ' required'; - } - $html .= html::input($form_name,$value,$type == 'passwd' ? 'password' : ($type == 'hidden' ? 'hidden' : $cell_opts[3]), - $options.html::formatOptions($cell_opts,'SIZE,MAXLENGTH'). - $required.($type == 'passwd'?' autocomplete="off"':'')); - - if (!$readonly) - { - self::$request->set_to_process($form_name,$cell['type'],array( - 'maxlength' => $cell_opts[1], - 'needed' => $cell['needed'], - 'preg' => $cell_opts[2], - 'min' => $min, // int and float only - 'max' => $max, - )); - } - } - unset($cell_opts); - break; - case 'textarea': // Multiline Text Input, size: [rows][,cols] - if ($readonly && !$cell_options) - { - $html .= '
'.nl2br(html::htmlspecialchars($value))."
\n"; - } - else - { - // if textarea is readonly, but form_name is already used by an other widget, dont use it - // browser would only send the content of the readonly (and therefore unchanged) field - if ($readonly && self::$request->isset_to_process($form_name)) $form_name = ''; - $html .= html::textarea($form_name,$value, - $options.html::formatOptions($cell_options,'ROWS,COLS').($cell['needed']?' required="required"':'')); - } - if (!$readonly) - { - self::$request->set_to_process($form_name,$cell['type'],array( - 'needed' => $cell['needed'], - )); - } - break; - case 'htmlarea': // Multiline formatted Text Input, size: {simple|extended|advanced},height,width,toolbar-expanded,upload-path - list($mode,$height,$width,$toolbar,$baseref,$convertnl) = explode(',',$cell_options); - - if ($convertnl) - { - $value = nl2br(html::htmlspecialchars($value)); - } - if (!$readonly) - { - $height = $height ? $height : '400px'; - $width = $width ? $width : '100%'; - $fckoptions = array( - 'toolbar_expanded' => $toolbar, - ); - // html::fckEditor runs everything through html::purify - $html .= html::fckEditor($form_name,$value,$mode,$fckoptions,$height,$width,$baseref); - - self::$request->set_to_process($form_name,$cell['type'],array( - 'needed' => $cell['needed'], - 'mode' => $mode, // need mode to not run purify for $mode=='ascii' - )); - } - else - { - $html .= html::div(html::purify(html::activate_links($value)),'style="overflow: auto; width='. $width. '; height='. $height. '"'); - } - break; - case 'checkbox': - $set_val = 1; $unset_val = 0; - if (!empty($cell_options)) - { - list($set_val,$unset_val,$ro_true,$ro_false) = self::csv_split($cell_options); - if (!$set_val && !$unset_val) $set_val = 1; - $value = $value == $set_val; - } - if (($multiple = substr($cell['name'],-2) == '[]') && !$readonly) - { - $readonly = $readonlys[substr($cell['name'],0,-1).$set_val.']']; - } - if ($readonly) - { - if (count(explode(',',$cell_options)) < 3) - { - $ro_true = 'x'; - $ro_false = ''; - } - if (!$value && $ro_false == 'disable') return ''; - - $html .= $value ? html::bold($ro_true) : $ro_false; - } - else - { - if ($value) $options .= ' checked="checked"'; - - if ($multiple) - { - // add the set_val to the id to make it unique - $options = str_replace('id="'.$form_name,'id="'.substr($form_name,0,-2)."[$set_val]",$options); - } - $html .= html::input($form_name,$set_val,'checkbox',$options); - - if ($multiple) $form_name = self::form_name($cname,substr($cell['name'],0,-2)); - - if (!self::$request->isset_to_process($form_name)) - { - self::$request->set_to_process($form_name,$cell['type'],array( - 'unset_value' => $unset_val, - 'multiple' => $multiple, - 'needed' => $cell['needed'], - )); - } - self::$request->set_to_process_attribute($form_name,'values',$set_val,true); - if (!$multiple) unset($set_val); // otherwise it will be added to the label - } - break; - case 'radio': // size: value if checked, readonly set, readonly unset - list($set_val,$ro_true,$ro_false) = self::csv_split($cell_options); - $set_val = $this->expand_name($set_val,$show_c,$show_row,$content['.c'],$content['.row'],$content); - - if ($value == $set_val) - { - $options .= ' checked="checked"'; - } - // add the set_val to the id to make it unique - $options = str_replace('id="'.$form_name,'id="'.$form_name."[$set_val]",$options); - - if ($readonly) - { - if (!$ro_true && !$ro_false) $ro_true = 'x'; - $html .= $value == $set_val ? html::bold($ro_true) : $ro_false; - } - else - { - $html .= html::input($form_name,$set_val,'RADIO',$options); - self::$request->set_to_process($form_name,$cell['type'],array( - 'needed' => $cell['needed'], - )); - } - break; - case 'button': - case 'buttononly': - case 'cancel': // cancel button - if ($name == 'cancel' || stripos($name,'[cancel]') !== false) $type = 'cancel'; - list($app) = explode('.',$this->name); - list($img,$ro_img) = explode(',',$cell_options); - if ($img[0] != '/' && strpos($img,'/') !== false && count($img_parts = explode('/',$img)) == 2) - { - list($app,$img) = $img_parts; // allow to specify app in image name (eg. img='addressbook/navbar') - } - $title = strlen($label) <= 1 || $cell['no_lang'] ? $label : lang($label); - if ($cell['onclick'] && - ($onclick = $this->expand_name($cell['onclick'],$show_c,$show_row,$content['.c'],$content['.row'],$content))) - { - $onclick = $this->js_pseudo_funcs($onclick,$cname); - } - unset($cell['onclick']); // otherwise the grid will handle it - if (($cell['onchange'] != '' || $img && !$readonly) && !$cell['needed']) // use a link instead of a button - { - $onclick = ($onclick ? preg_replace('/^return(.*);$/','if (\\1) ',$onclick) : ''). - (((string)$cell['onchange'] === '1' || $img) ? - 'return submitit('.self::$name_form.",'".$form_name."');" : $cell['onchange']).'; return false;'; - - if (!html::$netscape4 && substr($img,-1) == '%' && is_numeric($percent = substr($img,0,-1))) - { - $html .= html::progressbar($percent,$title,'onclick="'.$onclick.'" '.$options); - } - else - { - $html .= '' . - ($img ? html::image($app,$img,$title,'border="0"') : $title) . ''; - } - } - else - { - if (!empty($img)) - { - $options .= ' title="'.$title.'"'; - } - if ($cell['onchange'] && $cell['onchange'] != 1) - { - $onclick = ($onclick ? preg_replace('/^return(.*);$/','if (\\1) ',$onclick) : '').$cell['onchange']; - } - if ($type == 'cancel') $options .= ' novalidate="novalidate"'; // tell html5 form validation NOT to validate - $html .= !$readonly ? html::submit_button($form_name,$label,$onclick, - strlen($label) <= 1 || $cell['no_lang'],$options,$img,$app,$type == 'buttononly' ? 'button' : 'submit') : - html::image($app,$ro_img,'',$options); - } - $extra_label = False; - if (!$readonly && $type != 'buttononly') // input button, are never submitted back! - { - self::$request->set_to_process($form_name,$type); - } - break; - case 'hrule': - $html .= html::hr($cell_options); - break; - case 'grid': - if ($readonly && !$readonlys['__ALL__']) - { - if (!is_array($readonlys)) $readonlys = array(); - $set_readonlys_all = $readonlys['__ALL__'] = True; - } - if ($name != '') - { - $cname .= $cname == '' ? $name : '['.str_replace('[','][',str_replace(']','',$name)).']'; - } - $html .= $this->show_grid($cell,$name ? $value : $content,$readonlys+(array)$readonlys[$name],$cname,$show_c,$show_row,$path); - if ($set_readonlys_all) unset($readonlys['__ALL__']); - break; - case 'template': // size: index in content-array (if not full content is past further on) - if (is_object($cell['name'])) - { - $cell['obj'] = &$cell['name']; - unset($cell['name']); - $cell['name'] = 'was Object'; - echo "

Object in Name in tpl '$this->name': "; _debug_array($grid); - } - $obj_read = 'already loaded'; - if (is_array($cell['obj'])) - { - $obj = new etemplate_old(); - $obj->init($cell['obj']); - $cell['obj'] =& $obj; - unset($obj); - } - if (!is_object($cell['obj'])) - { - if ($cell['name'][0] == '@') - { - $cell['obj'] = $this->get_array($content,substr($cell['name'],1)); - $obj_read = is_object($cell['obj']) ? 'obj from content' : 'obj read, obj-name from content'; - if (!is_object($cell['obj'])) - { - $cell['obj'] = new etemplate_old($cell['obj'],$this->as_array()); - } - } - else - { $obj_read = 'obj read'; - $cell['obj'] = new etemplate_old($name,$this->as_array()); - } - } - if (is_int($this->debug) && $this->debug >= 3 || $this->debug == $cell['type']) - { - echo "

show_cell::template(tpl=$this->name,name=$cell[name]): $obj_read, readonly=$readonly

\n"; - } - if ($this->autorepeat_idx($cell,$show_c,$show_row,$idx,$idx_cname,false,$content) || $cell_options != '') - { - if ($span == '' && isset($content[$idx]['span'])) - { // this allows a colspan in autorepeated cells like the editor - list($span) = explode(',',$content[$idx]['span']); - if ($span == 'all') - { - $span = 1 + $content['cols'] - $show_c; - } - } - $readonlys = $this->get_array($readonlys,$idx); - $content = $this->get_array($content,$idx); - if ($idx_cname != '') - { - $cname .= $cname == '' ? $idx_cname : '['.str_replace('[','][',str_replace(']','',$idx_cname)).']'; - } - //echo "

show_cell-autorepeat($name,$show_c,$show_row,cname='$cname',idx='$idx',idx_cname='$idx_cname',span='$span'): content ="; _debug_array($content); - } - if ($readonly && !$readonlys['__ALL__']) - { - if (!is_array($readonlys)) $readonlys = array(); - $set_readonlys_all = $readonlys['__ALL__'] = True; - } - // propagate our onclick handler to embeded templates, if they dont have their own - if (!isset($cell['obj']->onclick_handler)) $cell['obj']->onclick_handler = $this->onclick_handler; - if ($cell['obj']->no_onclick) - { - $cell['obj']->onclick_proxy = $this->onclick_proxy ? $this->onclick_proxy : $this->name.':'.$this->version.':'.$path; - } - // propagate the CSS class to the template - if ($class) - { - $grid_size = array_pad(explode(',',$cell['obj']->size),4,''); - $grid_size[3] = ($grid_size[3] ? $grid_size[3].' ' : '') . $class; - $cell['obj']->size = implode(',',$grid_size); - } - $html = $cell['obj']->show($content,$this->sel_options,$readonlys,$cname,$show_c,$show_row); - - if ($set_readonlys_all) unset($readonlys['__ALL__']); - break; - case 'select': // size:[linesOnMultiselect|emptyLabel,extraStyleMulitselect, [,]{5} enhance] - $sels = array(); - list($multiple,$extraStyleMultiselect) = explode(',',$cell_options,2); - - // Allow widget to specify using enhanced select or not - $c_options = explode(',',$cell_options); - if(array_key_exists('enhance', $cell)) - { - $enhance = $cell['enhance']; - } - else if (count($c_options) >= 8) - { - // 8 or more optionsu - #7 is enhance flag - $enhance = ($c_options[7] == '1' || $c_options[7] == 'true'); - } - - if (!empty($multiple) && 0+$multiple <= 0) - { - $sels[''] = $multiple < 0 ? 'all' : $multiple; - // extra-option: no_lang=0 gets translated later and no_lang=1 gets translated too (now), only no_lang>1 gets not translated - if ((int)$cell['no_lang'] == 1) - { - $sels[''] = substr($sels[''],-3) == '...' ? lang(substr($sels[''],0,-3)).'...' : lang($sels['']); - } - $multiple = 0; - } - $sels += $this->_sel_options($cell,$name,$content); - if ($multiple && !is_array($value)) $value = explode(',',$value); - if ($readonly || $cell['noprint']) - { - foreach($multiple || is_array($value) ? $value : array($value) as $val) - { - if (is_array($sels[$val])) - { - $option_label = $sels[$val]['label']; - $option_title = $sels[$val]['title']; - } - else - { - $option_label = ($sels[$val]?$sels[$val]:$val); - $option_title = ''; - } - if (!$cell['no_lang']) $option_label = lang($option_label); - - if ($html) $html .= "
\n"; - - if ($option_title) - { - $html .= ''.html::htmlspecialchars($option_label).''; - } - else - { - $html .= html::htmlspecialchars($option_label); - } - } - } - if (!$readonly) - { - if ($cell['noprint']) - { - $html = ''.$html.''; - $options .= ' class="noPrint"'; - } - if ($multiple && is_numeric($multiple)) // eg. "3+" would give a regular multiselectbox - { - $html .= html::checkbox_multiselect($form_name.($multiple > 1 ? '[]' : ''),$value,$sels, - $cell['no_lang'],$options,$multiple,$multiple[0]!=='0', - $extraStyleMultiselect,$enhance); - } - else - { - $html .= html::select($form_name.($multiple > 1 ? '[]' : ''),$value,$sels, - $cell['no_lang'],$options,$multiple,$enhance); - } - if (!self::$request->isset_to_process($form_name)) - { - // fix for optgroup's - $options=array(); - foreach($sels as $key => $val) - { - # we want the key anyway, even if this allowes more values than wanted (the name/key of the optgroup if there is one, - # the keys of the arrays in case you have key/value pair(s) as value for the value of your option ). - $options[$key]=$key; - if (is_array($val)) - { - foreach(array_keys($val) as $key2) - { - $options[$key2]=$key2; - } - } - } - self::$request->set_to_process($form_name,$cell['type'],array( - 'needed' => $cell['needed'], - 'allowed' => array_keys($options), - 'multiple'=> $multiple, - )); - } - } - break; - case 'image': // size: [link],[link_target],[imagemap],[link_popup],[id] - case 'progress': - if (is_string($value) && $value !== '' && - ($is_progress = substr($value,-1) == '%' && is_numeric(substr($value,0,-1))) !== ($type == 'progress')) - { - error_log("Please use correct widget-type '".($is_progress?'progress':'image')."' in eTemplate '$this->name'!"); - } - $image = $value != '' ? $value : $name; - if (is_string($image)) list($app,$img) = explode('/',$image,2); - if (!$app || !$img || !is_dir(EGW_SERVER_ROOT.'/'.$app) || strpos($img,'/')!==false) - { - $img = $image; - list($app) = explode('.',$this->name); - } - if (!$readonly) - { - list($extra_link,$extra_link_target,$imagemap,$extra_link_popup,$id) = self::csv_split($cell['size']); - } - $html .= html::image($app,$img,strlen($label) > 1 && !$cell['no_lang'] ? lang($label) : $label, - 'border="0"'.($imagemap?' usemap="#'.html::htmlspecialchars($imagemap).'"':''). - ($id || $value ? self::get_id($name,$cell['name'],$id) : '')); - $extra_label = False; - break; - case 'file': // size: size of the filename field - if (!$readonly) - { - if ((int) $cell_options) $options .= ' size="'.(int)$cell_options.'"'; - if (substr($name,-2) == '[]') - { - self::$form_options .= ' enctype="multipart/form-data"'; - if (strpos($options,'onChange="') !== false) - { - $options = preg_replace('/onChange="([^"]+)"/i','onChange="\\1; if (!this.multiple) add_upload(this);"',$options); - } - else - { - $options .= ' onChange="if (!this.multiple) add_upload(this);"'; - } - $options .= ' multiple="multiple"'; // allow html5 browsers to select more then one file - } - else - { - $html .= html::input_hidden($path_name = str_replace($name,$name.'_path',$form_name),'.'); - self::$form_options = " enctype=\"multipart/form-data\" onsubmit=\"set_element2(this,'$path_name','$form_name')\""; - } - $html .= html::input($form_name,'','file',$options); - self::$request->set_to_process($form_name,$cell['type'],array( - 'needed' => $cell['needed'], - )); - } - break; - case 'split': - // Render this et2 widget as a box - $orient = $cell['orientation']; - case 'vbox': - case 'hbox': - case 'groupbox': - case 'box': // size: num,orient,cellpadding,cellspacing,keep - $rows = array(); - $box_row = 1; - $box_col = 'A'; - $box_anz = 0; - list($num,$orient,,,$keep_empty) = explode(',',$cell_options); - if (!$orient) $orient = $type == 'hbox' ? 'horizontal' : ($type == 'box' ? false : 'vertical'); - for ($n = 1; $n <= (int) $num; ++$n) - { - $child = $cell[$n]; // first param is a var_param now! - $h = $this->show_cell($child,$content,$readonlys,$cname,$show_c,$show_row,$nul,$cl,$path.'/'.$n); - if ($h != '' && $h != ' ' || $keep_empty) - { - if ($orient != 'horizontal') - { - $box_row = $n; - } - else - { - $box_col = $this->num2chrs($n); - } - if (!$orient) - { - $html .= $cl ? html::div($h," class=\"$cl\"") : $h; - } - else - { - $rows[$box_row][$box_col] = $html = $h; - } - $box_anz++; - if ($cell[$n]['align']) - { - $rows[$box_row]['.'.$box_col] = html::formatOptions($child['align'],'align'); - $sub_cell_has_align = true; - } - if (strlen($child['onclick']) > 1) - { - $rows[$box_row]['.'.$box_col] .= ' onclick="'.$this->js_pseudo_funcs($child['onclick'],$cname).'"'. - self::get_id('',$child['name'],$child['id']); - } - // allow to set further attributes in the tablecell, beside the class - if (is_array($cl)) - { - foreach($cl as $attr => $val) - { - if ($attr != 'class' && $val) - { - $rows[$box_row]['.'.$box_col] .= ' '.$attr.'="'.$val.'"'; - } - } - $cl = $cl['class']; - } - $box_item_class = $this->expand_name(isset(self::$class_conf[$cl]) ? self::$class_conf[$cl] : $cl, - $show_c,$show_row,$content['.c'],$content['.row'],$content); - $rows[$box_row]['.'.$box_col] .= html::formatOptions($box_item_class,'class'); - } - } - if ($box_anz > 1 && $orient) // a single cell is NOT placed into a table - { - $html = html::table($rows,html::formatOptions($cell_options,',,cellpadding,cellspacing'). - ($type != 'groupbox' ? html::formatOptions($class,'class'). - ($cell['name'] ? self::get_id($form_name,$cell['name'],$cell['id']) : '') : ''). - ($cell['align'] && $orient != 'horizontal' || $sub_cell_has_align ? ' width="100%"' : '')); // alignment only works if table has full width - if ($type != 'groupbox') $class = ''; // otherwise we create an extra div - } - // put the class of the box-cell, into the the class of this cell - elseif ($box_item_class && $box_anz == 1) - { - $class = ($class ? $class . ' ' : '') . $box_item_class; - // if we have onclick or tooltip, add it to an extra div around single cell - if (!empty($rows[$box_row]['.'.$box_col])) $html = html::div($html, $rows[$box_row]['.'.$box_col]); - } - if ($type == 'groupbox') - { - if (strlen($label) > 1 && $cell['label'] == $label) - { - $label = lang($label); - } - $html = html::fieldset($html,$label,self::get_id($form_name,$cell['name'],$cell['id']). - ($class ? ' class="'.$class.'"' : '')); - $class = ''; // otherwise we create an extra div - } - elseif (!$orient) - { - if (strpos($html,'class="'.$class)) $class = ''; // dont add class a 2. time - $html = html::div($html,html::formatOptions(array( - $cell['height'], - $cell['width'], - $class, - ),'height,width,class').self::get_id($form_name,$cell['name'],$cell['id'])). ($html ? '' : ''); - $class = ''; // otherwise we create an extra div - } - if ($box_anz > 1) // small docu in the html-source - { - $html = "\n\n\n\n".$html."\n\n\n\n"; - } - // we need noPrint on td - if (strpos($cell['span'], 'noPrint')) $class .= ' noPrint'; - $extra_label = False; - break; - case 'deck': - for ($n = 1; $n <= $cell_options && (empty($value) || $value != $cell[$n]['name']); ++$n) ; - if ($n > $cell_options) - { - $value = $cell[1]['name']; - } - if ($s_width = $cell['width']) - { - $s_width = "width: $s_width".(substr($s_width,-1) != '%' ? 'px' : '').';'; - } - if ($s_height = $cell['height']) - { - $s_height = "height: $s_height".(substr($s_height,-1) != '%' ? 'px' : '').';'; - } - $html = html::input_hidden($form_name,$value); - self::$request->set_to_process($form_name,$cell['type']); - - for ($n = 1; $n <= $cell_options; ++$n) - { - $child = $cell[$n]; // first param is a var_param now! - $html .= html::div($this->show_cell($child,$content,$readonlys,$cname,$show_c, - $show_row,$nul,$cl,$path.'/'.$n),html::formatOptions(array( - 'display: '.($value == $child['name'] ? 'inline' : 'none').';', - $child['name'] - ),'style,id')); - } - break; - case 'colorpicker': - if ($readonly) - { - $html = $value; - } - else - { - $html = html::inputColor($form_name,$value,$cell['help']); - - self::$request->set_to_process($form_name,$cell['type'],array( - 'maxlength' => 7, - 'needed' => $cell['needed'], - 'preg' => '/^(#[0-9a-f]{6}|)$/i', - )); - } - break; - default: - if ($ext_type && $this->haveExtension($ext_type,'render')) - { - $html .= $this->extensionRender($ext_type,$form_name,$value,$cell,$readonly); - } - else - { - $html .= "unknown type '$cell[type]'"; - } - break; - } - // extension-processing need to be after all other and only with diff. name - if ($ext_type && !$readonly && $this->haveExtension($ext_type,'post_process')) - { // unset it first, if it is already set, to be after the other widgets of the ext. - $to_process = self::$request->get_to_process($form_name); - self::$request->unset_to_process($form_name); - self::$request->set_to_process($form_name,'ext-'.$ext_type,$to_process); - } - // restoring value, as it is a reference into content - // some widgets change it and rely on it being a reference - // using same name for multiple widgets breaks, if we dont restore it now - $value = $old_value; - // save blur-value to strip it in process_exec - if (!empty($blur) && self::$request->isset_to_process($form_name)) - { - self::$request->set_to_process_attribute($form_name,'blur',$blur); - } - if ($extra_label && ($label != '' || $html == '')) - { - if (strlen($label) > 1 && !($cell['no_lang'] && $cell['label'] != $label || (int)$cell['no_lang'] == 2)) - { - $label = lang($label); - } - $accesskey = false; - if (($accesskey = $label && strpos($label,'&')!==false) && $accesskey[1] != ' ' && $form_name != '' && - (($pos = strpos($accesskey,';')) === false || $pos > 5)) - { - $label = str_replace('&'.$accesskey[1],''.$accesskey[1].'',$label); - $accesskey = $accesskey[1]; - } - if ($label && !$readonly && ($accesskey || $label_for || $type != 'label' && $cell['name'])) - { - if ($label_for) // if label_for starts with a '#', it is already an id - no need to create default id from it - { - $label_for = $label_for[0] == '#' ? substr($label_for,1) : self::form_name($cname,$label_for); - } - else - { - $label_for = $form_name.($set_val?"[$set_val]":''); - } - $label = html::label($label,$label_for,$accesskey); - } - if ($type == 'radio' || $type == 'checkbox' || $label && strpos($label,'%s')!==false) // default for radio is label after the button - { - $html = strpos($label,'%s')!==false ? str_replace('%s',$html,$label) : $html.' '.$label; - } - elseif (($html = $label . ' ' . $html) == ' ') - { - $html = ' '; - } - } - if ($extra_link && (($extra_link = $this->expand_name($extra_link,$show_c,$show_row,$content['.c'],$content['.row'],$content)))) - { - $options = $help ? ' onmouseover="self.status=\''.addslashes(html::htmlspecialchars($help)).'\'; return true;"' . - ' onmouseout="self.status=\'\'; return true;"' : ''; - - if ($extra_link_target && (($extra_link_target = $this->expand_name($extra_link_target,$show_c,$show_row,$content['.c'],$content['.row'],$content)))) - { - $options .= ' target="'.addslashes($extra_link_target).'"'; - } - if ($extra_link_popup && (($extra_link_popup = $this->expand_name($extra_link_popup,$show_c,$show_row,$content['.c'],$content['.row'],$content)))) - { - list($w,$h) = explode('x',$extra_link_popup); - $options .= ' onclick="window.open(this,this.target,\'width='.(int)$w.',height='.(int)$h.',location=no,menubar=no,toolbar=no,scrollbars=yes,status=yes\'); return false;"'; - } - if ($extra_link_title) - { - $options .= ' title="'.addslashes($extra_link_title).'"'; - } - return html::a_href($html,$extra_link,'',$options); - } - // if necessary show validation-error behind field - if (isset(self::$validation_errors[$form_name])) - { - $html .= ' '.self::$validation_errors[$form_name].''; - } - // generate an extra div, if we have an onclick handler and NO children or it's an extension - //echo "

$this->name($this->onclick_handler:$this->no_onclick:$this->onclick_proxy): $cell[type]/$cell[name]

\n"; - if ($this->onclick_handler && !isset(self::$widgets_with_children[$cell['type']])) - { - $handler = str_replace('%p',$this->no_onclick ? $this->onclick_proxy : $this->name.':'.$this->version.':'.$path, - $this->onclick_handler); - if ($type == 'button' || $type == 'buttononly' || !$label) // add something to click on - { - $html = (substr($html,-1) == "\n" ? substr($html,0,-1) : $html).' '; - } - return html::div($html,' ondblclick="'.$handler.'"','clickWidgetToEdit'); - } - return $html; - } - - /** - * Return id="..." attribute, using the following order to determine the id: - * - $id if not empty - * - $name if starting with a hash (#), without the hash of cause - * - $form_name otherwise - * - * This is necessary to not break backward compatibility: if you want to specify - * a certain id, you can use now "#something" as name to get id="something", - * otherwise the $form_name "exec[something]" is used. - * (If no id is directly supplied internally.) - * - * @param string $form_name - * @param string $name=null - * @param string $id=null - * @return string ' id="..."' or '' if no id found - */ - static public function get_id($form_name,$name=null,$id=null) - { - if (empty($id)) - { - if ($name[0] == '#') - { - $id = substr($name,1); - } - else - { - $id = $form_name; - } - } - return !empty($id) ? ' id="'.str_replace('"','"',$id).'"' : ''; - } - - /** - * Format a number according to user prefs with decimal and thousands separator (later only for readonly) - * - * HTML5 input type=number requires a float value with a dot, not comma! - * Chrome 22 and Safari 6 shows no value if a comma is used, - * while FF 16, IE 9 and 10 have no support for input type=number :-( - * --> use . as decimal separator for browser supporting html5 input type=number - * - * @param int|float|string $number - * @param int $num_decimal_places=2 - * @param boolean $readonly=true - * @return string - */ - static public function number_format($number,$num_decimal_places=2,$readonly=true) - { - static $dec_separator,$thousands_separator; - if (is_null($dec_separator)) - { - $dec_separator = $GLOBALS['egw_info']['user']['preferences']['common']['number_format'][0]; - if (empty($dec_separator)) $dec_separator = '.'; - $thousands_separator = $GLOBALS['egw_info']['user']['preferences']['common']['number_format'][1]; - } - if ((string)$number === '') return ''; - - $ret = number_format(str_replace(' ','',$number), $num_decimal_places, - // need to use '.' as decimal separator for all browser supporting html5 input type=number - $dec_sep_used=$readonly || !in_array(html::$user_agent, array('chrome', 'safari', 'opera')) ? - $dec_separator : '.', - $readonly ? $thousands_separator : ''); - //error_log(__METHOD__."($number, $num_decimal_places, $readonly) html::user_agent=".html::$user_agent.", dec_sep='$dec_separator' --> '$dec_sep_used', thousands_sep='$thousands_separator' returning '$ret'"); - return $ret; - } - - /** - * Retrive options for selectboxes and similar widgets (eg. the tree) - * - * @param array $cell - * @param string $name - * @param array $content=array(); - * @return array - */ - function _sel_options($cell,$name,$content=array()) - { - $sels = array(); - - if (!empty($cell['sel_options'])) - { - if (!is_array($cell['sel_options'])) - { - $opts = explode(',',$cell['sel_options']); - while (list(,$opt) = each($opts)) - { - list($k,$v) = explode('=',$opt); - $sels[$k] = $v; - } - } - else - { - $sels += $cell['sel_options']; - } - } - $explode_needed = true; - if (($options = $this->sel_options[$name]) && is_array($options) || - ($options = self::get_array($this->sel_options, $name)) && is_array($options)) - { - if(count($options) == 2 && $options['label'] && $options['title']) - { - // In too far - need to back up - } - else - { - $sels += $options; - $explode_needed = false; - } - } - if($explode_needed) - { - $name_parts = explode('[',str_replace(']','',$name)); - if (count($name_parts)) - { - $org_name = $name_parts[count($name_parts)-1]; - if (isset($this->sel_options[$org_name]) && is_array($this->sel_options[$org_name])) - { - $sels += $this->sel_options[$org_name]; - } - elseif (isset($this->sel_options[$name_parts[0]]) && is_array($this->sel_options[$name_parts[0]])) - { - $sels += $this->sel_options[$name_parts[0]]; - } - } - } - if (isset($content["options-$name"])) - { - $sels += $content["options-$name"]; - } - //error_log(__METHOD__."(, '$name') returning ".array2string($sels)); - return $sels; - } - - /** - * Resolve javascript pseudo functions in onclick or onchange: - * - egw::link('$l','$p') calls $egw->link($l,$p) - * - form::name('name') returns expanded name/id taking into account the name at that point of the template hierarchy - * - egw::lang('Message ...') translate the message - * - confirm('message') translates 'message' and adds a '?' if not present - * - window.open() replaces it with egw_openWindowCentered2() - * - xajax_doXMLHTTP('etemplate. replace ajax calls in widgets with special handler not requiring etemplate run rights - * - * @param string $on onclick, onchange, ... action - * @param string $cname name-prefix / name-space - * @return string - */ - function js_pseudo_funcs($on,$cname) - { - if (strpos($on,'::') !== false) // avoid the expensive regular expresions, for performance reasons - { - if (preg_match_all("/egw::link\\('([^']+)','(.+?)'(?:,'(.+?)')?\\)/",$on,$matches)) // the ? alters the expression to shortest match - { - foreach(array_keys($matches[1]) as $n) // this way we can correctly parse ' in the 2. argument - { - $url = $GLOBALS['egw']->link($matches[1][$n],$matches[2][$n],$matches[3][$n]); - $on = str_replace($matches[0][$n],'\''.addslashes($url).'\'',$on); - } - } - - if (preg_match_all("/form::name\\('([^']+)'\\)/",$on,$matches)) - { - foreach($matches[1] as $n => $matche_name) - { - $matches[1][$n] = '\''.self::form_name($cname,$matche_name).'\''; - } - $on = str_replace($matches[0],$matches[1],$on); - } - // we need to search ungready (shortest possible match), to avoid catching to much - if (preg_match_all('/egw::lang\(["\']{1}(.*)["\']{1}\)/U',$on,$matches)) { - foreach($matches[1] as $n => $string) { - $str = lang($string); - $on = str_replace($matches[0][$n],'\''.addslashes($str).'\'',$on); - } - } - - // inserts the styles of a named template - if (preg_match('/template::styles\(["\']{1}(.*)["\']{1}\)/U',$on,$matches)) - { - $tpl = $matches[1] == $this->name ? $this : new etemplate_old($matches[1]); - $on = str_replace($matches[0],"''",$on); - } - } - - // translate messages in confirm() - if (strpos($on,'confirm(') !== false && preg_match('/confirm\(["\']{1}(.*)["\']{1}\)/U',$on,$matches)) - { - $question = lang($matches[1]).(substr($matches[1],-1) != '?' ? '?' : ''); // add ? if not there, saves extra phrase - $on = str_replace($matches[0],'confirm(\''.str_replace("'","\\'",$question).'\')',$on); - } - - // replace window.open() with EGw's egw_openWindowCentered2() - if (strpos($on,'window.open(') !== false && preg_match("/window.open\('(.*)','(.*)','dependent=yes,width=([^,]*),height=([^,]*),scrollbars=yes,status=(.*)'\)/",$on,$matches)) - { - $on = str_replace($matches[0], "egw_openWindowCentered2('$matches[1]', '$matches[2]', $matches[3], $matches[4], '$matches[5]')", $on); - } - - // replace xajax calls to code in widgets, with the "etemplate" handler, - // this allows to call widgets with the current app, otherwise everyone would need etemplate run rights - if (strpos($on,"xajax_doXMLHTTP('etemplate.") !== false) - { - $on = preg_replace("/^xajax_doXMLHTTP\('etemplate\.([a-z]+_widget\.[a-zA-Z0-9_]+)\'/",'xajax_doXMLHTTP(\''.$GLOBALS['egw_info']['flags']['currentapp'].'.\\1.etemplate\'',$on); - } - - return $on; - } - - /** - * applies stripslashes recursivly on each element of an array - * - * @param array &$var - * @return array - */ - static function array_stripslashes($var) - { - if (!is_array($var)) - { - return stripslashes($var); - } - foreach($var as $key => $val) - { - $var[$key] = is_array($val) ? self::array_stripslashes($val) : stripslashes($val); - } - return $var; - } - - /** - * makes necessary adjustments on $_POST after a eTemplate / form gots submitted - * - * This is only an internal function, dont call it direct use only exec - * Process_show uses a list of input-fields/widgets generated by show. - * - * @internal - * @param array $content $_POST[$cname], on return the adjusted content - * @param array $to_process list of widgets/form-fields to process - * @param string $cname='' basename of our returnt content (same as in call to show) - * @param string $_type='regular' type of request - * @return array with validation errors - */ - function process_show(&$content,$to_process,$cname='',$_type='regular') - { - if (!isset($content) || !is_array($content) || !is_array($to_process)) - { - return; - } - if (is_int($this->debug) && $this->debug >= 1 || $this->debug == $this->name && $this->name) - { - echo "

process_show($this->name) cname='$cname' start: content ="; _debug_array($content); - } - $content_in = $cname ? array($cname => $content) : $content; - $content = array(); - if (get_magic_quotes_gpc()) - { - $content_in = self::array_stripslashes($content_in); - } - self::$validation_errors = array(); - $this->canceled = $this->button_pressed = False; - - foreach($to_process as $form_name => $type) - { - if (is_array($type)) - { - $attr = $type; - $type = $attr['type']; - } - else - { - $attr = array(); - } - $form_name = str_replace(array('[',']'), array('[',']'), $form_name); - $value = self::get_array($content_in,$form_name,True,$GLOBALS['egw_info']['flags']['currentapp'] == 'etemplate' ? false : true ); - // The comment below does only aplay to normal posts, not for xajax. Files are not supported anyway by xajax atm. - // not checked checboxes are not returned in HTML and file is in $_FILES and not in $content_in - if($value === false && $_type == 'xajaxResponse' /*!in_array($type,array('checkbox','file'))*/) continue; - - if (isset($attr['blur']) && $attr['blur'] == $value) - { - $value = ''; // blur-values is equal to emtpy - } - //echo "

process_show($this->name) loop was ".self::$loop.", $type: $form_name = ".array2string($value)."

\n"; - if (is_string($type)) list($type,$sub) = explode('-',$type); - switch ($type) - { - case 'ext': - $_cont = &self::get_array($content,$form_name,True); - if (!$this->extensionPostProcess($sub,$form_name,$_cont,$value)) - { - //echo "\n

unsetting content[$form_name] !!!

\n"; - $this->unset_array($content,$form_name); - } - // this else should NOT be unnecessary as $_cont is a reference to the index - // $form_name of $content, but under some circumstances a set/changed $_cont - // does not result in a change in $content -- RalfBecker 2004/09/18 - // seems to depend on the number of (not existing) dimensions of the array -- -- RalfBecker 2005/04/06 - elseif (!self::isset_array($content,$form_name)) - { - //echo "

setting content[$form_name]='$_cont' because is was unset !!!

\n"; - self::set_array($content,$form_name,$_cont); - } - if ($_cont === '' && $attr['needed'] && !$attr['blur']) - { - self::set_validation_error($form_name,lang('Field must not be empty !!!'),''); - } - break; - case 'htmlarea': - if ($attr['mode'] !== 'ascii') - { - self::set_array($content,$form_name,html::purify($value)); - break; - } - // fall-throught for mode 'ascii', which is identical to textarea - case 'int': - case 'integer': - case 'float': - case 'passwd': - case 'text': - case 'textbox': - case 'hidden': - case 'textarea': - case 'colorpicker': - if ((string)$value === '' && $attr['needed'] && !$attr['blur']) - { - self::set_validation_error($form_name,lang('Field must not be empty !!!'),''); - } - if ((int) $attr['maxlength'] > 0 && mb_strlen($value) > (int) $attr['maxlength']) - { - $value = mb_substr($value,0,(int) $attr['maxlength']); - } - if ($attr['preg'] && !preg_match($attr['preg'],$value)) - { - switch($type) - { - case 'int': - case 'integer': - self::set_validation_error($form_name,lang("'%1' is not a valid integer !!!",$value),''); - break; - case 'float': - self::set_validation_error($form_name,lang("'%1' is not a valid floatingpoint number !!!",$value),''); - break; - default: - self::set_validation_error($form_name,lang("'%1' has an invalid format !!!",$value)/*." !preg_match('$attr[preg]', '$value')"*/,''); - break; - } - } - elseif (in_array($type, array('int', 'integer', 'float'))) // cast int and float and check range - { - if ((string)$value !== '' || $attr['needed']) // empty values are Ok if needed is not set - { - $value = $type != 'float' ? (int) $value : (float) str_replace(',','.',$value); // allow for german (and maybe other) format - - if (!empty($attr['min']) && $value < $attr['min']) - { - self::set_validation_error($form_name,lang("Value has to be at least '%1' !!!",$attr['min']),''); - $value = $type != 'float' ? (int) $attr['min'] : (float) $attr['min']; - } - if (!empty($attr['max']) && $value > $attr['max']) - { - self::set_validation_error($form_name,lang("Value has to be at maximum '%1' !!!",$attr['max']),''); - $value = $type != 'float' ? (int) $attr['max'] : (float) $attr['max']; - } - } - } - self::set_array($content,$form_name,$value); - break; - case 'cancel': // cancel button ==> dont care for validation errors - if ($value) - { - $this->canceled = True; - self::set_array($content,$form_name,$value); - } - break; - case 'button': - if ($value) - { - $this->button_pressed = True; - self::set_array($content,$form_name,$value); - } - break; - case 'select': - if ($attr['allowed']) // only check for $value is allowed, if allowed values are set - { - foreach(is_array($value) ? $value : array($value) as $val) - { - if (!($attr['multiple'] && !$val) && !in_array($val,$attr['allowed'])) - { - self::set_validation_error($form_name,lang("'%1' is NOT allowed ('%2')!",$val,implode("','",$attr['allowed'])),''); - $value = ''; - break; - } - } - } - if (is_array($value)) $value = implode(',',$value); - if ($value === '' && $attr['needed']) - { - self::set_validation_error($form_name,lang('Field must not be empty !!!',$value),''); - } - self::set_array($content,$form_name,$value); - break; - case 'checkbox': - if (!$value && $attr['needed']) - { - self::set_validation_error($form_name,lang('Field must not be empty !!!',$value),''); - } - if ($value === false) - { - self::set_array($content,$form_name,$attr['multiple'] ? array() : $attr['unset_value']); // need to be reported too - } - else - { - $value = array_intersect(is_array($value) ? $value : array($value),$attr['values']); // return only allowed values - self::set_array($content,$form_name,$attr['multiple'] ? $value : $value[0]); - } - break; - case 'file': - if (($multiple = substr($form_name,-2) == '[]')) - { - $form_name = substr($form_name,0,-2); - } - $parts = explode('[',str_replace(']','',$form_name)); - $name = array_shift($parts); - $index = count($parts) ? '['.implode('][',$parts).']' : ''; - $value = array(); - for($i=0; $i < 100; ++$i) - { - $file = array(); - foreach(array('tmp_name','type','size','name','error') as $part) - { - if (!is_array($_FILES[$name])) break 2; // happens eg. in forms set via xajax, which do not upload files - $file[$part] = $this->get_array($_FILES[$name],$part.$index.($multiple ? "[$i]" : '')); - } - if (!$multiple) $file['path'] = $this->get_array($content_in,substr($form_name,0,-1).'_path]'); - $file['ip'] = $_SERVER['REMOTE_ADDR']; - // check if we have an upload error - if ($file['error'] && $file['name'] !== '' && !$file['size']) // ignore empty upload boxes - { - self::set_validation_error($form_name.($multiple?'[]':''), - lang('Error uploading file!')."\n".self::max_upload_size_message(),''); - } - if ((string)$file['name'] === '' || $file['tmp_name'] && function_exists('is_uploaded_file') && !is_uploaded_file($file['tmp_name'])) - { - if ($multiple && ($file['name'] === '' || $file['error'])) - { - continue; // ignore empty upload box - } - break; - } - if (!$multiple) - { - $value = $file; - break; - } - $value[] = $file; - } - //echo $form_name; _debug_array($value); - // fall-throught - default: - if ($attr['needed'] && !$value) - { - self::set_validation_error($form_name,lang('Field must not be empty !!!',$value),''); - } - self::set_array($content,$form_name,$value); - break; - } - } - if ($cname) - { - $content = $content[$cname]; - } - if (is_int($this->debug) && $this->debug >= 2 || $this->debug == $this->name && $this->name) - { - echo "

process_show($this->name) end: content ="; _debug_array($content); - if (count(self::$validation_errors)) - { - echo "

validation_errors = "; _debug_array(self::$validation_errors); - } - } - return self::$validation_errors; - } - - /** - * Sets a validation error, to be displayed in the next exec - * - * @param string $name (complete) name of the widget causing the error - * @param string|boolean $error error-message already translated or false to reset all existing error for given name - * @param string $cname=null set it to '', if the name is already a form-name, defaults to self::$name_vars - */ - static function set_validation_error($name,$error,$cname=null) - { - if (is_null($cname)) $cname = self::$name_vars; - //echo "

self::set_validation_error('$name','$error','$cname');

\n"; - if ($cname) $name = self::form_name($cname,$name); - - if ($error === false) - { - unset(self::$validation_errors[$name]); - } - else - { - if (self::$validation_errors[$name]) - { - self::$validation_errors[$name] .= ', '; - } - self::$validation_errors[$name] .= $error; - } - } -} diff --git a/phpgwapi/inc/common_functions.inc.php b/phpgwapi/inc/common_functions.inc.php index 4e49a3ffd7..592b1c9692 100755 --- a/phpgwapi/inc/common_functions.inc.php +++ b/phpgwapi/inc/common_functions.inc.php @@ -1347,6 +1347,7 @@ foreach(array('_GET','_POST','_REQUEST','HTTP_GET_VARS','HTTP_POST_VARS') as $n if (isset($GLOBALS['egw_info']) && isset($GLOBALS['egw_info']['flags']) && isset($GLOBALS['egw_info']['flags']['currentapp']) && !preg_match('/^[A-Za-z0-9_-]+$/',$GLOBALS['egw_info']['flags']['currentapp'])) { + error_log(__FILE__.': '.__LINE__.' Invalid $GLOBALS[egw_info][flags][currentapp]='.array2string($GLOBALS['egw_info']['flags']['currentapp']).', $_SERVER[REQUEST_URI]='.array2string($_SERVER[REQUEST_URI])); die('Invalid $GLOBALS[egw_info][flags][currentapp]!'); } @@ -1433,8 +1434,6 @@ function __autoload($class) // fixing warnings generated by php 5.3.8 is_a($obj) trying to autoload huge strings if (strlen($class) > 64 || strpos($class, '.') !== false) return; - if ($class == 'etemplate_new') $class = 'etemplate'; - $components = explode('_',$class); $app = array_shift($components); // classes using the new naming schema app_class_name, eg. admin_cmd