<?php

class Account
{
	static function trySync ($db, $userName, $password = NULL)
	{
		$isSync = $db->getValue (
			'SELECT sync FROM account.user WHERE name = #',
			[$userName]
		);

		if ($isSync)
			return;

		self::sync ($db, $userName, $password);
	}

	static function sync ($db, $userName, $password = NULL, $force = TRUE)
	{
		$hasAccount = $db->getValue (
			'SELECT COUNT(*) > 0
				FROM account.user u
					JOIN account.account a ON u.id = a.id
				WHERE u.name = #',
			[$userName]
		);
		
		if ($hasAccount)
		{
			self::ldapSync ($db, $userName, $password);
			self::sambaSync ($db, $userName, $password);
		}

		$db->query (
			'UPDATE account.user SET sync = TRUE WHERE name = #',
			[$userName]
		);
	}

	/**
	 * Synchronizes the user credentials in the LDAP server.
	 */
	static function ldapSync ($db, $userName, $password)
	{
		// Gets LDAP configuration parameters

		$conf = $db->getObject (
			'SELECT host, rdn, password, baseDn, filter
				FROM account.ldapConfig');

		// Connects an authenticates against server

		$ds = ldap_connect ($conf->host);

		if (!$ds)
			throw new Exception ("Can't connect to LDAP server: ". ldapError ($ds));

		try {
			ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);
			$bind = ldap_bind ($ds, $conf->rdn, base64_decode ($conf->password));

			if (!$bind)
				throw new Exception ("Authentication failed on LDAP server: ". ldapError ($ds));

			// Prepares the data

			$domain = $db->getValue ('SELECT domain FROM account.mailConfig');

			$user = $db->getObject (
				'SELECT id, nickname, lang
					FROM account.user
					WHERE name = #',
				[$userName]
			);

			$cn = empty ($user->nickname) ? $userName : $user->nickname;

			$nameArgs = explode (' ', $user->nickname);
			$givenName = $nameArgs[0];

			if (count ($nameArgs) > 1)
				$sn = $nameArgs[1];
			if (empty ($sn))
				$sn = 'Empty';

			$attrs = [
				'cn'                => $cn,
				'displayName'       => $user->nickname,
				'givenName'         => $givenName,
				'sn'                => $sn,
				'mail'              => "$userName@{$domain}",
				'userPassword'      => sshaEncode ($password),
				'preferredLanguage' => $user->lang
			];

			// Search the user entry

			$filter = "uid=$userName";

			if (!empty($conf->filter))
				$filter = "(&($filter)($conf->filter))";

			$res = ldap_search ($ds, $conf->baseDn, $filter);

			if (!$res)
				throw new Exception ("Can't get the LDAP entry: ". ldapError ($ds));

			$dn = "uid=$userName,{$conf->baseDn}";
			$entry = ldap_first_entry ($ds, $res);

			$classes = ldap_get_values ($ds, $entry, 'objectClass');

			if (!in_array ('inetOrgPerson', $classes))
			{
				ldap_delete ($ds, $dn);
				$entry = NULL;
			}

			if ($entry)
			{
				$modifs = [];
				$curAttrs = ldap_get_attributes ($ds, $entry);

				foreach ($attrs as $attribute => $value)
				if (!empty ($value))
				{
					$modifs[] = [
						'attrib'  => $attribute,
						'modtype' => LDAP_MODIFY_BATCH_REPLACE,
						'values'  => [$value]
					];
				}
				elseif (isset ($curAttrs[$attribute]))
				{
					$modifs[] = [
						'attrib'  => $attribute,
						'modtype' => LDAP_MODIFY_BATCH_REMOVE_ALL
					];
				}

				$updated = ldap_modify_batch ($ds, $dn, $modifs);
			}
			else
			{
				$addAttrs = [];
	
				foreach ($attrs as $attribute => $value)
				if (!empty ($value))
					$addAttrs[$attribute] = $value;
	
				$addAttrs = array_merge ($addAttrs, [
					'objectClass' => ['inetOrgPerson'],
					'uid'         => $userName
				]);
				$updated = ldap_add ($ds, $dn, $addAttrs);
			}

			if (!$updated)
				throw new Exception ("Can't update the LDAP entry: ". ldapError ($ds));
		}
		catch (Exception $e)
		{
			ldap_unbind ($ds);
			throw $e;
		}
	}

	/**
	 * Synchronizes the user credentials in the Samba server.
	 */
	static function sambaSync ($db, $userName, $password)
	{
		$conf = $db->getObject (
			'SELECT host, sshUser, sshPass, uidBase
				FROM account.sambaConfig'
		);
		
		$domain = $db->getValue ('SELECT domain FROM account.mailConfig');

		$samba = new SshConnection ($conf->host
			,$conf->sshUser
			,base64_decode ($conf->sshPass)
		);

		$scriptDir = '/mnt/cluster/scripts';

		// Creates the Samba user and initializes it's home directory

		$userId = $db->getValue (
			'SELECT id FROM account.user WHERE name = #', [$userName]);

		$samba->exec ("$scriptDir/create-user.sh %s %s %s"
			,$userName
			,$conf->uidBase + $userId
			,"$userName@{$domain}"
		);

		// Syncronizes the Samba password

		if (empty ($password))
			return;

		$samba->exec ("$scriptDir/set-password.sh %s %s"
			,$userName
			,$password
		);
	}
}

function ldapError ($ds)
{
    return ldap_errno ($ds) .': '. ldap_error ($ds);
}

function sshaEncode ($value)
{
    mt_srand ((double) microtime () * 1000000);
    $salt = pack ('CCCC', mt_rand (), mt_rand (), mt_rand (), mt_rand ());
    $hash = '{SSHA}' . base64_encode (pack ('H*', sha1 ($value . $salt)) . $salt);
    return $hash;
}

function sshaVerify ($hash, $value)
{
    $ohash = base64_decode (substr ($hash, 6));
    $osalt = substr ($ohash, 20);
    $ohash = substr ($ohash, 0, 20);
    $nhash = pack ('H*', sha1 ($value . $osalt));
    return $ohash == $nhash;
}

class SshConnection
{
	var $connection;

	/**
	 * Abrebiated method to make SSH connections.
	 */
	function __construct ($host, $user, $password)
	{
		$this->connection = $connection = ssh2_connect ($host);

		if (!$connection)
			throw new Exception ("Can't connect to SSH server $host");
		
		$authOk = ssh2_auth_password ($connection, $user, $password);

		if (!$authOk)
			throw new Exception ("SSH authentication failed on server $host");
			
		return $connection;
	}
	
	/**
	 * Executes a command on the host.
	 */
	function exec ()
	{
		$nargs = func_num_args ();
		$args = func_get_args ();

		for ($i = 1; $i < $nargs; $i++)
			$args[$i] = self::escape ($args[$i]);

		$command = call_user_func_array ('sprintf', $args);
		return ssh2_exec ($this->connection, $command);
	}
	
	/**
	 * Escapes the double quotes from an string.
	 */
	static function escape ($str)
	{
		return '"'. str_replace ('"', '\\"', $str) .'"';
	}
}
