2005-10-28 20:29:05 +02:00
< ? php
2008-06-22 10:51:38 +02:00
/**
* eGroupWare eTemplate Widget for custom fields
*
* @ link http :// www . egroupware . org
* @ author Ralf Becker < RalfBecker - AT - outdoor - training . de >
* @ author Cornelius Weiss < egw @ von - und - zu - weiss . de >
* @ 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 )
*
* 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' ,
);
2006-04-20 19:12:30 +02:00
/**
2008-06-22 10:51:38 +02:00
* Allowd types of customfields
2006-04-20 19:12:30 +02:00
*
2008-06-22 10:51:38 +02:00
* 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
2006-04-20 19:12:30 +02:00
*/
2008-06-22 10:51:38 +02:00
var $cf_types = array (
'text' => 'Text' ,
'label' => 'Label' ,
'select' => 'Selectbox' ,
'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
);
2005-10-28 20:29:05 +02:00
/**
2008-06-22 10:51:38 +02:00
* @ var $prefix string Prefix for every custiomfield name returned in $content ( # for general (admin) customfields)
*/
var $prefix = '#' ;
/**
* Current application
2005-10-28 20:29:05 +02:00
*
2008-06-22 10:51:38 +02:00
* @ var string
2005-10-28 20:29:05 +02:00
*/
2008-06-22 10:51:38 +02:00
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 ;
2007-07-11 17:39:41 +02:00
2005-10-28 20:29:05 +02:00
2008-06-22 10:51:38 +02:00
function customfields_widget ( $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' ];
}
2007-11-27 16:03:54 +01:00
2008-06-22 10:51:38 +02:00
function pre_process ( $name , & $value , & $cell , & $readonlys , & $extension_data , & $tmpl )
{
if ( $this -> appname == 'etemplate' || ! $this -> customfields ) // 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
2005-10-28 20:29:05 +02:00
{
2008-06-22 10:51:38 +02:00
list ( $app ) = explode ( '.' , $tmpl -> name );
if ( $app && $app != $this -> appname ) $this -> customfields_widget ( null , $app );
2005-10-28 20:29:05 +02:00
}
2008-06-22 10:51:38 +02:00
list ( $type2 , $use_private ) = explode ( ',' , $cell [ 'size' ]);
$fields_with_vals = array ();
$fields = $this -> customfields ;
2005-10-28 20:29:05 +02:00
2008-06-22 10:51:38 +02:00
// remove private or non-private cf's, if only one kind should be displayed
foreach (( array ) $fields as $key => $field )
2005-10-28 20:29:05 +02:00
{
2008-06-22 10:51:38 +02:00
if (( string ) $use_private !== '' && ( boolean ) $field [ 'private' ] != ( boolean ) $use_private )
2007-11-22 21:08:58 +01:00
{
2008-06-22 10:51:38 +02:00
unset ( $fields [ $key ]);
2007-11-22 21:08:58 +01:00
}
2008-06-22 10:51:38 +02:00
}
// 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 ;
}
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 ;
}
2008-10-21 10:08:56 +02:00
$link_types = egw_link :: app_list ();
2008-06-22 10:51:38 +02:00
ksort ( $link_types );
foreach ( $link_types as $lname => $label ) $cell [ 'sel_options' ][ $lname ] = '- ' . $label ;
$cell [ 'no_lang' ] = true ;
return true ;
2007-07-12 09:30:20 +02:00
2008-06-22 10:51:38 +02:00
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 )
2008-01-19 06:36:20 +01:00
{
2008-06-22 10:51:38 +02:00
$fields_with_vals [] = $lname ;
2008-01-19 06:36:20 +01:00
}
2008-06-22 10:51:38 +02:00
}
$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
2008-01-19 06:36:20 +01:00
}
2008-06-22 10:51:38 +02:00
$new_row = null ; etemplate :: add_child ( $cell , $new_row );
if ( $type != 'customfields-list' && $type == 'customfields' )
2007-11-22 21:08:58 +01:00
{
2008-06-22 10:51:38 +02:00
$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 ] !== '' ) {
2009-04-30 09:39:22 +02:00
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' =>/* lang ( " custom fields " ) . " : " .*/ $field [ 'label' ], 'width' => " 16px " ,
'onclick' => " return alert(' " . lang ( " custom fields " ) . " : " . $field [ 'label' ] . " '); " )));
}
2008-06-22 10:51:38 +02:00
}
2007-11-22 21:08:58 +01:00
}
2008-06-22 10:51:38 +02:00
switch (( string ) $field [ 'type' ])
2007-07-11 17:39:41 +02:00
{
2008-06-22 10:51:38 +02:00
case 'select' :
if ( count ( $field [ 'values' ]) == 1 && isset ( $field [ 'values' ][ '@' ]))
2007-11-27 16:03:54 +01:00
{
2008-06-22 10:51:38 +02:00
$field [ 'values' ] = $this -> _get_options_from_file ( $field [ 'values' ][ '@' ]);
2007-11-27 16:03:54 +01:00
}
2008-06-22 10:51:38 +02:00
if ( $this -> advanced_search && $field [ 'rows' ] <= 1 ) $field [ 'values' ][ '' ] = lang ( 'doesn\'t matter' );
foreach ( $field [ 'values' ] as $key => $val )
2007-07-11 17:39:41 +02:00
{
2008-06-22 10:51:38 +02:00
if ( substr ( $val = lang ( $val ), - 1 ) != '*' )
2007-11-27 16:03:54 +01:00
{
2008-06-22 10:51:38 +02:00
$field [ 'values' ][ $key ] = $val ;
2007-11-27 16:03:54 +01:00
}
2007-07-11 17:39:41 +02:00
}
2008-06-22 10:51:38 +02:00
$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 );
2009-04-30 09:39:22 +02:00
/* the following seem to double the select fields in advanced search .
2008-06-22 10:51:38 +02:00
etemplate :: add_child ( $input , etemplate :: empty_cell ( 'select' , $this -> prefix . $lname , array (
'sel_options' => $field [ 'values' ],
'size' => $field [ 'rows' ],
'no_lang' => True
)));
2009-04-30 09:39:22 +02:00
*/
2008-06-22 10:51:38 +02:00
}
break ;
case 'label' :
$row_class = 'th' ;
break ;
case 'radio' :
2009-04-30 09:39:22 +02:00
$showthis = '#a#l#l#' ;
2008-06-22 10:51:38 +02:00
if ( count ( $field [ 'values' ]) == 1 && isset ( $field [ 'values' ][ '@' ]))
2007-11-27 16:03:54 +01:00
{
2008-06-22 10:51:38 +02:00
$field [ 'values' ] = $this -> _get_options_from_file ( $field [ 'values' ][ '@' ]);
2007-11-27 16:03:54 +01:00
}
2009-04-30 09:39:22 +02:00
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' );
}
2008-06-22 10:51:38 +02:00
$m = 0 ;
foreach ( $field [ 'values' ] as $key => $val )
{
$radio = etemplate :: empty_cell ( 'radio' , $this -> prefix . $lname );
$radio [ 'label' ] = $val ;
$radio [ 'size' ] = $key ;
2009-04-30 09:39:22 +02:00
if ( $showthis == '#a#l#l#' || $showthis == $key ) etemplate :: add_child ( $input , $radio );
2008-06-22 10:51:38 +02:00
unset ( $radio );
2007-11-27 16:03:54 +01:00
}
2008-06-22 10:51:38 +02:00
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 ),
);
if ( is_array ( $field [ 'values' ]))
2005-10-28 20:29:05 +02:00
{
2007-11-27 16:03:54 +01:00
if ( array_key_exists ( 'readonly' , $field [ 'values' ]))
{
2009-04-30 09:39:22 +02:00
if ( ! $this -> advanced_search ) $tmparray [ 'readonly' ] = 'readonly' ;
2007-11-27 16:03:54 +01:00
}
}
2008-06-22 10:51:38 +02:00
$input =& etemplate :: empty_cell ( 'text' , $this -> prefix . $lname , $tmparray );
2005-10-28 20:29:05 +02:00
}
2008-06-22 10:51:38 +02:00
else
{ //textarea
$tmparray = array (
'size' => $field [ 'rows' ] . ( $field [ 'len' ] > 0 ? ',' . ( int ) $field [ 'len' ] : '' )
);
if ( is_array ( $field [ 'values' ]) && array_key_exists ( 'readonly' , $field [ 'values' ]))
2007-09-13 14:43:58 +02:00
{
2009-04-30 09:39:22 +02:00
if ( ! $this -> advanced_search ) $tmparray [ 'readonly' ] = 'readonly' ;
2007-09-13 14:43:58 +02:00
}
2008-06-22 10:51:38 +02:00
$input =& etemplate :: empty_cell ( 'textarea' , $this -> prefix . $lname , $tmparray );
2007-09-13 14:43:58 +02:00
}
2008-06-22 10:51:38 +02:00
} else {
$input =& etemplate :: empty_cell ( 'label' , $this -> prefix . $lname ,
array (
'onclick' => " return alert(' " . lang ( " custom fields " ) . " : " .
$lname . " => " . htmlentities ( str_replace ( " \r " , " " , str_replace ( " \n " , " " , $value [ $this -> prefix . $lname ]))) . " '); " ,
)
);
2007-07-12 09:53:21 +02:00
}
2008-06-22 10:51:38 +02:00
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)
2009-04-30 09:39:22 +02:00
// a button does not seem to be helpful in advanced search ???,
if ( $this -> advanced_search ) break ;
2008-06-22 10:51:38 +02:00
$input =& etemplate :: empty_cell ( 'hbox' );
foreach ( $field [ 'values' ] as $label => $js )
2007-12-07 08:52:14 +01:00
{
2008-06-22 10:51:38 +02:00
if ( ! $label ) // display an readonly input
{
2009-04-30 09:39:22 +02:00
$tmparray = array (
2008-06-22 10:51:38 +02:00
'size' => $field [ 'len' ] ? $field [ 'len' ] : 20 ,
'readonly' => $field [ 'len' ] < 0 ,
'onchange' => $js ,
2009-04-30 09:39:22 +02:00
);
$widget =& etemplate :: empty_cell ( 'text' , $this -> prefix . $lname . $label , $tmparray );
2008-06-22 10:51:38 +02:00
}
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 );
2007-12-07 08:52:14 +01:00
}
2008-06-22 10:51:38 +02:00
break ;
2008-06-24 11:56:39 +02:00
case 'url-email' :
2008-06-30 16:30:16 +02:00
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 (
2008-09-11 16:57:20 +02:00
'size' => $field [ 'len' ]
2008-06-30 16:30:16 +02:00
));
break ;
case 'url' :
2008-06-24 11:56:39 +02:00
case 'url-phone' :
2008-06-30 16:30:16 +02:00
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
2008-06-24 11:56:39 +02:00
case 'checkbox' :
$input =& etemplate :: empty_cell ( $field [ 'type' ], $this -> prefix . $lname );
break ;
2008-06-22 10:51:38 +02:00
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' ],
));
2005-10-28 20:29:05 +02:00
}
2008-06-22 10:51:38 +02:00
$cell [ 'data' ][ 0 ][ 'c' . $n ++ ] = $row_class . ',top' ;
if ( ! is_null ( $input ))
2005-10-28 20:29:05 +02:00
{
2008-06-22 10:51:38 +02:00
if ( $readonly ) $input [ 'readonly' ] = true ;
2005-10-28 20:29:05 +02:00
2008-06-22 10:51:38 +02:00
if ( $cell [ 'needed' ]) $input [ 'needed' ] = $cell [ 'needed' ];
2007-11-27 16:03:54 +01:00
2008-06-22 10:51:38 +02:00
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)
{
$cell = $input ;
if ( $type == 'customfields' ) $cell [ 'label' ] = $field [ 'label' ];
return true ;
}
etemplate :: add_child ( $cell , $input );
unset ( $input );
2007-09-13 14:43:58 +02:00
}
2008-06-22 10:51:38 +02:00
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
}
2007-11-27 16:03:54 +01:00
2008-06-22 10:51:38 +02:00
/**
* 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 :
*
* < ? php
* $options = array (
* 'a' => '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 ));
2007-09-13 14:43:58 +02:00
}
2008-06-22 10:51:38 +02:00
include ( $path );
return $options ;
2005-10-28 20:29:05 +02:00
}
2008-09-11 16:57:20 +02:00
/**
* 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 );
}
}
}
2008-06-22 10:51:38 +02:00
}