getDataAttributesAsString() . $idParam . '>';
 		foreach ($this->cells as $cell) {
 			$return = array_merge($return, $cell->generateHTML($module, $input, $values, $restricted, $tabindex, $scope));
 		}
@@ -4288,6 +4420,15 @@ class htmlResponsiveRadio extends htmlRadio {
 		return $row->generateHTML($module, $input, $values, $restricted, $tabindex, $scope);
 	}
 
+	/**
+	 * Returns the selector to use to find the show/hide elements.
+	 *
+	 * @return string selector
+	 */
+	protected function getShowHideSelector() {
+		return '.row';
+	}
+
 }
 
 /**
diff --git a/lam/lib/import.inc b/lam/lib/import.inc
new file mode 100644
index 00000000..6288eece
--- /dev/null
+++ b/lam/lib/import.inc
@@ -0,0 +1,777 @@
+extractImportChunks($lines);
+		$tasks = $this->convertToTasks($chunks);
+		return $tasks;
+	}
+
+	/**
+	 * Processes the import data stored in session.
+	 */
+	public function doImport() {
+		$data = '';
+		$tasks = &$_SESSION[Importer::SESSION_KEY_TASKS];
+		$stopOnError = $_SESSION[Importer::SESSION_KEY_STOP_ON_ERROR];
+		// check if any actions are needed at all
+		if (empty($tasks)) {
+			return $this->getStatus($data);
+		}
+		$endTime = $this->getEndTime();
+		while ((!empty($tasks)) && ($endTime > time())) {
+			$task = array_shift($tasks);
+			try {
+				$data .= $task->run();
+			}
+			catch (LAMException $e) {
+				if ($stopOnError) {
+					return $this->stopImport($data, $e);
+				}
+				else {
+					$data .= Importer::formatMessage('ERROR', $e->getTitle(), $e->getMessage());
+				}
+			}
+		}
+		return $this->getStatus($data);
+	}
+
+	/**
+	 * Stops the import process because of an exception.
+	 *
+	 * @param string $data HTML output
+	 * @param LAMException $e exception
+	 * @return string JSON status
+	 */
+	private function stopImport($data, LAMException $e) {
+		$data .= Importer::formatMessage('ERROR', $e->getTitle(), $e->getMessage());
+		if (isset($_SESSION[Importer::SESSION_KEY_TASKS])) {
+			unset($_SESSION[Importer::SESSION_KEY_TASKS]);
+		}
+		$status = array(
+			Importer::STATUS => 'failed',
+			Importer::DATA => $data
+		);
+		return json_encode($status);
+	}
+
+	/**
+	 * Returns the current status as JSON.
+	 *
+	 * @param string $data HTML output to display
+	 * @return string JSON status
+	 */
+	private function getStatus($data) {
+		if (empty($_SESSION[Importer::SESSION_KEY_TASKS])) {
+			if (isset($_SESSION[Importer::SESSION_KEY_TASKS])) {
+				unset($_SESSION[Importer::SESSION_KEY_TASKS]);
+			}
+			$status = array(
+				Importer::STATUS => 'done',
+				Importer::DATA => $data
+			);
+			return json_encode($status);
+		}
+		$progress = (sizeof($_SESSION[Importer::SESSION_KEY_TASKS]) / $_SESSION[Importer::SESSION_KEY_COUNT]) * 100.0;
+		$progress = floor(100 - $progress);
+		$status = array(
+			Importer::STATUS => 'inProgress',
+			Importer::PROGRESS => $progress,
+			Importer::DATA => $data
+		);
+		return json_encode($status);
+	}
+
+	/**
+	 * Returns the time when processing should end.
+	 *
+	 * @return number end time as Unix timestamp
+	 */
+	private function getEndTime() {
+		$startTime = time();
+		$maxTime = get_cfg_var('max_execution_time') - 10;
+		if ($maxTime > Importer::TIME_LIMIT) {
+			$maxTime = Importer::TIME_LIMIT;
+		}
+		if ($maxTime <= 0) {
+			$maxTime = Importer::TIME_LIMIT;
+		}
+		return $startTime + $maxTime;
+	}
+
+	/**
+	 * Continues the import with processing of a single entry.
+	 *
+	 * @param array[] $entries import entries
+	 * @return ImporterTask[] tasks
+	 */
+	private function convertToTasks($entries) {
+		$tasks = array();
+		$count = sizeof($entries);
+		for ($i = 0; $i < $count; $i++) {
+			$entry = $entries[$i];
+			$firstParts = explode(':', $entry[0], 2);
+			if ($firstParts[Importer::KEY] == 'version') {
+				if ($i > 0) {
+					// allow version only as first chunk
+					throw new LAMException(_('Invalid data'), _('Duplicate version entry found.'));
+				}
+				$this->processVersion($entry);
+			}
+			elseif ($firstParts[Importer::KEY] == 'dn') {
+				$tasks[] = $this->processDnEntry($entry);
+			}
+			else {
+				throw new LAMException(_('A valid dn line is required'), htmlspecialchars($entry[0]));
+			}
+		}
+		return $tasks;
+	}
+
+	/**
+	 * Checks a version entry.
+	 *
+	 * @param string[] $entry entry
+	 * @throws LAMException if version is invalid
+	 */
+	private function processVersion($entry) {
+		$keyValue = $this->getLineKeyValue($entry[0]);
+		if (($keyValue[Importer::VALUE] != '1') || (sizeof($entry) > 1)) {
+			$escapedLines = array_map('htmlspecialchars', $entry);
+			throw new LAMException(_('LDIF import only supports version 1.'), implode('
', $escapedLines));
+		}
+	}
+
+	/**
+	 * Checks a dn entry.
+	 *
+	 * @param string[] $entry entry
+	 * @return ImporterTask task
+	 * @throws LAMException if invalid format
+	 */
+	private function processDnEntry($entry) {
+		$dnLine = array_shift($entry);
+		$keyValue = $this->getLineKeyValue($dnLine);
+		$dn = $keyValue[Importer::VALUE];
+		if (empty($entry)) {
+			throw new LAMException(_('Invalid data'), htmlspecialchars($dnLine));
+		}
+		$firstAttributeLine = array_shift($entry);
+		$firstAttribute = $this->getLineKeyValue($firstAttributeLine);
+		if ($firstAttribute[Importer::KEY] != Importer::CHANGETYPE) {
+			// complete DN
+			$attributes = array(
+				$firstAttribute[Importer::KEY] => array($firstAttribute[Importer::VALUE])
+			);
+			foreach ($entry as $attributeLine) {
+				$attribute = $this->getLineKeyValue($attributeLine);
+				$attributes[$attribute[Importer::KEY]][] = $attribute[Importer::VALUE];
+			}
+			return new AddEntryTask($dn, $attributes);
+		}
+		else {
+			$type = $firstAttribute[Importer::VALUE];
+			if ($type === 'add') {
+				$attributes = array();
+				foreach ($entry as $line) {
+					$lineData = $this->getLineKeyValue($line);
+					$attributes[$lineData[Importer::KEY]][] = $lineData[Importer::VALUE];
+				}
+				return new AddEntryTask($dn, $attributes);
+			}
+			elseif ($type === 'modrdn') {
+				return $this->createModRdnTask($dn, $entry);
+			}
+			elseif ($type === 'delete') {
+				if (!empty($entry)) {
+					throw new LAMException(_('Invalid data'), htmlspecialchars($dn));
+				}
+				return new DeleteEntryTask($dn);
+			}
+			elseif ($type !== 'modify') {
+				throw new LAMException(_('Invalid data'), htmlspecialchars($dn) . ' - changeType: ' . htmlspecialchars($type));
+			}
+			$changes = array();
+			$subtasks = array();
+			$currentLines = array();
+			$linesCount = sizeof($entry);
+			for ($i = 0; $i < $linesCount; $i++) {
+				$line = $entry[$i];
+				if ($line === '-') {
+					$subtasks[] = $this->getChangeTypeTask($dn, $currentLines);
+					$currentLines = array();
+				}
+				else {
+					$currentLines[] = $line;
+				}
+			}
+			$subtasks[] = $this->getChangeTypeTask($dn, $currentLines);
+			return new MultiTask($subtasks, $dn);
+		}
+	}
+
+	/**
+	 * Returns a modrdn task.
+	 *
+	 * @param string $dn DN
+	 * @param string[] $entry entry lines
+	 * @return
+	 * @throws LAMException syntax error
+	 */
+	private function createModRdnTask($dn, $entry) {
+		if (sizeof($entry) !== 2) {
+			throw new LAMException(_('Invalid data'), htmlspecialchars($dn));
+		}
+		$newRdnData = $this->getLineKeyValue($entry[0]);
+		if ($newRdnData[Importer::KEY] !== 'newrdn') {
+			throw new LAMException(_('Invalid data'), htmlspecialchars($dn) . '
' . $newRdnData);
+		}
+		$newRdn = $newRdnData[Importer::VALUE];
+		$delOldRdnData = $this->getLineKeyValue($entry[1]);
+		if (($delOldRdnData[Importer::KEY] !== 'deleteoldrdn') || !in_array($delOldRdnData[Importer::VALUE], array('0', '1'), true)) {
+			throw new LAMException(_('Invalid data'), htmlspecialchars($dn) . '
' . $entry[1]);
+		}
+		$delOldRdn = ($delOldRdnData[Importer::VALUE] === '0') ? false : true;
+		return new RenameEntryTask($dn, $newRdn, $delOldRdn);
+	}
+
+	/**
+	 * Returns a task for LDIF changeType entry.
+	 *
+	 * @param string $dn DN
+	 * @param string $lines lines
+	 * @return ImporterTask task
+	 */
+	private function getChangeTypeTask($dn, $lines) {
+		$firstLine = array_shift($lines);
+		$firstLineData = $this->getLineKeyValue($firstLine);
+		$type = $firstLineData[Importer::KEY];
+		$attributeName = $firstLineData[Importer::VALUE];
+		$attributes = array();
+		foreach ($lines as $line) {
+			$lineData = $this->getLineKeyValue($line);
+			if ($lineData[Importer::KEY] !== $attributeName) {
+				throw new LAMException(_('Invalid data'), htmlspecialchars($dn) . ' - ' . htmlspecialchars($type));
+			}
+			$attributes[$attributeName][] = $lineData[Importer::VALUE];
+		}
+		if ($type === 'add') {
+			return new AddAttributesTask($dn, $attributes);
+		}
+		elseif ($type === 'delete') {
+			return new DeleteAttributesTask($dn, $attributeName, $attributes);
+		}
+		elseif ($type === 'replace') {
+			return new ReplaceAttributesTask($dn, $attributes);
+		}
+		throw new LAMException(_('Invalid data'), htmlspecialchars($dn) . ' - ' . htmlspecialchars($type));
+	}
+
+	/**
+	 * Returns the HTML for an error message.
+	 *
+	 * @param string $type message type (e.g. INFO)
+	 * @param string $title title
+	 * @param string $message message
+	 * @return string HTML
+	 */
+	public static function formatMessage($type, $title, $message) {
+		$msg = new htmlStatusMessage($type, $title, $message);
+		$tabindex = 0;
+		ob_start();
+		$msg->generateHTML(null, array($msg), array(), true, $tabindex, 'user');
+		$data = ob_get_contents();
+		ob_clean();
+		return $data;
+	}
+
+	/**
+	 * Returns the key and value part of the line.
+	 *
+	 * @param string $line line
+	 * @return string[] array(key, value)
+	 */
+	private function getLineKeyValue($line) {
+		$parts = explode(':', $line, 2);
+		if (sizeof($parts) !== 2) {
+			throw new LAMException(_('Invalid data'), htmlspecialchars($line));
+		}
+		if (substr($parts[Importer::VALUE], 0, 1) == ':') {
+			$value = base64_decode(trim(substr($parts[Importer::VALUE], 1)));
+		}
+		else {
+			$value = trim($parts[Importer::VALUE]);
+		}
+		return array($parts[Importer::KEY], $value);
+	}
+
+}
+
+/**
+ * A single import task.
+ *
+ * @author Roland Gruber
+ */
+interface ImporterTask {
+
+	/**
+	 * Runs the task.
+	 *
+	 * @return string HTML output or LAMException if error occured
+	 */
+	public function run();
+
+}
+
+/**
+ * Adds a complete LDAP entry.
+ *
+ * @author Roland Gruber
+ */
+class AddEntryTask implements ImporterTask {
+
+	private $dn = '';
+	private $attributes = array();
+
+	/**
+	 * Constructor
+	 *
+	 * @param string $dn DN
+	 * @param array[string[]] $attributes list of attributes
+	 */
+	public function __construct($dn, $attributes) {
+		$this->dn = $dn;
+		$this->attributes = $attributes;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * @see \LAM\TOOLS\IMPORT_EXPORT\ImporterTask::run()
+	 */
+	public function run() {
+		$ldap = $_SESSION['ldap']->server();
+		$success = @ldap_add($ldap, $this->dn, $this->attributes);
+		if ($success) {
+			return Importer::formatMessage('INFO', _('Entry created'), htmlspecialchars($this->dn));
+		}
+		throw new LAMException(sprintf(_('Was unable to create DN: %s.'), $this->dn), getExtendedLDAPErrorMessage($ldap));
+	}
+
+}
+
+/**
+ * Renames an LDAP entry.
+ *
+ * @author Roland Gruber
+ */
+class RenameEntryTask implements ImporterTask {
+
+	private $dn = '';
+	private $newRdn = '';
+	private $deleteOldRdn = true;
+
+	/**
+	 * Constructor
+	 *
+	 * @param string $dn DN
+	 * @param string $newRdn new RDN value
+	 * @param bool $deleteOldRdn delete old RDN value
+	 */
+	public function __construct($dn, $newRdn, $deleteOldRdn) {
+		$this->dn = $dn;
+		$this->newRdn = $newRdn;
+		$this->deleteOldRdn = $deleteOldRdn;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * @see \LAM\TOOLS\IMPORT_EXPORT\ImporterTask::run()
+	 */
+	public function run() {
+		$ldap = $_SESSION['ldap']->server();
+		$success = @ldap_rename($ldap, $this->dn, $this->newRdn, null, $this->deleteOldRdn);
+		if ($success) {
+			return Importer::formatMessage('INFO', _('Rename successful!'), htmlspecialchars($this->dn));
+		}
+		throw new LAMException(_('Could not rename the entry.') . '
' . $this->dn, getExtendedLDAPErrorMessage($ldap));
+	}
+
+}
+
+/**
+ * Deletes an LDAP entry.
+ *
+ * @author Roland Gruber
+ */
+class DeleteEntryTask implements ImporterTask {
+
+	private $dn = '';
+
+	/**
+	 * Constructor
+	 *
+	 * @param string $dn DN
+	 */
+	public function __construct($dn) {
+		$this->dn = $dn;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * @see \LAM\TOOLS\IMPORT_EXPORT\ImporterTask::run()
+	 */
+	public function run() {
+		$ldap = $_SESSION['ldap']->server();
+		$success = @ldap_delete($ldap, $this->dn);
+		if ($success) {
+			return Importer::formatMessage('INFO', sprintf(_('Successfully deleted DN %s'), $this->dn), '');
+		}
+		throw new LAMException(_('Could not delete the entry.') . '
' . $this->dn, getExtendedLDAPErrorMessage($ldap));
+	}
+
+}
+
+/**
+ * Combines multiple import tasks.
+ *
+ * @author Roland Gruber
+ */
+class MultiTask implements ImporterTask {
+
+	/**
+	 * @var ImporterTask[] tasks
+	 */
+	private $tasks = array();
+
+	/**
+	 * @var string DN
+	 */
+	private $dn = null;
+
+	/**
+	 * Constructor
+	 *
+	 * @param ImporterTask[] $tasks tasks
+	 */
+	public function __construct($tasks, $dn) {
+		$this->tasks = $tasks;
+		$this->dn = $dn;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * @see \LAM\TOOLS\IMPORT_EXPORT\ImporterTask::run()
+	 */
+	public function run() {
+		foreach ($this->tasks as $task) {
+			$task->run();
+		}
+		return Importer::formatMessage('INFO', _('LDAP operation successful.'), htmlspecialchars($this->dn));
+	}
+
+	/**
+	 * Returns the list of subtasks.
+	 *
+	 * @return ImporterTask[]
+	 */
+	public function getTasks() {
+		return $this->tasks;
+	}
+
+}
+
+/**
+ * Adds attributes to an existing LDAP entry.
+ *
+ * @author Roland Gruber
+ */
+class AddAttributesTask implements ImporterTask {
+
+	private $dn = '';
+	private $attributes = array();
+
+	/**
+	 * Constructor
+	 *
+	 * @param string $dn DN
+	 * @param array[string[]] $attributes list of attributes
+	 */
+	public function __construct($dn, $attributes) {
+		$this->dn = $dn;
+		$this->attributes = $attributes;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * @see \LAM\TOOLS\IMPORT_EXPORT\ImporterTask::run()
+	 */
+	public function run() {
+		$ldap = $_SESSION['ldap']->server();
+		$success = @ldap_mod_add($ldap, $this->dn, $this->attributes);
+		if ($success) {
+			return '';
+		}
+		throw new LAMException(sprintf(_('Was unable to create DN: %s.'), $this->dn), getExtendedLDAPErrorMessage($ldap));
+	}
+
+	/**
+	 * Returns the DN.
+	 *
+	 * @return string DN
+	 */
+	public function getDn() {
+		return $this->dn;
+	}
+
+	/**
+	 * Returns the attributes to add.
+	 *
+	 * @return string[] attributes (array('attr' => array('val1', 'val2')))
+	 */
+	public function getAttributes() {
+		return $this->attributes;
+	}
+
+}
+
+/**
+ * Deletes attributes from an existing LDAP entry.
+ *
+ * @author Roland Gruber
+ */
+class DeleteAttributesTask implements ImporterTask {
+
+	private $dn = '';
+	private $attributes = array();
+	private $attributeName = null;
+
+	/**
+	 * Constructor
+	 *
+	 * @param string $dn DN
+	 * @param string $attributeName attribute name
+	 * @param array[string[]] $attributes list of attributes
+	 */
+	public function __construct($dn, $attributeName, $attributes) {
+		$this->dn = $dn;
+		$this->attributeName = $attributeName;
+		$this->attributes = $attributes;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * @see \LAM\TOOLS\IMPORT_EXPORT\ImporterTask::run()
+	 */
+	public function run() {
+		$ldap = $_SESSION['ldap']->server();
+		if (!empty($this->attributes)) {
+			$success = @ldap_mod_del($ldap, $this->dn, $this->attributes);
+		}
+		else {
+			$success = @ldap_modify($ldap, $this->dn, array($this->attributeName => array()));
+		}
+		if ($success) {
+			return '';
+		}
+		throw new LAMException(sprintf(_('Was unable to create DN: %s.'), $this->dn), getExtendedLDAPErrorMessage($ldap));
+	}
+
+	/**
+	 * Returns the DN.
+	 *
+	 * @return string DN
+	 */
+	public function getDn() {
+		return $this->dn;
+	}
+
+	/**
+	 * Returns the attributes to add.
+	 *
+	 * @return string[] attributes (array('attr' => array('val1', 'val2')))
+	 */
+	public function getAttributes() {
+		return $this->attributes;
+	}
+
+	/**
+	 * Returns the attributes name.
+	 *
+	 * @return string name
+	 */
+	public function getAttributeName() {
+		return $this->attributeName;
+	}
+
+}
+
+/**
+ * Replaces attributes in an existing LDAP entry.
+ *
+ * @author Roland Gruber
+ */
+class ReplaceAttributesTask implements ImporterTask {
+
+	private $dn = '';
+	private $attributes = array();
+
+	/**
+	 * Constructor
+	 *
+	 * @param string $dn DN
+	 * @param array[string[]] $attributes list of attributes
+	 */
+	public function __construct($dn, $attributes) {
+		$this->dn = $dn;
+		$this->attributes = $attributes;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 * @see \LAM\TOOLS\IMPORT_EXPORT\ImporterTask::run()
+	 */
+	public function run() {
+		$ldap = $_SESSION['ldap']->server();
+		$success = @ldap_modify($ldap, $this->dn, $this->attributes);
+		if ($success) {
+			return '';
+		}
+		throw new LAMException(sprintf(_('Was unable to create DN: %s.'), $this->dn), getExtendedLDAPErrorMessage($ldap));
+	}
+
+	/**
+	 * Returns the DN.
+	 *
+	 * @return string DN
+	 */
+	public function getDn() {
+		return $this->dn;
+	}
+
+	/**
+	 * Returns the attributes to add.
+	 *
+	 * @return string[] attributes (array('attr' => array('val1', 'val2')))
+	 */
+	public function getAttributes() {
+		return $this->attributes;
+	}
+
+	/**
+	 * Returns the attributes name.
+	 *
+	 * @return string name
+	 */
+	public function getAttributeName() {
+		return $this->attributeName;
+	}
+
+}
+
+?>
diff --git a/lam/lib/tools/importexport.inc b/lam/lib/tools/importexport.inc
new file mode 100644
index 00000000..c15d69f0
--- /dev/null
+++ b/lam/lib/tools/importexport.inc
@@ -0,0 +1,131 @@
+
\ No newline at end of file
diff --git a/lam/style/500_layout.css b/lam/style/500_layout.css
index 870e9b75..d2fbd317 100644
--- a/lam/style/500_layout.css
+++ b/lam/style/500_layout.css
@@ -380,6 +380,12 @@ table.collapse {
 	background-position: 0px 0px !important;
 }
 
+.okButton {
+	background-image: url(../graphics/pass.png) !important;
+	background-size: 16px 16px;
+	background-position: 0px 0px !important;
+}
+
 .smallPadding span {
 	padding: 0.1em 0.4em !important;
 }
diff --git a/lam/templates/3rdParty/pla/config/config.php b/lam/templates/3rdParty/pla/config/config.php
index c0082362..bf182f7f 100644
--- a/lam/templates/3rdParty/pla/config/config.php
+++ b/lam/templates/3rdParty/pla/config/config.php
@@ -39,10 +39,10 @@ $config->custom->commands['script'] = array(
 	'delete_form' => true,
 	'draw_tree_node' => true,
 	'expand' => true,
-	'export' => true,
-	'export_form' => true,
-	'import' => true,
-	'import_form' => true,
+	'export' => false,
+	'export_form' => false,
+	'import' => false,
+	'import_form' => false,
 	'login' => true,
 	'logout' => true,
 	'login_form' => true,
diff --git a/lam/templates/lib/500_lam.js b/lam/templates/lib/500_lam.js
index 862bedb3..9a1ed75f 100644
--- a/lam/templates/lib/500_lam.js
+++ b/lam/templates/lib/500_lam.js
@@ -901,6 +901,210 @@ window.lam.tools.schema.select = function() {
 	});
 };
 
+window.lam.importexport = window.lam.importexport || {};
+
+/**
+ * Starts the import process.
+ *
+ * @param tokenName name of CSRF token
+ * @param tokenValue value of CSRF token
+ */
+window.lam.importexport.startImport = function(tokenName, tokenValue) {
+	jQuery(document).ready(function() {
+		jQuery('#progressbarImport').progressbar();
+		var output = jQuery('#importResults');
+		var data = {
+			jsonInput: ''
+		};
+		data[tokenName] = tokenValue;
+		jQuery.ajax({
+			url: '../misc/ajax.php?function=import',
+			method: 'POST',
+			data: data
+		})
+		.done(function(jsonData){
+			if (jsonData.data && (jsonData.data != '')) {
+				output.append(jsonData.data);
+			}
+			if (jsonData.status == 'done') {
+				jQuery('#progressbarImport').hide();
+				jQuery('#btn_submitImportCancel').hide();
+				jQuery('#statusImportInprogress').hide();
+				jQuery('#statusImportDone').show();
+				jQuery('.newimport').show();
+			}
+			else if (jsonData.status == 'failed') {
+				jQuery('#btn_submitImportCancel').hide();
+				jQuery('#statusImportInprogress').hide();
+				jQuery('#statusImportFailed').show();
+				jQuery('.newimport').show();
+			}
+			else {
+				jQuery('#progressbarImport').progressbar({
+					value: jsonData.progress
+				});
+				window.lam.import.startImport(tokenName, tokenValue);
+			}
+		});
+	});
+};
+
+/**
+ * Starts the export process.
+ *
+ * @param tokenName name of CSRF token
+ * @param tokenValue value of CSRF token
+ */
+window.lam.importexport.startExport = function(tokenName, tokenValue) {
+	jQuery(document).ready(function() {
+		jQuery('#progressbarExport').progressbar({value: 50});
+		var output = jQuery('#exportResults');
+		var data = {
+			jsonInput: ''
+		};
+		data[tokenName] = tokenValue;
+		data['baseDn'] = jQuery('#baseDn').val();
+		data['searchScope'] = jQuery('#searchScope').val();
+		data['filter'] = jQuery('#filter').val();
+		data['attributes'] = jQuery('#attributes').val();
+		data['format'] = jQuery('#format').val();
+		data['ending'] = jQuery('#ending').val();
+		data['includeSystem'] = jQuery('#includeSystem').val();
+		data['saveAsFile'] = jQuery('#saveAsFile').val();
+		jQuery.ajax({
+			url: '../misc/ajax.php?function=export',
+			method: 'POST',
+			data: data
+		})
+		.done(function(jsonData){
+			if (jsonData.data && (jsonData.data != '')) {
+				output.append(jsonData.data);
+			}
+			if (jsonData.status == 'done') {
+				jQuery('#progressbarExport').hide();
+				jQuery('#btn_submitExportCancel').hide();
+				jQuery('#statusExportInprogress').hide();
+				jQuery('#statusExportDone').show();
+				jQuery('.newexport').show();
+				if (jsonData.output) {
+					jQuery('#exportResults > pre').text(jsonData.output);
+				}
+				else if (jsonData.file) {
+					window.open(jsonData.file, '_blank');
+				}
+			}
+			else {
+				jQuery('#progressbarExport').hide();
+				jQuery('#btn_submitExportCancel').hide();
+				jQuery('#statusExportInprogress').hide();
+				jQuery('#statusExportFailed').show();
+				jQuery('.newexport').show();
+			}
+		})
+		.fail(function() {
+			jQuery('#progressbarExport').hide();
+			jQuery('#btn_submitExportCancel').hide();
+			jQuery('#statusExportInprogress').hide();
+			jQuery('#statusExportFailed').show();
+			jQuery('.newexport').show();
+		});
+	});
+};
+
+window.lam.html = window.lam.html || {};
+
+/**
+ * Shows a DN selection for the given input field.
+ *
+ * @param fieldId id of input field
+ * @param title title of dialog
+ * @param okText ok button text
+ * @param cancelText cancel button text
+ * @param tokenName CSRF token name
+ * @param tokenValue CSRF token value
+ */
+window.lam.html.showDnSelection = function(fieldId, title, okText, cancelText, tokenName, tokenValue) {
+	var field = jQuery('#' + fieldId);
+	var fieldDiv = jQuery('#dlg_' + fieldId);
+	if (!fieldDiv.length > 0) {
+		jQuery('body').append(jQuery('
'));
+	}
+	var dnValue = field.val();
+	var data = {
+		jsonInput: ''
+	};
+	data[tokenName] = tokenValue;
+	data['fieldId'] = fieldId;
+	data['dn'] = dnValue;
+	jQuery.ajax({
+		url: '../misc/ajax.php?function=dnselection',
+		method: 'POST',
+		data: data
+	})
+	.done(function(jsonData) {
+		jQuery('#dlg_' + fieldId).html(jsonData.dialogData);
+		var buttonList = {};
+		buttonList[cancelText] = function() { jQuery(this).dialog("destroy"); };
+		jQuery('#dlg_' + fieldId).dialog({
+			modal: true,
+			title: title,
+			dialogClass: 'defaultBackground',
+			buttons: buttonList,
+			width: 'auto',
+			maxHeight: 600,
+			position: {my: 'center', at: 'center', of: window}
+		});
+	});
+};
+
+/**
+ * Selects the DN from dialog.
+ *
+ * @param el ok button in dialog
+ * @param fieldId field id of input field
+ * @returns false
+ */
+window.lam.html.selectDn = function(el, fieldId) {
+	var field = jQuery('#' + fieldId);
+	var dn = jQuery(el).parents('.row').data('dn');
+	field.val(dn);
+	jQuery('#dlg_' + fieldId).dialog("destroy");
+	return false;
+}
+
+/**
+ * Updates the DN selection.
+ *
+ * @param el element
+ * @param fieldId field id of dialog
+ * @param tokenName CSRF token name
+ * @param tokenValue CSRF token value
+ */
+window.lam.html.updateDnSelection = function(el, fieldId, tokenName, tokenValue) {
+	var fieldDiv = jQuery('#dlg_' + fieldId);
+	var dn = jQuery(el).parents('.row').data('dn');
+	var data = {
+		jsonInput: ''
+	};
+	data[tokenName] = tokenValue;
+	data['fieldId'] = fieldId;
+	data['dn'] = dn;
+	jQuery.ajax({
+		url: '../misc/ajax.php?function=dnselection',
+		method: 'POST',
+		data: data
+	})
+	.done(function(jsonData) {
+		jQuery('#dlg_' + fieldId).html(jsonData.dialogData);
+		jQuery(fieldDiv).dialog({
+		    position: {my: 'center', at: 'center', of: window}
+		});
+	})
+	.fail(function() {
+		jQuery(fieldDiv).dialog("close");
+	});
+}
+
 jQuery(document).ready(function() {
 	window.lam.gui.equalHeight();
 	window.lam.form.autoTrim();
diff --git a/lam/templates/misc/ajax.php b/lam/templates/misc/ajax.php
index 0e45907f..5c6c718d 100644
--- a/lam/templates/misc/ajax.php
+++ b/lam/templates/misc/ajax.php
@@ -1,5 +1,12 @@
 managePasswordChange($jsonInput);
 		}
-		elseif ($function == 'upload') {
+		elseif ($function === 'import') {
+			include_once('../../lib/import.inc');
+			$importer = new Importer();
+			ob_start();
+			$jsonOut = $importer->doImport();
+			ob_end_clean();
+			echo $jsonOut;
+		}
+		elseif ($function === 'export') {
+			include_once('../../lib/export.inc');
+			$attributes = $_POST['attributes'];
+			$baseDn = $_POST['baseDn'];
+			$ending = $_POST['ending'];
+			$filter = $_POST['filter'];
+			$format = $_POST['format'];
+			$includeSystem = ($_POST['includeSystem'] === 'true');
+			$saveAsFile = ($_POST['saveAsFile'] === 'true');
+			$searchScope = $_POST['searchScope'];
+			$exporter = new Exporter($baseDn, $searchScope, $filter, $attributes, $includeSystem, $saveAsFile, $format, $ending);
+			ob_start();
+			$jsonOut = $exporter->doExport();
+			ob_end_clean();
+			echo $jsonOut;
+		}
+		elseif ($function === 'upload') {
 			include_once('../../lib/upload.inc');
 			$typeManager = new \LAM\TYPES\TypeManager();
 			$uploader = new \LAM\UPLOAD\Uploader($typeManager->getConfiguredType($_GET['typeId']));
@@ -100,6 +133,12 @@ class Ajax {
 			ob_end_clean();
 			echo $jsonOut;
 		}
+		elseif ($function === 'dnselection') {
+			ob_start();
+			$jsonOut = $this->dnSelection();
+			ob_end_clean();
+			echo $jsonOut;
+		}
 	}
 
 	/**
@@ -132,6 +171,128 @@ class Ajax {
 		echo json_encode(array("result" => $result));
 	}
 
+	/**
+	 * Handles DN selection fields.
+	 *
+	 * @return string JSON output
+	 */
+	private function dnSelection() {
+		$dn = trim($_POST['dn']);
+		if (empty($dn) || !get_preg($dn, 'dn')) {
+			$dnList = $this->getDefaultDns();
+			$dn = null;
+		}
+		else {
+			$dnList = $this->getSubDns($dn);
+		}
+		$html = $this->buildDnSelectionHtml($dnList, $dn);
+		return json_encode(array('dialogData' => $html));
+	}
+
+	/**
+	 * Returns a list of default DNs from account types + tree suffix.
+	 *
+	 * @return string[] default DNs
+	 */
+	private function getDefaultDns() {
+		$typeManager = new TypeManager();
+		$baseDnList = array();
+		foreach ($typeManager->getConfiguredTypes() as $type) {
+			$suffix = $type->getSuffix();
+			if (!empty($suffix)) {
+				$baseDnList[] = $suffix;
+			}
+		}
+		$treeSuffix = $_SESSION['config']->get_Suffix('tree');
+		if (!empty($treeSuffix)) {
+			$baseDnList[] = $suffix;
+		}
+		$baseDnList = array_unique($baseDnList);
+		usort($baseDnList, 'compareDN');
+		return $baseDnList;
+	}
+
+	/**
+	 * Returns the HTML to build the DN selection list.
+	 *
+	 * @param string[] $dnList DN list
+	 * @param string $currentDn current DN
+	 */
+	private function buildDnSelectionHtml($dnList, $currentDn) {
+		$fieldId = trim($_POST['fieldId']);
+		$mainRow = new htmlResponsiveRow();
+		$onclickUp = 'window.lam.html.updateDnSelection(this, \''
+				. htmlspecialchars($fieldId) . '\', \'' . getSecurityTokenName() . '\', \''
+				. getSecurityTokenValue() . '\')';
+		if (!empty($currentDn)) {
+			$row = new htmlResponsiveRow();
+			$row->addDataAttribute('dn', $currentDn);
+			$text = new htmlOutputText($currentDn);
+			$text->setIsBold(true);
+			$row->add($text, 12, 9);
+			$row->setCSSClasses(array('text-right'));
+			$buttonId = base64_encode($currentDn);
+			$buttonId = str_replace('=', '', $buttonId);
+			$button = new htmlButton($buttonId, _('Ok'));
+			$button->setIconClass('okButton');
+			$button->setOnClick('window.lam.html.selectDn(this, \'' . htmlspecialchars($fieldId) . '\')');
+			$row->add($button, 12, 3);
+			$mainRow->add($row, 12);
+			// back up
+			$row = new htmlResponsiveRow();
+			$row->addDataAttribute('dn', extractDNSuffix($currentDn));
+			$text = new htmlLink('..', '#');
+			$text->setCSSClasses(array('bold'));
+			$text->setOnClick($onclickUp);
+			$row->add($text, 12, 9);
+			$row->setCSSClasses(array('text-right'));
+			$buttonId = base64_encode('..');
+			$buttonId = str_replace('=', '', $buttonId);
+			$button = new htmlButton($buttonId, _('Up'));
+			$button->setIconClass('upButton');
+			$button->setOnClick($onclickUp);
+			$row->add($button, 12, 3);
+			$mainRow->add($row, 12);
+		}
+		foreach ($dnList as $dn) {
+			$row = new htmlResponsiveRow();
+			$row->addDataAttribute('dn', $dn);
+			$link = new htmlLink($dn, '#');
+			$link->setOnClick($onclickUp);
+			$row->add($link, 12, 9);
+			$row->setCSSClasses(array('text-right'));
+			$buttonId = base64_encode($dn);
+			$buttonId = str_replace('=', '', $buttonId);
+			$button = new htmlButton($buttonId, _('Ok'));
+			$button->setIconClass('okButton');
+			$button->setOnClick('window.lam.html.selectDn(this, \'' . htmlspecialchars($fieldId) . '\')');
+			$row->add($button, 12, 3);
+			$mainRow->add($row, 12);
+		}
+		$tabindex = 1000;
+		ob_start();
+		parseHtml(null, $mainRow, array(), false, $tabindex, 'user');
+		$out = ob_get_contents();
+		ob_end_clean();
+		return $out;
+	}
+
+	/**
+	 * Returns the sub DNs of given DN.
+	 *
+	 * @param string $dn DN
+	 * @return string[] sub DNs
+	 */
+	private function getSubDns($dn) {
+		$dnEntries = ldapListDN($dn);
+		$dnList = array();
+		foreach ($dnEntries as $entry) {
+			$dnList[] = $entry['dn'];
+		}
+		usort($dnList, 'compareDN');
+		return $dnList;
+	}
+
 }
 
 
diff --git a/lam/templates/tools/importexport.php b/lam/templates/tools/importexport.php
new file mode 100644
index 00000000..22dc8bc7
--- /dev/null
+++ b/lam/templates/tools/importexport.php
@@ -0,0 +1,397 @@
+
+
+
+
+
+	
+		
+			- 
+				 + +
+- 
+				 + +
+
+		
+			
+		
+		
+			
+		
+	
+
+
+\n";
+	$container = new htmlResponsiveRow();
+	$container->add(new htmlTitle(_("Import")), 12);
+	$sources = array(
+		_('Text input') => 'text',
+		_('File') => 'file',
+	);
+	$sourceRadio = new htmlResponsiveRadio(_('Source'), 'source', $sources, 'text');
+	$sourceRadio->setTableRowsToHide(
+		array(
+			'file' => array('text'),
+			'text' => array('file')
+		)
+	);
+	$sourceRadio->setTableRowsToShow(
+		array(
+			'text' => array('text'),
+			'file' => array('file')
+		)
+	);
+	$container->add($sourceRadio, 12);
+	$container->addVerticalSpacer('1rem');
+	$container->add(new htmlResponsiveInputFileUpload('file', _('File'), '750'), 12);
+	$container->add(new htmlResponsiveInputTextarea('text', '', '60', '20', _('LDIF data'), '750'), 12);
+	$container->add(new htmlResponsiveInputCheckbox('noStop', false, _('Don\'t stop on errors')), 12);
+
+	$container->addVerticalSpacer('3rem');
+	$button = new htmlButton('submitImport', _('Submit'));
+	$container->add($button, 12, 12, 12, 'text-center');
+
+	addSecurityTokenToMetaHTML($container);
+
+	parseHtml(null, $container, array(), false, $tabindex, 'user');
+	echo ("\n");
+}
+
+/**
+ * Prints the content area for the import tab during processing state.
+ *
+ * @param int $tabindex tabindex
+ */
+function printImportTabProcessing(&$tabindex) {
+	try {
+		checkImportData();
+	}
+	catch (LAMException $e) {
+		$container = new htmlResponsiveRow();
+		$container->add(new htmlStatusMessage('ERROR', $e->getTitle(), $e->getMessage()), 12);
+		parseHtml(null, $container, array(), false, $tabindex, 'user');
+		printImportTabContent($tabindex);
+		return;
+	}
+	echo "
\n");
+}
+
+/**
+ * Checks if the import data is ok.
+ *
+ * @throws LAMException error message if not valid
+ */
+function checkImportData() {
+	$source = $_POST['source'];
+	$ldif = '';
+	if ($source == 'text') {
+		$ldif = $_POST['text'];
+	}
+	else {
+		$handle = fopen($_FILES['file']['tmp_name'], "r");
+		$ldif = fread($handle, 100000000);
+		fclose($handle);
+	}
+	if (empty($ldif)) {
+		throw new LAMException(_('You must either upload a file or provide an import in the text box.'));
+	}
+	$lines = preg_split("/\n|\r\n|\r/", $ldif);
+	$importer = new Importer();
+	$tasks = $importer->getTasks($lines);
+	$_SESSION[Importer::SESSION_KEY_TASKS] = $tasks;
+	$_SESSION[Importer::SESSION_KEY_COUNT] = sizeof($tasks);
+	$_SESSION[Importer::SESSION_KEY_STOP_ON_ERROR] = (!isset($_POST['noStop']) || ($_POST['noStop'] != 'on'));
+}
+
+/**
+ * Prints the content area for the export tab.
+ *
+ * @param int $tabindex tabindex
+ */
+function printExportTabContent(&$tabindex) {
+	echo "
\n");
+}
+
+/**
+ * Returns the default base DN.
+ *
+ * @return string base DN
+ */
+function getDefaultBaseDn() {
+	$typeManager = new TypeManager();
+	$baseDn = '';
+	foreach ($typeManager->getConfiguredTypes() as $type) {
+		$suffix = $type->getSuffix();
+		if (empty($baseDn) || (!empty($suffix) && (strlen($suffix) < strlen($baseDn)))) {
+			$baseDn = $suffix;
+		}
+	}
+	$treeSuffix = $_SESSION['config']->get_Suffix('tree');
+	if (empty($baseDn) || (!empty($treeSuffix) && (strlen($treeSuffix) < strlen($baseDn)))) {
+		$baseDn = $treeSuffix;
+	}
+	return $baseDn;
+}
+
+/**
+ * Prints the content area for the export tab during processing state.
+ *
+ * @param int $tabindex tabindex
+ */
+function printExportTabProcessing(&$tabindex) {
+	try {
+		checkExportData();
+	}
+	catch (LAMException $e) {
+		$container = new htmlResponsiveRow();
+		$container->add(new htmlStatusMessage('ERROR', $e->getTitle(), $e->getMessage()), 12);
+		parseHtml(null, $container, array(), false, $tabindex, 'user');
+		printExportTabContent($tabindex);
+		return;
+	}
+	echo "
\n");
+}
+
+/**
+ * Checks if the export data is ok.
+ *
+ * @throws LAMException error message if not valid
+ */
+function checkExportData() {
+	if (empty($_POST['baseDn'])) {
+		throw new LAMException(_('This field is required.'), _('Base DN'));
+	}
+}
+
+include '../../lib/adminFooter.inc';
diff --git a/lam/tests/lib/importTest.php b/lam/tests/lib/importTest.php
new file mode 100644
index 00000000..b60528fa
--- /dev/null
+++ b/lam/tests/lib/importTest.php
@@ -0,0 +1,462 @@
+setExpectedException(LAMException::class, 'this is no LDIF');
+
+		$importer = new Importer();
+		$importer->getTasks($lines);
+	}
+
+	/**
+	 * Wrong format version.
+	 */
+	public function testWrongVersion() {
+		$lines = array(
+			"version: 3"
+		);
+
+		$this->setExpectedException(LAMException::class, 'version: 3');
+
+		$importer = new Importer();
+		$importer->getTasks($lines);
+	}
+
+	/**
+	 * Multiple versions.
+	 */
+	public function testMultipleVersions() {
+		$lines = array(
+			"version: 1",
+			"",
+			"version: 1"
+		);
+
+		$this->setExpectedException(LAMException::class);
+
+		$importer = new Importer();
+		$importer->getTasks($lines);
+	}
+
+	/**
+	 * Data after version.
+	 */
+	public function testDataAfterVersion() {
+		$lines = array(
+			"version: 1",
+			"some: data"
+		);
+
+		$this->setExpectedException(LAMException::class);
+
+		$importer = new Importer();
+		$importer->getTasks($lines);
+	}
+
+	/**
+	 * DN line without any data.
+	 */
+	public function testDnNoData() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com"
+		);
+
+		$this->setExpectedException(LAMException::class, 'dn: uid=test,dc=example,dc=com');
+
+		$importer = new Importer();
+		$importer->getTasks($lines);
+	}
+
+	/**
+	 * One complete entry.
+	 */
+	public function testSingleFullEntry() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"objectClass: inetOrgPerson",
+			"uid: test",
+		);
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+		$this->assertEquals(1, sizeof($tasks));
+	}
+
+	/**
+	 * Change entry with invalid changetype.
+	 */
+	public function testChangeInvalidType() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"changeType: invalid",
+			"uid: test",
+		);
+
+		$this->setExpectedException(LAMException::class, 'uid=test,dc=example,dc=com - changeType: invalid');
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+	}
+
+	/**
+	 * Change entry with add changetype.
+	 */
+	public function testChangeAdd() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"changeType: add",
+			"uid: test",
+		);
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+		$this->assertEquals(1, sizeof($tasks));
+		$task = $tasks[0];
+		$this->assertEquals(AddEntryTask::class, get_class($task));
+	}
+
+	/**
+	 * Change entry with modrdn changetype and invalid options.
+	 */
+	public function testChangeModRdnInvalidData() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"changeType: modrdn",
+			"uid: test",
+		);
+
+		$this->setExpectedException(LAMException::class, 'uid=test,dc=example,dc=com');
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+	}
+
+	/**
+	 * Change entry with modrdn changetype and invalid deleteoldrdn.
+	 */
+	public function testChangeModRdnInvalidDeleteoldrdn() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"changeType: modrdn",
+			"newrdn: uid1=test",
+			"deleteoldrdn: x",
+		);
+
+		$this->setExpectedException(LAMException::class, 'uid=test,dc=example,dc=com');
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+	}
+
+	/**
+	 * Change entry with modrdn changetype.
+	 */
+	public function testChangeModRdn() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"changeType: modrdn",
+			"newrdn: uid1=test",
+			"deleteoldrdn: 0",
+		);
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+		$this->assertEquals(1, sizeof($tasks));
+		$task = $tasks[0];
+		$this->assertEquals(RenameEntryTask::class, get_class($task));
+	}
+
+	/**
+	 * Change entry with delete changetype with extra line.
+	 */
+	public function testChangeDeleteInvalid() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"changeType: delete",
+			"uid: test",
+		);
+
+		$this->setExpectedException(LAMException::class, 'uid=test,dc=example,dc=com');
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+	}
+
+	/**
+	 * Change entry with delete changetype.
+	 */
+	public function testChangeDelete() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"changeType: delete",
+		);
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+		$this->assertEquals(1, sizeof($tasks));
+		$task = $tasks[0];
+		$this->assertEquals(DeleteEntryTask::class, get_class($task));
+	}
+
+	/**
+	 * Change entry with modify changetype with invalid operation.
+	 */
+	public function testChangeModifyInvalid() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"changeType: modify",
+			"invalid: test",
+		);
+
+		$this->setExpectedException(LAMException::class, 'uid=test,dc=example,dc=com');
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+	}
+
+	/**
+	 * Change entry with modify changetype and add operation.
+	 */
+	public function testChangeModifyAddInvalid() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"changeType: modify",
+			"add: uid",
+			"uid: uid1",
+			"invalid: uid2"
+		);
+
+		$this->setExpectedException(LAMException::class, 'uid=test,dc=example,dc=com');
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+	}
+
+	/**
+	 * Change entry with modify changetype and add operation.
+	 */
+	public function testChangeModifyAdd() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"changeType: modify",
+			"add: uid",
+			"uid: uid1",
+			"uid: uid2"
+		);
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+		$this->assertEquals(1, sizeof($tasks));
+		$task = $tasks[0];
+		$this->assertEquals(MultiTask::class, get_class($task));
+		$subtasks = $task->getTasks();
+		$this->assertEquals(1, sizeof($subtasks));
+		$subTask = $subtasks[0];
+		$this->assertEquals(AddAttributesTask::class, get_class($subTask));
+		$this->assertEquals($subTask->getDn(), 'uid=test,dc=example,dc=com');
+		$attributes = $subTask->getAttributes();
+		$this->assertEquals(1, sizeof($attributes));
+		$this->assertEquals(2, sizeof($attributes['uid']));
+		$this->assertTrue(in_array('uid1', $attributes['uid']));
+		$this->assertTrue(in_array('uid2', $attributes['uid']));
+	}
+
+	/**
+	 * Change entry with modify changetype and two add operations.
+	 */
+	public function testChangeModifyAddTwice() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"changeType: modify",
+			"add: uid",
+			"uid: uid1",
+			"uid: uid2",
+			"-",
+			"add: gn",
+			"gn: name1",
+			"gn: name2"
+		);
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+		$this->assertEquals(1, sizeof($tasks));
+		$task = $tasks[0];
+		$this->assertEquals(MultiTask::class, get_class($task));
+		$subtasks = $task->getTasks();
+		$this->assertEquals(2, sizeof($subtasks));
+		$subTask = $subtasks[0];
+		$this->assertEquals(AddAttributesTask::class, get_class($subTask));
+		$this->assertEquals($subTask->getDn(), 'uid=test,dc=example,dc=com');
+		$attributes = $subTask->getAttributes();
+		$this->assertEquals(1, sizeof($attributes));
+		$this->assertEquals(2, sizeof($attributes['uid']));
+		$this->assertTrue(in_array('uid1', $attributes['uid']));
+		$this->assertTrue(in_array('uid2', $attributes['uid']));
+		$subTask = $subtasks[1];
+		$this->assertEquals(AddAttributesTask::class, get_class($subTask));
+		$this->assertEquals($subTask->getDn(), 'uid=test,dc=example,dc=com');
+		$attributes = $subTask->getAttributes();
+		$this->assertEquals(1, sizeof($attributes));
+		$this->assertEquals(2, sizeof($attributes['gn']));
+		$this->assertTrue(in_array('name1', $attributes['gn']));
+		$this->assertTrue(in_array('name2', $attributes['gn']));
+	}
+
+	/**
+	 * Change entry with modify changetype and delete operation.
+	 */
+	public function testChangeModifyDelete() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"changeType: modify",
+			"delete: uid",
+			"uid: uid1",
+			"uid: uid2"
+		);
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+		$this->assertEquals(1, sizeof($tasks));
+		$task = $tasks[0];
+		$this->assertEquals(MultiTask::class, get_class($task));
+		$subtasks = $task->getTasks();
+		$this->assertEquals(1, sizeof($subtasks));
+		$subTask = $subtasks[0];
+		$this->assertEquals(DeleteAttributesTask::class, get_class($subTask));
+		$this->assertEquals($subTask->getDn(), 'uid=test,dc=example,dc=com');
+		$attributes = $subTask->getAttributes();
+		$this->assertEquals(1, sizeof($attributes));
+		$this->assertEquals(2, sizeof($attributes['uid']));
+		$this->assertTrue(in_array('uid1', $attributes['uid']));
+		$this->assertTrue(in_array('uid2', $attributes['uid']));
+	}
+
+	/**
+	 * Change entry with modify changetype and delete operation.
+	 */
+	public function testChangeModifyDeleteAll() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"changeType: modify",
+			"delete: uid",
+		);
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+		$this->assertEquals(1, sizeof($tasks));
+		$task = $tasks[0];
+		$this->assertEquals(MultiTask::class, get_class($task));
+		$subtasks = $task->getTasks();
+		$this->assertEquals(1, sizeof($subtasks));
+		$subTask = $subtasks[0];
+		$this->assertEquals(DeleteAttributesTask::class, get_class($subTask));
+		$this->assertEquals($subTask->getDn(), 'uid=test,dc=example,dc=com');
+		$attributes = $subTask->getAttributes();
+		$this->assertTrue(empty($attributes));
+	}
+
+	/**
+	 * Change entry with modify changetype and replace operation.
+	 */
+	public function testChangeModifyReplace() {
+		$lines = array(
+			"version: 1",
+			"",
+			"dn: uid=test,dc=example,dc=com",
+			"changeType: modify",
+			"replace: uid",
+			"uid: uid1",
+			"uid: uid2",
+		);
+
+		$importer = new Importer();
+		$tasks = $importer->getTasks($lines);
+		$this->assertEquals(1, sizeof($tasks));
+		$task = $tasks[0];
+		$this->assertEquals(MultiTask::class, get_class($task));
+		$subtasks = $task->getTasks();
+		$this->assertEquals(1, sizeof($subtasks));
+		$subTask = $subtasks[0];
+		$this->assertEquals(ReplaceAttributesTask::class, get_class($subTask));
+		$this->assertEquals($subTask->getDn(), 'uid=test,dc=example,dc=com');
+		$attributes = $subTask->getAttributes();
+		$this->assertEquals(1, sizeof($attributes));
+		$this->assertEquals(2, sizeof($attributes['uid']));
+		$this->assertTrue(in_array('uid1', $attributes['uid']));
+		$this->assertTrue(in_array('uid2', $attributes['uid']));
+	}
+
+}