Preferences::read_repository()   F
last analyzed

Complexity

Conditions 27
Paths > 20000

Size

Total Lines 107
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 27
eloc 52
nc 55296
nop 1
dl 0
loc 107
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * EGroupware API - Preferences
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Joseph Engo <[email protected]>
7
 * @author Mark Peters <[email protected]>
8
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de> merging prefs on runtime, session prefs and reworked the class
9
 * Copyright (C) 2000, 2001 Joseph Engo
10
 * @license http://opensource.org/licenses/lgpl-license.php LGPL - GNU Lesser General Public License
11
 * @package api
12
 * @version $Id$
13
 */
14
15
namespace EGroupware\Api;
16
17
/**
18
 * preferences class used for setting application preferences
19
 *
20
 * preferences are read into following arrays:
21
 * - $data effective prefs used everywhere in EGroupware
22
 * Effective prefs are merged together in following precedence from:
23
 * - $forced forced preferences set by the admin, they take precedence over user or default prefs
24
 * - $session temporary prefs eg. language set on login just for session
25
 * - $user the stored user prefs, only used for manipulating and storeing the user prefs
26
 * - $group the stored prefs of all group-memberships of current user, can NOT be deleted or stored directly!
27
 * - $default the default preferences, always used when the user has no own preference set
28
 *
29
 * To update the prefs of a certain group, not just the primary group of the user, you have to
30
 * create a new instance of preferences class, with the given id of the group. This takes into
31
 * account the offset of DEFAULT_ID, we are using currently for groups (as -1, and -2) are already
32
 * taken!
33
 *
34
 * Preferences get now json-encoded and no longer PHP serialized and addslashed,
35
 * thought they only change when they get updated.
36
 */
37
class Preferences
38
{
39
	/**
40
	 * account_id for default prefs
41
	 */
42
	const DEFAULT_ID = -2;
43
	/**
44
	 * account_id for forced prefs
45
	 */
46
	const FORCED_ID = -1;
47
48
	/**
49
	 * account the class is instanciated for
50
	 * @var int
51
	 */
52
	var $account_id;
53
	/**
54
	 * account-type u or g
55
	 * @var string
56
	 */
57
	var $account_type;
58
	/**
59
	 * effectiv user prefs, used by all apps
60
	 * @var array
61
	 */
62
	var $data = array();
63
	/**
64
	 * set user prefs for saveing (no defaults/forced prefs merged)
65
	 * @var array
66
	 */
67
	var $user = array();
68
	/**
69
	 * primary group prefs
70
	 * @var array
71
	 */
72
	var $group = array();
73
	/**
74
	 * default prefs
75
	 * @var array
76
	 */
77
	var $default = array();
78
	/**
79
	 * forced prefs
80
	 * @var array
81
	 */
82
	var $forced = array();
83
	/**
84
	 * session / tempory prefs
85
	 * @var array
86
	 */
87
	var $session = array();
88
	/**
89
	 * @var Db
90
	 */
91
	var $db;
92
	/**
93
	 * table-name
94
	 */
95
	const TABLE = 'egw_preferences';
96
	var $table = self::TABLE;
97
98
	var $values,$vars;	// standard notify substitues, will be set by standard_substitues()
99
100
	/**
101
	 * Contstructor
102
	 *
103
	 * @param int|string $account_id =''
104
	 * @return preferences
105
	 */
106
	function __construct($account_id = '')
107
	{
108
		if (isset($GLOBALS['egw']->db))
109
		{
110
			$this->db = $GLOBALS['egw']->db;
111
		}
112
		else
113
		{
114
			$this->db = $GLOBALS['egw_setup']->db;
115
			$this->table = $GLOBALS['egw_setup']->prefs_table;
116
		}
117
		$this->set_account_id($account_id);
118
	}
119
120
	/**
121
	 * Set account_id for class
122
	 *
123
	 * Takes care of offset for groups.
124
	 *
125
	 * @param int|string $account_id numeric account_id, "default", "forced" to load default or forced preferences
126
	 *	or account_lid (only if !== "default" or "forced"!)
127
	 */
128
	function set_account_id($account_id)
129
	{
130
		if ($account_id === 'default')
131
		{
132
			$this->account_id = self::DEFAULT_ID;
133
		}
134
		elseif ($account_id === 'forced')
135
		{
136
			$this->account_id = self::FORCED_ID;
137
		}
138
		// if we got instancated for a group, need to set offset of DEFAULT_ID!
139
		elseif ($account_id < 0 || !is_numeric($account_id) && ($account_id = get_account_id($account_id)) < 0)
140
		{
141
			$this->account_id = $account_id + self::DEFAULT_ID;
142
		}
143
		else
144
		{
145
			$this->account_id = $account_id;
0 ignored issues
show
Documentation Bug introduced by
It seems like $account_id can also be of type string. However, the property $account_id is declared as type integer. 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...
146
		}
147
		//error_log(__METHOD__."($account_id) setting this->account_id to $this->account_id");
148
	}
149
150
	/**
151
	 * Return account_id class is instanciated for or "default" or "forced"
152
	 *
153
	 * Takes care of offset for groups.
154
	 *
155
	 * @return string|int
156
	 */
157
	function get_account_id()
158
	{
159
		switch ($this->account_id)
160
		{
161
			case self::DEFAULT_ID:
162
				return 'default';
163
			case self::FORCED_ID:
164
				return 'forced';
165
		}
166
		return $this->account_id < self::DEFAULT_ID ? $this->account_id-self::DEFAULT_ID : $this->account_id;
167
	}
168
169
	/**
170
	 * Magic function to avoid storing perferences in session, as they get re-read on each request by egw_session::verify()
171
	 *
172
	 * @return array with class vars to store
173
	 */
174
	function __sleep()
175
	{
176
		$vars = array_keys(get_object_vars($this));
177
178
		return array_diff($vars, array('data', 'user', 'group', 'default', 'forced', 'session'));
179
	}
180
181
	/**
182
	 * Lifetime in seconds of cached items 1d
183
	 */
184
	const CACHE_LIFETIME = 86400;
185
186
	/**
187
	 * Read preferences of requested id(s)
188
	 *
189
	 * @param int|array $ids
190
	 * @return array id => app => preference data
191
	 */
192
	function cache_read($ids)
193
	{
194
		$prefs = Cache::getInstance(__CLASS__, $ids);
195
		$db_read = array();
196
		foreach((array)$ids as $id)
197
		{
198
			// if prefs are not returned, null or not an array, read them from db
199
			if (!isset($prefs[$id]) && !is_array($prefs[$id])) $db_read[] = $id;
200
		}
201
		if ($db_read)
202
		{
203
			foreach($this->db->select($this->table,'*',array('preference_owner' => $db_read),__LINE__,__FILE__) as $row)
204
			{
205
				// The following replacement is required for PostgreSQL to work
206
				$app = trim($row['preference_app']);
207
208
				$prefs[$row['preference_owner']][$app] = self::unserialize($row['preference_value']);
209
			}
210
			foreach($db_read as $id)
211
			{
212
				if (!isset($prefs[$id])) $prefs[$id] = array();
213
				Cache::setInstance(__CLASS__, $id, $prefs[$id]);
214
			}
215
		}
216
		//error_log(__METHOD__.'('.array2string($ids).') read-from-db='.array2string($db_read));
217
		return $prefs;
218
	}
219
220
	/**
221
	 * parses a notify and replaces the substitutes
222
	 *
223
	 * @param string $msg message to parse / substitute
224
	 * @param array $values =array() extra vars to replace in addition to $this->values, vars are in an array with \
225
	 * 	$key => $value pairs, $key does not include the $'s and is the *untranslated* name
226
	 * @param boolean $use_standard_values =true should the standard values are used
227
	 * @return string with parsed notify-msg
228
	 */
229
	function parse_notify($msg,$values=array(),$use_standard_values=True)
230
	{
231
		$vals = $values ? $values : array();
232
233
		if ($use_standard_values && is_array($this->values))
234
		{
235
			$vals += $this->values;
236
		}
237
		$replace = $with = array();
238
		foreach($vals as $key => $val)
239
		{
240
			if ($this->debug) error_log(__METHOD__." replacing \$\$$key\$\$ with $val  ");
0 ignored issues
show
Bug Best Practice introduced by
The property debug does not exist on EGroupware\Api\Preferences. Did you maybe forget to declare it?
Loading history...
241
			$replace[] = '$$'.$key.'$$';
242
			$with[]    = $val;
243
		}
244
		return str_replace($replace,$with,$msg);
245
	}
246
247
	/**
248
	 * replaces the english key's with translated ones, or if $un_lang the opposite
249
	 *
250
	 * @param string $msg message to translate
251
	 * @param array $vals =array() extra vars to replace in addition to $this->values, vars are in an array with \
252
	 * 	$key => $value pairs, $key does not include the $'s and is the *untranslated* name
253
	 * @param boolean $un_lang =false if true translate back
254
	 * @return string
255
	 */
256
	function lang_notify($msg,$vals=array(),$un_lang=False)
257
	{
258
		foreach(array_keys($vals) as $key)
259
		{
260
			$lname = ($lname = lang($key)) == $key.'*' ? $key : $lname;
261
			if ($un_lang)
262
			{
263
				$langs[$lname] = '$$'.$key.'$$';
264
			}
265
			else
266
			{
267
				$langs[$key] = '$$'.$lname.'$$';
268
			}
269
		}
270
		return $this->parse_notify($msg,$langs,False);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $langs seems to be defined by a foreach iteration on line 258. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
271
	}
272
273
	/**
274
	 * define some standard substitues-values and use them on the prefs, if needed
275
	 */
276
	function standard_substitutes()
277
	{
278
		if ($this->debug) error_log(__METHOD__." is called ");
0 ignored issues
show
Bug Best Practice introduced by
The property debug does not exist on EGroupware\Api\Preferences. Did you maybe forget to declare it?
Loading history...
279
		if (!is_array(@$GLOBALS['egw_info']['user']['preferences']))
280
		{
281
			$GLOBALS['egw_info']['user']['preferences'] = $this->data;	// else no lang()
282
		}
283
		// we cant use egw_info/user/fullname, as it's not set when we run
284
		$this->values = array(	// standard notify replacements
285
			'fullname'  => Accounts::id2name($this->account_id, 'account_fullname'),
286
			'firstname' => Accounts::id2name($this->account_id, 'account_firstname'),
287
			'lastname'  => Accounts::id2name($this->account_id, 'account_lastname'),
288
			'domain'    => $GLOBALS['egw_info']['server']['mail_suffix'],
289
			'email'     => $this->email_address($this->account_id),
290
			'date'      => DateTime::to('now',$GLOBALS['egw_info']['user']['preferences']['common']['dateformat']),
291
		);
292
		// do this first, as it might be already contain some substitues
293
		//
294
		$this->values['email'] = $this->parse_notify($this->values['email']);
295
296
		$this->vars = array(	// langs have to be in common !!!
297
			'fullname'  => lang('name of the user, eg. "%1"',$this->values['fullname']),
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $this->values['fullname']. ( Ignorable by Annotation )

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

297
			'fullname'  => /** @scrutinizer ignore-call */ lang('name of the user, eg. "%1"',$this->values['fullname']),

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...
298
			'firstname' => lang('first name of the user, eg. "%1"',$this->values['firstname']),
299
			'lastname'  => lang('last name of the user, eg. "%1"',$this->values['lastname']),
300
			'domain'    => lang('domain name for mail-address, eg. "%1"',$this->values['domain']),
301
			'email'     => lang('email-address of the user, eg. "%1"',$this->values['email']),
302
			'date'      => lang('todays date, eg. "%1"',$this->values['date']),
303
		);
304
		if ($this->debug) error_log(__METHOD__.print_r($this->vars,true));
305
		// do the substituetion in the effective prefs (data)
306
		//
307
		foreach($this->data as $app => $data)
308
		{
309
			if(!is_array($data)) continue;
310
			foreach($data as $key => $val)
311
			{
312
				if (!is_array($val) && strpos($val,'$$') !== False)
313
				{
314
					$this->data[$app][$key] = $this->parse_notify($val);
315
				}
316
				elseif (is_array($val))
317
				{
318
					foreach($val as $k => $v)
319
					{
320
						if (!is_array($v) && strpos($v,'$$') !== False)
321
						{
322
							$this->data[$app][$key][$k] = $this->parse_notify($v);
323
						}
324
					}
325
				}
326
			}
327
		}
328
	}
329
330
	/**
331
	 * Unserialize data from either json_encode or PHP serialize and addslashes
332
	 *
333
	 * @param string $str serialized prefs
334
	 * @return array
335
	 */
336
	protected static function unserialize($str)
337
	{
338
		// handling of new json-encoded prefs
339
		if ($str[0] != 'a' && $str[1] != ':')
340
		{
341
			return json_decode($str, true);
342
		}
343
		// handling of old PHP serialized and addslashed prefs
344
		$data = php_safe_unserialize($str);
345
		if($data === false)
346
		{
347
			// manually retrieve the string lengths of the serialized array if unserialize failed
348
			$data = php_safe_unserialize(preg_replace_callback('!s:(\d+):"(.*?)";!s', function($matches)
349
			{
350
				return 's:'.mb_strlen($matches[2],'8bit').':"'.$matches[2].'";';
351
			}, $str));
352
		}
353
		self::unquote($data);
354
		return $data;
355
	}
356
357
	/**
358
	 * unquote (stripslashes) recursivly the whole array
359
	 *
360
	 * @param array &$arr array to unquote (var-param!)
361
	 */
362
	protected static function unquote(&$arr)
363
	{
364
		if (!is_array($arr))
0 ignored issues
show
introduced by
The condition is_array($arr) is always true.
Loading history...
365
		{
366
			$arr = stripslashes($arr);
367
			return;
368
		}
369
		foreach($arr as $key => $value)
370
		{
371
			if (is_array($value))
372
			{
373
				self::unquote($arr[$key]);
374
			}
375
			else
376
			{
377
				$arr[$key] = stripslashes($value);
378
			}
379
		}
380
	}
381
382
	/**
383
	 * read preferences from the repository
384
	 *
385
	 * the function ready all 3 prefs user/default/forced and merges them to the effective ones
386
	 *
387
	 * @param boolean $use_session =true should the session prefs get used (default true) or not (false)
388
	 * @return array with effective prefs ($this->data)
389
	 */
390
	function read_repository($use_session=true)
391
	{
392
		$this->session = $use_session ? Cache::getSession('preferences','preferences') : array();
393
		if (!is_array($this->session))
394
		{
395
			$this->session = array();
396
		}
397
		$this->forced = $this->default = $this->user = $this->group = array();
398
		$to_read = array(self::DEFAULT_ID,self::FORCED_ID,$this->account_id);
399
		if ($this->account_id > 0)
400
		{
401
			$primary_group = Accounts::id2name($this->account_id, 'account_primary_group');
402
			foreach((array)$GLOBALS['egw']->accounts->memberships($this->account_id, true) as $gid)
403
			{
404
				if ($gid != $primary_group) $to_read[] = $gid + self::DEFAULT_ID;	// need to offset it with DEFAULT_ID = -2!
405
			}
406
			$to_read[] = $primary_group + self::DEFAULT_ID;
407
		}
408
		foreach($this->cache_read($to_read) as $id => $values)
409
		{
410
			switch($id)
411
			{
412
				case self::FORCED_ID:
413
					$this->forced = $values;
414
					break;
415
				case self::DEFAULT_ID:
416
					$this->default = $values;
417
					break;
418
				case $this->account_id:	// user
419
					$this->user = $values;
420
					break;
421
				default:
422
					foreach($values as $app => $vals)
423
					{
424
						$this->group[$app] = (array)$vals + (array)$this->group[$app];
425
					}
426
					break;
427
			}
428
		}
429
		$this->data = $this->user;
430
431
		// let the (temp.) session prefs. override the user prefs.
432
		//
433
		foreach($this->session as $app => $values)
434
		{
435
			foreach($values as $var => $value)
436
			{
437
				$this->data[$app][$var] = $value;
438
			}
439
		}
440
441
		// now use (primary) group defaults if needed (user-value unset or empty)
442
		//
443
		foreach((array)$this->group as $app => $values)
444
		{
445
			foreach((array)$values as $var => $value)
446
			{
447
				if (!isset($this->data[$app][$var]) || $this->data[$app][$var] === '')
448
				{
449
					$this->data[$app][$var] = $value;
450
				}
451
			}
452
		}
453
		// now use defaults if needed (user-value unset or empty)
454
		//
455
		foreach((array)$this->default as $app => $values)
456
		{
457
			foreach((array)$values as $var => $value)
458
			{
459
				if (!isset($this->data[$app][$var]) || $this->data[$app][$var] === '')
460
				{
461
					//if ($var=='remote_application_url') error_log(__METHOD__.__LINE__.' default for '.$var.' with '.$value);
462
					$this->data[$app][$var] = $value;
463
				}
464
			}
465
		}
466
		// now set/force forced values
467
		//
468
		foreach((array)$this->forced as $app => $values)
469
		{
470
			foreach((array)$values as $var => $value)
471
			{
472
				$this->data[$app][$var] = $value;
473
			}
474
		}
475
		// setup the standard substitutes and substitutes the data in $this->data
476
		//
477
		if ($GLOBALS['egw_info']['flags']['load_translations'] !== false)
478
		{
479
			$this->standard_substitutes();
480
		}
481
		// This is to supress warnings during login
482
		if (is_array($this->data))
483
		{
484
			reset($this->data);
485
		}
486
		if (isset($this->debug) && substr($GLOBALS['egw_info']['flags']['currentapp'],0,3) != 'log')
487
		{
488
			echo 'user<pre>';     print_r($this->user); echo "</pre>\n";
489
			echo 'forced<pre>';   print_r($this->forced); echo "</pre>\n";
490
			echo 'default<pre>';  print_r($this->default); echo "</pre>\n";
491
			echo 'group<pre>';    print_r($this->group); echo "</pre>\n";
492
			echo 'effectiv<pre>'; print_r($this->data); echo "</pre>\n";
493
		}
494
		$this->check_set_tz_offset();
495
496
		return $this->data;
497
	}
498
499
	/**
500
	 * Get default preferences (also taking forced preferences into account!)
501
	 *
502
	 * @param string $app =null
503
	 * @param string $name =null
504
	 * @return mixed
505
	 */
506
	function default_prefs($app=null,$name=null)
507
	{
508
		// Etemplate::complete_array_merge() is identical to PHP >= 5.3 array_replace_recursive()
509
		$default = Etemplate::complete_array_merge($this->default, $this->forced);
510
511
		if ($app) $default = $default[$app];
512
513
		if ($name && is_array($default)) $default = $default[$name];
514
515
		return $default;
516
	}
517
518
	/**
519
	 * Checking new timezone ('tz') pref and setting old tz_offset pref from it
520
	 *
521
	 */
522
	function check_set_tz_offset()
523
	{
524
		$prefs =& $this->data['common'];
525
526
		if (!empty($prefs['tz']))
527
		{
528
			DateTime::setUserPrefs($prefs['tz'],$prefs['dateformat'],$prefs['timeformat']);
529
			// set the old preference for compatibilty with old code
530
			$prefs['tz_offset'] = DateTime::tz_offset_s()/3600;
531
		}
532
	}
533
534
	/**
535
	 * Set user timezone, if we get restored from session
536
	 *
537
	 */
538
	function __wakeup()
539
	{
540
		$this->check_set_tz_offset();
541
	}
542
543
	/**
544
	 * read preferences from repository and stores in an array
545
	 *
546
	 * @return array containing the effective user preferences
547
	 */
548
	function read()
549
	{
550
		if (count($this->data) == 0)
551
		{
552
			$this->read_repository();
553
		}
554
		reset ($this->data);
555
		return $this->data;
556
	}
557
558
	/**
559
	 * add preference to $app_name a particular app
560
	 *
561
	 * the effective prefs ($this->data) are updated to reflect the change
562
	 *
563
	 * @param string $app_name name of the app
564
	 * @param string $var name of preference to be stored
565
	 * @param mixed $value ='##undef##' value of the preference, if not given $GLOBALS[$var] is used
566
	 * @param string $type ='user' of preference to set: forced, default, user
567
	 * @return array with new effective prefs (even when forced or default prefs are set !)
568
	 */
569
	function add($app_name,$var,$value = '##undef##',$type='user')
570
	{
571
		//echo "<p>add('$app_name','$var','$value')</p>\n";
572
		if ($value === '##undef##')
573
		{
574
			$value = $GLOBALS[$var];
575
		}
576
577
		switch ($type)
578
		{
579
			case 'session':
580
				if (!isset($this->forced[$app_name][$var]) || $this->forced[$app_name][$var] === '')
581
				{
582
					$this->session[$app_name][$var] = $this->data[$app_name][$var] = $value;
583
					Cache::setSession('preferences','preferences',$this->session);
584
					if (method_exists($GLOBALS['egw'],'invalidate_session_cache'))	// egw object in setup is limited
585
					{
586
						$GLOBALS['egw']->invalidate_session_cache();	// in case with cache the egw_info array in the session
587
					}
588
				}
589
				break;
590
591
			case 'forced':
592
				$this->data[$app_name][$var] = $this->forced[$app_name][$var] = $value;
593
				break;
594
595
			case 'default':
596
				$this->default[$app_name][$var] = $value;
597
				if ((!isset($this->forced[$app_name][$var]) || $this->forced[$app_name][$var] === '') &&
598
					(!isset($this->user[$app_name][$var]) || $this->user[$app_name][$var] === ''))
599
				{
600
					$this->data[$app_name][$var] = $value;
601
				}
602
				break;
603
604
			case 'user':
605
			default:
606
				$this->user[$app_name][$var] = $value;
607
				if (!isset($this->forced[$app_name][$var]) || $this->forced[$app_name][$var] === '')
608
				{
609
					$this->data[$app_name][$var] = $value;
610
				}
611
				break;
612
		}
613
		reset($this->data);
614
		return $this->data;
615
	}
616
617
	/**
618
	 * delete preference from $app_name
619
	 *
620
	 * the effektive prefs ($this->data) are updated to reflect the change
621
	 *
622
	 * @param string $app_name name of app
623
	 * @param string $var =false variable to be deleted
624
	 * @param string $type ='user' of preference to set: forced, default, user
625
	 * @return array with new effective prefs (even when forced or default prefs are deleted!)
626
	 */
627
	function delete($app_name, $var = False,$type = 'user')
628
	{
629
		//echo "<p>delete('$app_name','$var','$type')</p>\n";
630
		$set_via = array(
631
			'forced'  => array('user','default'),
632
			'default' => array('forced','user'),
633
			'user'    => array('forced','group','default'),
634
			'group'   => array('forced'),
635
		);
636
		if (!isset($set_via[$type]))
637
		{
638
			$type = 'user';
639
		}
640
		$pref = &$this->$type;
641
642
		if (($all = empty($var))) // to check if $var is regarded as empty (false, 0, '', null, array() should do the trick
643
		{
644
			unset($pref[$app_name]);
645
			unset($this->data[$app_name]);
646
		}
647
		else
648
		{
649
			unset($pref[$app_name][$var]);
650
			unset($this->data[$app_name][$var]);
651
		}
652
		// set the effectiv pref again if needed
653
		//
654
		foreach ($set_via[$type] as $set_from)
655
		{
656
			$arr = &$this->$set_from;
657
			if ($all)
658
			{
659
				if (isset($arr[$app_name]))
660
				{
661
					$this->data[$app_name] = $arr[$app_name];
662
					break;
663
				}
664
			}
665
			else
666
			{
667
				if($var && @isset($arr[$app_name][$var]) && $arr[$app_name][$var] !== '')
668
				{
669
					$this->data[$app_name][$var] = $arr[$app_name][$var];
670
					break;
671
				}
672
			}
673
			unset($arr);
674
		}
675
		reset ($this->data);
676
		return $this->data;
677
	}
678
679
	/**
680
	 * delete all prefs of a given user
681
	 *
682
	 * @param int $accountid
683
	 */
684
	function delete_user($accountid)
685
	{
686
		if ($accountid > 0)
687
		{
688
			$this->db->delete($this->table,array('preference_owner' => $accountid),__LINE__,__FILE__);
689
690
			Cache::unsetInstance(__CLASS__, $accountid);
691
		}
692
	}
693
694
	/**
695
	 * delete all prefs of a given group
696
	 *
697
	 * @param int $accountid
698
	 */
699
	function delete_group($accountid)
700
	{
701
		if ($accountid < 0)
702
		{
703
			$this->db->delete($this->table,array('preference_owner' => $accountid+self::DEFAULT_ID),__LINE__,__FILE__);
704
705
			Cache::unsetInstance(__CLASS__, $accountid+self::DEFAULT_ID);
706
		}
707
	}
708
709
	/**
710
	 * Change single value in preferences of all users (incl. groups, default and forced)
711
	 *
712
	 * @param string $app app-name or null for all apps
713
	 * @param string $name attribute name or regular expression (enclosed in /) to match attribute-name eg. '/^favorite_/'
714
	 * @param string|callable $value new value to set, or null or '' to delete it or callable returning new value: function($attr, $old_value, $owner, $prefs)
715
	 * @param string $old_value if given, only change if that's current value
716
	 * @param string $type if given limit to "user", "forced", "default", "group"
717
	 */
718
	public static function change_preference($app, $name, $value, $old_value=null, $type=null)
719
	{
720
		$db = isset($GLOBALS['egw_setup']->db) ? $GLOBALS['egw_setup']->db : $GLOBALS['egw']->db;
721
722
		$where = array();
723
		if ($app) $where['preference_app'] = $app;
724
725
		switch($type)
726
		{
727
			case 'forced':
728
				$where['preference_owner'] = self::FORCED_ID;
729
				break;
730
			case 'default':
731
				$where['preference_owner'] = self::DEFAULT_ID;
732
				break;
733
			case 'user':
734
				$where[] = 'preference_owner > 0';
735
				break;
736
			case 'group':
737
				$where[] = 'preference_owner < '.self::DEFAULT_ID;
738
				break;
739
		}
740
		foreach($db->select(self::TABLE, '*', $where, __LINE__, __FILE__) as $row)
741
		{
742
			$prefs = self::unserialize($row['preference_value']);
743
			if (!is_array($prefs)) $prefs = array();	// would stall update otherwise
744
745
			if ($name[0] == '/' && substr($name, -1) == '/')
746
			{
747
				$attrs = array_filter(array_keys($prefs), function($n) use ($name)
748
				{
749
					return preg_match($name, $n);
750
				});
751
			}
752
			else
753
			{
754
				$attrs = array($name);
755
			}
756
757
			$updated = false;
758
			foreach($attrs as $attr)
759
			{
760
				if (isset($old_value) && $prefs[$attr] != $old_value) continue;
761
762
				$val = is_callable($value) ? call_user_func($value, $attr, $prefs[$attr], $row['preference_owner'], $prefs) : $value;
763
				if ($val === $prefs[$attr]) continue;
764
765
				$updated = true;
766
				if ((string)$val !== '')
767
				{
768
					$prefs[$attr] = $val;
769
				}
770
				else
771
				{
772
					unset($prefs[$attr]);
773
				}
774
			}
775
			// if somethings changed or old row was php-serialized --> store it again json-encoded
776
			if ($updated || $row['preference_value'][0] == 'a' && $row['preference_value'][1] == ':')
777
			{
778
				$db->update(self::TABLE, array(
779
					'preference_value' => json_encode($prefs),
780
				), array(
781
					'preference_owner' => $row['preference_owner'],
782
					'preference_app'   => $row['preference_app'],
783
				), __LINE__, __FILE__);
784
785
				// update instance-wide cache
786
				$cached = Cache::getInstance(__CLASS__, $row['preference_owner']);
787
				if($cached && $cached[$row['preference_app']])
788
				{
789
					$cached[$row['preference_app']] = $prefs;
790
					Cache::setInstance(__CLASS__, $row['preference_owner'], $cached);
791
				}
792
			}
793
		}
794
	}
795
796
	/**
797
	 * Completely delete the specified preference name from all users
798
	 *
799
	 * @param string $app Application name
800
	 * @param string $name Preference name
801
	 * @param string $type ='user' of preference to set: forced, default, user
802
	 */
803
	public static function delete_preference($app, $name, $type='user')
804
	{
805
		self::change_preference($app, $name, null, null, $type);
806
	}
807
808
	/**
809
	 * Copy preferences from one app to an other
810
	 *
811
	 * @param string $from_app
812
	 * @param string $to_app
813
	 * @param array $names =null array of names to copy or null for all
814
	 */
815
	public static function copy_preferences($from_app, $to_app, array $names=null)
816
	{
817
		//error_log(__METHOD__."('$from_app', '$to_app', ".array2string($names).')');
818
		$db = isset($GLOBALS['egw_setup']->db) ? $GLOBALS['egw_setup']->db : $GLOBALS['egw']->db;
819
820
		foreach($db->select(self::TABLE, '*', array('preference_app' => $from_app), __LINE__, __FILE__) as $row)
821
		{
822
			$prefs = self::unserialize($row['preference_value']);
823
824
			if ($names)
825
			{
826
				$prefs = array_intersect_key($prefs, array_flip($names));
827
			}
828
			if (!$prefs) continue;	// nothing to change, as nothing set
0 ignored issues
show
Bug Best Practice introduced by
The expression $prefs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
829
830
			$row['preference_app'] = $to_app;
831
			unset($row['preference_value']);
832
833
			if (($values = $db->select(self::TABLE, 'preference_value', $row, __LINE__, __FILE__)->fetchColumn()))
834
			{
835
				$prefs = array_merge(self::unserialize($values), $prefs);
836
			}
837
			unset($row['preference_id']);
838
			//error_log(__LINE__.': '.__METHOD__."() inserting app=$row[preference_app], owner=$row[preference_owner]: ".array2string($prefs));
839
			$db->insert(self::TABLE, array(
840
				'preference_value' => json_encode($prefs)
841
			), $row, __LINE__, __FILE__);
842
843
			// update instance-wide cache
844
			if (($cached = Cache::getInstance(__CLASS__, $row['prefences_owner'])))
845
			{
846
				$cached[$from_app] = $prefs;
847
				Cache::setInstance(__CLASS__, $row['preference_owner'], $cached);
848
			}
849
		}
850
	}
851
852
	/**
853
	 * Save the the preferences to the repository
854
	 *
855
	 * User prefs for saveing are in $this->user not in $this->data, which are the effectiv prefs only!
856
	 *
857
	 * @param boolean $update_session_info =false old param, seems not to be used (not used anymore)
858
	 * @param string $type ='user' which prefs to update: user/default/forced
859
	 * @param boolean $invalid_cache =true should we invalidate the cache, default true (not used anymore)
860
	 * @return array with new effective prefs (even when forced or default prefs are deleted!)
861
	 */
862
	function save_repository($update_session_info = False,$type='user',$invalid_cache=true)
863
	{
864
		unset($update_session_info, $invalid_cache);	// no longer used
865
866
		switch($type)
867
		{
868
			case 'forced':
869
				$account_id = self::FORCED_ID;
870
				$prefs = &$this->forced;
871
				break;
872
			case 'default':
873
				$account_id = self::DEFAULT_ID;
874
				$prefs = &$this->default;
875
				break;
876
			case 'group':
877
				throw new Exception\WrongParameter("Can NOT save group preferences, as they are from multiple groups!");
878
879
			default:
880
				$account_id = (int)$this->account_id;
881
				$prefs = &$this->user;	// we use the user-array as data contains default values too
882
				break;
883
		}
884
		//echo "<p>preferences::save_repository(,$type): account_id=$account_id, prefs="; print_r($prefs); echo "</p>\n";
885
886
		if (isset($GLOBALS['egw_setup']) || !$GLOBALS['egw']->acl->check('session_only_preferences',1,'preferences') &&
887
			(!($old_prefs = $this->cache_read($account_id)) || $old_prefs != $prefs))
888
		{
889
			//error_log(__METHOD__."(type=$type) saved, because old_prefs[$account_id] != prefs=".array2string($prefs));
890
			$changed = 0;
891
			foreach($prefs as $app => $value)
892
			{
893
				// check if app preferences have changed, if not no need to save them
894
				if ($old_prefs && $old_prefs[$app] == $value) continue;
895
896
				if (!$changed++) $this->db->transaction_begin();
897
898
				if (!is_array($value) || !$value)
0 ignored issues
show
Bug Best Practice introduced by
The expression $value of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
899
				{
900
					$this->db->delete($this->table, array(
901
						'preference_owner' => $account_id,
902
						'preference_app'   => $app,
903
					), __LINE__, __FILE__);
904
					unset($prefs[$app]);
905
				}
906
				else
907
				{
908
					$this->db->insert($this->table,array(
909
						'preference_value' => json_encode($value),
910
					),array(
911
						'preference_owner' => $account_id,
912
						'preference_app'   => $app,
913
					),__LINE__,__FILE__);
914
				}
915
			}
916
			if ($changed)
917
			{
918
				$this->db->transaction_commit();
919
920
				// update instance-wide cache
921
				Cache::setInstance(__CLASS__, $account_id, $prefs);
922
			}
923
		}
924
		//else error_log(__METHOD__."(type=$type) NOT saved because old_prefs[$account_id] == prefs=".array2string($prefs));
925
		return $this->data;
926
	}
927
928
	/**
929
	 * returns the custom email-address (if set) or generates a default one
930
	 *
931
	 * This will generate the appropriate email address used as the "From:"
932
	 * email address when the user sends email, the localpert * part. The "personal"
933
	 * part is generated elsewhere.
934
	 * In the absence of a custom ['email']['address'], this function should be used to set it.
935
	 *
936
	 * @access public
937
	 * @param int $account_id as determined in and/or passed to "create_email_preferences"
938
	 * @return string with email-address
939
	 */
940
	function email_address($account_id='')
941
	{
942
		if (isset($this->data['email']['address']))
943
		{
944
			return $this->data['email']['address'];
945
		}
946
		// if email-address is set in the account, return it
947
		if (($email = Accounts::id2name($account_id,'account_email')))
948
		{
949
			return $email;
950
		}
951
		$prefs_email_address = Accounts::id2name($account_id);
952
		if ($prefs_email_address && strpos($prefs_email_address,'@') === False)
953
		{
954
			$prefs_email_address .= '@' . $GLOBALS['egw_info']['server']['mail_suffix'];
955
		}
956
		return $prefs_email_address;
957
	}
958
959
	/**
960
	 * Try to guess and set a locale supported by the server, with fallback to 'en_EN' and 'C'
961
	 *
962
	 * This method uses the language and nationalty set in the users common prefs.
963
	 *
964
	 * @param $category =LC_MESSAGES category to set, see setlocal function
0 ignored issues
show
Documentation Bug introduced by
The doc comment =LC_MESSAGES at position 0 could not be parsed: Unknown type name '=LC_MESSAGES' at position 0 in =LC_MESSAGES.
Loading history...
965
	 * @param $charset =null default system charset
966
	 * @return string the local (or best estimate) set
967
	 */
968
	static function setlocale($category=LC_MESSAGES,$charset=null)
969
	{
970
		$lang = $GLOBALS['egw_info']['user']['preferences']['common']['lang'];
971
		$country = $GLOBALS['egw_info']['user']['preferences']['common']['country'];
972
973
		if (strlen($lang) == 2)
974
		{
975
			$country_from_lang = strtoupper($lang);
976
		}
977
		else
978
		{
979
			list($lang,$lang2) = explode('-',$lang);
980
			$country_from_lang = strtoupper($lang2);
981
		}
982
		if (is_null($charset)) $charset = Translation::charset();
983
984
		foreach(array(
985
			$lang.'_'.$country,
986
			$lang.'_'.$country_from_lang,
987
			$lang.'_'.$country.'.utf8',
988
			$lang.'_'.$country_from_lang.'.utf8',
989
			$lang,
990
			'en_US',
991
			'C',
992
		) as $local)
993
		{
994
			if (($ret = setlocale($category,$local.'.'.$charset)) ||
995
				($ret = setlocale($category,$local.'@'.$charset)) ||
996
				($ret = setlocale($category,$local)))
997
			{
998
				//error_log(__METHOD__."($category,$charset) lang=$lang, country=$country, country_from_lang=$country_from_lang: returning '$ret'");
999
				return $ret;
1000
			}
1001
		}
1002
		error_log(__METHOD__."($category,$charset) lang=$lang, country=$country, country_from_lang=$country_from_lang: Could not set local!");
1003
		return false;	// should not happen, as the 'C' local should at least be available everywhere
1004
	}
1005
1006
	/**
1007
	 * Get preferences by calling various hooks to supply them
1008
	 *
1009
	 * @param string $_appname appname or 'common'
1010
	 * @param string $type ='user' 'default', 'forced', 'user' or 'group'
1011
	 * @param int|string $account_id =null account_id for user or group prefs, or "forced" or "default"
1012
	 * @return array
1013
	 */
1014
	public static function settings($_appname, $type='user', $account_id=null)
1015
	{
1016
		$all_settings = [];
1017
		$appname = $_appname == 'common' ? 'preferences' : $_appname;
1018
1019
		// make type available, to hooks from applications can use it, eg. activesync
1020
		$hook_data = array(
1021
			'location' => 'settings',
1022
			'type' => $type,
1023
			'account_id' => $account_id,
1024
		);
1025
		$GLOBALS['type'] = $type;	// old global variable
1026
1027
		// calling app specific settings hook
1028
		$settings = Hooks::single($hook_data, $appname);
1029
		// it either returns the settings or save it in $GLOBALS['settings'] (deprecated!)
1030
		if (isset($settings) && is_array($settings) && $settings)
1031
		{
1032
			$all_settings = array_merge($all_settings, $settings);
1033
		}
1034
		elseif(isset($GLOBALS['settings']) && is_array($GLOBALS['settings']) && $GLOBALS['settings'])
1035
		{
1036
			$all_settings = array_merge($all_settings, $GLOBALS['settings']);
1037
		}
1038
		else
1039
		{
1040
			return False;	// no settings returned
1041
		}
1042
1043
		// calling settings hook all apps can answer (for a specific app)
1044
		$hook_data['location'] = 'settings_'.$appname;
1045
		foreach(Hooks::process($hook_data, $appname,true) as $settings)
1046
		{
1047
			if (isset($settings) && is_array($settings) && $settings)
0 ignored issues
show
Bug Best Practice introduced by
The expression $settings of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1048
			{
1049
				$all_settings = array_merge($all_settings,$settings);
1050
			}
1051
		}
1052
		return $all_settings;
1053
	}
1054
}
1055