Ldap::_read_group()   B
last analyzed

Complexity

Conditions 8
Paths 18

Size

Total Lines 54
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 32
nc 18
nop 1
dl 0
loc 54
rs 8.1635
c 0
b 0
f 0

How to fix   Long Method   

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
 * API - accounts LDAP backend
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de> complete rewrite in 6/2006
7
 *
8
 * This class replaces the former accounts_ldap class written by
9
 * Joseph Engo <[email protected]>, Lars Kneschke <[email protected]>,
10
 * Miles Lott <[email protected]> and Bettina Gille <[email protected]>.
11
 * Copyright (C) 2000 - 2002 Joseph Engo, Lars Kneschke
12
 * Copyright (C) 2003 Lars Kneschke, Bettina Gille
13
 *
14
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
15
 * @package api
16
 * @subpackage accounts
17
 */
18
19
namespace EGroupware\Api\Accounts;
20
21
use EGroupware\Api;
22
23
// explicitly reference classes still in phpgwapi or old structure
24
use setup_cmd_ldap;
25
26
/**
27
 * LDAP Backend for accounts
28
 *
29
 * The LDAP backend of the accounts class now stores accounts, groups and the memberships completly in LDAP.
30
 * It does NO longer use the ACL class/table for group membership information.
31
 * Nor does it use the phpgwAcounts schema (part of that information is stored via shadowAccount now).
32
 *
33
 * A user is recogniced by eGW, if he's in the user_context tree AND has the posixAccount object class AND
34
 * matches the LDAP search filter specified in setup >> configuration.
35
 * A group is recogniced by eGW, if it's in the group_context tree AND has the posixGroup object class.
36
 * The group members are stored as memberuid's.
37
 *
38
 * The (positive) group-id's (gidnumber) of LDAP groups are mapped in this class to negative numeric
39
 * account_id's to not conflict with the user-id's, as both share in eGW internaly the same numberspace!
40
 *
41
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
42
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
43
 * @access internal only use the interface provided by the accounts class
44
 */
45
class Ldap
46
{
47
	/**
48
	 * Name of mail attribute
49
	 */
50
	const MAIL_ATTR = 'mail';
51
	/**
52
	 * resource with connection to the ldap server
53
	 *
54
	 * @var resource
55
	 */
56
	var $ds;
57
	/**
58
	 * LDAP context for users, eg. ou=account,dc=domain,dc=com
59
	 *
60
	 * @var string
61
	 */
62
	var $user_context;
63
	/**
64
	 * LDAP search filter for user accounts, eg. (uid=%user)
65
	 *
66
	 * @var string
67
	 */
68
	var $account_filter;
69
	/**
70
	 * LDAP context for groups, eg. ou=groups,dc=domain,dc=com
71
	 *
72
	 * @var string
73
	 */
74
	var $group_context;
75
	/**
76
	 * total number of found entries from get_list method
77
	 *
78
	 * @var int
79
	 */
80
	var $total;
81
82
	var $ldapServerInfo;
83
84
	/**
85
	 * required classe for user and groups
86
	 *
87
	 * @var array
88
	 */
89
	var $requiredObjectClasses = array(
90
		'user' => array(
91
			'top','person','organizationalperson','inetorgperson','posixaccount','shadowaccount'
92
		),
93
		'user-if-supported' => array(	// these classes get added, if server supports them
94
			'mozillaabpersonalpha', 'mozillaorgperson', 'evolutionperson',
95
			'univentionperson', 'univentionmail', array('univentionobject', 'univentionObjectType' => 'users/user'),
96
		),
97
		'group' => array(
98
			'top','posixgroup','groupofnames'
99
		),
100
		'group-if-supported' => array(	// these classes get added, if servers supports them
101
			'univentiongroup', array('univentionobject', 'univentionObjectType' => 'groups/group'),
102
		)
103
	);
104
	/**
105
	 * Classes allowing to set a mail-address for a group and specify the memberaddresses as forwarding addresses
106
	 *
107
	 * $objectclass => $forward
108
	 * $objectclass => [$forward, $extra_attr, $keep_objectclass]
109
	 * $forward          : name of attribute to set forwards for members mail addresses, false if not used/required
110
	 * $extra_attr       : required attribute (eg. 'uid'), which need to be set, default none
111
	 * $keep_objectclass : true to not remove objectclass, if not mail set
112
	 *
113
	 * @var array
114
	 */
115
	var $group_mail_classes = array(
116
		'dbmailforwardingaddress' => 'mailforwardingaddress',
117
		'dbmailuser' => array('mailforwardingaddress','uid'),
118
		'qmailuser' => array('mailforwardingaddress','uid'),
119
		'mailaccount' => 'mailalias',
120
		'univentiongroup' => array(false, false, true),
121
	);
122
123
	/**
124
	 * Reference to our frontend
125
	 *
126
	 * @var Api\Accounts
127
	 */
128
	protected $frontend;
129
130
	/**
131
	 * Instance of the ldap class
132
	 *
133
	 * @var Api\Ldap
134
	 */
135
	private $ldap;
136
137
	/**
138
	 * does backend allow to change account_lid
139
	 */
140
	const CHANGE_ACCOUNT_LID = true;
141
142
	/**
143
	 * does backend requires password to be set, before allowing to enable an account
144
	 */
145
	const REQUIRE_PASSWORD_FOR_ENABLE = false;
146
147
	/**
148
	 * Constructor
149
	 *
150
	 * @param Api\Accounts $frontend reference to the frontend class, to be able to call it's methods if needed
151
	 */
152
	function __construct(Api\Accounts $frontend)
153
	{
154
		$this->frontend = $frontend;
155
156
		// enable the caching in the session, done by the accounts class extending this class.
157
		$this->use_session_cache = true;
0 ignored issues
show
Bug Best Practice introduced by
The property use_session_cache does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
158
159
		$this->ldap = Api\Ldap::factory(false, $this->frontend->config['ldap_host'],
0 ignored issues
show
Documentation Bug introduced by
It seems like EGroupware\Api\Ldap::fac...config['ldap_root_pw']) can also be of type resource. However, the property $ldap is declared as type EGroupware\Api\Ldap. 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...
160
			$this->frontend->config['ldap_root_dn'],$this->frontend->config['ldap_root_pw']);
161
		$this->ds = $this->ldap->ds;
162
163
		$this->user_context  = $this->frontend->config['ldap_context'];
164
		$this->account_filter = $this->frontend->config['ldap_search_filter'];
165
		$this->group_context = $this->frontend->config['ldap_group_context'] ?
166
			$this->frontend->config['ldap_group_context'] : $this->frontend->config['ldap_context'];
167
	}
168
169
	/**
170
	 * Reads the data of one account
171
	 *
172
	 * @param int $account_id numeric account-id
173
	 * @return array|boolean array with account data (keys: account_id, account_lid, ...) or false if account not found
174
	 */
175
	function read($account_id)
176
	{
177
		if (!(int)$account_id) return false;
178
179
		if ($account_id < 0)
180
		{
181
			return $this->_read_group($account_id);
182
		}
183
		return $this->_read_user($account_id);
184
	}
185
186
	/**
187
	 * Saves / adds the data of one account
188
	 *
189
	 * If no account_id is set in data the account is added and the new id is set in $data.
190
	 *
191
	 * @param array $data array with account-data
192
	 * @return int|boolean the account_id or false on error
193
	 */
194
	function save(&$data)
195
	{
196
		$is_group = $data['account_id'] < 0 || $data['account_type'] === 'g';
197
198
		$data_utf8 = Api\Translation::convert($data,Api\Translation::charset(),'utf-8');
199
		$members = $data['account_members'];
200
201
		if (!is_object($this->ldapServerInfo))
202
		{
203
			$this->ldapServerInfo = $this->ldap->getLDAPServerInfo($this->frontend->config['ldap_host']);
0 ignored issues
show
Unused Code introduced by
The call to EGroupware\Api\Ldap::getLDAPServerInfo() has too many arguments starting with $this->frontend->config['ldap_host']. ( Ignorable by Annotation )

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

203
			/** @scrutinizer ignore-call */ 
204
   $this->ldapServerInfo = $this->ldap->getLDAPServerInfo($this->frontend->config['ldap_host']);

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...
204
		}
205
		// common code for users and groups
206
		// checks if accout_lid (dn) has been changed or required objectclass'es are missing
207
		if ($data_utf8['account_id'] && $data_utf8['account_lid'])
208
		{
209
			// read the entry first, to check if the dn (account_lid) has changed
210
			$sri = $is_group ? ldap_search($this->ds,$this->group_context,'gidnumber='.abs($data['account_id'])) :
211
				ldap_search($this->ds,$this->user_context,'uidnumber='.$data['account_id']);
212
			$old = ldap_get_entries($this->ds, $sri);
213
214
			if (!$old['count'])
215
			{
216
				unset($old);
217
			}
218
			else
219
			{
220
				$old = Api\Ldap::result2array($old[0]);
221
				$old['objectclass'] = array_map('strtolower', $old['objectclass']);
222
				$key = false;
223
				if ($is_group && ($key = array_search('namedobject',$old['objectclass'])) !== false ||
224
					$is_group && ($old['cn'] != $data_utf8['account_lid'] || substr($old['dn'],0,3) != 'cn=') ||
225
					!$is_group && ($old['uid'] != $data_utf8['account_lid'] || substr($old['dn'],0,4) != 'uid='))
226
				{
227
					// query the memberships to set them again later
228
					if (!$is_group)
229
					{
230
						$memberships = $this->memberships($data['account_id']);
231
					}
232
					else
233
					{
234
						$members = $old ? $old['memberuid'] : $this->members($data['account_id']);
235
					}
236
					// if dn has changed --> delete the old entry, as we cant rename the dn
237
					$this->delete($data['account_id']);
238
					unset($old['dn']);
239
					// removing the namedObject object-class, if it's included
240
					if ($key !== false) unset($old['objectclass'][$key]);
0 ignored issues
show
introduced by
The condition $key !== false is always false.
Loading history...
241
					$to_write = $old;
242
					unset($old);
243
				}
244
			}
245
		}
246
		if (!$data['account_id'])	// new account
247
		{
248
			if (!($data['account_id'] = $data_utf8['account_id'] = $this->_get_nextid($is_group ? 'g' : 'u')))
249
			{
250
				return false;
251
			}
252
		}
253
		// check if we need to write the objectclass: new entry or required object classes are missing
254
		if (!$old || array_diff($this->requiredObjectClasses[$is_group ? 'group' : 'user'],$old['objectclass']))
255
		{
256
			// additional objectclasse might be already set in $to_write or $old
257
			if (!is_array($to_write['objectclass']))
258
			{
259
				$to_write['objectclass'] = $old ? $old['objectclass'] : array();
260
			}
261
			if (!$old)	// for new accounts add additional addressbook object classes, if supported by server
262
			{			// as setting them later might loose eg. password, if we are not allowed to read them
263
				foreach($this->requiredObjectClasses[$is_group?'group-if-supported':'user-if-supported'] as $additional)
264
				{
265
					$add = array();
266
					if (is_array($additional))
267
					{
268
						$add = $additional;
269
						$additional = array_shift($add);
270
					}
271
					if ($this->ldapServerInfo->supportsObjectClass($additional))
272
					{
273
						$to_write['objectclass'][] = $additional;
274
						if ($add) $to_write += $add;
275
					}
276
				}
277
			}
278
			$to_write['objectclass'] = array_values(array_unique(array_merge($to_write['objectclass'],
279
				$this->requiredObjectClasses[$is_group ? 'group' : 'user'])));
280
		}
281
		if (!($dn = $old['dn']))
282
		{
283
			if (!$data['account_lid']) return false;
284
285
			$dn = $is_group ? 'cn='.$data_utf8['account_lid'].','.$this->group_context :
286
				'uid='.$data_utf8['account_lid'].','.$this->user_context;
287
		}
288
		// now we merge the user or group data
289
		if ($is_group)
290
		{
291
			$to_write = $this->_merge_group($to_write, $data_utf8, $old);
292
			$data['account_type'] = 'g';
293
294
			$objectclass = $old ? $old['objectclass'] : $to_write['objectclass'];
295
			if ($members || !$old && array_intersect(array('groupofnames','groupofuniquenames','univentiongroup'), $objectclass))
296
			{
297
				$to_write = array_merge($to_write, $this->set_members($members, $data['account_id'], $objectclass, $dn));
298
			}
299
			// check if we should set a mail address and forwards for each member
300
			foreach($this->group_mail_classes as $objectclass => $forward)
301
			{
302
				$extra_attr = false;
303
				$keep_objectclass = false;
304
				if (is_array($forward)) list($forward,$extra_attr,$keep_objectclass) = $forward;
305
306
				if ($this->ldapServerInfo->supportsObjectClass($objectclass) &&
307
					($old && in_array($objectclass,$old['objectclass']) || $data_utf8['account_email'] || $old[static::MAIL_ATTR]))
308
				{
309
					if ($data_utf8['account_email'])	// setting an email
310
					{
311
						if (!in_array($objectclass,$old ? $old['objectclass'] : $to_write['objectclass']))
312
						{
313
							if ($old) $to_write['objectclass'] = $old['objectclass'];
314
							$to_write['objectclass'][] = $objectclass;
315
						}
316
						if ($extra_attr) $to_write[$extra_attr] = $data_utf8['account_lid'];
317
						$to_write[static::MAIL_ATTR] = $data_utf8['account_email'];
318
319
						if ($forward)
320
						{
321
							if (!$members) $members = $this->members($data['account_id']);
322
							$to_write[$forward] = array();
323
							foreach (array_keys($members) as $member)
324
							{
325
								if (($email = $this->id2name($member,'account_email')))
326
								{
327
									$to_write[$forward][] = $email;
328
								}
329
							}
330
						}
331
					}
332
					elseif($old)	// remove the mail and forwards only for existing entries
333
					{
334
						$to_write[static::MAIL_ATTR] = array();
335
						if ($forward) $to_write[$forward] = array();
336
						if ($extra_attr) $to_write[$extra_attr] = array();
337
						if (!$keep_objectclass && ($key = array_search($objectclass,$old['objectclass'])))
338
						{
339
							$to_write['objectclass'] = $old['objectclass'];
340
							unset($to_write['objectclass'][$key]);
341
							$to_write['objectclass'] = array_values($to_write['objectclass']);
342
						}
343
					}
344
					break;
345
				}
346
			}
347
348
		}
349
		else
350
		{
351
			$to_write = $this->_merge_user($to_write,$data_utf8,!$old);
352
			// make sure multiple email-addresses in the mail attribute "survive"
353
			if (isset($to_write[static::MAIL_ATTR]) && is_array($old[static::MAIL_ATTR]) && count($old[static::MAIL_ATTR]) > 1)
354
			{
355
				$mail = $old[static::MAIL_ATTR];
356
				$mail[0] = $to_write[static::MAIL_ATTR];
357
				$to_write[static::MAIL_ATTR] = array_values(array_unique($mail));
358
			}
359
			$data['account_type'] = 'u';
360
361
			// Check if an account already exists as system user, and if it does deny creation
362
			if (!$GLOBALS['egw_info']['server']['ldap_allow_systemusernames'] && !$old &&
363
				function_exists('posix_getpwnam') && posix_getpwnam($data['account_lid']))
364
			{
365
				throw new Api\Exception\WrongUserinput(lang('There already is a system-user with this name. User\'s should not have the same name as a systemuser'));
366
			}
367
		}
368
369
		// remove memberuid when adding a group
370
		if(!$old && is_array($to_write['memberuid']) && empty($to_write['memberuid'])) {
371
			unset($to_write['memberuid']);
372
		}
373
		// modifying or adding the entry
374
		if ($old && !@ldap_modify($this->ds,$dn,$to_write) ||
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($old && ! @ldap_modify(...is->ds, $dn, $to_write), Probably Intended Meaning: $old && (! @ldap_modify(...s->ds, $dn, $to_write))
Loading history...
375
			!$old && !@ldap_add($this->ds,$dn,$to_write))
376
		{
377
			$err = true;
378
			if ($is_group && ($key = array_search('groupofnames',$to_write['objectclass'])) !== false)
379
			{
380
				// try again with removed groupOfNames stuff, as I cant detect if posixGroup is a structural object
381
				unset($to_write['objectclass'][$key]);
382
				$to_write['objectclass'] = array_values($to_write['objectclass']);
383
				unset($to_write['member']);
384
				$err = $old ? !ldap_modify($this->ds,$dn,$to_write) : !ldap_add($this->ds,$dn,$to_write);
385
			}
386
			if ($err)
387
			{
388
				error_log(__METHOD__."() ldap_".($old ? 'modify' : 'add')."(,'$dn',".array2string($to_write).") --> ldap_error()=".ldap_error($this->ds));
389
				return false;
390
			}
391
		}
392
		if ($memberships)
393
		{
394
			$this->set_memberships($memberships,$data['account_id']);
395
		}
396
		return $data['account_id'];
397
	}
398
399
	/**
400
	 * Delete one account, deletes also all acl-entries for that account
401
	 *
402
	 * @param int $account_id numeric account_id
403
	 * @return boolean true on success, false otherwise
404
	 */
405
	function delete($account_id)
406
	{
407
		if (!(int)$account_id) return false;
408
409
		if ($account_id < 0)
410
		{
411
			$sri = ldap_search($this->ds, $this->group_context, 'gidnumber=' . abs($account_id));
412
		}
413
		else
414
		{
415
			// remove the user's memberships
416
			$this->set_memberships(array(),$account_id);
417
418
			$sri = ldap_search($this->ds, $this->user_context, 'uidnumber=' . $account_id);
419
		}
420
		if (!$sri) return false;
0 ignored issues
show
introduced by
$sri is of type resource, thus it always evaluated to false.
Loading history...
421
422
		$allValues = ldap_get_entries($this->ds, $sri);
423
		if (!$allValues['count']) return false;
424
425
		return ldap_delete($this->ds, $allValues[0]['dn']);
426
	}
427
428
	/**
429
	 * Reads the data of one group
430
	 *
431
	 * @internal
432
	 * @param int $account_id numeric account-id (< 0 as it's for a group)
433
	 * @return array|boolean array with account data (keys: account_id, account_lid, ...) or false if account not found
434
	 */
435
	protected function _read_group($account_id)
436
	{
437
		$group = array();
438
		if (!is_object($this->ldapServerInfo))
439
		{
440
			$this->ldapServerInfo = $this->ldap->getLDAPServerInfo($this->frontend->config['ldap_host']);
0 ignored issues
show
Unused Code introduced by
The call to EGroupware\Api\Ldap::getLDAPServerInfo() has too many arguments starting with $this->frontend->config['ldap_host']. ( Ignorable by Annotation )

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

440
			/** @scrutinizer ignore-call */ 
441
   $this->ldapServerInfo = $this->ldap->getLDAPServerInfo($this->frontend->config['ldap_host']);

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...
441
		}
442
		foreach(array_keys($this->group_mail_classes) as $objectclass)
443
		{
444
			if ($this->ldapServerInfo->supportsObjectClass($objectclass))
445
			{
446
				$group['mailAllowed'] = $objectclass;
447
				break;
448
			}
449
		}
450
		$sri = ldap_search($this->ds, $this->group_context,'(&(objectClass=posixGroup)(gidnumber=' . abs($account_id).'))',
451
			array('dn', 'gidnumber', 'cn', 'objectclass', static::MAIL_ATTR, 'memberuid', 'description'));
452
453
		$ldap_data = ldap_get_entries($this->ds, $sri);
454
		if (!$ldap_data['count'])
455
		{
456
			return false;	// group not found
457
		}
458
		$data = Api\Translation::convert($ldap_data[0],'utf-8');
459
		unset($data['objectclass']['count']);
460
461
		$group += array(
462
			'account_dn'        => $data['dn'],
463
			'account_id'        => -$data['gidnumber'][0],
464
			'account_lid'       => $data['cn'][0],
465
			'account_type'      => 'g',
466
			'account_firstname' => $data['cn'][0],
467
			'account_lastname'  => lang('Group'),
468
			'account_fullname'  => lang('Group').' '.$data['cn'][0],
469
			'objectclass'       => array_map('strtolower', $data['objectclass']),
470
			'account_email'     => $data[static::MAIL_ATTR][0],
471
			'members'           => array(),
472
			'account_description' => $data['description'][0],
473
		);
474
475
		if (isset($data['memberuid']))
476
		{
477
			unset($data['memberuid']['count']);
478
479
			foreach($data['memberuid'] as $lid)
480
			{
481
				if (($id = $this->name2id($lid, 'account_lid', 'u')))
482
				{
483
					$group['members'][$id] = $lid;
484
				}
485
			}
486
		}
487
488
		return $group;
489
	}
490
491
	/**
492
	 * Reads the data of one user
493
	 *
494
	 * @internal
495
	 * @param int $account_id numeric account-id
496
	 * @return array|boolean array with account data (keys: account_id, account_lid, ...) or false if account not found
497
	 */
498
	protected function _read_user($account_id)
499
	{
500
		$sri = ldap_search($this->ds, $this->user_context, '(&(objectclass=posixAccount)(uidnumber=' . (int)$account_id.'))',
501
			array('dn','uidnumber','uid','gidnumber','givenname','sn','cn',static::MAIL_ATTR,'userpassword','telephonenumber',
502
				'shadowexpire','shadowlastchange','homedirectory','loginshell','createtimestamp','modifytimestamp'));
503
504
		$ldap_data = ldap_get_entries($this->ds, $sri);
505
		if (!$ldap_data['count'])
506
		{
507
			return false;	// user not found
508
		}
509
		$data = Api\Translation::convert($ldap_data[0],'utf-8');
510
511
		$utc_diff = date('Z');
512
		$user = array(
513
			'account_dn'        => $data['dn'],
514
			'account_id'        => (int)$data['uidnumber'][0],
515
			'account_lid'       => $data['uid'][0],
516
			'account_type'      => 'u',
517
			'account_primary_group' => -$data['gidnumber'][0],
518
			'account_firstname' => $data['givenname'][0],
519
			'account_lastname'  => $data['sn'][0],
520
			'account_email'     => $data[static::MAIL_ATTR][0],
521
			'account_fullname'  => $data['cn'][0],
522
			'account_pwd'       => $data['userpassword'][0],
523
			'account_phone'     => $data['telephonenumber'][0],
524
			// both status and expires are encoded in the single shadowexpire value in LDAP
525
			// - if it's unset an account is enabled AND does never expire
526
			// - if it's set to 0, the account is disabled
527
			// - if it's set to > 0, it will or already has expired --> acount is active if it not yet expired
528
			// shadowexpire is in days since 1970/01/01 (equivalent to a timestamp (int UTC!) / (24*60*60)
529
			'account_status'    => isset($data['shadowexpire']) && $data['shadowexpire'][0]*24*3600+$utc_diff < time() ? false : 'A',
530
			'account_expires'   => isset($data['shadowexpire']) && $data['shadowexpire'][0] ? $data['shadowexpire'][0]*24*3600+$utc_diff : -1, // LDAP date is in UTC
531
			'account_lastpwd_change' => isset($data['shadowlastchange']) ? $data['shadowlastchange'][0]*24*3600+($data['shadowlastchange'][0]!=0?$utc_diff:0) : null,
532
			// lastlogin and lastlogin from are not availible via the shadowAccount object class
533
			// 'account_lastlogin' => $data['phpgwaccountlastlogin'][0],
534
			// 'account_lastloginfrom' => $data['phpgwaccountlastloginfrom'][0],
535
			'person_id'         => $data['uid'][0],	// id of associated contact
536
			'account_created' => isset($data['createtimestamp'][0]) ? self::accounts_ldap2ts($data['createtimestamp'][0]) : null,
537
			'account_modified' => isset($data['modifytimestamp'][0]) ? self::accounts_ldap2ts($data['modifytimestamp'][0]) : null,
538
		);
539
540
		if ($this->frontend->config['ldap_extra_attributes'])
541
		{
542
			$user['homedirectory']  = $data['homedirectory'][0];
543
			$user['loginshell']     = $data['loginshell'][0];
544
		}
545
		return $user;
546
	}
547
548
	/**
549
	 * Merges the group releavant account data from $data into $to_write
550
	 *
551
	 * @internal
552
	 * @param array $to_write data to write to ldap incl. objectclass ($data is NOT yet merged)
553
	 * @param array $data array with account-data in utf-8
554
	 * @return array merged data
555
	 */
556
	protected function _merge_group($to_write,$data,$old=null)
557
	{
558
		$to_write['gidnumber'] = abs($data['account_id']);
559
		$to_write['cn'] = $data['account_lid'];
560
		// do not overwrite exitsting description, if non is given
561
		if (isset($data['account_description']))
562
		{
563
			$to_write['description'] = !empty($data['account_description']) ? $data['account_description'] : array();
564
		}
565
		// to kope with various dependencies / requirements of objectclasses, simply write everything again
566
		foreach($old as $name => $value)
567
		{
568
			if (!isset($to_write[$name]) && !in_array($name, ['dn', 'objectclass']))
569
			{
570
				$to_write[$name] = $value;
571
			}
572
		}
573
		return $to_write;
574
	}
575
576
	/**
577
	 * Merges the user releavant account data from $data into $to_write
578
	 *
579
	 * @internal
580
	 * @param array $to_write data to write to ldap incl. objectclass ($data is NOT yet merged)
581
	 * @param array $data array with account-data in utf-8
582
	 * @param boolean $new_entry
583
	 * @return array merged data
584
	 */
585
	protected function _merge_user($to_write,$data,$new_entry)
586
	{
587
		$to_write['uidnumber'] = $data['account_id'];
588
		$to_write['uid']       = $data['account_lid'];
589
		$to_write['gidnumber'] = abs($data['account_primary_group']);
590
		if (!$new_entry || $data['account_firstname'])
591
		{
592
			$to_write['givenname'] = $data['account_firstname'] ? $data['account_firstname'] : array();
593
		}
594
		$to_write['sn']        = $data['account_lastname'];
595
		if (!$new_entry || $data['account_email'])
596
		{
597
			$to_write[static::MAIL_ATTR]  = $data['account_email'] ? $data['account_email'] : array();
598
		}
599
		$to_write['cn']        = $data['account_fullname'] ? $data['account_fullname'] : $data['account_firstname'].' '.$data['account_lastname'];
600
601
		$utc_diff = date('Z');
602
		if (isset($data['account_passwd']) && $data['account_passwd'])
603
		{
604
			if (preg_match('/^[a-f0-9]{32}$/', $data['account_passwd']))	// md5 --> ldap md5
605
			{
606
				$data['account_passwd'] = setup_cmd_ldap::hash_sql2ldap($data['account_passwd']);
607
			}
608
			elseif (!preg_match('/^\\{[a-z5]{3,5}\\}.+/i',$data['account_passwd']))	// if it's not already entcrypted, do so now
609
			{
610
				$data['account_passwd'] = Api\Auth::encrypt_ldap($data['account_passwd']);
611
			}
612
			$to_write['userpassword'] = $data['account_passwd'];
613
			$to_write['shadowlastchange'] = round((time()-$utc_diff) / (24*3600));
614
		}
615
		// both status and expires are encoded in the single shadowexpire value in LDAP
616
		// - if it's unset an account is enabled AND does never expire
617
		// - if it's set to 0, the account is disabled
618
		// - if it's set to > 0, it will or already has expired --> acount is active if it not yet expired
619
		// shadowexpire is in days since 1970/01/01 (equivalent to a timestamp (int UTC!) / (24*60*60)
620
		$shadowexpire = ($data['account_expires']-$utc_diff) / (24*3600);
621
622
		$to_write['shadowexpire'] = !$data['account_status'] ?
623
			($data['account_expires'] != -1 && $data['account_expires'] < time() ? round($shadowexpire) : 0) :
624
			($data['account_expires'] != -1 ? round($shadowexpire) : array());	// array() = unset value
625
626
		if ($new_entry && is_array($to_write['shadowexpire']) && !count($to_write['shadowexpire']))
627
		{
628
			unset($to_write['shadowexpire']);	// gives protocoll error otherwise
629
		}
630
		//error_log(__METHOD__.__LINE__.$data['account_lid'].'#'.$data['account_lastpwd_change'].'#');
631
		if ($data['account_lastpwd_change']) $to_write['shadowlastchange'] = round(($data['account_lastpwd_change']-$utc_diff)/(24*3600));
632
		if ($data['mustchangepassword'] == 1 || isset($data['account_lastpwd_change']) && $data['account_lastpwd_change'] == 0)
633
		{
634
			$to_write['shadowlastchange'] = 0;
635
		}
636
		// lastlogin and lastlogin from are not availible via the shadowAccount object class
637
		// $to_write['phpgwaccountlastlogin'] = $data['lastlogin'];
638
		// $to_write['phpgwaccountlastloginfrom'] = $data['lastloginfrom'];
639
640
		if ($this->frontend->config['ldap_extra_attributes'])
641
		{
642
			if (isset($data['homedirectory'])) $to_write['homedirectory']  = $data['homedirectory'];
643
			if (isset($data['loginshell'])) $to_write['loginshell'] = $data['loginshell'] ? $data['loginshell'] : array();
644
		}
645
		if (($new_entry || isset($to_write['homedirectory'])) && empty($to_write['homedirectory']))
646
		{
647
			$to_write['homedirectory']  = '/dev/null';	// is a required attribute of posixAccount
648
		}
649
		if ($new_entry && empty($to_write['loginshell']))
650
		{
651
			unset($to_write['loginshell']);	// setting array() for new entry gives "Protocol error", must not set it
652
		}
653
		return $to_write;
654
	}
655
656
	/**
657
	 * Searches / lists accounts: users and/or groups
658
	 *
659
	 * @param array with the following keys:
660
	 * @param $param['type'] string/int 'accounts', 'groups', 'owngroups' (groups the user is a member of), 'both'
0 ignored issues
show
Documentation Bug introduced by
The doc comment string/int at position 0 could not be parsed: Unknown type name 'string/int' at position 0 in string/int.
Loading history...
661
	 *	or integer group-id for a list of members of that group
662
	 * @param $param['start'] int first account to return (returns offset or max_matches entries) or all if not set
663
	 * @param $param['order'] string column to sort after, default account_lid if unset
664
	 * @param $param['sort'] string 'ASC' or 'DESC', default 'ASC' if not set
665
	 * @param $param['query'] string to search for, no search if unset or empty
666
	 * @param $param['query_type'] string:
667
	 *	'all'   - query all fields for containing $param[query]
668
	 *	'start' - query all fields starting with $param[query]
669
	 *	'exact' - query all fields for exact $param[query]
670
	 *	'lid','firstname','lastname','email' - query only the given field for containing $param[query]
671
	 * @param $param['offset'] int - number of matches to return if start given, default use the value in the prefs
672
	 * @param $param['objectclass'] boolean return objectclass(es) under key 'objectclass' in each account
673
	 * @return array with account_id => data pairs, data is an array with account_id, account_lid, account_firstname,
674
	 *	account_lastname, person_id (id of the linked addressbook entry), account_status, account_expires, account_primary_group
675
	 */
676
	function search($param)
677
	{
678
		//error_log(__METHOD__."(".array2string($param).")");
679
		$account_search =& Api\Accounts::$cache['account_search'];
680
681
		// check if the query is cached
682
		$serial = serialize($param);
683
		if (isset($account_search[$serial]))
684
		{
685
			$this->total = $account_search[$serial]['total'];
686
			return $account_search[$serial]['data'];
687
		}
688
		// if it's a limited query, check if the unlimited query is cached
689
		$start = $param['start'];
690
		if (!($maxmatchs = $GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs'])) $maxmatchs = 15;
691
		if (!($offset = $param['offset'])) $offset = $maxmatchs;
692
		unset($param['start']);
693
		unset($param['offset']);
694
		$unl_serial = serialize($param);
695
		if (isset($account_search[$unl_serial]))
696
		{
697
			$this->total = $account_search[$unl_serial]['total'];
698
			$sortedAccounts = $account_search[$unl_serial]['data'];
699
		}
700
		else	// we need to run the unlimited query
701
		{
702
			$query = Api\Ldap::quote(strtolower($param['query']));
703
704
			$accounts = array();
705
			if($param['type'] != 'groups')
706
			{
707
				$filter = "(&(objectclass=posixaccount)";
708
				if (!empty($query) && $query != '*')
709
				{
710
					switch($param['query_type'])
711
					{
712
						case 'all':
713
						default:
714
							$query = '*'.$query;
715
							// fall-through
716
						case 'start':
717
							$query .= '*';
718
							// use now exact, as otherwise groups have "**pattern**", which dont match anything
719
							$param['query_type'] = 'exact';
720
							// fall-through
721
						case 'exact':
722
							$filter .= "(|(uid=$query)(sn=$query)(cn=$query)(givenname=$query)(mail=$query))";
723
							break;
724
						case 'firstname':
725
						case 'lastname':
726
						case 'lid':
727
						case 'email':
728
							$to_ldap = array(
729
								'firstname' => 'givenname',
730
								'lastname'  => 'sn',
731
								'lid'       => 'uid',
732
								'email'     => static::MAIL_ATTR,
733
							);
734
							$filter .= '('.$to_ldap[$param['query_type']].'=*'.$query.'*)';
735
							break;
736
					}
737
				}
738
				// add account_filter to filter (user has to be '*', as we otherwise only search uid's)
739
				$filter .= str_replace(array('%user','%domain'),array('*',$GLOBALS['egw_info']['user']['domain']),$this->account_filter);
740
				$filter .= ')';
741
742
				if ($param['type'] != 'both')
743
				{
744
					// folw:
745
					// - first query only few attributes for sorting and throwing away not needed results
746
					// - throw away & sort
747
					// - fetch relevant accounts with full information
748
					// - map and resolve
749
					$propertyMap = array(
750
						'account_id'        => 'uidnumber',
751
						'account_lid'       => 'uid',
752
						'account_firstname' => 'givenname',
753
						'account_lastname'  => 'sn',
754
						'account_email'     => 'email',
755
						'account_fullname'  => 'cn',
756
						'account_primary_group' => 'gidnumber',
757
					);
758
					$orders = explode(',',$param['order']);
759
					$order = isset($propertyMap[$orders[0]]) ? $propertyMap[$orders[0]] : 'uid';
760
					$sri = ldap_search($this->ds, $this->user_context, $filter,array('uid', $order));
761
					$fullSet = array();
762
					foreach ((array)ldap_get_entries($this->ds, $sri) as $key => $entry)
763
					{
764
						if ($key !== 'count') $fullSet[$entry['uid'][0]] = $entry[$order][0];
765
					}
766
767
					if (is_numeric($param['type'])) // return only group-members
768
					{
769
						$relevantAccounts = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $relevantAccounts is dead and can be removed.
Loading history...
770
						$sri = ldap_search($this->ds,$this->group_context,"(&(objectClass=posixGroup)(gidnumber=" . abs($param['type']) . "))",array('memberuid'));
771
						$group = ldap_get_entries($this->ds, $sri);
772
						$fullSet = $group[0]['memberuid'] ? array_intersect_key($fullSet, array_flip($group[0]['memberuid'])) : array();
773
					}
774
					$totalcount = count($fullSet);
775
776
					$sortFn = $param['sort'] == 'DESC' ? 'arsort' : 'asort';
777
					$sortFn($fullSet);
778
					$relevantAccounts = is_numeric($start) ? array_slice(array_keys($fullSet), $start, $offset) : array_keys($fullSet);
779
					$filter = '(&(objectclass=posixaccount)(|(uid='.implode(')(uid=',$relevantAccounts).'))' . $this->account_filter.')';
780
					$filter = str_replace(array('%user','%domain'),array('*',$GLOBALS['egw_info']['user']['domain']),$filter);
781
				}
782
				$sri = ldap_search($this->ds, $this->user_context, $filter,array('uid','uidNumber','givenname','sn',static::MAIL_ATTR,'shadowExpire','createtimestamp','modifytimestamp','objectclass','gidNumber'));
783
784
				$utc_diff = date('Z');
785
				foreach(ldap_get_entries($this->ds, $sri) as $allVals)
786
				{
787
					settype($allVals,'array');
788
					$test = @$allVals['uid'][0];
789
					if (!$this->frontend->config['global_denied_users'][$test] && $allVals['uid'][0])
790
					{
791
						$account = Array(
792
							'account_id'        => $allVals['uidnumber'][0],
793
							'account_lid'       => Api\Translation::convert($allVals['uid'][0],'utf-8'),
794
							'account_type'      => 'u',
795
							'account_firstname' => Api\Translation::convert($allVals['givenname'][0],'utf-8'),
796
							'account_lastname'  => Api\Translation::convert($allVals['sn'][0],'utf-8'),
797
							'account_status'    => isset($allVals['shadowexpire'][0]) && $allVals['shadowexpire'][0]*24*3600-$utc_diff < time() ? false : 'A',
798
							'account_expires'   => isset($allVals['shadowexpire']) && $allVals['shadowexpire'][0] ? $allVals['shadowexpire'][0]*24*3600+$utc_diff : -1, // LDAP date is in UTC
799
							'account_email'     => $allVals[static::MAIL_ATTR][0],
800
							'account_created' => isset($allVals['createtimestamp'][0]) ? self::accounts_ldap2ts($allVals['createtimestamp'][0]) : null,
801
							'account_modified' => isset($allVals['modifytimestamp'][0]) ? self::accounts_ldap2ts($allVals['modifytimestamp'][0]) : null,
802
							'account_primary_group' => (string)-$allVals['gidnumber'][0],
803
						);
804
						//error_log(__METHOD__."() ldap=".array2string($allVals)." --> account=".array2string($account));
805
						if ($param['active'] && !$this->frontend->is_active($account))
806
						{
807
							if (isset($totalcount)) --$totalcount;
808
							continue;
809
						}
810
						$account['account_fullname'] = Api\Accounts::format_username($account['account_lid'],
811
							$account['account_firstname'], $account['account_lastname'], $allVals['uidnumber'][0]);
812
						// return objectclass(es)
813
						if ($param['objectclass'])
814
						{
815
							$account['objectclass'] = array_map('strtolower', $allVals['objectclass']);
816
							unset($account['objectclass']['count']);
817
						}
818
						$accounts[$account['account_id']] = $account;
819
					}
820
				}
821
			}
822
			if ($param['type'] == 'groups' || $param['type'] == 'both')
823
			{
824
				if(empty($query) || $query == '*')
825
				{
826
					$filter = '(objectclass=posixgroup)';
827
				}
828
				else
829
				{
830
					switch($param['query_type'])
831
					{
832
						case 'all':
833
						default:
834
							$query = '*'.$query;
835
							// fall-through
836
						case 'start':
837
							$query .= '*';
838
							// fall-through
839
						case 'exact':
840
							break;
841
					}
842
					$filter = "(&(objectclass=posixgroup)(cn=$query))";
843
				}
844
				$sri = ldap_search($this->ds, $this->group_context, $filter,array('cn','gidNumber'));
845
				foreach((array)ldap_get_entries($this->ds, $sri) as $allVals)
846
				{
847
					settype($allVals,'array');
848
					$test = $allVals['cn'][0];
849
					if (!$this->frontend->config['global_denied_groups'][$test] && $allVals['cn'][0])
850
					{
851
						$accounts[(string)-$allVals['gidnumber'][0]] = Array(
852
							'account_id'        => -$allVals['gidnumber'][0],
853
							'account_lid'       => Api\Translation::convert($allVals['cn'][0],'utf-8'),
854
							'account_type'      => 'g',
855
							'account_firstname' => Api\Translation::convert($allVals['cn'][0],'utf-8'),
856
							'account_lastname'  => lang('Group'),
857
							'account_status'    => 'A',
858
							'account_fullname'  => Api\Translation::convert($allVals['cn'][0],'utf-8'),
859
						);
860
						if (isset($totalcount)) ++$totalcount;
861
					}
862
				}
863
			}
864
			// sort the array
865
			$this->_callback_sort = strtoupper($param['sort']);
866
			$this->_callback_order = empty($param['order']) ? array('account_lid') : explode(',',$param['order']);
867
			foreach($this->_callback_order as &$col)
868
			{
869
				if (substr($col, 0, 8) !== 'account_') $col = 'account_'.$col;
870
			}
871
			$sortedAccounts = $accounts;
872
			uasort($sortedAccounts,array($this,'_sort_callback'));
873
			$this->total = isset($totalcount) ? $totalcount : count($accounts);
874
875
			// if totalcount is set, $sortedAccounts is NOT the full set, but already a limited set!
876
			if (!isset($totalcount))
877
			{
878
				$account_search[$unl_serial]['data'] = $sortedAccounts;
879
				$account_search[$unl_serial]['total'] = $this->total;
880
			}
881
		}
882
		// return only the wanted accounts
883
		reset($sortedAccounts);
884
		if(is_numeric($start) && is_numeric($offset))
885
		{
886
			$account_search[$serial]['total'] = $this->total;
887
			return $account_search[$serial]['data'] = isset($totalcount) ? $sortedAccounts : array_slice($sortedAccounts, $start, $offset);
888
		}
889
		return $sortedAccounts;
890
	}
891
892
	/**
893
	 * DESC or ASC
894
	 *
895
	 * @var string
896
	 */
897
	private $_callback_sort = 'ASC';
898
	/**
899
	 * column_names to sort by
900
	 *
901
	 * @var array
902
	 */
903
	private $_callback_order = array('account_lid');
904
905
	/**
906
	 * Sort callback for uasort
907
	 *
908
	 * @param array $a
909
	 * @param array $b
910
	 * @return int
911
	 */
912
	function _sort_callback($a,$b)
913
	{
914
		foreach($this->_callback_order as $col )
915
		{
916
			if($this->_callback_sort != 'DESC')
917
			{
918
				$cmp = strcasecmp( $a[$col], $b[$col] );
919
			}
920
			else
921
			{
922
				$cmp = strcasecmp( $b[$col], $a[$col] );
923
			}
924
			if ( $cmp != 0 )
925
			{
926
				return $cmp;
927
			}
928
		}
929
		return 0;
930
	}
931
932
	/**
933
	 * Creates a timestamp from the date returned by the ldap server
934
	 *
935
	 * @internal
936
	 * @param string $date YYYYmmddHHiiss
937
	 * @return int
938
	 */
939
	protected static function accounts_ldap2ts($date)
940
	{
941
		if (!empty($date))
942
		{
943
			return gmmktime(substr($date,8,2),substr($date,10,2),substr($date,12,2),
944
				substr($date,4,2),substr($date,6,2),substr($date,0,4));
945
		}
946
		return NULL;
947
	}
948
949
	/**
950
	 * convert an alphanumeric account-value (account_lid, account_email) to the account_id
951
	 *
952
	 * Please note:
953
	 * - if a group and an user have the same account_lid the group will be returned (LDAP only)
954
	 * - if multiple user have the same email address, the returned user is undefined
955
	 *
956
	 * @param string $_name value to convert
957
	 * @param string $which ='account_lid' type of $name: account_lid (default), account_email, person_id, account_fullname
958
	 * @param string $account_type u = user, g = group, default null = try both
959
	 * @return int|false numeric account_id or false on error ($name not found)
960
	 */
961
	function name2id($_name,$which='account_lid',$account_type=null)
962
	{
963
		$name = Api\Ldap::quote(Api\Translation::convert($_name,Api\Translation::charset(),'utf-8'));
964
965
		if (in_array($which, array('account_lid','account_email')) && $account_type !== 'u') // groups only support account_(lid|email)
966
		{
967
			$attr = $which == 'account_lid' ? 'cn' : static::MAIL_ATTR;
968
			$sri = ldap_search($this->ds, $this->group_context, '(&('.$attr.'=' . $name . ')(objectclass=posixgroup))', array('gidNumber'));
969
			$allValues = ldap_get_entries($this->ds, $sri);
970
971
			if (@$allValues[0]['gidnumber'][0])
972
			{
973
				return -$allValues[0]['gidnumber'][0];
974
			}
975
		}
976
		$to_ldap = array(
977
			'account_lid'   => 'uid',
978
			'account_email' => static::MAIL_ATTR,
979
			'account_fullname' => 'cn',
980
		);
981
		if (!isset($to_ldap[$which]) || $account_type === 'g')
982
		{
983
		    return False;
984
		}
985
986
		$sri = ldap_search($this->ds, $this->user_context, '(&('.$to_ldap[$which].'=' . $name . ')(objectclass=posixaccount))', array('uidNumber'));
987
988
		$allValues = ldap_get_entries($this->ds, $sri);
989
990
		if (@$allValues[0]['uidnumber'][0])
991
		{
992
			return (int)$allValues[0]['uidnumber'][0];
993
		}
994
		return False;
995
	}
996
997
	/**
998
	 * Convert an numeric account_id to any other value of that account (account_lid, account_email, ...)
999
	 *
1000
	 * Uses the read method to fetch all data.
1001
	 *
1002
	 * @param int $account_id numerica account_id
1003
	 * @param string $which ='account_lid' type to convert to: account_lid (default), account_email, ...
1004
	 * @return string|false converted value or false on error ($account_id not found)
1005
	 */
1006
	function id2name($account_id,$which='account_lid')
1007
	{
1008
		return $this->frontend->id2name($account_id,$which);
1009
	}
1010
1011
	/**
1012
	 * Update the last login timestamps and the IP
1013
	 *
1014
	 * @param int $_account_id
1015
	 * @param string $ip
1016
	 * @return int lastlogin time
1017
	 */
1018
	function update_lastlogin($_account_id, $ip)
1019
	{
1020
		unset($_account_id, $ip);
1021
		return false;	// not longer supported
1022
	}
1023
1024
	/**
1025
	 * Query memberships of a given account
1026
	 *
1027
	 * @param int $account_id
1028
	 * @return array|boolean array with account_id => account_lid pairs or false if account not found
1029
	 */
1030
	function memberships($account_id)
1031
	{
1032
		if (!(int) $account_id || !($account_lid = $this->id2name($account_id))) return false;
1033
1034
		$sri = ldap_search($this->ds,$this->group_context,'(&(objectClass=posixGroup)(memberuid='.Api\Ldap::quote($account_lid).'))',array('cn','gidnumber'));
1035
		$memberships = array();
1036
		foreach((array)ldap_get_entries($this->ds, $sri) as $key => $data)
1037
		{
1038
			if ($key === 'count') continue;
1039
1040
			$memberships[(string) -$data['gidnumber'][0]] = $data['cn'][0];
1041
		}
1042
		return $memberships;
1043
	}
1044
1045
	/**
1046
	 * Query the members of a group
1047
	 *
1048
	 * @param int $_gid
1049
	 * @return array|boolean array with uidnumber => uid pairs,
1050
	 *	false if $_git is not nummeric and can't be resolved to a nummeric gid
1051
	 */
1052
	function members($_gid)
1053
	{
1054
		if (!is_numeric($_gid))
0 ignored issues
show
introduced by
The condition is_numeric($_gid) is always true.
Loading history...
1055
		{
1056
			// try to recover
1057
			$_gid = $this->name2id($_gid,'account_lid','g');
1058
			if (!is_numeric($_gid)) return false;
1059
		}
1060
1061
		$gid = abs($_gid);	// our gid is negative!
1062
1063
		$sri = ldap_search($this->ds,$this->group_context,"(&(objectClass=posixGroup)(gidnumber=$gid))",array('memberuid'));
1064
		$group = ldap_get_entries($this->ds, $sri);
1065
1066
		$members = array();
1067
		if (isset($group[0]['memberuid']))
1068
		{
1069
			unset($group[0]['memberuid']['count']);
1070
1071
			foreach($group[0]['memberuid'] as $lid)
1072
			{
1073
				if (($id = $this->name2id($lid, 'account_lid')))	// also return groups!
1074
				{
1075
					$members[$id] = $lid;
1076
				}
1077
			}
1078
		}
1079
		return $members;
1080
	}
1081
1082
	/**
1083
	 * Sets the memberships of the given account
1084
	 *
1085
	 * @param array $groups array with gidnumbers
1086
	 * @param int $account_id uidnumber
1087
	 */
1088
	function set_memberships($groups,$account_id)
1089
	{
1090
		// remove not longer existing memberships
1091
		if (($old_memberships = $this->memberships($account_id)))
1092
		{
1093
			$old_memberships = array_keys($old_memberships);
1094
			foreach(array_diff($old_memberships,$groups) as $gid)
1095
			{
1096
				if (($members = $this->members($gid)))
1097
				{
1098
					unset($members[$account_id]);
1099
					$this->set_members($members,$gid);
1100
				}
1101
			}
1102
		}
1103
		// adding new memberships
1104
		foreach($old_memberships ? array_diff($groups,$old_memberships) : $groups as $gid)
1105
		{
1106
			if (!($members = $this->members($gid))) $members = array();
1107
			$members[$account_id] = $this->id2name($account_id);
1108
			$this->set_members($members,$gid);
1109
		}
1110
	}
1111
1112
	/**
1113
	 * Set the members of a group
1114
	 *
1115
	 * @param array $members array with uidnumber or uid's
1116
	 * @param int $gid gidnumber of group to set
1117
	 * @param array $objectclass =null should we set the member and uniqueMember attributes (groupOf(Unique)Names|univentionGroup) (default detect it)
1118
	 * @param string $use_cn =null if set $cn is used instead $gid and the attributes are returned, not written to ldap
1119
	 * @return boolean/array false on failure, array or true otherwise
0 ignored issues
show
Documentation Bug introduced by
The doc comment boolean/array at position 0 could not be parsed: Unknown type name 'boolean/array' at position 0 in boolean/array.
Loading history...
1120
	 */
1121
	function set_members($members, $gid, array $objectclass=null, $use_cn=null)
1122
	{
1123
		if (!($cn = $use_cn) && !($cn = $this->id2name($gid))) return false;
1124
1125
		// do that group is a groupOf(Unique)Names or univentionGroup?
1126
		if (!isset($objectclass))
1127
		{
1128
			$objectclass = $this->id2name($gid, 'objectclass');
1129
			// if we cant find objectclass, we might ge in the middle of a migration
1130
			if (!isset($objectclass))
1131
			{
1132
				Api\Accounts::cache_invalidate($gid);
1133
				if (!($objectclass = $this->id2name($gid, 'objectclass')))
1134
				{
1135
					// group does not yet exist --> return false
1136
					return false;
1137
				}
1138
			}
1139
		}
1140
1141
		$to_write = array('memberuid' => array());
1142
		foreach((array)$members as $member)
1143
		{
1144
			if (!$member) continue;
1145
1146
			$member_dn = $this->id2name($member, 'account_dn');
1147
			if (is_numeric($member)) $member = $this->id2name($member);
1148
1149
			// only add a member, if we have the neccessary info / he already exists in migration
1150
			if ($member && ($member_dn || !array_intersect(array('groupofnames','groupofuniquenames','univentiongroup'), $objectclass)))
1151
			{
1152
				$to_write['memberuid'][] = $member;
1153
				if (in_array('groupofnames', $objectclass))
1154
				{
1155
					$to_write['member'][] = $member_dn;
1156
				}
1157
				if (array_intersect(array('groupofuniquenames','univentiongroup'), $objectclass))
1158
				{
1159
					$to_write['uniquemember'][] = $member_dn;
1160
				}
1161
			}
1162
		}
1163
		// hack as groupOfNames requires the member attribute
1164
		if (in_array('groupofnames', $objectclass) && !$to_write['member'])
1165
		{
1166
			$to_write['member'][] = 'uid=dummy'.','.$this->user_context;
1167
		}
1168
		if (array_intersect(array('groupofuniquenames','univentiongroup'), $objectclass) && !$to_write['uniquemember'])
1169
		{
1170
			$to_write['uniquemember'][] = 'uid=dummy'.','.$this->user_context;
1171
		}
1172
		if ($use_cn) return $to_write;
1173
1174
		// set the member email addresses as forwards
1175
		if ($this->id2name($gid,'account_email') &&	($objectclass = $this->id2name($gid,'mailAllowed')))
1176
		{
1177
			$forward = $this->group_mail_classes[$objectclass];
1178
			if (is_array($forward)) list($forward,$extra_attr) = $forward;
1179
			if ($extra_attr && ($uid = $this->id2name($gid))) $to_write[$extra_attr] = $uid;
1180
1181
			if ($forward)
1182
			{
1183
				$to_write[$forward] = array();
1184
				foreach($members as $member)
1185
				{
1186
					if (($email = $this->id2name($member,'account_email')))	$to_write[$forward][] = $email;
1187
				}
1188
			}
1189
		}
1190
		if (!ldap_modify($this->ds,'cn='.Api\Ldap::quote($cn).','.$this->group_context,$to_write))
1191
		{
1192
			error_log(__METHOD__."(members=".array2string($members).", gid=$gid, objectclass=".array2string($objectclass).", use_cn=$use_cn) !ldap_modify(,'cn=$cn,$this->group_context', ".array2string($to_write).") --> ldap_error()=".ldap_error($this->ds));
1193
			return false;
1194
		}
1195
		return true;
1196
	}
1197
1198
	/**
1199
	 * Using the common functions next_id and last_id, find the next available account_id
1200
	 *
1201
	 * @internal
1202
	 * @param string $account_type ='u' (optional, default to 'u')
1203
	 * @return int|boolean integer account_id (negative for groups) or false if none is free anymore
1204
	 */
1205
	protected function _get_nextid($account_type='u')
1206
	{
1207
		$min = $this->frontend->config['account_min_id'] ? $this->frontend->config['account_min_id'] : 0;
1208
		$max = $this->frontend->config['account_max_id'] ? $this->frontend->config['account_max_id'] : 0;
1209
1210
		// prefer ids above 1000 (below reserved for system users under AD or Linux),
1211
		// if that's possible within what is configured, or nothing is configured
1212
		if ($min < 1000 && (!$max || $max > 1000)) $min = 1000;
1213
1214
		if ($account_type == 'g')
1215
		{
1216
			$type = 'groups';
1217
			$sign = -1;
1218
		}
1219
		else
1220
		{
1221
			$type = 'accounts';
1222
			$sign = 1;
1223
		}
1224
		/* Loop until we find a free id */
1225
		do
1226
		{
1227
			$account_id = (int) self::next_id($type,$min,$max);
1228
		}
1229
		while ($account_id && ($this->frontend->exists($sign * $account_id) ||	// check need to include the sign!
1230
			$this->frontend->exists(-1 * $sign * $account_id) ||
1231
			// if sambaadmin is installed, call it to check there's not yet a relative id (last part of SID) with that number
1232
			// to ease migration to AD or Samba4
1233
			file_exists(EGW_SERVER_ROOT.'/sambaadmin') && $GLOBALS['egw_info']['apps']['sambaadmin'] &&
1234
				ExecMethod2('sambaadmin.sosambaadmin.sidExists', $account_id)));
0 ignored issues
show
Deprecated Code introduced by
The function ExecMethod2() has been deprecated: use autoloadable class-names, instanciate and call method or use static methods ( Ignorable by Annotation )

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

1234
				/** @scrutinizer ignore-deprecated */ ExecMethod2('sambaadmin.sosambaadmin.sidExists', $account_id)));

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1235
1236
		if	(!$account_id || $max && $account_id > $max)
1237
		{
1238
			return False;
1239
		}
1240
		return $sign * $account_id;
1241
	}
1242
1243
	/**
1244
	 * Return a value for the next id an app/class may need to insert values into LDAP
1245
	 *
1246
	 * @param string $location name for id eg. "groups" or "accounts"
1247
	 * @param int $min =0 if != 0 minimum id
1248
	 * @param int $max =0 if != 0 maximum id allowed, if it would be exceeded we return false
1249
	 * @return int|boolean the next id or false if $max given and exceeded
1250
	 */
1251
	static function next_id($location,$min=0,$max=0)
1252
	{
1253
		if (!$location)
1254
		{
1255
			return -1;
1256
		}
1257
1258
		$id = (int)$GLOBALS['egw_info']['server'][$key='last_id_'.$location];
1259
1260
		if ($max && $id >= $max)
1261
		{
1262
			return False;
1263
		}
1264
		++$id;
1265
1266
		if($id < $min) $id = $min;
1267
1268
		Api\Config::save_value($key, $id, 'phpgwapi', true);
1269
		$GLOBALS['egw_info']['server'][$key='last_id_'.$location] = $id;
0 ignored issues
show
Unused Code introduced by
The assignment to $key is dead and can be removed.
Loading history...
1270
1271
		return (int)$id;
1272
	}
1273
1274
	/**
1275
	 * Return a value for the last id entered, which an app may need to check values for LDAP
1276
	 *
1277
	 * @param string $location name for id eg. "groups" or "accounts"
1278
	 * @param int $min =0 if != 0 minimum id
1279
	 * @param int $max =0 if != 0 maximum id allowed, if it would be exceeded we return false
1280
	 * @return int|boolean current id in the next_id table for a particular app/class or -1 for no app and false if $max is exceeded.
1281
	 */
1282
	static function last_id($location,$min=0,$max=0)
1283
	{
1284
		if (!$location)
1285
		{
1286
			return -1;
1287
		}
1288
1289
		$id = (int)$GLOBALS['egw_info']['server'][$key='last_id_'.$location];
0 ignored issues
show
Unused Code introduced by
The assignment to $key is dead and can be removed.
Loading history...
1290
1291
		if (!$id || $id < $min)
1292
		{
1293
			return self::next_id($location,$min,$max);
1294
		}
1295
		if ($max && $id > $max)
1296
		{
1297
			return False;
1298
		}
1299
		return $id;
1300
	}
1301
1302
	/**
1303
	 * __wakeup function gets called by php while unserializing the object to reconnect with the ldap server
1304
	 */
1305
	function __wakeup()
1306
	{
1307
		$this->ds = Api\Ldap::factory(true, $this->frontend->config['ldap_host'],
0 ignored issues
show
Documentation Bug introduced by
It seems like EGroupware\Api\Ldap::fac...config['ldap_root_pw']) can also be of type EGroupware\Api\Ldap. However, the property $ds is declared as type resource. 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...
1308
			$this->frontend->config['ldap_root_dn'],$this->frontend->config['ldap_root_pw']);
1309
	}
1310
}
1311