Select::set_attrs()   B
last analyzed

Complexity

Conditions 7
Paths 20

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 10
nc 20
nop 2
dl 0
loc 22
rs 8.8333
c 0
b 0
f 0
1
<?php
2
/**
3
 * EGroupware - eTemplate serverside select 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 Ralf Becker <[email protected]>
10
 * @copyright 2002-18 by [email protected]
11
 */
12
13
namespace EGroupware\Api\Etemplate\Widget;
14
15
use EGroupware\Api\Etemplate;
16
use EGroupware\Api;
17
18
// explicitly import old not yet ported classes
19
use calendar_timezones;
20
21
/**
22
 * eTemplate select widget
23
 *
24
 * @todo unavailable cats (eg. private cats of an other user) need to be preserved!
25
 * @todo fully implement attr[multiple] === "dynamic" to render widget with a button to switch to multiple
26
 *	as it is used in account_id selection in admin >> mailaccount (app.admin.edit_multiple method client-side)
27
 */
28
class Select extends Etemplate\Widget
29
{
30
	/**
31
	 * If the selectbox has this many rows, give it a search box automatically
32
	 */
33
	const SEARCH_ROW_LIMIT = PHP_INT_MAX; // Automatic disabled, only explicit
34
35
	/**
36
	 * These types are either set or cached on the client side, so we don't send
37
	 * their options unless asked via AJAX
38
	 */
39
	public static $cached_types = array(
40
		'select-account',
41
		'select-app',
42
		'select-bool',
43
		'select-country',
44
		// DOW needs some server-side pre-processing to unpack the options,
45
		// so can't be skipped.
46
		//'select-dow',
47
		'select-number',
48
		'select-priority',
49
		'select-percent',
50
		'select-year',
51
		'select-month',
52
		'select-day',
53
		'select-hour',
54
		'select-lang',
55
		'select-timezone'
56
	);
57
58
	/**
59
	 * @var array
60
	 */
61
	protected static $monthnames = array(
62
		0  => '',
63
		1  => 'January',
64
		2  => 'February',
65
		3  => 'March',
66
		4  => 'April',
67
		5  => 'May',
68
		6  => 'June',
69
		7  => 'July',
70
		8  => 'August',
71
		9  => 'September',
72
		10 => 'October',
73
		11 => 'November',
74
		12 => 'December'
75
	);
76
77
	/**
78
	 * Constructor
79
	 *
80
	 * @param string|XMLReader $xml string with xml or XMLReader positioned on the element to construct
0 ignored issues
show
Bug introduced by
The type EGroupware\Api\Etemplate\Widget\XMLReader was not found. Did you mean XMLReader? If so, make sure to prefix the type with \.
Loading history...
81
	 * @throws Api\Exception\WrongParameter
82
	 */
83
	public function __construct($xml = '')
84
	{
85
		$this->bool_attr_default += array(
86
			//'multiple' => false,	// handeled in set_attrs, as we additional allow "dynamic"
87
			'selected_first' => true,
88
			'search' => false,
89
			'tags' => false,
90
			'allow_single_deselect' => true,
91
		);
92
93
		if($xml) {
94
			parent::__construct($xml);
95
		}
96
	}
97
98
	/**
99
	 * Parse and set extra attributes from xml in template object
100
	 *
101
	 * Reimplemented to parse our differnt attributes
102
	 *
103
	 * @param string|XMLReader $xml
104
	 * @param boolean $cloned =true true: object does NOT need to be cloned, false: to set attribute, set them in cloned object
105
	 * @return Template current object or clone, if any attribute was set
106
	 * @todo Use legacy_attributes instead of leaving it to typeOptions method to parse them
107
	 */
108
	public function set_attrs($xml, $cloned=true)
109
	{
110
		parent::set_attrs($xml, $cloned);
111
112
		if ($this->attrs['multiple'] !== 'dynamic')
113
		{
114
			$this->attrs['multiple'] = !isset($this->attrs['multiple']) ? false :
115
				!(!$this->attrs['multiple'] || $this->attrs['multiple'] === 'false');
116
		}
117
118
		// set attrs[multiple] from attrs[options], unset options only if it just contains number or rows
119
		if ($this->attrs['options'] > 1)
120
		{
121
			$this->attrs['multiple'] = (int)$this->attrs['options'];
122
			if ((string)$this->attrs['multiple'] == $this->attrs['options'])
123
			{
124
				unset($this->attrs['options']);
125
			}
126
		}
127
		elseif($this->attrs['rows'] > 1)
128
		{
129
			$this->attrs['multiple'] = true;
130
		}
131
	}
132
133
	const UNAVAILABLE_CAT_POSTFIX = '-unavailable';
134
135
	/**
136
	 * Validate input
137
	 *
138
	 * @param string $cname current namespace
139
	 * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
140
	 * @param array $content
141
	 * @param array &$validated=array() validated content
142
	 */
143
	public function validate($cname, array $expand, array $content, &$validated=array())
144
	{
145
		$form_name = self::form_name($cname, $this->id, $expand);
146
		$widget_type = $this->attrs['type'] ? $this->attrs['type'] : $this->type;
147
		$multiple = $this->attrs['multiple'] || $this->getElementAttribute($form_name, 'rows') > 1;
148
149
		$ok = true;
150
		if (!$this->is_readonly($cname, $form_name))
151
		{
152
			$value = $value_in = self::get_array($content, $form_name);
0 ignored issues
show
Unused Code introduced by
The assignment to $value_in is dead and can be removed.
Loading history...
153
154
			$allowed2 = self::selOptions($form_name, true);	// true = return array of option-values
155
			$type_options = self::typeOptions($this,
156
				// typeOptions thinks # of rows is the first thing in options
157
				($this->attrs['rows'] && strpos($this->attrs['options'], $this->attrs['rows']) !== 0 ? $this->attrs['rows'].','.$this->attrs['options'] : $this->attrs['options']));
158
			$allowed = array_merge($allowed2,array_keys($type_options));
159
160
			// add option children's values too, "" is not read, therefore we cast to string
161
			foreach($this->children as $child)
162
			{
163
				if ($child->type == 'option') $allowed[] = (string)$child->attrs['value'];
164
			}
165
166
			if (!$multiple || $this->attrs['multiple'] === "dynamic") $allowed[] = '';
167
168
			foreach((array) $value as $val)
169
			{
170
				// handle empty-label for all widget types
171
				if ((string)$val === '' && in_array('', $allowed)) continue;
172
173
				switch ($widget_type)
174
				{
175
					case 'select-account':
176
						// If in allowed options, skip account check to support app-specific options
177
						if(count($allowed) > 0 && in_array($val, $allowed)) continue 2;	// +1 for switch
178
179
						// validate accounts independent of options know to server
180
						$account_type = $this->attrs['account_type'] ? $this->attrs['account_type'] : 'accounts';
181
						$type = $GLOBALS['egw']->accounts->exists($val);
182
						//error_log(__METHOD__."($cname,...) form_name=$form_name, widget_type=$widget_type, account_type=$account_type, type=$type");
183
						if (!$type || $type == 1 && in_array($account_type, array('groups', 'owngroups', 'memberships')) ||
184
							$type == 2 && $account_type == 'users' ||
185
							in_array($account_type, array('owngroups', 'memberships')) &&
186
								!in_array($val, $GLOBALS['egw']->accounts->memberships(
187
									$GLOBALS['egw_info']['user']['account_id'], true))
188
						)
189
						{
190
							self::set_validation_error($form_name, lang("'%1' is NOT allowed ('%2')!", $val,
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $val. ( Ignorable by Annotation )

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

190
							self::set_validation_error($form_name, /** @scrutinizer ignore-call */ lang("'%1' is NOT allowed ('%2')!", $val,

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...
191
								!$type?'not found' : ($type == 1 ? 'user' : 'group')),'');
192
							$value = '';
193
							break 2;
194
						}
195
						break;
196
197
					case 'select-timezone':
198
						if (!calendar_timezones::tz2id($val))
199
						{
200
							self::set_validation_error($form_name, lang("'%1' is NOT a valid timezone!", $val));
201
						}
202
						break;
203
204
					default:
205
						if(!in_array($val, $allowed))
206
						{
207
							self::set_validation_error($form_name,lang("'%1' is NOT allowed ('%2')!", $val, implode("','",$allowed)),'');
208
							$value = '';
209
							break 2;
210
						}
211
				}
212
			}
213
			if ($ok && $value === '' && $this->attrs['needed'])
214
			{
215
				self::set_validation_error($form_name,lang('Field must not be empty !!!',$value),'');
216
			}
217
			if (!$multiple && is_array($value) && count($value) > 1)
218
			{
219
				$value = array_shift($value);
220
			}
221
			// some widgets sub-types need some post-processing
222
			// ToDo: move it together with preprocessing to clientside
223
			switch ($widget_type)
224
			{
225
				case 'select-dow':
226
					$dow = 0;
227
					foreach((array)$value as $val)
228
					{
229
						$dow |= $val;
230
					}
231
					$value = $dow;
232
					break;
233
234
				case 'select-country':
235
					$legacy_options = $this->attrs['rows'] && strpos($this->attrs['options'], $this->attrs['rows']) !== 0 ?
236
						$this->attrs['rows'].','.$this->attrs['options'] : $this->attrs['options'];
237
					list(,$country_use_name) = explode(',', $legacy_options);
238
					if ($country_use_name && $value)
239
					{
240
						$value = Api\Country::get_full_name($value);
241
					}
242
					break;
243
244
				case 'select-cat':
245
					// unavailable cats need to be merged in again
246
					$unavailable_name = $form_name.self::UNAVAILABLE_CAT_POSTFIX;
247
					if (isset(self::$request->preserv[$unavailable_name]))
248
					{
249
						if ($this->attrs['multiple'])
250
						{
251
							$value = array_merge($value, (array)self::$request->preserv[$unavailable_name]);
252
						}
253
						elseif(!$value)	// for single cat, we only restore unavailable one, if no other was selected
254
						{
255
							$value = self::$request->preserv[$unavailable_name];
256
						}
257
					}
258
					break;
259
				case 'select-bitwise':
260
					// Sum up into a single value
261
					$sum = 0;
262
					foreach((array) $value as $val)
263
					{
264
						$sum += $val;
265
					}
266
					$value = $sum;
267
					break;
268
			}
269
			if (isset($value))
270
			{
271
				self::set_array($validated, $form_name, $value);
272
				//error_log(__METHOD__."() $form_name: ".array2string($value_in).' --> '.array2string($value).', allowed='.array2string($allowed));
273
			}
274
		}
275
		else
276
		{
277
			//error_log($this . "($form_name) is read-only, skipping validate");
278
		}
279
	}
280
281
	/**
282
	 * Fill type options in self::$request->sel_options to be used on the client
283
	 *
284
	 * @param string $cname
285
	 * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont'
286
	 */
287
	public function beforeSendToClient($cname, array $expand=null)
288
	{
289
		//error_log(__METHOD__."('$cname') this->id=$this->id, this->type=$this->type, this->attrs=".array2string($this->attrs));
290
		$matches = null;
291
		if ($cname == '$row')	// happens eg. with custom-fields: $cname='$row', this->id='#something'
292
		{
293
			$form_name = $this->id;
294
		}
295
		// happens with fields in nm-header: $cname='nm', this->id='${row}[something]' or '{$row}[something]'
296
		elseif (preg_match('/(\${row}|{\$row})\[([^]]+)\]$/', $this->id, $matches))
297
		{
298
			$form_name = $matches[2];
299
		}
300
		// happens in auto-repeat grids: $cname='', this->id='something[{$row}]'
301
		elseif (preg_match('/([^[]+)\[({\$row})\]$/', $this->id, $matches))
302
		{
303
			$form_name = $matches[1];
304
		}
305
		// happens with autorepeated grids: this->id='some[$row_cont[thing]][else]' --> just use 'else'
306
		elseif (preg_match('/\$row.*\[([^]]+)\]$/', $this->id, $matches))
307
		{
308
			$form_name = $matches[1];
309
		}
310
		else
311
		{
312
			$form_name = self::form_name($cname, $this->id, $expand);
313
		}
314
		if (!is_array(self::$request->sel_options[$form_name])) self::$request->sel_options[$form_name] = array();
315
		$type = $this->attrs['type'] ? $this->attrs['type'] : $this->type;
316
		if ($type != 'select' && $type != 'menupopup')
317
		{
318
			// Check selection preference, we may be able to skip reading some data
319
			$select_pref = $GLOBALS['egw_info']['user']['preferences']['common']['account_selection'];
320
			if($this->attrs['type'] == 'select-account' && !$GLOBALS['egw_info']['user']['apps']['admin'] && $select_pref == 'none')
321
			{
322
				// Preserve but do not send the value if preference is 'none'
323
				self::$request->preserv[$this->id] = self::$request->content[$this->id];
324
				unset(self::$request->content[$this->id]);
325
				$this->attrs['readonly'] = true;
326
			}
327
			if(!in_array($type, self::$cached_types))
328
			{
329
				// adding type specific options here, while keep further options set by app code
330
				// we need to make sure to run only once for auto-repeated rows, because
331
				// array_merge used to keep options from app would otherwise add
332
				// type-specific ones multiple time (and of cause better performance)
333
				$no_lang = null;
334
				static $form_names_done = array();
335
				if (!isset($form_names_done[$form_name]) &&
336
					($type_options = self::typeOptions($this,
337
					// typeOptions thinks # of rows is the first thing in options
338
					($this->attrs['rows'] && strpos($this->attrs['options'], $this->attrs['rows']) !== 0 ? $this->attrs['rows'].','.$this->attrs['options'] : $this->attrs['options']),
339
					$no_lang, $this->attrs['readonly'], self::get_array(self::$request->content, $form_name), $form_name)))
340
				{
341
					self::fix_encoded_options($type_options);
342
343
					self::$request->sel_options[$form_name] = array_merge(self::$request->sel_options[$form_name], $type_options);
344
345
					// if no_lang was modified, forward modification to the client
346
					if ($no_lang != $this->attrs['no_lang'])
347
					{
348
						self::setElementAttribute($form_name, 'no_lang', $no_lang);
349
					}
350
				}
351
				$form_names_done[$form_name] = true;
352
			}
353
		}
354
355
		// Make sure &nbsp;s, etc.  are properly encoded when sent, and not double-encoded
356
		$options = (isset(self::$request->sel_options[$form_name]) ? $form_name : $this->id);
357
		if(is_array(self::$request->sel_options[$options]))
358
		{
359
			if(in_array($this->attrs['type'], self::$cached_types) && !isset($form_names_done[$options]))
360
			{
361
				// Fix any custom options from application
362
				self::fix_encoded_options(self::$request->sel_options[$options],true);
363
				$form_names_done[$options] = true;
364
			}
365
			// Turn on search, if there's a lot of rows (unless explicitly set)
366
			if(!array_key_exists('search',$this->attrs) && count(self::$request->sel_options[$options]) >= self::SEARCH_ROW_LIMIT)
367
			{
368
				self::setElementAttribute($form_name, "search", true);
369
			}
370
			if(!self::$request->sel_options[$options])
371
			{
372
				unset(self::$request->sel_options[$options]);
373
			}
374
		}
375
	}
376
377
	/**
378
	 * Fix already html-encoded options, eg. "&nbps" AND optinal re-index array to keep order
379
	 *
380
	 * Get run automatic for everything in $sel_options by etemplate_new::exec / etemplate_new::fix_sel_options
381
	 *
382
	 * @param array $options
383
	 * @param boolean $use_array_of_objects Re-indexes options, making everything more complicated
384
	 */
385
	public static function fix_encoded_options(array &$options, $use_array_of_objects=null)
386
	{
387
		$backup_options = $options;
388
389
		$values = array();
390
		foreach($options as $value => &$label)
391
		{
392
			// Of course once we re-index the options, we can't detect duplicates
393
			// so check here, as we re-index
394
			// Duplicates might happen if app programmer isn't paying attention and
395
			// either uses the same ID in the template, or adds the options twice
396
			$check_value = (string)(is_array($label) && array_key_exists('value', $label) ? $label['value'] : $value);
397
			if (isset($values[$check_value]))
398
			{
399
				unset($options[$value]);
400
				continue;
401
			}
402
			$values[$check_value] = $label;
403
404
			if (is_null($use_array_of_objects) && is_numeric($value) && (!is_array($label) || !isset($label['value'])))
405
			{
406
				$options = $backup_options;
407
				return self::fix_encoded_options($options, true);
408
			}
409
			// optgroup or values for keys "label" and "title"
410
			if(is_array($label))
411
			{
412
				self::fix_encoded_options($label, false);
413
				if ($use_array_of_objects && !array_key_exists('value', $label)) $label['value'] = $value;
414
			}
415
			else
416
			{
417
				$label = html_entity_decode($label, ENT_NOQUOTES, 'utf-8');
418
419
				if ($use_array_of_objects)
420
				{
421
					$label = array(
422
						'value' => $value,
423
						'label' => $label,
424
					);
425
				}
426
			}
427
		}
428
		if ($use_array_of_objects)
429
		{
430
			$options = array_values($options);
431
		}
432
	}
433
434
	/**
435
	 * Get options from $sel_options array for a given selectbox name
436
	 *
437
	 * @param string $name
438
	 * @param boolean $return_values =false true: return array with option values, instead of value => label pairs
439
	 * @return array
440
	 */
441
	public static function selOptions($name, $return_values=false)
442
	{
443
		$options = array();
444
445
		// Check for exact match on name
446
		if (isset(self::$request->sel_options[$name]) && is_array(self::$request->sel_options[$name]))
447
		{
448
			$options += self::$request->sel_options[$name];
449
		}
450
451
		// Check for non-trivial name like a[b]
452
		$name_parts = explode('[',str_replace(array('&#x5B;','&#x5D;',']'),array('['),$name));
453
		if(!$options)
454
		{
455
			$options = (array)self::get_array(self::$request->sel_options,$name);
456
			if(is_numeric(end($name_parts)) && $options['label'] && $options['value'])
457
			{
458
				// Too deep, we got a single option
459
				$options = array();
460
			}
461
		}
462
463
		// Check for base of name in root of sel_options
464
		if(!$options)
465
		{
466
			if (count($name_parts))
467
			{
468
				$org_name = $name_parts[count($name_parts)-1];
469
				if (isset(self::$request->sel_options[$org_name]) && is_array(self::$request->sel_options[$org_name]))
470
				{
471
					$options += self::$request->sel_options[$org_name];
472
				}
473
				elseif (isset(self::$request->sel_options[$name_parts[0]]) && is_array(self::$request->sel_options[$name_parts[0]]))
474
				{
475
					$options += self::$request->sel_options[$name_parts[0]];
476
				}
477
			}
478
		}
479
480
		// Check for options-$name in content
481
		if (is_array(self::$request->content['options-'.$name]))
482
		{
483
			$options += self::$request->content['options-'.$name];
484
		}
485
		if ($return_values)
486
		{
487
			$values = array();
488
			foreach($options as $key => $val)
489
			{
490
				if (is_array($val))
491
				{
492
					if (isset($val['value']) && count(array_filter(array_keys($val), 'is_int')) == 0)
493
					{
494
						$values[] = $val['value'];
495
					}
496
					else if ((isset($val['label']) || isset($val['title'])) && count($val) == 1 ||
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode || IssetNode ...ode && count($val) == 2, Probably Intended Meaning: IssetNode || IssetNode &...de && count($val) == 2)
Loading history...
497
						isset($val['title']) && isset($val['label']) && count($val) == 2)
498
					{
499
						// key => {label, title}
500
						$values[] = $key;
501
					}
502
					else	// optgroup
503
					{
504
						foreach($val as $k => $v)
505
						{
506
							$values[] = is_array($v) && isset($v['value']) ? $v['value'] : $k;
507
						}
508
					}
509
				}
510
				else
511
				{
512
					$values[] = $key;
513
				}
514
			}
515
			//error_log(__METHOD__."('$name', TRUE) options=".array2string($options).' --> values='.array2string($values));
516
			$options = $values;
517
		}
518
		else if (end($options) && is_array(end($options)) && isset(end($options)['value']))
519
		{
520
			$values = array();
521
			foreach($options as $index => $option)
522
			{
523
				if(is_array($option) && isset($option['value']))
524
				{
525
					$values[$option['value']] = $option['label'];
526
				}
527
				else
528
				{
529
					$values[$index] = $option;
530
				}
531
			}
532
			$options = $values;
533
		}
534
		//error_log(__METHOD__."('$name') returning ".array2string($options));
535
		return $options;
536
	}
537
538
	/**
539
	 * Fetch options for certain select-box types
540
	 *
541
	 * @param string|Select $widget_type Type of widget, or actual widget to get attributes since $legacy_options are legacy
542
	 * @param string $_legacy_options options string of widget
543
	 * @param boolean $no_lang =false initial value of no_lang attribute (some types set it to true)
544
	 * @param boolean $readonly =false for readonly we dont need to fetch all options, only the one for value
545
	 * @param mixed $value =null value for readonly
546
	 * @param string $form_name =null
547
	 * @return array with value => label pairs
548
	 */
549
	public static function typeOptions($widget_type, $_legacy_options, &$no_lang=false, $readonly=false, &$value=null, $form_name=null)
550
	{
551
		if($widget_type && is_object($widget_type))
552
		{
553
			$widget = $widget_type;
554
			$widget_type = $widget->attrs['type'] ? $widget->attrs['type'] : $widget->type;
555
		}
556
		// Legacy / static support
557
		// Have to do this explicitly, since legacy options is not defined on class level
558
		$legacy_options = explode(',',$_legacy_options);
559
		foreach($legacy_options as &$field)
560
		{
561
			$field = self::expand_name($field, 0, 0,'','',self::$cont);
562
		}
563
564
		list($rows,$type,$type2,$type3,$type4,$type5) = $legacy_options;
565
		$no_lang = false;
566
		$options = array();
567
		switch ($widget_type)
568
		{
569
			case 'select-percent':	// options: #row,decrement(default=10)
570
				$decr = $type > 0 ? $type : 10;
571
				for ($i=0; $i <= 100; $i += $decr)
572
				{
573
					$options[intval($i)] = intval($i).'%';
574
				}
575
				$options[100] = '100%';
576
				if (!$rows || !empty($value))
577
				{
578
					$value = intval(($value+($decr/2)) / $decr) * $decr;
579
				}
580
				$no_lang = True;
581
				break;
582
583
			case 'select-priority':
584
				$options = array('','low','normal','high');
585
				break;
586
587
			case 'select-bool':	// equal to checkbox, can be used with nextmatch-customfilter to filter a boolean column
588
				$options = array(0 => 'no',1 => 'yes');
589
				break;
590
591
			case 'select-country':	// #Row|Extralabel,1=use country name, 0=use 2 letter-code,custom country field name
592
				if($type == 0 && $type2)
593
				{
594
					$custom_label = is_numeric($type2) ? 'Custom' : $type2;
595
					$options = array('-custom-' => lang($custom_label)) + Api\Country::countries();
596
				}
597
				else
598
				{
599
					$options = Api\Country::countries();
600
				}
601
				if ($type && $value)
602
				{
603
					$value = Api\Country::country_code($value);
604
					if (!isset($options[$value]))
605
					{
606
						if($type2)
607
						{
608
							$options[$value] = $value;
609
						}
610
					}
611
				}
612
				$no_lang = True;
613
				break;
614
615
			case 'select-state':
616
				$options = (array)Api\Country::get_states($field);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $field seems to be defined by a foreach iteration on line 559. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
617
				$no_lang = True;
618
				break;
619
620
			case 'select-cat':	// !$type == globals cats too, $type2: extraStyleMultiselect, $type3: application, if not current-app, $type4: parent-id, $type5=owner (-1=global),$type6=show missing
621
				if ((!$type3 || $type3 === $GLOBALS['egw']->categories->app_name) &&
622
					(!$type5 || $type5 ==  $GLOBALS['egw']->categories->account_id))
623
				{
624
					$categories = $GLOBALS['egw']->categories;
625
				}
626
				else	// we need to instanciate a new cat object for the correct application
627
				{
628
					$categories = new Api\Categories($type5,$type3);
629
				}
630
				// Allow text for global
631
				$type = ($type && strlen($type) > 1 ? $type : !$type);
632
				// we cast $type4 (parent) to int, to get default of 0 if omitted
633
				foreach((array)$categories->return_sorted_array(0,False,'','','',$type,(int)$type4,true) as $cat)
634
				{
635
					$s = str_repeat('&nbsp;',$cat['level']) . stripslashes($cat['name']);
636
637
					if (Api\Categories::is_global($cat))
638
					{
639
						$s .= Api\Categories::$global_marker;
640
					}
641
					$options[$cat['id']] = array(
642
						'label' => $s,
643
						'title' => $cat['description'],
644
						// These are extra info for easy dealing with categories
645
						// client side, without extra loading
646
						'main'  => (int)$cat['main'],
647
						'children'	=> $cat['children'],
648
						//add different class per level to allow different styling for each category level:
649
						'class' => "cat_level". $cat['level']
650
					);
651
					// Send data too
652
					if(is_array($cat['data']))
653
					{
654
						$options[$cat['id']] += $cat['data'];
655
					}
656
				}
657
				// preserv unavailible cats (eg. private user-cats)
658
				if ($value && ($unavailible = array_diff(is_array($value) ? $value : explode(',',$value),array_keys((array)$options))))
659
				{
660
					// unavailable cats need to be merged in again
661
					$unavailable_name = $form_name.self::UNAVAILABLE_CAT_POSTFIX;
662
					self::$request->preserv[$unavailable_name] = $unavailible;
663
				}
664
				$no_lang = True;
665
				break;
666
667
			case 'select-year':	// options: #rows,#before(default=3),#after(default=2)
668
				$options[''] = '';
669
				if ($type <= 0)  $type  = 3;
670
				if ($type2 <= 0) $type2 = 2;
671
				if ($type > 100 && $type2 > 100 && $type > $type) { $y = $type; $type=$type2; $type2=$y; }
672
				$y = date('Y')-$type;
673
				if ($value && $value-$type < $y || $type > 100) $y = $type > 100 ? $type : $value-$type;
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($value && $value - $type < $y) || $type > 100, Probably Intended Meaning: $value && ($value - $type < $y || $type > 100)
Loading history...
674
				$to = date('Y')+$type2;
675
				if ($value && $value+$type2 > $to || $type2 > 100) $to = $type2 > 100 ? $type2 : $value+$type2;
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($value && $value + $type2 > $to) || $type2 > 100, Probably Intended Meaning: $value && ($value + $type2 > $to || $type2 > 100)
Loading history...
676
				for ($n = 0; $y <= $to && $n < 200; ++$n)
677
				{
678
					$options[$y] = $y++;
679
				}
680
				$no_lang = True;
681
				break;
682
683
			case 'select-month':
684
				$options = self::$monthnames;
685
				$value = intval($value);
686
				break;
687
688
			case 'select-dow':	// options: rows[,0=summaries befor days, 1=summaries after days, 2=no summaries[,extraStyleMultiselect]]
689
				if (!defined('MCAL_M_SUNDAY'))
690
				{
691
					define('MCAL_M_SUNDAY',1);
692
					define('MCAL_M_MONDAY',2);
693
					define('MCAL_M_TUESDAY',4);
694
					define('MCAL_M_WEDNESDAY',8);
695
					define('MCAL_M_THURSDAY',16);
696
					define('MCAL_M_FRIDAY',32);
697
					define('MCAL_M_SATURDAY',64);
698
699
					define('MCAL_M_WEEKDAYS',62);
700
					define('MCAL_M_WEEKEND',65);
701
					define('MCAL_M_ALLDAYS',127);
702
				}
703
				$weekstart = $GLOBALS['egw_info']['user']['preferences']['calendar']['weekdaystarts'];
704
				$options = array();
705
				if ($rows >= 2 && !$type)
706
				{
707
					$options = array(
708
						MCAL_M_ALLDAYS	=> 'all days',
709
						MCAL_M_WEEKDAYS	=> 'working days',
710
						MCAL_M_WEEKEND	=> 'weekend',
711
					);
712
				}
713
				if ($weekstart == 'Saturday') $options[MCAL_M_SATURDAY] = 'saturday';
714
				if ($weekstart != 'Monday') $options[MCAL_M_SUNDAY] = 'sunday';
715
				$options += array(
716
					MCAL_M_MONDAY	=> 'monday',
717
					MCAL_M_TUESDAY	=> 'tuesday',
718
					MCAL_M_WEDNESDAY=> 'wednesday',
719
					MCAL_M_THURSDAY	=> 'thursday',
720
					MCAL_M_FRIDAY	=> 'friday',
721
				);
722
				if ($weekstart != 'Saturday') $options[MCAL_M_SATURDAY] = 'saturday';
723
				if ($weekstart == 'Monday') $options[MCAL_M_SUNDAY] = 'sunday';
724
				if ($rows >= 2 && $type == 1)
725
				{
726
					$options += array(
727
						MCAL_M_ALLDAYS	=> 'all days',
728
						MCAL_M_WEEKDAYS	=> 'working days',
729
						MCAL_M_WEEKEND	=> 'weekend',
730
					);
731
				}
732
				$value_in = $value;
733
				$value = array();
734
				foreach(array_keys($options) as $val)
735
				{
736
					if (($value_in & $val) == $val)
737
					{
738
						$value[] = $val;
739
740
						if ($val == MCAL_M_ALLDAYS ||
741
							$val == MCAL_M_WEEKDAYS && $value_in == MCAL_M_WEEKDAYS ||
742
							$val == MCAL_M_WEEKEND && $value_in == MCAL_M_WEEKEND)
743
						{
744
							break;	// dont set the others
745
						}
746
					}
747
				}
748
				break;
749
750
			case 'select-day':
751
				$type = 1;
752
				$type2 = 31;
753
				$type3 = 1;
754
				// fall-through
755
756
			case 'select-number':	// options: rows,min,max,decrement,suffix
757
				$type = $type === '' ? 1 : intval($type);		// min
758
				$type2 = $type2 === '' ? 10 : intval($type2);	// max
759
				$format = '%d';
760
				if (!empty($type3) && $type3[0] == '0')			// leading zero
761
				{
762
					$format = '%0'.strlen($type3).'d';
763
				}
764
				$type3 = !$type3 ? 1 : intval($type3);			// decrement
765
				if (($type <= $type2) != ($type3 > 0))
766
				{
767
					$type3 = -$type3;	// void infinite loop
768
				}
769
				if (!empty($type4)) $format .= lang($type4);
770
				for ($i=0,$n=$type; $n <= $type2 && $i <= 100; $n += $type3,++$i)
771
				{
772
					$options[$n] = sprintf($format,$n);
773
				}
774
				$no_lang = True;
775
				break;
776
777
			case 'select-hour':
778
				$minutes = !$type2 ? ':00' : '';
779
				for ($h = 0; $h <= 23; ++$h)
780
				{
781
					$options[$h] = $GLOBALS['egw_info']['user']['preferences']['common']['timeformat'] == 12 ?
782
						(($h % 12 ? $h % 12 : 12).$minutes.' '.($h < 12 ? lang('am') : lang('pm'))) :
783
						sprintf('%02d',$h).$minutes;
784
				}
785
				$no_lang = True;
786
				break;
787
788
			case 'select-app':	// type2: 'user'=apps of current user, 'enabled', 'installed' (default), 'all' = not installed ones too
789
				$apps = self::app_options($type2);
790
				$options = is_array($options) ? $options+$apps : $apps;
0 ignored issues
show
introduced by
The condition is_array($options) is always true.
Loading history...
791
				break;
792
793
			case 'select-lang':
794
				$options = Api\Translation::list_langs();
795
				$no_lang = True;
796
				break;
797
798
			case 'select-timezone':	// options: #rows,$type
799
				if (is_numeric($value))
800
				{
801
					$value = calendar_timezones::id2tz($value);
802
				}
803
				if ($readonly)	// for readonly we dont need to fetch all TZ's
804
				{
805
					$options[$value] = calendar_timezones::tz2id($value,'name');
806
				}
807
				else
808
				{
809
					$options = $type ? Api\DateTime::getTimezones() : Api\DateTime::getUserTimezones($value);
810
				}
811
				break;
812
			case 'select-bitwise':
813
				// type = app name
814
				$options = $form_name ? self::selOptions($form_name) : array();
815
				$new_value = array();
816
				$appname = $type ? $type : ($widget && $widget->attrs['appname'] ?
817
					self::expand_name($widget->attrs['appname'], 0, 0,'','',self::$cont) : '');
818
				if($appname)
819
				{
820
					$options += (array)Api\Hooks::single(array('location' => 'acl_rights'), $appname);
821
				}
822
				else
823
				{
824
					$options += array(
825
						'run' => 'Run',
826
						Api\Acl::READ => 'Read',
827
						Api\Acl::ADD => 'Add',
828
						Api\Acl::EDIT => 'Edit',
829
						Api\Acl::DELETE => 'Delete',
830
						Api\Acl::PRIVAT => 'Private',
831
						Api\Acl::GROUPMGRS => 'Group managers',
832
						Api\Acl::CUSTOM1 => 'Custom 1',
833
						Api\Acl::CUSTOM2 => 'Custom 2',
834
						Api\Acl::CUSTOM3 => 'Custom 3',
835
					);
836
				}
837
				foreach((array)$options as $right => $name)
838
				{
839
					if(!!($value & $right))
840
					{
841
						$new_value[] = $right;
842
					}
843
				}
844
				$value = $new_value;
845
		}
846
		if ($rows > 1 || $readonly)
847
		{
848
			unset($options['']);
849
		}
850
851
		//error_log(__METHOD__."('$widget_type', '$_legacy_options', no_lang=".array2string($no_lang).', readonly='.array2string($readonly).", value=$value) returning ".array2string($options));
852
		return $options;
853
	}
854
855
	/**
856
	 * Get available apps as options
857
	 *
858
	 * @param string $type2 ='installed[:home;groupdav; ...]' 'user'=apps of current user,
859
	 * 'enabled', 'installed' (default), 'all' = not installed ones too. In order to
860
	 * exclude apps explicitly we can list them (app name separator is ';') in front of the type.
861
	 *
862
	 * @return array app => label pairs sorted by label
863
	 */
864
	public static function app_options($type2)
865
	{
866
		$apps = array();
867
		$parts = explode(":", $type2);
868
		$exceptions = explode(";", $parts[1]);
869
		$type2 = $parts[0];
870
871
		foreach ($GLOBALS['egw_info']['apps'] as $app => $data)
872
		{
873
			if ($type2 == 'enabled' && (!$data['enabled'] || !$data['status'] || $data['status'] == 3 || in_array($app, $exceptions)))
874
			{
875
				continue;	// app not enabled (user can not have run rights for these apps)
876
			}
877
			if (($type2 != 'user' || $GLOBALS['egw_info']['user']['apps'][$app]) && !in_array($app, $exceptions))
878
			{
879
				$apps[$app] = lang($app);
880
			}
881
		}
882
		if ($type2 == 'all')
883
		{
884
			$dir = opendir(EGW_SERVER_ROOT);
885
			while ($file = readdir($dir))
886
			{
887
				if (@is_dir(EGW_SERVER_ROOT."/$file/setup") && $file[0] != '.' &&
888
				!isset($apps[$app = basename($file)]))
889
				{
890
					$apps[$app] = $app . ' (*)';
891
				}
892
			}
893
			closedir($dir);
894
		}
895
		natcasesort($apps);
896
		return $apps;
897
	}
898
899
	/**
900
	 * internal function to format account-data
901
	 *
902
	 * @param int $id
903
	 * @param array $acc =null optional values for keys account_(type|lid|lastname|firstname) to not read them again
904
	 * @param int $longnames =0
905
	 * @param boolean $show_type =false true: return array with values for keys label and icon, false: only label
906
	 * @return string|array
907
	 */
908
	public static function accountInfo($id,$acc=null,$longnames=0,$show_type=false)
909
	{
910
		if (!$id)
911
		{
912
			return '&nbsp;';
913
		}
914
915
		if (!is_array($acc))
916
		{
917
			$data = $GLOBALS['egw']->accounts->get_account_data($id);
918
			if (!isset($data[$id])) return '#'.$id;
919
			foreach(array('type','lid','firstname','lastname') as $name)
920
			{
921
				$acc['account_'.$name] = $data[$id][$name];
922
			}
923
		}
924
925
		if ($acc['account_type'] == 'g')
926
		{
927
			$longnames = 1;
928
		}
929
		$info = '';
930
		switch ($longnames)
931
		{
932
			case 2:
933
				$info .= '&lt;'.$acc['account_lid'].'&gt; ';
934
				// fall-through
935
			case 1:
936
				$info .= $acc['account_type'] == 'g' ? lang('group').' '.$acc['account_lid'] :
937
					$acc['account_firstname'].' '.$acc['account_lastname'];
938
				break;
939
			case '0':
940
				$info .= $acc['account_lid'];
941
				break;
942
			default:			// use the phpgw default
943
				$info = Api\Accounts::format_username($acc['account_lid'],
944
					$acc['account_firstname'],$acc['account_lastname']);
945
				break;
946
		}
947
		if($show_type) {
948
			$info = array(
949
				'label'	=> $info,
950
				'icon' => $acc['account_type'] == 'g' ? 'addressbook/group' : 'user'
951
			);
952
		}
953
		return $info;
954
	}
955
956
	/**
957
	 * Some select options are fairly static, but can only be generated on the server
958
	 * so we generate them here, then cache them client-side
959
	 *
960
	 * @param string $type
961
	 * @param Array|String $attributes
962
	 * @param string $value Optional current value, to make sure it's included
963
	 */
964
	public static function ajax_get_options($type, $attributes, $value = null)
965
	{
966
		$no_lang = false;
967
		if(is_array($attributes))
968
		{
969
			$attributes = implode(',',$attributes);
970
		}
971
		$options = self::typeOptions($type, $attributes,$no_lang,false,$value);
972
		self::fix_encoded_options($options,true);
973
		$response = Api\Json\Response::get();
974
		$response->data($options);
975
	}
976
}
977
Etemplate\Widget::registerWidget(__NAMESPACE__.'\\Select', array('selectbox', 'listbox', 'select', 'menupopup'));
978