Completed
Push — 16.1 ( 277864...22528c )
by Ralf
24:04 queued 06:02
created

setup_cmd_ldap::accounts_obj()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 15
nc 8
nop 1
dl 0
loc 24
rs 6.7272
c 0
b 0
f 0
1
<?php
2
/**
3
 * EGroupware setup - test or create the ldap connection and hierarchy
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
7
 * @package setup
8
 * @copyright (c) 2007-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
9
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10
 * @version $Id$
11
 */
12
13
use EGroupware\Api;
14
15
/**
16
 * setup command: test or create the ldap connection and hierarchy
17
 *
18
 * All commands can be run via setup-cli eg:
19
 *
20
 * setup/setup-cli.php [--dry-run] --setup_cmd_ldap <domain>,<config-user>,<config-pw> sub_command=set_mailbox \
21
 * 	ldap_base=dc=local ldap_root_dn=cn=admin,dc=local ldap_root_pw=secret ldap_host=localhost
22
 *
23
 * Changing uid/gidNumber to match SID in preparation to Samba4 migration:
24
 *
25
 * setup/setup-cli.php [--dry-run] --setup_cmd_ldap <domain>,<config-user>,<config-pw> sub_command=sid2uidnumber \
26
 * 	ldap_base=dc=local ldap_root_dn=cn=admin,dc=local ldap_root_pw=secret ldap_host=localhost
27
 *
28
 * - First run it with --dry-run to get ids to change / admin-cli command to change ids in EGroupware.
29
 * - Then run admin/admin-cli.php --change-account-id and after this command again without --dry-run.
30
 * - After that you can run the given setup/doc/chown.php command to change filesystem uid/gid in samba share.
31
 *   This is usually not needed as samba-tool clasicupgrade takes care of existing filesystem uid/gid by installing
32
 *   rfc2307 schema with uidNumber attributes.
33
 *
34
 * setup/setup-cli.php [--dry-run] --setup-cmd-ldap <domain>,<config-user>,<config-pw> sub_command=copy2ad \
35
 * 	ldap_base=dc=local ldap_root_dn=cn=admin,dc=local ldap_root_pw=secret ldap_host=localhost \
36
 * 	ads_domain=samba4.intern [ads_admin_user=Administrator] ads_admin_pw=secret ads_host=ad.samba4.intern [ads_connection=(ssl|tls)] \
37
 * 	attributes=@inetOrgPerson,accountExpires=shadowExpire
38
 *
39
 * - copies from samba-tool clasicupgrade not copied inetOrgPerson attributes and mail attributes to AD
40
 *
41
 * setup/setup-cli.php [--dry-run] --setup-cmd-ldap <domain>,<config-user>,<config-pw> sub_command=copy2ad \
42
 * 	ldap_base=dc=local ldap_root_dn=cn=admin,dc=local ldap_root_pw=secret ldap_host=localhost \
43
 * 	ads_domain=samba4.intern [ads_admin_user=Administrator] ads_admin_pw=secret \
44
 * 	ads_host=ad.samba4.intern [ads_connection=(ssl|tls)] [no_sid_check=1] \
45
 * 	attributes={smtp:}proxyAddresses=mail,{smtp:}proxyAddresses=mailalias,{quota:}proxyAddresses=mailuserquota,{forward:}proxyaddresses=maildrop
46
 *
47
 * - copies mail-attributes from ldap to AD (example is from Mandriva mailAccount schema, need to adapt to other schema!)
48
 *   (no_sid_check=1 uses all objectClass=posixAccount, not checking for having a SID and uid not ending in $ for computer Api\Accounts)
49
 *
50
 * setup/setup-cli.php [--dry-run] --setup-cmd-ldap <domain>,<config-user>,<config-pw> sub_command=passwords_to_sql \
51
 * 	ldap_context=ou=accounts,dc=local ldap_root_dn=cn=admin,dc=local ldap_root_pw=secret ldap_host=localhost
52
 *
53
 * - updating passwords for existing users in SQL from LDAP, eg. to switch off authentication to LDAP on a SQL install.
54
 */
55
class setup_cmd_ldap extends setup_cmd
56
{
57
	/**
58
	 * Allow to run this command via setup-cli
59
	 */
60
	const SETUP_CLI_CALLABLE = true;
61
62
	/**
63
	 * Instance of ldap object
64
	 *
65
	 * @var ldap
66
	 */
67
	private $test_ldap;
68
69
	/**
70
	 * Constructor
71
	 *
72
	 * @param string/array $domain domain-name to customize the defaults or array with all parameters
73
	 * @param string $ldap_host =null
74
	 * @param string $ldap_suffix =null base of the whole ldap install, default "dc=local"
75
	 * @param string $ldap_admin =null root-dn needed to create new entries in the suffix
76
	 * @param string $ldap_admin_pw =null
77
	 * @param string $ldap_base =null base of the instance, default "o=$domain,$suffix"
78
	 * @param string $ldap_root_dn =null root-dn used for the instance, default "cn=admin,$base"
79
	 * @param string $ldap_root_pw =null
80
	 * @param string $ldap_context =null ou for accounts, default "ou=accounts,$base"
81
	 * @param string $ldap_search_filter =null search-filter for accounts, default "(uid=%user)"
82
	 * @param string $ldap_group_context =null ou for groups, default "ou=groups,$base"
83
	 * @param string $sub_command ='create_ldap' 'create_ldap', 'test_ldap', 'test_ldap_root', see exec method
84
	 * @param string $ldap_encryption_type ='des'
85
	 * @param boolean $truncate_egw_accounts =false truncate accounts table before migration to SQL
86
	 */
87
	function __construct($domain,$ldap_host=null,$ldap_suffix=null,$ldap_admin=null,$ldap_admin_pw=null,
88
		$ldap_base=null,$ldap_root_dn=null,$ldap_root_pw=null,$ldap_context=null,$ldap_search_filter=null,
89
		$ldap_group_context=null,$sub_command='create_ldap',$ldap_encryption_type='des',$truncate_egw_accounts=false)
90
	{
91
		if (!is_array($domain))
92
		{
93
			$domain = array(
94
				'domain'        => $domain,
95
				'ldap_host'     => $ldap_host,
96
				'ldap_suffix'   => $ldap_suffix,
97
				'ldap_admin'    => $ldap_admin,
98
				'ldap_admin_pw' => $ldap_admin_pw,
99
				'ldap_base'     => $ldap_base,
100
				'ldap_root_dn'  => $ldap_root_dn,
101
				'ldap_root_pw'  => $ldap_root_pw,
102
				'ldap_context'  => $ldap_context,
103
				'ldap_search_filter' => $ldap_search_filter,
104
				'ldap_group_context' => $ldap_group_context,
105
				'sub_command'   => $sub_command,
106
				'ldap_encryption_type' => $ldap_encryption_type,
107
				'truncate_egw_accounts' => $truncate_egw_accounts,
108
			);
109
		}
110
		//echo __CLASS__.'::__construct()'; _debug_array($domain);
111
		admin_cmd::__construct($domain);
112
	}
113
114
	/**
115
	 * run the command: test or create the ldap connection and hierarchy
116
	 *
117
	 * @param boolean $check_only =false only run the checks (and throw the exceptions), but not the command itself
118
	 * @return string success message
119
	 * @throws Exception(lang('Wrong credentials to access the header.inc.php file!'),2);
120
	 * @throws Exception('header.inc.php not found!');
121
	 */
122
	protected function exec($check_only=false)
123
	{
124 View Code Duplication
		if (!empty($this->domain) && !preg_match('/^([a-z0-9_-]+\.)*[a-z0-9]+/i',$this->domain))
125
		{
126
			throw new Api\Exception\WrongUserinput(lang("'%1' is no valid domain name!",$this->domain));
127
		}
128
		if ($this->remote_id && $check_only && !in_array($this->sub_command, array('set_mailbox', 'sid2uidnumber', 'copy2ad')))
129
		{
130
			return true;	// further checks can only done locally
131
		}
132
		$this->_merge_defaults();
133
		//_debug_array($this->as_array());
134
135
		switch($this->sub_command)
136
		{
137
			case 'test_ldap_root':
138
				$msg = $this->connect($this->ldap_admin,$this->ldap_admin_pw);
139
				break;
140
			case 'test_ldap':
141
				$msg = $this->connect();
142
				break;
143
			case 'delete_ldap':
144
				$msg = $this->delete_base();
145
				break;
146
			case 'users_ldap':
147
				$msg = $this->users();
148
				break;
149
			case 'migrate_to_ldap':
150
			case 'migrate_to_sql':
151
			case 'migrate_to_univention':
152
			case 'passwords_to_sql':
153
				$msg = $this->migrate($this->sub_command);
154
				break;
155
			case 'set_mailbox':
156
				$msg = $this->set_mailbox($check_only);
157
				break;
158
			case 'sid2uidnumber':
159
				$msg = $this->sid2uidnumber($check_only);
160
				break;
161
			case 'copy2ad':
162
				$msg = $this->copy2ad($check_only);
163
				break;
164
			case 'create_ldap':
165
			default:
166
				$msg = $this->create();
167
				break;
168
		}
169
		return $msg;
170
	}
171
172
	const sambaSID = 'sambasid';
0 ignored issues
show
Coding Style introduced by
This class constant is not uppercase (expected SAMBASID).
Loading history...
173
174
	/**
175
	 * Change uidNumber and gidNumber to match rid (last part of sambaSID)
176
	 *
177
	 * First run it with --dry-run to get ids to change / admin-cli command to change ids in EGroupware.
178
	 * Then run admin/admin-cli.php --change-account-id and after this command again without --dry-run.
179
	 * After that you need to run the given chown.php command to change filesystem uid/gid in samba share.
180
	 *
181
	 * @param boolean $check_only =false true: only connect and output necessary commands
182
	 */
183
	private function sid2uidnumber($check_only=false)
184
	{
185
		$msg = array();
186
		$this->connect();
187
188
		// check if base does exist
189 View Code Duplication
		if (!@ldap_read($this->test_ldap->ds,$this->ldap_base,'objectClass=*'))
190
		{
191
			throw new Api\Exception\WrongUserinput(lang('Base dn "%1" NOT found!',$this->ldap_base));
192
		}
193
194
		if (!($sr = ldap_search($this->test_ldap->ds,$this->ldap_base,
195
			$search='(&(|(objectClass=posixAccount)(objectClass=posixGroup))('.self::sambaSID.'=*)(!(uid=*$)))',
196
			array('uidNumber','gidNumber','uid','cn', 'objectClass',self::sambaSID))) ||
197
			!($entries = ldap_get_entries($this->test_ldap->ds, $sr)))
198
		{
199
			throw new Api\Exception(lang('Error searching "dn=%1" for "%2"!',$this->ldap_base, $search));
200
		}
201
		$change = $accounts = array();
202
		$cmd_change_account_id = 'admin/admin-cli.php --change-account-id <admin>@<domain>,<adminpw>';
203
		$change_account_id = '';
204
		foreach($entries as $key => $entry)
205
		{
206
			if ($key === 'count') continue;
207
208
			$entry = Api\Ldap::result2array($entry);
209
			$accounts[$entry['dn']] = $entry;
210
			//print_r($entry);
211
212
			$parts = explode('-', $entry[self::sambaSID]);
213
			$rid = array_pop($parts);
214
215
			if (in_array('posixAccount', $entry['objectclass']))
216
			{
217
				$id = $entry['uidnumber'];
218
			}
219
			else
220
			{
221
				$id = -$entry['gidnumber'];
222
				$rid *= -1;
223
			}
224
			if ($id != $rid)
225
			{
226
				$change[$id] = $rid;
227
				$change_account_id .= ','.$id.','.$rid;
228
			}
229
		}
230
		//print_r($change); die('Stop');
231
232
		// change account-ids inside EGroupware
233
		if ($check_only) $msg[] = "You need to run now:\n$cmd_change_account_id $change_account_id";
234
		//$cmd = new admin_cmd_change_account_id($change);
235
		//$msg[] = $cmd->run($time=null, $set_modifier=false, $skip_checks=false, $check_only);
236
237
		// now change them in LDAP
238
		$changed = 0;
239
		foreach($accounts as $dn => $account)
240
		{
241
			$modify = array();
242
			if (!empty($account['uidnumber']) && isset($change[$account['uidnumber']]))
243
			{
244
				$modify['uidnumber'] = $change[$account['uidnumber']];
245
			}
246
			if (isset($change[-$account['gidnumber']]))
247
			{
248
				$modify['gidnumber'] = -$change[-$account['gidnumber']];
249
			}
250
			if (!$check_only && $modify && !ldap_modify($this->test_ldap->ds, $dn, $modify))
251
			{
252
				throw new Api\Exception("Failed to modify ldap: !ldap_modify({$this->test_ldap->ds}, '$dn', ".array2string($modify).") ".ldap_error($this->test_ldap->ds).
253
					"\n- ".implode("\n- ", $msg));	// EGroupware change already run successful
254
			}
255
			if ($modify) ++$changed;
256
		}
257
		$msg[] = "You need to run now on your samba share(s):\nsetup/doc/chown.php -R $change_account_id <share>";
258
259
		return ($check_only ? 'Need to update' : 'Updated')." $changed entries with new uid/gidNumber in LDAP".
260
			"\n- ".implode("\n- ", $msg);
261
	}
262
263
	/**
264
	 * Copy given attributes of accounts of one ldap to active directory
265
	 *
266
	 * @param boolean $check_only =false true: only connect and output necessary commands
267
	 */
268
	private function copy2ad($check_only=false)
269
	{
270
		$msg = array();
271
		$attrs = $rename = array();
272
		foreach(explode(',', $this->attributes) as $attr)
273
		{
274
			if ($attr[0] == '@' ||	// copy whole objectclass without renaming, eg. @inetOrgPerson
275
				strpos($attr, '=') === false)
276
			{
277
				$attrs[] = $attr;
278
			}
279
			else
280
			{
281
				list($to, $from) = explode('=', $attr);
282
				if ($from) $attrs[] = $from;
283
				$rename[strtolower($from)] = $to;
284
			}
285
		}
286
		$ignore_attr = array_flip(array('dn', 'objectclass', 'cn', 'userpassword'));
287
		if (!in_array('uid', $attrs))
288
		{
289
			$attrs[] = 'uid';	// need to match account
290
			$ignore_attr['uid'] = true;
291
		}
292
		// connect to destination ads
293
		if (empty($this->ads_context))
294
		{
295
			$this->ads_context = 'CN=Users,DC='.implode(',DC=', explode('.', $this->ads_domain));
0 ignored issues
show
Documentation introduced by
The property ads_context does not exist on object<setup_cmd_ldap>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
296
		}
297
		if (empty($this->ads_admin_user)) $this->ads_admin_user = 'Administrator';
0 ignored issues
show
Documentation introduced by
The property ads_admin_user does not exist on object<setup_cmd_ldap>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
298
		$admin_dn = strpos($this->ads_admin_user, '=') !== false ? $this->ads_admin_user :
299
			'CN='.$this->ads_admin_user.','.$this->ads_context;
300
		switch($this->ads_connection)
301
		{
302
			case 'ssl':
303
				$url = 'ldaps://'.$this->ads_host.'/';
304
				break;
305
			case 'tls':
306
				$url = 'tls://'.$this->ads_host.'/';
307
				break;
308
			default:
309
				$url = 'ldap://'.$this->ads_host.'/';
310
				break;
311
		}
312
		$this->connect($admin_dn, $this->ads_admin_pw, $url);
313
		$ads = $this->test_ldap; unset($this->test_ldap);
314
315
		// check if ads base does exist
316
		if (!@ldap_read($ads->ds, $this->ads_context, 'objectClass=*'))
317
		{
318
			throw new Api\Exception\WrongUserinput(lang('Ads dn "%1" NOT found!',$this->ads_context));
319
		}
320
321
		// connect to source ldap
322
		$this->connect();
323
324
		// check if ldap base does exist
325 View Code Duplication
		if (!@ldap_read($this->test_ldap->ds,$this->ldap_base,'objectClass=*'))
326
		{
327
			throw new Api\Exception\WrongUserinput(lang('Base dn "%1" NOT found!',$this->ldap_base));
328
		}
329
330
		if (!($sr = ldap_search($this->test_ldap->ds,$this->ldap_base,
331
			$search = $this->no_sid_check ? '(objectClass=posixAccount)' :
332
				'(&(objectClass=posixAccount)('.self::sambaSID.'=*)(!(uid=*$)))', $attrs)) ||
333
			!($entries = ldap_get_entries($this->test_ldap->ds, $sr)))
334
		{
335
			throw new Api\Exception(lang('Error searching "dn=%1" for "%2"!',$this->ldap_base, $search));
336
		}
337
		$changed = 0;
338
		$utc_diff = null;
339
		foreach($entries as $key => $entry)
340
		{
341
			if ($key === 'count') continue;
342
343
			$entry_arr = Api\Ldap::result2array($entry);
344
			$uid = $entry_arr['uid'];
345
			$entry = array_diff_key($entry_arr, $ignore_attr);
346
347
			if (!($sr = ldap_search($ads->ds, $this->ads_context,
348
				$search='(&(objectClass=user)(sAMAccountName='.Api\Ldap::quote($uid).'))', array('dn'))) ||
349
				!($dest = ldap_get_entries($ads->ds, $sr)))
350
			{
351
				$msg[] = lang('User "%1" not found!', $uid);
352
				continue;
353
			}
354
			$dn = $dest[0]['dn'];
355
			if (isset($rename[''])) $entry[''] = '';
356
			// special handling for copying shadowExpires to accountExpires
357
			if (strtolower($rename['shadowexpire']) === 'accountexpires')
358
			{
359
				// need to write accountExpires for never expiring account, as samba-tool classicupgrade sets it to 2038-01-19
360
				if (!isset($entry['shadowexpire']) || !$entry['shadowexpire'])
361
				{
362
					$entry['shadowexpire'] = accounts_ads::EXPIRES_NEVER;
363
				}
364
				else
365
				{
366
					if (is_null($utc_diff)) $utc_diff = date('Z');
367
					$entry['shadowexpire'] = accounts_ads::convertUnixTimeToWindowsTime(
368
						$entry['shadowexpire']*24*3600+$utc_diff);	// ldap time to unixTime
369
				}
370
			}
371
			$update = array();
372
			foreach($entry as $attr => $value)
373
			{
374
				if ($value || $attr === '')
375
				{
376
					$to = isset($rename[$attr]) ? $rename[$attr] : $attr;
377
					$prefix = null;
378
					if ($to[0] == '{')	// eg. {smtp:}proxyAddresses=forwardTo
379
					{
380
						list($prefix, $to) = explode('}', substr($to, 1));
381
					}
382
					foreach((array)$value as $val)
383
					{
384
						if (isset($update[$to]))
385
						{
386
							if (!is_array($update[$to])) $update[$to] = array($update[$to]);
387
							// we need to check (caseinsensitive) if value already exists in set
388
							// as AD chokes on doublicate values "Type or value exists"
389
							foreach($update[$to] as $v)
390
							{
391
								if (!strcasecmp($v, $prefix.$val)) continue 2;
392
							}
393
							$update[$to][] = $prefix.$val;
394
						}
395
						else
396
						{
397
							$update[$to] = $prefix.$val;
398
						}
399
					}
400
				}
401
			}
402
			if ($check_only)
403
			{
404
				print_r($dn);
405
				print_r($update);
406
				continue;
407
			}
408
			if ($update && !ldap_modify($ads->ds, $dn, $update))
409
			{
410
				error_log(lang('Failed updating user "%1" dn="%2"!', $uid, $dn).' '.ldap_error($ads->ds));
411
			}
412
			else
413
			{
414
				print_r(lang('User "%1" dn="%2" successful updated.', $uid, $dn)."\n");
415
				$changed++;
416
			}
417
		}
418
		if ($check_only) return lang("%1 accounts to copy found.", count($entries));
419
420
		return "Copied data of $changed accounts from LDAP to AD ".
421
			(count($msg) > $changed ? ' ('.(count($msg)-$changed).' errors!)' : '');
422
	}
423
424
	/**
425
	 * Migrate to other account storage
426
	 *
427
	 * @param string $mode "passwords_to_sql", "migrate_to_(sql|ldap|univention)"
428
	 * @return string with success message
429
	 * @throws Exception on error
430
	 */
431
	private function migrate($mode)
432
	{
433
		// support old boolean mode
434
		if (is_bool($mode)) $mode = $mode ? 'migrate_to_ldap' : 'migrate_to_sql';
435
436
		$passwords2sql = $mode === "passwords_to_sql";
437
		list(,$to) = explode('_to_', $mode);
438
439
		$msg = array();
440
		// if migrating to ldap, check ldap and create context if not yet exiting
441
		if ($to == 'ldap' && !empty($this->ldap_admin_pw))
442
		{
443
			$msg[] = $this->create();
444
		}
445
		elseif ($this->account_repository !== 'ads')
446
		{
447
			$msg[] = $this->connect();
448
		}
449
		// read accounts from old store
450
		$accounts = $this->accounts($to == 'sql' ? $this->account_repository : 'sql', $passwords2sql ? 'accounts' : 'both');
451
452
		// clean up SQL before migration
453 View Code Duplication
		if ($to == 'sql' && $this->truncate_egw_accounts)
0 ignored issues
show
Bug introduced by
The property truncate_egw_accounts does not seem to exist. Did you mean accounts?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
454
		{
455
			$GLOBALS['egw']->db->query('TRUNCATE TABLE egw_accounts', __LINE__, __FILE__);
456
			$GLOBALS['egw']->db->query('DELETE FROM egw_addressbook WHERE account_id IS NOT NULL', __LINE__, __FILE__);
457
		}
458
		// instanciate accounts obj for new store
459
		$accounts_obj = $this->accounts_obj($to);
460
461
		$accounts_created = $groups_created = $errors = $egw_info_set = 0;
462
		$emailadmin_src = $ldap_class = null;
463
		$target = strtoupper($to);
464
		foreach($accounts as $account_id => $account)
465
		{
466
			if (isset($this->only) && !in_array($account_id,$this->only))
467
			{
468
				continue;
469
			}
470
			$what = ($account['account_type'] == 'u' ? lang('User') : lang('Group')).' '.
471
				$account_id.' ('.$account['account_lid'].')';
472
473
			// if we migrate passwords from an authentication source, we need to use account_lid, not numerical id
474
			if ($passwords2sql && ($id = $accounts_obj->name2id($account['account_lid'], 'account_lid', 'u')))
475
			{
476
				$account_id = $id;
477
			}
478
479
			// invalidate cache: otherwise no migration takes place, if cached results says account already exists
480
			Api\Accounts::cache_invalidate($account_id);
481
482
			if ($passwords2sql)
483
			{
484
				if (!($sql_account = $accounts_obj->read($account_id)))
485
				{
486
					$msg[] = lang('%1 does NOT exist in %2.',$what,$target);
487
					$errors++;
488
				}
489
				elseif(empty($account['account_pwd']))
490
				{
491
					$msg[] = lang('%1 does NOT have a password (userPassword attribute) or we are not allowed to read it!',$what);
492
					$errors++;
493
				}
494
				else
495
				{
496
					$sql_account['account_passwd'] = self::hash_ldap2sql($account['account_pwd']);
497
498
					if (!$accounts_obj->save($sql_account))
499
					{
500
						$msg[] = lang('Update of %1 in %2 failed !!!',$what,$target);
501
						$errors++;
502
					}
503
					else
504
					{
505
						$msg[] = lang('%1 password set in %2.',$what,$target);
506
						$accounts_created++;
507
					}
508
				}
509
				continue;
510
			}
511
512
			if ($account['account_type'] == 'u')
513
			{
514
				if ($accounts_obj->exists($account_id))
515
				{
516
					$msg[] = lang('%1 already exists in %2.',$what,$target);
517
					$errors++;
518
					continue;
519
				}
520
				if ($to != 'sql')
521
				{
522
					if ($GLOBALS['egw_info']['server']['ldap_extra_attributes'])
523
					{
524
						$account['homedirectory'] = $GLOBALS['egw_info']['server']['ldap_account_home'] . '/' . $account['account_lid'];
525
						$account['loginshell'] = $GLOBALS['egw_info']['server']['ldap_account_shell'];
526
					}
527
					$account['account_passwd'] = self::hash_sql2ldap($account['account_pwd']);
528
				}
529
				else
530
				{
531
					$account['account_passwd'] = self::hash_ldap2sql($account['account_pwd']);
532
				}
533
				unset($account['person_id']);
534
535 View Code Duplication
				if (!$accounts_obj->save($account))
536
				{
537
					$msg[] = lang('Creation of %1 in %2 failed !!!',$what,$target);
538
					$errors++;
539
					continue;
540
				}
541
				$accounts_obj->set_memberships($account['memberships'],$account_id);
542
				$msg[] = lang('%1 created in %2.',$what,$target);
543
				$accounts_created++;
544
545
				// check if we need to migrate mail-account
546
				if (!isset($ldap_class) && $this->account_repository !== 'ads')
547
				{
548
					$ldap_class = false;
549
					$ldap = Api\Ldap::factory(false);
550
					foreach(array(	// todo: have these enumerated by emailadmin ...
551
						'qmailUser' => 'EGroupware\\Api\\Mail\\Smtp\\Oldqmailuser',
552
						'dbMailUser' => 'EGroupware\\Api\\Mail\\Smtp\\Dbmailuser',
553
						// nothing to migrate for inetOrgPerson ...
554
					) as $object_class => $class)
555
					{
556
						if ($ldap->getLDAPServerInfo()->supportsObjectClass($object_class))
557
						{
558
							$ldap_class = $class;
559
							break;
560
						}
561
					}
562
				}
563
				if ($ldap_class)
564
				{
565
					if (!isset($emailadmin_src))
566
					{
567
						if ($to != 'sql')
568
						{
569
							$emailadmin_src = new Api\Mail\Smtp\Sql();
570
							$emailadmin_dst = new $ldap_class();
571
						}
572
						else
573
						{
574
							$emailadmin_src = new $ldap_class();
575
							$emailadmin_dst = new Api\Mail\Smtp\Sql();
576
						}
577
					}
578
					if (($mailaccount = $emailadmin_src->getUserData($account_id)))
579
					{
580
						//echo "<p>".array2string($mailaccount).': ';
581
						$emailadmin_dst->setUserData($account_id, (array)$mailaccount['mailAlternateAddress'],
582
							(array)$mailaccount['mailForwardingAddress'], $mailaccount['deliveryMode'],
583
							$mailaccount['accountStatus'], $mailaccount['mailLocalAddress'],
584
							$mailaccount['quotaLimit'], false, $mailaccount['mailMessageStore']);
585
586
						$msg[] = lang("Mail account of %1 migraged", $account['account_lid']);
587
					}
588
					//else echo "<p>No mail account data found for #$account_id $account[account_lid]!</p>\n";
589
				}
590
591
				// should we run any or some addAccount hooks
592
				if ($this->add_account_hook)
593
				{
594
					// setting up egw_info array with new ldap information, so hook can use Api\Ldap::ldapConnect()
595
					if (!$egw_info_set++)
596
					{
597
						foreach(array('ldap_host','ldap_root_dn','ldap_root_pw','ldap_context','ldap_group_context','ldap_search_filter','ldap_encryptin_type','mail_suffix','mail_login_type') as $name)
598
						{
599
							 if (!empty($this->$name)) $GLOBALS['egw_info']['server'][$name] = $this->$name;
600
						}
601
						//error_log(__METHOD__."() setup up egw_info[server]: ldap_host='{$GLOBALS['egw_info']['server']['ldap_host']}', ldap_root_dn='{$GLOBALS['egw_info']['server']['ldap_root_dn']}', ldap_root_pw='{$GLOBALS['egw_info']['server']['ldap_root_pw']}', ldap_context='{$GLOBALS['egw_info']['server']['ldap_context']}', mail_suffix='{$GLOBALS['egw_info']['server']['mail_suffix']}', mail_logig_type='{$GLOBALS['egw_info']['server']['mail_login-type']}'");
602
					}
603
					try
604
					{
605
						$account['location'] = 'addAccount';
606
						// running all addAccount hooks (currently NOT working, as not all work in setup)
607
						if ($this->add_account_hook === true)
608
						{
609
							Api\Hooks::process($account, array(), true);
610
						}
611
						elseif(is_callable($this->add_account_hook))
612
						{
613
							call_user_func($this->add_account_hook,$account);
614
						}
615
					}
616
					catch(Exception $e)
617
					{
618
						$msg[] = $e->getMessage();
619
						$errors++;
620
					}
621
				}
622
			}
623
			else
624
			{
625
				// check if group already exists
626
				if (!$accounts_obj->exists($account_id))
627
				{
628 View Code Duplication
					if (!$accounts_obj->save($account))
629
					{
630
						$msg[] = lang('Creation of %1 in %2 failed !!!',$what,$target);
631
						++$errors;
632
						continue;
633
					}
634
					$msg[] = lang('%1 created in %2.',$what,$target);
635
					$groups_created++;
636
				}
637
				else
638
				{
639
					$msg[] = lang('%1 already exists in %2.',$what,$target);
640
					$errors++;
641
642
					if ($accounts_obj->id2name($account_id) != $account['account_lid'])
643
					{
644
						$msg[] = lang("==> different group '%1' under that gidNumber %2, NOT setting memberships!",$account['account_lid'],$account_id);
645
						++$errors;
646
						continue;	// different group under that gidnumber!
647
					}
648
				}
649
				// now saving / updating the memberships
650
				$accounts_obj->set_members($account['members'],$account_id);
651
			}
652
		}
653
		if ($passwords2sql)
654
		{
655
			return lang('%1 passwords updated, %3 errors',$accounts_created,$groups_created,$errors).
656
				($errors || $this->verbose ? "\n- ".implode("\n- ",$msg) : '');
657
		}
658
		// migrate addressbook data
659
		$GLOBALS['egw_info']['user']['apps']['admin'] = true;	// otherwise migration will not run in setup!
660
		$addressbook = new Api\Contacts\Storage();
661
		foreach($this->as_array() as $name => $value)
662
		{
663
			if (substr($name, 5) == 'ldap_')
664
			{
665
				$GLOBALS['egw_info']['server'][$name] = $value;
666
			}
667
		}
668
		ob_start();
669
		$addressbook->migrate2ldap($to != 'sql' ? 'accounts' : 'accounts-back'.
670
			($this->account_repository == 'ads' ? '-ads' : ''));
671
		$msgs = array_merge($msg, explode("\n", strip_tags(ob_get_clean())));
672
673
		$this->restore_db();
674
675
		return lang('%1 users and %2 groups created, %3 errors',$accounts_created,$groups_created,$errors).
676
			($errors || $this->verbose ? "\n- ".implode("\n- ",$msgs) : '');
677
	}
678
679
	/**
680
	 * Convert SQL hash to LDAP hash
681
	 *
682
	 * @param string $hash
683
	 * @return string
684
	 */
685
	public static function hash_sql2ldap($hash)
686
	{
687
		if (!($type = $GLOBALS['egw_info']['server']['sql_encryption_type'])) $type = 'md5';
688
689
		$matches = null;
690 View Code Duplication
		if (preg_match('/^\\{(.*)\\}(.*)$/',$hash,$matches))
691
		{
692
			list(,$type,$hash) = $matches;
693
		}
694
		elseif (preg_match('/^[0-9a-f]{32}$/',$hash))
695
		{
696
			$type = 'md5';
697
		}
698
		switch(strtolower($type))
699
		{
700
			case 'plain':
701
				// ldap stores plaintext passwords without {plain} prefix
702
				break;
703
704
			case 'md5':
705
				$hash = base64_encode(pack("H*",$hash));
706
				// fall through
707
			default:
708
				$hash = '{'.strtoupper($type).'}'.$hash;
709
		}
710
		return $hash;
711
	}
712
713
	/**
714
	 * Convert LDAP hash to SQL hash
715
	 *
716
	 * @param string $hash
717
	 * @return string
718
	 */
719
	public static function hash_ldap2sql($hash)
720
	{
721
		if ($hash[0] != '{')	// plain has to be explicitly specified for sql, in ldap it's the default
722
		{
723
			$hash = '{PLAIN}'.$hash;
724
		}
725
		return $hash;
726
	}
727
728
	/**
729
	 * Read all accounts from sql or ldap
730
	 *
731
	 * @param string $from ='ldap', 'ldap', 'sql', 'univention'
732
	 * @param string $type ='both'
733
	 * @return array
734
	 */
735
	public function accounts($from='ldap', $type='both')
736
	{
737
		$accounts_obj = $this->accounts_obj($from);
738
		//error_log(__METHOD__."(from_ldap=".array2string($from_ldap).') get_class(accounts_obj->backend)='.get_class($accounts_obj->backend));
739
740
		$accounts = $accounts_obj->search(array('type' => $type, 'objectclass' => true, 'active' => false));
741
742
		foreach($accounts as $account_id => &$account)
743
		{
744
			if ($account_id != $account['account_id'])	// not all backends have as key the account_id
745
			{
746
				unset($account);
747
				$account_id = $account['account_id'];
748
			}
749
			$account += $accounts_obj->read($account_id);
750
751
			if ($account['account_type'] == 'g')
752
			{
753
				$account['members'] = $accounts_obj->members($account_id,true);
754
			}
755
			else
756
			{
757
				$account['memberships'] = $accounts_obj->memberships($account_id,true);
758
			}
759
		}
760
		Api\Accounts::cache_invalidate();
761
762
		return $accounts;
763
	}
764
765
	/**
766
	 * Instanciate accounts object from either sql of ldap
767
	 *
768
	 * @param string $type 'ldap', 'sql', 'univention'
769
	 * @return Api\Accounts
770
	 */
771
	private function accounts_obj($type)
772
	{
773
		static $enviroment_setup=null;
774
		if (!$enviroment_setup)
775
		{
776
			parent::_setup_enviroment($this->domain);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (_setup_enviroment() instead of accounts_obj()). Are you sure this is correct? If so, you might want to change this to $this->_setup_enviroment().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
777
			$enviroment_setup = true;
778
		}
779
		if ($type != 'sql' && $type != 'ads') $this->connect();	// throws exception, if it can NOT connect
780
781
		// otherwise search does NOT work, as accounts_sql uses addressbook_bo for it
782
		$GLOBALS['egw_info']['server']['account_repository'] = $type;
783
784
		if (!self::$egw_setup->setup_account_object(
785
			array(
786
				'account_repository' => $GLOBALS['egw_info']['server']['account_repository'],
787
			) + $this->as_array()) ||
788
			!is_a(self::$egw_setup->accounts, 'EGroupware\\Api\\Accounts') ||
789
			!is_a(self::$egw_setup->accounts->backend, 'EGroupware\\Api\\Accounts\\'.ucfirst($type)))
790
		{
791
			throw new Exception(lang("Can NOT instancate accounts object for %1", strtoupper($type)));
792
		}
793
		return self::$egw_setup->accounts;
794
	}
795
796
	/**
797
	 * Connect to ldap server
798
	 *
799
	 * @param string $dn =null default $this->ldap_root_dn
800
	 * @param string $pw =null default $this->ldap_root_pw
801
	 * @param string $host =null default $this->ldap_host, hostname, ip or ldap-url
802
	 * @throws Api\Exception\WrongUserinput Can not connect to ldap ...
803
	 */
804
	private function connect($dn=null,$pw=null,$host=null)
805
	{
806
		if (is_null($dn)) $dn = $this->ldap_root_dn;
0 ignored issues
show
Documentation introduced by
The property ldap_root_dn does not exist on object<setup_cmd_ldap>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
807
		if (is_null($pw)) $pw = $this->ldap_root_pw;
0 ignored issues
show
Documentation introduced by
The property ldap_root_pw does not exist on object<setup_cmd_ldap>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
808
		if (is_null($host)) $host = $this->ldap_host;
0 ignored issues
show
Documentation introduced by
The property ldap_host does not exist on object<setup_cmd_ldap>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
809
810
		if (!$pw)	// Api\Ldap::ldapConnect use the current eGW's pw otherwise
811
		{
812
			throw new Api\Exception\WrongUserinput(lang('You need to specify a password!'));
813
		}
814
815
		try {
816
			$this->test_ldap = Api\Ldap::factory(false, $host, $dn, $pw);
817
		}
818
		catch (Api\Exception\NoPermission $e) {
819
			_egw_log_exception($e);
820
			throw new Api\Exception\WrongUserinput(lang('Can not connect to LDAP server on host %1 using DN %2!',
821
				$host,$dn).($this->test_ldap->ds ? ' ('.ldap_error($this->test_ldap->ds).')' : ''));
822
		}
823
		return lang('Successful connected to LDAP server on %1 using DN %2.',$this->ldap_host,$dn);
824
	}
825
826
	/**
827
	 * Count active (not expired) users
828
	 *
829
	 * @return int number of active users
830
	 * @throws Api\Exception\WrongUserinput
831
	 */
832
	private function users()
833
	{
834
		$this->connect();
835
836
		$sr = ldap_list($this->test_ldap->ds,$this->ldap_context,'ObjectClass=posixAccount',array('dn','shadowExpire'));
837
		if (!($entries = ldap_get_entries($this->test_ldap->ds, $sr)))
838
		{
839
			throw new Api\Exception('Error listing "dn=%1"!',$this->ldap_context);
840
		}
841
		$num = 0;
842
		foreach($entries as $n => $entry)
843
		{
844
			if ($n === 'count') continue;
845
			if (isset($entry['shadowexpire']) && $entry['shadowexpire'][0]*24*3600 < time()) continue;
846
			++$num;
847
		}
848
		return $num;
849
	}
850
851
	/**
852
	 * Check and if does not yet exist create the new database and user
853
	 *
854
	 * @return string with success message
855
	 * @throws Api\Exception\WrongUserinput
856
	 */
857
	private function create()
858
	{
859
		$this->connect($this->ldap_admin,$this->ldap_admin_pw);
860
861
		foreach(array(
862
			$this->ldap_base => array(),
863
			$this->ldap_context => array(),
864
			$this->ldap_group_context => array(),
865
			$this->ldap_root_dn => array('userPassword' => Api\Auth::encrypt_ldap($this->ldap_root_pw,'ssha')),
866
		) as $dn => $extra)
867
		{
868
			if (!$this->_create_node($dn,$extra,$this->check_only) && $dn == $this->ldap_root_dn)
0 ignored issues
show
Unused Code introduced by
The call to setup_cmd_ldap::_create_node() has too many arguments starting with $this->check_only.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
869
			{
870
				// ldap_root already existed, lets check the pw is correct
871
				$this->connect();
872
			}
873
		}
874
		return lang('Successful connected to LDAP server on %1 and created/checked required structur %2.',
875
			$this->ldap_host,$this->ldap_base);
876
	}
877
878
	/**
879
	 * Delete whole LDAP tree of an instance dn=$this->ldap_base using $this->ldap_admin/_pw
880
	 *
881
	 * @return string with success message
882
	 * @throws Api\Exception if dn not found, not listable or delete fails
883
	 */
884
	private function delete_base()
885
	{
886
		$this->connect($this->ldap_admin,$this->ldap_admin_pw);
887
888
		// if base not set, use context minus one hierarchy, eg. ou=accounts,(o=domain,dc=local)
889 View Code Duplication
		if (empty($this->ldap_base) && $this->ldap_context)
890
		{
891
			list(,$this->ldap_base) = explode(',',$this->ldap_context,2);
892
		}
893
		// some precausion to not delete whole ldap tree!
894
		if (count(explode(',',$this->ldap_base)) < 2)
895
		{
896
			throw new Api\Exception\AssertionFailed(lang('Refusing to delete dn "%1"!',$this->ldap_base));
897
		}
898
		// check if base does exist
899 View Code Duplication
		if (!@ldap_read($this->test_ldap->ds,$this->ldap_base,'objectClass=*'))
900
		{
901
			throw new Api\Exception\WrongUserinput(lang('Base dn "%1" NOT found!',$this->ldap_base));
902
		}
903
		return lang('LDAP dn="%1" with %2 entries deleted.',
904
			$this->ldap_base,$this->rdelete($this->ldap_base));
905
	}
906
907
	/**
908
	 * Recursive delete a dn
909
	 *
910
	 * @param string $dn
911
	 * @return int integer number of deleted entries
912
	 * @throws Api\Exception if dn not listable or delete fails
913
	 */
914
	private function rdelete($dn)
915
	{
916
		if (!($sr = ldap_list($this->test_ldap->ds,$dn,'ObjectClass=*',array(''))) ||
917
			!($entries = ldap_get_entries($this->test_ldap->ds, $sr)))
918
		{
919
			throw new Api\Exception(lang('Error listing "dn=%1"!',$dn));
920
		}
921
		$deleted = 0;
922
		foreach($entries as $n => $entry)
923
		{
924
			if ($n === 'count') continue;
925
			$deleted += $this->rdelete($entry['dn']);
926
		}
927
		if (!ldap_delete($this->test_ldap->ds,$dn))
928
		{
929
			throw new Api\Exception(lang('Error deleting "dn=%1"!',$dn));
930
		}
931
		return ++$deleted;
932
	}
933
934
	/**
935
	 * Set mailbox attribute in $this->ldap_base according to given format
936
	 *
937
	 * Uses $this->ldap_host, $this->ldap_admin and $this->ldap_admin_pw to connect.
938
	 *
939
	 * @param string $this->object_class ='qmailUser'
940
	 * @param string $this->mbox_attr ='mailmessagestore' lowercase!!!
941
	 * @param string $this->mail_login_type ='email' 'email', 'vmailmgr', 'standard' or 'uidNumber'
942
	 * @return string with success message N entries modified
943
	 * @throws Api\Exception if dn not found, not listable or delete fails
944
	 */
945
	private function set_mailbox($check_only=false)
946
	{
947
		$this->connect($this->ldap_admin,$this->ldap_admin_pw);
948
949
		// if base not set, use context minus one hierarchy, eg. ou=accounts,(o=domain,dc=local)
950 View Code Duplication
		if (empty($this->ldap_base) && $this->ldap_context)
951
		{
952
			list(,$this->ldap_base) = explode(',',$this->ldap_context,2);
953
		}
954
		// check if base does exist
955 View Code Duplication
		if (!@ldap_read($this->test_ldap->ds,$this->ldap_base,'objectClass=*'))
956
		{
957
			throw new Api\Exception\WrongUserinput(lang('Base dn "%1" NOT found!',$this->ldap_base));
958
		}
959
		$object_class = $this->object_class ? $this->object_class : 'qmailUser';
960
		$mbox_attr = $this->mbox_attr ? $this->mbox_attr : 'mailmessagestore';
961
		$mail_login_type = $this->mail_login_type ? $this->mail_login_type : 'email';
962
963
		if (!($sr = ldap_search($this->test_ldap->ds,$this->ldap_base,
964
				'objectClass='.$object_class,array('mail','uidNumber','uid',$mbox_attr))) ||
965
			!($entries = ldap_get_entries($this->test_ldap->ds, $sr)))
966
		{
967
			throw new Api\Exception(lang('Error listing "dn=%1"!',$this->ldap_base));
968
		}
969
		$modified = 0;
970
		foreach($entries as $n => $entry)
971
		{
972
			if ($n === 'count') continue;
973
974
			$mbox = Api\Mail\Smtp\Ldap::mailbox_addr(array(
975
				'account_id' => $entry['uidnumber'][0],
976
				'account_lid' => $entry['uid'][0],
977
				'account_email' => $entry['mail'][0],
978
			),$this->domain,$mail_login_type);
979
980
			if ($mbox === $entry[$mbox_attr][0]) continue;	// nothing to change
981
982
			if (!$check_only && !ldap_modify($this->test_ldap->ds,$entry['dn'],array(
983
				$mbox_attr => $mbox,
984
			)))
985
			{
986
				throw new Api\Exception(lang("Error modifying dn=%1: %2='%3'!",$entry['dn'],$mbox_attr,$mbox));
987
			}
988
			++$modified;
989
			if ($check_only) echo "$modified: $entry[dn]: $mbox_attr={$entry[$mbox_attr][0]} --> $mbox\n";
990
		}
991
		return $check_only ? lang('%1 entries would have been modified.',$modified) :
992
			lang('%1 entries modified.',$modified);
993
	}
994
995
	/**
996
	 * array with objectclasses for the objects we can create
997
	 *
998
	 * @var array of name => objectClass pairs (or array with multiple)
999
	 */
1000
	static $requiredObjectclasses = array(
1001
		'o' => 'organization',
1002
		'ou' => 'organizationalUnit',
1003
		'cn' => array('organizationalRole','simpleSecurityObject'),
1004
		'uid' => array('uidObject','organizationalRole','simpleSecurityObject'),
1005
		'dc' => array('organization','dcObject'),
1006
	);
1007
1008
	/**
1009
	 * Create a new node in the ldap tree
1010
	 *
1011
	 * @param string $dn dn to create, eg. "cn=admin,dc=local"
1012
	 * @param array $extra =array() extra attributes to set
1013
	 * @return boolean true if the node was create, false if it was already there
1014
	 * @throws Api\Exception\WrongUserinput
1015
	 */
1016
	private function _create_node($dn,$extra=array())
1017
	{
1018
		// check if the node already exists and return if it does
1019
		if (@ldap_read($this->test_ldap->ds,$dn,'objectClass=*'))
1020
		{
1021
			return false;
1022
		}
1023
		list($node,$base) = explode(',',$dn,2);
1024
1025
		if (!@ldap_read($this->test_ldap->ds,$base,'objectClass=*'))
1026
		{
1027
			$this->_create_node($base);		// create the base if it's not already there
1028
		}
1029
		// now we need to create the node itself
1030
		list($name,$value) = explode('=',$node);
1031
1032
		if (!isset(self::$requiredObjectclasses[$name]))
1033
		{
1034
			throw new Api\Exception\WrongUserinput(lang('Can not create DN %1!',$dn).' '.
1035
				lang('Supported node types:').implode(', ',array_keys(self::$requiredObjectclasses)));
1036
		}
1037
		if ($name == 'dc') $extra['o'] = $value;	// required by organisation
1038
		if ($name == 'uid') $extra['cn'] = $value;	// required by organizationalRole
1039
1040
		if (!@ldap_add($this->test_ldap->ds,$dn,$attr = array(
1041
			$name => $value,
1042
			'objectClass' => self::$requiredObjectclasses[$name],
1043
		)+$extra))
1044
		{
1045
			throw new Api\Exception\WrongUserinput(lang('Can not create DN %1!',$dn).
1046
				' ('.ldap_error($this->test_ldap->ds).', attributes='.print_r($attr,true).')');
1047
		}
1048
		return true;
1049
	}
1050
1051
	/**
1052
	 * Return default database settings for a given domain
1053
	 *
1054
	 * @return array
1055
	 */
1056
	static function defaults()
1057
	{
1058
		return array(
1059
			'ldap_host'     => 'localhost',
1060
			'ldap_suffix'   => 'dc=local',
1061
			'ldap_admin'    => 'cn=admin,$suffix',
1062
			'ldap_admin_pw' => '',
1063
			'ldap_base'     => 'o=$domain,$suffix',
1064
			'ldap_root_dn'  => 'cn=admin,$base',
1065
			'ldap_root_pw'  => self::randomstring(),
1066
			'ldap_context'  => 'ou=accounts,$base',
1067
			'ldap_search_filter' => '(uid=%user)',
1068
			'ldap_group_context' => 'ou=groups,$base',
1069
		);
1070
	}
1071
1072
	/**
1073
	 * Merges the default into the current properties, if they are empty or contain placeholders
1074
	 */
1075
	private function _merge_defaults()
1076
	{
1077
		foreach(self::defaults() as $name => $default)
1078
		{
1079
			if ($this->sub_command == 'delete_ldap' && in_array($name,array('ldap_base','ldap_context')))
1080
			{
1081
				continue;	// no default on what to delete!
1082
			}
1083
			if (!$this->$name)
1084
			{
1085
				//echo "<p>setting $name='{$this->$name}' to it's default='$default'</p>\n";
1086
				$this->set_defaults[$name] = $this->$name = $default;
1087
			}
1088
			if (strpos($this->$name,'$') !== false)
1089
			{
1090
				$this->set_defaults[$name] = $this->$name = str_replace(array(
1091
					'$domain',
1092
					'$suffix',
1093
					'$base',
1094
					'$admin_pw',
1095
				),array(
1096
					$this->domain,
1097
					$this->ldap_suffix,
1098
					$this->ldap_base,
1099
					$this->ldap_admin_pw,
1100
				),$this->$name);
1101
			}
1102
		}
1103
	}
1104
}
1105