Issues (4868)

admin/inc/class.admin_customfields.inc.php (12 issues)

1
<?php
2
/**
3
 * EGgroupware admin - UI for adding custom fields
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
7
 * @author Cornelius Weiss <nelius-AT-von-und-zu-weiss.de>
8
 * @package admin
9
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10
 * @version $Id$
11
 */
12
13
use EGroupware\Api;
14
use EGroupware\Api\Framework;
15
use EGroupware\Api\Etemplate;
16
17
/**
18
 * Customfields class -  manages customfield definitions in egw_config table
19
 *
20
 * The repository name (config_name) is 'customfields'.
21
 *
22
 * Applications can have customfields by sub-type by having a template
23
 * named '<appname>.admin.types'.  See admin.customfields.types as an
24
 * example, but the template can even be empty if types are handled by the
25
 * application in another way.
26
 *
27
 * Applications can extend this class to customize the custom fields and handle
28
 * extra information from the above template by extending and implementing
29
 * update() and app_index().
30
 */
31
class admin_customfields
32
{
33
34
	/**
35
	* appname of app which want to add / edit its customfields
36
	*
37
	* @var string
38
	*/
39
	var $appname;
40
41
	/**
42
	 * Allow custom fields to be restricted to certain users/groups
43
	 */
44
	protected $use_private = false;
45
46
	/**
47
	* userdefiened types e.g. type of infolog
48
	*
49
	* @var array
50
	*/
51
	var $types2 = array();
52
	var $content_types,$fields;
53
54
	/**
55
	 * Does App uses content-types
56
	 *
57
	 * @var boolean
58
	 */
59
	protected $manage_content_types = false;
60
61
	/**
62
	 * Currently selected content type (if used by app)
63
	 * @var string
64
	 */
65
	protected $content_type = null;
66
67
	var $public_functions = array(
68
		'index' => true,
69
		'edit' => True
70
	);
71
	/**
72
	 * Instance of etemplate class
73
	 *
74
	 * @var etemplate
75
	 */
76
	var $tmpl;
77
78
	/**
79
	 * @var Description of the options or value format for each cf_type
0 ignored issues
show
The type Description was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
80
	 */
81
	public static $type_option_help = array(
82
		'search'	=> 'set get_rows, get_title and id_field, or use @path to read options from a file in EGroupware directory',
83
		'select'	=> 'each value is a line like id[=label], or use @path to read options from a file in EGroupware directory',
84
		'radio'		=> 'each value is a line like id[=label], or use @path to read options from a file in EGroupware directory',
85
		'button'	=> 'each value is a line like label=[javascript]'
86
	);
87
88
	/**
89
	 * Custom fields can also have length and rows set, but these are't used for all types
90
	 * If not set to true here, the field will be disabled when selecting the type
91
	 */
92
	public static $type_attribute_flags = array(
93
		'text'		=> array('cf_len' => true, 'cf_rows' => true),
94
		'float'		=> array('cf_len' => true),
95
		'label'		=> array('cf_values' => true),
96
		'select'	=> array('cf_len' => false, 'cf_rows' => true, 'cf_values' => true),
97
		'date'		=> array('cf_len' => true, 'cf_rows' => false, 'cf_values' => true),
98
		'date-time'	=> array('cf_len' => true, 'cf_rows' => false, 'cf_values' => true),
99
		'select-account'	=> array('cf_len' => false, 'cf_rows' => true),
100
		'htmlarea'	=> array('cf_len' => true, 'cf_rows' => true),
101
		'button'	=> array('cf_values' => true),
102
		'ajax_select' => array('cf_values' => true),
103
		'radio'		=> array('cf_values' => true),
104
		'checkbox'	=> array('cf_values' => true),
105
		'filemanager' => array('cf_values' => true),
106
	);
107
108
	/**
109
	 * Constructor
110
	 *
111
	 * @param string $appname
112
	 */
113
	function __construct($appname='')
114
	{
115
		if (($this->appname = $appname))
116
		{
117
			$this->fields = Api\Storage\Customfields::get($this->appname,true);
118
			$this->content_types = Api\Config::get_content_types($this->appname);
119
		}
120
		$this->so = new Api\Storage\Base('phpgwapi','egw_customfields',null,'',true);
0 ignored issues
show
Bug Best Practice introduced by
The property so does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
121
	}
122
123
	/**
124
	 * List custom fields
125
	 */
126
	public function index($content = array())
127
	{
128
		// determine appname
129
		$this->appname = $this->appname ? $this->appname : ($_GET['appname'] ? $_GET['appname'] : ($content['appname'] ? $content['appname'] : false));
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->appname ? $this->...tent['appname'] : false can also be of type false. However, the property $appname is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
130
		if(!$this->appname) die(lang('Error! No appname found'));
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
131
132
		$this->use_private = !isset($_GET['use_private']) || (boolean)$_GET['use_private'] || $content['use_private'];
133
134
		// Read fields, constructor doesn't always know appname
135
		$this->fields = Api\Storage\Customfields::get($this->appname,true);
136
137
		$this->tmpl = new Etemplate();
138
		$this->tmpl->read('admin.customfields');
139
140
		// do we manage content-types?
141
		$test = new Etemplate();
142
		if($test->read($this->appname.'.admin.types')) $this->manage_content_types = true;
143
144
		// Handle incoming - types, options, etc.
145
		if($this->manage_content_types)
146
		{
147
			if(count($this->content_types) == 0)
148
			{
149
				$this->content_types = Api\Config::get_content_types($this->appname);
150
			}
151
			if (count($this->content_types)==0)
152
			{
153
				// if you define your default types of your app with the search_link hook, they are available here, if no types were found
154
				$this->content_types = (array)Api\Link::get_registry($this->appname,'default_types');
155
			}
156
			// Set this now, we need to know it for updates
157
			$this->content_type = $content['content_types']['types'] ? $content['content_types']['types'] : (array_key_exists(0,$this->content_types) ? $this->content_types[0] : key($this->content_types));
158
159
			// Common type changes - add, delete
160
			if($content['content_types']['delete'])
161
			{
162
				$this->delete_content_type($content);
163
			}
164
			elseif($content['content_types']['create'])
165
			{
166
				if(($new_type = $this->create_content_type($content)))
167
				{
168
					$content['content_types']['types'] = $this->content_type = $new_type;
169
				}
170
				unset($content['content_types']['create']);
171
				unset($content['content_types']['name']);
172
			}
173
			// No common type change and type didn't change, try an update to check new type statuses
174
			elseif($this->content_type && is_array($content) && $this->content_type == $content['old_content_type'])
175
			{
176
				$this->update($content);
177
			}
178
		}
179
180
		// Custom field deleted from nextmatch
181
		if($content['nm']['action'] == 'delete')
182
		{
183
			foreach($this->fields as $name => $data)
184
			{
185
				if(in_array($data['id'],$content['nm']['selected']))
186
				{
187
					$cmd = new admin_cmd_customfield(
188
							$this->appname,
189
							array('id' => $data['id'],'name' => $name),
190
							null,
191
							$content['nm']['admin_cmd']
192
					);
193
					$cmd->run();
194
					unset($this->fields[$name]);
195
196
					Framework::refresh_opener('Deleted', 'admin', $data['id'] /* Conflicts with Api\Accounts 'delete'*/);
197
				}
198
			}
199
		}
200
201
		$content['nm']= Api\Cache::getSession('admin', 'customfield-index');
202
		if (!is_array($content['nm']))
203
		{
204
			// Initialize nextmatch
205
			$content['nm'] = array(
206
				'get_rows'       =>	'admin.admin_customfields.get_rows',
207
				'no_cat'         => 'true',
208
				'no_filter'      => 'true',
209
				'no_filter2'     => 'true',
210
				'row_id'         => 'cf_id',
211
				'order'          =>	'cf_order',// IO name of the column to sort
212
				'sort'           =>	'ASC',// IO direction of the sort: 'ASC' or 'DESC'
213
				'actions'        => $this->get_actions()
214
			);
215
		}
216
		$content['nm']['appname'] = $this->appname;
217
		$content['nm']['use_private'] = $this->use_private;
218
219
		// Set up sub-types
220
		if($this->manage_content_types)
221
		{
222
			foreach($this->content_types as $type => $entry)
223
			{
224
				if(!is_array($entry))
225
				{
226
					$this->content_types[$type] = array('name' => $entry);
227
					$entry = $this->content_types[$type];
228
				}
229
				$this->types2[$type] = $entry['name'];
230
			}
231
			$sel_options['types'] = $sel_options['cf_type2'] = $this->types2;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$sel_options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $sel_options = array(); before regardless.
Loading history...
232
233
			$content['type_template'] = $this->appname . '.admin.types';
234
			$content['content_types']['appname'] = $this->appname;
235
236
			$content['content_type_options'] = $this->content_types[$this->content_type]['options'];
237
			$content['content_type_options']['type'] = $this->types2[$this->content_type];
238
			if ($this->content_types[$this->content_type]['non_deletable'])
239
			{
240
				$content['content_types']['non_deletable'] = true;
241
			}
242
			if ($this->content_types['']['no_add'])
243
			{
244
				$content['content_types']['no_add'] = true;
245
			}
246
			if ($content['content_types']['non_deletable'] && $content['content_types']['no_add'])
247
			{
248
				// Hide the whole line if you can't add or delete
249
				$content['content_types']['no_edit_types'] = true;
250
			}
251
			// do NOT allow to delete original contact content-type for addressbook,
252
			// as it only creates support problems as users incidently delete it
253
			if ($this->appname == 'addressbook' && $this->content_type == 'n')
254
			{
255
				$readonlys['content_types']['delete'] = true;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$readonlys was never initialized. Although not strictly required by PHP, it is generally a good practice to add $readonlys = array(); before regardless.
Loading history...
256
			}
257
			$content['nm']['type2'] = true;
258
		}
259
		else
260
		{
261
			// Disable content types
262
			$this->tmpl->disableElement('content_types', true);
263
		}
264
		$preserve = array(
265
			'appname' => $this->appname,
266
			'use_private' => $this->use_private,
267
			'old_content_type' => $this->content_type
268
		);
269
270
		// Allow extending app a change to change content before display
271
		$readonlys = null;
272
		static::app_index($content, $sel_options, $readonlys, $preserve);
0 ignored issues
show
Bug Best Practice introduced by
The method admin_customfields::app_index() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

272
		static::/** @scrutinizer ignore-call */ 
273
          app_index($content, $sel_options, $readonlys, $preserve);
Loading history...
273
274
		// Make sure app css & lang get loaded, extending app might cause et2 to miss it
275
		Framework::includeCSS('admin','app');
276
		Api\Translation::add_app('admin');
277
278
		// Set app to admin to make sure actions are correctly loaded into admin
279
		$GLOBALS['egw_info']['flags']['currentapp'] = 'admin';
280
		$GLOBALS['egw_info']['flags']['app_header'] = $GLOBALS['egw_info']['apps'][$this->appname]['title'].' - '.lang('Custom fields');
281
282
		// Some logic to make sure extending class (if there is one) gets called
283
		// when etemplate2 comes back instead of parent class
284
		$exec = get_class() == get_called_class() || get_called_class() == 'customfields' ?
285
			'admin.admin_customfields.index' : $this->appname . '.' . get_called_class() . '.index';
286
287
		$this->tmpl->exec($exec,$content,$sel_options,$readonlys,$preserve);
288
	}
289
290
	/**
291
	 * Delete a type over ajax.
292
	 *
293
	 * Used when Policy is involved, otherwise things go normally
294
	 *
295
	 * @param array $content
296
	 * @param string $etemplate_exec_id to check against CSRF
297
	 */
298
	public function ajax_delete_type($content, $etemplate_exec_id)
299
	{
300
		Api\Etemplate\Request::csrfCheck($etemplate_exec_id, __METHOD__, func_get_args());
301
302
		// Read fields
303
		$this->appname = $content['appname'];
304
		$this->fields = Api\Storage\Customfields::get($content['appname'],true);
305
		$this->content_types = Api\Config::get_content_types($content['appname']);
306
		$this->delete_content_type($content);
307
	}
308
309
	/**
310
	 * Check selectbox values to match regular expression in et2_widget_selectbox.js: _is_multiple_regexp
311
	 *
312
	 * If values do not match, comma-separated values are not split by comma!
313
	 */
314
	const CHECK_MULTISELCT_VALUE = '/^[0-9A-Za-z\/_ -]+$/';
315
316
	/**
317
	 * Edit/Create Custom fields with type
318
	 *
319
	 * @author Ralf Becker <ralfbecker-AT-outdoor-training.de>
320
	 * @param array $content Content from the eTemplate Exec
321
	 */
322
	function edit($content = null)
323
	{
324
		$cf_id = $_GET['cf_id'] ? (int)$_GET['cf_id'] : (int)$content['cf_id'];
325
326
		// determine appname
327
		$this->appname = $this->appname ? $this->appname : ($_GET['appname'] ? $_GET['appname'] : ($content['cf_app'] ? $content['cf_app'] : false));
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->appname ? $this->...ntent['cf_app'] : false can also be of type false. However, the property $appname is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
328
		if(!$this->appname)
329
		{
330
			if($cf_id && $this->so)
331
			{
332
				$content = $this->so->read($cf_id);
333
				$this->appname = $content['cf_app'];
334
			}
335
		}
336
		if(!$this->appname)
337
		{
338
			die(lang('Error! No appname found'));
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
339
		}
340
		$this->use_private = !isset($_GET['use_private']) || (boolean)$_GET['use_private'] || $content['use_private'];
341
342
		// Read fields, constructor doesn't always know appname
343
		$this->fields = Api\Storage\Customfields::get($this->appname,true);
344
345
		// Update based on info returned from template
346
		if (is_array($content))
347
		{
348
			$action = @key($content['button']);
349
			switch($action)
350
			{
351
				case 'delete':
352
					$field = $this->so->read($cf_id);
353
					$cmd = new admin_cmd_customfield($this->appname, array('id' => $cf_id,'name' => $field['cf_name']));
354
					$cmd->run();
355
					Framework::refresh_opener('Deleted', 'admin', $cf_id /* Conflicts with Api\Accounts 'delete'*/);
356
					Framework::window_close();
357
					break;
358
				case 'save':
359
				case 'apply':
360
					if(!$cf_id && $this->fields[$content['cf_name']])
361
					{
362
						Framework::message(lang("Field '%1' already exists !!!",$content['cf_name']),'error');
0 ignored issues
show
The call to lang() has too many arguments starting with $content['cf_name']. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

362
						Framework::message(/** @scrutinizer ignore-call */ lang("Field '%1' already exists !!!",$content['cf_name']),'error');

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
363
						$content['cf_name'] = '';
364
						break;
365
					}
366
					if(empty($content['cf_label']))
367
					{
368
						$content['cf_label'] = $content['cf_name'];
369
					}
370
					if (!empty($content['cf_values']))
371
					{
372
						$values = array();
373
						if($content['cf_values'][0] === '@')
374
						{
375
							$values['@'] = substr($content['cf_values'], $content['cf_values'][1] === '=' ? 2:1);
376
						}
377
						else
378
						{
379
							foreach(explode("\n",trim($content['cf_values'])) as $idx => $line)
380
							{
381
								list($var_raw,$value) = explode('=',trim($line),2);
382
								$var = trim($var_raw);
383
								if (!preg_match(self::CHECK_MULTISELCT_VALUE, $var) && !($idx == 0 && !$var && $value))
384
								{
385
									Api\Etemplate::set_validation_error('cf_values',
386
										lang('Invalid value "%1", use only:', $var)."\n".
387
											preg_replace('/^.*\[([^]]+)\].*$/', '$1', self::CHECK_MULTISELCT_VALUE));
388
									$action = 'apply';	// do not close the window to show validation error
389
									if (!$cf_id) break 2;	// only stop storing of new CFs
390
								}
391
								$values[$var] = trim($value)==='' ? $var : $value;
392
							}
393
						}
394
						$content['cf_values'] = $values;
395
					}
396
					$update_content = array();
397
					foreach($content as $key => $value)
398
					{
399
						if(substr($key,0,3) == 'cf_')
400
						{
401
							$update_content[substr($key,3)] = $value;
402
						}
403
					}
404
					$cmd = new admin_cmd_customfield($this->appname, $update_content,null,$content['admin_cmd']);
405
					$cmd->run();
406
					if(!$cf_id)
407
					{
408
						$this->fields = Api\Storage\Customfields::get($this->appname,true);
409
						$cf_id = (int)$this->fields[$content['cf_name']]['id'];
410
					}
411
					Framework::refresh_opener(lang('Entry saved'), 'admin', $cf_id, 'edit');
412
					if ($action != 'save')
413
					{
414
						break;
415
					}
416
				//fall through
417
				case 'cancel':
418
					Framework::window_close();
419
			}
420
		}
421
		else
422
		{
423
			$content['use_private'] = !isset($_GET['use_private']) || (boolean)$_GET['use_private'];
424
		}
425
426
427
		// do we manage content-types?
428
		$test = new Etemplate();
429
		if($test->read($this->appname.'.admin.types')) $this->manage_content_types = true;
430
431
		$this->tmpl = new Etemplate();
432
		$this->tmpl->read('admin.customfield_edit');
433
434
		Api\Translation::add_app('infolog');	// til we move the translations
435
436
		$GLOBALS['egw_info']['flags']['app_header'] = $GLOBALS['egw_info']['apps'][$this->appname]['title'].' - '.lang('Custom fields');
437
		$sel_options = array();
438
		$readonlys = array();
439
440
		//echo 'customfields=<pre style="text-align: left;">'; print_r($this->fields); echo "</pre>\n";
441
		$content['cf_order'] = (count($this->fields)+1) * 10;
442
		$content['use_private'] = $this->use_private;
443
444
		if($cf_id)
445
		{
446
			$content = array_merge($content, $this->so->read($cf_id));
447
			$this->appname = $content['cf_app'];
448
			if($content['cf_private'])
449
			{
450
				$content['cf_private'] = explode(',',$content['cf_private']);
451
			}
452
			if($content['cf_name'])
453
			{
454
				$readonlys['cf_name'] = true;
455
			}
456
			$content['cf_values'] = json_decode($content['cf_values'], true);
457
		}
458
		else
459
		{
460
			$readonlys['button[delete]'] = true;
461
		}
462
		if (is_array($content['cf_values']))
463
		{
464
			$values = '';
465
			foreach($content['cf_values'] as $var => $value)
466
			{
467
				$values .= (!empty($values) ? "\n" : '').$var.'='.$value;
468
			}
469
			$content['cf_values'] = $values;
470
		}
471
472
		// Show sub-type row, and get types
473
		if($this->manage_content_types)
474
		{
475
			if(count($this->content_types) == 0)
476
			{
477
				$this->content_types = Api\Config::get_content_types($this->appname);
478
			}
479
			if (count($this->content_types)==0)
480
			{
481
				// if you define your default types of your app with the search_link hook, they are available here, if no types were found
482
				$this->content_types = (array)Api\Link::get_registry($this->appname, 'default_types');
483
			}
484
			foreach($this->content_types as $type => $entry)
485
			{
486
				$this->types2[$type] = is_array($entry) ? $entry['name'] : $entry;
487
			}
488
			$sel_options['cf_type2'] = $this->types2;
489
		}
490
		else
491
		{
492
			$content['no_types'] = true;
493
		}
494
495
		// Include type-specific value help
496
		foreach(self::$type_option_help as $key => $value)
497
		{
498
			$content['options'][$key] = lang($value);
499
		}
500
		$content['statustext'] = $content['options'][$content['cf_type']];
501
		$content['attributes'] = self::$type_attribute_flags;
502
503
		$this->tmpl->exec('admin.admin_customfields.edit',$content,$sel_options,$readonlys,array(
504
			'cf_id' => $cf_id,
505
			'cf_app' => $this->appname,
506
			'cf_name' => $content['cf_name'],
507
			'use_private' => $this->use_private,
508
		),2);
509
	}
510
511
	/**
512
	 * Allow extending apps a change to interfere and add content to support
513
	 * their custom template.  This is called right before etemplate->exec().
514
	 */
515
	protected function app_index(&$content, &$sel_options, &$readonlys, &$preserve)
516
	{
517
		unset($content, $sel_options, $readonlys, $preserve);	// not used, as this is a stub
518
		// This is just a stub.
519
	}
520
521
	/**
522
	 * Get actions / context menu for index
523
	 *
524
	 * Changes here, require to log out, as $content['nm'] get stored in session!
525
	 *
526
	 * @return array see nextmatch_widget::egw_actions()
527
	 */
528
	protected function get_actions()
529
	{
530
		$actions = array(
531
			'open' => array(	// does edit if allowed, otherwise view
532
				'caption' => 'Open',
533
				'default' => true,
534
				'allowOnMultiple' => false,
535
				'url' => 'menuaction=admin.admin_customfields.edit&cf_id=$id&use_private='.$this->use_private,
536
				'popup' => '500x380',
537
				'group' => $group=1,
538
				'disableClass' => 'th',
539
			),
540
			'add' => array(
541
				'caption' => 'Add',
542
				'url' => 'menuaction=admin.admin_customfields.edit&appname='.$this->appname.'&use_private='.$this->use_private,
543
				'popup' => '500x380',
544
				'group' => $group,
545
			),
546
			'delete' => array(
547
				'caption' => 'Delete',
548
				'confirm' => 'Delete this entry',
549
				'confirm_multiple' => 'Delete these entries',
550
				'policy_confirmation' => 'Oh yeah',
551
				'group' => ++$group,
552
				'disableClass' => 'rowNoDelete',
553
			),
554
		);
555
		return $actions;
556
	}
557
558
	function update(&$content)
559
	{
560
		$this->content_types[$this->content_type]['options'] = $content['content_type_options'];
561
		// save changes to repository
562
		$this->save_repository();
563
	}
564
565
	/**
566
	* deletes custom field from customfield definitions
567
	*/
568
	function delete_field(&$content)
569
	{
570
		unset($this->fields[key($content['fields']['delete'])]);
571
		// save changes to repository
572
		$this->save_repository();
573
	}
574
575
	function delete_content_type(&$content)
576
	{
577
		$old = array('types' => $this->content_types);
578
		unset($this->content_types[$content['content_types']['types']]);
579
		unset($this->status[$content['content_types']['types']]);
580
		$cmd = new admin_cmd_config($this->appname,array('types' => $this->content_types), $old, $content['admin_cmd']);
581
		$cmd->run();
582
583
		// save changes to repository
584
		$this->save_repository();
585
	}
586
587
	/**
588
	* create a new custom field
589
	*/
590
	function create_field(&$content)
591
	{
592
		$new_name = trim($content['fields'][count($content['fields'])-1]['name']);
593
		if (empty($new_name) || isset($this->fields[$new_name]))
594
		{
595
			$content['error_msg'] .= empty($new_name) ?
596
				lang('You have to enter a name, to create a new field!!!') :
597
				lang("Field '%1' already exists !!!",$new_name);
0 ignored issues
show
The call to lang() has too many arguments starting with $new_name. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

597
				/** @scrutinizer ignore-call */ 
598
    lang("Field '%1' already exists !!!",$new_name);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
598
		}
599
		else
600
		{
601
			$this->fields[$new_name] = $content['fields'][count($content['fields'])-1];
602
			if(!$this->fields[$new_name]['label']) $this->fields[$new_name]['label'] = $this->fields[$new_name]['name'];
603
			$this->save_repository();
604
		}
605
	}
606
607
	/**
608
	 * Validate and create a new content type
609
	 *
610
	 * @param array $content
611
	 * @return string|boolean New type ID, or false for error
612
	 */
613
	function create_content_type(&$content)
614
	{
615
		$new_name = trim($content['content_types']['name']);
616
		$new_type = false;
617
		if (empty($new_name))
618
		{
619
			$this->tmpl->set_validation_error('content_types[name]',lang('you have to enter a name, to create a new type!'));
620
		}
621
		else
622
		{
623
			foreach($this->content_types as $type)
624
			{
625
				if($type['name'] == $new_name)
626
				{
627
					$this->tmpl->set_validation_error('content_types[name]',lang("type '%1' already exists !!!",$new_name));
0 ignored issues
show
The call to lang() has too many arguments starting with $new_name. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

627
					$this->tmpl->set_validation_error('content_types[name]',/** @scrutinizer ignore-call */ lang("type '%1' already exists !!!",$new_name));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
628
					return false;
629
				}
630
			}
631
			// search free type character
632
			for($i=97;$i<=122;$i++)
633
			{
634
				if (!$this->content_types[chr($i)] &&
635
					// skip letter of deleted type for addressbook content-types, as it gives SQL error
636
					// content-type are lowercase, Api\Contacts::DELETED_TYPE === 'D', but DB is case-insensitive
637
					($this->appname !== 'addressbook' || chr($i) !== strtolower(Api\Contacts::DELETED_TYPE)))
638
				{
639
					$new_type = chr($i);
640
					break;
641
				}
642
			}
643
			$this->content_types[$new_type] = array('name' => $new_name);
644
			$this->save_repository();
645
		}
646
		return $new_type;
647
	}
648
649
	/**
650
	* save changes to repository
651
	*/
652
	function save_repository()
653
	{
654
		//echo '<p>uicustomfields::save_repository() \$this->fields=<pre style="text-aling: left;">'; print_r($this->fields); echo "</pre>\n";
655
		$config = new Api\Config($this->appname);
656
		$config->read_repository();
657
		$config->value('types',$this->content_types);
658
		$config->save_repository();
659
	}
660
661
	/**
662
	* get customfields of using application
663
	*
664
	* @deprecated use Api\Storage\Customfields::get() direct, no need to instanciate this UI class
665
	* @author Cornelius Weiss
666
	* @param boolean $all_private_too =false should all the private fields be returned too
667
	* @return array with customfields
668
	*/
669
	function get_customfields($all_private_too=false)
670
	{
671
		return Api\Storage\Customfields::get($this->appname,$all_private_too);
672
	}
673
674
	/**
675
	* get_content_types of using application
676
	*
677
	* @deprecated use Api\Config::get_content_types() direct, no need to instanciate this UI class
678
	* @author Cornelius Weiss
679
	* @return array with content-types
680
	*/
681
	function get_content_types()
682
	{
683
		return Api\Config::get_content_types($this->appname);
684
	}
685
686
	/**
687
	 * Get list of customfields for the nextmatch
688
	 */
689
	public function get_rows(&$query, &$rows, &$readonlys)
690
	{
691
		$rows = array();
692
693
		$query['col_filter']['cf_app'] = $query['appname'];
694
		$total = $this->so->get_rows($query, $rows, $readonlys);
695
		unset($query['col_filter']['cf_app']);
696
697
		foreach($rows as &$row)
698
		{
699
			$row['cf_values'] = json_decode($row['cf_values'], true);
700
			if (is_array($row['cf_values']))
701
			{
702
				$values = '';
703
				foreach($row['cf_values'] as $var => $value)
704
				{
705
					$values .= (!empty($values) ? "\n" : '').$var.'='.$value;
706
				}
707
				$row['cf_values'] = $values;
708
			}
709
		}
710
		return $total;
711
	}
712
}
713