* @author Cornelius Weiss * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage extensions * @version $Id$ */ /** * This widget generates a template for customfields based on definitions in egw_config table * * All widgets here have 2+ comma-separated options ($cell[size]): * - sub-type to display only the cf's without subtype or with a matching one * - use-private to display only (non-)private cf's (0=regular ones, 1=private ones, default both) * - field-name to display only the named custom field(s). Use ! before to display all but given field(s). * Additional fields can be added with a comma between them * * Private cf's the user has no right to see (neither him nor his memberships are mentioned) are never displayed. */ class customfields_widget { var $public_functions = array( 'pre_process' => True, ); var $human_name = array( 'customfields' => 'custom fields', 'customfields-types' => 'custom field types', 'customfields-list' => 'custom field list', 'customfields-no-label' => 'custom fields without label', ); /** * Allowd types of customfields * * The additionally allowed app-names from the link-class, will be add by the edit-method only, * as the link-class has to be called, which can NOT be instanciated by the constructor, as * we get a loop in the instanciation. * * @var array */ var $cf_types = array( 'text' => 'Text', 'float' => 'Float', 'label' => 'Label', 'select' => 'Selectbox', 'ajax_select' => 'Search', 'radio' => 'Radiobutton', 'checkbox' => 'Checkbox', 'date' => 'Date', 'date-time'=> 'Date+Time', 'select-account' => 'Select account', 'button' => 'Button', // button to execute javascript 'url' => 'Url', 'url-email'=> 'EMail', 'url-phone'=> 'Phone number', 'link-entry' => 'Select entry', // should be last type, as the individual apps get added behind ); /** * @var $prefix string Prefix for every custiomfield name returned in $content (# for general (admin) customfields) */ var $prefix = '#'; /** * Current application * * @var string */ var $appname; /** * Instance of the config class for $appname * * @var config */ var $config; /** * Our customfields as name => data array * * @var array */ var $customfields; var $types; var $advanced_search; function __construct($ui,$appname=null) { $this->appname = $appname ? $appname : $GLOBALS['egw_info']['flags']['currentapp']; $this->customfields = config::get_customfields($this->appname); $this->types = config::get_content_types($this->appname); $this->advanced_search = $GLOBALS['egw_info']['etemplate']['advanced_search']; } /** * pre-processing of the extension * * This function is called before the extension gets rendered * * @param string $form_name form-name of the control * @param mixed &$value value / existing content, can be modified * @param array &$cell array with the widget, can be modified for ui-independent widgets * @param array &$readonlys names of widgets as key, to be made readonly * @param mixed &$extension_data data the extension can store persisten between pre- and post-process * @param etemplate &$tmpl reference to the template we belong too * @return boolean true if extra label is allowed, false otherwise */ function pre_process($form_name,&$value,&$cell,&$readonlys,&$extension_data,etemplate $tmpl) { list($app) = explode('.',$tmpl->name); // if we are in the etemplate editor or the app has no cf's, load the cf's from the app the tpl belongs too if ($app && $app != 'stylite' && $app != $this->appname && ($this->appname == 'etemplate' || !$this->customfields)) { self::__construct(null,$app); // app changed } list($type2,$use_private,$field_names) = explode(',',$cell['size'],3); $fields_with_vals=array(); // Filter fields if($field_names) { if($field_names[0] == '!') { $negate_field_filter = true; $field_names = substr($field_names,1); } $field_filter = explode(',', $field_names); } $fields = $this->customfields; foreach((array)$fields as $key => $field) { // remove private or non-private cf's, if only one kind should be displayed if ((string)$use_private !== '' && (boolean)$field['private'] != (boolean)$use_private) { unset($fields[$key]); } // Remove filtered fields if($field_filter && (!$negate_field_filter && !in_array($key, $field_filter) || $negate_field_filter && in_array($key, $field_filter))) { unset($fields[$key]); } } // check if name refers to a single custom field --> show only that if (($pos=strpos($cell['name'],$this->prefix)) !== false && // allow the prefixed name to be an array index too preg_match("/$this->prefix([^\]]+)/",$cell['name'],$matches) && isset($fields[$name=$matches[1]])) { $fields = array($name => $fields[$name]); $value = array($this->prefix.$name => $value); $singlefield = true; $form_name = substr($form_name,0,-strlen("[$this->prefix$name]")); } switch($type = $cell['type']) { case 'customfields-types': $cell['type'] = 'select'; foreach($this->cf_types as $lname => $label) { $cell['sel_options'][$lname] = lang($label); $fields_with_vals[]=$lname; } $link_types = egw_link::app_list(); ksort($link_types); foreach($link_types as $lname => $label) $cell['sel_options'][$lname] = '- '.$label; $cell['no_lang'] = true; return true; case 'customfields-list': foreach(array_reverse($fields) as $lname => $field) { if (!empty($type2) && !empty($field['type2']) && strpos(','.$field['type2'].',',','.$type2.',') === false) continue; // not for our content type// if (isset($value[$this->prefix.$lname]) && $value[$this->prefix.$lname] !== '') //break; { $fields_with_vals[]=$lname; } //$stop_at_field = $name; } break; default: foreach(array_reverse($fields) as $lname => $field) { $fields_with_vals[]=$lname; } } $readonly = $cell['readonly'] || $readonlys[$name] || $type == 'customfields-list'; if(!is_array($fields)) { $cell['type'] = 'label'; return True; } // making the cell an empty grid $cell['type'] = 'grid'; $cell['data'] = array(array()); $cell['rows'] = $cell['cols'] = 0; $n = 1; foreach($fields as $lname => $field) { if (array_search($lname,$fields_with_vals) !== false) { if ($stop_at_field && $lname == $stop_at_field) break; // no further row necessary // check if the customfield get's displayed for type $value, we can have multiple comma-separated types now if (!empty($type2) && !empty($field['type2']) && strpos(','.$field['type2'].',',','.$type2.',') === false) { continue; // not for our content type } $new_row = null; etemplate::add_child($cell,$new_row); if ($type != 'customfields-list' && $type == 'customfields') { $row_class = 'row'; etemplate::add_child($cell,$label =& etemplate::empty_cell('label','',array( 'label' => $field['label'], 'no_lang' => substr(lang($field['label']),-1) == '*' ? 2 : 0, 'span' => $field['type'] === 'label' ? '2' : '', ))); } elseif ($type == 'customfields-list') { if (isset($value[$this->prefix.$lname]) && $value[$this->prefix.$lname] !== '') { switch ((string)$field['type']) { case 'checkbox': if ($value[$this->prefix.$lname]==0) break; default: etemplate::add_child($cell,$input =& etemplate::empty_cell('image','info.png', array('label'=> $field['label'],'width'=>"16px"))); } } } switch ((string)$field['type']) { case 'select' : if (count($field['values']) == 1 && isset($field['values']['@'])) { $field['values'] = $this->_get_options_from_file($field['values']['@']); } if($this->advanced_search && $field['rows'] <= 1) $field['values'][''] = lang('doesn\'t matter'); foreach($field['values'] as $key => $val) { if (substr($val = lang($val),-1) != '*') { $field['values'][$key] = $val; } } $input =& etemplate::empty_cell('select',$this->prefix.$lname,array( 'sel_options' => $field['values'], 'size' => $field['rows'], 'no_lang' => True, )); if($this->advanced_search) { $select =& $input; unset($input); $input =& etemplate::empty_cell('hbox'); etemplate::add_child($input, $select); unset($select); /* the following seem to double the select fields in advanced search. etemplate::add_child($input, etemplate::empty_cell('select',$this->prefix.$lname,array( 'sel_options' => $field['values'], 'size' => $field['rows'], 'no_lang' => True ))); */ } break; case 'ajax_select' : // Set some reasonable defaults for the widget $options = array( 'get_title' => 'etemplate.ajax_select_widget.array_title', 'get_rows' => 'etemplate.ajax_select_widget.array_rows', 'id_field' => ajax_select_widget::ARRAY_KEY, ); if($field['rows']) { $options['num_rows'] = $field['rows']; } // If you specify an option known to the AJAX Select widget, it will be pulled from the list of values // and used as such. All unknown values will be used for selection, not passed through to the query if (isset($field['values']['@'])) { $options['values'] = $this->_get_options_from_file($field['values']['@']); unset($field['values']['@']); } else { $options['values'] = array_diff_key($field['values'], array_flip(ajax_select_widget::$known_options)); } $options = array_merge($options, array_intersect_key($field['values'], array_flip(ajax_select_widget::$known_options))); $input =& etemplate::empty_cell('ajax_select', $this->prefix.$lname, array( 'readonly' => $readonly, 'no_lang' => True, 'size' => $options )); break; case 'label' : $row_class = 'th'; break; case 'radio' : $showthis = '#a#l#l#'; if (count($field['values']) == 1 && isset($field['values']['@'])) { $field['values'] = $this->_get_options_from_file($field['values']['@']); } if($this->advanced_search && $field['rows'] <= 1) $field['values'][''] = lang('doesn\'t matter'); if ($readonly) { $showthis = $value[$this->prefix.$lname]; $input =& etemplate::empty_cell('hbox'); } else { $input =& etemplate::empty_cell('groupbox'); } $m = 0; foreach ($field['values'] as $key => $val) { $radio = etemplate::empty_cell('radio',$this->prefix.$lname); $radio['label'] = $val; $radio['size'] = $key; if ($showthis == '#a#l#l#' || $showthis == $key) etemplate::add_child($input,$radio); unset($radio); } break; case 'float': $known_options = array('min'=>null, 'max'=>null,'size' => $field['len'], 'precision' => null); $options = array_merge($known_options, $field['values']); $input =& etemplate::empty_cell($field['type'],$this->prefix.$lname,array( 'size' => implode(',',$options) )); break; case 'text' : case 'textarea' : case '' : // not set $field['len'] = $field['len'] ? $field['len'] : 20; if ($type != 'customfields-list') { if($field['rows'] <= 1) {//text list($max,$shown) = explode(',',$field['len']); $tmparray=array( 'size' => intval($shown > 0 ? $shown : $max).','.intval($max), 'maxlength'=>intval($max), 'no_lang' => True, ); if (is_array($field['values'])) { if (array_key_exists('readonly',$field['values'])) { if(!$this->advanced_search) $tmparray['readonly']='readonly'; } } $input =& etemplate::empty_cell('text',$this->prefix.$lname,$tmparray); } else {//textarea $tmparray=array( 'size' => $field['rows'].($field['len'] >0 ? ','.(int)$field['len'] : ''), 'no_lang' => True, ); if (is_array($field['values']) && array_key_exists('readonly',$field['values'])) { if(!$this->advanced_search) $tmparray['readonly']='readonly'; } $input =& etemplate::empty_cell('textarea',$this->prefix.$lname,$tmparray); } } else { $input =& etemplate::empty_cell('label',$this->prefix.$lname,array('no_lang' => True)); } break; case 'date': case 'date-time': $input =& etemplate::empty_cell($field['type'],$this->prefix.$lname,array( 'size' => $field['len'] ? $field['len'] : ($field['type'] == 'date' ? 'Y-m-d' : 'Y-m-d H:i:s'), )); break; case 'select-account': list($opts) = explode('=',$field['values'][0]); $input =& etemplate::empty_cell('select-account',$this->prefix.$lname,array( 'size' => ($field['rows']>1?$field['rows']:lang('None')).','.$opts, )); break; case 'button': // button(s) to execute javascript (label=onclick) or textinputs (empty label, readonly with neg. length) // a button does not seem to be helpful in advanced search ???, if($this->advanced_search) break; $input =& etemplate::empty_cell('hbox'); foreach($field['values'] as $label => $js) { if (!$label) // display an readonly input { $tmparray = array( 'size' => $field['len'] ? $field['len'] : 20, 'readonly' => $field['len'] < 0, 'onchange' => $js, ); $widget =& etemplate::empty_cell('text',$this->prefix.$lname.$label,$tmparray); } else { if ($readonly) continue; // dont display buttons if we're readonly $widget =& etemplate::empty_cell('buttononly',$this->prefix.$lname.$label,array( 'label' => $label ? $label : lang('Submit'), 'onclick' => $js, 'no_lang' => True )); } etemplate::add_child($input,$widget); unset($widget); } break; case 'url-email': list($max,$shown,$validation_type,$default) = explode(',',$field['len'],4); if (empty($max)) $max =128; if (empty($shown)) $shown = 28; if (empty($validation_type)) $validation_type = 1; $field['len'] = implode(',',array($shown, $max, $validation_type, $default)); $input =& etemplate::empty_cell($field['type'],$this->prefix.$lname,array( 'size' => $field['len'] )); break; case 'url': case 'url-phone': list($max,$shown,$validation_type) = explode(',',$field['len'],3); if (empty($max)) $max =128; if (empty($shown)) $shown = 28; $field['len']=implode(',',array( $shown, $max, $validation_type)); $input =& etemplate::empty_cell($field['type'],$this->prefix.$lname,array( 'size' => $field['len'] )); break; // other etemplate types, which are used just as is case 'checkbox' : $input =& etemplate::empty_cell($field['type'],$this->prefix.$lname); break; case 'link-entry': default : // link-entry to given app $input =& etemplate::empty_cell('link-entry',$this->prefix.$lname,array( 'size' => $field['type'] == 'link-entry' ? '' : $field['type'], )); // register post-processing of link widget to get eg. needed/required validation etemplate::$request->set_to_process(etemplate::form_name($form_name,$this->prefix.$lname), 'ext-link'); } $cell['data'][0]['c'.$n++] = $row_class.',top'; if (!is_null($input)) { if ($readonly) $input['readonly'] = true; $input['needed'] = $cell['needed'] || $field['needed']; if (!empty($field['help']) && $row_class != 'th') { $input['help'] = $field['help']; $input['no_lang'] = substr(lang($help),-1) == '*' ? 2 : 0; } if ($singlefield) // a single field, can & need to be returned instead of the cell (no grid) { $input['span'] = $cell['span']; // set span & class from original cell $cell = $input; if ($type == 'customfields') $cell['label'] = $field['label']; $value = $value[$this->prefix.$lname]; return true; } etemplate::add_child($cell,$input); unset($input); } unset($label); } } if ($type != 'customfields-list') { $cell['data'][0]['A'] = '100'; } list($span,$class) = explode(',',$cell['span']); // msie (at least 5.5) shows nothing with div overflow=auto // we dont want to use up the full space for the table created, so we skip the line below //$cell['size'] = '100%,100%,0,'.$class.','.(in_array($type,array('customfields-list','customfields-no-label'))?'0,0':',').(html::$user_agent != 'msie' ? ',auto' : ''); return True; // extra Label is ok } /** * Read the options of a 'select' or 'radio' custom field from a file * * For security reasons that file has to be relative to the eGW root * (to not use that feature to explore arbitrary files on the server) * and it has to be a php file setting one variable called options, * (to not display it to anonymously by the webserver). * The $options var has to be an array with value => label pairs, eg: * * 'Option A', * 'b' => 'Option B', * 'c' => 'Option C', * ); * * @param string $file file name inside the eGW server root, either relative to it or absolute * @return array in case of an error we return a single option with the message */ function _get_options_from_file($file) { if (!($path = realpath($file{0} == '/' ? $file : EGW_SERVER_ROOT.'/'.$file)) || // file does not exist substr($path,0,strlen(EGW_SERVER_ROOT)+1) != EGW_SERVER_ROOT.'/' || // we are NOT inside the eGW root basename($path,'.php').'.php' != basename($path) || // extension is NOT .php basename($path) == 'header.inc.php') // dont allow to include our header again { return array(lang("'%1' is no php file in the eGW server root (%2)!".': '.$path,$file,EGW_SERVER_ROOT)); } include($path); return $options; } /** * Get the customfield types containing links * * @return array with customefield types as values */ public static function get_customfield_link_types() { static $link_types; if (is_null($link_types)) { $link_types = array_keys(egw_link::app_list()); $link_types[] = 'link-entry'; } return $link_types; } /** * Check if there are links in the custom fields and update them * * This function have to be called manually by an application, if cf's linking * to other entries should be stored as links too (beside as cf's). * * @param string $own_app own appname * @param array $values new values including the custom fields * @param array $old=null old values before the update, if existing * @param string $id_name='id' name/key of the (link-)id in $values */ public static function update_customfield_links($own_app,array $values,array $old=null,$id_name='id') { $link_types = self::get_customfield_link_types(); foreach(config::get_customfields($own_app) as $name => $data) { if (!in_array($data['type'],$link_types)) continue; // do we have a different old value --> delete that link if ($old && $old['#'.$name] && $old['#'.$name] != $values['#'.$name]) { if ($data['type'] == 'link-entry') { list($app,$id) = explode(':',$old['#'.$name]); } else { $app = $data['type']; $id = $old['#'.$name]; } egw_link::unlink(false,$own_app,$values[$id_name],'',$app,$id); } if ($data['type'] == 'link-entry') { list($app,$id) = explode(':',$values['#'.$name]); } else { $app = $data['type']; $id = $values['#'.$name]; } if ($id) // create new link, does nothing for already existing links { egw_link::link($own_app,$values[$id_name],$app,$id); } } } }