<?php declare(strict_types = 1);
/*
** Zabbix
** Copyright (C) 2001-2022 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/


class CUserDirectory extends CApiService {

	public const ACCESS_RULES = [
		'get' => ['min_user_type' => USER_TYPE_SUPER_ADMIN],
		'create' => ['min_user_type' => USER_TYPE_SUPER_ADMIN],
		'update' => ['min_user_type' => USER_TYPE_SUPER_ADMIN],
		'delete' => ['min_user_type' => USER_TYPE_SUPER_ADMIN],
		'test' => ['min_user_type' => USER_TYPE_SUPER_ADMIN]
	];

	protected $tableName = 'userdirectory';
	protected $tableAlias = 'ud';
	protected $sortColumns = ['host', 'name'];

	/**
	 * @var array
	 */
	protected $output_fields = ['userdirectoryid', 'base_dn', 'bind_dn', 'description', 'host', 'name', 'port',
		'search_attribute', 'search_filter', 'start_tls'
	];

	/**
	 * Get list of user directories.
	 *
	 * @param array $options
	 *
	 * @return array|string
	 *
	 * @throws APIException
	 */
	public function get(array $options) {
		$usergroups_fields = array_keys($this->getTableSchema('usrgrp')['fields']);
		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
			'userdirectoryids' => 			['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
			'filter' =>						['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => [
				'userdirectoryid' =>			['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
				'host' =>						['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE],
				'name' =>						['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE]
			]],
			'search' =>						['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => array_fill_keys(
				['base_dn', 'bind_dn', 'description', 'host', 'name', 'search_attribute', 'search_filter'],
				['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE]
			)],
			'searchByAny' =>				['type' => API_BOOLEAN, 'default' => false],
			'startSearch' =>				['type' => API_FLAG, 'default' => false],
			'excludeSearch' =>				['type' => API_FLAG, 'default' => false],
			'searchWildcardsEnabled' =>		['type' => API_BOOLEAN, 'default' => false],
			// output
			'output' =>						['type' => API_OUTPUT, 'in' => implode(',', $this->output_fields), 'default' => API_OUTPUT_EXTEND],
			'countOutput' =>				['type' => API_FLAG, 'default' => false],
			'selectUsrgrps' =>				['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL | API_ALLOW_COUNT, 'in' => implode(',', $usergroups_fields), 'default' => null],
			// sort and limit
			'sortorder' =>					['type' => API_SORTORDER, 'default' => []],
			'sortfield' => 					['type' => API_STRINGS_UTF8, 'flags' => API_NORMALIZE, 'in' => implode(',', $this->sortColumns), 'uniq' => true, 'default' => []],
			'limit' =>						['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '1:'.ZBX_MAX_INT32, 'default' => null],
			// flags
			'preservekeys' =>				['type' => API_BOOLEAN, 'default' => false],
		]];

		if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) {
			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
		}

		if ($options['output'] === API_OUTPUT_EXTEND) {
			$options['output'] = $this->output_fields;
		}

		$user_directories = [];
		$result = DBselect($this->createSelectQuery($this->tableName(), $options));

		if ($options['countOutput']) {
			$row = DBfetch($result);

			return (string) $row['rowscount'];
		}

		while ($row = DBfetch($result)) {
			$user_directories[$row['userdirectoryid']] = $row;
		}

		if ($user_directories) {
			$user_directories = $this->addRelatedObjects($options, $user_directories);
			$user_directories = $this->unsetExtraFields($user_directories, ['userdirectoryids'], $options['output']);

			if (!$options['preservekeys']) {
				$user_directories = array_values($user_directories);
			}
		}

		return $user_directories;
	}

	/**
	 * Create user directories.
	 *
	 * @param array $userdirectories
	 *
	 * @return array
	 *
	 * @throws APIException
	 */
	public function create(array $userdirectories) {
		static::validateCreate($userdirectories);

		$ids = DB::insert($this->tableName(), $userdirectories);

		foreach (array_keys($userdirectories) as $i) {
			$userdirectories[$i]['userdirectoryid'] = $ids[$i];
		}

		static::addAuditLog(CAudit::ACTION_ADD, CAudit::RESOURCE_USERDIRECTORY, $userdirectories);

		return ['userdirectoryids' => $ids];
	}

	/**
	 * Update user directories.
	 *
	 * @param array $userdirectories
	 *
	 * @return array
	 *
	 * @throws APIException
	 */
	public function update(array $userdirectories) {
		$update = [];
		$db_userdirectories = [];

		static::validateUpdate($userdirectories, $db_userdirectories);

		foreach ($userdirectories as $userdirectory) {
			$columns = DB::getUpdatedValues('userdirectory', $userdirectory,
				$db_userdirectories[$userdirectory['userdirectoryid']]
			);

			if ($columns) {
				$update[] = [
					'values' => $columns,
					'where' => ['userdirectoryid' => $userdirectory['userdirectoryid']]
				];
			}
		}

		if ($update) {
			DB::update('userdirectory', $update);

			static::addAuditLog(CAudit::ACTION_UPDATE, CAudit::RESOURCE_USERDIRECTORY, $userdirectories,
				$db_userdirectories
			);
		}


		return ['userdirectoryids' => array_column($userdirectories, 'userdirectoryid')];
	}

	/**
	 * Delete user directories by userdirectoryid.
	 *
	 * @param array $userdirectoryids
	 *
	 * @return array
	 *
	 * @throws APIException
	 */
	public function delete(array $userdirectoryids) {
		static::validateDelete($userdirectoryids);

		DB::delete('userdirectory', ['userdirectoryid' => $userdirectoryids]);

		static::addAuditLog(CAudit::ACTION_DELETE, CAudit::RESOURCE_USERDIRECTORY, $userdirectoryids);

		return ['userdirectoryids' => $userdirectoryids];
	}

	/**
	 * Test user against specific userdirectory connection.
	 *
	 * @param array $userdirectory
	 *
	 * @throws APIException
	 */
	public function test(array $userdirectory) {
		static::validateTest($userdirectory);

		$user = [
			'username' => $userdirectory['test_username'],
			'password' => $userdirectory['test_password']
		];
		$ldapValidator = new CLdapAuthValidator(['conf' => $userdirectory]);

		if (!$ldapValidator->validate($user)) {
			self::exception($ldapValidator->isConnectionError()
					? ZBX_API_ERROR_PARAMETERS
					: ZBX_API_ERROR_PERMISSIONS,
				$ldapValidator->getError()
			);
		}

		return true;
	}

	/**
	 * Add user groups data if requested.
	 *
	 * @param array $options
	 * @param array $result
	 *
	 * @return array
	 */
	protected function addRelatedObjects(array $options, array $userdirectories): array {
		$userdirectories = parent::addRelatedObjects($options, $userdirectories);

		if ($options['selectUsrgrps'] === API_OUTPUT_COUNT) {
			static::addUserGroupsCounts($options, $userdirectories);
		}
		else if ($options['selectUsrgrps'] !== null) {
			static::addUserGroups($options, $userdirectories);
		}

		return $userdirectories;
	}

	/**
	 * Add user groups details to $userdirectories array, passed by reference.
	 *
	 * @static
	 *
	 * @param array $options
	 * @param array $userdirectories
	 */
	protected static function addUserGroups(array $options, array &$userdirectories): void {
		$ids = array_unique(array_column($userdirectories, 'userdirectoryid'));
		$fields = ($options['selectUsrgrps'] === API_OUTPUT_EXTEND)
			? array_keys(DB::getSchema('usrgrp')['fields'])
			: $options['selectUsrgrps'];
		$keys = array_fill_keys($fields, '');

		if (!in_array('userdirectoryid', $fields)) {
			$fields[] = 'userdirectoryid';
		}

		foreach (array_keys($userdirectories) as $i) {
			$userdirectories[$i]['usrgrps'] = [];
		}

		$db_usergroups = API::UserGroup()->get([
			'output' => $fields,
			'filter' => ['userdirectoryid' => $ids]
		]);

		foreach ($db_usergroups as $db_usergroup) {
			$userdirectories[$db_usergroup['userdirectoryid']]['usrgrps'][] = array_intersect_key($db_usergroup, $keys);
		}
	}

	/**
	 * Add user groups count details to $userdirectories array, passed by reference.
	 *
	 * @static
	 *
	 * @param array $options
	 * @param array $userdirectories
	 */
	protected static function addUserGroupsCounts(array $options, array &$userdirectories): void {
		$ids = array_unique(array_column($userdirectories, 'userdirectoryid'));

		$db_usergroups = API::UserGroup()->get([
			'output' => ['userdirectoryid'],
			'filter' => ['userdirectoryid' => $ids]
		]);

		foreach (array_keys($userdirectories) as $i) {
			$userdirectories[$i]['usrgrps'] = 0;
		}

		foreach ($db_usergroups as $db_usergroup) {
			++$userdirectories[$db_usergroup['userdirectoryid']]['usrgrps'];
		}
	}

	/**
	 * Validate input data before create. Modify input data in $userdirectories.
	 *
	 * @static
	 *
	 * @param array $userdirectories
	 *
	 * @throws APIException
	 */
	protected static function validateCreate(array &$userdirectories): void {
		$rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['name']], 'fields' => [
			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'name')],
			'description' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'description'), 'default' => ''],
			'host' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'host')],
			'port' =>				['type' => API_PORT, 'flags' => API_REQUIRED | API_NOT_EMPTY],
			'base_dn' =>			['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'base_dn')],
			'bind_dn' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'bind_dn'), 'default' => ''],
			'bind_password' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'bind_password'), 'default' => ''],
			'search_attribute' =>	['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'search_attribute')],
			'start_tls' =>			['type' => API_INT32, 'in' => ZBX_AUTH_START_TLS_OFF.','.ZBX_AUTH_START_TLS_ON, 'default' => ZBX_AUTH_START_TLS_OFF],
			'search_filter' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'search_filter'), 'default' => '']
		]];

		if (!CApiInputValidator::validate($rules, $userdirectories, '/', $error)) {
			static::exception(ZBX_API_ERROR_PARAMETERS, $error);
		}

		$names_sql = dbConditionString('name', array_column($userdirectories, 'name'));
		$duplicate = DBfetch(DBselect('SELECT name FROM userdirectory WHERE '.$names_sql, 1));

		if ($duplicate) {
			$subpath = '/'.(array_search($duplicate['name'], array_column($userdirectories, 'name')) + 1);
			$error = _s('Invalid parameter "%1$s": %2$s.', $subpath,
				_s('value %1$s already exists', '(name)=('.$duplicate['name'].')')
			);
			static::exception(ZBX_API_ERROR_PARAMETERS, $error);
		}
	}

	/**
	 * Validate input data before update. Modify input data in $db_userdirectories.
	 *
	 * @static
	 *
	 * @param array $userdirectories
	 * @param array $db_userdirectories
	 *
	 * @throws APIException
	 */
	protected static function validateUpdate(array &$userdirectories, ?array &$db_userdirectories) {
		$rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['userdirectoryid'], ['name']], 'fields' => [
			'userdirectoryid' =>	['type' => API_ID, 'flags' => API_REQUIRED],
			'name' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'name')],
			'description' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'description')],
			'host' =>				['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'host')],
			'port' =>				['type' => API_PORT],
			'base_dn' =>			['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'base_dn')],
			'bind_dn' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'bind_dn')],
			'bind_password' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'bind_password')],
			'search_attribute' =>	['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'search_attribute')],
			'start_tls' =>			['type' => API_INT32, 'in' => ZBX_AUTH_START_TLS_OFF.','.ZBX_AUTH_START_TLS_ON],
			'search_filter' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'search_filter')]
		]];

		if (!CApiInputValidator::validate($rules, $userdirectories, '/', $error)) {
			static::exception(ZBX_API_ERROR_PARAMETERS, $error);
		}

		$duplicates = DB::select('userdirectory', [
			'output' =>			['userdirectoryid', 'name'],
			'filter' =>			['name' => array_column($userdirectories, 'name')]
		]);
		$duplicates = array_column($duplicates, 'name', 'userdirectoryid');
		$duplicates = array_diff_key($duplicates, array_column($userdirectories, 'name', 'userdirectoryid'));

		if ($duplicates) {
			$name = reset($duplicates);
			$subpath = '/'.(array_search($name, array_column($userdirectories, 'name')) + 1);
			$error = _s('Invalid parameter "%1$s": %2$s.', $subpath,
				_s('value %1$s already exists', '(name)=('.$name.')')
			);
			static::exception(ZBX_API_ERROR_PARAMETERS, $error);
		}

		$db_userdirectories = DB::select('userdirectory', [
			'output' => array_keys($rules['fields']),
			'userdirectoryids' => array_column($userdirectories, 'userdirectoryid'),
			'preservekeys' => true
		]);
	}

	/**
	 * Validate user directory to be deleted.
	 *
	 * @static
	 *
	 * @param array $userdirectoryids
	 *
	 * @throws APIException
	 */
	protected static function validateDelete(array $userdirectoryids) {
		$rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];

		if (!CApiInputValidator::validate($rules, $userdirectoryids, '/', $error)) {
			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
		}

		$db_userdirectories = API::UserDirectory()->get([
			'output' => ['name'],
			'userdirectoryids' => $userdirectoryids,
			'preservekeys' => true
		]);

		if (count($db_userdirectories) != count($userdirectoryids)) {
			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
		}

		$auth = API::Authentication()->get([
			'output' => ['ldap_userdirectoryid', 'authentication_type', 'ldap_configured']
		]);

		if ($auth['authentication_type'] == ZBX_AUTH_LDAP
				&& in_array($auth['ldap_userdirectoryid'], $userdirectoryids)) {
			// Check there are no user groups with default user directory.
			$userdirectoryids[] = 0;
		}

		$db_groups = API::UserGroup()->get([
			'output' => ['userdirectoryid', 'name'],
			'filter' => [
				'gui_access' => ZBX_AUTH_LDAP,
				'userdirectoryid' => $userdirectoryids
			],
			'limit' => 1
		]);

		if (!$db_groups) {
			return;
		}

		$db_group = reset($db_groups);

		if ($db_group['userdirectoryid'] === $auth['ldap_userdirectoryid']) {
			$error = _('Cannot delete default user directory "%1$s".',
				$db_userdirectories[$auth['ldap_userdirectoryid']]['name']
			);
		}
		else {
			$error = _('Cannot delete user directory "%1$s".', $db_group['name']);
		}

		static::exception(ZBX_API_ERROR_PARAMETERS, $error);
	}

	/**
	 * Validate user directory and test user credentials to be used for testing.
	 *
	 * @param array $userdirectory
	 *
	 * @throws APIException
	 */
	protected static function validateTest(array &$userdirectory) {
		$rules = ['type' => API_OBJECT, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'fields' => [
			'userdirectoryid' =>	['type' => API_ID, 'flags' => API_ALLOW_NULL, 'default' => null],
			'host' =>				['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'host')],
			'port' =>				['type' => API_PORT, 'flags' => API_REQUIRED | API_NOT_EMPTY],
			'base_dn' =>			['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'base_dn')],
			'bind_dn' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'bind_dn'), 'default' => ''],
			'bind_password' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'bind_password')],
			'search_attribute' =>	['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('userdirectory', 'search_attribute')],
			'start_tls' =>			['type' => API_INT32, 'in' => ZBX_AUTH_START_TLS_OFF.','.ZBX_AUTH_START_TLS_ON, 'default' => ZBX_AUTH_START_TLS_OFF],
			'search_filter' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('userdirectory', 'search_filter'), 'default' => ''],
			'test_username' => 		['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY],
			'test_password' => 		['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY]
		]];

		if (!CApiInputValidator::validate($rules, $userdirectory, '/', $error)) {
			static::exception(ZBX_API_ERROR_PARAMETERS, $error);
		}

		if ($userdirectory['userdirectoryid'] !== null) {
			$db_userdirectory = DB::select('userdirectory', [
				'output' => [
					'host', 'port', 'base_dn', 'bind_dn', 'bind_password', 'search_attribute', 'start_tls',
					'search_filter'
				],
				'userdirectoryids' => $userdirectory['userdirectoryid']
			]);
			$db_userdirectory = reset($db_userdirectory);

			if (!$db_userdirectory) {
				self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
			}

			$userdirectory += $db_userdirectory;
		}
	}
}