2016-08-12 11:35:30 +02:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* EGgroupware admin - UI for adding custom fields
|
|
|
|
*
|
|
|
|
* @link http://www.egroupware.org
|
|
|
|
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
|
|
|
|
* @author Cornelius Weiss <nelius-AT-von-und-zu-weiss.de>
|
|
|
|
* @package admin
|
|
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
|
|
* @version $Id$
|
|
|
|
*/
|
|
|
|
|
|
|
|
use EGroupware\Api;
|
|
|
|
use EGroupware\Api\Framework;
|
|
|
|
use EGroupware\Api\Etemplate;
|
|
|
|
|
|
|
|
/**
|
2024-03-30 10:18:28 +01:00
|
|
|
* Customfields class - manages custom-field definitions in egw_customfields table through Api\Storage\Customfields class.
|
2016-08-12 11:35:30 +02:00
|
|
|
*
|
|
|
|
* Applications can have customfields by sub-type by having a template
|
|
|
|
* named '<appname>.admin.types'. See admin.customfields.types as an
|
|
|
|
* example, but the template can even be empty if types are handled by the
|
|
|
|
* application in another way.
|
|
|
|
*
|
|
|
|
* Applications can extend this class to customize the custom fields and handle
|
|
|
|
* extra information from the above template by extending and implementing
|
|
|
|
* update() and app_index().
|
|
|
|
*/
|
|
|
|
class admin_customfields
|
|
|
|
{
|
|
|
|
|
|
|
|
/**
|
|
|
|
* appname of app which want to add / edit its customfields
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
var $appname;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Allow custom fields to be restricted to certain users/groups
|
|
|
|
*/
|
|
|
|
protected $use_private = false;
|
|
|
|
|
2024-03-20 14:05:40 +01:00
|
|
|
/**
|
|
|
|
* Allow custom fields to be readonly for certain users/groups
|
|
|
|
*/
|
|
|
|
protected $use_readonly = false;
|
|
|
|
|
2016-08-12 11:35:30 +02:00
|
|
|
/**
|
|
|
|
* userdefiened types e.g. type of infolog
|
|
|
|
*
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
var $types2 = array();
|
|
|
|
var $content_types,$fields;
|
|
|
|
|
2016-08-15 11:56:32 +02:00
|
|
|
/**
|
|
|
|
* Does App uses content-types
|
|
|
|
*
|
|
|
|
* @var boolean
|
|
|
|
*/
|
|
|
|
protected $manage_content_types = false;
|
|
|
|
|
2016-08-12 11:35:30 +02:00
|
|
|
/**
|
|
|
|
* Currently selected content type (if used by app)
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $content_type = null;
|
|
|
|
|
|
|
|
var $public_functions = array(
|
|
|
|
'index' => true,
|
|
|
|
'edit' => True
|
|
|
|
);
|
|
|
|
/**
|
|
|
|
* Instance of etemplate class
|
|
|
|
*
|
|
|
|
* @var etemplate
|
|
|
|
*/
|
|
|
|
var $tmpl;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var Description of the options or value format for each cf_type
|
|
|
|
*/
|
|
|
|
public static $type_option_help = array(
|
|
|
|
'search' => 'set get_rows, get_title and id_field, or use @path to read options from a file in EGroupware directory',
|
|
|
|
'select' => 'each value is a line like id[=label], or use @path to read options from a file in EGroupware directory',
|
|
|
|
'radio' => 'each value is a line like id[=label], or use @path to read options from a file in EGroupware directory',
|
2020-06-12 18:56:44 +02:00
|
|
|
'button' => 'each value is a line like label=[javascript]',
|
2024-03-27 16:06:38 +01:00
|
|
|
'password' => 'set length=# for minimum password length, strength=# for password strength',
|
2024-03-27 16:44:57 +01:00
|
|
|
'serial' => 'you can set an initial value, which gets incremented every time a new serial get generated',
|
2024-06-25 22:58:12 +02:00
|
|
|
'filemanager' => "use the following options:\nnoVfsSelect=1\nnoUpload=1\nmime=application/pdf or /^image\//i\naccept=pdf,docx\nmax_upload_size=2M",
|
2016-08-12 11:35:30 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Custom fields can also have length and rows set, but these are't used for all types
|
|
|
|
* If not set to true here, the field will be disabled when selecting the type
|
|
|
|
*/
|
|
|
|
public static $type_attribute_flags = array(
|
|
|
|
'text' => array('cf_len' => true, 'cf_rows' => true),
|
|
|
|
'float' => array('cf_len' => true),
|
2020-06-12 18:56:44 +02:00
|
|
|
'passwd'=> array('cf_len' => true, 'cf_rows' => false, 'cf_values' => true),
|
2016-08-12 11:35:30 +02:00
|
|
|
'label' => array('cf_values' => true),
|
|
|
|
'select' => array('cf_len' => false, 'cf_rows' => true, 'cf_values' => true),
|
|
|
|
'date' => array('cf_len' => true, 'cf_rows' => false, 'cf_values' => true),
|
|
|
|
'date-time' => array('cf_len' => true, 'cf_rows' => false, 'cf_values' => true),
|
|
|
|
'select-account' => array('cf_len' => false, 'cf_rows' => true),
|
|
|
|
'htmlarea' => array('cf_len' => true, 'cf_rows' => true),
|
|
|
|
'button' => array('cf_values' => true),
|
|
|
|
'radio' => array('cf_values' => true),
|
|
|
|
'checkbox' => array('cf_values' => true),
|
|
|
|
'filemanager' => array('cf_values' => true),
|
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor
|
|
|
|
*
|
|
|
|
* @param string $appname
|
|
|
|
*/
|
|
|
|
function __construct($appname='')
|
|
|
|
{
|
|
|
|
if (($this->appname = $appname))
|
|
|
|
{
|
|
|
|
$this->fields = Api\Storage\Customfields::get($this->appname,true);
|
|
|
|
$this->content_types = Api\Config::get_content_types($this->appname);
|
|
|
|
}
|
2023-03-07 08:18:58 +01:00
|
|
|
$this->so = new Api\Storage\Base('api','egw_customfields',null,'',true);
|
2024-03-27 16:44:57 +01:00
|
|
|
|
|
|
|
// Make sure app css & lang get loaded, extending app might cause et2 to miss it
|
|
|
|
Framework::includeCSS('admin','app');
|
|
|
|
Api\Translation::add_app('admin');
|
2016-08-12 11:35:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* List custom fields
|
|
|
|
*/
|
|
|
|
public function index($content = array())
|
|
|
|
{
|
|
|
|
// determine appname
|
2021-10-09 08:42:02 +02:00
|
|
|
$this->appname = $this->appname ?: (!empty($_GET['appname']) ? $_GET['appname'] : (!empty($content['appname']) ? $content['appname'] : false));
|
2018-01-23 12:33:48 +01:00
|
|
|
if(!$this->appname) die(lang('Error! No appname found'));
|
2016-08-12 11:35:30 +02:00
|
|
|
|
2021-10-09 08:42:02 +02:00
|
|
|
$this->use_private = !empty($_GET['use_private']) && $_GET['use_private'] !== 'undefined' || !empty($content['use_private']);
|
2016-08-12 11:35:30 +02:00
|
|
|
|
|
|
|
// Read fields, constructor doesn't always know appname
|
2024-03-20 21:52:51 +01:00
|
|
|
$this->fields = Api\Storage\Customfields::get($this->appname,true, null, null, null);
|
2016-08-12 11:35:30 +02:00
|
|
|
|
|
|
|
$this->tmpl = new Etemplate();
|
|
|
|
$this->tmpl->read('admin.customfields');
|
|
|
|
|
|
|
|
// do we manage content-types?
|
|
|
|
$test = new Etemplate();
|
|
|
|
if($test->read($this->appname.'.admin.types')) $this->manage_content_types = true;
|
|
|
|
|
|
|
|
// Handle incoming - types, options, etc.
|
|
|
|
if($this->manage_content_types)
|
|
|
|
{
|
2021-03-28 20:48:55 +02:00
|
|
|
if(empty($this->content_types))
|
2016-08-12 11:35:30 +02:00
|
|
|
{
|
|
|
|
$this->content_types = Api\Config::get_content_types($this->appname);
|
|
|
|
}
|
2021-03-28 20:48:55 +02:00
|
|
|
if (empty($this->content_types))
|
2016-08-12 11:35:30 +02:00
|
|
|
{
|
|
|
|
// if you define your default types of your app with the search_link hook, they are available here, if no types were found
|
|
|
|
$this->content_types = (array)Api\Link::get_registry($this->appname,'default_types');
|
|
|
|
}
|
|
|
|
// Set this now, we need to know it for updates
|
2023-03-07 08:18:58 +01:00
|
|
|
$this->content_type = $content['content_types']['types'] ?: (array_key_exists(0,$this->content_types) ? $this->content_types[0] : key($this->content_types));
|
2016-08-12 11:35:30 +02:00
|
|
|
|
|
|
|
// Common type changes - add, delete
|
|
|
|
if($content['content_types']['delete'])
|
|
|
|
{
|
|
|
|
$this->delete_content_type($content);
|
|
|
|
}
|
|
|
|
elseif($content['content_types']['create'])
|
|
|
|
{
|
|
|
|
if(($new_type = $this->create_content_type($content)))
|
|
|
|
{
|
|
|
|
$content['content_types']['types'] = $this->content_type = $new_type;
|
|
|
|
}
|
|
|
|
unset($content['content_types']['create']);
|
|
|
|
unset($content['content_types']['name']);
|
|
|
|
}
|
2019-10-09 21:09:27 +02:00
|
|
|
// No common type change and type didn't change, try an update to check new type statuses
|
|
|
|
elseif($this->content_type && is_array($content) && $this->content_type == $content['old_content_type'])
|
|
|
|
{
|
|
|
|
$this->update($content);
|
|
|
|
}
|
2016-08-12 11:35:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Custom field deleted from nextmatch
|
|
|
|
if($content['nm']['action'] == 'delete')
|
|
|
|
{
|
|
|
|
foreach($this->fields as $name => $data)
|
|
|
|
{
|
|
|
|
if(in_array($data['id'],$content['nm']['selected']))
|
|
|
|
{
|
2019-05-15 00:43:15 +02:00
|
|
|
$cmd = new admin_cmd_customfield(
|
|
|
|
$this->appname,
|
|
|
|
array('id' => $data['id'],'name' => $name),
|
|
|
|
null,
|
|
|
|
$content['nm']['admin_cmd']
|
|
|
|
);
|
2018-12-11 18:10:09 +01:00
|
|
|
$cmd->run();
|
2016-08-12 11:35:30 +02:00
|
|
|
unset($this->fields[$name]);
|
2019-06-24 19:35:30 +02:00
|
|
|
|
|
|
|
Framework::refresh_opener('Deleted', 'admin', $data['id'] /* Conflicts with Api\Accounts 'delete'*/);
|
2016-08-12 11:35:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$content['nm']= Api\Cache::getSession('admin', 'customfield-index');
|
|
|
|
if (!is_array($content['nm']))
|
|
|
|
{
|
|
|
|
// Initialize nextmatch
|
|
|
|
$content['nm'] = array(
|
2022-10-27 19:13:04 +02:00
|
|
|
'get_rows' => 'admin.admin_customfields.get_rows',
|
|
|
|
'no_cat' => 'true',
|
|
|
|
'no_filter' => 'true',
|
|
|
|
'no_filter2' => 'true',
|
|
|
|
'row_id' => 'cf_id',
|
|
|
|
'order' => 'cf_order',// IO name of the column to sort
|
|
|
|
'sort' => 'ASC',// IO direction of the sort: 'ASC' or 'DESC'
|
|
|
|
'actions' => $this->get_actions(),
|
|
|
|
'dataStorePrefix' => 'customfield'
|
2016-08-12 11:35:30 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
$content['nm']['appname'] = $this->appname;
|
|
|
|
$content['nm']['use_private'] = $this->use_private;
|
|
|
|
|
|
|
|
// Set up sub-types
|
|
|
|
if($this->manage_content_types)
|
|
|
|
{
|
|
|
|
foreach($this->content_types as $type => $entry)
|
|
|
|
{
|
|
|
|
if(!is_array($entry))
|
|
|
|
{
|
|
|
|
$this->content_types[$type] = array('name' => $entry);
|
|
|
|
$entry = $this->content_types[$type];
|
|
|
|
}
|
|
|
|
$this->types2[$type] = $entry['name'];
|
|
|
|
}
|
|
|
|
$sel_options['types'] = $sel_options['cf_type2'] = $this->types2;
|
|
|
|
|
|
|
|
$content['type_template'] = $this->appname . '.admin.types';
|
|
|
|
$content['content_types']['appname'] = $this->appname;
|
|
|
|
|
|
|
|
$content['content_type_options'] = $this->content_types[$this->content_type]['options'];
|
|
|
|
$content['content_type_options']['type'] = $this->types2[$this->content_type];
|
|
|
|
if ($this->content_types[$this->content_type]['non_deletable'])
|
|
|
|
{
|
|
|
|
$content['content_types']['non_deletable'] = true;
|
|
|
|
}
|
|
|
|
if ($this->content_types['']['no_add'])
|
|
|
|
{
|
|
|
|
$content['content_types']['no_add'] = true;
|
|
|
|
}
|
|
|
|
if ($content['content_types']['non_deletable'] && $content['content_types']['no_add'])
|
|
|
|
{
|
|
|
|
// Hide the whole line if you can't add or delete
|
|
|
|
$content['content_types']['no_edit_types'] = true;
|
|
|
|
}
|
|
|
|
// do NOT allow to delete original contact content-type for addressbook,
|
2024-04-23 10:42:00 +02:00
|
|
|
// as it only creates support problems as users accidentally delete it
|
2016-08-12 11:35:30 +02:00
|
|
|
if ($this->appname == 'addressbook' && $this->content_type == 'n')
|
|
|
|
{
|
|
|
|
$readonlys['content_types']['delete'] = true;
|
|
|
|
}
|
|
|
|
$content['nm']['type2'] = true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Disable content types
|
|
|
|
$this->tmpl->disableElement('content_types', true);
|
|
|
|
}
|
2024-04-23 10:42:00 +02:00
|
|
|
$sel_options['cf_type'] = Etemplate\Widget\Customfields::getCfTypes();
|
|
|
|
$sel_options['cf_tab'] = $this->so->query_list('cf_tab', '', ['cf_app' => $this->appname]);
|
2016-08-12 11:35:30 +02:00
|
|
|
$preserve = array(
|
|
|
|
'appname' => $this->appname,
|
|
|
|
'use_private' => $this->use_private,
|
|
|
|
'old_content_type' => $this->content_type
|
|
|
|
);
|
|
|
|
|
|
|
|
// Allow extending app a change to change content before display
|
2024-04-24 14:55:05 +02:00
|
|
|
if (!isset($readonlys)) $readonlys = [];
|
2016-08-12 11:35:30 +02:00
|
|
|
static::app_index($content, $sel_options, $readonlys, $preserve);
|
|
|
|
|
2019-09-03 22:50:18 +02:00
|
|
|
// Set app to admin to make sure actions are correctly loaded into admin
|
|
|
|
$GLOBALS['egw_info']['flags']['currentapp'] = 'admin';
|
2016-08-12 11:35:30 +02:00
|
|
|
$GLOBALS['egw_info']['flags']['app_header'] = $GLOBALS['egw_info']['apps'][$this->appname]['title'].' - '.lang('Custom fields');
|
|
|
|
|
|
|
|
// Some logic to make sure extending class (if there is one) gets called
|
|
|
|
// when etemplate2 comes back instead of parent class
|
2017-01-17 18:24:56 +01:00
|
|
|
$exec = get_class() == get_called_class() || get_called_class() == 'customfields' ?
|
|
|
|
'admin.admin_customfields.index' : $this->appname . '.' . get_called_class() . '.index';
|
2016-08-12 11:35:30 +02:00
|
|
|
|
|
|
|
$this->tmpl->exec($exec,$content,$sel_options,$readonlys,$preserve);
|
|
|
|
}
|
|
|
|
|
2019-05-22 17:35:01 +02:00
|
|
|
/**
|
2020-01-29 11:08:44 +01:00
|
|
|
* Delete a type over ajax.
|
2019-05-22 17:35:01 +02:00
|
|
|
*
|
2020-01-29 11:08:44 +01:00
|
|
|
* Used when Policy is involved, otherwise things go normally
|
|
|
|
*
|
|
|
|
* @param array $content
|
|
|
|
* @param string $etemplate_exec_id to check against CSRF
|
2019-05-22 17:35:01 +02:00
|
|
|
*/
|
2020-01-29 11:08:44 +01:00
|
|
|
public function ajax_delete_type($content, $etemplate_exec_id)
|
2019-05-22 17:35:01 +02:00
|
|
|
{
|
2020-01-29 11:08:44 +01:00
|
|
|
Api\Etemplate\Request::csrfCheck($etemplate_exec_id, __METHOD__, func_get_args());
|
|
|
|
|
2019-05-22 17:35:01 +02:00
|
|
|
// Read fields
|
|
|
|
$this->appname = $content['appname'];
|
|
|
|
$this->fields = Api\Storage\Customfields::get($content['appname'],true);
|
|
|
|
$this->content_types = Api\Config::get_content_types($content['appname']);
|
|
|
|
$this->delete_content_type($content);
|
|
|
|
}
|
|
|
|
|
2019-01-17 18:08:58 +01:00
|
|
|
/**
|
|
|
|
* Check selectbox values to match regular expression in et2_widget_selectbox.js: _is_multiple_regexp
|
|
|
|
*
|
|
|
|
* If values do not match, comma-separated values are not split by comma!
|
|
|
|
*/
|
|
|
|
const CHECK_MULTISELCT_VALUE = '/^[0-9A-Za-z\/_ -]+$/';
|
|
|
|
|
2016-08-12 11:35:30 +02:00
|
|
|
/**
|
|
|
|
* Edit/Create Custom fields with type
|
|
|
|
*
|
|
|
|
* @author Ralf Becker <ralfbecker-AT-outdoor-training.de>
|
|
|
|
* @param array $content Content from the eTemplate Exec
|
|
|
|
*/
|
|
|
|
function edit($content = null)
|
|
|
|
{
|
2021-10-09 08:42:02 +02:00
|
|
|
$cf_id = isset($_GET['cf_id']) ? (int)$_GET['cf_id'] : (int)$content['cf_id'];
|
2016-08-12 11:35:30 +02:00
|
|
|
|
|
|
|
// determine appname
|
2021-10-09 08:42:02 +02:00
|
|
|
$this->appname = $this->appname ?: (isset($_GET['appname']) ? $_GET['appname'] : (!empty($content['cf_app']) ? $content['cf_app'] : false));
|
2016-08-12 11:35:30 +02:00
|
|
|
if(!$this->appname)
|
|
|
|
{
|
|
|
|
if($cf_id && $this->so)
|
|
|
|
{
|
|
|
|
$content = $this->so->read($cf_id);
|
|
|
|
$this->appname = $content['cf_app'];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(!$this->appname)
|
|
|
|
{
|
|
|
|
die(lang('Error! No appname found'));
|
|
|
|
}
|
2021-10-09 08:42:02 +02:00
|
|
|
$this->use_private = !isset($_GET['use_private']) || (boolean)$_GET['use_private'] || !empty($content['use_private']);
|
2024-03-20 14:05:40 +01:00
|
|
|
$this->use_readonly = !isset($_GET['use_readonly']) || (boolean)$_GET['use_readonly'] || !empty($content['use_readonly']);
|
2016-08-12 11:35:30 +02:00
|
|
|
|
|
|
|
// Read fields, constructor doesn't always know appname
|
2024-03-20 21:52:51 +01:00
|
|
|
$this->fields = Api\Storage\Customfields::get($this->appname,true, null, null, null);
|
2016-08-12 11:35:30 +02:00
|
|
|
|
|
|
|
// Update based on info returned from template
|
|
|
|
if (is_array($content))
|
|
|
|
{
|
2021-10-09 08:42:02 +02:00
|
|
|
$action = key($content['button'] ?? []);
|
2016-08-12 11:35:30 +02:00
|
|
|
switch($action)
|
|
|
|
{
|
|
|
|
case 'delete':
|
2018-12-11 18:10:09 +01:00
|
|
|
$field = $this->so->read($cf_id);
|
|
|
|
$cmd = new admin_cmd_customfield($this->appname, array('id' => $cf_id,'name' => $field['cf_name']));
|
|
|
|
$cmd->run();
|
2016-08-12 11:35:30 +02:00
|
|
|
Framework::refresh_opener('Deleted', 'admin', $cf_id /* Conflicts with Api\Accounts 'delete'*/);
|
|
|
|
Framework::window_close();
|
|
|
|
break;
|
|
|
|
case 'save':
|
|
|
|
case 'apply':
|
|
|
|
if(!$cf_id && $this->fields[$content['cf_name']])
|
|
|
|
{
|
|
|
|
Framework::message(lang("Field '%1' already exists !!!",$content['cf_name']),'error');
|
|
|
|
$content['cf_name'] = '';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if(empty($content['cf_label']))
|
|
|
|
{
|
|
|
|
$content['cf_label'] = $content['cf_name'];
|
|
|
|
}
|
|
|
|
if (!empty($content['cf_values']))
|
|
|
|
{
|
|
|
|
$values = array();
|
2024-04-04 08:56:35 +02:00
|
|
|
if ($content['cf_type'] === 'serial' && !str_starts_with($content['cf_values'], 'last='))
|
2024-03-30 10:18:28 +01:00
|
|
|
{
|
2024-04-04 08:56:35 +02:00
|
|
|
$content['cf_values'] = 'last=' . $content['cf_values'];
|
2024-03-30 10:18:28 +01:00
|
|
|
}
|
2024-04-03 21:12:31 +02:00
|
|
|
if($content['cf_values'][0] === '@')
|
2016-08-12 11:35:30 +02:00
|
|
|
{
|
|
|
|
$values['@'] = substr($content['cf_values'], $content['cf_values'][1] === '=' ? 2:1);
|
|
|
|
}
|
2024-03-27 16:44:57 +01:00
|
|
|
elseif (isset($GLOBALS['egw_info']['apps'][$content['cf_type']]) && $content['cf_type'] !== 'filemanager')
|
2024-02-08 21:26:07 +01:00
|
|
|
{
|
|
|
|
if (!empty($content['cf_values']) && ($content['cf_values'][0] !== '{' || ($values=json_decode($content['cf_values'])) === null))
|
|
|
|
{
|
|
|
|
Api\Etemplate::set_validation_error('cf_values', lang('Invalid JSON object!'));
|
|
|
|
}
|
|
|
|
}
|
2016-08-12 11:35:30 +02:00
|
|
|
else
|
|
|
|
{
|
2019-04-09 19:59:31 +02:00
|
|
|
foreach(explode("\n",trim($content['cf_values'])) as $idx => $line)
|
2016-08-12 11:35:30 +02:00
|
|
|
{
|
|
|
|
list($var_raw,$value) = explode('=',trim($line),2);
|
|
|
|
$var = trim($var_raw);
|
2019-04-09 19:59:31 +02:00
|
|
|
if (!preg_match(self::CHECK_MULTISELCT_VALUE, $var) && !($idx == 0 && !$var && $value))
|
2019-01-17 18:08:58 +01:00
|
|
|
{
|
|
|
|
Api\Etemplate::set_validation_error('cf_values',
|
2019-04-09 19:59:31 +02:00
|
|
|
lang('Invalid value "%1", use only:', $var)."\n".
|
2019-01-17 18:08:58 +01:00
|
|
|
preg_replace('/^.*\[([^]]+)\].*$/', '$1', self::CHECK_MULTISELCT_VALUE));
|
|
|
|
$action = 'apply'; // do not close the window to show validation error
|
|
|
|
if (!$cf_id) break 2; // only stop storing of new CFs
|
|
|
|
}
|
2016-08-12 11:35:30 +02:00
|
|
|
$values[$var] = trim($value)==='' ? $var : $value;
|
|
|
|
}
|
|
|
|
}
|
2024-04-04 08:56:35 +02:00
|
|
|
if ($content['cf_type'] === 'serial' && !preg_match(Api\Storage\Customfields::SERIAL_PREG, $values['last']))
|
2024-04-03 21:12:31 +02:00
|
|
|
{
|
|
|
|
Api\Etemplate::set_validation_error('cf_values', lang('Invalid Format, must end in a group of digits e.g. %1 or %2', "'0000'", "'RE2024-0000'"));
|
|
|
|
break;
|
|
|
|
}
|
2016-08-12 11:35:30 +02:00
|
|
|
$content['cf_values'] = $values;
|
|
|
|
}
|
|
|
|
$update_content = array();
|
|
|
|
foreach($content as $key => $value)
|
|
|
|
{
|
|
|
|
if(substr($key,0,3) == 'cf_')
|
|
|
|
{
|
|
|
|
$update_content[substr($key,3)] = $value;
|
|
|
|
}
|
|
|
|
}
|
2019-04-09 22:48:44 +02:00
|
|
|
$cmd = new admin_cmd_customfield($this->appname, $update_content,null,$content['admin_cmd']);
|
2018-12-11 18:10:09 +01:00
|
|
|
$cmd->run();
|
2016-08-12 11:35:30 +02:00
|
|
|
if(!$cf_id)
|
|
|
|
{
|
|
|
|
$this->fields = Api\Storage\Customfields::get($this->appname,true);
|
|
|
|
$cf_id = (int)$this->fields[$content['cf_name']]['id'];
|
|
|
|
}
|
2019-05-23 19:25:06 +02:00
|
|
|
Framework::refresh_opener(lang('Entry saved'), 'admin', $cf_id, 'edit');
|
2016-08-12 11:35:30 +02:00
|
|
|
if ($action != 'save')
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
//fall through
|
|
|
|
case 'cancel':
|
|
|
|
Framework::window_close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2021-10-09 08:42:02 +02:00
|
|
|
$content['use_private'] = !empty($_GET['use_private']) && $_GET['use_private'] !== 'undefined';
|
2016-08-12 11:35:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// do we manage content-types?
|
|
|
|
$test = new Etemplate();
|
|
|
|
if($test->read($this->appname.'.admin.types')) $this->manage_content_types = true;
|
|
|
|
|
2022-11-16 22:10:34 +01:00
|
|
|
if(is_null($this->tmpl))
|
|
|
|
{
|
|
|
|
$this->tmpl = new Etemplate();
|
|
|
|
}
|
2016-08-12 11:35:30 +02:00
|
|
|
$this->tmpl->read('admin.customfield_edit');
|
|
|
|
|
|
|
|
Api\Translation::add_app('infolog'); // til we move the translations
|
|
|
|
|
|
|
|
$GLOBALS['egw_info']['flags']['app_header'] = $GLOBALS['egw_info']['apps'][$this->appname]['title'].' - '.lang('Custom fields');
|
|
|
|
$sel_options = array();
|
|
|
|
$readonlys = array();
|
|
|
|
|
|
|
|
//echo 'customfields=<pre style="text-align: left;">'; print_r($this->fields); echo "</pre>\n";
|
|
|
|
$content['use_private'] = $this->use_private;
|
2024-03-20 14:05:40 +01:00
|
|
|
$content['use_readonly'] = $this->use_readonly;
|
2016-08-12 11:35:30 +02:00
|
|
|
|
|
|
|
if($cf_id)
|
|
|
|
{
|
|
|
|
$content = array_merge($content, $this->so->read($cf_id));
|
|
|
|
$this->appname = $content['cf_app'];
|
|
|
|
if($content['cf_private'])
|
|
|
|
{
|
|
|
|
$content['cf_private'] = explode(',',$content['cf_private']);
|
|
|
|
}
|
2024-03-27 17:10:34 +01:00
|
|
|
if($content['cf_readonly'])
|
2016-08-12 11:35:30 +02:00
|
|
|
{
|
2024-03-27 17:10:34 +01:00
|
|
|
$content['cf_readonly'] = explode(',',$content['cf_readonly']);
|
2024-03-27 16:06:38 +01:00
|
|
|
}
|
2024-03-27 16:44:57 +01:00
|
|
|
if (!isset($GLOBALS['egw_info']['apps'][$content['cf_type']]) || $content['cf_type'] === 'filemanager')
|
2024-02-08 21:26:07 +01:00
|
|
|
{
|
|
|
|
$content['cf_values'] = json_decode($content['cf_values'], true);
|
|
|
|
}
|
2024-03-27 17:10:34 +01:00
|
|
|
if ($_GET['action'] ?? null === 'copy')
|
|
|
|
{
|
|
|
|
unset($content['cf_id'], $cf_id);
|
|
|
|
$content['cf_name'] = lang('Copy of').' '.$content['cf_name'];
|
|
|
|
$content['cf_order'] += 5; // behind copied field
|
|
|
|
$readonlys['button[delete]'] = true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if($content['cf_name'])
|
|
|
|
{
|
|
|
|
$readonlys['cf_name'] = true;
|
|
|
|
}
|
|
|
|
if ($content['cf_type'] === 'serial')
|
|
|
|
{
|
|
|
|
$readonlys['cf_values'] = true; // only allow to set start-value, but not change it after
|
|
|
|
}
|
|
|
|
}
|
2016-08-12 11:35:30 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
$readonlys['button[delete]'] = true;
|
2024-03-30 10:18:28 +01:00
|
|
|
$content['cf_order'] = 10*(1+count($this->fields));
|
2016-08-12 11:35:30 +02:00
|
|
|
}
|
|
|
|
if (is_array($content['cf_values']))
|
|
|
|
{
|
|
|
|
$values = '';
|
|
|
|
foreach($content['cf_values'] as $var => $value)
|
|
|
|
{
|
|
|
|
$values .= (!empty($values) ? "\n" : '').$var.'='.$value;
|
|
|
|
}
|
|
|
|
$content['cf_values'] = $values;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Show sub-type row, and get types
|
|
|
|
if($this->manage_content_types)
|
|
|
|
{
|
2023-04-24 22:04:21 +02:00
|
|
|
$content['cf_type2'] = is_array($content['cf_type2']) ? $content['cf_type2'] : explode(",", $content['cf_type2']);
|
2021-10-09 08:42:02 +02:00
|
|
|
if(empty($this->content_types))
|
2016-08-12 11:35:30 +02:00
|
|
|
{
|
|
|
|
$this->content_types = Api\Config::get_content_types($this->appname);
|
|
|
|
}
|
2023-04-24 22:04:21 +02:00
|
|
|
if(empty($this->content_types))
|
2016-08-12 11:35:30 +02:00
|
|
|
{
|
|
|
|
// if you define your default types of your app with the search_link hook, they are available here, if no types were found
|
|
|
|
$this->content_types = (array)Api\Link::get_registry($this->appname, 'default_types');
|
|
|
|
}
|
|
|
|
foreach($this->content_types as $type => $entry)
|
|
|
|
{
|
|
|
|
$this->types2[$type] = is_array($entry) ? $entry['name'] : $entry;
|
|
|
|
}
|
2023-04-24 22:04:21 +02:00
|
|
|
// Make sure there are no invalid types in the value (from deleted types)
|
|
|
|
$content['cf_type2'] = array_intersect($content['cf_type2'], array_keys($this->types2));
|
|
|
|
|
2016-08-12 11:35:30 +02:00
|
|
|
$sel_options['cf_type2'] = $this->types2;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
$content['no_types'] = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Include type-specific value help
|
|
|
|
foreach(self::$type_option_help as $key => $value)
|
|
|
|
{
|
|
|
|
$content['options'][$key] = lang($value);
|
|
|
|
}
|
|
|
|
$content['statustext'] = $content['options'][$content['cf_type']];
|
|
|
|
$content['attributes'] = self::$type_attribute_flags;
|
2022-11-16 22:10:34 +01:00
|
|
|
$exec = static::class == 'admin_customfields' ? 'admin.admin_customfields.edit' : $this->appname . '.' . static::class . '.edit';
|
2016-08-12 11:35:30 +02:00
|
|
|
|
2022-11-16 22:10:34 +01:00
|
|
|
$this->tmpl->exec($exec, $content, $sel_options, $readonlys, array(
|
|
|
|
'cf_id' => $cf_id,
|
|
|
|
'cf_app' => $this->appname,
|
|
|
|
'cf_name' => $content['cf_name'],
|
2024-04-03 21:12:31 +02:00
|
|
|
'cf_values' => $content['cf_values'],
|
2016-08-12 11:35:30 +02:00
|
|
|
'use_private' => $this->use_private,
|
2024-04-03 21:12:31 +02:00
|
|
|
), 2);
|
2016-08-12 11:35:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Allow extending apps a change to interfere and add content to support
|
|
|
|
* their custom template. This is called right before etemplate->exec().
|
|
|
|
*/
|
2016-08-15 11:56:32 +02:00
|
|
|
protected function app_index(&$content, &$sel_options, &$readonlys, &$preserve)
|
2016-08-12 11:35:30 +02:00
|
|
|
{
|
2016-08-15 11:56:32 +02:00
|
|
|
unset($content, $sel_options, $readonlys, $preserve); // not used, as this is a stub
|
2016-08-12 11:35:30 +02:00
|
|
|
// This is just a stub.
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get actions / context menu for index
|
|
|
|
*
|
|
|
|
* Changes here, require to log out, as $content['nm'] get stored in session!
|
|
|
|
*
|
|
|
|
* @return array see nextmatch_widget::egw_actions()
|
|
|
|
*/
|
|
|
|
protected function get_actions()
|
|
|
|
{
|
2020-06-25 21:06:20 +02:00
|
|
|
$edit = $this->appname . '.' . get_class($this) . '.edit';
|
2016-08-12 11:35:30 +02:00
|
|
|
$actions = array(
|
|
|
|
'open' => array( // does edit if allowed, otherwise view
|
|
|
|
'caption' => 'Open',
|
|
|
|
'default' => true,
|
|
|
|
'allowOnMultiple' => false,
|
2020-06-25 21:06:20 +02:00
|
|
|
'url' => 'menuaction='.$edit.'&cf_id=$id&use_private='.$this->use_private,
|
2016-08-12 11:35:30 +02:00
|
|
|
'popup' => '500x380',
|
|
|
|
'group' => $group=1,
|
|
|
|
'disableClass' => 'th',
|
|
|
|
),
|
2024-03-27 17:10:34 +01:00
|
|
|
'copy' => array(
|
|
|
|
'caption' => 'Copy',
|
|
|
|
'allowOnMultiple' => false,
|
|
|
|
'url' => 'menuaction='.$edit.'&cf_id=$id&use_private='.$this->use_private.'&action=copy',
|
|
|
|
'popup' => '500x380',
|
|
|
|
'group' => $group,
|
|
|
|
),
|
2016-08-12 11:35:30 +02:00
|
|
|
'add' => array(
|
|
|
|
'caption' => 'Add',
|
2020-06-25 21:06:20 +02:00
|
|
|
'url' => 'menuaction='.$edit.'&appname='.$this->appname.'&use_private='.$this->use_private,
|
2016-08-12 11:35:30 +02:00
|
|
|
'popup' => '500x380',
|
|
|
|
'group' => $group,
|
|
|
|
),
|
|
|
|
'delete' => array(
|
|
|
|
'caption' => 'Delete',
|
|
|
|
'confirm' => 'Delete this entry',
|
|
|
|
'confirm_multiple' => 'Delete these entries',
|
2019-05-15 00:43:15 +02:00
|
|
|
'policy_confirmation' => 'Oh yeah',
|
2016-08-12 11:35:30 +02:00
|
|
|
'group' => ++$group,
|
|
|
|
'disableClass' => 'rowNoDelete',
|
|
|
|
),
|
|
|
|
);
|
|
|
|
return $actions;
|
|
|
|
}
|
|
|
|
|
|
|
|
function update(&$content)
|
|
|
|
{
|
|
|
|
$this->content_types[$this->content_type]['options'] = $content['content_type_options'];
|
|
|
|
// save changes to repository
|
|
|
|
$this->save_repository();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* deletes custom field from customfield definitions
|
|
|
|
*/
|
|
|
|
function delete_field(&$content)
|
|
|
|
{
|
|
|
|
unset($this->fields[key($content['fields']['delete'])]);
|
|
|
|
// save changes to repository
|
|
|
|
$this->save_repository();
|
|
|
|
}
|
|
|
|
|
|
|
|
function delete_content_type(&$content)
|
|
|
|
{
|
2019-05-22 17:35:01 +02:00
|
|
|
$old = array('types' => $this->content_types);
|
2016-08-12 11:35:30 +02:00
|
|
|
unset($this->content_types[$content['content_types']['types']]);
|
|
|
|
unset($this->status[$content['content_types']['types']]);
|
2019-05-22 17:35:01 +02:00
|
|
|
$cmd = new admin_cmd_config($this->appname,array('types' => $this->content_types), $old, $content['admin_cmd']);
|
|
|
|
$cmd->run();
|
|
|
|
|
2016-08-12 11:35:30 +02:00
|
|
|
// save changes to repository
|
|
|
|
$this->save_repository();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* create a new custom field
|
|
|
|
*/
|
|
|
|
function create_field(&$content)
|
|
|
|
{
|
2021-10-09 08:42:02 +02:00
|
|
|
$new_name = trim($content['fields'][count((array)$content['fields'])-1]['name']);
|
2016-08-12 11:35:30 +02:00
|
|
|
if (empty($new_name) || isset($this->fields[$new_name]))
|
|
|
|
{
|
|
|
|
$content['error_msg'] .= empty($new_name) ?
|
|
|
|
lang('You have to enter a name, to create a new field!!!') :
|
|
|
|
lang("Field '%1' already exists !!!",$new_name);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2021-10-09 08:42:02 +02:00
|
|
|
$this->fields[$new_name] = $content['fields'][count((array)$content['fields'])-1];
|
2016-08-12 11:35:30 +02:00
|
|
|
if(!$this->fields[$new_name]['label']) $this->fields[$new_name]['label'] = $this->fields[$new_name]['name'];
|
|
|
|
$this->save_repository();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validate and create a new content type
|
|
|
|
*
|
|
|
|
* @param array $content
|
|
|
|
* @return string|boolean New type ID, or false for error
|
|
|
|
*/
|
|
|
|
function create_content_type(&$content)
|
|
|
|
{
|
|
|
|
$new_name = trim($content['content_types']['name']);
|
|
|
|
$new_type = false;
|
|
|
|
if (empty($new_name))
|
|
|
|
{
|
2019-05-23 19:25:06 +02:00
|
|
|
$this->tmpl->set_validation_error('content_types[name]',lang('you have to enter a name, to create a new type!'));
|
2016-08-12 11:35:30 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
foreach($this->content_types as $type)
|
|
|
|
{
|
|
|
|
if($type['name'] == $new_name)
|
|
|
|
{
|
|
|
|
$this->tmpl->set_validation_error('content_types[name]',lang("type '%1' already exists !!!",$new_name));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// search free type character
|
|
|
|
for($i=97;$i<=122;$i++)
|
|
|
|
{
|
|
|
|
if (!$this->content_types[chr($i)] &&
|
|
|
|
// skip letter of deleted type for addressbook content-types, as it gives SQL error
|
|
|
|
// content-type are lowercase, Api\Contacts::DELETED_TYPE === 'D', but DB is case-insensitive
|
|
|
|
($this->appname !== 'addressbook' || chr($i) !== strtolower(Api\Contacts::DELETED_TYPE)))
|
|
|
|
{
|
|
|
|
$new_type = chr($i);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$this->content_types[$new_type] = array('name' => $new_name);
|
|
|
|
$this->save_repository();
|
|
|
|
}
|
|
|
|
return $new_type;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* save changes to repository
|
|
|
|
*/
|
|
|
|
function save_repository()
|
|
|
|
{
|
|
|
|
//echo '<p>uicustomfields::save_repository() \$this->fields=<pre style="text-aling: left;">'; print_r($this->fields); echo "</pre>\n";
|
|
|
|
$config = new Api\Config($this->appname);
|
|
|
|
$config->read_repository();
|
|
|
|
$config->value('types',$this->content_types);
|
|
|
|
$config->save_repository();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* get customfields of using application
|
|
|
|
*
|
|
|
|
* @deprecated use Api\Storage\Customfields::get() direct, no need to instanciate this UI class
|
|
|
|
* @author Cornelius Weiss
|
|
|
|
* @param boolean $all_private_too =false should all the private fields be returned too
|
|
|
|
* @return array with customfields
|
|
|
|
*/
|
|
|
|
function get_customfields($all_private_too=false)
|
|
|
|
{
|
|
|
|
return Api\Storage\Customfields::get($this->appname,$all_private_too);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* get_content_types of using application
|
|
|
|
*
|
|
|
|
* @deprecated use Api\Config::get_content_types() direct, no need to instanciate this UI class
|
|
|
|
* @author Cornelius Weiss
|
|
|
|
* @return array with content-types
|
|
|
|
*/
|
|
|
|
function get_content_types()
|
|
|
|
{
|
|
|
|
return Api\Config::get_content_types($this->appname);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get list of customfields for the nextmatch
|
|
|
|
*/
|
|
|
|
public function get_rows(&$query, &$rows, &$readonlys)
|
|
|
|
{
|
|
|
|
$rows = array();
|
|
|
|
|
|
|
|
$query['col_filter']['cf_app'] = $query['appname'];
|
|
|
|
$total = $this->so->get_rows($query, $rows, $readonlys);
|
|
|
|
unset($query['col_filter']['cf_app']);
|
|
|
|
|
|
|
|
foreach($rows as &$row)
|
|
|
|
{
|
2024-03-30 10:18:28 +01:00
|
|
|
$row['cf_values'] = json_decode($row['cf_values'], true) ?? $row['cf_values'];
|
2016-08-12 11:35:30 +02:00
|
|
|
if (is_array($row['cf_values']))
|
|
|
|
{
|
|
|
|
$values = '';
|
|
|
|
foreach($row['cf_values'] as $var => $value)
|
|
|
|
{
|
|
|
|
$values .= (!empty($values) ? "\n" : '').$var.'='.$value;
|
|
|
|
}
|
|
|
|
$row['cf_values'] = $values;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $total;
|
|
|
|
}
|
2023-03-07 08:18:58 +01:00
|
|
|
}
|