<?php

Lib::loadClass('UserStorageBase');

/**
 * Test remove user:
 *   $ dscl /Local/Default -list /Groups GroupMembership | grep test13
 *   ... for all groups:
 *     $ dscl -u diradmin -p /Local/Default -delete /Groups/workgroup GroupMembership test13
 *   $ dscl /LDAPv3/127.0.0.1 -list /Groups GroupMembership | grep test13
 *   ... for all groups:
 *     $ dscl -u diradmin -p /LDAPv3/127.0.0.1 -delete /Groups/workgroup GroupMembership test13
 *   $ dscl -u diradmin -p /Local/Default -delete /Users/test13
 */
class UserStorageMacOSX extends UserStorageBase {

	private $_rootUser;
	private $_rootPass;
	private $_host;
	private $_ldapRoot;

	public function __construct($rootUser, $rootPass, $host) {
		$this->_rootUser = $rootUser;
		$this->_rootPass = $rootPass;
		$this->_host = $host;
	}

	/**
	 * @returns object $usr
	 *   $usr->primaryKey
	 *   $usr->login
	 *   $usr->password					optional (required in createUser)
	 *   $usr->name
	 *   $usr->email
	 *   $usr->phone
	 *   $usr->homeEmail
	 *   $usr->homePhone
	 *   $usr->employeeType			'Pracownik','Kandydat','Partner','Anonymous'
	 *   $usr->isDisabled				1, 0 or null if not set
	 */
	public function getUser($userName) {
		Lib::loadClass('UsersLdapHelper');
		$usrLdap = UsersLdapHelper::getUser($userName, true);
		if (empty($usrLdap[0])) return null;

		$user = new stdClass();
		$user->primaryKey = V::get('uidNumber', '', $usrLdap[0]);
		$user->login = V::get('uid', '', $usrLdap[0]);
		$user->password = '';
		$user->name = V::get('cn', '', $usrLdap[0]);
		$user->email = V::get('mail', '', $usrLdap[0]);
		$user->phone = V::get('telephoneNumber', '', $usrLdap[0]);
		$user->homeEmail = '';
		$user->homePhone = V::get('homePhone', '', $usrLdap[0]);
		$user->employeeType = V::get('employeeType', '', $usrLdap[0]);
		return $user;
	}

	/**
	 * Group.
	 * 
	 * @returns object $group
	 *   $group->primaryKey
	 *   $group->realName		realName eg. "[123] Grupa 123"
	 *   $group->nestedGroups
	 *   $group->type			'STANOWISKO','PODMIOT','DZIAL','local','network'
	 *     'local' - access groups like com.apple.access_mail
	 *     'network' - network group: workgroup
	 *   (optional) $group->zasobID
	 *   (optional) $group->zasobDESC
	 */
	public function getGroup($groupID) {
		return $this->_getGroup($groupID, $fetchNested = true);
	}

	private function _getGroup($groupID, $fetchNested = false) {
		if ($groupID <= 0) return false;

		$group = null;
		Lib::loadClass('UsersLdapHelper');
		$groups = UsersLdapHelper::getGroupsByID($groupID);
		if (count($groups) == 1) {
			$group = reset($groups);
			if(V::get('DBG_SU', 0, $_GET, 'int') > 2){echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">groupLdap (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($group);echo'</pre>';}
			$group = $this->_buildGroupFromLdap($group, $fetchNested);
			if(V::get('DBG_SU', 0, $_GET, 'int') > 2){echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">group (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($group);echo'</pre>';}
		} else if (count($groups) > 1) {
			if(V::get('DBG_SU', 0, $_GET, 'int') > 0){echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">'."Too much groups in ldap by ID {$groupID}".' (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($groups);echo'</pre>';}
			trigger_error("Too much groups in ldap by ID {$groupID}", E_USER_WARNING);
		}
		return $group;
	}

	/**
	 * @returns bool
	 */
	public function isDisabled($usr) {
		if (null == $usr->isDisabled) {
			//$cmd = "sudo pwpolicy -u {$usr->login} -getpolicy";
			$cmd = "sudo pwpolicy -u {$usr->login} --get-effective-policy";// BUG wersja 10.9.3 opcja -getpolicy pokazuje tylko włączone opcje, nie pokaże "isDisabled=0"
			$cmdOut = null; $cmdRet = null;
			exec($cmd, $cmdOut, $cmdRet);
			if(V::get('DBG_SU', 0, $_GET, 'int') > 1){echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">cmd('.$cmd.') ret('.$cmdRet.') (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($cmdOut);echo'</pre>';}
			foreach ($cmdOut as $vLine) {
				if (false !== strpos($vLine, 'isDisabled=')) {
					if (false !== strpos($vLine, 'isDisabled=0')) {
						$usr->isDisabled = 0;
					}
					else if (false !== strpos($vLine, 'isDisabled=1')) {
						$usr->isDisabled = 1;
					}
					break;
				}
			}
		}
		return $usr->isDisabled;
	}

	/**
	 * @returns bool
	 */
	public function setDisabled($usrLogin, $isDisabled) {
		if (empty($usrLogin) || null === $isDisabled) {
			return false;
		}
		$cmdDisabled = ($isDisabled)? '1' : '0';
		$cmd = "pwpolicy -a {$this->_rootUser} -p {$this->_rootPass} -u {$usrLogin} -setpolicy \"isDisabled={$cmdDisabled}\" 2>&1 ";
		$cmdOut = null; $cmdRet = null;
		exec($cmd, $cmdOut, $cmdRet);
		if(V::get('DBG_SU', 0, $_GET, 'int') > 1){echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">cmd('.str_replace($this->_rootPass, '***', $cmd).') ret('.$cmdRet.') (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($cmdOut);echo'</pre>';}
		if ($cmdRet !== 0) {
			return false;
		}
		return true;
	}

	/**
	 * @param $usr - user object @see UserStorageBase::getUser()
	 *   $usr->employeeType: Pracownik, Kandydat, Partner, Anonymous
	 *     Pracownik - all access
	 *     Kandydat  - no access
	 *     Partner   - access: smb/afp? calendar? addressbook?
	 *     Anonymous - no access
	 */
	public function createUser($usr) {
	//public function createUser($login, $type, $name = '', $email = '', $pass = '') {

		if (V::get('DBG_SU', 0, $_GET, 'int') > 1) {
			echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">usr (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($usr);echo'</pre>';
		}

		$cmdDsclAuth = "dscl -u {$this->_rootUser} -P {$this->_rootPass} /LDAPv3/127.0.0.1 ";
		$login = $this->_cleanUid($usr->login);
		$name = $this->_cleanText($usr->name);
		$type = $usr->employeeType;
		$email = $usr->email;
		$pass = $usr->password;

		$uniqueID = 0;

		// test user login and pass by searching for $uniqueID for new user
		$cmd = "{$cmdDsclAuth} -list /Users > /dev/null && {$cmdDsclAuth} -list /Users UniqueID|awk '{print \$2}'|sort -n|tail -1 ";
		$cmdOut = null; $cmdRet = null;
		exec($cmd, $cmdOut, $cmdRet);
		if ($cmdRet == 0 && !empty($cmdOut[0])) {
			$uniqueID = intval($cmdOut[0]);
			if ($uniqueID > 0) {
				$uniqueID += 1;
			}
		}

		if ($uniqueID <= 0) {
			$this->setError(1, "Error: dscl auth - check login and password in ldap config", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
			return false;
		}
		if (empty($name)) {
			$name = $login;
		} else {
			// TODO: replace bad signs str_replace($_SESSION['CONFIG']['BAD_FILE_SIGNS_LETTERS'],$_SESSION['CONFIG']['OK_FILE_SIGNS_LETTERS'],$ADM_NAME)
		}
		if (empty($pass)) {
			$pass = $login;
		}

		$cmds = array();
		//$cmds[] = "{$cmdDsclAuth} -create /Users/{$login} HomeDirectory \"<home_dir><url>afp://{$this->_host}/Users</url><path>{$login}</path></home_dir>\" ";
		//$cmds[] = "{$cmdDsclAuth} -create /Users/{$login} NFSHomeDirectory /Network/Servers/{$this->_host}/Users/{$login} ";
		$cmds[] = "{$cmdDsclAuth} -create /Users/{$login} NFSHomeDirectory /Users/{$login} ";
		$cmds[] = "{$cmdDsclAuth} -create /Users/{$login} UserShell /bin/bash ";// TODO: bash?
		$cmds[] = "{$cmdDsclAuth} -create /Users/{$login} UniqueID {$uniqueID} ";
		$cmds[] = "{$cmdDsclAuth} -create /Users/{$login} PrimaryGroupID 20 ";// TODO: 20 maja domyslnie inne konta?
		$cmds[] = "{$cmdDsclAuth} -create /Users/{$login} RealName \"{$name}\" ";
		if (!empty($email)) $cmds[] = "{$cmdDsclAuth} -create /Users/{$login} EMailAddress {$email} ";
		$cmds[] = "{$cmdDsclAuth} -passwd /Users/{$login} \"{$pass}\" ";

		foreach ($cmds as $cmd) {
			$cmdOut = null; $cmdRet = null;
			exec($cmd, $cmdOut, $cmdRet);
			if ($cmdRet != 0) {
				$this->setError(1, "cmd failed: " . str_replace($cmdDsclAuth, "dscl __auth__ ", $cmd), '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
				return false;
			}
		}

		// TODO: $cmd = "sudo createhomedir -c -u {$login} "; - zawiesza się (nie kończy działania) ale tworzy katalog domowy

		return true;
	}

	private function _getAdminLdap() {
		if (!$this->_ldapRoot) {
			Lib::loadClass('LDAP');
			$this->_ldapRoot = LDAP::getInstance();
			if (!$this->_ldapRoot->bindDiradmin($errorMsg)) {
				// $errorMsg?
				$this->setError(1, "cant bind as diradmin", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
				return null;
			}
		}
		return $this->_ldapRoot;
	}

	public function updateUser($userName, $updateData) {
		if (empty($updateData)) return true;
		foreach ($updateData as $fldName => $val) {
			switch ($fldName) {
				case 'email':
					$ldap = $this->_getAdminLdap();
					if ($ldap) {
						$attr = array();
						$attr['mail'] = $val;
						$ldap->mod_replace($userName, $attr);
					}
					break;
				case 'name':
					$ldap = $this->_getAdminLdap();
					if ($ldap) {
						$attr = array();
						$attr['cn'] = $val;
						$ldap->mod_replace($userName, $attr);
					}
					break;
				case 'phone':
					$ldap = $this->_getAdminLdap();
					if ($ldap) {
						$attr = array();
						$attr['telephoneNumber'] = $val;
						$ldap->mod_replace($userName, $attr);
					}
					break;
				case 'employeeType':
					$ldap = $this->_getAdminLdap();
					if ($ldap) {
						$attr = array();
						$attr['employeeType'] = $val;
						$ldap->mod_replace($userName, $attr);
					}
					break;
				case 'password':
					if (!empty($val) && !$this->changePassword($userName, $val)) {
						$this->setError(1, "Nie udało się zmienić hasła dla usera '{$userName}'", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
					}
					break;
				default:
					$this->setError(1, "TODO: update user {$userName} field {$fldName} to value '{$val}'", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
			}
		}

		if ($this->hasErrors()) {
			$this->setError(1, "Nie udało się zaktualizować danych usera '{$userName}'", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
			return false;
		}
		return true;
	}

	public function updateGroup($group, $updateData) {
		if (!$group) return false;

		if (empty($updateData)) return true;
		foreach ($updateData as $fldName => $val) {
			switch ($fldName) {
				case 'realName':
					$ldap = $this->_getAdminLdap();
					if ($ldap) {
						$attr = array();
						$attr['apple-group-realname'] = $val;
						$ldap->groupAttrUpdate($group->primaryKey, $attr);
					}
					break;
				default:
					$this->setError(1, "TODO: update group {$group->primaryKey} field {$fldName} to value '{$val}'", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
			}
		}
		return true;
	}

	private function _getGroupIdFromUid($groupUid) {
		if (empty($groupUid)) return null;
		if (!is_numeric(substr($groupUid, 0, 1))) return null;

		$tmp = str_replace(array('-', '_'), '_', $groupUid);
		$tmp = explode('_', $tmp);
		$tmp = reset($tmp);
		if (!empty($tmp) && is_numeric($tmp)) {
			return $tmp;
		}
		return null;
	}

	/**
	 * User group list by id.
	 * 
	 * @param bool $fetchNested - contain all groups below connected groups and group PODMIOT from above.
	 * 
	 * @returns array with group objects @see getGroup
	 */
	public function getUserGroups($usrLogin, $fetchNested = false) {
		$groups = array();
		Lib::loadClass('UsersLdapHelper');
		$groupsNetwork = $this->_getUserGroupsNetwork($usrLogin);
		$groupsLocal = $this->_getUserGroupsLocal($usrLogin);

		foreach ($groupsLocal as $kGroupUid => $vGroup) {
			$groups[$kGroupUid] = $vGroup;
		}

		foreach ($groupsNetwork as $kGroupUid => $vGroupNetwork) {
			if ($vGroupNetwork->primaryKey == 'workgroup') {
				$groups[$vGroupNetwork->primaryKey] = $vGroupNetwork;
			}
			else if ($vGroupNetwork->zasobID > 0) {
				$groups[$vGroupNetwork->zasobID] = $vGroupNetwork;
			}
		}

		if (V::get('DBG_SU', 0, $_GET, 'int') > 2) {
			echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">groupsNetwork (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($groupsNetwork);echo'</pre>';
			echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">groupsLocal (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($groupsLocal);echo'</pre>';
		}
		return $groups;
	}

	/**
	 * Build network group object.
	 * 
	 * @param object $groupDB {ID, DESC} @see _getUserGroupsAll
	 * @returns object $group @see getGroup
	 * 
	 * Example: _buildGroupFromLdap($groupLdap) => {@see getGroup}
	 */
	private function _buildGroupFromLdap($groupLdap, $fetchNested = false) {
		$group = new stdClass();
		$group->primaryKey = $groupLdap->cn;
		$group->realName = V::get('realName', '', $groupLdap);
		$group->zasobID = $this->_getGroupIdFromUid($groupLdap->cn);
		$group->type = 'unknown';// TODO: try to fetch from name or from ldap attribute
		if ($groupLdap->cn == 'workgroup') $group->type = 'network';
		if ($fetchNested && !empty($groupLdap->nestedGroups)) {
			$group->nestedGroups = $this->_fetchNestedGroupsByAppleUids($groupLdap->nestedGroups);
		}
		return $group;
	}

	private function _fetchNestedGroupsByAppleUids($appleUids) {
		$groups = array();
		if (!is_array($appleUids)) $appleUids = array($appleUids);
		Lib::loadClass('UsersLdapHelper');
		$groupsLdap = UsersLdapHelper::getGroupsByAppleUids($appleUids);
		foreach ($groupsLdap as $vGroupLdap) {
			$group = $this->_buildGroupFromLdap($vGroupLdap, $fetchNested = false);
			if ($group && $group->zasobID > 0) {
				$groups[$group->zasobID] = $group;
			}
		}
		return $groups;
	}

	/**
	 * @param string $usrLogin - user login
	 * @returns array of group objects @see getGroup
	 */
	private function _getUserGroupsNetwork($usrLogin) {
		$groups = array();
		Lib::loadClass('UsersLdapHelper');
		$groupsNetwork = UsersLdapHelper::getUserGroups($usrLogin, 0);
		foreach ($groupsNetwork as $vGroupNetwork) {
			$groups[$vGroupNetwork->cn] = $this->_buildGroupFromLdap($vGroupNetwork);
		}
		return $groups;
	}

	/**
	 * @param string $usrLogin - user login
	 * @returns array of group objects @see getGroup
	 */
	private function _getUserGroupsLocal($usrLogin) {
		$groups = array();

		$cmd = "groups {$usrLogin}";
		$cmdOut = null; $cmdRet = null;
		exec($cmd, $cmdOut, $cmdRet);
		if ($cmdRet == 0 && !empty($cmdOut[0])) {
			$groupsCmd = explode(' ', $cmdOut[0]);
			foreach ($groupsCmd as $group) {
				if ($this->_isGroupLocal($group)) {
					$groups[$group] = $this->_buildGroupLocal($group);
				}
			}
		}
		if (V::get('DBG_SU', 0, $_GET, 'int') > 1) {
			echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">groups (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($groups);echo'</pre>';
		}

		return $groups;
	}

	public function getUserGroupsWithNested($usrLogin) {// TODO: NOT USED
		$groups = array();
		$groupsAll = array();

		$cmd = "groups {$usrLogin}";
		$cmdOut = null; $cmdRet = null;
		exec($cmd, $cmdOut, $cmdRet);
		if ($cmdRet == 0 && !empty($cmdOut[0])) {
			$pominGrupy = array('staff','everyone','netaccounts');
			$groupsCmd = explode(' ', $cmdOut[0]);
			foreach ($groupsCmd as $group) {
				$groupsAll[] = $group;

				$groupID = $this->_getGroupIdFromUid($group);
				if (!empty($groupID)) {
					$groups[$groupID] = $group;
				}
				else if ('workgroup' == $group) {
					$groups[$group] = $group;
				}
				else if (substr($group, 0, strlen('com.apple.access_')) == 'com.apple.access_') {
					$groups[$group] = $group;
				}
			}
		}
		if (V::get('DBG_SU', 0, $_GET, 'int') > 1) {
			echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">groupsAll (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($groupsAll);echo'</pre>';
			echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">groups (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($groups);echo'</pre>';
		}

		return $groups;
	}

	private function _groupNameRemoveID($groupName) {
		if (substr($groupName, 0, 1) == '[' && strpos($groupName, ']')) {
			$groupName = substr($groupName, strpos($groupName, ']') + 1);
			$groupName = trim($groupName);
		}
		return $groupName;
	}

	private function _generateGroupName($id, $groupName) {
		$groupNameShort = $groupName;
		$groupNameShort = $this->_groupNameRemoveID($groupNameShort);
		// TODO: polish chars - replace to ascii?
		$groupNameShort = preg_replace('/[^a-zA-Z0-9_-]+/', '_', $groupNameShort);
		// TODO: skrócić nazwę bo nie widać w aplikacji Server, np.
		//   RealName: [5] Typowe_stanowisko_obs_uguj_ce_Obieg_Dokument_w_do_implementacji_po_instalacji_systemu
		//   w apliakcji Server pokauje tylko "[5] ", tak samo w edycji
		return "[{$id}] {$groupNameShort}";
	}

	private function _generateGroupUid($id, $groupName) {
		$groupNameShort = $groupName;
		$groupNameShort = $this->_groupNameRemoveID($groupNameShort);
		$groupNameShort = str_replace(' ', '_', $groupNameShort);
		$groupNameShort = preg_replace('/[^a-zA-Z0-9_-]+/', '_', $groupNameShort);
		if (strlen($groupNameShort) > 30) {
			$groupNameShort = substr($groupNameShort, 0, 30);
		}
		return "{$id}_{$groupNameShort}";
	}

	/**
	 * Create group.
	 * 
	 * @param object $group @see getGroup
	 * @returns bool
	 * 
	 * @require $group->zasobID - Allowed only network group based on Zasob.
	 */
	public function createGroup($group) {
		// TEST: $ dscl /LDAPv3/127.0.0.1 -list /Groups PrimaryGroupID
		if ($group->zasobID <= 0) {
			$this->setError(1, "Error: create group {$group->primaryKey} {$group->realName} - missing zasobID", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
			return false;
		}
		$groupName = $this->_generateGroupName($group->zasobID, $group->realName);
		$groupUidGenerated = $this->_generateGroupUid($group->zasobID, $group->realName);

		/*
		 * dseditgroup -o create -n /LDAPv3/ldap.company.com -u {$this->_rootUser} -P {$this->_rootPass} -r "Extra Group" -c "a nice comment" -k "some keyword" extragroup
		 *   The group extragroup is created from the node /LDAPv3/ldap.company.com with the realname, comment,
		 *   timetolive (instead of default of 14400 = 4 hours), and keyword atttribute values given above if the user
		 *   myusername has supplied a correct password and has write access.
		 * 
		 * -r realname
		 *   This is a simple text string.
		 * 
		 * -t recordtype
		 *   The type of the record to be added to or deleted from the group specified by groupname. Valid values are user, computer, group, or computergroup.
		 * 
		 */
		$cmd = "dseditgroup -o create -n /LDAPv3/127.0.0.1 -u {$this->_rootUser} -P {$this->_rootPass} -r \"{$groupName}\" {$groupUidGenerated}";
		$cmdOut = null; $cmdRet = null;
		exec($cmd, $cmdOut, $cmdRet);
		if(V::get('DBG_SU', 0, $_GET, 'int') > 1){echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">create group cmd('.str_replace($this->_rootPass, '***', $cmd).') ret('.$cmdRet.') (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($cmdOut);echo'</pre>';}
		if ($cmdRet !== 0) {
			return false;
		}
		//$command8 = "dscl -u {$user} -P {$pass} /LDAPv3/127.0.0.1 -append /Groups/{$groupUid} GroupMembership {$ACCOUNT} ";
		//$command8 = "dscl -u {$user} -P {$pass} /LDAPv3/127.0.0.1 -delete /Groups/{$groupUid} GroupMembership {$ACCOUNT} ";
		//$command1 = "dscl -u {$user} -P {$pass} /LDAPv3/127.0.0.1 -create /Groups/{$groupUid} PrimaryGroupID {$PrimaryGroupID} ";
		//$command2 = "dscl -u {$user} -P {$pass} /LDAPv3/127.0.0.1 -create /Groups/{$groupUid} RealName \"{$groupName}\" ";
		return true;
	}

	private function _isGroupLocal($groupUid) {
		$localGroups = array();
    $localGroups[] = 'com.apple.access_mail';
    $localGroups[] = 'com.apple.access_addressbook';
    $localGroups[] = 'com.apple.access_calendar';
    $localGroups[] = 'com.apple.access_smb';
    $localGroups[] = 'com.apple.access_afp';
    $localGroups[] = 'com.apple.access_vpn';
    $localGroups[] = 'com.apple.access_chat';
    //$localGroups[] = 'workgroup'; - Network Group

		return in_array($groupUid, $localGroups);
	}

	/**
	 * Add local group member.
	 * 
	 * @param string $usrLogin - user login
	 * @param object $group - @see getGroup
	 * @returns bool
	 * 
	 * @require sudoers dla _www
	 * 
	 * cat /etc/sudoers |grep "'.$ADMIN_USERNAME.' ALL = NOPASSWD: /usr/bin/su" || echo "'.$ADMIN_USERNAME.' ALL = NOPASSWD: /usr/bin/su " >> /etc/sudoers;
	 * cat /etc/sudoers |grep "'.$ADMIN_USERNAME.' ALL = NOPASSWD: /usr/bin/su"
	 * cat /etc/sudoers |grep "_www ALL = NOPASSWD: /Applications/Server.app/Contents/ServerRoot/usr/sbin/calendarserver_manage_principals" || echo "_www ALL = NOPASSWD: /Applications/Server.app/Contents/ServerRoot/usr/sbin/calendarserver_manage_principals " >> /etc/sudoers;
	 * cat /etc/sudoers |grep "_www ALL = NOPASSWD: /Applications/Server.app/Contents/ServerRoot/usr/sbin/calendarserver_manage_principals"';
	 * cat /etc/sudoers |grep "_www ALL = NOPASSWD: /usr/bin/dscl" || echo "_www ALL = NOPASSWD: /usr/bin/dscl " >> /etc/sudoers;
	 * cat /etc/sudoers |grep "_www ALL = NOPASSWD: /usr/bin/dscl";
	 * cat /etc/sudoers |grep "_www ALL = NOPASSWD: /usr/bin/pwpolicy" || echo "_www ALL = NOPASSWD: /usr/bin/pwpolicy" >> /etc/sudoers;
	 * cat /etc/sudoers |grep "_www ALL = NOPASSWD: /usr/bin/pwpolicy";
	 * cat /etc/sudoers |grep "_www ALL = NOPASSWD: /usr/sbin/createhomedir" || echo "_www ALL = NOPASSWD: /usr/sbin/createhomedir" >> /etc/sudoers;
	 * cat /etc/sudoers |grep "_www ALL = NOPASSWD: /usr/sbin/createhomedir";
	 * 
	 * cat /etc/sudoers |grep "_www ALL = NOPASSWD: /usr/sbin/dseditgroup" || echo "_www ALL = NOPASSWD: /usr/sbin/dseditgroup" >> /etc/sudoers;
	 * cat /etc/sudoers |grep "_www ALL = NOPASSWD: /usr/sbin/dseditgroup";
	 */
	private function _addUserGroupLocal($usrLogin, $group) {
		if (!$group || empty($group->primaryKey) || empty($usrLogin)) return false;
		$groupUid = $group->primaryKey;
		$cmd = "sudo dscl /Local/Default -append /Groups/{$groupUid} GroupMembership {$usrLogin} ";
		$cmdOut = null; $cmdRet = null;
		exec($cmd, $cmdOut, $cmdRet);
		if ($cmdRet != 0) {
			$this->setError(1, "Error: add user '{$usrLogin}' to network group '{$groupUid}' ", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
			return false;
		}
		return true;
	}

	/**
	 * Remove local group member.
	 * 
	 * @param string $usrLogin - user login
	 * @param object $group - @see getGroup
	 * @returns bool
	 */
	private function _removeUserGroupLocal($usrLogin, $group) {
		if (!$group || empty($group->primaryKey) || empty($usrLogin)) return false;
		$groupUid = $group->primaryKey;
		//$cmd = "sudo dscl /Local/Default -delete /Groups/{$groupUid} GroupMembership {$usrLogin} 2>&1 ";
		//$cmd = "dseditgroup -o edit -n /Local/Default -u diradmin -p ... -d username -t user {$groupUid} ";
		$cmd = "sudo dseditgroup -o edit -n /Local/Default -d {$usrLogin} -t user {$groupUid} 2>&1 ";
		//	The group extragroup from the node /LDAPv3/ldap.company.com will have the username deleted if the correct
		//	password is presented interactively for the user myusername which also need to have write access.
		//	-t recordtype  type of the record to add or delete
		//	-d recordname  name of the record to delete

		$cmdOut = null; $cmdRet = null;
		exec($cmd, $cmdOut, $cmdRet);
		if(V::get('DBG_SU', 0, $_GET, 'int') > 1){echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">cmd('.$cmd.') ret('.$cmdRet.') (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($cmdOut);echo'</pre>';}
		if ($cmdRet != 0) {
			$this->setError(1, "Error: remove user '{$usrLogin}' from local group '{$groupUid}' ", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
			//return false;
		}
		return true;
	}

	public function findGroupUidDscl($groupUid) {// not used @see findGroupUid
		$groupRealUid = null;
		$cmd = "dscl /LDAPv3/127.0.0.1 -list /Groups | grep '^{$groupUid}' ";
		$cmdOut = null; $cmdRet = null;
		exec($cmd, $cmdOut, $cmdRet);
		if ($cmdRet != 0) {
			$this->setError(1, "cmd failed - search for group by uid", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
			return false;
		}
		if (!empty($cmdOut)) {
			foreach ($cmdOut as $vGroupUid) {
				$vGroupID = $this->_getGroupIdFromUid($vGroupUid);
				if (!empty($vGroupID) && $vGroupID == $groupUid) {
					$groupRealUid = $vGroupUid;
					break;
				}
			}
		}
		return $groupRealUid;
	}

	public function findGroupUidLdap($groupUid) {
		$groupRealUid = null;

		Lib::loadClass('UsersLdapHelper');
		$groups = UsersLdapHelper::getGroupsByID($groupUid);
		if (count($groups) == 1) {
			$groupRealUid = reset($groups)->cn;
		}

		return $groupRealUid;
	}

	public function findGroupUid($groupUid) {
		return $this->findGroupUidLdap($groupUid);
	}

	/**
	 * Add network group member.
	 * 
	 * @param string $usrLogin - user login
	 * @param object $group - @see getGroup
	 * @returns bool
	 */
	private function _addUserGroupNetwork($usrLogin, $group) {
		if (!$group || empty($group->primaryKey) || empty($usrLogin)) return false;
		$groupUid = $group->primaryKey;
		$groupName = $group->realName;

		$groupRealUid = '';
		if ($group->type == 'network') {
			$groupRealUid = $group->primaryKey;//  workgroup
		}
		else if (is_numeric($groupUid)) {
			$groupRealUid = $this->findGroupUid($groupUid);
		}

		if (!$groupRealUid) {
			if ($group->type == 'network') {
				$this->setError(1, "Error: access denied to create network group {$group->primaryKey}", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
				return false;
			}
			else if ($group->type == 'local') {
				$this->setError(1, "Error: access denied to create local group {$group->primaryKey}", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
				return false;
			}

			$created = $this->createGroup($group);
			if (!$created) {
				$this->setError(1, "Error: create group {$group->primaryKey} {$group->realName}", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
				return false;
			}
		}

		$cmdDsclAuth = "dscl -u {$this->_rootUser} -P {$this->_rootPass} /LDAPv3/127.0.0.1 ";
		$cmd = "{$cmdDsclAuth} -append /Groups/{$groupRealUid} GroupMembership {$usrLogin} ";
		$cmdOut = null; $cmdRet = null;
		exec($cmd, $cmdOut, $cmdRet);
		if ($cmdRet != 0) {// TODO: may return 62 - user already in this group
			$this->setError(1, "Error: add user '{$usrLogin}' to network group '{$groupRealUid}' ", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
			return false;
		}
		return true;
	}

	/**
	 * Remove network group member.
	 * 
	 * @param string $usrLogin - user login
	 * @param object $group - @see getGroup
	 * @returns bool
	 */
	private function _removeUserGroupNetwork($usrLogin, $group) {
		if (!$group || empty($group->primaryKey) || empty($usrLogin)) return false;
		$groupUid = $group->primaryKey;
		$cmdDsclAuth = "dscl -u {$this->_rootUser} -P {$this->_rootPass} /LDAPv3/127.0.0.1 ";
		$cmd = "{$cmdDsclAuth} -delete /Groups/{$groupUid} GroupMembership {$usrLogin} ";
		$cmdOut = null; $cmdRet = null;
		exec($cmd, $cmdOut, $cmdRet);
		if ($cmdRet != 0) {
			$this->setError(1, "Error: remove user '{$usrLogin}' from network group '{$groupUid}' ", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
			//return false;// TODO: test
		}
		return true;
	}

	/**
	 * Add group member.
	 * 
	 * @param string $usrLogin - user login
	 * @param object $group - @see getGroup
	 * @returns bool
	 */
	public function addUserGroup($usrLogin, $group) {
		// $groupUid, $groupName
		if ($group->type == 'local') {
			return $this->_addUserGroupLocal($usrLogin, $group);
		}
		else {
			return $this->_addUserGroupNetwork($usrLogin, $group);
		}
		return false;
	}

	/**
	 * Remove group member.
	 * 
	 * @param string $usrLogin - user login
	 * @param object $group - @see getGroup
	 * @returns bool
	 */
	public function removeUserGroup($usrLogin, $group) {
		if ($group->type == 'local') {
			return $this->_removeUserGroupLocal($usrLogin, $group);
		}
		else {
			return $this->_removeUserGroupNetwork($usrLogin, $group);
		}
		return false;
	}

	public function addNestedGroup($groupID, $nestedGroup) {
		if ($groupID <= 0) return false;
		if ($nestedGroup->zasobID <= 0) return false;

		$group = $this->_getGroup($groupID);
		$groupNested = $this->_getGroup($nestedGroup->zasobID);
		if (!$group || !$groupNested) {
			return false;
		}

		$groupToAdd = $groupNested->primaryKey;
		$groupName = $group->primaryKey;

		// put a group called {$groupToAdd} into the {$groupName} group
		$cmd = "dseditgroup -o edit -n /LDAPv3/127.0.0.1 -u {$this->_rootUser} -P {$this->_rootPass} -a {$groupToAdd} -t group {$groupName}";
		$cmdOut = null; $cmdRet = null;
		exec($cmd, $cmdOut, $cmdRet);
		if ($cmdRet != 0) {
			if(V::get('DBG_SU', 0, $_GET, 'int') > 1){echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">cmd('.str_replace($this->_rootPass, '***', $cmd).') ret('.$cmdRet.') (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($cmdOut);echo'</pre>';}
			$this->setError(1, "Error: add nested group '{$groupToAdd}' to group '{$groupName}' ", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
			return false;
		}
		return true;
	}

	public function removeNestedGroup($groupID, $nestedGroup) {
		if ($groupID <= 0) return false;
		if ($nestedGroup->zasobID <= 0) return false;

		$group = $this->_getGroup($groupID);
		$groupNested = $this->_getGroup($nestedGroup->zasobID);
		if (!$group || !$groupNested) {
			return false;
		}

		$groupToRemove = $groupNested->primaryKey;
		$groupName = $group->primaryKey;

		// put a group called {$groupToAdd} into the {$groupName} group
		$cmd = "dseditgroup -o edit -n /LDAPv3/127.0.0.1 -u {$this->_rootUser} -P {$this->_rootPass} -d {$groupToRemove} -t group {$groupName}";
		$cmdOut = null; $cmdRet = null;
		exec($cmd, $cmdOut, $cmdRet);
		if ($cmdRet != 0) {
			if(V::get('DBG_SU', 0, $_GET, 'int') > 1){echo'<pre style="max-height:200px;overflow:auto;border:1px solid red;text-align:left;">cmd('.str_replace($this->_rootPass, '***', $cmd).') ret('.$cmdRet.') (' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . '): ';print_r($cmdOut);echo'</pre>';}
			$this->setError(1, "Error: remove nested group '{$groupToRemove}' from group '{$groupName}' ", '(' . __CLASS__ . '::' . __FUNCTION__ . ':' . __LINE__ . ')');
			return false;
		}
		return true;
	}

	public function changePassword($usrLogin, $passwd) {
		$cmdDsclAuth = "dscl -u {$this->_rootUser} -P {$this->_rootPass} /LDAPv3/127.0.0.1 ";
		$cmd = "{$cmdDsclAuth} -passwd /Users/{$usrLogin} \"{$passwd}\" ";
		$cmdOut = null; $cmdRet = null;
		exec($cmd, $cmdOut, $cmdRet);
		if ($cmdRet != 0) {
			return false;
		}
		return true;
	}

}

