Issues (4868)

api/src/Etemplate/Widget/Customfields.php (1 issue)

1
<?php
2
/**
3
 * EGroupware - eTemplate custom fields widget
4
 *
5
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
6
 * @package api
7
 * @subpackage etemplate
8
 * @link http://www.egroupware.org
9
 * @author Nathan Gray
10
 * @copyright 2011 Nathan Gray
11
 * @version $Id$
12
 */
13
14
namespace EGroupware\Api\Etemplate\Widget;
15
16
use EGroupware\Api;
17
18
/**
19
 * Widgets for custom fields and listing custom fields
20
 */
21
class Customfields extends Transformer
22
{
23
24
	/**
25
	 * Allowd types of customfields
26
	 *
27
	 * The additionally allowed app-names from the link-class, will be add by the edit-method only,
28
	 * as the link-class has to be called, which can NOT be instanciated by the constructor, as
29
	 * we get a loop in the instanciation.
30
	 *
31
	 * @var array
32
	 */
33
	protected static $cf_types = array(
34
		'text'     => 'Text',
35
		'int'      => 'Integer',
36
		'float'    => 'Float',
37
		'label'    => 'Label',
38
		'select'   => 'Selectbox',
39
		'ajax_select' => 'Search',
40
		'radio'    => 'Radiobutton',
41
		'checkbox' => 'Checkbox',
42
		'date'     => 'Date',
43
		'date-time'=> 'Date+Time',
44
		'select-account' => 'Select account',
45
		'button'   => 'Button',         // button to execute javascript
46
		'url'      => 'Url',
47
		'url-email'=> 'EMail',
48
		'url-phone'=> 'Phone number',
49
		'htmlarea' => 'Formatted Text (HTML)',
50
		'link-entry' => 'Select entry',         // should be last type, as the individual apps get added behind
51
	);
52
53
	/**
54
	 * @var $prefix string Prefix for every custiomfield name returned in $content (# for general (admin) customfields)
55
	 */
56
	protected static $prefix = '#';
57
58
	// Make settings available globally
59
	const GLOBAL_VALS = '~custom_fields~';
60
61
	// Used if there's no ID provided
62
	const GLOBAL_ID = 'custom_fields';
63
64
	protected $legacy_options = 'sub-type,use-private,field-names';
65
66
	protected static $transformation = array(
67
		'type' => array(
68
			'customfields-types' => array(
69
				'type'	=>	'select',
70
				'sel_options'	=> array()
71
			),
72
			'customfields-list' => array(
73
				'readonly'	=> true
74
			)
75
		)
76
	);
77
78
	public function __construct($xml)
79
	{
80
		parent::__construct($xml);
81
	}
82
83
	/**
84
	 * Fill type options in self::$request->sel_options to be used on the client
85
	 *
86
	 * @param string $cname
87
	 * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
88
	 */
89
	public function beforeSendToClient($cname, array $expand=null)
90
	{
91
		// No name, no way to get parameters client-side.
92
		if(!$this->id) $this->id = self::GLOBAL_ID;
93
94
		$form_name = self::form_name($cname, $this->id, $expand);
95
96
		// Store properties at top level, so all customfield widgets can share
97
		if($this->attrs['app'])
98
		{
99
			$app = $this->attrs['app'];
100
		}
101
		else
102
		{
103
			$app =& $this->getElementAttribute(self::GLOBAL_VALS, 'app');
104
			if($this->getElementAttribute($form_name, 'app'))
105
			{
106
				$app =& $this->getElementAttribute($form_name, 'app');
107
			}
108
			else
109
			{
110
				// Checking creates it even if it wasn't there
111
				unset(self::$request->modifications[$form_name]['app']);
112
			}
113
		}
114
115
		if($this->getElementAttribute($form_name, 'customfields'))
116
		{
117
			$customfields =& $this->getElementAttribute($form_name, 'customfields');
118
		}
119
		elseif($app)
120
		{
121
			// Checking creates it even if it wasn't there
122
			unset(self::$request->modifications[$form_name]['customfields']);
123
			$customfields =& $this->getElementAttribute(self::GLOBAL_VALS, 'customfields');
124
		}
125
126
		if(!$app)
127
		{
128
			$app =& $this->setElementAttribute(self::GLOBAL_VALS, 'app', $GLOBALS['egw_info']['flags']['currentapp']);
129
			if ($this->attrs['sub-app']) $app .= '-'.$this->attrs['sub-app'];
130
			$customfields =& $this->setElementAttribute(self::GLOBAL_VALS, 'customfields', Api\Storage\Customfields::get($app));
131
		}
132
133
		// 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
134
		if ($app && $app != 'stylite' && $app != $GLOBALS['egw_info']['flags']['currentapp'] && !isset($customfields) &&
135
			($GLOBALS['egw_info']['flags']['currentapp'] == 'etemplate' || !$this->attrs['customfields']) || !isset($customfields))
136
		{
137
			// app changed
138
			$customfields =& Api\Storage\Customfields::get($app);
139
		}
140
		// Filter fields
141
		if($this->attrs['field-names'])
142
		{
143
			$fields_name = explode(',', $this->attrs['field-names']);
144
			foreach($fields_name as &$f)
145
			{
146
				if ($f[0] == "!")
147
				{
148
					$f= substr($f,1);
149
					$negate_fields[]= $f;
150
				}
151
				$field_filters []= $f;
152
			}
153
		}
154
155
		$fields = $customfields;
156
157
		$use_private = self::expand_name($this->attrs['use-private'],0,0,'','',self::$cont);
158
		$this->attrs['sub-type'] = self::expand_name($this->attrs['sub-type'],0,0,'','',self::$cont);
159
160
		foreach((array)$fields as $key => $field)
161
		{
162
			// remove private or non-private cf's, if only one kind should be displayed
163
			if ((string)$use_private !== '' && (boolean)$field['private'] != (boolean)$use_private)
164
			{
165
				unset($fields[$key]);
166
			}
167
168
			// Remove filtered fields
169
			if($field_filters && in_array($key, $negate_fields) && in_array($key, $field_filters))
170
			{
171
				unset($fields[$key]);
172
			}
173
174
			// Rmove fields for none private cutomfields when name refers to a single custom field
175
			$matches = null;
176
			if (($pos=strpos($form_name,self::$prefix)) !== false &&
177
			preg_match($preg = '/'.self::$prefix.'([^\]]+)/',$form_name,$matches) && !isset($fields[$name=$matches[1]]))
178
			{
179
				unset($fields[$key]);
180
			}
181
		}
182
		// check if name refers to a single custom field --> show only that
183
		$matches = null;
184
		if (($pos=strpos($form_name,self::$prefix)) !== false && // allow the prefixed name to be an array index too
185
			preg_match($preg = '/'.self::$prefix.'([^\]]+)/',$form_name,$matches) && isset($fields[$name=$matches[1]]))
186
		{
187
			$fields = array($name => $fields[$name]);
188
			$value = array(self::$prefix.$name => $value);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $value seems to be never defined.
Loading history...
189
			$form_name = self::$prefix.$name;
190
		}
191
192
		if(!is_array($fields)) $fields = array();
193
		switch($type = $this->type)
194
		{
195
			case 'customfields-types':
196
				foreach(self::$cf_types as $lname => $label)
197
				{
198
					$sel_options[$lname] = lang($label);
199
					$fields_with_vals[]=$lname;
200
				}
201
				$link_types = array_intersect_key(Api\Link::app_list('query'), Api\Link::app_list('title'));
202
				// Explicitly add in filemanager, which does not support query or title
203
				$link_types['filemanager'] = lang('filemanager');
204
205
				ksort($link_types);
206
				foreach($link_types as $lname => $label)
207
				{
208
					$sel_options[$lname] = '- '.$label;
209
				}
210
				self::$transformation['type'][$type]['sel_options'] = $sel_options;
211
				self::$transformation['type'][$type]['no_lang'] = true;
212
				return parent::beforeSendToClient($cname, $expand);
213
			case 'customfields-list':
214
				foreach(array_reverse($fields) as $lname => $field)
215
				{
216
					if (!empty($this->attrs['sub-type']) && !empty($field['type2']) &&
217
						strpos(','.$field['type2'].',',','.$field['type2'].',') === false) continue;    // not for our content type//
218
					if (isset($value[self::$prefix.$lname]) && $value[self::$prefix.$lname] !== '') //break;
219
					{
220
						$fields_with_vals[]=$lname;
221
					}
222
					//$stop_at_field = $name;
223
				}
224
				break;
225
			default:
226
				foreach(array_reverse($fields) as $lname => $field)
227
				{
228
					$fields_with_vals[]=$lname;
229
				}
230
		}
231
		// need to encode values/select-options to keep their order
232
		foreach($customfields as &$data)
233
		{
234
			if (!empty($data['values']))
235
			{
236
				Select::fix_encoded_options($data['values']);
237
			}
238
		}
239
		if($fields != $customfields)
240
		{
241
			// This widget has different settings from global
242
			$this->setElementAttribute($form_name, 'customfields', $fields);
243
			$this->setElementAttribute($form_name, 'fields', array_merge(
244
				array_fill_keys(array_keys($customfields), false),
245
				array_fill_keys(array_keys($fields), true)
246
			));
247
		}
248
		parent::beforeSendToClient($cname, $expand);
249
250
		// Re-format date custom fields from Y-m-d
251
		$field_settings =& self::get_array(self::$request->modifications, "{$this->id}[customfields]",true);
252
		if (true) $field_settings = array();
253
		$link_types = Api\Link::app_list();
254
		foreach($fields as $fname => $field)
255
		{
256
			// Run beforeSendToClient for each field
257
			$widget = $this->_widget($fname, $field);
258
			if(method_exists($widget, 'beforeSendToClient'))
259
			{
260
				$widget->beforeSendToClient($this->id == self::GLOBAL_ID ? '' : $this->id, $expand);
261
			}
262
		}
263
	}
264
265
	/**
266
	 * Instanciate (server-side) widget used to implement custom-field, to run its beforeSendToClient or validate method
267
	 *
268
	 * @param string $fname custom field name
269
	 * @param array $field custom field data
270
	 * @return Etemplate\Widget
271
	 */
272
	protected function _widget($fname, array $field)
273
	{
274
		static $link_types = null;
275
		if (!isset($link_types)) $link_types = Api\Link::app_list ();
276
277
		$type = $field['type'];
278
		// Link-tos needs to change from appname to link-to
279
		if($link_types[$field['type']])
280
		{
281
			if($type == 'filemanager')
282
			{
283
				$type = 'vfs-upload';
284
			}
285
			else
286
			{
287
				$type = 'link-to';
288
			}
289
		}
290
		$xml = '<'.$type.' type="'.$type.'" id="'.self::$prefix.$fname.'"/>';
291
		$widget = self::factory($type, $xml, self::$prefix.$fname);
292
		$widget->id = self::$prefix.$fname;
293
		$widget->attrs['type'] = $type;
294
		$widget->set_attrs($xml);
295
296
		// some type-specific (default) attributes
297
		switch($type)
298
		{
299
			case 'date':
300
			case 'date-time':
301
				if (!empty($field['values']['format']))
302
				{
303
					$widget->attrs['data_format'] = $field['values']['format'];
304
				}
305
				else
306
				{
307
					$widget->attrs['data_format'] = $type == 'date' ? 'Y-m-d' : 'Y-m-d H:i:s';
308
				}
309
				if($field['values']['min']) $widget->attrs['min'] = $field['values']['min'];
310
				if($field['values']['max']) $widget->attrs['min'] = $field['values']['max'];
311
				break;
312
313
			case 'vfs-upload':
314
				$widget->attrs['path'] = $field['app'] . ':' .
315
					self::expand_name('$cont['.Api\Link::get_registry($field['app'],'view_id').']',0,0,0,0,self::$request->content).
316
					':'.$field['label'];
317
				break;
318
319
			case 'link-to':
320
				$widget->attrs['only_app'] = $field['type'];
321
				break;
322
323
			case 'text':
324
				break;
325
326
			default:
327
				if (substr($type, 0, 7) !== 'select-' && $type != 'ajax_select') break;
328
				// fall-through for all select-* widgets
329
			case 'select':
330
				$widget->attrs['multiple'] = $field['rows'] > 1;
331
				// fall through
332
			case 'radio':
333
				if (!empty($field['values']) && count($field['values']) == 1 && isset($field['values']['@']))
334
				{
335
					$field['values'] = Api\Storage\Customfields::get_options_from_file($field['values']['@']);
336
				}
337
				// keep extra values set by app code, eg. addressbook advanced search
338
				if (is_array(self::$request->sel_options[self::$prefix.$fname]))
339
				{
340
					self::$request->sel_options[self::$prefix.$fname] += (array)$field['values'];
341
				}
342
				else
343
				{
344
					self::$request->sel_options[self::$prefix.$fname] = $field['values'];
345
				}
346
				//error_log(__METHOD__."('$fname', ".array2string($field).") request->sel_options['".self::$prefix.$fname."']=".array2string(self::$request->sel_options[$this->id]));
347
				// to keep order of numeric values, we have to explicit run fix_encoded_options, as sel_options are already encoded
348
				$options = self::$request->sel_options[self::$prefix.$fname];
349
				if (is_array($options))
350
				{
351
					Select::fix_encoded_options($options);
352
					self::$request->sel_options[self::$prefix.$fname] = $options;
353
				}
354
				break;
355
		}
356
		return $widget;
357
	}
358
359
	/**
360
	 * Validate input
361
	 *
362
	 * Following attributes get checked:
363
	 * - needed: value must NOT be empty
364
	 * - min, max: int and float widget only
365
	 * - maxlength: maximum length of string (longer strings get truncated to allowed size)
366
	 * - preg: perl regular expression incl. delimiters (set by default for int, float and colorpicker)
367
	 * - int and float get casted to their type
368
	 *
369
	 * @param string $cname current namespace
370
	 * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
371
	 * @param array $content
372
	 * @param array &$validated=array() validated content
373
	 */
374
	public function validate($cname, array $expand, array $content, &$validated=array())
375
	{
376
		if ($this->id)
377
		{
378
			$form_name = self::form_name($cname, $this->id, $expand);
379
		}
380
		else
381
		{
382
			$form_name = self::GLOBAL_ID;
383
		}
384
385
		$all_readonly = $this->is_readonly($cname, $form_name);
386
		$value_in = self::get_array($content, $form_name);
387
		// if we have no id / use self::GLOBAL_ID, we have to set $value_in in global namespace for regular widgets validation to find
388
		if (!$this->id) $content = array_merge($content, $value_in);
389
		//error_log(__METHOD__."($cname, ...) form_name=$form_name, use-private={$this->attrs['use-private']}, value_in=".array2string($value_in));
390
		$customfields =& $this->getElementAttribute(self::GLOBAL_VALS, 'customfields');
391
		if(is_array($value_in))
392
		{
393
			foreach(array_keys($value_in) as $field)
394
			{
395
				$field_settings = $customfields[$fname=substr($field,1)];
396
397
				if ((string)$this->attrs['use-private'] !== '' &&	// are only (non-)private fields requested
398
					(boolean)$field_settings['private'] != ($this->attrs['use-private'] != '0'))
399
				{
400
					continue;
401
				}
402
403
				// check if single field is set readonly, used in apps as it was only way to make cfs readonly in old eT
404
				// single fields set to false in $readonly overwrite a global __ALL__
405
				$cf_readonly = $this->is_readonly($form_name != self::GLOBAL_ID ? $form_name : $cname, $field);
406
				if ($cf_readonly || $all_readonly && $cf_readonly !== false)
407
				{
408
					continue;
409
				}
410
				// run validation method of widget implementing this custom field
411
				$widget = $this->_widget($fname, $field_settings);
412
				// widget has no validate method, eg. is only displaying stuff --> nothing to validate
413
				if (!method_exists($widget, 'validate')) continue;
414
				$widget->validate($form_name != self::GLOBAL_ID ? $form_name : $cname, $expand, $content, $validated);
415
				$field_name = $this->id[0] == self::$prefix && $customfields[substr($this->id,1)] ? $this->id : self::form_name($form_name != self::GLOBAL_ID ? $form_name : $cname, $field);
416
				$valid =& self::get_array($validated, $field_name, true);
417
418
				// Arrays are not valid, but leave filemanager alone, we'll catch it
419
				// when saving.  This allows files for new entries.
420
				if (is_array($valid) && $field_settings['type'] !== 'filemanager') $valid = implode(',', $valid);
421
422
				// NULL is valid for most fields, but not custom fields due to backend handling
423
				// See so_sql_cf->save()
424
				if (is_null($valid)) $valid = false;
425
				//error_log(__METHOD__."() $field_name: ".array2string($value).' --> '.array2string($valid));
426
			}
427
		}
428
		elseif ($this->type == 'customfields-types')
429
		{
430
			// Transformation doesn't handle validation
431
			$valid =& self::get_array($validated, $this->id ? $form_name : $field, true);
432
			if (true) $valid = $value_in;
433
			//error_log(__METHOD__."() $form_name $field: ".array2string($value).' --> '.array2string($value));
434
		}
435
	}
436
}
437