Issues (4868)

api/src/Accounts/Ads.php (19 issues)

1
<?php
2
/**
3
 * API - accounts active directory backend
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Ralf Becker <[email protected]>
7
 *
8
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
9
 * @package api
10
 * @subpackage accounts
11
 * @version $Id$
12
 */
13
14
namespace EGroupware\Api\Accounts;
15
16
use EGroupware\Api;
17
18
require_once EGW_INCLUDE_ROOT.'/vendor/adldap2/adldap2/src/adLDAP.php';
19
use adLDAPException;
20
21
/**
22
 * Active directory backend for accounts
23
 *
24
 * RID (realtive id / last part of string-SID) is used as nummeric account-id (negativ for groups).
25
 * SID for queries get reconstructed from account_id by prepending domain-SID.
26
 *
27
 * Easiest way to enable SSL on a win2008r2 DC is to install role "Active Director Certificate Services"
28
 * or in German "Active Directory-Zertificatsdienste" AND reboot.
29
 *
30
 * Changing passwords require ldap_modify_batch method available in PHP 5.4 >= 5.4.26,
31
 * PHP 5.5 >= 5.5.10 or PHP 5.6+. In earlier PHP versions ads_admin user (configured in setup)
32
 * has to have "Reset Password" priveledges!
33
 *
34
 * @access internal only use the interface provided by the accounts class
35
 * @link http://www.selfadsi.org/user-attributes-w2k8.htm
36
 * @link http://www.selfadsi.org/attributes-e2k7.htm
37
 * @link http://msdn.microsoft.com/en-us/library/ms675090(v=vs.85).aspx
38
 */
39
class Ads
40
{
41
	/**
42
	 * Instance of adLDAP class
43
	 *
44
	 * @var adLDAP
45
	 */
46
	private $adldap;
47
	/**
48
	 * total number of found entries from get_list method
49
	 *
50
	 * @var int
51
	 */
52
	public $total;
53
54
	/**
55
	 * Reference to our frontend
56
	 *
57
	 * @var Api\Accounts
58
	 */
59
	protected $frontend;
60
61
	/**
62
	 * Value of expires attribute for never
63
	 */
64
	const EXPIRES_NEVER = '9223372036854775807';
65
66
	/**
67
	 * AD does NOT allow to change sAMAccountName / account_lid
68
	 */
69
	const CHANGE_ACCOUNT_LID = false;
70
71
	/**
72
	 * Backend requires password to be set, before allowing to enable an account
73
	 */
74
	const REQUIRE_PASSWORD_FOR_ENABLE = true;
75
76
	/**
77
	 * Attributes to query to be able to generate account_id and account_lid
78
	 *
79
	 * @var array
80
	 */
81
	protected static $default_attributes = array(
82
		'objectsid', 'samaccounttype', 'samaccountname',
83
	);
84
85
	/**
86
	 * Attributes to query for a user (need to contain $default_attributes!)
87
	 *
88
	 * @var array
89
	 */
90
	protected static $user_attributes = array(
91
		'objectsid', 'samaccounttype', 'samaccountname',
92
		'primarygroupid', 'givenname', 'sn', 'mail', 'displayname', 'telephonenumber',
93
		'objectguid', 'useraccountcontrol', 'accountexpires', 'pwdlastset', 'whencreated', 'whenchanged',
94
	);
95
96
	/**
97
	 * Attributes to query for a group (need to contain $default_attributes!)
98
	 *
99
	 * @var array
100
	 */
101
	protected static $group_attributes = array(
102
		'objectsid', 'samaccounttype', 'samaccountname',
103
		'objectguid', 'mail', 'whencreated', 'whenchanged', 'description',
104
	);
105
106
	/**
107
	 * All users with an account_id below that get ignored, because they are system users (incl. 501="Administrator")
108
	 */
109
	const MIN_ACCOUNT_ID = 1000;
110
111
	/**
112
	 * Enable extra debug messages via error_log (error always get logged)
113
	 */
114
	public static $debug = false;
115
116
	/**
117
	 * Constructor
118
	 *
119
	 * @param Api\Accounts $frontend reference to the frontend class, to be able to call it's methods if needed
120
	 * @throws adLDAPException
121
	 */
122
	function __construct(Api\Accounts $frontend)
123
	{
124
		$this->frontend = $frontend;
125
126
		$this->adldap = self::get_adldap($this->frontend->config);
127
	}
128
129
	/**
130
	 * Factory method and singelton to get adLDAP object for given configuration or default server config
131
	 *
132
	 * @param array $config=null values for keys 'ads_domain', 'ads_host' (required) and optional 'ads_admin_user', 'ads_admin_passwd', 'ads_connection'
133
	 * @return adLDAP
134
	 * @throws adLDAPException
135
	 */
136
	public static function get_adldap(array &$config=null)
137
	{
138
		static $adldap = array();
139
		if (!$config) $config =& $GLOBALS['egw_info']['server'];
140
141
		if (!isset($adldap[$config['ads_domain']]))
142
		{
143
			if (empty($config['ads_host'])) throw new Api\Exception("Required ADS host name(s) missing!");
144
			if (empty($config['ads_domain'])) throw new Api\Exception("Required ADS domain missing!");
145
146
			$base_dn_parts = array();
147
			foreach(explode('.', $config['ads_domain']) as $dc)
148
			{
149
				$base_dn_parts[] = 'DC='.$dc;
150
			}
151
			$base_dn = implode(',', $base_dn_parts);
152
153
			// check if a port is specified as host[:port] and pass it correctly to adLDAP
154
			$matches = null;
155
			if (preg_match('/:(\d+)/', $host=$config['ads_host'], $matches))
156
			{
157
				$port = $matches[1];
158
				$host = preg_replace('/:(\d+)/', '', $config['ads_host']);
159
			}
160
			$options = array(
161
				'domain_controllers' => preg_split('/[ ,]+/', $host),
162
				'base_dn' => $base_dn ? $base_dn : null,
163
				'account_suffix' => '@'.$config['ads_domain'],
164
				'admin_username' => $config['ads_admin_user'],
165
				'admin_password' => $config['ads_admin_passwd'],
166
				'use_tls' => $config['ads_connection'] == 'tls',
167
				'use_ssl' => $config['ads_connection'] == 'ssl',
168
				'charset' => Api\Translation::charset(),
169
			);
170
			if (isset($port)) $options['ad_port'] = $port;
171
172
			$adldap[$config['ads_domain']] = new adLDAP($options);
173
			if (self::$debug) error_log(__METHOD__."() new adLDAP(".array2string($options).") returned ".array2string($adldap[$config['ads_domain']]).' '.function_backtrace());
174
		}
175
		//else error_log(__METHOD__."() returning cached adLDAP ".array2string($adldap[$config['ads_domain']]).' '.function_backtrace());
176
		return $adldap[$config['ads_domain']];
177
	}
178
179
	/**
180
	 * Get SID of domain or an account
181
	 *
182
	 * @param int $account_id
183
	 * @return string|NULL
184
	 */
185
	protected function get_sid($account_id=null)
186
	{
187
		static $domain_sid = null;
188
		if (!isset($domain_sid))
189
		{
190
			$domain_sid = Api\Cache::getCache($this->frontend->config['install_id'], __CLASS__, 'ads_domain_sid');
191
			if ((!is_array($domain_sid) || !isset($domain_sid[$this->frontend->config['ads_domain']])) &&
192
				($adldap = self::get_adldap($this->frontend->config)) &&
193
				($sr = ldap_search($adldap->getLdapConnection(), $adldap->getBaseDn(), '(objectclass=domain)', array('objectsid'))) &&
194
				(($entries = ldap_get_entries($adldap->getLdapConnection(), $sr)) || true))
195
			{
196
				$domain_sid = array();
197
				$domain_sid[$this->frontend->config['ads_domain']] = $adldap->utilities()->getTextSID($entries[0]['objectsid'][0]);
198
				Api\Cache::setCache($this->frontend->config['install_id'], __CLASS__, 'ads_domain_sid', $domain_sid);
199
			}
200
		}
201
		$sid = $domain_sid[$this->frontend->config['ads_domain']];
202
		if ($sid && abs($account_id))
203
		{
204
			$sid .= '-'.abs($account_id);
205
		}
206
		return $sid;
207
	}
208
209
	const DOMAIN_USERS_GROUP = 513;
210
	const ADS_CONTEXT = 'ads_context';
211
212
	/**
213
	 * Get context for user and group objects
214
	 *
215
	 * Can be set via server-config "ads_context", otherwise baseDN is used
216
	 *
217
	 * @param boolean $set_if_empty =false true set from DN of "Domain Users" group #
218
	 * @return string
219
	 */
220
	public function ads_context($set_if_empty=false)
221
	{
222
		if (empty($this->frontend->config[self::ADS_CONTEXT]))
223
		{
224
			if ($set_if_empty && ($dn = $this->id2name(-self::DOMAIN_USERS_GROUP, 'account_dn')))
225
			{
226
				$dn = preg_replace('/^CN=.*?,(CN|OU)=/i', '$1=', $dn);
227
				Api\Config::save_value(self::ADS_CONTEXT, $this->frontend->config[self::ADS_CONTEXT]=$dn, 'phpgwapi');
228
			}
229
			else
230
			{
231
				return $this->adldap->getBaseDn();
232
			}
233
		}
234
		return $this->frontend->config[self::ADS_CONTEXT];
235
	}
236
237
	/**
238
	 * Get container for new user and group objects
239
	 *
240
	 * Can be set via server-config "ads_context", otherwise parent of DN from "Domain Users" is used
241
	 *
242
	 * @return string
243
	 */
244
	protected function _get_container()
245
	{
246
		$context = $this->ads_context(true);
247
		$base = $this->adldap->getBaseDn();
248
		$matches = null;
249
		if (!preg_match('/^(.*),'.preg_quote($base, '/').'$/i', $context, $matches))
250
		{
251
			throw new Api\Exception\WrongUserinput("Wrong or not configured ADS context '$context' (baseDN='$base')!");
252
		}
253
		$container = $matches[1];
254
		if (self::$debug) error_log(__METHOD__."() context='$context', base='$base' returning ".array2string($container));
255
		return $container;
256
	}
257
258
	/**
259
	 * Get connection to ldap server from adLDAP
260
	 *
261
	 * @param boolean $reconnect =false true: reconnect even if already connected
262
	 * @return resource
263
	 */
264
	public function ldap_connection($reconnect=false)
265
	{
266
		if (($reconnect || !($ds = $this->adldap->getLdapConnection())) &&
0 ignored issues
show
Consider adding parentheses for clarity. Current Interpretation: ($reconnect || ! $ds = $...ap->getLdapConnection(), Probably Intended Meaning: $reconnect || ! $ds = $t...p->getLdapConnection())
Loading history...
267
			// call connect, thought I dont know how it can be not connected ...
268
			!$this->adldap->connect() || !($ds = $this->adldap->getLdapConnection()))
269
		{
270
			error_log(__METHOD__."() !this->adldap->getLdapConnection() this->adldap=".array2string($this->adldap));
271
		}
272
		return $ds;
273
	}
274
275
	/**
276
	 * Get GUID from SID, as adLDAP only works on GUID not SID currently
277
	 *
278
	 * @param string $sid
279
	 * @return string|NULL
280
	 */
281
	/*protected function sid2guid($sid)
282
	{
283
		if (($sr = ldap_search($this->adldap->getLdapConnection(), $this->ads_context(), 'objectsid='.$sid, array('objectguid'))) &&
284
			($entries = ldap_get_entries($this->adldap->getLdapConnection(), $sr)))
285
		{
286
			return $this->adldap->utilities()->decodeGuid($entries[0]['objectguid'][0]);
287
		}
288
		return null;
289
	}*/
290
291
	/**
292
	 * Convert SID to account_id (RID = last part of SID)
293
	 *
294
	 * @param string $sid
295
	 * @return int
296
	 */
297
	public static function sid2account_id($sid)
298
	{
299
		$parts = explode('-', $sid);
300
301
		return (int)array_pop($parts);
302
	}
303
304
	/**
305
	 * Convert binary SID to account_id (RID = last part of SID)
306
	 *
307
	 * @param string $objectsid
308
	 * @return int
309
	 */
310
	public function objectsid2account_id($objectsid)
311
	{
312
		$sid = $this->adldap->utilities()->getTextSID(is_array($objectsid) ? $objectsid[0] : $objectsid);
0 ignored issues
show
The condition is_array($objectsid) is always false.
Loading history...
313
314
		return self::sid2account_id($sid);
315
	}
316
317
	/**
318
	 * Convert binary GUID to string
319
	 *
320
	 * @param string $objectguid
321
	 * @return int
322
	 */
323
	public function objectguid2str($objectguid)
324
	{
325
		return $this->adldap->utilities()->decodeGuid(is_array($objectguid) ? $objectguid[0] : $objectguid);
0 ignored issues
show
The condition is_array($objectguid) is always false.
Loading history...
326
	}
327
328
	/**
329
	 * Convert a string GUID to hex string used in filter
330
	 *
331
	 * @param string $strGUID
332
	 * @return int
333
	 */
334
	public function objectguid2hex($strGUID)
335
	{
336
		return $this->adldap->utilities()->strGuidToHex($strGUID);
337
	}
338
339
	/**
340
	 * Reads the data of one account
341
	 *
342
	 * @param int $account_id numeric account-id
343
	 * @return array|boolean array with account data (keys: account_id, account_lid, ...) or false if account not found
344
	 */
345
	public function read($account_id)
346
	{
347
		if (!(int)$account_id) return false;
348
349
		$ret = $account_id < 0 ? $this->_read_group($account_id) : $this->_read_user($account_id);
350
		if (self::$debug) error_log(__METHOD__."($account_id) returning ".array2string($ret));
351
		return $ret;
352
	}
353
354
	/**
355
	 * Saves / adds the data of one account
356
	 *
357
	 * If no account_id is set in data the account is added and the new id is set in $data.
358
	 *
359
	 * @param array $data array with account-data
360
	 * @return int|boolean the account_id or false on error
361
	 */
362
	function save(&$data)
363
	{
364
		$is_group = $data['account_id'] < 0 || $data['account_type'] === 'g';
365
		$data = Api\Translation::convert($data, Api\Translation::charset(), 'utf-8');
366
367
		if ($data['account_id'] && !($old = $this->read($data['account_id'])))
368
		{
369
			error_log(__METHOD__.'('.array2string($data).") account NOT found!");
370
			return false;
371
		}
372
		if ($old)
373
		{
374
			if (($old['account_type'] == 'g') != $is_group)
375
			{
376
				error_log(__METHOD__.'('.array2string($data).") changing account-type user <--> group forbidden!");
377
				return false;
378
			}
379
			$old = Api\Translation::convert($old, Api\Translation::charset(), 'utf-8');
380
		}
381
		$ret = $is_group ? $this->_save_group($data, $old) : $this->_save_user($data, $old);
382
383
		if (self::$debug) error_log(__METHOD__.'('.array2string($data).') returning '.array2string($ret));
384
		return $ret;
385
	}
386
387
	/**
388
	 * Delete one account, deletes also all acl-entries for that account
389
	 *
390
	 * @param int $account_id numeric account_id
391
	 * @return boolean true on success, false otherwise
392
	 */
393
	function delete($account_id)
394
	{
395
		if (!(int)$account_id || !($account_lid = $this->id2name($account_id)))
396
		{
397
			error_log(__METHOD__."($account_id) NOT found!");
398
			return false;
399
		}
400
401
		// for some reason deleting fails with "ldap_search(): supplied argument is not a valid ldap link resource"
402
		// forcing a reconnect fixes it ;-)
403
		$this->ldap_connection(true);
404
405
		if ($account_id < 0)
406
		{
407
			$ret = $this->adldap->group()->delete($account_lid);
408
		}
409
		else
410
		{
411
			$ret = $this->adldap->user()->delete($account_lid);
412
		}
413
		if (self::$debug) error_log(__METHOD__."($account_id) account_lid='$account_lid' returning ".array2string($ret));
414
		return $ret;
415
	}
416
417
	/**
418
	 * Convert ldap data of a group
419
	 *
420
	 * @param array $_data
421
	 * @return array
422
	 */
423
	protected function _ldap2group($_data)
424
	{
425
		$data = Api\Translation::convert($_data, 'utf-8');
426
427
		// no need to calculate sid, if already calculated
428
		$sid = is_string($data['objectsid']) ? $data['objectsid'] :
429
			$this->adldap->utilities()->getTextSID($data['objectsid'][0]);
430
		$account_id = -self::sid2account_id($sid);
431
432
		$group = array(
433
			'account_dn'        => $data['dn'],
434
			'account_id'        => $account_id,
435
			'account_sid'       => $sid,
436
			'account_guid'      => $this->adldap->utilities()->decodeGuid($data['objectguid'][0]),
437
			'account_lid'       => $data['samaccountname'][0],
438
			'account_type'      => 'g',
439
			'account_firstname' => $data['samaccountname'][0],
440
			'account_lastname'  => lang('Group'),
441
			'account_fullname'  => lang('Group').' '.$data['samaccountname'][0],
442
			'account_email'     => $data['mail'][0],
443
			'account_created'   => !isset($data['whencreated'][0]) ? null :
444
				self::_when2ts($data['whencreated'][0]),
445
			'account_modified'  => !isset($data['whenchanged'][0]) ? null :
446
				self::_when2ts($data['whenchanged'][0]),
447
			'account_description' => $data['description'][0],
448
			'mailAllowed'       => true,
449
		);
450
		//error_log(__METHOD__."(".array2string($data).") returning ".array2string($group));
451
		return $group;
452
	}
453
454
	/**
455
	 * Reads the data of one group
456
	 *
457
	 * @internal
458
	 * @todo take recursive group memberships into account
459
	 * @param int $account_id numeric account-id (< 0 as it's for a group)
460
	 * @return array|boolean array with account data (keys: account_id, account_lid, ...) or false if account not found
461
	 */
462
	protected function _read_group($account_id)
463
	{
464
		if (!($data = $this->filter(array('objectsid' => $this->get_sid($account_id)), 'g', self::$group_attributes)))
465
		{
466
			return false;	// group not found
467
		}
468
		$group = $this->_ldap2group(array_shift($data));
469
470
		// for memberships we have to query primaryGroupId and memberOf of users
471
		$group['members'] = $this->filter(array('memberOf' => $group['account_dn']), 'u');
472
		// primary group is not stored in memberOf attribute, need to add them too
473
		$group['members'] = $this->filter(array('primaryGroupId' => abs($account_id)), 'u', null, $group['members']);
474
475
		return $group;
476
	}
477
478
	/**
479
	 * Convert ldap data of a user
480
	 *
481
	 * @param array $_data
482
	 * @return array
483
	 */
484
	protected function _ldap2user(array $_data)
485
	{
486
		$data = Api\Translation::convert($_data, 'utf-8');
487
488
		// no need to calculate sid, if already calculated
489
		$sid = is_string($data['objectsid']) ? $data['objectsid'] :
490
			$this->adldap->utilities()->getTextSID($data['objectsid'][0]);
491
		$account_id = self::sid2account_id($sid);
492
493
		$user = array(
494
			'account_dn'        => $data['dn'],
495
			'account_id'        => $account_id,
496
			'account_sid'       => $sid,
497
			'account_guid'      => $this->adldap->utilities()->decodeGuid($data['objectguid'][0]),
498
			'account_lid'       => $data['samaccountname'][0],
499
			'account_type'      => 'u',
500
			'account_primary_group' => (string)-$data['primarygroupid'][0],
501
			'account_firstname' => $data['givenname'][0],
502
			'account_lastname'  => $data['sn'][0],
503
			'account_email'     => $data['mail'][0],
504
			'account_fullname'  => $data['displayname'][0],
505
			'account_phone'     => $data['telephonenumber'][0],
506
			'account_status'    => $data['useraccountcontrol'][0] & 2 ? false : 'A',
507
			'account_expires'   => !isset($data['accountexpires']) || !$data['accountexpires'][0] ||
508
				$data['accountexpires'][0] == self::EXPIRES_NEVER ? -1 :
509
				$this->adldap->utilities()->convertWindowsTimeToUnixTime($data['accountexpires'][0]),
510
			'account_lastpwd_change' => !isset($data['pwdlastset']) ? null : (!$data['pwdlastset'][0] ? 0 :
511
				$this->adldap->utilities()->convertWindowsTimeToUnixTime($data['pwdlastset'][0])),
512
			'account_created' => !isset($data['whencreated'][0]) ? null :
513
				self::_when2ts($data['whencreated'][0]),
514
			'account_modified' => !isset($data['whenchanged'][0]) ? null :
515
				self::_when2ts($data['whenchanged'][0]),
516
		);
517
		// expired accounts are NOT active
518
		if ($user['account_expires'] !== -1 && $user['account_expires'] < time())
519
		{
520
			$user['account_status'] = false;
521
		}
522
		$user['person_id'] = $user['account_guid'];	// id of contact
523
		//error_log(__METHOD__."(".array2string($data).") returning ".array2string($user));
524
		return $user;
525
	}
526
527
	/**
528
	 * Check if user is active
529
	 *
530
	 * @param array $data values for attributes 'useraccountcontrol' and 'accountexpires'
531
	 * @return boolean true if user is active, false otherwise
532
	 */
533
	public function user_active(array $data)
534
	{
535
		$user = $this->_ldap2user($data);
536
		$active = Api\Accounts::is_active($user);
537
		//error_log(__METHOD__."(cn={$data['cn'][0]}, useraccountcontrol={$data['useraccountcontrol'][0]}, accountexpires={$data['accountexpires'][0]}) user=".array2string($user)." returning ".array2string($active));
538
		return $active;
539
	}
540
541
	/**
542
	 * Reads the data of one user
543
	 *
544
	 * @internal
545
	 * @param int $account_id numeric account-id
546
	 * @return array|boolean array with account data (keys: account_id, account_lid, ...) or false if account not found
547
	 */
548
	protected function _read_user($account_id)
549
	{
550
		if (!($data = $this->filter(array('objectsid' => $this->get_sid($account_id)), 'u', self::$user_attributes)))
551
		{
552
			return false;	// user not found
553
		}
554
		$user = $this->_ldap2user(array_shift($data));
555
556
		// query memberships direct, as accounts class will query it anyway and we still have dn and primary group available
557
		$user['memberships'] = $this->filter(array('member' => $user['account_dn']), 'g');
558
		if (!isset($user['memberships'][$user['account_primary_group']]))
559
		{
560
			$user['memberships'][$user['account_primary_group']] = $this->id2name($user['account_primary_group']);
561
		}
562
		return $user;
563
	}
564
565
	const WHEN_FORMAT = 'YmdHis';
566
567
	/**
568
	 * Convert when(Created|Changed) attribute to unix timestamp
569
	 *
570
	 * @param string $_when eg. "20130520200000.0Z"
571
	 * @return int
572
	 */
573
	protected static function _when2ts($_when)
574
	{
575
		static $utc=null;
576
		if (!isset($utc)) $utc = new \DateTimeZone('UTC');
577
578
		list($when) = explode('.', $_when);	// remove .0Z not understood by createFromFormat
579
		$datetime = Api\DateTime::createFromFormat(self::WHEN_FORMAT, $when, $utc);
580
		if (Api\DateTime::$server_timezone) $datetime->setTimezone(Api\DateTime::$server_timezone);
581
582
		return $datetime->getTimestamp();
583
	}
584
585
	/**
586
	 * Saves a group
587
	 *
588
	 * @internal
589
	 * @param array $data array with account-data in utf-8
590
	 * @param array $old =null current data
591
	 * @return int|false account_id or false on error
592
	 */
593
	protected function _save_group(array &$data, array $old=null)
594
	{
595
		//error_log(__METHOD__.'('.array2string($data).', old='.array2string($old).')');
596
597
		if (!$old)	// new entry
598
		{
599
			static $new2adldap = array(
600
				'account_lid'       => 'group_name',
601
				'account_description' => 'description',
602
			);
603
			$attributes = array();
604
			foreach($new2adldap as $egw => $adldap)
605
			{
606
				$attributes[$adldap] = (string)$data[$egw];
607
			}
608
			$attributes['container'] = $this->_get_container();
609
610
			$ret = $this->adldap->group()->create($attributes);
611
			if ($ret !== true)
612
			{
613
				error_log(__METHOD__."(".array2string($data).") adldap->group()->create(".array2string($attributes).') returned '.array2string($ret));
614
				return false;
615
			}
616
			if (!($ret = $this->name2id($data['account_lid'])) || !($old = $this->read($ret)))
617
			{
618
				error_log(__METHOD__."(".array2string($data).") newly created group NOT found!");
619
				return false;
620
			}
621
		}
622
623
		// Samba4 does NOT allow to change samaccountname, but CN or DN of a group!
624
		// therefore we do NOT allow to change group-name for now (adLDAP also has no method for it)
625
		/* check if DN/account_lid changed (not yet supported by adLDAP)
626
		if ($old['account_lid'] !== $data['account_lid'])
627
		{
628
			if (!($ret = ldap_rename($ds=$this->ldap_connection(), $old['account_dn'],
629
				'CN='.$this->adldap->utilities()->ldapSlashes($data['account_lid']), null, true)))
630
			{
631
				error_log(__METHOD__."(".array2string($data).") rename to new CN failed!");
632
				return false;
633
			}
634
		}*/
635
		static $egw2adldap = array(
636
			//'account_lid'       => 'samaccountname',	// need to be changed too
637
			'account_email'       => 'mail',
638
			'account_description' => 'description',
639
		);
640
		$ldap = array();
641
		foreach($egw2adldap as $egw => $adldap)
642
		{
643
			if (isset($data[$egw]) && (string)$data[$egw] != (string)$old[$egw])
644
			{
645
				switch($egw)
646
				{
647
					case 'account_description':
648
						$ldap[$adldap] = !empty($data[$egw]) ? $data[$egw] : array();
649
						break;
650
651
					default:
652
						$ldap[$adldap] = $data[$egw];
653
						break;
654
				}
655
			}
656
		}
657
		// attributes not (yet) suppored by adldap
658
		if ($ldap && !($ret = @ldap_modify($ds=$this->ldap_connection(), $old['account_dn'], $ldap)))
659
		{
660
			error_log(__METHOD__."(".array2string($data).") ldap_modify($ds, '$old[account_dn]', ".array2string($ldap).') returned '.array2string($ret));
661
			return false;
662
		}
663
		return $old['account_id'];
664
	}
665
666
	/**
667
	 * Saves a user account
668
	 *
669
	 * @internal
670
	 * @param array $data array with account-data in utf-8
671
	 * @param array $old =null current data
672
	 * @return int|false account_id or false on error
673
	 */
674
	protected function _save_user(array &$data, array $old=null)
675
	{
676
		//error_log(__METHOD__.'('.array2string($data).', old='.array2string($old).')');
677
		if (!isset($data['account_fullname']) && !empty($data['account_firstname']) && !empty($data['account_lastname']))
678
		{
679
			$data['account_fullname'] = $data['account_firstname'].' '.$data['account_lastname'];
680
		}
681
682
		if (($new_entry = !$old))	// new entry
683
		{
684
			static $new2adldap = array(
685
				'account_lid'       => 'username',
686
				'account_firstname' => 'firstname',
687
				'account_lastname'  => 'surname',
688
				'account_email'     => 'email',
689
				'account_fullname'  => 'display_name',
690
				'account_passwd'    => 'password',
691
				'account_status'    => 'enabled',
692
			);
693
			$attributes = array();
694
			foreach($new2adldap as $egw => $adldap)
695
			{
696
				if ($egw == 'account_passwd' && (empty($data[$egw]) ||
697
					!$this->adldap->getUseSSL() && !$this->adldap->getUseTLS()))
698
				{
699
					continue;	// do not try to set password, if no SSL or TLS, whole user creation will fail
700
				}
701
				if (isset($data[$egw])) $attributes[$adldap] = $data[$egw];
702
			}
703
			$attributes['enabled'] = !isset($data['account_status']) || $data['account_status'] === 'A';
704
			$attributes['container'] = $this->_get_container();
705
706
			$ret = $this->adldap->user()->create($attributes);
707
			if ($ret !== true)
708
			{
709
				error_log(__METHOD__."(".array2string($data).") adldap->user()->create(".array2string($attributes).') returned '.array2string($ret));
710
				return false;
711
			}
712
			if (!($ret = $this->name2id($data['account_lid'])) || !($old = $this->read($ret)))
713
			{
714
				error_log(__METHOD__."(".array2string($data).") newly created user NOT found!");
715
				return false;
716
			}
717
			$data['account_id'] = $old['account_id'];
718
		}
719
		// check if DN/account_lid changed (not yet supported by adLDAP)
720
		/* disabled as AD does NOT allow to change user-name (account_lid), which is used for DN
721
		if (isset($data['account_lid']) && $old['account_lid'] !== $data['account_lid'] ||
722
			(stripos($old['account_dn'], 'CN='.$data['account_lid'].',') !== 0))
723
		{
724
			if (!($ret = ldap_rename($ds=$this->ldap_connection(), $old['account_dn'],
725
				'CN='.$this->adldap->utilities()->ldapSlashes($data['account_lid']), null, true)))
726
			{
727
				error_log(__METHOD__."(".array2string($data).") rename to new CN failed!");
728
				return false;
729
			}
730
		}*/
731
		static $egw2adldap = array(
732
			'account_lid'       => 'samaccountname',
733
			'account_firstname' => 'firstname',
734
			'account_lastname'  => 'surname',
735
			'account_email'     => 'email',
736
			'account_fullname'  => 'display_name',	// handeled currently in rename above, as not supported by adLDAP
737
			'account_passwd'    => 'password',
738
			'account_status'    => 'enabled',
739
			'account_primary_group' => 'primarygroupid',
740
			'account_expires'   => 'expires',
741
			//'mustchangepassword'=> 'change_password',	// can only set it, but not reset it, therefore we set pwdlastset direct
742
			'account_lastpwd_change' => 'pwdlastset',
743
			//'account_phone'   => 'telephone',	not updated by accounts, only read so far
744
		);
745
		$attributes = $ldap = array();
746
		// for a new entry set certain values (eg. profilePath) to in setup configured value
747
		if ($new_entry)
748
		{
749
			foreach($this->frontend->config as $name => $value)
750
			{
751
				if (substr($name, 0, 8) == 'ads_new_')
752
				{
753
					$ldap[substr($name, 8)] = str_replace('%u', $data['account_lid'], $value);
754
				}
755
			}
756
		}
757
		foreach($egw2adldap as $egw => $adldap)
758
		{
759
			if (isset($data[$egw]) && (string)$data[$egw] != (string)$old[$egw])
760
			{
761
				switch($egw)
762
				{
763
					case 'account_passwd':
764
						if (!empty($data[$egw]) && ($this->adldap->getUseSSL() || $this->adldap->getUseTLS()))
765
						{
766
							$attributes[$adldap] = $data[$egw];	// only try to set password, if no SSL or TLS
767
						}
768
						break;
769
					case 'account_primary_group':
770
						// setting a primary group seems to fail, if user is no member of that group
771
						if (isset($old['memberships'][$data[$egw]]) ||
772
							($group=$this->id2name($data[$egw])) && $this->adldap->group()->addUser($group, $data['account_id']))
773
						{
774
							$old['memberships'][$data[$egw]] = $group;
775
							$ldap[$adldap] = abs($data[$egw]);
776
						}
777
						break;
778
					case 'account_lid':
779
						$ldap[$adldap] = $data[$egw];
780
						$ldap['userPrincipalName'] = $data[$egw].'@'.$this->frontend->config['ads_domain'];
781
						break;
782
					case 'account_expires':
783
						$attributes[$adldap] = $data[$egw] == -1 ? self::EXPIRES_NEVER :
784
							self::convertUnixTimeToWindowsTime($data[$egw]);
785
						break;
786
					case 'account_status':
787
						if ($new_entry && empty($data['account_passwd'])) continue;	// cant active new account without passwd!
788
						$attributes[$adldap] = $data[$egw] == 'A';
789
						break;
790
					case 'account_lastpwd_change':
791
						// Samba4 does not understand -1 for current time, but Win2008r2 only allows to set -1 (beside 0)
792
						// call Api\Auth\Ads::setLastPwdChange with true to get correct modification for both
793
						$ldap = array_merge($ldap, Api\Auth\Ads::setLastPwdChange($data['account_lid'], null, $data[$egw], true));
794
						break;
795
					default:
796
						$attributes[$adldap] = $data[$egw];
797
						break;
798
				}
799
			}
800
		}
801
		// check if we need to update something
802
		if ($attributes && !($ret = $this->adldap->user()->modify($data['account_lid'], $attributes)))
803
		{
804
			error_log(__METHOD__."(".array2string($data).") adldap->user()->modify('$data[account_lid]', ".array2string($attributes).') returned '.array2string($ret).' '.function_backtrace());
805
			return false;
806
		}
807
		//elseif ($attributes) error_log(__METHOD__."(".array2string($data).") adldap->user()->modify('$data[account_lid]', ".array2string($attributes).') returned '.array2string($ret).' '.function_backtrace());
808
		// attributes not (yet) suppored by adldap
809
		if ($ldap && !($ret = @ldap_modify($ds=$this->ldap_connection(), $old['account_dn'], $ldap)))
810
		{
811
			error_log(__METHOD__."(".array2string($data).") ldap_modify($ds, '$old[account_dn]', ".array2string($ldap).') returned '.array2string($ret).' ('.ldap_error($ds).') '.function_backtrace());
812
			return false;
813
		}
814
		//elseif ($ldap) error_log(__METHOD__."(".array2string($data).") ldap_modify($ds, '$old[account_dn]', ".array2string($ldap).') returned '.array2string($ret).' '.function_backtrace());
815
816
		//error_log(__METHOD__."(".array2string($data).") returning ".array2string($old['account_id']));
817
		return $old['account_id'];
818
	}
819
820
	/**
821
	* Add seconds between 1601-01-01 and 1970-01-01 and multiply by 10000000
822
	*
823
	* @param long $unixTime
0 ignored issues
show
The type EGroupware\Api\Accounts\long was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
824
	* @return long windowsTime
825
	*/
826
	public static function convertUnixTimeToWindowsTime($unixTime)
827
	{
828
		return ($unixTime + 11644477200) * 10000000;
829
	}
830
831
	/**
832
	 * Searches / lists accounts: users and/or groups
833
	 *
834
	 * @todo sort and limit query on AD, PHP5.4 and AD support it
835
	 *
836
	 * @param array with the following keys:
837
	 * @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...
838
	 *	or integer group-id for a list of members of that group
839
	 * @param $param['start'] int first account to return (returns offset or max_matches entries) or all if not set
840
	 * @param $param['order'] string column to sort after, default account_lid if unset
841
	 * @param $param['sort'] string 'ASC' or 'DESC', default 'ASC' if not set
842
	 * @param $param['query'] string to search for, no search if unset or empty
843
	 * @param $param['query_type'] string:
844
	 *	'all'   - query all fields for containing $param[query]
845
	 *	'start' - query all fields starting with $param[query]
846
	 *	'exact' - query all fields for exact $param[query]
847
	 *	'lid','firstname','lastname','email' - query only the given field for containing $param[query]
848
	 * @param $param['offset'] int - number of matches to return if start given, default use the value in the prefs
849
	 * @param $param['objectclass'] boolean return objectclass(es) under key 'objectclass' in each account
850
	 * @return array with account_id => data pairs, data is an array with account_id, account_lid, account_firstname,
851
	 *	account_lastname, person_id (id of the linked addressbook entry), account_status, account_expires, account_primary_group
852
	 */
853
	function search($param)
854
	{
855
		//error_log(__METHOD__.'('.array2string($param).')');
856
		$account_search = &$this->cache['account_search'];
0 ignored issues
show
Bug Best Practice introduced by
The property cache does not exist on EGroupware\Api\Accounts\Ads. Did you maybe forget to declare it?
Loading history...
857
858
		// check if the query is cached
859
		$serial = serialize($param);
860
		if (isset($account_search[$serial]))
861
		{
862
			$this->total = $account_search[$serial]['total'];
863
			return $account_search[$serial]['data'];
864
		}
865
		// if it's a limited query, check if the unlimited query is cached
866
		$start = $param['start'];
867
		if (!($maxmatchs = $GLOBALS['egw_info']['user']['preferences']['common']['maxmatchs'])) $maxmatchs = 15;
868
		if (!($offset = $param['offset'])) $offset = $maxmatchs;
869
		unset($param['start']);
870
		unset($param['offset']);
871
		$unl_serial = serialize($param);
872
		if (isset($account_search[$unl_serial]))
873
		{
874
			$this->total = $account_search[$unl_serial]['total'];
875
			$sortedAccounts = $account_search[$unl_serial]['data'];
876
		}
877
		else	// we need to run the unlimited query
878
		{
879
			$query = Api\Ldap::quote(strtolower($param['query']));
880
881
			$accounts = array();
882
			if($param['type'] !== 'groups')
883
			{
884
				if (!empty($query) && $query != '*')
885
				{
886
					switch($param['query_type'])
887
					{
888
						case 'all':
889
						default:
890
							$query = '*'.$query;
891
							// fall-through
892
						case 'start':
893
							$query .= '*';
894
							// fall-through
895
						case 'exact':
896
							$filter = "(|(samaccountname=$query)(sn=$query)(cn=$query)(givenname=$query)(mail=$query))";
897
							break;
898
						case 'firstname':
899
						case 'lastname':
900
						case 'lid':
901
						case 'email':
902
							static $to_ldap = array(
903
								'firstname' => 'givenname',
904
								'lastname'  => 'sn',
905
								'lid'       => 'uid',
906
								'email'     => 'mail',
907
							);
908
							$filter = '('.$to_ldap[$param['query_type']].'=*'.$query.'*)';
909
							break;
910
					}
911
				}
912
				if (is_numeric($param['type']))
913
				{
914
					$membership_filter = '(|(memberOf='.$this->id2name((int)$param['type'], 'account_dn').')(PrimaryGroupId='.abs($param['type']).'))';
915
					$filter = $filter ? "(&$membership_filter$filter)" : $membership_filter;
916
				}
917
				foreach($this->filter($filter, 'u', self::$user_attributes) as $account_id => $data)
918
				{
919
					$account = $this->_ldap2user($data);
920
					if ($param['active'] && !$this->frontend->is_active($account))
921
					{
922
						continue;
923
					}
924
					$account['account_fullname'] = Api\Accounts::format_username($account['account_lid'],$account['account_firstname'],$account['account_lastname'],$account['account_id']);
925
					$accounts[$account_id] = $account;
926
				}
927
			}
928
			if ($param['type'] === 'groups' || $param['type'] === 'both')
929
			{
930
				$query = Api\Ldap::quote(strtolower($param['query']));
931
932
				$filter = null;
933
				if(!empty($query) && $query != '*')
934
				{
935
					switch($param['query_type'])
936
					{
937
						case 'all':
938
						default:
939
							$query = '*'.$query;
940
							// fall-through
941
						case 'start':
942
							$query .= '*';
943
							// fall-through
944
						case 'exact':
945
							break;
946
					}
947
					$filter = "(|(cn=$query)(description=$query))";
948
				}
949
				foreach($this->filter($filter, 'g', self::$group_attributes) as $account_id => $data)
950
				{
951
					$accounts[$account_id] = $this->_ldap2group($data);
952
				}
953
			}
954
			// sort the array
955
			$this->_callback_sort = strtoupper($param['sort']);
956
			$this->_callback_order = empty($param['order']) ? array('account_lid') : explode(',',$param['order']);
957
			foreach($this->_callback_order as &$col)
958
			{
959
				if (substr($col, 0, 8) !== 'account_') $col = 'account_'.$col;
960
			}
961
			$sortedAccounts = $accounts;
962
			uasort($sortedAccounts,array($this,'_sort_callback'));
963
			$account_search[$unl_serial]['data'] = $sortedAccounts;
964
965
			$account_search[$unl_serial]['total'] = $this->total = count($accounts);
966
		}
967
		// return only the wanted accounts
968
		reset($sortedAccounts);
969
		if(is_numeric($start) && is_numeric($offset))
970
		{
971
			$account_search[$serial]['data'] = array_slice($sortedAccounts, $start, $offset);
972
			$account_search[$serial]['total'] = $this->total;
973
			//error_log(__METHOD__.'('.array2string($param).") returning $offset/$this->total entries from $start ".array2string($account_search[$serial]['data']));
974
			return $account_search[$serial]['data'];
975
		}
976
		//error_log(__METHOD__.'('.array2string($param).') returning all '.array2string($sortedAccounts));
977
		return $sortedAccounts;
978
	}
979
980
	/**
981
	 * DESC or ASC
982
	 *
983
	 * @var string
984
	 */
985
	private $_callback_sort = 'ASC';
986
	/**
987
	 * column_names to sort by
988
	 *
989
	 * @var array
990
	 */
991
	private $_callback_order = array('account_lid');
992
993
	/**
994
	 * Sort callback for uasort
995
	 *
996
	 * @param array $a
997
	 * @param array $b
998
	 * @return int
999
	 */
1000
	protected function _sort_callback($a,$b)
1001
	{
1002
		foreach($this->_callback_order as $col )
1003
		{
1004
			if($this->_callback_sort != 'DESC')
1005
			{
1006
				$cmp = strcasecmp( $a[$col], $b[$col] );
1007
			}
1008
			else
1009
			{
1010
				$cmp = strcasecmp( $b[$col], $a[$col] );
1011
			}
1012
			if ( $cmp != 0 )
1013
			{
1014
				return $cmp;
1015
			}
1016
		}
1017
		return 0;
1018
	}
1019
1020
	/**
1021
	 * Query ADS by (optional) filter and (optional) account-type filter
1022
	 *
1023
	 * All reading ADS queries are done throught this methods.
1024
	 *
1025
	 * @param string|array $attr_filter array with attribute => value pairs or filter string or empty
1026
	 * @param string $account_type u = user, g = group, default null = try both
1027
	 * @param array $attrs =null default return account_lid, else return raw values from ldap-query
1028
	 * @param array $accounts =array() array to add filtered accounts too, default empty array
1029
	 * @return array account_id => account_lid or values for $attrs pairs
1030
	 */
1031
	protected function filter($attr_filter, $account_type=null, array $attrs=null, array $accounts=array())
1032
	{
1033
		switch($account_type)
1034
		{
1035
			case 'u':
1036
				$type_filter = '(samaccounttype='.adLDAP::ADLDAP_NORMAL_ACCOUNT.')';
1037
				break;
1038
			case 'g':
1039
				$type_filter = '(samaccounttype='.adLDAP::ADLDAP_SECURITY_GLOBAL_GROUP.')';
1040
				break;
1041
			default:
1042
				$type_filter = '(|(samaccounttype='.adLDAP::ADLDAP_NORMAL_ACCOUNT.')(samaccounttype='.adLDAP::ADLDAP_SECURITY_GLOBAL_GROUP.'))';
1043
				break;
1044
		}
1045
		if (!$attr_filter)
1046
		{
1047
			$filter = $type_filter;
1048
		}
1049
		else
1050
		{
1051
			$filter = '(&';
1052
			if (is_string($attr_filter))
1053
			{
1054
				$filter .= $attr_filter;
1055
			}
1056
			else
1057
			{
1058
				foreach($attr_filter as $attr => $value)
1059
				{
1060
					$filter .= '('.$attr.'='.$this->adldap->utilities()->ldapSlashes($value).')';
1061
				}
1062
			}
1063
			$filter .= $type_filter.')';
1064
		}
1065
		$sri = ldap_search($ds=$this->ldap_connection(), $context=$this->ads_context(), $filter,
1066
			$attrs ? $attrs : self::$default_attributes);
1067
		if (!$sri)
0 ignored issues
show
$sri is of type resource, thus it always evaluated to false.
Loading history...
1068
		{
1069
			if (self::$debug) error_log(__METHOD__.'('.array2string($attr_filter).", '$account_type') ldap_search($ds, '$context', '$filter') returned ".array2string($sri).' trying to reconnect ...');
1070
			$sri = ldap_search($ds=$this->ldap_connection(true), $context=$this->ads_context(), $filter,
1071
				$attrs ? $attrs : self::$default_attributes);
1072
		}
1073
1074
		if ($sri && ($allValues = ldap_get_entries($ds, $sri)))
0 ignored issues
show
$sri is of type resource, thus it always evaluated to false.
Loading history...
1075
		{
1076
			foreach($allValues as $key => $data)
1077
			{
1078
				if ($key === 'count') continue;
1079
1080
				if ($account_type && !($account_type == 'u' && $data['samaccounttype'][0] == adLDAP::ADLDAP_NORMAL_ACCOUNT ||
1081
					$account_type == 'g' && $data['samaccounttype'][0] == adLDAP::ADLDAP_SECURITY_GLOBAL_GROUP))
1082
				{
1083
					continue;
1084
				}
1085
				$sid = $data['objectsid'] = $this->adldap->utilities()->getTextSID($data['objectsid'][0]);
1086
				$rid = self::sid2account_id($sid);
1087
1088
				if ($data['samaccounttype'][0] == adLDAP::ADLDAP_NORMAL_ACCOUNT && $rid < self::MIN_ACCOUNT_ID)
1089
				{
1090
					continue;	// ignore system accounts incl. "Administrator"
1091
				}
1092
				$accounts[($data['samaccounttype'][0] == adLDAP::ADLDAP_SECURITY_GLOBAL_GROUP ? '-' : '').$rid] =
1093
					$attrs ? $data : Api\Translation::convert($data['samaccountname'][0], 'utf-8');
1094
			}
1095
		}
1096
		else if (self::$debug) error_log(__METHOD__.'('.array2string($attr_filter).", '$account_type') ldap_search($ds, '$context', '$filter')=$sri allValues=".array2string($allValues));
1097
1098
		//error_log(__METHOD__.'('.array2string($attr_filter).", '$account_type') ldap_search($ds, '$context', '$filter') returning ".array2string($accounts).' '.function_backtrace());
1099
		return $accounts;
1100
	}
1101
1102
	/**
1103
	 * convert an alphanumeric account-value (account_lid, account_email) to the account_id
1104
	 *
1105
	 * Please note:
1106
	 * - if a group and an user have the same account_lid the group will be returned (LDAP only)
1107
	 * - if multiple user have the same email address, the returned user is undefined
1108
	 *
1109
	 * @param string $name value to convert
1110
	 * @param string $which ='account_lid' type of $name: account_lid (default), account_email, person_id, account_fullname
1111
	 * @param string $account_type u = user, g = group, default null = try both
1112
	 * @return int|false numeric account_id or false on error ($name not found)
1113
	 */
1114
	public function name2id($name, $which='account_lid', $account_type=null)
1115
	{
1116
		static $to_ldap = array(
1117
			'account_lid'   => 'samaccountname',
1118
			'account_email' => 'mail',
1119
			'account_fullname' => 'cn',
1120
			'account_sid'   => 'objectsid',
1121
			'account_guid'  => 'objectguid',
1122
		);
1123
		$ret = false;
1124
		if (isset($to_ldap[$which]))
1125
		{
1126
			foreach($this->filter(array($to_ldap[$which] => $name), $account_type) as $account_id => $account_lid)
1127
			{
1128
				unset($account_lid);
1129
				$ret = $account_id;
1130
				break;
1131
			}
1132
		}
1133
		if (self::$debug) error_log(__METHOD__."('$name', '$which', '$account_type') returning ".array2string($ret));
1134
		return $ret;
1135
	}
1136
1137
	/**
1138
	 * Convert an numeric account_id to any other value of that account (account_lid, account_email, ...)
1139
	 *
1140
	 * Calls frontend which uses (cached) read method to fetch all data by account_id.
1141
	 *
1142
	 * @param int $account_id numerica account_id
1143
	 * @param string $which ='account_lid' type to convert to: account_lid (default), account_email, ...
1144
	 * @return string/false converted value or false on error ($account_id not found)
0 ignored issues
show
Documentation Bug introduced by
The doc comment string/false at position 0 could not be parsed: Unknown type name 'string/false' at position 0 in string/false.
Loading history...
1145
	 */
1146
	public function id2name($account_id, $which='account_lid')
1147
	{
1148
		return $this->frontend->id2name($account_id,$which);
1149
	}
1150
1151
	/**
1152
	 * Update the last login timestamps and the IP
1153
	 *
1154
	 * @param int $_account_id
1155
	 * @param string $ip
1156
	 * @return int lastlogin time
1157
	 */
1158
	function update_lastlogin($_account_id, $ip)
1159
	{
1160
		unset($_account_id, $ip);	// not used, but required by function signature
1161
1162
		return false;	// not longer supported
1163
	}
1164
1165
	/**
1166
	 * Query memberships of a given account
1167
	 *
1168
	 * Calls frontend which uses (cached) read method to fetch all data by account_id.
1169
	 *
1170
	 * @param int $account_id
1171
	 * @return array|boolean array with account_id => account_lid pairs or false if account not found
1172
	 */
1173
	function memberships($account_id)
1174
	{
1175
		if (!($data = $this->frontend->read($account_id)) || $data['account_id'] <= 0) return false;
1176
1177
		return $data['memberships'];
1178
	}
1179
1180
	/**
1181
	 * Query the members of a group
1182
	 *
1183
	 * Calls frontend which uses (cached) read method to fetch all data by account_id.
1184
	 *
1185
	 * @param int $gid
1186
	 * @return array with uidnumber => uid pairs
1187
	 */
1188
	function members($gid)
1189
	{
1190
		if (!($data = $this->frontend->read($gid)) || $data['account_id'] >= 0) return false;
1191
1192
		return $data['members'];
1193
	}
1194
1195
	/**
1196
	 * Sets the memberships of the given account
1197
	 *
1198
	 * @param array $groups array with gidnumbers
1199
	 * @param int $account_id uidnumber
1200
	 * @return int number of added or removed memberships
1201
	 */
1202
	function set_memberships($groups,$account_id)
1203
	{
1204
		if (!($account = $this->id2name($account_id))) return;
1205
		$current = array_keys($this->memberships($account_id));
1206
1207
		$changed = 0;
1208
		foreach(array(
1209
			'add' => array_diff($groups, $current),		// add account to all groups he is currently not in
1210
			'remove' => array_diff($current, $groups),	// remove account from all groups he is only currently in
1211
		) as $op => $memberships)
1212
		{
1213
			$func = $op.($account_id > 0 ? 'User' : 'Group');
1214
			foreach($memberships as $gid)
1215
			{
1216
				$ok = $this->adldap->group()->$func($group=$this->id2name($gid), $account);
1217
				//error_log(__METHOD__.'('.array2string($groups).", $account_id) $func('$group', '$account') returned ".array2string($ok));
1218
				$changed += (int)$ok;
1219
			}
1220
		}
1221
		if (self::$debug) error_log(__METHOD__.'('.array2string($groups).", $account_id) current=".array2string($current)." returning $changed");
1222
		return $changed;
1223
	}
1224
1225
	/**
1226
	 * Set the members of a group
1227
	 *
1228
	 * @param array $users array with uidnumber or uid's
1229
	 * @param int $gid gidnumber of group to set
1230
	 * @return int number of added or removed members
1231
	 */
1232
	function set_members($users, $gid)
1233
	{
1234
		if (!($group = $this->id2name($gid))) return;
1235
		$current = array_keys($this->members($gid));
1236
1237
		$changed = 0;
1238
		foreach(array(
1239
			'add' => array_diff($users, $current),	// add members currently not in
1240
			'remove' => array_diff($current, $users),	// remove members only currently in
1241
		) as $op => $members)
1242
		{
1243
			foreach($members as $account_id)
1244
			{
1245
				$func = $op.($account_id > 0 ? 'User' : 'Group');
1246
				$ok = $this->adldap->group()->$func($group, $account=$this->id2name($account_id));
1247
				//error_log(__METHOD__.'('.array2string($users).", $account_id) $func('$group', '$account') returned ".array2string($ok));
1248
				$changed += (int)$ok;
1249
			}
1250
		}
1251
		if (self::$debug) error_log(__METHOD__.'('.array2string($users).", $gid) current=".array2string($current)." returning $changed");
1252
		return $changed;
1253
	}
1254
}
1255
1256
/**
1257
 * Fixes and enhancements for adLDAP required by EGroupware
1258
 *
1259
 * - allow to use utf-8 charset internally, not just an 8-bit iso-charset
1260
 * - support for Windows2008r2 (maybe earlier too) and Samba4 "CN=Users" DN as container to create users or groups
1261
 */
1262
class adLDAP extends \adLDAP
1263
{
1264
	/**
1265
	 * Charset used for internal encoding
1266
	 *
1267
	 * @var string
1268
	 */
1269
	public $charset = 'iso-8859-1';
1270
1271
	function __construct(array $options=array())
1272
	{
1273
		if (isset($options['charset']))
1274
		{
1275
			$this->charset = strtolower($options['charset']);
1276
		}
1277
		parent::__construct($options);
1278
	}
1279
1280
	/**
1281
    * Convert 8bit characters e.g. accented characters to UTF8 encoded characters
1282
    *
1283
    * Extended to use mbstring to convert from arbitrary charset to utf-8
1284
	*/
1285
	public function encode8Bit(&$item, $key)
1286
	{
1287
		if ($this->charset != 'utf-8' && $key != 'password')
1288
		{
1289
			if (function_exists('mb_convert_encoding'))
1290
			{
1291
				$item = mb_convert_encoding($item, 'utf-8', $this->charset);
1292
			}
1293
			else
1294
			{
1295
				parent::encode8Bit($item, $key);
1296
			}
1297
		}
1298
	}
1299
1300
	/**
1301
	 * Get the userclass interface
1302
	 *
1303
	 * @return adLDAPUsers
1304
	 */
1305
	public function user() {
1306
		if (!$this->userClass) {
1307
			$this->userClass = new adLDAPUsers($this);
1308
		}
1309
		return $this->userClass;
1310
	}
1311
1312
    /**
1313
    * Get the group class interface
1314
    *
1315
    * @return adLDAPGroups
1316
    */
1317
    public function group() {
1318
        if (!$this->groupClass) {
1319
            $this->groupClass = new adLDAPGroups($this);
1320
        }
1321
        return $this->groupClass;
1322
    }
1323
1324
    /**
1325
    * Get the utils class interface
1326
    *
1327
    * @return adLDAPUtils
1328
    */
1329
    public function utilities() {
1330
        if (!$this->utilClass) {
1331
            $this->utilClass = new adLDAPUtils($this);
1332
        }
1333
        return $this->utilClass;
1334
    }
1335
}
1336
1337
/**
1338
 * Fixes an enhancements for adLDAPUser required by EGroupware
1339
 */
1340
class adLDAPUsers extends \adLDAPUsers
1341
{
1342
	/**
1343
	 * Create a user
1344
	 *
1345
	 * Extended to allow to specify $attribute["container"] as string, because array hardcodes "OU=", while Samba4 and win2008r2 uses "CN=Users"
1346
	 *
1347
	 * Extended to ensure following creating order required by at least win2008r2:
1348
	 * - new user without password and deactivated
1349
	 * - add password, see new method setPassword
1350
	 * - activate user
1351
	 *
1352
	 * @param array $attributes The attributes to set to the user account
1353
	 * @return bool
1354
	 */
1355
	public function create($attributes)
1356
	{
1357
		// Check for compulsory fields
1358
		if (!array_key_exists("username", $attributes)){ return "Missing compulsory field [username]"; }
1359
		if (!array_key_exists("firstname", $attributes)){ return "Missing compulsory field [firstname]"; }
1360
		if (!array_key_exists("surname", $attributes)){ return "Missing compulsory field [surname]"; }
1361
		if (!array_key_exists("email", $attributes)){ return "Missing compulsory field [email]"; }
1362
		if (!array_key_exists("container", $attributes)){ return "Missing compulsory field [container]"; }
1363
		if (empty($attributes["container"])){ return "Container attribute must be an array or string."; }
1364
1365
		if (array_key_exists("password",$attributes) && (!$this->adldap->getUseSSL() && !$this->adldap->getUseTLS())){
1366
			throw new adLDAPException('SSL must be configured on your webserver and enabled in the class to set passwords.');
1367
		}
1368
1369
		if (!array_key_exists("display_name", $attributes)) {
1370
			$attributes["display_name"] = $attributes["firstname"] . " " . $attributes["surname"];
1371
		}
1372
1373
		// Translate the schema
1374
		$add = $this->adldap->adldap_schema($attributes);
1375
1376
		// Additional stuff only used for adding accounts
1377
		$add["cn"][0] = $attributes["username"];
1378
		$add["samaccountname"][0] = $attributes["username"];
1379
		$add["userPrincipalName"][0] = $attributes["username"].$this->adldap->getAccountSuffix();
1380
		$add["objectclass"][0] = "top";
1381
		$add["objectclass"][1] = "person";
1382
		$add["objectclass"][2] = "organizationalPerson";
1383
		$add["objectclass"][3] = "user"; //person?
1384
		//$add["name"][0]=$attributes["firstname"]." ".$attributes["surname"];
1385
1386
		// Set the account control attribute
1387
		$control_options = array("NORMAL_ACCOUNT", "ACCOUNTDISABLE");
1388
		$add["userAccountControl"][0] = $this->accountControl($control_options);
1389
1390
		// Determine the container
1391
		if (is_array($attributes['container'])) {
1392
			$attributes["container"] = array_reverse($attributes["container"]);
1393
			$attributes["container"] = "OU=" . implode(",OU=",$attributes["container"]);
1394
		}
1395
		// we can NOT set password with ldap_add or ldap_modify, it needs ldap_mod_replace, at least under Win2008r2
1396
		unset($add['unicodePwd']);
1397
1398
		// Add the entry
1399
		$result = ldap_add($ds=$this->adldap->getLdapConnection(), $dn="CN=" . $add["cn"][0] . "," . $attributes["container"] . "," . $this->adldap->getBaseDn(), $add);
1400
		if ($result != true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison !== instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1401
			error_log(__METHOD__."(".array2string($attributes).") ldap_add($ds, '$dn', ".array2string($add).") returned ".array2string($result)." ldap_error()=".ldap_error($ds));
1402
			return false;
1403
		}
1404
1405
		// now password can be added to still disabled account
1406
		if (array_key_exists("password",$attributes))
1407
		{
1408
			if (!$this->setPassword($dn, $attributes['password'])) return false;
1409
1410
			// now account can be enabled
1411
			if ($attributes["enabled"])
1412
			{
1413
				$control_options = array("NORMAL_ACCOUNT");
1414
				$mod = array("userAccountControl" => $this->accountControl($control_options));
1415
				$result = ldap_modify($ds, $dn, $mod);
1416
				if (!$result) error_log(__METHOD__."(".array2string($attributes).") ldap_modify($ds, '$dn', ".array2string($mod).") returned ".array2string($result)." ldap_error()=".ldap_error($ds));
1417
			}
1418
		}
1419
1420
		return true;
1421
	}
1422
1423
    /**
1424
    * Encode a password for transmission over LDAP
1425
    *
1426
    * Extended to use mbstring to convert from arbitrary charset to UTF-16LE
1427
    *
1428
    * @param string $password The password to encode
1429
    * @return string
1430
    */
1431
    public function encodePassword($password)
1432
    {
1433
        $password="\"".$password."\"";
1434
        if (function_exists('mb_convert_encoding') && !empty($this->adldap->charset))
1435
        {
1436
            return mb_convert_encoding($password, 'UTF-16LE', $this->adldap->charset);
1437
        }
1438
        $encoded="";
1439
        for ($i=0; $i <strlen($password); $i++){ $encoded.="{$password{$i}}\000"; }
1440
        return $encoded;
1441
    }
1442
1443
    /**
1444
     * Set a password
1445
     *
1446
     * Requires "Reset password" priviledges from bind user!
1447
     *
1448
	 * We can NOT set password with ldap_add or ldap_modify, it needs ldap_mod_replace, at least under Win2008r2!
1449
	 *
1450
     * @param string $dn
1451
     * @param string $password
1452
     * @return boolean
1453
     */
1454
    public function setPassword($dn, $password)
1455
    {
1456
    	$result = ldap_mod_replace($ds=$this->adldap->getLdapConnection(), $dn, array(
1457
    		'unicodePwd' => $this->encodePassword($password),
1458
    	));
1459
    	if (!$result) error_log(__METHOD__."('$dn', '$password') ldap_mod_replace($ds, '$dn', \$password) returned FALSE: ".ldap_error($ds));
1460
    	return $result;
1461
    }
1462
1463
	/**
1464
	 * Check if we can to a real password change, not just a password reset
1465
	 *
1466
	 * Requires PHP 5.4 >= 5.4.26, PHP 5.5 >= 5.5.10 or PHP 5.6 >= 5.6.0
1467
	 *
1468
	 * @return boolean
1469
	 */
1470
	public static function changePasswordSupported()
1471
	{
1472
		return function_exists('ldap_modify_batch');
1473
	}
1474
1475
    /**
1476
    * Set the password of a user - This must be performed over SSL
1477
    *
1478
    * @param string $username The username to modify
1479
    * @param string $password The new password
1480
    * @param bool $isGUID Is the username passed a GUID or a samAccountName
1481
	* @param string $old_password old password for password change, if supported
1482
    * @return bool
1483
    */
1484
    public function password($username, $password, $isGUID = false, $old_password=null)
1485
    {
1486
        if ($username === NULL) { return false; }
0 ignored issues
show
The condition $username === NULL is always false.
Loading history...
1487
        if ($password === NULL) { return false; }
0 ignored issues
show
The condition $password === NULL is always false.
Loading history...
1488
        if (!$this->adldap->getLdapBind()) { return false; }
1489
        if (!$this->adldap->getUseSSL() && !$this->adldap->getUseTLS()) {
1490
            throw new adLDAPException('SSL must be configured on your webserver and enabled in the class to set passwords.');
1491
        }
1492
1493
        $userDn = $this->dn($username, $isGUID);
1494
        if ($userDn === false) {
0 ignored issues
show
The condition $userDn === false is always false.
Loading history...
1495
            return false;
1496
        }
1497
1498
        $add=array();
1499
1500
		if (empty($old_password) || !function_exists('ldap_modify_batch')) {
1501
			$add["unicodePwd"][0] = $this->encodePassword($password);
1502
1503
			$result = @ldap_mod_replace($this->adldap->getLdapConnection(), $userDn, $add);
1504
		}
1505
		else {
1506
			$mods = array(
1507
				array(
1508
					"attrib"  => "unicodePwd",
1509
					"modtype" => LDAP_MODIFY_BATCH_REMOVE,
1510
					"values"  => array($this->encodePassword($old_password)),
1511
				),
1512
				array(
1513
					"attrib"  => "unicodePwd",
1514
					"modtype" => LDAP_MODIFY_BATCH_ADD,
1515
					"values"  => array($this->encodePassword($password)),
1516
				),
1517
			);
1518
			$result = ldap_modify_batch($this->adldap->getLdapConnection(), $userDn, $mods);
1519
		}
1520
        if ($result === false){
1521
            $err = ldap_errno($this->adldap->getLdapConnection());
1522
            if ($err) {
1523
                $msg = 'Error ' . $err . ': ' . ldap_err2str($err) . '.';
1524
                if($err == 53) {
1525
                    $msg .= ' Your password might not match the password policy.';
1526
                }
1527
                throw new adLDAPException($msg);
1528
            }
1529
            else {
1530
                return false;
1531
            }
1532
        }
1533
1534
        return true;
1535
    }
1536
1537
    /**
1538
    * Modify a user
1539
    *
1540
    * @param string $username The username to query
1541
    * @param array $attributes The attributes to modify.  Note if you set the enabled attribute you must not specify any other attributes
1542
    * @param bool $isGUID Is the username passed a GUID or a samAccountName
1543
    * @return bool
1544
    */
1545
    public function modify($username, $attributes, $isGUID = false)
1546
    {
1547
        if ($username === NULL) { return "Missing compulsory field [username]"; }
0 ignored issues
show
The condition $username === NULL is always false.
Loading history...
1548
        if (array_key_exists("password", $attributes) && !$this->adldap->getUseSSL() && !$this->adldap->getUseTLS()) {
1549
            throw new adLDAPException('SSL/TLS must be configured on your webserver and enabled in the class to set passwords.');
1550
        }
1551
1552
        // Find the dn of the user
1553
        $userDn = $this->dn($username, $isGUID);
1554
        if ($userDn === false) {
0 ignored issues
show
The condition $userDn === false is always false.
Loading history...
1555
            return false;
1556
        }
1557
1558
        // Translate the update to the LDAP schema
1559
        $mod = $this->adldap->adldap_schema($attributes);
1560
1561
        // Check to see if this is an enabled status update
1562
        if (!$mod && !array_key_exists("enabled", $attributes)){
1563
            return false;
1564
        }
1565
1566
        // Set the account control attribute (only if specified)
1567
        if (array_key_exists("enabled", $attributes)){
1568
            if ($attributes["enabled"]){
1569
                $controlOptions = array("NORMAL_ACCOUNT");
1570
            }
1571
            else {
1572
                $controlOptions = array("NORMAL_ACCOUNT", "ACCOUNTDISABLE");
1573
            }
1574
            $mod["userAccountControl"][0] = $this->accountControl($controlOptions);
1575
        }
1576
		// we can NOT set password with ldap_add or ldap_modify, it needs ldap_mod_replace, at least under Win2008r2
1577
		unset($mod['unicodePwd']);
1578
1579
		if ($mod)
1580
		{
1581
	        // Do the update
1582
	        $result = @ldap_modify($ds=$this->adldap->getLdapConnection(), $userDn, $mod);
1583
	        if ($result == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1584
				if (isset($mod['unicodePwd'])) $mod['unicodePwd'] = '***';
1585
				error_log(__METHOD__."(".array2string($attributes).") ldap_modify($ds, '$userDn', ".array2string($mod).") returned ".array2string($result)." ldap_error()=".ldap_error($ds));
1586
	        	return false;
1587
	        }
1588
		}
1589
        if (array_key_exists("password",$attributes) && !$this->setPassword($userDn, $attributes['password']))
1590
		{
1591
			return false;
1592
		}
1593
		return true;
1594
	}
1595
}
1596
1597
/**
1598
 * Fixes an enhancements for adLDAPGroups required by EGroupware
1599
 */
1600
class adLDAPGroups extends \adLDAPGroups
1601
{
1602
	/**
1603
	 * Create a group
1604
	 *
1605
	 * Extended to allow to specify $attribute["container"] as string, because array hardcodes "OU=", while Samba4 and win2008r2 uses "CN=Users"
1606
	 *
1607
	 * @param array $attributes Default attributes of the group
1608
	 * @return bool
1609
	 */
1610
	public function create($attributes)
1611
	{
1612
		if (!is_array($attributes)){ return "Attributes must be an array"; }
0 ignored issues
show
The condition is_array($attributes) is always true.
Loading history...
1613
		if (!array_key_exists("group_name", $attributes)){ return "Missing compulsory field [group_name]"; }
1614
		if (!array_key_exists("container", $attributes)){ return "Missing compulsory field [container]"; }
1615
		if (empty($attributes["container"])){ return "Container attribute must be an array or string."; }
1616
1617
		//$member_array = array();
1618
		//$member_array[0] = "cn=user1,cn=Users,dc=yourdomain,dc=com";
1619
		//$member_array[1] = "cn=administrator,cn=Users,dc=yourdomain,dc=com";
1620
1621
		$add = array();
1622
		$add["cn"] = $attributes["group_name"];
1623
		$add["samaccountname"] = $attributes["group_name"];
1624
		$add["objectClass"] = "Group";
1625
		if (!empty($attributes["description"])) $add["description"] = $attributes["description"];
1626
		//$add["member"] = $member_array; UNTESTED
1627
1628
		// Determine the container
1629
		if (is_array($attributes['container'])) {
1630
			$attributes["container"] = array_reverse($attributes["container"]);
1631
			$attributes["container"] = "OU=" . implode(",OU=",$attributes["container"]);
1632
		}
1633
		$result = ldap_add($this->adldap->getLdapConnection(), "CN=" . $add["cn"] . "," . $attributes["container"] . "," . $this->adldap->getBaseDn(), $add);
1634
		if ($result != true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison !== instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1635
			return false;
1636
		}
1637
		return true;
1638
	}
1639
}
1640
1641
/**
1642
 * Fixes an enhancements for adLDAPUtils required by EGroupware
1643
 */
1644
class adLDAPUtils extends \adLDAPUtils
1645
{
1646
	/**
1647
	 * Convert 8bit characters e.g. accented characters to UTF8 encoded characters
1648
	 */
1649
	public function encode8Bit(&$item, $key)
1650
	{
1651
		return $this->adldap->encode8bit($item, $key);
0 ignored issues
show
Are you sure the usage of $this->adldap->encode8bit($item, $key) targeting adLDAP::encode8Bit() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1652
	}
1653
1654
    /**
1655
    * Escape strings for the use in LDAP filters
1656
    *
1657
    * DEVELOPERS SHOULD BE DOING PROPER FILTERING IF THEY'RE ACCEPTING USER INPUT
1658
    * Ported from Perl's Net::LDAP::Util escape_filter_value
1659
    *
1660
    * @param string $str The string the parse
1661
    * @author Port by Andreas Gohr <[email protected]>
1662
    * @return string
1663
    */
1664
    public function ldapSlashes($str){
1665
        return preg_replace_callback(
1666
      		'/([\x00-\x1F\*\(\)\\\\])/',
1667
        	function ($matches) {
1668
            	return "\\".join("", unpack("H2", $matches[1]));
1669
        	},
1670
        	$str
1671
    	);
1672
    }
1673
}
1674