<?php

use LAM\JOB\Job;
use LAM\PDF\PDFEntry;
use LAM\PDF\PDFLabelValue;
use LAM\PDF\PDFTable;
use LAM\TYPES\ConfiguredType;
use function LAM\TYPES\getScopeFromTypeId;
use LAM\PDF\PDFImage;

/*

  This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
  Copyright (C) 2003 - 2026  Roland Gruber

  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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA


*/

/**
 * This is the parent class for all account modules.
 *
 * It implements the complete module interface and uses meta-data
 * provided by the account modules for its functions.
 *
 * @package modules
 * @author Roland Gruber
 * @see baseModule
 */

/** PDF functions */
include_once(__DIR__ . '/pdf.inc');

/**
 * Parent class of all account modules.
 * It implements the complete module interface and uses meta-data
 * provided by the account modules for its functions.<br>
 * <br>
 * <b>Location and naming of modules</b><br>
 * All LAM modules are placed in lib/modules/ and are named "<class name>.inc".
 * E.g. if you create a new module and its class name is "qmail" then the filename would be "qmail.inc".
 * The class name of a module must contain only a-z, A-Z, 0-9, -, and _.<br>
 * <br>
 * You can avoid to override many functions by using {@link get_metaData()}.<br>
 * <br>
 * All module classes should extend the baseModule class.
 *
 * @package modules
 * @author Roland Gruber
 */
abstract class baseModule {

	/**
	 * These attributes will be ignored by default if a new account is copied from an existing one.
	 */
	public const ATTRIBUTES_TO_IGNORE_ON_COPY_DEFAULT = ['uid', 'uidNumber', 'gid', 'gidNumber',
		'cn', 'userpassword', 'sn', 'givenName', 'initials', 'telephoneNumber',
		'homePhone', 'mobile', 'facsimileTelephoneNumber', 'pager', 'mail',
		'employeeNumber', 'userCertificate;binary', 'userCertificate',
		'homeDirectory', 'unixHomeDirectory', 'jpegPhoto'];

	/** @var array<string, mixed>|null $meta includes all meta data provided by the sub class */
	protected ?array $meta = null;

	/** the account type of this module (user, group, host) */
	private ?string $scope;

	/** @var array<string, string[]>|null $moduleSettings configuration settings of all modules */
	protected ?array $moduleSettings = null;

	/**
	 * @var selfServiceProfile|null self-service profile with settings of all modules
	 */
	protected ?selfServiceProfile $selfServiceSettings = null;

	/** name of parent accountContainer ($_SESSION[$base]) */
	private ?string $base = null;

	/** @var array<string, string[]|string>|null contains all ldap attributes which should be written */
	protected ?array $attributes = null;

	/** @var array<string, string[]|string>|null contains all ldap attributes which are loaded from LDAP */
	protected ?array $orig = null;

	/** @var array<string, array<int, array<string|string[]>>>|null $messages contains all error messages of a module */
	protected ?array $messages = null;

	/** if true, managed object classes are added when an account is created or loaded (default: true) */
	protected bool $autoAddObjectClasses = true;

	/**
	 * Creates a new base module class
	 *
	 * @param string|null $scope the account type (user, group, host)
	 */
	public function __construct(?string $scope) {
		$this->scope = $scope;
		// load configuration
		if ($this->can_manage() || ($scope === null)) {
			if (isset($_SESSION['config'])) {
				$this->moduleSettings = $_SESSION['config']->get_moduleSettings();
			}
			if (isset($_SESSION['selfServiceProfile'])) {
				$this->selfServiceSettings = $_SESSION['selfServiceProfile'];
			}
			// initialize module
			$this->load_Messages();
			$this->meta = $this->get_metaData();
		}
	}

	/**
	 * This function fills the $messages variable with output messages from this module.
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.
	 */
	protected function load_Messages(): void {
	}

	/**
	 * Initializes the module after it became part of an {@link accountContainer}
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.
	 *
	 * @param string $base the name of the {@link accountContainer} object ($_SESSION[$base])
	 */
	public function init(string $base): void {
		$this->base = $base;
		$this->attributes = [];
		$this->orig = [];
		// add object classes if needed
		$this->attributes['objectClass'] = [];
		$this->orig['objectClass'] = [];
		if ($this->autoAddObjectClasses) {
			$objectClasses = $this->getManagedObjectClasses($this->getAccountContainer()->get_type()->getId());
			for ($i = 0; $i < count($objectClasses); $i++) {
				if (!in_array($objectClasses[$i], $this->attributes['objectClass'])) {
					$this->attributes['objectClass'][] = $objectClasses[$i];
				}
			}
		}
	}


	/**
	 * This function loads the LDAP attributes when an account should be loaded.
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * By default this method loads the object classes and accounts which are specified in {@link getManagedObjectClasses()}
	 * and {@link getManagedAttributes()}.
	 *
	 * @param array<string, string[]|string> $attributes array like the array returned by get_ldap_attributes(dn of account) but without count indices
	 */
	public function load_attributes(array $attributes): void {
		$this->attributes = [];
		// load object classes
		if (isset($attributes['objectClass'])) {
			$this->attributes['objectClass'] = $attributes['objectClass'];
			$this->orig['objectClass'] = $attributes['objectClass'];
		}
		else {
			$this->attributes['objectClass'] = [];
			$this->orig['objectClass'] = [];
		}
		$typeId = $this->getAccountContainer()->get_type()->getId();
		// add object classes if needed
		if ($this->autoAddObjectClasses) {
			$objectClasses = $this->getManagedObjectClasses($typeId);
			for ($i = 0; $i < count($objectClasses); $i++) {
				if (!in_array($objectClasses[$i], $this->attributes['objectClass'])) {
					$this->attributes['objectClass'][] = $objectClasses[$i];
				}
			}
		}
		// load attributes
		$attributeNames = array_merge($this->getManagedAttributes($typeId), $this->getManagedHiddenAttributes($typeId));
		$attributeNames = array_unique($attributeNames);
		$attributeNames = array_values($attributeNames);
		for ($i = 0; $i < count($attributeNames); $i++) {
			if (isset($attributes[$attributeNames[$i]])) {
				$this->attributes[$attributeNames[$i]] = $attributes[$attributeNames[$i]];
				$this->orig[$attributeNames[$i]] = $attributes[$attributeNames[$i]];
			}
		}
	}

	/**
	 * Loads the LDAP data from an account to copy.
	 *
	 * @param array<string, string[]|string> $ldapAttributes LDAP attributes of copy
	 * @param string[] $attributesToIgnore list of attributes to ignore during load (defaults to self::ATTRIBUTES_TO_IGNORE_ON_COPY_DEFAULT)
	 */
	public function loadAttributesFromAccountCopy(array $ldapAttributes, array $attributesToIgnore = []): void {
		if (empty($attributesToIgnore)) {
			$attributesToIgnore = self::ATTRIBUTES_TO_IGNORE_ON_COPY_DEFAULT;
		}
		$typeId = $this->getAccountContainer()->get_type()->getId();
		$validAttributeNames = array_merge($this->getManagedAttributes($typeId), $this->getManagedHiddenAttributes($typeId));
		foreach ($ldapAttributes as $ldapAttribute => $values) {
			if (in_array_ignore_case($ldapAttribute, $attributesToIgnore)) {
				continue;
			}
			if (!in_array($ldapAttribute, $validAttributeNames)) {
				continue;
			}
			$this->attributes[$ldapAttribute] = $values;
		}
		$objectClassesToCopy = $ldapAttributes['objectClass'];
		$validObjectClasses = $this->getManagedObjectClasses($typeId);
		foreach ($validObjectClasses as $validObjectClass) {
			if (in_array_ignore_case($validObjectClass, $objectClassesToCopy) &&
				(empty($this->attributes['objectClass']) || !in_array_ignore_case($validObjectClass, $this->attributes['objectClass']))) {
				$this->attributes['objectClass'][] = $validObjectClass;
			}
		}
	}

	/**
	 * This function provides meta data which is interpreted by baseModule.
	 * Only subclasses will return real data.<br>
	 * <br>
	 * The aim of the meta data is to reduce the number
	 * of functions in the subclasses. All major data is centralized in one place.<br>
	 * <br>
	 * The returned array contains a list of key-value pairs for the different functions.<br>
	 * <ul>
	 *
	 * <li><b>{@link is_base_module()}</b><br>
	 * <br>
	 * <b>Key:</b> is_base<br>
	 * <b>Value:</b> boolean<br>
	 * <br>
	 * <b>Example:</b> "is_base" => true
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link get_ldap_filter()}</b><br>
	 * <br>
	 * <b>Key:</b> ldap_filter<br>
	 * <b>Value:</b> array of filters<br>
	 * <br>
	 * <b>Example:</b> "ldap_filter" => array('or' => 'objectClass=posixAccount', 'and' => '(!(uid=*$))')
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link getManagedObjectClasses()}</b><br>
	 * <br>
	 * <b>Key:</b> objectClasses<br>
	 * <b>Value:</b> array of object classes<br>
	 * <br>
	 * <b>Example:</b> "objectClasses" => array('posixAccount')
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link getLDAPAliases()}</b><br>
	 * <br>
	 * <b>Key:</b> LDAPaliases<br>
	 * <b>Value:</b> array of aliases<br>
	 * <br>
	 * <b>Example:</b> "LDAPaliases" => array('commonName' => 'cn')
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link get_RDNAttributes()}</b><br>
	 * <br>
	 * <b>Key:</b> RDN<br>
	 * <b>Value:</b> array of RDNs<br>
	 * <br>
	 * <b>Example:</b> "RDN" => array('uid' => 'normal', 'cn' => 'low')
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link get_dependencies()}</b><br>
	 * <br>
	 * <b>Key:</b> dependencies<br>
	 * <b>Value:</b> array of dependencies<br>
	 * <br>
	 * <b>Example:</b> "dependencies" => array("depends" => array("posixAccount", array("qmail", "sendmail")), "conflicts" => array("exim"))
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link get_profileOptions()}</b><br>
	 * <br>
	 * <b>Key:</b> profile_options<br>
	 * <b>Value:</b> array of profile options<br>
	 * <br>
	 * The syntax for the value array is the same as for the return value of get_profileOptions().
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link check_profileOptions()}</b><br>
	 * <br>
	 * <b>Key:</b> profile_checks<br>
	 * <b>Value:</b> array of checks (array("optionName" => []))<br>
	 * <br>
	 * The "optionName" keys of the value array are the names of the option identifiers.<br>
	 * Each array element is an array itself containing these values:
	 * <ul>
	 * <li><b>type:</b> determines how to check input<br>
	 * Possible values:
	 *    <ul>
	 *    <li><b>regex:</b> check with regular expression from regex variable, case sensitive</li>
	 *    <li><b>regex_i:</b> check with regular expression from regex variable, case insensitive</li>
	 *    <li><b>int_greater:</b> integer value of cmp_name1 must be greater than the integer value from the option cmp_name2</li>
	 *    <li><b>int_greaterOrEqual:</b> integer value of cmp_name1 must be greater or equal than the integer value from the option cmp_name2</li>
	 *    </ul>
	 * </li>
	 * <li><b>error_message:</b> message that is displayed if input value was syntactically incorrect<br>
	 *    error_message is an array to build StatusMessages (message type, message head, message text, additional variables)
	 * <li><b>regex:</b> regular expression string (only if type is regex/regex_i)</li>
	 * <li><b>cmp_name1:</b> name of first input variable that is used for comparison (only if type is int_greater/int_greaterOrEqual)</li>
	 * <li><b>cmp_name2:</b> name of second input variable that is used for comparison (only if type is int_greater/int_greaterOrEqual)</li>
	 * <li><b>required:</b> true or false, if this input field must be filled set to true (optional)
	 * <li><b>required_message:</b> message that is displayed if no input value was given (only if required == true)<br>
	 *    required_message is an array to build StatusMessages (message type, message head, message text, additional variables)
	 * </li>
	 * </ul>
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link load_profile()}</b><br>
	 * <br>
	 * <b>Key:</b> profile_mappings<br>
	 * <b>Value:</b> array('profile_identifier1' => 'LDAP_attribute1', 'profile_identifier2' => 'LDAP_attribute2')<br>
	 * <br>
	 * The mapped values are stored directly in $this->attributes.
	 * <br>
	 * <b>Example:</b> "profile_mappings" => array('inetOrgPerson_title' => 'title')
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link get_configOptions()}</b><br>
	 * <br>
	 * <b>Key:</b> config_options<br>
	 * <b>Value:</b> array('user' => array, 'host' => array, 'all' => array)<br>
	 * <br>
	 * The values from 'all' are always returned, the other values only if they are inside the $scopes array.<br>
	 * The syntax for sub arrays is the same as for the return value of {@link get_configOptions()}.
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link check_configOptions()}</b><br>
	 * <br>
	 * <b>Key:</b> config_checks<br>
	 * <b>Value:</b> array('user' => array, 'host' => 'array', 'all' => array)<br>
	 * <br>
	 * The values from 'all' are always used for checking, the other values only if they are inside the $scopes array.
	 * The syntax for sub arrays is the same as for {@link check_configOptions()}.
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link get_uploadColumns()}</b><br>
	 * <br>
	 * <b>Key:</b> upload_columns<br>
	 * <b>Value:</b> array<br>
	 * <br>
	 * The syntax for array is the same as for the return value of {@link get_uploadColumns()}.
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link get_uploadPreDepends()}</b><br>
	 * <br>
	 * <b>Key:</b> upload_preDepends<br>
	 * <b>Value:</b> array<br>
	 * <br>
	 * The syntax for array is the same as for the return value of {@link get_uploadPreDepends()}.
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link get_help()}</b><br>
	 * <br>
	 * <b>Key:</b> help<br>
	 * <b>Value:</b> hashtable of help entries<br>
	 * <br>
	 * The hashtable is an array which maps help IDs to help entries.<br>
	 * <br>
	 * <b>Example:</b> 'help' => array('myEntry' => array('Headline' => 'This is the head line', 'Text' => 'Help content'))
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link getSelfServiceSearchAttributes()}</b><br>
	 * <br>
	 * <b>Key:</b> selfServiceSearchAttributes<br>
	 * <b>Value:</b> array of attribute names<br>
	 * <br>
	 * <b>Example:</b> "selfServiceSearchAttributes" => array('uid')
	 * <br><br>
	 * </li>
	 *
	 * <li><b>{@link getSelfServiceFields()}</b><br>
	 * <br>
	 * <b>Key:</b> selfServiceFieldSettings<br>
	 * <b>Value:</b> array of self-service fields<br>
	 * <br>
	 * <b>Example:</b> "selfServiceFieldSettings" => array('pwd' => 'Password')
	 * <br><br>
	 * </li>
	 *
	 * </ul>
	 * <b>Example:</b> return array("is_base" => true);
	 *
	 * @return array<string, mixed> meta data
	 */
	public function get_metaData(): array {
		return [];
	}

	/**
	 * Returns the account type of this module (user, group, host)
	 *
	 * @return string account type
	 */
	public function get_scope() {
		return $this->scope;
	}

	/**
	 * Returns true if this module can manage accounts of the current type, otherwise false.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.
	 *
	 * @return bool true if module fits
	 *
	 * @see baseModule::get_metaData()
	 */
	abstract public function can_manage(): bool;

	/**
	 * Returns true if your module is a base module and otherwise false.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * Every account type needs exactly one base module. A base module manages a structural object class.
	 * E.g. the inetOrgPerson module is a base module since its object class is structural.
	 *
	 * @return boolean true if base module (defaults to false if no meta data is provided)
	 *
	 * @see baseModule::get_metaData()
	 */
	public function is_base_module() {
		return (isset($this->meta['is_base']) && ($this->meta['is_base']));
	}

	/**
	 * Returns an LDAP filter for the account lists
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * Returns an array('or' => '...', 'and' => '...') that is used to build the LDAP filter. Usually, this is used to filter object classes.
	 * All "or" filter parts of the base modules are combined with OR and then combined with the "and" parts.<br>
	 * The resulting LDAP filter will look like this: (&(|(OR1)(OR2)(OR3))(AND1)(AND2)(AND3))<br>
	 * <br>
	 * <b>Example:</b> return array('or' => '(objectClass=posixAccount)', 'and' => '(!(uid=*$))')
	 *
	 * @param string $typeId account type id
	 * @return array<string, string> LDAP filter
	 *
	 * @see baseModule::get_metaData()
	 */
	public function get_ldap_filter($typeId) {
		return $this->meta['ldap_filter'] ?? [];
	}

	/**
	 * Returns an alias name for the module.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * This function returns a more descriptive string than the class name. Alias names are used for the buttons on the account pages and the module selection in the configuration wizard.<br>
	 * Please take care that your alias name is not too long. It may contain any character but should not include parts that may be interpreted by the browser (e.g. '<' or '>').
	 * If you use different aliases dependent on the account type please make sure that there is a general alias for unknown types.
	 *
	 * @return string alias name
	 *
	 * @see baseModule::get_metaData()
	 */
	public function get_alias() {
		return $this->meta['alias'] ?? static::class;
	}

	/**
	 * Returns a hash array containing a list of possible LDAP attributes that can be used to form the RDN (Relative Distinguished Name).
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * The returned elements have this form: <attribute> => <priority>
	 * <br> <attribute> is the name of the LDAP attribute
	 * <br> <priority> defines the priority of the attribute (can be "low", "normal", "high")<br>
	 * <br>
	 * <b>Example:</b> return array('uid' => 'normal', 'cn' => 'low')
	 *
	 * @param string $typeId account type
	 * @return array<string, string> list of attributes
	 *
	 * @see baseModule::get_metaData()
	 */
	public function get_RDNAttributes($typeId): array {
		return $this->meta['RDN'] ?? [];
	}

	/**
	 * This function returns a list with all depending and conflicting modules.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * The return value is an array with two sub arrays, "depends" and "conflicts".
	 * All values of the conflict array are string values with module names. All values of the depends
	 * array are either string values with module names or arrays which include only string values with
	 * module names.<br>
	 * If an element of the depends array is itself an array, this means that your module
	 * depends on one of these modules.<br>
	 * <br>
	 * <b>Example:</b> return array("depends" => array("posixAccount", array("qmail", "sendmail")), "conflicts" => array("exim"))
	 *
	 * @return array<string, array<string|string[]>> list of dependencies and conflicts
	 *
	 * @see baseModule::get_metaData()
	 */
	public function get_dependencies(): array {
		return $this->meta['dependencies'] ?? ['depends' => [], 'conflicts' => []];
	}

	/**
	 * This function defines what attributes will be used in the account profiles and their appearance in the profile editor.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * The return value is an object implementing htmlElement.<br>
	 * The field name are used as keywords to load
	 * and save profiles. We recommend to use the module name as prefix for them
	 * (e.g. posixAccount_homeDirectory) to avoid naming conflicts.
	 *
	 * @param string $typeId type id (user, group, host, ...)
	 * @return htmlResponsiveRow|null meta HTML object
	 *
	 * @see baseModule::get_metaData()
	 * @see htmlResponsiveRow
	 */
	public function get_profileOptions($typeId) {
		return $this->meta['profile_options'] ?? null;
	}

	/**
	 * Checks input values of account profiles.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * $options is an hash array (option name => value) that contains the user input.
	 * The option values are all arrays containing one or more elements.<br>
	 * If the input data is invalid the return value is an array that contains arrays
	 * to build StatusMessages (message type, message head, message text). If no errors occurred
	 * the function returns an empty array.
	 *
	 * @param array<string, string[]> $options a hash array (name => value) containing the user input
	 * @param string $typeId type id (user, group, host)
	 * @return string[][] list of error messages (array(type, title, text)) to generate StatusMessages, if any
	 *
	 * @see baseModule::get_metaData()
	 */
	public function check_profileOptions(array $options, string $typeId): array {
		$errors = [];
		if (isset($this->meta['profile_checks'])) {
			foreach ($this->meta['profile_checks'] as $identifier => $check) {
				// check if option is required
				if (isset($check['required']) && $check['required']
					&& (!isset($options[$identifier][0]) || ($options[$identifier][0] == ''))) {
					$errors[] = $check['required_message'];
					continue;
				}
				switch ($check['type']) {
					// check by regular expression (from account.inc)
					case "ext_preg":
						// ignore empty fields
						if (!empty($options[$identifier][0])
							&& !get_preg($options[$identifier][0], $check['regex'])) {
							$errors[] = $check['error_message'];
						}
						break;
					// check by regular expression (case insensitive)
					case 'regex_i':
						// ignore empty fields
						if (!empty($options[$identifier][0])
							&& !preg_match('/' . $check['regex'] . '/i', $options[$identifier][0])) {
							$errors[] = $check['error_message'];
						}
						break;
					// check by regular expression (case sensitive)
					case 'regex':
						// ignore empty fields
						if (!empty($options[$identifier][0])
							&& !preg_match('/' . $check['regex'] . '/', $options[$identifier][0])) {
							$errors[] = $check['error_message'];
						}
						break;
					// check by integer comparison (greater)
					case 'int_greater':
						$val1 = $options[$check['cmp_name1']][0] ?? '';
						$val2 = $options[$check['cmp_name2']][0] ?? '';
						// ignore if both fields or minimum are empty
						if (!empty($val1)
							&& ($val2 !== '')
							&& (intval($val1) <= intval($val2))) {
							$errors[] = $check['error_message'];
						}
						break;
					// check by integer comparison (greater or equal)
					case 'int_greaterOrEqual':
						$val1 = $options[$check['cmp_name1']][0] ?? '';
						$val2 = $options[$check['cmp_name2']][0] ?? '';
						// ignore if both fields are empty
						if (!empty($val1)
							&& ($val2 !== '')
							&& (intval($val1) < intval($val2))) {
							$errors[] = $check['error_message'];
						}
						break;
					// print error message for invalid types
					default:
						StatusMessage("ERROR", "Unsupported type!", $check['type']);
						break;
				}
			}
		}
		return $errors;
	}

	/**
	 * This function loads the values from an account profile to the module's internal data structures.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.
	 *
	 * @param array<string, string[]> $profile hash array with profile values (identifier => value)
	 *
	 * @see baseModule::get_metaData()
	 */
	public function load_profile(array $profile): void {
		if (isset($this->meta['profile_mappings'])) {
			$identifiers = array_keys($this->meta['profile_mappings']);
			for ($i = 0; $i < count($identifiers); $i++) {
				if (isset($profile[$identifiers[$i]])) {
					$this->attributes[$this->meta['profile_mappings'][$identifiers[$i]]] = $profile[$identifiers[$i]];
				}
			}
		}
	}

	/**
	 * Returns a list of configuration options.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * The field names are used as keywords to load and save settings.
	 * We recommend to use the module name as prefix for them (e.g. posixAccount_homeDirectory) to avoid naming conflicts.
	 *
	 * @param string[] $scopes account types (user, group, host)
	 * @param array<string, string[]> $allScopes list of all active account modules and their account type id (module => array(type id))
	 * @return htmlElement[]|htmlElement htmlElement or array of htmlElement
	 *
	 * @see baseModule::get_metaData()
	 * @see htmlElement
	 */
	public function get_configOptions(array $scopes, array $allScopes): htmlElement|array {
		$return = [];
		for ($i = 0; $i < count($scopes); $i++) {
			if (isset($this->meta['config_options'][$scopes[$i]])) {
				if (is_array($this->meta['config_options'][$scopes[$i]])) {
					$return = array_merge($return, $this->meta['config_options'][$scopes[$i]]);
				}
				elseif (isset($return[0]) && ($return[0] instanceof htmlTable) && ($this->meta['config_options'][$scopes[$i]] instanceof htmlTable)) {
					$return[0]->mergeTableElements($this->meta['config_options'][$scopes[$i]]);
				}
				else {
					$return[] = $this->meta['config_options'][$scopes[$i]];
				}
			}
		}
		if (isset($this->meta['config_options']['all'])) {
			if (is_array($this->meta['config_options']['all'])) {
				$return = array_merge($return, $this->meta['config_options']['all']);
			}
			elseif (isset($return[0]) && ($return[0] instanceof htmlTable) && ($this->meta['config_options']['all'] instanceof htmlTable)) {
				$return[0]->mergeTableElements($this->meta['config_options']['all']);
			}
			else {
				$return[] = $this->meta['config_options']['all'];
			}
		}
		return $return;
	}

	/**
	 * Checks input values of module settings.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * If the input data is invalid the return value is an array that contains subarrays to build StatusMessages ('message type', 'message head', 'message text').
	 * <br>If no errors occurred the function returns an empty array.
	 *
	 * @param string[] $typeIds list of account type ids which are used
	 * @param array<string, string[]> $options hash array (option name => value) that contains the input. The option values are all arrays containing one or more elements.
	 * @return string[][] list of error messages
	 *
	 * @see baseModule::get_metaData()
	 */
	public function check_configOptions(array $typeIds, array &$options): array {
		$messages = [];
		// add checks that are independent of scope
		$scopes = ['all'];
		foreach ($typeIds as $typeId) {
			$scopes[] = getScopeFromTypeId($typeId);
		}
		$scopes = array_unique($scopes);
		foreach ($scopes as $scope) {
			if (isset($this->meta['config_checks'][$scope]) && is_array($this->meta['config_checks'][$scope])) {
				$identifiers = array_keys($this->meta['config_checks'][$scope]);
				for ($i = 0; $i < count($identifiers); $i++) {
					// check if option is required
					if (isset($this->meta['config_checks'][$scope][$identifiers[$i]]['required']) && ($this->meta['config_checks'][$scope][$identifiers[$i]]['required']) && ($options[$identifiers[$i]][0] == '')) {
						$messages[] = $this->meta['config_checks'][$scope][$identifiers[$i]]['required_message'];
					}
					switch ($this->meta['config_checks'][$scope][$identifiers[$i]]['type']) {
						// check by regular expression (from account.inc)
						case "ext_preg":
							// ignore empty fields
							if ($options[$identifiers[$i]][0] == '') {
								break;
							}
							if (!get_preg($options[$identifiers[$i]][0], $this->meta['config_checks'][$scope][$identifiers[$i]]['regex'])) {
								$messages[] = $this->meta['config_checks'][$scope][$identifiers[$i]]['error_message'];
							}
							break;
						// check by regular expression (case insensitive)
						case "regex_i":
							// ignore empty fields
							if ($options[$identifiers[$i]][0] == '') {
								break;
							}
							if (!preg_match('/' . $this->meta['config_checks'][$scope][$identifiers[$i]]['regex'] . '/i', $options[$identifiers[$i]][0])) {
								$messages[] = $this->meta['config_checks'][$scope][$identifiers[$i]]['error_message'];
							}
							break;
						// check by regular expression (case sensitive)
						case "regex":
							// ignore empty fields
							if ($options[$identifiers[$i]][0] == '') {
								break;
							}
							if (!preg_match('/' . $this->meta['config_checks'][$scope][$identifiers[$i]]['regex'] . '/', $options[$identifiers[$i]][0])) {
								$messages[] = $this->meta['config_checks'][$scope][$identifiers[$i]]['error_message'];
							}
							break;
						// check by integer comparison (greater)
						case "int_greater":
							// ignore if both fields are empty
							if (($options[$this->meta['config_checks'][$scope][$identifiers[$i]]['cmp_name1']][0] == '') && ($options[$this->meta['config_checks'][$scope][$identifiers[$i]]['cmp_name2']][0] == '')) {
								break;
							}
							// print error message if only one field is empty
							if (($options[$this->meta['config_checks'][$scope][$identifiers[$i]]['cmp_name1']][0] == '') || ($options[$this->meta['config_checks'][$scope][$identifiers[$i]]['cmp_name2']][0] == '')) {
								$messages[] = $this->meta['config_checks'][$scope][$identifiers[$i]]['error_message'];
								break;
							}
							// compare
							if (intval($options[$this->meta['config_checks'][$scope][$identifiers[$i]]['cmp_name1']][0]) <= intval($options[$this->meta['config_checks'][$scope][$identifiers[$i]]['cmp_name2']][0])) {
								$messages[] = $this->meta['config_checks'][$scope][$identifiers[$i]]['error_message'];
							}
							break;
						// check by integer comparison (greater or equal)
						case "int_greaterOrEqual":
							// ignore if both fields are empty
							if (($options[$this->meta['config_checks'][$scope][$identifiers[$i]]['cmp_name1']][0] == '') && ($options[$this->meta['config_checks'][$scope][$identifiers[$i]]['cmp_name2']][0] == '')) {
								break;
							}
							// print error message if only one field is empty
							if (($options[$this->meta['config_checks'][$scope][$identifiers[$i]]['cmp_name1']][0] == '') || ($options[$this->meta['config_checks'][$scope][$identifiers[$i]]['cmp_name2']][0] == '')) {
								$messages[] = $this->meta['config_checks'][$scope][$identifiers[$i]]['error_message'];
								break;
							}
							// compare
							if (intval($options[$this->meta['config_checks'][$scope][$identifiers[$i]]['cmp_name1']][0]) < intval($options[$this->meta['config_checks'][$scope][$identifiers[$i]]['cmp_name2']][0])) {
								$messages[] = $this->meta['config_checks'][$scope][$identifiers[$i]]['error_message'];
							}
							break;
						// print error message on undefined type
						default:
							StatusMessage("ERROR", "Unsupported type!", $this->meta['config_checks'][$scope][$identifiers[$i]]['type']);
							break;
					}
				}
			}
		}
		return $messages;
	}

	/**
	 * Returns a list of config options for LAM's main configuration.
	 *
	 * @param array<string, array<string, string>> $currentSettings current settings
	 * @return htmlElement[] config options
	 */
	public function getGlobalConfigOptions(array $currentSettings): array {
		return [];
	}

	/**
	 * Checks the global config options.
	 *
	 * @param array<string, array<string, string>> $options config options
	 * @param string[] $messages info messages can be added here
	 * @param string[] $errors error messages can be added here
	 */
	public function checkGlobalConfigOptions(array &$options, array &$messages, array &$errors): void {
		// to be implemented by modules if needed
	}

	/**
	 * Specifies if the module supports global cron job actions.
	 *
	 * @return bool supports cron
	 */
	public function supportsGlobalCronJob(): bool {
		return false;
	}

	/**
	 * Runs any global cron actions.
	 *
	 * @param bool $isDryRun dry-run active
	 */
	public function runGlobalCronActions(bool $isDryRun): void {
		// needs to be implemented by submodule if needed
	}

	/**
	 * Returns a hashtable with all entries that may be printed out in the PDF.
	 *
	 * @param string $typeId type id (user, group, host)
	 * @return array<string, string> PDF entries as key => label
	 *
	 * @see baseModule::get_metaData()
	 */
	public function get_pdfFields(string $typeId): array {
		return $this->meta['PDF_fields'] ?? [];
	}

	/**
	 * Returns the PDF entries for this module.
	 *
	 * @param string[] $pdfKeys list of PDF keys that are included in document
	 * @param string $typeId type id (user, group, host)
	 * @return array<string, PDFEntry[]> list of key => PDFEntry
	 */
	public function get_pdfEntries(array $pdfKeys, string $typeId): array {
		return [];
	}

	/**
	 * Adds a simple PDF entry to the given array.
	 *
	 * @param array<string, PDFEntry[]> $result result array (entry will be added here)
	 * @param string $name ID
	 * @param string $label label name
	 * @param string|null $attrName attribute name (default: =$name)
	 * @param string $delimiter delimiter if multiple attribute values exist (default: ", ")
	 */
	protected function addSimplePDFField(array &$result, string $name, string $label, ?string $attrName = null, string $delimiter = ', '): void {
		if ($attrName == null) {
			$attrName = $name;
		}
		$value = '';
		if (isset($this->attributes[strtolower($attrName)][0])) {
			$attrName = strtolower($attrName);
		}
		if (isset($this->attributes[$attrName][0])) {
			natcasesort($this->attributes[$attrName]);
			$value = implode($delimiter, $this->attributes[$attrName]);
			$value = trim($value);
		}
		$result[static::class . '_' . $name][] = new PDFLabelValue($label, $value);
	}

	/**
	 * Adds a simple PDF entry with the given key and value.
	 *
	 * @param array<string, PDFEntry[]> $result result array (entry will be added here)
	 * @param string $name ID
	 * @param string $label label name
	 * @param string|string[] $value value as string or array
	 * @param string $delimiter delimiter if value is array (default: ", ")
	 */
	public function addPDFKeyValue(&$result, $name, $label, $value, $delimiter = ', '): void {
		if (is_array($value)) {
			natcasesort($value);
			$value = implode($delimiter, $value);
		}
		$result[static::class . '_' . $name][] = new PDFLabelValue($label, $value);
	}

	/**
	 * Adds a table entry to the PDF.
	 *
	 * @param array<string, PDFEntry[]> $result result array (entry will be added here)
	 * @param string $name ID
	 * @param PDFTable $table table
	 */
	public function addPDFTable(array &$result, string $name, PDFTable $table): void {
		if (empty($table->rows)) {
			return;
		}
		$result[static::class . '_' . $name][] = $table;
	}

	/**
	 * Adds an image to the PDF.
	 *
	 * @param array<string, PDFEntry[]> $result result array (entry will be added here)
	 * @param string $attrName attribute name
	 */
	public function addPDFImage(array &$result, string $attrName): void {
		if (isset($this->attributes[$attrName]) && (count($this->attributes[$attrName]) > 0)) {
			$result[static::class . '_' . $attrName][] = new PDFImage($this->attributes[$attrName][0]);
		}
	}

	/**
	 * Returns an array containing all input columns for the file upload.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * This function returns an array which contains subarrays which represent an upload column.
	 * <b>Syntax of column arrays:</b>
	 * <br>
	 * <br> array(
	 * <br>  string: name,  // fixed non-translated name which is used as column name (should be of format: <module name>_<column name>)
	 * <br>  string: description,  // short descriptive name
	 * <br>  string: help,  // help ID
	 * <br>  string: example,  // example value
	 * <br>  string: values, // possible input values (optional)
	 * <br>  string: default, // default value (optional)
	 * <br>  boolean: required  // true, if user must set a value for this column
	 * <br>  boolean: unique // true if all values of this column must be different values (optional, default: "false")
	 * <br> )
	 *
	 * @param string[] $selectedModules list of selected account modules
	 * @param ConfiguredType $type account type
	 * @return array<string, string|bool>[] column list
	 *
	 * @see baseModule::get_metaData()
	 */
	public function get_uploadColumns(array $selectedModules, ConfiguredType $type): array {
		return $this->meta['upload_columns'] ?? [];
	}

	/**
	 * Returns a list of module names which must be processed in building the account before this module.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * The named modules may not be active, LAM will check this automatically.
	 *
	 * @return string[] list of module names
	 *
	 * @see baseModule::get_metaData()
	 */
	public function get_uploadPreDepends(): array {
		return $this->meta['upload_preDepends'] ?? [];
	}

	/**
	 * In this function the LDAP accounts are built.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * Returns an array which contains subarrays to generate StatusMessages if any errors occurred.
	 *
	 * @param array<int, string>[] $rawAccounts the user input data, contains one subarray for each account.
	 * @param array<string, int> $ids list of IDs for column position (e.g. "posixAccount_uid" => 5)
	 * @param array<string, string[]|string>[] $partialAccounts list of hash arrays (name => value) which are later added to LDAP
	 * @param string[] $selectedModules list of selected account modules
	 * @param ConfiguredType $type account type
	 * @return string[][] list of error messages if any
	 */
	public function build_uploadAccounts(array $rawAccounts, array $ids, array &$partialAccounts, array $selectedModules, ConfiguredType &$type): array {
		// must be implemented in submodules
		return [];
	}

	/**
	 * Maps simple upload fields directly to LDAP attribute values.
	 *
	 * @param array<int, string>[] $rawAccounts the user input data, contains one subarray for each account.
	 * @param array<string, int> $ids list of IDs for column position (e.g. "posixAccount_uid" => 5)
	 * @param array<string, string[]|string>[] $partialAccounts list of hash arrays (name => value) which are later added to LDAP
	 * @param int $position current position in CSV
	 * @param string $colName column name
	 * @param string $attrName LDAP attribute name
	 * @param string|string[] $regex for get_preg() (e.g. 'ascii')
	 * @param string[] $message error message to add if regex does not match
	 * @param array<string|string[]>[] $errors list of error messages if any
	 * @param string $regexSplit multiple values are separated and can be split with this preg_split expression (e.g. "/;[ ]?/")
	 */
	protected function mapSimpleUploadField(&$rawAccounts, &$ids, &$partialAccounts, $position, $colName, $attrName, $regex = null, $message = [], &$errors = [], $regexSplit = null): void {
		if (!isset($ids[$colName])) {
			return;
		}
		if (!empty($rawAccounts[$position][$ids[$colName]])) {
			$regexIDs = is_array($regex) ? $regex : [$regex];
			// single value
			if ($regexSplit == null) {
				if (!empty($regex)) {
					$this->checkUploadRegex($regexIDs, $rawAccounts[$position][$ids[$colName]], $message, $position, $errors);
				}
				$partialAccounts[$position][$attrName] = [trim($rawAccounts[$position][$ids[$colName]])];
			}
			// multi-value
			else {
				$list = preg_split($regexSplit, trim($rawAccounts[$position][$ids[$colName]]));
				$partialAccounts[$position][$attrName] = $list;
				if (!empty($regex)) {
					for ($x = 0; $x < count($list); $x++) {
						if (!$this->checkUploadRegex($regexIDs, $list[$x], $message, $position, $errors)) {
							break;
						}
					}
				}
			}
		}
	}

	/**
	 * Checks the upload value against a list of regular expressions.
	 *
	 * @param string[] $regexIDs regular expression IDs for get_preg()
	 * @param string $value value to check
	 * @param string[] $message error message array if not matching
	 * @param int $position upload position
	 * @param array<string|string[]>[] $errors error messages
	 * @return bool value is ok
	 * @see get_preg()
	 */
	private function checkUploadRegex($regexIDs, $value, $message, $position, &$errors) {
		$matched = false;
		foreach ($regexIDs as $regexID) {
			if (get_preg($value, $regexID)) {
				$matched = true;
				break;
			}
		}
		if (!$matched) {
			$errMsg = $message;
			$errMsg[] = [(string) $position];
			$errors[] = $errMsg;
			return false;
		}
		return true;
	}

	/**
	 * This function returns the help entry array for a specific help id.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * The result is an hashtable with the following keys:<br>
	 * <ul>
	 * <li><b>Headline (required)</b><br>
	 * The headline of this help entry. Can consist of any alphanumeric characters. No HTML/CSS elements are allowed.</li>
	 * <li><b>Text (required)</b><br>
	 * The text of the help entry which may contain any alphanumeric characters.</li>
	 * <li><b>SeeAlso (optional)</b><br>
	 * A reference to another related web site. It must be an array containing a field called "text" with the link text
	 * that should be displayed and a field called "link" which is the link target.</li>
	 * </ul>
	 * <br>
	 * <b>Example:</b><br>
	 * <br>
	 * array('Headline' => 'This is the head line', 'Text' => 'Help content', 'SeeAlso' => array('text' => 'LAM homepage', 'link' => 'http://www.ldap-account-manager.org/'))
	 *
	 * @param string $id The id string for the help entry needed.
	 * @return array<string, string>|null The desired help entry.
	 *
	 * @see baseModule::get_metaData()
	 */
	public function get_help(string $id): ?array {
		if (isset($this->meta['help'][$id])) {
			return $this->meta['help'][$id];
		}
		elseif (isset($this->meta['help'][$this->scope][$id])) {
			return $this->meta['help'][$this->scope][$id];
		}
		else {
			return null;
		}
	}

	/**
	 * This function is used to check if this module page can be displayed.
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * Your module might depend on input of other modules. This function determines if the user
	 * can change to your module page or not. The return value is true if your module accepts
	 * input, otherwise false.<br>
	 * This method's return value defaults to true.
	 *
	 * @return bool true, if page can be displayed
	 */
	public function module_ready() {
		return true;
	}

	/**
	 * This function is used to check if all settings for this module have been made.
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * This function tells LAM if it can create/modify the LDAP account. If your module needs any
	 * additional input then set this to false. The user will be notified that your module needs
	 * more input.<br>
	 * This method's return value defaults to true.
	 *
	 * @return bool true, if settings are complete
	 */
	public function module_complete(): bool {
		return true;
	}

	/**
	 * Controls if the module button the account page is visible and activated.
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * <b>Possible return values:</b>
	 * <ul>
	 * <li><b>enabled:</b> button is visible and active</li>
	 * <li><b>disabled:</b> button is visible and deactivated (greyed)</li>
	 * <li><b>hidden:</b> no button will be shown</li>
	 * </ul>
	 *
	 * @return string status ("enabled", "disabled", "hidden")
	 */
	public function getButtonStatus(): string {
		return "enabled";
	}

	/**
	 * Runs any actions that need to be done before an LDAP entry is created.
	 *
	 * @param array<string, string[]|string> $attributes LDAP attributes of this entry (attributes are provided as reference, handle modifications of $attributes with care)
	 * @param ConfiguredType $type account type
	 * @return array<string|string[]>[] array which contains status messages. Each entry is an array containing the status message parameters.
	 */
	public function doUploadPreActions($attributes, $type) {
		return [];
	}

	/**
	 * This function is responsible to do additional tasks after the account has been created in LDAP (e.g. modifying group memberships, adding Quota etc..).
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * This function is called as long as the returned status is 'finished'. Please make sure
	 * that one function call lasts no longer than 3-4 seconds. Otherwise the upload may fail
	 * because the time limit is exceeded. You should not make more than one LDAP operation in
	 * each call.
	 *
	 * @param array<int, array<int, string>> $data array containing one account in each element
	 * @param array<string, int> $ids maps the column names to keys for the sub arrays (array(<column_name> => <column number>))
	 * @param int[] $failed list of account numbers which could not be successfully uploaded to LDAP
	 * @param array<string, int|string|string[]|string[][]|array<string, string[]>|mixed> $temp variable to store temporary data between two post actions
	 * @param array<string, string[]|string>[] $accounts list of LDAP entries
	 * @param string[] $selectedModules selected account modules
	 * @param ConfiguredType $type account type
	 * @return array<string, string|int|string[][]|htmlStatusMessage[]> current status
	 * <br> array (
	 * <br>  'status' => 'finished' | 'inProgress' // defines if all operations are complete
	 * <br>  'progress' => 0..100 // the progress of the operations in percent
	 * <br>  'errors' => array // list of arrays which are used to generate StatusMessages
	 * <br> )
	 */
	public function doUploadPostActions(array &$data, array $ids, array $failed, array &$temp, array &$accounts,
										array $selectedModules, ConfiguredType $type): array {
		return [
			'status' => 'finished',
			'progress' => 100,
			'errors' => []
		];
	}

	/**
	 * Returns a list of modifications which have to be made to the LDAP account.
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 *
	 * <br>This function returns an array with 3 entries:
	 * <br>array( DN1 ('add' => array($attr), 'remove' => array($attr), 'modify' => array($attr)), DN2 .... )
	 * <br>DN is the DN to change. It is possible to change several DNs (e.g. create a new user and add him
	 * to some groups via attribute memberUid)<br>
	 * <br><b>"add"</b> are attributes which have to be added to the LDAP entry
	 * <br><b>"remove"</b> are attributes which have to be removed from the LDAP entry
	 * <br><b>"modify"</b> are attributes which have to be modified in the LDAP entry
	 * <br><b>"notchanged"</b> are attributes which stay unchanged
	 * <br><b>"info"</b> values with informational value (e.g. to be used later by pre/postModify actions)
	 * <br>
	 * <br>This builds the required commands from $this-attributes and $this->orig.
	 *
	 * @return array<string, array<string, array<string, string[]>>> list of modifications
	 */
	public function save_attributes(): array {
		$this->getAccountContainer()->replaceWildcardsInArray($this->getWildcardTargetAttributeNames(), $this->attributes);
		return $this->getAccountContainer()->save_module_attributes($this->attributes, $this->orig);
	}

	/**
	 * Allows the module to run commands before the LDAP entry is changed or created.
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * The modification is aborted if an error message is returned.
	 *
	 * @param boolean $newAccount new account
	 * @param array<string, string[]|string> $attributes LDAP attributes of this entry
	 * @return array<string|string[]>[] array which contains status messages. Each entry is an array containing the status message parameters.
	 */
	public function preModifyActions(bool $newAccount, array $attributes): array {
		return [];
	}

	/**
	 * Allows the module to run commands after the LDAP entry is changed or created.
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.
	 *
	 * @param bool $newAccount new account
	 * @param array<string, string[]|string> $attributes LDAP attributes of this entry
	 * @return string[][] array which contains status messages. Each entry is an array containing the status message parameters.
	 */
	public function postModifyActions(bool $newAccount, array $attributes): array {
		return [];
	}

	/**
	 * Allows the module to run commands before the LDAP entry is deleted.
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.<br>
	 *
	 * @return string[][] array which contains status messages. Each entry is an array containing the status message parameters.
	 */
	public function preDeleteActions(): array {
		return [];
	}

	/**
	 * Allows the module to run commands after the LDAP entry is deleted.
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.
	 *
	 * @return array<string|string[]>[] array which contains status messages. Each entry is an array containing the status message parameters.
	 */
	public function postDeleteActions(): array {
		return [];
	}

	/**
	 * This function returns an array with the same syntax as save_attributes().
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * It allows additional LDAP changes when an account is deleted.
	 *
	 * @return array<string, array<string, array<string|string[]>>> of LDAP operations, same as for save_attributes()
	 */
	public function delete_attributes(): array {
		return [];
	}

	/**
	 * This function creates meta HTML code which will be displayed when an account should be deleted.
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * This can be used to interact with the user, e.g. should the home directory be deleted? The output
	 * of all modules is displayed on a single page.
	 *
	 * @return htmlElement|null meta HTML object
	 * @see htmlElement
	 */
	public function display_html_delete() {
		return null;
	}

	/**
	 * Defines if the LDAP entry has only virtual child entries. This is the case for e.g. LDAP views.
	 *
	 * @return boolean has only virtual children
	 */
	public function hasOnlyVirtualChildren() {
		return false;
	}

	/**
	 * This function processes user input of the primary module page.
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * It checks the user input and saves changes in the module's data structures.<br>
	 * <br>
	 * <b>Example:</b> return [[('ERROR', 'Invalid input!', 'This is not allowed here.']];
	 *
	 * @return string[][] Array which contains status messages. Each entry is an array containing the status message parameters.
	 */
	abstract public function process_attributes(): array;

	/**
	 * This function creates meta HTML code to display the module page.
	 *
	 * Calling this method requires the existence of an enclosing {@link accountContainer}.
	 *
	 * @return htmlElement|htmlElement[] meta HTML object
	 *
	 * @see htmlElement
	 */
	abstract public function display_html_attributes();

	/**
	 * Adds a simple text input field to the given htmlResponsiveRow.
	 * The field name will be the same as the attribute name. There must also be a help entry with the attribute name as ID.
	 * A new line will also be added after this entry so multiple calls will show the fields one below the other.
	 *
	 * @param htmlResponsiveRow $container parent container
	 * @param string $attrName attribute name
	 * @param string $label label name
	 * @param bool $required this is a required field (default false)
	 * @param int $length field length
	 * @param bool $isTextArea show as text area (default false)
	 * @param string[]|null $autoCompleteValues values for auto-completion
	 * @return htmlResponsiveInputField|htmlResponsiveInputTextarea reference to htmlResponsiveInputField/htmlResponsiveInputTextarea
	 */
	protected function &addSimpleInputTextField(&$container, $attrName, $label, $required = false, $length = null, $isTextArea = false, $autoCompleteValues = null) {
		$value = '';
		if (isset($this->attributes[$attrName][0])) {
			$value = $this->attributes[$attrName][0];
		}
		if ($isTextArea) {
			$cols = 30;
			if ($length != null) {
				$cols = $length;
			}
			$input = new htmlResponsiveInputTextarea($attrName, $value, $cols, 3, $label, $attrName);
		}
		else {
			$input = new htmlResponsiveInputField($label, $attrName, $value, $attrName);
			if ($length != null) {
				$input->setFieldSize($length);
			}
			if (!empty($autoCompleteValues)) {
				$input->enableAutocompletion($autoCompleteValues);
			}
		}
		$input->setRequired($required);
		$container->add($input);
		return $input;
	}

	/**
	 * Adds a simple read-only field to the given container.
	 *
	 * @param htmlResponsiveRow $container parent container
	 * @param string $attrName attribute name
	 * @param string $label field label
	 */
	protected function addSimpleReadOnlyField(&$container, $attrName, $label): void {
		$val = '&nbsp;';
		if (!empty($this->attributes[$attrName][0])) {
			$values = $this->attributes[$attrName];
			array_map(htmlspecialchars(...), $values);
			$val = implode('<br>', $values);
		}
		$labelBox = new htmlOutputText($label);
		if (!empty($this->attributes[$attrName]) && (count($this->attributes[$attrName]) > 1)) {
			$labelBox->alignment = htmlElement::ALIGN_TOP;
		}
		$container->addLabel($labelBox);
		$container->addField(new htmlOutputText($val, false));
	}

	/**
	 * Validates a text field.
	 * If validation is used then there must exist a message named [{attribute name}][0] (e.g. $this->messages['street'][0]).
	 *
	 * @param string $attrName attribute name
	 * @param array<string|string[]>[] $errors errors array where to put validation errors
	 * @param bool $required value required
	 * @param string $validationID validation ID for function get_preg() (default: null, null means no validation)
	 */
	protected function processSimpleTextInput($attrName, &$errors, $required = false, $validationID = null): void {
		if ((!isset($_POST[$attrName]) || ($_POST[$attrName] === '')) && isset($this->attributes[$attrName][0])) {
			unset($this->attributes[$attrName][0]);
			if ($required) {
				$errors[] = $this->messages[$attrName][0];
			}
			return;
		}
		$this->attributes[$attrName][0] = $_POST[$attrName] ?? '';
		if ($required && (!isset($_POST[$attrName]) || ($_POST[$attrName] === ''))) {
			$errors[] = $this->messages[$attrName][0];
		}
		elseif (($validationID !== null)
			&& (isset($_POST[$attrName]) && ($_POST[$attrName] !== ''))
			&& !get_preg($_POST[$attrName], $validationID)) {
			$errors[] = $this->messages[$attrName][0];
		}
	}

	/**
	 * Adds a text input field that may contain multiple values to the given htmlResponsiveRow.
	 * The field name will be the same as the attribute name plus a counting number (e.g. street_0).
	 * The last field will be followed by a button to add a new value. This is named add_{attribute name} (e.g. add_street).
	 * There must be a help entry with the attribute name as ID.
	 * A new line will also be added after this entry so multiple calls will show the fields one below the other.
	 *
	 * @param htmlResponsiveRow $container parent container
	 * @param string $attrName attribute name
	 * @param string|null $label label name
	 * @param bool $required this is a required field (default false)
	 * @param int|null $length field length
	 * @param bool $isTextArea show as text area (default false)
	 * @param string[]|null $autoCompleteValues values for auto-completion
	 * @param integer $fieldSize field size
	 * @param string[] $htmlIDs reference to array where to add the generated HTML IDs of the input fields
	 * @param string $cssClasses additional CSS classes of input fields
	 */
	protected function addMultiValueInputTextField(&$container, $attrName, $label, $required = false, $length = null, $isTextArea = false,
												   $autoCompleteValues = null, $fieldSize = null, &$htmlIDs = null, $cssClasses = ''): void {
		$values = [];
		if (isset($this->attributes[$attrName][0])) {
			$values = $this->attributes[$attrName];
		}
		if (count($values) === 0) {
			$values[] = '';
		}
		natcasesort($values);
		$first = array_shift($values);
		// shift empty value to the end
		if ($first === '') {
			$values[] = $first;
		}
		else {
			array_unshift($values, $first);
		}
		$values = array_values($values);
		if ($label !== null) {
			$labelTextOut = new htmlOutputText($label, true, $required);
			$labelTextOut->alignment = htmlElement::ALIGN_TOP;
			$container->addLabel($labelTextOut);
		}
		$help = new htmlHelpLink($attrName);
		$help->alignment = htmlElement::ALIGN_TOP;
		$subContainer = new htmlTable();
		$subContainer->setCSSClasses(['fullwidth']);
		$subContainer->alignment = htmlElement::ALIGN_TOP;
		for ($i = 0; $i < count($values); $i++) {
			if (!$isTextArea) {
				$input = new htmlInputField($attrName . '_' . $i, $values[$i]);
				$input->setAccessibilityLabel($label);
				if (!empty($cssClasses)) {
					$input->setCSSClasses([$cssClasses]);
				}
				if (!empty($length)) {
					$input->setFieldMaxLength($length);
				}
				if (!empty($fieldSize)) {
					$input->setFieldSize($fieldSize);
				}
				if (!empty($autoCompleteValues)) {
					$input->enableAutocompletion($autoCompleteValues);
				}
				$subContainer->addElement($input);
			}
			else {
				$cols = 30;
				if ($length != null) {
					$cols = $length;
				}
				$textArea = new htmlInputTextarea($attrName . '_' . $i, $values[$i], $cols, 3);
				if (!empty($cssClasses)) {
					$textArea->setCSSClasses([$cssClasses]);
				}
				$subContainer->addElement($textArea);
			}
			if (!empty($htmlIDs)) {
				$htmlIDs[] = $attrName . '_' . $i;
			}
			if (!empty($values[$i])) {
				$delButton = new htmlButton('del_' . $attrName . '_' . $i, 'del.svg', true);
				$delButton->setCSSClasses(['noMarginSides']);
				$delButton->setTitle(_('Delete'));
				$subContainer->addElement($delButton);
			}
			if ($i === 0) {
				$addButton = new htmlButton('add_' . $attrName, 'add.svg', true);
				$addButton->setCSSClasses(['noMarginSides']);
				$addButton->setTitle(_('Add'));
				$subContainer->addElement($addButton);
				$subContainer->addElement($help);
			}
			$subContainer->addNewLine();
		}
		if ($label !== null) {
			$container->addField($subContainer);
		}
		else {
			$container->add($subContainer);
		}
	}

	/**
	 * Validates a multi-value text field.
	 * The input fields must be created with function addMultiValueInputTextField().
	 * If validation is used then there must exist a message named [{attribute name}][0] (e.g. $this->messages['street'][0]).
	 *
	 * @param string $attrName attribute name
	 * @param array<string|string[]>[] $errors errors array where to put validation errors
	 * @param string|null $validationID validation ID for function get_preg() (default: null, null means no validation)
	 * @param bool $required the field is required (default: false)
	 */
	protected function processMultiValueInputTextField($attrName, &$errors, $validationID = null, $required = false): void {
		$counter = 0;
		while (isset($_POST[$attrName . '_' . $counter])) {
			$this->attributes[$attrName][$counter] = trim($_POST[$attrName . '_' . $counter]);
			if (($this->attributes[$attrName][$counter] === '') || isset($_POST['del_' . $attrName . '_' . $counter])) {
				unset($this->attributes[$attrName][$counter]);
			}
			elseif (($validationID !== null) && !get_preg($this->attributes[$attrName][$counter], $validationID)) {
				$msg = $this->messages[$attrName][0];
				if (count($msg) < 3) {
					$msg[] = htmlspecialchars($this->attributes[$attrName][$counter]);
				}
				else {
					$msg[] = [htmlspecialchars($this->attributes[$attrName][$counter])];
				}
				$errors[] = $msg;
			}
			$counter++;
		}
		if (isset($_POST['add_' . $attrName])) {
			$this->attributes[$attrName][] = '';
		}
		if (!empty($this->attributes[$attrName])) {
			$this->attributes[$attrName] = array_values(array_unique($this->attributes[$attrName]));
		}
		if ($required && empty($this->attributes[$attrName])) {
			$errors[] = $this->messages[$attrName][0];
		}
	}

	/**
	 * Adds a select field type that may contain multiple values to the given htmlTable.
	 * The field name will be the same as the attribute name plus a counting number (e.g. street_0).
	 * The last field will be followed by a button to add a new value. This is named add_{attribute name} (e.g. add_street).
	 * There must be a help entry with the attribute name as ID.
	 * A new line will also be added after this entry so multiple calls will show the fields one below the other.
	 *
	 * @param htmlResponsiveRow $container parent container
	 * @param string $attrName attribute name
	 * @param string|null $label label name
	 * @param string[]|array<string, string>|array<string, string[]|array<string, string>> $options options for the selects
	 * @param bool $hasDescriptiveOptions has descriptive options
	 * @param bool $required this is a required field (default false)
	 * @param integer $fieldSize field size
	 * @param string[] $htmlIDs reference to array where to add the generated HTML IDs of the input fields
	 */
	protected function addMultiValueSelectField(&$container, $attrName, $label, $options, $hasDescriptiveOptions = false,
												$required = false, $fieldSize = 1, &$htmlIDs = null): void {
		$values = [];
		if (isset($this->attributes[$attrName][0])) {
			$values = $this->attributes[$attrName];
		}
		if (count($values) === 0) {
			$values[] = '';
		}
		natcasesort($values);
		$values = array_values($values);
		if ($label !== null) {
			$labelTextOut = new htmlOutputText($label);
			$labelTextOut->setMarkAsRequired($required);
			$container->addLabel($labelTextOut);
		}
		$subContainer = new htmlTable();
		$subContainer->alignment = htmlElement::ALIGN_TOP;
		for ($i = 0; $i < count($values); $i++) {
			$input = new htmlSelect($attrName . '_' . $i, $options, [$values[$i]], $fieldSize);
			$input->setHasDescriptiveElements($hasDescriptiveOptions);
			$subContainer->addElement($input);
			if (!empty($htmlIDs)) {
				$htmlIDs[] = $attrName . '_' . $i;
			}
			if (!empty($values[$i])) {
				$subContainer->addElement(new htmlButton('del_' . $attrName . '_' . $i, 'del.svg', true));
			}
			if ($i === 0) {
				$subContainer->addElement(new htmlButton('add_' . $attrName, 'add.svg', true));
				$subContainer->addElement(new htmlHelpLink($attrName));
			}
			$subContainer->addNewLine();
		}
		$container->addField($subContainer);
	}

	/**
	 * Validates a multi-value select field.
	 * The select fields must be created with function addMultiValueSelectField().
	 *
	 * @param string $attrName attribute name
	 */
	protected function processMultiValueSelectField(string $attrName): void {
		$counter = 0;
		while (isset($_POST[$attrName . '_' . $counter])) {
			$this->attributes[$attrName][$counter] = trim($_POST[$attrName . '_' . $counter]);
			if (($this->attributes[$attrName][$counter] === '') || isset($_POST['del_' . $attrName . '_' . $counter])) {
				unset($this->attributes[$attrName][$counter]);
			}
			$counter++;
		}
		if (isset($_POST['add_' . $attrName])) {
			$this->attributes[$attrName][] = '';
		}
		$this->attributes[$attrName] = array_values(array_unique($this->attributes[$attrName]));
	}

	/**
	 * Adds an area with two multi-select fields with buttons to move items from right to left and vice-versa.
	 * The options of the selects must be presorted.
	 * <br>Names:
	 * <ul>
	 * <li>First select: $namePrefix_1
	 * <li>Second select: $namePrefix_2
	 * <li>Button move left: $namePrefix_left
	 * <li>Button move right: $namePrefix_right
	 * </ul>
	 *
	 * @param htmlResponsiveRow $container row
	 * @param string $labelFirst label of first select
	 * @param string $labelSecond label of second select
	 * @param string[] $optionsFirst options of first select ('label' => 'value')
	 * @param string[] $selectedFirst selected options of first select
	 * @param string[] $optionsSecond options of first select ('label' => 'value')
	 * @param string[] $selectedSecond selected options of second select
	 * @param string $namePrefix prefix for select field and button names
	 * @param bool $rightToLeftText sets the text direction in select to right to left
	 * @param bool $showFilter displays a live filter
	 */
	protected function addDoubleSelectionArea(&$container, $labelFirst, $labelSecond, $optionsFirst, $selectedFirst,
											  $optionsSecond, $selectedSecond, $namePrefix, $rightToLeftText = false, $showFilter = false): void {
		$forceTablet = false;
		foreach ($optionsFirst as $label => $value) {
			if (mb_strlen($label) > 75) {
				$forceTablet = true;
				break;
			}
		}
		if (!$forceTablet) {
			foreach ($optionsSecond as $label => $value) {
				if (mb_strlen($label) > 75) {
					$forceTablet = true;
					break;
				}
			}
		}
		// first select
		$firstRow = new htmlResponsiveRow();
		$firstRow->add(new htmlOutputText($labelFirst));
		$firstSelect = new htmlSelect($namePrefix . '_1', $optionsFirst, $selectedFirst, 15);
		$firstSelect->setHasDescriptiveElements(true);
		$firstSelect->setMultiSelect(true);
		$firstSelect->setRightToLeftTextDirection($rightToLeftText);
		$firstSelect->setSortElements(false);
		$firstSelect->enableDynamicScrolling();
		$firstRow->add($firstSelect);
		if ($showFilter) {
			$firstFilterGroup = new htmlGroup();
			$firstFilterGroup->addElement(new htmlOutputText(_('Filter')));
			$firstFilterInput = new htmlInputField($namePrefix . '_filterFirst');
			$firstFilterInput->filterSelectBox($namePrefix . '_1');
			$firstFilterGroup->addElement($firstFilterInput);
			$firstRow->add($firstFilterGroup);
		}
		$firstContainerDesktop = ($forceTablet) ? 12 : 5;
		$container->add($firstRow, 12, 12, $firstContainerDesktop);
		// buttons
		$buttonActiveDesktop = ($forceTablet) ? 0 : 12;
		$buttonInactiveDesktop = ($forceTablet) ? 6 : 0;
		$buttonRow = new htmlResponsiveRow();
		$buttonRow->setCSSClasses(['text-center']);
		$buttonRow->add(new htmlSpacer(null, '1rem'), 0, 0, $buttonActiveDesktop);
		$buttonRow->add(new htmlButton($namePrefix . '_left', 'go-previous.svg', true), 0, 0, $buttonActiveDesktop);
		$buttonRow->add(new htmlButton($namePrefix . '_left', 'up.svg', true), 6, 6, $buttonInactiveDesktop);
		$buttonRow->add(new htmlButton($namePrefix . '_right', 'go-next.svg', true), 0, 0, $buttonActiveDesktop);
		$buttonRow->add(new htmlButton($namePrefix . '_right', 'down.svg', true), 6, 6, $buttonInactiveDesktop);
		$buttonRowDesktop = ($forceTablet) ? 12 : 1;
		$container->add($buttonRow, 12, 12, $buttonRowDesktop);
		// second select
		$secondRow = new htmlResponsiveRow();
		$secondRow->add(new htmlOutputText($labelSecond));
		$secondSelect = new htmlSelect($namePrefix . '_2', $optionsSecond, $selectedSecond, 15);
		$secondSelect->setHasDescriptiveElements(true);
		$secondSelect->setMultiSelect(true);
		$secondSelect->setRightToLeftTextDirection($rightToLeftText);
		$secondSelect->setSortElements(false);
		$secondSelect->enableDynamicScrolling();
		$secondRow->add($secondSelect);
		if ($showFilter) {
			$secondFilterGroup = new htmlGroup();
			$secondFilterGroup->addElement(new htmlOutputText(_('Filter')));
			$secondFilterInput = new htmlInputField($namePrefix . '_filterSecond');
			$secondFilterInput->filterSelectBox($namePrefix . '_2');
			$secondFilterGroup->addElement($secondFilterInput);
			$secondRow->add($secondFilterGroup);
		}
		$secondContainerDesktop = ($forceTablet) ? 12 : 6;
		$container->add($secondRow, 12, 12, $secondContainerDesktop);
	}

	/**
	 * Adds a simple text input field for the self-service.
	 * The field name will be the same as the class name plus "_" plus attribute name (e.g. posixAccount_cn).
	 *
	 * @param array<string, htmlResponsiveRow> $container array that is used as return value for getSelfServiceOptions()
	 * @param string $name attribute name (== field name)
	 * @param string $label label to display in front of input field
	 * @param string[] $fields list of active fields
	 * @param array<string, string[]|string> $attributes attributes of LDAP account
	 * @param string[] $readOnlyFields list of read-only fields
	 * @param bool $required field is required
	 * @param bool $isTextArea display as text area
	 * @param string|null $attributeName attribute name (defaults to $name)
	 */
	protected function addSimpleSelfServiceTextField(&$container, $name, $label, &$fields, &$attributes, &$readOnlyFields, $required = false, $isTextArea = false, $attributeName = null): void {
		if (!in_array($name, $fields)) {
			return;
		}
		$ldapAttrName = $attributeName ?? $name;
		$value = '';
		if (isset($attributes[$ldapAttrName][0])) {
			$value = $attributes[$ldapAttrName][0];
		}
		if (!$isTextArea && !in_array($name, $readOnlyFields)) {
			$field = new htmlInputField(static::class . '_' . $name, $value);
			$field->setRequired($required);
			$field->setFieldSize(null);
		}
		elseif ($isTextArea && !in_array($name, $readOnlyFields)) {
			$field = new htmlInputTextarea(static::class . '_' . $name, $value, 100, 3);
		}
		elseif (!$isTextArea) {
			$field = new htmlOutputText($value);
		}
		else {
			$value = htmlspecialchars($value);
			$value = str_replace("\n", '<br>', $value);
			$field = new htmlOutputText($value, false);
		}
		$row = new htmlResponsiveRow();
		$fieldLabel = new htmlLabel(static::class . '_' . $name, $this->getSelfServiceLabel($name, $label));
		$fieldLabel->setMarkAsRequired($required && !in_array($name, $readOnlyFields));
		$row->addLabel($fieldLabel);
		$row->addField($field);
		$container[$name] = $row;
	}

	/**
	 * Checks the input value of a self-service text field.
	 * The field name must be the same as the class name plus "_" plus attribute name (e.g. posixAccount_cn).
	 * If validation is used then there must exist a message named [{attribute name}][0] (e.g. $this->messages['street'][0]).
	 *
	 * @param array<string, array<string|string[]>[]|array<string, string[]|string>> $container return value of checkSelfServiceOptions()
	 * @param string $name attribute name
	 * @param array<string, string[]|string> $attributes LDAP attributes
	 * @param string[] $fields input fields
	 * @param string[] $readOnlyFields list of read-only fields
	 * @param string $validationID validation ID for get_preg()
	 * @param array<string|string[]> $validationMessage validation message data (defaults to $this->messages[$name][0])
	 * @param array<string|string[]> $requiredMessage message data when no value is set by user (no check if null)
	 * @param string $attributeName attribute name (defaults to $name)
	 */
	protected function checkSimpleSelfServiceTextField(&$container, $name, &$attributes, $fields, &$readOnlyFields,
													   $validationID = null, $validationMessage = null, $requiredMessage = null, $attributeName = null): void {
		if (in_array($name, $fields) && !in_array($name, $readOnlyFields)) {
			$fieldName = static::class . '_' . $name;
			$ldapAttrName = $attributeName ?? $name;
			if (!empty($_POST[$fieldName])) {
				if (($validationID !== null) && !get_preg($_POST[$fieldName], $validationID)) {
					$container['messages'][] = $validationMessage ?? $this->messages[$name][0];
				}
				elseif (isset($attributes[$ldapAttrName]) && ($attributes[$ldapAttrName][0] != $_POST[$fieldName])) {
					$container['mod'][$ldapAttrName] = [(string) $_POST[$fieldName]];
				}
				elseif (!isset($attributes[$ldapAttrName])) {
					$container['add'][$ldapAttrName] = [(string) $_POST[$fieldName]];
				}
			}
			elseif ($requiredMessage !== null) {
				$container['messages'][] = $requiredMessage;
			}
			elseif (isset($attributes[$ldapAttrName])) {
				$container['del'][$ldapAttrName] = $attributes[$ldapAttrName];
			}
		}
	}

	/**
	 * Adds a simple text input field for the self-service.
	 * The field name will be the same as the class name plus "_" plus attribute name (e.g. posixAccount_cn).
	 *
	 * @param array<string, htmlResponsiveRow> $container array that is used as return value for getSelfServiceOptions()
	 * @param string $name attribute name (== field name)
	 * @param string $label label to display in front of input field
	 * @param string[] $fields list of active fields
	 * @param array<string, string[]|string> $attributes attributes of LDAP account
	 * @param string[] $readOnlyFields list of read-only fields
	 * @param bool $required field is required
	 * @param bool $isTextArea display as text area
	 * @param string $attributeName attribute name (defaults to $name)
	 */
	protected function addMultiValueSelfServiceTextField(&$container, $name, $label, &$fields, &$attributes,
														 &$readOnlyFields, $required = false, $isTextArea = false, $attributeName = null): void {
		if (!in_array($name, $fields)) {
			return;
		}
		$ldapAttrName = $attributeName ?? $name;
		$values = [];
		if (isset($attributes[$ldapAttrName][0])) {
			$values = $attributes[$ldapAttrName];
		}
		$readOnly = in_array($name, $readOnlyFields);
		$field = new htmlResponsiveRow();
		$fieldNamePrefix = static::class . '_' . $name . '_';
		if (!$readOnly) {
			if (empty($values)) {
				$values[] = '';
			}
			for ($i = 0; $i < count($values); $i++) {
				$fieldRow = new htmlResponsiveRow();
				$value = $values[$i];
				if (!$isTextArea) {
					$inputField = new htmlInputField($fieldNamePrefix . $i, $value);
					$inputField->setRequired($required);
					$inputField->setFieldSize(null);
				}
				else {
					$inputField = new htmlInputTextarea($fieldNamePrefix . $i, $value, 100, 3);
				}
				$inputField->setAccessibilityLabel($this->getSelfServiceLabel($name, $label));
				$fieldRow->add($inputField, 9, 10);
				$linkGroup = new htmlGroup();
				if ($value !== '') {
					$delLink = new htmlLink(null, '#', '../../graphics/del.svg');
					$delLink->setOnClick('window.lam.selfservice.delMultiValue(\'' . $fieldNamePrefix . '\', this); return false;');
					$delLink->setCSSClasses(['del-link icon']);
					$delLink->setTitle(_('Delete'));
					$linkGroup->addElement($delLink);
				}
				if ($i === (count($values) - 1)) {
					$addLink = new htmlLink(null, '#', '../../graphics/add.svg');
					$addLink->setOnClick('window.lam.selfservice.addMultiValue(\'' . $fieldNamePrefix . '\', this); return false;');
					$addLink->setCSSClasses(['add-link icon', 'margin-left5']);
					$addLink->setTitle(_('Add'));
					$linkGroup->addElement($addLink);
				}
				$fieldRow->add($linkGroup, 3, 2);
				$field->add($fieldRow);
			}
		}
		else {
			foreach ($values as $value) {
				if (!$isTextArea) {
					$inputField = new htmlOutputText($value);
				}
				else {
					$inputField = new htmlOutputText($value);
					$inputField->setPreformatted();
				}
				$field->add($inputField);
			}
		}
		$row = new htmlResponsiveRow();
		$fieldLabel = new htmlLabel($fieldNamePrefix . '0', $this->getSelfServiceLabel($name, $label));
		$fieldLabel->setMarkAsRequired($required);
		$row->addLabel($fieldLabel);
		$row->addField($field);
		$container[$name] = $row;
	}

	/**
	 * Checks the input value of a self-service multi-value text field.
	 * The field name must be the same as the class name plus "_" plus attribute name (e.g. posixAccount_cn).
	 * If validation is used then there must exist a message named [{attribute name}][0] (e.g. $this->messages['street'][0]).
	 *
	 * @param array<string, array<string|string[]>[]|array<string, string[]|string>> $container return value of checkSelfServiceOptions()
	 * @param string $name attribute name
	 * @param array<string, string[]|string> $attributes LDAP attributes
	 * @param string[] $fields input fields
	 * @param string[] $readOnlyFields list of read-only fields
	 * @param string $validationID validation ID for get_preg()
	 * @param array<string|string[]> $validationMessage validation message data (defaults to $this->messages[$name][0])
	 * @param array<string|string[]> $requiredMessage message data when no value is set by user (no check if null)
	 * @param string $attributeName attribute name (defaults to $name)
	 */
	protected function checkMultiValueSelfServiceTextField(&$container, $name, &$attributes, $fields, &$readOnlyFields,
														   $validationID = null, $validationMessage = null, $requiredMessage = null, $attributeName = null): void {
		if (in_array($name, $fields) && !in_array($name, $readOnlyFields)) {
			$fieldName = static::class . '_' . $name;
			$ldapAttrName = $attributeName ?? $name;
			$valuesNew = [];
			foreach ($_POST as $postKey => $postValue) {
				if (!str_contains($postKey, $fieldName)) {
					continue;
				}
				if ($postValue === '') {
					continue;
				}
				if (($validationID != null) && !get_preg($postValue, $validationID)) {
					$container['messages'][] = $validationMessage ?? $this->messages[$name][0];
					return;
				}
				$valuesNew[] = (string) $postValue;
			}
			if (empty($valuesNew) && ($requiredMessage !== null)) {
				$container['messages'][] = $requiredMessage;
			}
			$valuesOld = $attributes[$ldapAttrName] ?? [];
			if (!areArrayContentsEqual($valuesOld, $valuesNew)) {
				$container['mod'][$ldapAttrName] = $valuesNew;
			}
		}
	}

	/**
	 * Returns a list of managed object classes for this module.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * This is used to fix spelling errors in LDAP-Entries (e.g. if "posixACCOUNT" is read instead of "posixAccount" from LDAP).<br>
	 * <br>
	 * <b>Example:</b> return array('posixAccount')
	 *
	 * @param string $typeId type id (user, group, host)
	 * @return string[] list of object classes
	 *
	 * @see baseModule::get_metaData()
	 */
	public function getManagedObjectClasses(string $typeId): array {
		if (isset($this->meta['objectClasses']) && is_array($this->meta['objectClasses'])) {
			return $this->meta['objectClasses'];
		}
		return [];
	}

	/**
	 * Returns a list of aliases for LDAP attributes.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * All alias attributes will be renamed to the given attribute names.
	 *
	 * @param string $typeId type id (user, group, host)
	 * @return array<string, string> list of aliases like array("alias name" => "attribute name")
	 *
	 * @see baseModule::get_metaData()
	 */
	public function getLDAPAliases(string $typeId): array {
		if (isset($this->meta['LDAPaliases']) && is_array($this->meta['LDAPaliases'])) {
			return $this->meta['LDAPaliases'];
		}
		return [];
	}

	/**
	 * Returns a list of LDAP attributes which are managed by this module.
	 * All attribute names will be renamed to match the given spelling.
	 *
	 * @param string $typeId type id (user, group, host)
	 * @return string[] list of attributes
	 *
	 * @see baseModule::get_metaData()
	 */
	public function getManagedAttributes(string $typeId): array {
		if (isset($this->meta['attributes']) && is_array($this->meta['attributes'])) {
			return $this->meta['attributes'];
		}
		return [];
	}

	/**
	 * Returns a list of operational LDAP attributes which are managed by this module and need to be explicitly set for LDAP search.
	 *
	 * @param string $typeId account type id
	 * @return string[] list of hidden attributes
	 *
	 * @see baseModule::get_metaData()
	 */
	public function getManagedHiddenAttributes(string $typeId): array {
		if (isset($this->meta['hiddenAttributes']) && is_array($this->meta['hiddenAttributes'])) {
			return $this->meta['hiddenAttributes'];
		}
		return [];
	}

	/**
	 * This function returns a list of possible LDAP attributes (e.g. uid, cn, ...) which can be used to search for LDAP objects.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.
	 *
	 * @return string[] attributes
	 *
	 * @see baseModule::get_metaData()
	 */
	public function getSelfServiceSearchAttributes(): array {
		if (isset($this->meta['selfServiceSearchAttributes']) && is_array($this->meta['selfServiceSearchAttributes'])) {
			return $this->meta['selfServiceSearchAttributes'];
		}
		return [];
	}

	/**
	 * Returns a list of possible input fields and their descriptions.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * <b>Format:</b> array(<field identifier> => <field description>)
	 *
	 * @return array<string, string> fields
	 *
	 * @see baseModule::get_metaData()
	 */
	public function getSelfServiceFields(): array {
		if (isset($this->meta['selfServiceFieldSettings']) && is_array($this->meta['selfServiceFieldSettings'])) {
			return $this->meta['selfServiceFieldSettings'];
		}
		return [];
	}

	/**
	 * Returns if a given self-service field can be set in read-only mode.
	 *
	 * @param string $fieldID field identifier
	 * @param selfServiceProfile $profile currently edited profile
	 * @return bool may be set read-only
	 */
	public function canSelfServiceFieldBeReadOnly(string $fieldID, selfServiceProfile $profile): bool {
		if (isset($this->meta['selfServiceReadOnlyFields']) && is_array($this->meta['selfServiceReadOnlyFields'])) {
			return in_array($fieldID, $this->meta['selfServiceReadOnlyFields']);
		}
		return false;
	}

	/**
	 * Returns if a self-service field can be relabeled.
	 *
	 * @param string $fieldID field ID
	 * @param selfServiceProfile $profile currently edited profile
	 * @return bool may be relabeled
	 */
	public function canSelfServiceFieldBeRelabeled(string $fieldID, selfServiceProfile $profile): bool {
		if (isset($this->meta['selfServiceNoRelabelFields']) && is_array($this->meta['selfServiceNoRelabelFields'])) {
			return !in_array($fieldID, $this->meta['selfServiceNoRelabelFields']);
		}
		return true;
	}

	/**
	 * Returns the field label. This can be either the given default label or an override value from profile.
	 *
	 * @param string $fieldID field ID
	 * @param string $defaultLabel default label text
	 * @return string label
	 */
	protected function getSelfServiceLabel(string $fieldID, string $defaultLabel): string {
		if (!$this->canSelfServiceFieldBeRelabeled($fieldID, $this->selfServiceSettings)) {
			return $defaultLabel;
		}
		$key = static::class . '_' . $fieldID;
		return empty($this->selfServiceSettings->relabelFields[$key]) ? $defaultLabel : $this->selfServiceSettings->relabelFields[$key];
	}

	/**
	 * Returns the meta HTML code for each input field.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * It is not possible to display help links.
	 *
	 * @param string[] $fields list of active fields
	 * @param array<string, string[]|string> $attributes attributes of LDAP account
	 * @param bool $passwordChangeOnly indicates that the user is only allowed to change his password and no LDAP content is readable
	 * @param string[] $readOnlyFields list of read-only fields
	 * @return array<string, htmlResponsiveRow> list of meta HTML elements (field name => htmlResponsiveRow)
	 *
	 * @see htmlElement
	 */
	public function getSelfServiceOptions(array $fields, array $attributes, bool $passwordChangeOnly, array $readOnlyFields): array {
		// this function must be overwritten by subclasses.
		return [];
	}

	/**
	 * Checks if all input values are correct and returns the LDAP attributes which should be changed.
	 * <br>Return values:
	 * <br>messages: array of parameters to create status messages
	 * <br>add: array of attributes to add
	 * <br>del: array of attributes to remove
	 * <br>mod: array of attributes to modify
	 * <br>info: array of values with informational value (e.g. to be used later by pre/postModify actions)
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.
	 *
	 * @param string[] $fields input fields
	 * @param array<string, string[]|string> $attributes LDAP attributes
	 * @param bool $passwordChangeOnly indicates that the user is only allowed to change his password and no LDAP content is readable
	 * @param string[] $readOnlyFields list of read-only fields
	 * @return array<string, array<string, string[]>|string[]> messages and attributes (array('messages' => [], 'add' => array('mail' => array('test@test.com')), 'del' => [], 'mod' => [], 'info' => []))
	 */
	public function checkSelfServiceOptions(array $fields, array $attributes, bool $passwordChangeOnly, array $readOnlyFields): array {
		return ['messages' => [], 'add' => [], 'del' => [], 'mod' => [], 'info' => []];
	}

	/**
	 * Returns a list of self-service configuration settings.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * The name attributes are used as keywords to load
	 * and save settings. We recommend to use the module name as prefix for them
	 * (e.g. posixAccount_homeDirectory) to avoid naming conflicts.
	 *
	 * @param selfServiceProfile $profile currently edited profile
	 * @return htmlElement|null meta HTML object
	 *
	 * @see baseModule::get_metaData()
	 * @see htmlElement
	 */
	public function getSelfServiceSettings(selfServiceProfile $profile): ?htmlElement {
		return $this->meta['selfServiceSettings'] ?? null;
	}

	/**
	 * Checks if the self-service settings are valid.
	 *
	 * Calling this method does not require the existence of an enclosing {@link accountContainer}.<br>
	 * <br>
	 * If the input data is invalid, the return value is an array that contains arrays
	 * to build StatusMessages (message type, message head, message text). If no errors
	 * occurred, the function returns an empty array.
	 *
	 * @param array<string, string[]> $options hash array (option name => value) that contains the input. The option values are all arrays containing one or more elements.
	 * @param selfServiceProfile $profile self-service profile
	 * @return string[][] error messages
	 */
	public function checkSelfServiceSettings(array &$options, selfServiceProfile &$profile): array {
		// needs to be implemented by the subclasses, if needed
		return [];
	}

	/**
	 * Allows the module to run commands before the LDAP entry is changed or created.
	 *
	 * An error message should be printed if the function returns false.
	 *
	 * @param boolean $newAccount is new account or existing one
	 * @param array<string, string[]|string> $attributes LDAP attributes of this entry
	 * @return bool true, if no problems occurred
	 */
	public function preModifySelfService(bool $newAccount, array $attributes): bool {
		return true;
	}

	/**
	 * Allows the module to run commands after the LDAP entry is changed or created.
	 *
	 * @param bool $newAccount is new account or existing one
	 * @param array<string, string[]|string> $attributes LDAP attributes of this entry
	 * @return bool true, if no problems occurred
	 */
	public function postModifySelfService(bool $newAccount, array $attributes): bool {
		return true;
	}

	/**
	 * This allows modules to create a link to a module specific page
	 * for the self-service.
	 * The link is shown on the login page of the self-service. You
	 * can use this to provide e.g. a page to reset passwords.
	 *
	 * @param array<string, string[]> $settings self-service settings
	 * @return string|null link text (null if no special page used)
	 */
	public function getLinkToSpecialSelfServicePage(array $settings): ?string {
		return null;
	}

	/**
	 * This function creates meta HTML code to display the module specific page
	 * for the self-service.
	 *
	 * @param selfServiceProfile $profile self-service settings
	 * @return htmlElement|null meta HTML object
	 *
	 * @see htmlElement
	 */
	public function displaySpecialSelfServicePage($profile) {
		return null;
	}

	/**
	 * Returns the {@link accountContainer} object.
	 *
	 * @return ?accountContainer accountContainer object
	 *
	 * @see accountContainer
	 */
	protected function getAccountContainer(): ?accountContainer {
		if ($this->base !== null && isset($_SESSION[$this->base])) {
			return $_SESSION[$this->base];
		}
		else {
			return null;
		}
	}

	/**
	 * Returns the LDAP attributes which are managed in this module.
	 *
	 * @return array<string, string[]|string>|null attributes
	 */
	public function getAttributes(): ?array {
		return $this->attributes;
	}

	/**
	 * Returns the LDAP attributes which are managed in this module (with unchanged values).
	 *
	 * @return array<string, string[]|string>|null attributes
	 */
	public function getOriginalAttributes(): ?array {
		return $this->orig;
	}

	/**
	 * Returns the path to the module icon.
	 * The path must be relative to graphics (e.g. key.svg) or a URL (/icons/icon.svg or http://server/icon.svg).
	 * You can also set $this->meta['icon']. The preferred size is 32x32px, format SVG.
	 *
	 * @return string icon
	 *
	 * @see baseModule::get_metaData()
	 */
	public function getIcon(): string {
		if (!empty($this->meta['icon']) && is_string($this->meta['icon'])) {
			return $this->meta['icon'];
		}
		return 'configure.svg';
	}

	/**
	 * Manages AJAX requests.
	 * This function may be called with or without an account container.
	 */
	public function handleAjaxRequest(): void {
		// modules that use AJAX need to implement this function
	}

	/**
	 * Specifies if this module supports the LAM admin interface.
	 * The LAM admin interface are the pages that allow to manage e.g. users and groups.
	 * In contrast there is also the LAM self-service interface. Most modules support
	 * the admin interface.
	 *
	 * @return boolean support admin interface
	 */
	public function supportsAdminInterface() {
		return true;
	}

	/**
	 * Returns a list of jobs that can be run.
	 *
	 * @param LAMConfig $config configuration
	 * @return Job[] jobs
	 */
	public function getSupportedJobs(LAMConfig $config): array {
		return [];
	}



	// helper functions

	/**
	 * Returns if the given configuration option is set.
	 * This function returns false if the configuration options cannot be read.
	 *
	 * @param String $optionName name of the option
	 * @param boolean $default default value if config option is not set at all (default: false)
	 * @return boolean true if option is set
	 */
	protected function isBooleanConfigOptionSet($optionName, $default = false) {
		// abort if configuration is not available
		if (!is_array($this->moduleSettings) || !isset($this->moduleSettings[$optionName][0])) {
			return $default;
		}
		return ($this->moduleSettings[$optionName][0] == 'true');
	}

	/**
	 * Returns a list of wildcards that can be replaced in input fields.
	 * E.g. "$firstname" is replaced with "givenName" attribute value.
	 *
	 * @return array<string, string> replacements as wildcard => value
	 */
	public function getWildCardReplacements(): array {
		return [];
	}

	/**
	 * Returns a list of attribute names that may contain wildcards.
	 *
	 * @return string[] list of attribute names
	 */
	public function getWildcardTargetAttributeNames(): array {
		return [];
	}

	/**
	 * Returns a callable if there should be a custom filtering for the given attribute name.
	 *
	 * @param string $attributeName attribute name
	 * @return callable|null custom function for filtering (?array $values, ?string $filterValue)
	 */
	public function getListFilterFunction(string $attributeName): ?callable {
		return null;
	}

	/**
	 * Returns a callable if there should be a custom display for the given attribute name.
	 *
	 * @param string $attributeName attribute name
	 * @return callable|null custom function for rendering (array $entry, string $attribute)
	 */
	public function getListRenderFunction(string $attributeName): ?callable {
		return null;
	}

	/**
	 * Returns a list of attribute descriptions for the account list.
	 *
	 * @param ConfiguredType $type type
	 * @return array<string, string> attribute name => description label
	 */
	public function getListAttributeDescriptions(ConfiguredType $type): array {
		return [];
	}

}
