1<?php
2use \LAM\TYPES\TypeManager;
3use function LAM\TYPES\getScopeFromTypeId;
4use LAM\TYPES\ConfiguredType;
5/*
6
7This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
8Copyright (C) 2003 - 2006  Tilo Lutz
9              2007 - 2020  Roland Gruber
10
11This program is free software; you can redistribute it and/or modify
12it under the terms of the GNU General Public License as published by
13the Free Software Foundation; either version 2 of the License, or
14(at your option) any later version.
15
16This program is distributed in the hope that it will be useful,
17but WITHOUT ANY WARRANTY; without even the implied warranty of
18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19GNU General Public License for more details.
20
21You should have received a copy of the GNU General Public License
22along with this program; if not, write to the Free Software
23Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
24*/
25
26
27/**
28* Manages Unix accounts for groups.
29*
30* @package modules
31*
32* @author Tilo Lutz
33* @author Roland Gruber
34* @author Michael Duergner
35*/
36
37/**
38* Manages the object class "posixGroup" for groups.
39*
40* @package modules
41*/
42class posixGroup extends baseModule implements passwordService {
43
44	/** change GIDs of users and hosts? */
45	private $changegids;
46	/** password attribute */
47	protected $passwordAttrName = 'userPassword';
48	/** cache for existing GID numbers */
49	private $cachedGIDList = null;
50	/** cache for existing users and their GIDs */
51	private $cachedUserToGIDList = null;
52	/** cache for existing groups */
53	private $cachedGroupNameList = null;
54
55	/**
56	 * {@inheritDoc}
57	 * @see baseModule::getManagedAttributes()
58	 */
59	function get_uploadColumns($selectedModules, &$type) {
60		$return = parent::get_uploadColumns($selectedModules, $type);
61		$typeId = $type->getId();
62		if ($this->manageCnAndDescription($selectedModules)) {
63			array_unshift($return,
64				array(
65					'name' => 'posixGroup_cn',
66					'description' => _('Group name'),
67					'help' => 'cn',
68					'example' => _('adminstrators'),
69					'required' => true,
70					'unique' => true
71				)
72			);
73			array_unshift($return,
74				array(
75				'name' => 'posixGroup_description',
76				'description' => _('Group description'),
77				'help' => 'description',
78				'example' => _('Administrators group')
79				)
80			);
81		}
82		if (!$this->isBooleanConfigOptionSet('posixGroup_' . $typeId . '_hidememberUid')) {
83			$return[] = array(
84				'name' => 'posixGroup_members',
85				'description' => _('Group members'),
86				'help' => 'upload_members',
87				'example' => _('user01,user02,user03')
88			);
89		}
90		return $return;
91	}
92
93	/**
94	 * {@inheritDoc}
95	 * @see baseModule::build_uploadAccounts()
96	 */
97	function build_uploadAccounts($rawAccounts, $ids, &$partialAccounts, $selectedModules, &$type) {
98		$error_messages = array();
99		$needAutoGID = array();
100		$typeId = $type->getId();
101		for ($i = 0; $i < sizeof($rawAccounts); $i++) {
102			if (!in_array("posixGroup", $partialAccounts[$i]['objectClass'])) $partialAccounts[$i]['objectClass'][] = "posixGroup";
103			if ($this->manageCnAndDescription($selectedModules)) {
104				// group name
105				$this->mapSimpleUploadField($rawAccounts, $ids, $partialAccounts, $i, 'posixGroup_cn', 'cn', 'groupname', $this->messages['cn'][3], $error_messages);
106			}
107			// GID
108			if ($rawAccounts[$i][$ids['posixGroup_gid']] == "") {
109				// autoGID
110				$needAutoGID[] = $i;
111			}
112			elseif (get_preg($rawAccounts[$i][$ids['posixGroup_gid']], 'digit')) {
113				$partialAccounts[$i]['gidNumber'] = $rawAccounts[$i][$ids['posixGroup_gid']];
114			}
115			else {
116				$errMsg = $this->messages['gidNumber'][8];
117				array_push($errMsg, array($i));
118				$error_messages[] = $errMsg;
119			}
120			if ($this->manageCnAndDescription($selectedModules)) {
121				// description (UTF-8, no regex check needed)
122				if ($rawAccounts[$i][$ids['posixGroup_description']] == "") {
123					$partialAccounts[$i]['description'] = $partialAccounts[$i]['cn'];
124				}
125				else {
126					$partialAccounts[$i]['description'] = $rawAccounts[$i][$ids['posixGroup_description']];
127				}
128			}
129			// group members
130			if (!$this->isBooleanConfigOptionSet('posixGroup_' . $typeId . '_hidememberUid') && ($rawAccounts[$i][$ids['posixGroup_members']] != "")) {
131				if (get_preg($rawAccounts[$i][$ids['posixGroup_members']], 'usernameList')) {
132					$partialAccounts[$i]['memberUid'] = explode(",", $rawAccounts[$i][$ids['posixGroup_members']]);
133				}
134				else {
135					$errMsg = $this->messages['memberUID'][0];
136					array_push($errMsg, $i);
137					$error_messages[] =$errMsg;
138				}
139			}
140			// password
141			if ($rawAccounts[$i][$ids['posixGroup_password']] != "") {
142				if (get_preg($rawAccounts[$i][$ids['posixGroup_password']], 'password')) {
143					$partialAccounts[$i][$this->passwordAttrName] = pwd_hash($rawAccounts[$i][$ids['posixGroup_password']], true, $this->moduleSettings['posixAccount_pwdHash'][0]);
144				}
145				else {
146					$error_messages[] = $this->messages['userPassword'][1];
147				}
148			}
149		}
150		// fill in autoGIDs
151		if (sizeof($needAutoGID) > 0) {
152			$errorsTemp = array();
153			$gids = $this->getNextGIDs(sizeof($needAutoGID), $errorsTemp, $type);
154			if (is_array($gids)) {
155				for ($i = 0; $i < sizeof($needAutoGID); $i++) {
156					$partialAccounts[$i]['gidNumber'] = $gids[$i];
157				}
158			}
159			else {
160				$error_messages[] = $this->messages['gidNumber'][2];
161			}
162		}
163		return $error_messages;
164	}
165
166	/**
167	* Checks if the group which should be deleted is still used as primary group.
168	*
169	* @return List of LDAP operations, same as for save_attributes()
170	*/
171	function delete_attributes() {
172		$return = array();
173		$result = searchLDAPByFilter('(&(objectClass=posixAccount)(gidNumber=' . $this->attributes['gidNumber'][0] . '))', array('dn'), array('user', 'host'));
174		if (sizeof($result) > 0) {
175			$max = 5;
176			if (sizeof($result) < 5) {
177				$max = sizeof($result);
178			}
179			$users = array();
180			for ($i = 0; $i < $max; $i++) {
181				$users[] = getAbstractDN($result[$i]['dn']);
182			}
183			$message = $this->messages['primaryGroup'][0];
184			$message[] = implode(', ', $users);
185			$return[$this->getAccountContainer()->dn_orig]['errors'][] = $message;
186		}
187		return $return;
188	}
189
190	/**
191	 * Returns the HTML meta data for the main account page.
192	 *
193	 * @return array HTML meta data
194	 *
195	 * @see baseModule::get_metaData()
196	 */
197	function display_html_attributes() {
198		$return = new htmlResponsiveRow();
199		$modules = $this->getAccountContainer()->get_type()->getModules();
200		$typeId = $this->getAccountContainer()->get_type()->getId();
201		if ($this->autoAddObjectClasses || (isset($this->attributes['objectClass']) && in_array('posixGroup', $this->attributes['objectClass']))) {
202			// auto sync group members
203			if ($this->isBooleanConfigOptionSet('posixGroup_autoSyncGon')) {
204				$this->syncGon(true);
205			}
206			// group name
207			if ($this->manageCnAndDescription($modules)) {
208				$this->addSimpleInputTextField($return, 'cn', _("Group name"), true);
209			}
210			// GID number
211			$gidNumberInput = $this->addSimpleInputTextField($return, 'gidNumber', _('GID number'));
212			$gidNumberInput->setValidationRule(htmlElement::VALIDATE_NUMERIC);
213			// description
214			if ($this->manageCnAndDescription($modules)) {
215				$this->addSimpleInputTextField($return, 'description', _('Description'));
216			}
217			// password buttons
218			if (checkIfWriteAccessIsAllowed($this->get_scope()) && isset($this->attributes[$this->passwordAttrName][0])) {
219				$return->addLabel(new htmlOutputText(_('Password')));
220				$pwdContainer = new htmlGroup();
221				if (pwd_is_enabled($this->attributes[$this->passwordAttrName][0])) {
222					$pwdContainer->addElement(new htmlButton('lockPassword', _('Lock password')));
223				}
224				else {
225					$pwdContainer->addElement(new htmlButton('unlockPassword', _('Unlock password')));
226				}
227				$pwdContainer->addElement(new htmlSpacer('0.5rem', null));
228				$pwdContainer->addElement(new htmlButton('removePassword', _('Remove password')));
229				$return->addField($pwdContainer);
230			}
231			if (isset($this->orig['gidNumber'][0]) && $this->attributes['gidNumber'][0]!=$this->orig['gidNumber'][0]) {
232				$return->add(new htmlResponsiveInputCheckbox('changegids', $this->changegids, _('Change GID number of users and hosts'), 'changegids'), 12);
233			}
234			// group members
235			if (!$this->isBooleanConfigOptionSet('posixGroup_' . $typeId . '_hidememberUid')) {
236				$return->addVerticalSpacer('0.5rem');
237				$return->addLabel(new htmlOutputText(_("Group members")));
238				$membersGroup = new htmlGroup();
239				if (!$this->isBooleanConfigOptionSet('posixGroup_autoSyncGon')) {
240					$membersGroup->addElement(new htmlAccountPageButton(get_class($this), 'user', 'open', _('Edit members')));
241				}
242				$membersGroup->addElement(new htmlHelpLink('members'));
243				$return->addField($membersGroup);
244				$return->addLabel(new htmlOutputText('&nbsp;', false));
245				$users = $this->getUsers();
246				$members = array();
247				if (isset($this->attributes['memberUid'][0])) {
248					foreach ($this->attributes['memberUid'] as $uid) {
249						if (isset($users[$uid]) && isset($users[$uid]['cn'])) {
250							$members[] = $uid . ' (' . $users[$uid]['cn'] . ')';
251						}
252						else {
253							$members[] = $uid;
254						}
255					}
256				}
257				$members = array_unique($members);
258				natcasesort($members);
259				$members = array_map('htmlspecialchars', $members);
260				$return->addField(new htmlOutputText(implode('<br>', $members), false));
261			}
262			// remove button
263			if (!$this->autoAddObjectClasses) {
264				$return->addVerticalSpacer('2rem');
265				$remButton = new htmlButton('remObjectClass', _('Remove Unix extension'));
266				$return->add($remButton, 12, 12, 12, 'text-center');
267			}
268		}
269		else {
270			// add button
271			$return->add(new htmlButton('addObjectClass', _('Add Unix extension')), 12);
272		}
273		return $return;
274	}
275
276
277	/**
278	* Displays selections to add or remove users from current group.
279	*
280	* @return array meta HTML output
281	*/
282	function display_html_user() {
283		$return = new htmlResponsiveRow();
284		if (!isset($this->attributes['memberUid'])) {
285			$this->attributes['memberUid'] = array();
286		}
287		// load list with all users
288		$userAndGIDs = $this->getUsers();
289		$users = array();
290		foreach ($userAndGIDs as $user => $userAttrs) {
291			if (!in_array($user, $this->attributes['memberUid'])) {
292				$display = $user . ' (' . $userAttrs['cn'] . ')';
293				if ($this->attributes['gidNumber'][0] == $userAttrs['gid']) {
294					if (isset($this->moduleSettings['posixAccount_primaryGroupAsSecondary'][0])
295						&& ($this->moduleSettings['posixAccount_primaryGroupAsSecondary'][0] == 'true')) {
296							$users[$display] = $user;
297					}
298				}
299				else {
300					$users[$display] = $user;
301				}
302			}
303		}
304		natcasesort($users);
305		$return->add(new htmlSubTitle(_("Group members")), 12);
306
307		$remUsers = array();
308		if (isset($this->attributes['memberUid'])) {
309			$remUsers = $this->attributes['memberUid'];
310		}
311		natcasesort($remUsers);
312		$remUsersDescriptive = array();
313		foreach ($remUsers as $user) {
314			if (isset($userAndGIDs[$user])) {
315				$remUsersDescriptive[$user . ' (' . $userAndGIDs[$user]['cn'] . ')'] = $user;
316			}
317			else {
318				$remUsersDescriptive[$user] = $user;
319			}
320		}
321
322		$this->addDoubleSelectionArea($return, _("Selected users"), _("Available users"), $remUsersDescriptive, null,
323				$users, null, 'members', false, true);
324
325		// sync from group of names
326		$gon = $this->getAccountContainer()->getAccountModule('groupOfNames');
327		if ($gon == null) {
328			$gon = $this->getAccountContainer()->getAccountModule('groupOfUniqueNames');
329		}
330                if ($gon == null) {
331                        $gon = $this->getAccountContainer()->getAccountModule('groupOfMembers');
332                }
333		if ($gon != null) {
334			$return->addVerticalSpacer('2rem');
335			$syncButton = new htmlButton('syncGON', sprintf(_('Sync from %s'), $gon->get_alias()));
336			$syncButton->setIconClass('refreshButton');
337			$return->add($syncButton, 12, 12, 12, 'text-center');
338			$return->addVerticalSpacer('1rem');
339			$return->add(new htmlResponsiveInputCheckbox('syncGON_delete', true, _('Delete non-matching entries'), null, false), 12);
340		}
341		$windows = $this->getAccountContainer()->getAccountModule('windowsGroup');
342		if ($windows != null) {
343			$return->addVerticalSpacer('2rem');
344			$syncButton = new htmlButton('syncWindows', sprintf(_('Sync from %s'), $windows->get_alias()));
345			$syncButton->setIconClass('refreshButton');
346			$return->add($syncButton, 12, 12, 12, 'text-center');
347			$return->addVerticalSpacer('1rem');
348			$return->add(new htmlResponsiveInputCheckbox('syncWindows_delete', true, _('Delete non-matching entries'), null, false), 12);
349		}
350
351		// back button
352		$return->addVerticalSpacer('2rem');
353		$return->add(new htmlAccountPageButton(get_class($this), 'attributes', 'back', _('Back')), 12);
354
355		return $return;
356	}
357
358	/**
359	* Returns true if this module can manage accounts of the current type, otherwise false.
360	*
361	* @return boolean true if module fits
362	*/
363	public function can_manage() {
364		return in_array($this->get_scope(), array('group'));
365	}
366
367	/**
368	* Returns meta data that is interpreted by parent class
369	*
370	* @return array array with meta data
371	*/
372	function get_metaData() {
373		$return = array();
374		// icon
375		$return['icon'] = 'tux.png';
376		if ($this->get_scope() == "group") {
377			// this is a base module
378			$return["is_base"] = true;
379			// LDAP filter
380			$return["ldap_filter"] = array('or' => "(objectClass=posixGroup)");
381		}
382		// alias name
383		$return["alias"] = _('Unix');
384		// RDN attribute
385		$return["RDN"] = array("cn" => "normal");
386		// module dependencies
387		$return['dependencies'] = array('depends' => array(), 'conflicts' => array());
388		// managed object classes
389		$return['objectClasses'] = array('posixGroup');
390		// LDAP aliases
391		$return['LDAPaliases'] = array('commonName' => 'cn');
392		// managed attributes
393		$return['attributes'] = array('gidNumber', $this->passwordAttrName, 'memberUid');
394		// profile options
395		if (!$this->autoAddObjectClasses) {
396			$profileContainer = new htmlResponsiveRow();
397			$profileContainer->add(new htmlResponsiveInputCheckbox('posixGroup_addExt', false, _('Automatically add this extension'), 'autoAdd'), 12);
398			$return['profile_options'] = $profileContainer;
399		}
400		// available PDF fields
401		$return['PDF_fields'] = array(
402			'gidNumber' => _('GID number'),
403		);
404		// upload fields
405		$return['upload_columns'] = array(
406		array(
407			'name' => 'posixGroup_gid',
408			'description' => _('GID number'),
409			'help' => 'gidNumber',
410			'example' => '2034'
411		),
412		array(
413			'name' => 'posixGroup_password',
414			'description' => _('Group password'),
415			'help' => 'password',
416			'example' => _('secret')
417		)
418		);
419		// help Entries
420		$return['help'] = array(
421			'gidNumber'	=> array(
422				"Headline" => _("GID number"), 'attr' => 'gidNumber',
423				"Text" => _("If empty GID number will be generated automatically depending on your configuration settings.")
424			),
425			'description' => array(
426				"Headline" => _("Description"), 'attr' => 'description',
427				"Text" => _("Group description. If left empty group name will be used.")
428			),
429			'members' => array(
430				"Headline" => _("Group members"), 'attr' => 'memberUid',
431				"Text" => _("Users who are member of the current group. Users who have set their primary group to this group will not be shown.")
432			),
433			'upload_members' => array(
434				"Headline" => _("Group members"), 'attr' => 'memberUid',
435				"Text" => _("Users who will become member of the current group. User names are separated by semicolons.")
436			),
437			'password' => array(
438				"Headline" => _("Group password"), 'attr' => $this->passwordAttrName,
439				"Text" => _("Sets the group password.")
440			),
441			'minMaxGID' => array(
442				"Headline" => _("GID number"),
443				"Text" => _("These are the minimum and maximum numbers to use for group IDs when creating new group accounts. New group accounts will always get the highest number in use plus one.")
444			),
445			'pwdHash' => array(
446				"Headline" => _("Password hash type"),
447				"Text" => _("LAM supports a large number of possibilities to generate the hash value of passwords. CRYPT-SHA512 and SSHA are the most common. We do not recommend to use plain text passwords unless passwords are hashed server-side.")
448			),
449			'cn' => array(
450				"Headline" => _("Group name"), 'attr' => 'cn',
451				"Text" => _("Group name of the group which should be created. Valid characters are: a-z, A-Z, 0-9 and .-_ . If group name is already used group name will be expanded with a number. The next free number will be used.")
452			),
453			'changegids' => array(
454				"Headline" => _("Change GID number of users and hosts"),
455				"Text" => _("The ID of this group was changed. You can update all user and host entries to the new group ID.")
456			),
457			'gidCheckSuffix' => array (
458				"Headline" => _("Suffix for GID/group name check"),
459				"Text" => _("LAM checks if the entered group name and GID are unique. Here you can enter the LDAP suffix that is used to search for duplicates. By default the account type suffix is used. You only need to change this if you use multiple server profiles with different OUs but need unique group names or GIDs.")
460			),
461			'gidGenerator' => array (
462				"Headline" => _("GID generator"),
463				"Text" => _("LAM will automatically suggest UID/GID numbers. You can either use a fixed range of numbers or an LDAP entry with object class \"sambaUnixIdPool\" or \"msSFU30DomainInfo\".")
464					. ' ' . _('Magic number will set a fixed value that must match your server configuration.')
465			),
466			'sambaIDPoolDN' => array (
467				"Headline" => _("Samba ID pool DN"),
468				"Text" => _("Please enter the DN of the LDAP entry with object class \"sambaUnixIdPool\".")
469			),
470			'windowsIDPoolDN' => array (
471				"Headline" => _("Windows domain info DN"),
472				"Text" => _("Please enter the DN of the LDAP entry with object class \"msSFU30DomainInfo\".")
473			),
474			'filter' => array(
475				"Headline" => _("Filter"),
476				"Text" => _("Here you can enter a filter value. Only entries which contain the filter text will be shown.")
477					. ' ' . _('The filter can be any regular expression, e.g. ".*" = any characters, "^" = line start, "$" = line end.')
478			),
479			'hidememberUid' => array(
480				"Headline" => _('Disable membership management'), 'attr' => 'memberUid',
481				"Text" => _('Disables the group membership management.')
482			),
483			'autoAdd' => array(
484				"Headline" => _("Automatically add this extension"),
485				"Text" => _("This will enable the extension automatically if this profile is loaded.")
486			),
487			'autoSyncGon' => array(
488				"Headline" => _("Force sync with group of names"),
489				"Text" => _("This will force syncing with group of names members of the same group.")
490			),
491			'magicNumber' => array(
492				"Headline" => _("Magic number"),
493				"Text" => _("Please enter the magic number you configured on server side.")
494			),
495		);
496
497		return $return;
498	}
499
500
501	/**
502	 * {@inheritDoc}
503	 * @see baseModule::get_configOptions()
504	 */
505	public function get_configOptions($scopes, $allScopes) {
506		$typeManager = new TypeManager($_SESSION['conf_config']);
507		// configuration options
508		$configContainer = new htmlResponsiveRow();
509		$configContainer->add(new htmlSubTitle(_("Groups")), 12);
510		foreach ($allScopes[get_class($this)] as $typeId) {
511			if (sizeof($allScopes[get_class($this)]) > 1) {
512				$title = new htmlDiv(null, new htmlOutputText($typeManager->getConfiguredType($typeId)->getAlias()));
513				$title->setCSSClasses(array('bold', 'responsiveLabel'));
514				$configContainer->add($title, 12, 6);
515				$configContainer->add(new htmlOutputText('&nbsp;', false), 0, 6);
516			}
517			$this->addAccountSpecificConfigOptions($configContainer, $typeId);
518			$configContainer->addVerticalSpacer('2rem');
519		}
520		$gonModules = array('groupOfNames', 'groupOfUniqueNames', 'groupOfMembers');
521		$gonFound = false;
522		foreach ($gonModules as $gonModule) {
523			if (!empty($allScopes[$gonModule])) {
524				foreach ($allScopes[$gonModule] as $gonTypeId) {
525					if (getScopeFromTypeId($gonTypeId) === 'group') {
526						$gonFound = true;
527					}
528				}
529			}
530		}
531		if ($gonFound || !isset($allScopes['posixAccount'])) {
532			$configContainer->add(new htmlSubTitle(_("Options")), 12);
533		}
534		if ($gonFound) {
535			$configContainer->add(new htmlResponsiveInputCheckbox('posixGroup_autoSyncGon', false, _('Force sync with group of names'), 'autoSyncGon'), 12);
536		}
537		// display password hash option only if posixAccount module is not used
538		if (!isset($allScopes['posixAccount'])) {
539			$configContainer->add(new htmlResponsiveSelect('posixAccount_pwdHash', getSupportedHashTypes(), array('CRYPT-SHA512'), _("Password hash type"), 'pwdHash'), 12);
540		}
541		return $configContainer;
542	}
543
544	/**
545	 * Adds the account specific options to the config container.
546	 *
547	 * @param htmlResponsiveRow $configContainer container
548	 * @param string $typeId type ID
549	 */
550	protected function addAccountSpecificConfigOptions(&$configContainer, $typeId) {
551		$genOptions = array(
552			_('Fixed range') => 'range',
553			_('Samba ID pool') => 'sambaPool',
554			_('Windows domain info') => 'windowsDomain',
555			_('Magic number') => 'magicNumber'
556		);
557		$gidGeneratorSelect = new htmlResponsiveSelect('posixGroup_' . $typeId . '_gidGenerator', $genOptions, array('range'), _('GID generator'), 'gidGenerator');
558		$gidGeneratorSelect->setHasDescriptiveElements(true);
559		$gidGeneratorSelect->setTableRowsToHide(array(
560			'range' => array('posixGroup_' . $typeId . '_sambaIDPoolDN', 'posixGroup_' . $typeId . '_windowsIDPoolDN', 'posixGroup_' . $typeId . '_magicNumber'),
561			'sambaPool' => array('posixGroup_' . $typeId . '_minGID', 'posixGroup_' . $typeId . '_maxGID', 'posixGroup_' . $typeId . '_windowsIDPoolDN', 'posixGroup_' . $typeId . '_magicNumber'),
562			'windowsDomain' => array('posixGroup_' . $typeId . '_minGID', 'posixGroup_' . $typeId . '_maxGID', 'posixGroup_' . $typeId . '_sambaIDPoolDN', 'posixGroup_' . $typeId . '_magicNumber'),
563			'magicNumber' => array('posixGroup_' . $typeId . '_minGID', 'posixGroup_' . $typeId . '_maxGID', 'posixGroup_' . $typeId . '_windowsIDPoolDN', 'posixGroup_' . $typeId . '_sambaIDPoolDN')
564		));
565		$gidGeneratorSelect->setTableRowsToShow(array(
566			'range' => array('posixGroup_' . $typeId . '_minGID', 'posixGroup_' . $typeId . '_maxGID'),
567			'sambaPool' => array('posixGroup_' . $typeId . '_sambaIDPoolDN'),
568			'windowsDomain' => array('posixGroup_' . $typeId . '_windowsIDPoolDN'),
569			'magicNumber' => array('posixGroup_' . $typeId . '_magicNumber')
570		));
571		$configContainer->add($gidGeneratorSelect, 12);
572		$minGidInput = new htmlResponsiveInputField(_('Minimum GID number'), 'posixGroup_' . $typeId . '_minGID', null, 'minMaxGID');
573		$minGidInput->setRequired(true);
574		$configContainer->add($minGidInput, 12);
575		$maxGidInput = new htmlResponsiveInputField(_('Maximum GID number'), 'posixGroup_' . $typeId . '_maxGID', null, 'minMaxGID');
576		$maxGidInput->setRequired(true);
577		$configContainer->add($maxGidInput, 12);
578		$gidGeneratorDN = new htmlResponsiveInputField(_('Samba ID pool DN'), 'posixGroup_' . $typeId . '_sambaIDPoolDN', null, 'sambaIDPoolDN');
579		$gidGeneratorDN->setRequired(true);
580		$configContainer->add($gidGeneratorDN, 12);
581		$winGeneratorDN = new htmlResponsiveInputField(_('Windows domain info DN'), 'posixGroup_' . $typeId . '_windowsIDPoolDN', null, 'windowsIDPoolDN');
582		$winGeneratorDN->setRequired(true);
583		$configContainer->add($winGeneratorDN, 12);
584		$magicNumber = new htmlResponsiveInputField(_('Magic number'), 'posixGroup_' . $typeId . '_magicNumber', null, 'magicNumber');
585		$magicNumber->setRequired(true);
586		$configContainer->add($magicNumber, 12);
587		$configContainer->add(new htmlResponsiveInputField(_('Suffix for GID/group name check'), 'posixGroup_' . $typeId . '_gidCheckSuffix', '', 'gidCheckSuffix'), 12);
588		$configContainer->add(new htmlResponsiveInputCheckbox('posixGroup_' . $typeId . '_hidememberUid', false, _('Disable membership management'), 'hidememberUid'), 12);
589	}
590
591	/**
592	 * {@inheritDoc}
593	 * @see baseModule::check_configOptions()
594	 */
595	public function check_configOptions($typeIds, &$options) {
596		foreach ($typeIds as $typeId) {
597			if ($options['posixGroup_' . $typeId . '_gidGenerator'][0] == 'range') {
598				$this->meta['config_checks']['group']['posixGroup_' . $typeId . '_minGID'] = array (
599					'type' => 'ext_preg',
600					'regex' => 'digit',
601					'required' => true,
602					'required_message' => $this->messages['gidNumber'][5],
603					'error_message' => $this->messages['gidNumber'][5]);
604				$this->meta['config_checks']['group']['posixGroup_' . $typeId . '_maxGID'] = array (
605					'type' => 'ext_preg',
606					'regex' => 'digit',
607					'required' => true,
608					'required_message' => $this->messages['gidNumber'][6],
609					'error_message' => $this->messages['gidNumber'][6]);
610				$this->meta['config_checks']['group']['cmpGID'] = array (
611					'type' => 'int_greater',
612					'cmp_name1' => 'posixGroup_' . $typeId . '_maxGID',
613					'cmp_name2' => 'posixGroup_' . $typeId . '_minGID',
614					'error_message' => $this->messages['gidNumber'][7]);
615			}
616			elseif ($options['posixGroup_' . $typeId . '_gidGenerator'][0] == 'sambaPool') {
617				$this->meta['config_checks']['group']['posixGroup_' . $typeId . '_sambaIDPoolDN'] = array (
618					'type' => 'ext_preg',
619					'regex' => 'dn',
620					'required' => true,
621					'required_message' => $this->messages['sambaIDPoolDN'][0],
622					'error_message' => $this->messages['sambaIDPoolDN'][0]);
623			}
624			elseif ($options['posixGroup_' . $typeId . '_gidGenerator'][0] == 'windowsDomain') {
625				$this->meta['config_checks']['group']['posixGroup_' . $typeId . '_windowsIDPoolDN'] = array (
626					'type' => 'ext_preg',
627					'regex' => 'dn',
628					'required' => true,
629					'required_message' => $this->messages['windowsIDPoolDN'][0],
630					'error_message' => $this->messages['windowsIDPoolDN'][0]);
631			}
632			elseif ($options['posixGroup_' . $typeId . '_gidGenerator'][0] == 'magicNumber') {
633				$this->meta['config_checks']['group']['posixGroup_' . $typeId . '_magicNumber'] = array (
634					'type' => 'ext_preg',
635					'regex' => 'digit',
636					'required' => true,
637					'required_message' => $this->messages['magicNumber'][0],
638					'error_message' => $this->messages['magicNumber'][0]);
639			}
640		}
641		return parent::check_configOptions($typeIds, $options);
642	}
643
644	/**
645	 * {@inheritDoc}
646	 * @see baseModule::get_pdfFields()
647	 */
648	public function get_pdfFields($typeId) {
649		$fields = parent::get_pdfFields($typeId);
650		$typeManager = new TypeManager();
651		$modules = $typeManager->getConfiguredType($typeId)->getModules();
652		if ($this->manageCnAndDescription($modules)) {
653			$fields['cn'] = _('Group name');
654			$fields['description'] = _('Description');
655		}
656		if (!$this->isBooleanConfigOptionSet('posixGroup_' . $typeId . '_hidememberUid')) {
657			$fields['memberUid'] = _('Group members');
658			$fields['memberUidPrimary'] = _('Group members (incl. primary members)');
659		}
660		return $fields;
661	}
662
663	/**
664	 * {@inheritDoc}
665	 * @see baseModule::get_pdfEntries()
666	 */
667	function get_pdfEntries($pdfKeys, $typeId) {
668		$return = array();
669		$this->addSimplePDFField($return, 'memberUid', _('Group members'));
670		$this->addSimplePDFField($return, 'cn', _('Group name'));
671		$this->addSimplePDFField($return, 'gidNumber', _('GID number'));
672		$this->addSimplePDFField($return, 'description', _('Description'));
673		if (in_array(get_class($this) . '_memberUidPrimary', $pdfKeys)) {
674			$members = !empty($this->attributes['memberUid']) ? $this->attributes['memberUid'] : array();
675			if (!empty($this->attributes['gidNumber'])) {
676				$filter = "(&(&" . get_ldap_filter('user')  . ")(gidNumber=" . $this->attributes['gidNumber'][0] . "))";
677				$entries = searchLDAPByFilter($filter, array('uid'), array('user'));
678				foreach ($entries as $entry) {
679					$members[] = $entry['uid'][0];
680				}
681			}
682			$this->addPDFKeyValue($return, 'memberUidPrimary', _('Group members'), $members);
683		}
684		return $return;
685	}
686
687
688	/**
689	* This function will be called when the module will be loaded
690	*
691	* @param String $base the name of the {@link accountContainer} object ($_SESSION[$base])
692	*/
693	function init($base) {
694		// call parent init
695		parent::init($base);
696		$this->changegids=false;
697	}
698
699
700	/**
701	* This function fills the $messages variable with output messages from this module.
702	*/
703	function load_Messages() {
704		$this->messages['userPassword'][1] = array('ERROR', _('Password'), _('Password contains invalid characters. Valid characters are:') . ' a-z, A-Z, 0-9 and #*,.;:_-+!%&/|?{[()]}=@$ §°!');
705		$this->messages['gidNumber'][0] = array('INFO', _('GID number'), _('GID number has changed. Please select checkbox to change GID number of users and hosts.'));
706		$this->messages['gidNumber'][2] = array('WARN', _('ID-Number'), _('It is possible that this ID-number is reused. This can cause several problems because files with old permissions might still exist. To avoid this warning set maxUID to a higher value.'));
707		$this->messages['gidNumber'][3] = array('ERROR', _('ID-Number'), _('No free ID-Number!'));
708		$this->messages['gidNumber'][4] = array('ERROR', _('ID-Number'), _('ID is already in use'));
709		$this->messages['gidNumber'][5] = array('ERROR', _('Minimum GID number'), _('Minimum GID number is invalid or empty!'));
710		$this->messages['gidNumber'][6] = array('ERROR', _('Maximum GID number'), _('Maximum GID number is invalid or empty!'));
711		$this->messages['gidNumber'][7] = array('ERROR', _('Maximum GID number'), _('Maximum GID number must be greater than minimum GID number!'));
712		$this->messages['gidNumber'][8] = array('ERROR', _('Account %s:') . ' posixGroup_gid', _('GID number has to be a numeric value!'));
713		$this->messages['cn'][0] = array('WARN', _('Group name'), _('You are using capital letters. This can cause problems because Windows is not case-sensitive.'));
714		$this->messages['cn'][1] = array('WARN', _('Group name'), _('Group name in use. Selected next free group name.'));
715		$this->messages['cn'][2] = array('ERROR', _('Group name'), _('Group name contains invalid characters. Valid characters are: a-z, A-Z, 0-9 and .-_ !'));
716		$this->messages['cn'][3] = array('ERROR', _('Account %s:') . ' posixGroup_cn', _('Group name contains invalid characters. Valid characters are: a-z, A-Z, 0-9 and .-_ !'));
717		$this->messages['memberUID'][0] = array('ERROR', _('Account %s:') . ' posixGroup_members', _("This value must be a list of user names separated by semicolons."));
718		$this->messages['primaryGroup'][0] = array('ERROR', _('There are still users who have this group as their primary group.'));
719		$this->messages['sambaIDPoolDN'][0] = array('ERROR', _('Samba ID pool DN'), _('This is not a valid DN!'));
720		$this->messages['windowsIDPoolDN'][0] = array('ERROR', _('Windows domain info DN'), _('This is not a valid DN!'));
721		$this->messages['magicNumber'][0] = array('ERROR', _('Magic number'), _('Please enter a valid number.'));
722	}
723
724
725	/**
726	 * {@inheritDoc}
727	 * @see baseModule::getManagedAttributes()
728	 */
729	public function getManagedAttributes($typeId) {
730		$attrs = parent::getManagedAttributes($typeId);
731		$typeManager = new TypeManager();
732		$modules = $typeManager->getConfiguredType($typeId)->getModules();
733		if ($this->manageCnAndDescription($modules)) {
734			$attrs[] = 'cn';
735			$attrs[] = 'description';
736		}
737		return $attrs;
738	}
739
740	/**
741	* This functions is used to check if all settings for this module have been made.
742	*
743	* @return boolean true, if settings are complete
744	*/
745	function module_complete() {
746		if (!$this->getAccountContainer()->isNewAccount) {
747			// check if account is based on our object class
748			$objectClasses = $this->getAccountContainer()->attributes_orig['objectClass'];
749			if (is_array($objectClasses) && !in_array('posixGroup', $objectClasses)) {
750				return true;
751			}
752		}
753		$modules = $this->getAccountContainer()->get_type()->getModules();
754		if ($this->manageCnAndDescription($modules) && ($this->attributes['cn'][0] == '')) {
755			return false;
756		}
757		if ((!isset($this->attributes['gidNumber'][0])) || $this->attributes['gidNumber'][0] === '') {
758			return false;
759		}
760		return true;
761	}
762
763
764	/**
765	* Controls if the module button the account page is visible and activated.
766	*
767	* @return string status ("enabled", "disabled", "hidden")
768	*/
769	function getButtonStatus() {
770		if (!$this->getAccountContainer()->isNewAccount) {
771			// check if account is based on our object class
772			$objectClasses = $this->getAccountContainer()->attributes_orig['objectClass'];
773			if (is_array($objectClasses) && !in_array('posixGroup', $objectClasses)) {
774				return "disabled";
775			}
776		}
777		return "enabled";
778	}
779
780
781	/**
782	* Processes user input of the primary module page.
783	* It checks if all input values are correct and updates the associated LDAP attributes.
784	*
785	* @return array list of info/error messages
786	*/
787	function process_attributes() {
788		$errors = array();
789		if (isset($_POST['addObjectClass'])) {
790			if (!isset($this->attributes['objectClass'])) {
791				$this->attributes['objectClass'] = array();
792			}
793			if (!in_array('posixGroup', $this->attributes['objectClass'])) {
794				$this->attributes['objectClass'][] = 'posixGroup';
795			}
796			return $errors;
797		}
798		if (isset($_POST['remObjectClass'])) {
799			$this->attributes['objectClass'] = array_delete(array('posixGroup'), $this->attributes['objectClass']);
800			$attrs = $this->getManagedAttributes($this->getAccountContainer()->get_type()->getId());
801			foreach ($attrs as $name) {
802				if (isset($this->attributes[$name])) {
803					unset($this->attributes[$name]);
804				}
805			}
806			return $errors;
807		}
808		$modules = $this->getAccountContainer()->get_type()->getModules();
809		$typeId = $this->getAccountContainer()->get_type()->getId();
810		// skip processing if object class is not set
811		if (!$this->autoAddObjectClasses && (!isset($this->attributes['objectClass']) || !in_array('posixGroup', $this->attributes['objectClass']))) {
812			return $errors;
813		}
814		if ($this->manageCnAndDescription($modules)) {
815			$this->attributes['description'][0] = $_POST['description'];
816		}
817		if (isset($_POST['lockPassword'])) {
818			$this->attributes[$this->passwordAttrName][0] = pwd_disable($this->attributes[$this->passwordAttrName][0]);
819		}
820		if (isset($_POST['unlockPassword'])) {
821			$this->attributes[$this->passwordAttrName][0] = pwd_enable($this->attributes[$this->passwordAttrName][0]);
822		}
823		if (isset($_POST['removePassword'])) {
824			unset($this->attributes[$this->passwordAttrName]);
825		}
826		if (isset($_POST['changegids'])) $this->changegids=true;
827		else $this->changegids=false;
828		if (!isset($this->attributes['gidNumber'][0]) || ($this->attributes['gidNumber'][0] != $_POST['gidNumber'])) {
829			// Check if GID is valid. If none value was entered, the next usable value will be inserted
830			// load min and max GID number
831			$minID = intval($this->moduleSettings['posixGroup_' . $typeId . '_minGID'][0]);
832			$maxID = intval($this->moduleSettings['posixGroup_' . $typeId . '_maxGID'][0]);
833			$this->attributes['gidNumber'][0] = $_POST['gidNumber'];
834			if ($this->attributes['gidNumber'][0] == '') {
835				// No id-number given, find free GID
836				if (!isset($this->orig['gidNumber'][0])) {
837					$newGID = $this->getNextGIDs(1, $errors, $this->getAccountContainer()->get_type());
838					if (is_array($newGID)) {
839						$this->attributes['gidNumber'][0] = $newGID[0];
840					}
841					else {
842						$errors[] = $this->messages['gidNumber'][3];
843					}
844				}
845				else $this->attributes['gidNumber'][0] = $this->orig['gidNumber'][0];
846				// old account -> return id-number which has been used
847			}
848			else {
849				$gids = $this->getGIDs($this->getAccountContainer()->get_type());
850				// Check manual ID
851				if ($this->getAccountContainer()->isNewAccount || !isset($this->orig['gidNumber'][0]) || ($this->orig['gidNumber'][0] != $this->attributes['gidNumber'][0])) {
852					// check range
853					if ($this->moduleSettings['posixGroup_' . $typeId . '_gidGenerator'][0] == 'range') {
854						if (($this->attributes['gidNumber'][0] < $minID) || ($this->attributes['gidNumber'][0] > $maxID) || !is_numeric($this->attributes['gidNumber'][0])) {
855							$errors[] = array('ERROR', _('ID-Number'), sprintf(_('Please enter a value between %s and %s!'), $minID, $maxID));
856							if (isset($this->orig['gidNumber'][0])) $this->attributes['gidNumber'][0] = $this->orig['gidNumber'][0];
857							else unset($this->attributes['gidNumber'][0]);
858						}
859					}
860					// $uids is always an array but not if no entries were found
861					if (is_array($gids)) {
862						// id-number is in use and account is a new account
863						if ((in_array($this->attributes['gidNumber'][0], $gids)) && $this->orig['gidNumber'][0]=='') {
864							$errors[] = $this->messages['gidNumber'][4];
865							unset($this->attributes['gidNumber'][0]);
866						}
867						// id-number is in use, account is existing account and id-number is not used by itself
868						if ((in_array($this->attributes['gidNumber'][0], $gids)) && $this->orig['gidNumber'][0]!='' && ($this->orig['gidNumber'][0] != $this->attributes['gidNumber'][0]) ) {
869							$errors[] = $this->messages['gidNumber'][4];
870							$this->attributes['gidNumber'][0] = $this->orig['gidNumber'][0];
871						}
872					}
873				}
874			}
875		}
876		if ($this->manageCnAndDescription($modules)) {
877			$this->attributes['cn'][0] = $_POST['cn'];
878			if (preg_match('/^[A-Z]+$/', $_POST['cn'])) {
879				$errors[] = $this->messages['cn'][0];
880			}
881			// Check if Groupname contains only valid characters
882			if (!get_preg($this->attributes['cn'][0],'groupname')) {
883				$errors[] = $this->messages['cn'][2];
884			}
885			// Create automatic useraccount with number if original user already exists
886			// Reset name to original name if new name is in use
887			// Set username back to original name if new group name is in use
888			if ($this->groupNameExists($this->attributes['cn'][0]) && ($this->orig['cn'][0] != '')) {
889				$this->attributes['cn'][0] = $this->orig['cn'][0];
890			}
891			// Change gid to a new gid until a free gid is found
892			else {
893				while ($this->groupNameExists($this->attributes['cn'][0])) {
894					// get last character of group name
895					$lastchar = substr($this->attributes['cn'][0], strlen($this->attributes['cn'][0])-1, 1);
896					// Last character is no number
897					if (!preg_match('/^([0-9])+$/', $lastchar)) {
898						/* Last character is no number. Therefore we only have to
899						* add "2" to it.
900						*/
901						$this->attributes['cn'][0] = $this->attributes['cn'][0] . '2';
902					}
903					else {
904						/* Last character is a number -> we have to increase the number until we've
905						* found a groupname with trailing number which is not in use.
906						*
907						* $i will show us were we have to split groupname so we get a part
908						* with the groupname and a part with the trailing number
909						*/
910						$i = strlen($this->attributes['cn'][0]) - 1;
911						// Set $i to the last character which is a number in $account_new->general_username
912						while (true) {
913							if (preg_match('/^([0-9])+$/',substr($this->attributes['cn'][0], $i, strlen($this->attributes['cn'][0]) - $i))) {
914								$i--;
915							}
916							else {
917								break;
918							}
919						}
920						// increase last number with one
921						$firstchars = substr($this->attributes['cn'][0], 0, $i+1);
922						$lastchars = substr($this->attributes['cn'][0], $i+1, strlen($this->attributes['cn'][0])-$i);
923						// Put username together
924						$this->attributes['cn'][0] = $firstchars . (intval($lastchars)+1);
925					}
926				}
927			}
928			// Show warning if lam has changed group name
929			if ($this->attributes['cn'][0] != $_POST['cn']) {
930				$errors[] = $this->messages['cn'][1];
931			}
932		}
933		// show info when gidnumber has changed
934		if (isset($this->orig['gidNumber'][0]) && ($this->orig['gidNumber'][0] != $this->attributes['gidNumber'][0])
935			&& ($this->orig['gidNumber'][0] != '') && !$this->changegids) {
936			$errors[] = $this->messages['gidNumber'][0];
937		}
938		// Return error-messages
939		return $errors;
940	}
941
942
943	/**
944	* Processes user input of the user selection page.
945	* It checks if all input values are correct and updates the associated LDAP attributes.
946	*
947	* @return array list of info/error messages
948	*/
949	function process_user() {
950		$return = array();
951		if (!isset($this->attributes['memberUid'])) {
952			$this->attributes['memberUid'] = array();
953		}
954		if (isset($_POST['members_2']) && isset($_POST['members_left'])) { // Add groups to list
955			// add new group
956			$this->attributes['memberUid'] = @array_merge($this->attributes['memberUid'], $_POST['members_2']);
957		}
958		elseif (isset($_POST['members_1']) && isset($_POST['members_right'])) { // remove groups from list
959			$this->attributes['memberUid'] = array_delete($_POST['members_1'], $this->attributes['memberUid']);
960		}
961		// sync users
962		elseif (isset($_POST['syncGON'])) {
963			$return = array_merge($return, $this->syncGon());
964		}
965		// sync Windows
966		elseif (isset($_POST['syncWindows'])) {
967			$return = array_merge($return, $this->syncWindows());
968		}
969		return $return;
970	}
971
972	/**
973	 * Syncs with group of names members.
974	 *
975	 * @param bool $forceDelete force deletion of members
976	 * @return array list of status messages
977	 */
978	protected function syncGon($forceDelete = false) {
979		$delete = $forceDelete || (isset($_POST['syncGON_delete']) && ($_POST['syncGON_delete'] == 'on'));
980		$return = array();
981		$gon = $this->getAccountContainer()->getAccountModule('groupOfNames');
982		if ($gon == null) {
983			$gon = $this->getAccountContainer()->getAccountModule('groupOfUniqueNames');
984		}
985                if ($gon == null) {
986                        $gon = $this->getAccountContainer()->getAccountModule('groupOfMembers');
987                }
988		if ($gon == null) {
989			return;
990		}
991		if (!isset($this->attributes['memberUid'])) {
992			$this->attributes['memberUid'] = array();
993		}
994		$memberDNs = $gon->getMembers();
995		$users = $this->getUsers();
996		$oldValues = $this->attributes['memberUid'];
997		if ($delete) {
998			$this->attributes['memberUid'] = array();
999		}
1000		foreach ($memberDNs as $dn) {
1001			foreach ($users as $userName => $userAttrs) {
1002				if ($userAttrs['dn'] != $dn) {
1003					continue;
1004				}
1005				$this->attributes['memberUid'][] = $userName;
1006			}
1007		}
1008		$added = array_delete($oldValues, $this->attributes['memberUid']);
1009		if (!empty($added)) {
1010			$return[] = array('INFO', _('Added users'), htmlspecialchars(implode(', ', $added)));
1011		}
1012		if ($delete) {
1013			$deleted = array_delete($this->attributes['memberUid'], $oldValues);
1014			if (!empty($deleted)) {
1015				$return[] = array('INFO', _('Removed users'), htmlspecialchars(implode(', ', $deleted)));
1016			}
1017		}
1018		return $return;
1019	}
1020
1021	/**
1022	 * Syncs with Windows.
1023	 *
1024	 * @return array list of status messages
1025	 */
1026	protected function syncWindows() {
1027		$delete = isset($_POST['syncWindows_delete']) && ($_POST['syncWindows_delete'] == 'on');
1028		$return = array();
1029		$windows = $this->getAccountContainer()->getAccountModule('windowsGroup');
1030		if (!isset($this->attributes['memberUid'])) {
1031			$this->attributes['memberUid'] = array();
1032		}
1033		$windowsAttributes = $windows->getAttributes();
1034		$memberDNs = array();
1035		if (!empty($windowsAttributes['member'])) {
1036			$memberDNs = $windowsAttributes['member'];
1037		}
1038		$users = $this->getUsers();
1039		$oldValues = $this->attributes['memberUid'];
1040		if ($delete) {
1041			$this->attributes['memberUid'] = array();
1042		}
1043		foreach ($memberDNs as $dn) {
1044			foreach ($users as $userName => $userAttrs) {
1045				if ($userAttrs['dn'] != $dn) {
1046					continue;
1047				}
1048				$this->attributes['memberUid'][] = $userName;
1049			}
1050		}
1051		$added = array_delete($oldValues, $this->attributes['memberUid']);
1052		if (!empty($added)) {
1053			$return[] = array('INFO', _('Added users'), htmlspecialchars(implode(', ', $added)));
1054		}
1055		if ($delete) {
1056			$deleted = array_delete($this->attributes['memberUid'], $oldValues);
1057			if (!empty($deleted)) {
1058				$return[] = array('INFO', _('Removed users'), htmlspecialchars(implode(', ', $deleted)));
1059			}
1060		}
1061		return $return;
1062	}
1063
1064	/**
1065	* Returns a list of modifications which have to be made to the LDAP account.
1066	*
1067	* @return array list of modifications
1068	* <br>This function returns an array with 3 entries:
1069	* <br>array( DN1 ('add' => array($attr), 'remove' => array($attr), 'modify' => array($attr)), DN2 .... )
1070	* <br>DN is the DN to change. It may be possible to change several DNs (e.g. create a new user and add him to some groups via attribute memberUid)
1071	* <br>"add" are attributes which have to be added to LDAP entry
1072	* <br>"remove" are attributes which have to be removed from LDAP entry
1073	* <br>"modify" are attributes which have to been modified in LDAP entry
1074	* <br>"info" are values with informational value (e.g. to be used later by pre/postModify actions)
1075	*/
1076	function save_attributes() {
1077		// skip saving if account is based on another structural object class
1078		if ($this->is_base_module() && !$this->getAccountContainer()->isNewAccount && !in_array('posixGroup', $this->getAccountContainer()->attributes_orig['objectClass'])) {
1079			return array();
1080		}
1081		if (!in_array('posixGroup', $this->attributes['objectClass']) && !in_array('posixGroup', $this->orig['objectClass'])) {
1082			// skip saving if the extension was not added/modified
1083			return array();
1084		}
1085		// auto sync group members
1086		if ($this->isBooleanConfigOptionSet('posixGroup_autoSyncGon')) {
1087			$this->syncGon(true);
1088		}
1089		$return = $this->getAccountContainer()->save_module_attributes($this->attributes, $this->orig);
1090		// Change gids of users and hosts?
1091		if ($this->changegids) {
1092			// find all accounts to change
1093			$result = searchLDAPByFilter('(&(objectClass=posixAccount)(gidNumber=' . $this->orig['gidNumber'][0] . '))', array('dn'), array('user', 'host'));
1094			if (sizeof($result) > 0) {
1095				for ($i = 0; $i < sizeof($result); $i++) {
1096					$return[$result[$i]['dn']]['modify']['gidNumber'][0] = $this->attributes['gidNumber'][0];
1097				}
1098			}
1099		}
1100		return $return;
1101	}
1102
1103	/**
1104	* Loads the values of an account profile into internal variables.
1105	*
1106	* @param array $profile hash array with profile values (identifier => value)
1107	*/
1108	function load_profile($profile) {
1109		// profile mappings in meta data
1110		parent::load_profile($profile);
1111		// add extension
1112		if (isset($profile['posixGroup_addExt'][0]) && ($profile['posixGroup_addExt'][0] == "true")
1113				&& !in_array('posixGroup', $this->attributes['objectClass'])) {
1114			$this->attributes['objectClass'][] = 'posixGroup';
1115		}
1116	}
1117
1118	/**
1119	* Returns one or more free GID numbers.
1120	*
1121	* @param integer $count Number of needed free GIDs.
1122	* @param array $errors list of error messages where errors can be added
1123	* @param ConfiguredType $type account type
1124	* @return mixed Null if no GIDs are free else an array of free GIDs.
1125	*/
1126	function getNextGIDs($count, &$errors, $type) {
1127		$typeId = $type->getId();
1128		// check if UIDs should be taken from Samba pool entry
1129		if (isset($this->moduleSettings['posixGroup_' . $typeId . '_gidGenerator']) && ($this->moduleSettings['posixGroup_' . $typeId . '_gidGenerator'][0] == 'sambaPool')) {
1130			return $this->getNextSambaPoolGIDs($count, $errors, $typeId);
1131		}
1132		// check if UIDs should be taken from domain info entry
1133		if (isset($this->moduleSettings['posixGroup_' . $typeId . '_gidGenerator']) && ($this->moduleSettings['posixGroup_' . $typeId . '_gidGenerator'][0] == 'windowsDomain')) {
1134			return $this->getNextDomainInfoGIDs($count, $errors, $typeId);
1135		}
1136		// use magic number
1137		if (isset($this->moduleSettings['posixGroup_' . $typeId . '_gidGenerator']) && ($this->moduleSettings['posixGroup_' . $typeId . '_gidGenerator'][0] == 'magicNumber')) {
1138			$return = array();
1139			for ($i = 0; $i < $count; $i++) {
1140				$return[] = $this->moduleSettings['posixGroup_' . $typeId . '_magicNumber'][0];
1141			}
1142			return $return;
1143		}
1144		$ret = array();
1145		$minID = intval($this->moduleSettings['posixGroup_' . $typeId . '_minGID'][0]);
1146		$maxID = intval($this->moduleSettings['posixGroup_' . $typeId . '_maxGID'][0]);
1147		$gidList = $this->getGIDs($type);
1148		$gids = array();
1149		foreach ($gidList as $gid) {
1150			if (($gid <= $maxID) && ($gid >= $minID)) {
1151				$gids[] = $gid;  // ignore GIDs > maxID and GIDs < minID
1152			}
1153		}
1154		for ($i = 0; $i < $count; $i++) {
1155			if (count($gids) != 0) {
1156				// there already are some GIDs
1157				// store highest id-number
1158				$id = $gids[count($gids)-1];
1159				// Return minimum allowed id-number if all found id-numbers are too low
1160				if ($id < $minID) {
1161					$ret[] = $minID;
1162					$gids[] = $minID;
1163				}
1164				// return highest used id-number + 1 if it's still in valid range
1165				elseif ($id < $maxID) {
1166					$ret[] = $id + 1;
1167					$gids[] = $id + 1;
1168				}
1169				// find free numbers between existing ones
1170				else {
1171					$k = intval($minID);
1172					while (in_array($k, $gids)) {
1173						$k++;
1174					}
1175					if ($k > $maxID) {
1176						return null;
1177					}
1178					else {
1179						$ret[] = $k;
1180						$gids[] = $k;
1181						sort ($gids, SORT_NUMERIC);
1182					}
1183					// show warning message
1184					$errors[] = $this->messages['gidNumber'][2];
1185				}
1186			}
1187			else {
1188				// return minimum allowed id-number if no id-numbers are found
1189				$ret[] = $minID;
1190				$gids[] = $minID;
1191			}
1192		}
1193		return $ret;
1194	}
1195
1196	/**
1197	 * Gets the free GID numbers from an Samba pool entry in LDAP.
1198	 *
1199	 * @param integer $count number of needed free GIDs.
1200	 * @param array $errors list of error messages where errors can be added
1201	 * @param string $typeId account type id
1202	 * @return mixed null if no GIDs are free else an array of free GIDs
1203	 */
1204	private function getNextSambaPoolGIDs($count, &$errors, $typeId) {
1205		$dn = $this->moduleSettings['posixGroup_' . $typeId . '_sambaIDPoolDN'][0];
1206		$attrs = ldapGetDN($dn, array('gidNumber'));
1207		if (isset($attrs['gidnumber'][0]) && ($attrs['gidnumber'][0] != '')) {
1208			$newValue = $attrs['gidnumber'][0] + $count;
1209			$ldapHandle = $_SESSION['ldap']->server();
1210			ldap_modify($ldapHandle, $dn, array('gidnumber' => array($newValue)));
1211			logNewMessage(LOG_DEBUG, 'Updated Samba ID pool ' . $dn . ' with GID number ' . $newValue . ' and LDAP code ' . ldap_errno($ldapHandle));
1212			if (ldap_errno($ldapHandle) != 0) {
1213				logNewMessage(LOG_NOTICE, 'Updating Samba ID pool ' . $dn . ' with GID number ' . $newValue . ' failed. ' . ldap_error($ldapHandle));
1214				return null;
1215			}
1216			$result = array();
1217			for ($i = 0; $i < $count; $i++) {
1218				$result[] = $attrs['gidnumber'][0] + $i;
1219			}
1220			return $result;
1221		}
1222		return null;
1223	}
1224
1225	/**
1226	 * Gets the free GID numbers from an Windows domain info entry in LDAP.
1227	 *
1228	 * @param integer $count number of needed free GIDs.
1229	 * @param array $errors list of error messages where errors can be added
1230	 * @param string $typeId account type id
1231	 * @return mixed null if no GIDs are free else an array of free GIDs
1232	 */
1233	private function getNextDomainInfoGIDs($count, &$errors, $typeId) {
1234		$dn = $this->moduleSettings['posixGroup_' . $typeId . '_windowsIDPoolDN'][0];
1235		$attrs = ldapGetDN($dn, array('msSFU30MaxGidNumber'));
1236		if (isset($attrs['mssfu30maxgidnumber'][0]) && ($attrs['mssfu30maxgidnumber'][0] != '')) {
1237			$newValue = $attrs['mssfu30maxgidnumber'][0] + $count;
1238			$ldapHandle = $_SESSION['ldap']->server();
1239			ldap_modify($ldapHandle, $dn, array('mssfu30maxgidnumber' => array($newValue)));
1240			logNewMessage(LOG_DEBUG, 'Updated domain info ' . $dn . ' with GID number ' . $newValue . ' and LDAP code ' . ldap_errno($ldapHandle));
1241			if (ldap_errno($ldapHandle) != 0) {
1242				logNewMessage(LOG_NOTICE, 'Updating domain info ' . $dn . ' with GID number ' . $newValue . ' failed. ' . ldap_error($ldapHandle));
1243				return null;
1244			}
1245			$result = array();
1246			for ($i = 0; $i < $count; $i++) {
1247				$result[] = $attrs['mssfu30maxgidnumber'][0] + $i;
1248			}
1249			return $result;
1250		}
1251		return null;
1252	}
1253
1254	/**
1255	 * This method specifies if a module manages password attributes.
1256	 * @see passwordService::managesPasswordAttributes
1257	 *
1258	 * @return boolean true if this module manages password attributes
1259	 */
1260	public function managesPasswordAttributes() {
1261		return true;
1262	}
1263
1264	/**
1265	 * Specifies if this module supports to force that a user must change his password on next login.
1266	 *
1267	 * @return boolean force password change supported
1268	 */
1269	public function supportsForcePasswordChange() {
1270		return false;
1271	}
1272
1273	/**
1274	 * This function is called whenever the password should be changed. Account modules
1275	 * must change their password attributes only if the modules list contains their module name.
1276	 *
1277	 * @param String $password new password
1278	 * @param $modules list of modules for which the password should be changed
1279	 * @param boolean $forcePasswordChange force the user to change his password at next login
1280	 * @return array list of error messages if any as parameter array for StatusMessage
1281	 *               e.g. return array(array('ERROR', 'Password change failed.'))
1282	 * @see passwordService::passwordChangeRequested
1283	 */
1284	public function passwordChangeRequested($password, $modules, $forcePasswordChange) {
1285		if (!in_array(get_class($this), $modules)) {
1286			return array();
1287		}
1288		$this->attributes[$this->passwordAttrName][0] = pwd_hash($password, true, $this->moduleSettings['posixAccount_pwdHash'][0]);
1289		return array();
1290	}
1291
1292	/**
1293	 * Returns a list of existing GID numbers.
1294	 *
1295	 * @param ConfiguredType $type account type
1296	 * @return array list of GID numbers
1297	 */
1298	private function getGIDs($type) {
1299		if ($this->cachedGIDList != null) {
1300			return $this->cachedGIDList;
1301		}
1302		$this->cachedGIDList = array();
1303		$attrs = array('gidNumber');
1304		$filter = '(&(objectClass=posixGroup)(gidNumber=*))';
1305		$suffix = $type->getSuffix();
1306		$typeId = $type->getId();
1307		if (!empty($this->moduleSettings['posixGroup_' . $typeId . '_gidCheckSuffix'][0])) {
1308			$suffix = $this->moduleSettings['posixGroup_' . $typeId . '_gidCheckSuffix'][0];
1309		}
1310		$result = searchLDAP($suffix, $filter, $attrs);
1311		for ($i = 0; $i < sizeof($result); $i++) {
1312			$this->cachedGIDList[] = $result[$i]['gidnumber'][0];
1313		}
1314		sort($this->cachedGIDList, SORT_NUMERIC);
1315		return $this->cachedGIDList;
1316	}
1317
1318	/**
1319	 * Returns a list of existing users and their GID numbers and cn.
1320	 *
1321	 * @return array list in format array(uid => array('gid' => 123, 'cn' => 'Some user'))
1322	 */
1323	protected function getUsers() {
1324		if ($this->cachedUserToGIDList != null) {
1325			return $this->cachedUserToGIDList;
1326		}
1327		$this->cachedUserToGIDList = array();
1328		$typeManager = new TypeManager();
1329		foreach ($typeManager->getConfiguredTypesForScope('user') as $type) {
1330			$filter = '(&(objectClass=posixAccount)(gidNumber=*))';
1331			if ($this->isWindows()) {
1332				$filter = '(&(objectClass=user)(gidNumber=*))';
1333			}
1334			$result = searchLDAPByFilter($filter, array('uid', 'gidNumber', 'cn'), array('user'));
1335			$resultCount = sizeof($result);
1336			for ($i = 0; $i < $resultCount; $i++) {
1337				$this->cachedUserToGIDList[$result[$i]['uid'][0]] = array(
1338					'gid' => $result[$i]['gidnumber'][0],
1339					'cn' => $result[$i]['cn'][0],
1340					'dn' => $result[$i]['dn']);
1341			}
1342		}
1343		logNewMessage(LOG_DEBUG, 'Found ' . $resultCount . ' Unix users.');
1344		return $this->cachedUserToGIDList;
1345	}
1346
1347	/**
1348	 * Checks if the given group name already exists in LDAP.
1349	 *
1350	 * @param String $groupName group name
1351	 * @return boolean true if already exists
1352	 */
1353	private function groupNameExists($groupName) {
1354		return in_array($groupName, $this->getGroupNames());
1355	}
1356
1357	/**
1358	 * Returns a list of all group names in LDAP.
1359	 *
1360	 * @return array group names
1361	 */
1362	private function getGroupNames() {
1363		if ($this->cachedGroupNameList != null) {
1364			return $this->cachedGroupNameList;
1365		}
1366		$this->cachedGroupNameList = array();
1367		$attrs = array('cn');
1368		$filter = '(&(objectClass=posixGroup)(cn=*))';
1369		$suffix = $this->getAccountContainer()->get_type()->getSuffix();
1370		$typeId = $this->getAccountContainer()->get_type()->getId();
1371		if (!empty($this->moduleSettings['posixGroup_' . $typeId . '_gidCheckSuffix'][0])) {
1372			$suffix = $this->moduleSettings['posixGroup_' . $typeId . '_gidCheckSuffix'][0];
1373		}
1374		$result = searchLDAP($suffix, $filter, $attrs);
1375		for ($i = 0; $i < sizeof($result); $i++) {
1376			$this->cachedGroupNameList[] = $result[$i]['cn'][0];
1377		}
1378		return $this->cachedGroupNameList;
1379	}
1380
1381	/**
1382	 * Returns if the Windows module is active.
1383	 *
1384	 * @return boolean is Windows
1385	 */
1386	private function isWindows() {
1387		return (in_array('windowsGroup', $this->getAccountContainer()->get_type()->getModules()));
1388	}
1389
1390	/**
1391	 * Returns if cn and description attributes should be managed.
1392	 *
1393	 * @param string[] $modules modules
1394	 * @return boolean manage cn+description
1395	 */
1396	protected function manageCnAndDescription($modules) {
1397		return true;
1398	}
1399
1400}
1401
1402?>
1403