diff --git a/lam/HISTORY b/lam/HISTORY index 028c4fbe..47ebe3aa 100644 --- a/lam/HISTORY +++ b/lam/HISTORY @@ -1,3 +1,7 @@ +December 2018 6.6 + - New import/export in tools menu + + 25.09.2018 6.5 - Password change possible via LDAP EXOP operation (set LDAP_EXOP as password hash, requires PHP 7.2) - Support Imagick and GD @@ -25,6 +29,7 @@ - Fixed bugs: -> Error on password reset page when custom fields is used (194) + 19.03.2018 6.3 - Server profile: added option if referential integrity overlay is active to skip cleanup actions - Unix: several options are now specific to subaccount types (reconfiguration required!) diff --git a/lam/docs/manual-sources/chapter-modules.xml b/lam/docs/manual-sources/chapter-modules.xml index fbe7b7f4..00263ab7 100644 --- a/lam/docs/manual-sources/chapter-modules.xml +++ b/lam/docs/manual-sources/chapter-modules.xml @@ -5880,9 +5880,6 @@ OK (10 msec) There are also some special functions available: - Export: This allows you to export - entries to a file (e.g. LDIF or CSV format). - Show internal attributes: Shows internal attributes of the current entry. This includes information about the creator and creation time of the entry. diff --git a/lam/docs/manual-sources/chapter-tools.xml b/lam/docs/manual-sources/chapter-tools.xml index a4a4195f..9efe0758 100644 --- a/lam/docs/manual-sources/chapter-tools.xml +++ b/lam/docs/manual-sources/chapter-tools.xml @@ -74,14 +74,16 @@ -
+
File upload When you need to create lots of accounts then you can use LAM's file - upload to create them. LAM will read a CSV formatted file and create the - related LDAP entries. Please check the data in you CSV file carefully. LAM - will do less checks for the file upload than for single account - creation. + upload to create them. In contrast to LDAP + import/export this operates on account type level. + + LAM will read a CSV formatted file and create the related LDAP + entries. Please check the data in you CSV file carefully. LAM will do less + checks for the file upload than for single account creation. At the first page please select the account type and what extensions should be activated. @@ -201,6 +203,66 @@
+
+ LDAP import/export + + Here you can import and export plain LDAP data. In contrast to file upload this operates on plain LDAP + attribute level. + +
+ Import + + The LDAP import supports input data in LDIF + format. You can provide plain text or upload an LDIF file. + + The "Don't stop on errors" option will cause the import to + continue even if entries could not be created. + + + + +
+ +
+ Export + + Here you can export your plain LDAP data as LDIF or CSV + file. + + + + + + Base DN: this is the starting point of the export. Enter a DN or + press the magnifying glass icon to open the DN selection dialog. + + Search scope: You can export just the base DN, base DN + its + direct children or the whole subtree. + + Search filter: this can be used to filter the entries by + specifying a standard LDAP filter. The preselected filter + "(objectclass=*)" matches all entries. + + Attributes: the list of attributes that should be part of export. + "*" matches all standard attributes (excluding system + attributes). + + Include system attributes: this will also include system + attributes like the entry creation time and creator's DN. + + Save as file: will save to file instead of printing the data on + the web page. + + Export format: you can select LDIF or CSV (e.g. for usage in + spreadsheet applications). + + End of line: use the one appropriate for your operating + system. +
+
+
OU editor diff --git a/lam/docs/manual-sources/images/schemaBrowser.png b/lam/docs/manual-sources/images/schemaBrowser.png index a709e14f..39f2d737 100644 Binary files a/lam/docs/manual-sources/images/schemaBrowser.png and b/lam/docs/manual-sources/images/schemaBrowser.png differ diff --git a/lam/docs/manual-sources/images/tool_export.png b/lam/docs/manual-sources/images/tool_export.png new file mode 100644 index 00000000..713ccd52 Binary files /dev/null and b/lam/docs/manual-sources/images/tool_export.png differ diff --git a/lam/docs/manual-sources/images/tool_import.png b/lam/docs/manual-sources/images/tool_import.png new file mode 100644 index 00000000..d0afbf98 Binary files /dev/null and b/lam/docs/manual-sources/images/tool_import.png differ diff --git a/lam/graphics/importexport.png b/lam/graphics/importexport.png new file mode 100644 index 00000000..f2445164 Binary files /dev/null and b/lam/graphics/importexport.png differ diff --git a/lam/help/help.inc b/lam/help/help.inc index c248c9c1..c9d29e82 100644 --- a/lam/help/help.inc +++ b/lam/help/help.inc @@ -330,7 +330,7 @@ $helpArray = array ( "Text" => _("This will create a new organisational unit under the selected one.")), "602" => array ("Headline" => _("OU-Editor") . " - " . _("Delete organisational unit"), "Text" => _("This will delete the selected organisational unit. The OU has to be empty.")), - // 700 - 799 + // 700 - 749 // multi edit tool "700" => array ("Headline" => _('LDAP suffix'), "Text" => _('Please select the suffix where changes should be done.')), @@ -338,6 +338,18 @@ $helpArray = array ( "Text" => _('Use this to enter an additional LDAP filter (e.g. "(cn!=admin)") to reduce the number of entries to modify.')), "702" => array ("Headline" => _('Operations'), "Text" => _('Please specify which attributes should be changed. The modify operation will also add an value if the attribute does not yet exist. To delete all values of an attribute please leave the value field empty.')), + // 750 - 799 + // import/export + "750" => array ("Headline" => _('LDIF data'), + "Text" => _('The input data must be formatted in LDIF format.')), + "751" => array ("Headline" => _('Base DN'), + "Text" => _('The export will read entries of this DN.')), + "752" => array ("Headline" => _('Search filter'), + "Text" => _('Please enter an LDAP filter to specifiy the exported entries.')), + "753" => array ("Headline" => _('Attributes'), + "Text" => _('Please enter a comma separated list of attributes to export. Using "*" will export all attributes.')), + "754" => array ("Headline" => _('Include system attributes'), + "Text" => _('Activate this option to export internal attributes that are not visible by default.')), // 800 - 899 // jobs '800' => array( diff --git a/lam/lib/account.inc b/lam/lib/account.inc index 515b0990..acee4707 100644 --- a/lam/lib/account.inc +++ b/lam/lib/account.inc @@ -854,6 +854,32 @@ function ldapGetDN($dn, $attributes = array('dn'), $handle = null) { return $return; } +/** + * Returns the DN and children of a given DN. + * + * @param String $dn DN + * @param String $filter LDAP filter + * @param array $attributes list of attributes to fetch + * @param handle $handle LDAP handle (optional for admin interface pages) + * @return array attributes or null if not found + */ +function ldapListDN($dn, $filter = '(objectclass=*)', $attributes = array('dn'), $handle = null) { + if ($handle == null) { + $handle = $_SESSION['ldap']->server(); + } + $return = null; + $sr = @ldap_list($handle, escapeDN($dn), $filter, $attributes, 0, 0, 0, LDAP_DEREF_NEVER); + if ($sr) { + $entries = ldap_get_entries($handle, $sr); + if ($entries) { + cleanLDAPResult($entries); + $return = $entries; + } + @ldap_free_result($sr); + } + return $return; +} + /** * Deletes a DN and all child entries. * diff --git a/lam/lib/export.inc b/lam/lib/export.inc new file mode 100644 index 00000000..43a9381b --- /dev/null +++ b/lam/lib/export.inc @@ -0,0 +1,321 @@ +baseDn = $baseDn; + $this->searchScope = $searchScope; + $this->filter = $filter; + $this->attributes = $attributes; + $this->includeSystem = $includeSystem; + $this->saveAsFile = $saveAsFile; + $this->format = $format; + $this->ending = $ending; + } + + /** + * Starts the export process. + * + * @return string JSON result + */ + public function doExport() { + try { + $this->checkParameters(); + $results = $this->getLDAPData(); + return $this->writeDataAndReturnJson($results); + } + catch (LAMException $e) { + $data = Exporter::formatMessage('ERROR', $e->getTitle(), $e->getMessage()); + $status = array( + Exporter::STATUS => 'failed', + Exporter::DATA => $data + ); + return json_encode($status); + } + } + + /** + * 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; + } + + /** + * Checks the input parameters for validity. + * + * @throws LAMException in case of errors + */ + private function checkParameters() { + if (!get_preg($this->baseDn, 'dn')) { + throw new LAMException(_('Please enter a valid DN in the field:'), _('Base DN')); + } + } + + /** + * Returns the LDAP entries + * + * @return array[] LDAP entries + */ + private function getLDAPData() { + $attributes = preg_split('/,[ ]*/', $this->attributes); + if ($this->includeSystem) { + $attributes = array_merge($attributes, array('+', 'passwordRetryCount', 'accountUnlockTime', 'nsAccountLock', + 'nsRoleDN', 'passwordExpirationTime', 'pwdChangedTime')); + } + $attributes = array_unique($attributes); + switch ($this->searchScope) { + case 'base': + return array(ldapGetDN($this->baseDn, $attributes)); + break; + case 'one': + return ldapListDN($this->baseDn, $this->filter, $attributes); + break; + case 'sub': + return searchLDAP($this->baseDn, $this->filter, $attributes); + break; + default: + throw new LAMException('Invalid scope'); + break; + } + } + + /** + * Writes the entries to file/response and prints JSON. + * + * @param array $entries LDAP entries + */ + private function writeDataAndReturnJson(&$entries) { + $lineEnding = ($this->ending === 'windows') ? "\r\n" : "\n"; + if ($this->format === 'csv') { + $output = $this->getCsvOutput($entries, $lineEnding); + } + elseif ($this->format === 'ldif') { + $output = $this->getLdifOutput($entries, $lineEnding); + } + else { + throw new LAMException('Invalid format'); + } + if ($this->saveAsFile) { + $filename = '../../tmp/' . getRandomNumber() . time() .'.' . $this->format; + $handle = fopen($filename, 'w'); + chmod($filename, 0640); + fwrite($handle, $output); + fclose($handle); + return json_encode(array( + Exporter::FILE => $filename, + Exporter::STATUS => 'done' + )); + } + return json_encode(array( + Exporter::OUTPUT => htmlspecialchars($output, ENT_NOQUOTES), + Exporter::STATUS => 'done' + )); + } + + /** + * Converts the given LDAP entries to CSV format. + * + * @param string $entries entries + * @param string $lineEnding line ending + */ + private function getCsvOutput(&$entries, $lineEnding) { + $attributeNames = array(); + foreach ($entries as $entry) { + $entryAttributeNames = array_keys($entry); + foreach ($entryAttributeNames as $name) { + if (!in_array($name, $attributeNames)) { + $attributeNames[] = $name; + } + } + } + $attributeNames = array_delete(array('dn'), $attributeNames); + sort($attributeNames); + array_unshift($attributeNames, 'dn'); + + $attributeNamesQuoted = array_map(array($this, 'escapeCsvAndAddQuotes'), $attributeNames); + $output = ''; + // header + $output .= implode(',', $attributeNamesQuoted) . $lineEnding; + // content + foreach ($entries as $entry) { + $values = array(); + foreach ($attributeNames as $name) { + if (!isset($entry[$name])) { + $values[] = $this->escapeCsvAndAddQuotes(''); + } + elseif (is_array($entry[$name])) { + $values[] = $this->escapeCsvAndAddQuotes(implode(' | ', $entry[$name])); + } + else { + $values[] = $this->escapeCsvAndAddQuotes($entry[$name]); + } + } + $output .= implode(',', $values) . $lineEnding; + } + return $output; + } + + /** + * Escapes a CSV value and adds quotes arround it. + * + * @param string $value CSV value + * @return string escaped and quoted value + */ + private function escapeCsvAndAddQuotes($value) { + return '"' . str_replace('"', '""', $value) . '"'; + } + + /** + * Converts the given LDAP entries to LDIF format. + * + * @param string $entries entries + * @param string $lineEnding line ending + */ + private function getLdifOutput(&$entries, $lineEnding) { + $output = ''; + $output .= '#' . $lineEnding; + $output .= '# ' . _('Base DN') . ': ' . $this->baseDn . $lineEnding; + $output .= '# ' . _('Search scope') . ': ' . $this->searchScope . $lineEnding; + $output .= '# ' . _('Search filter') . ': ' . $this->filter . $lineEnding; + $output .= '# ' . _('Total entries') . ': ' . sizeof($entries) . $lineEnding; + $output .= '#' . $lineEnding; + $output .= '# Generated by LDAP Account Manager on ' . date('Y-m-d H:i:s') . $lineEnding; + $output .= $lineEnding; + $output .= $lineEnding; + $output .= 'version: 1'; + $output .= $lineEnding; + $output .= $lineEnding; + foreach ($entries as $entry) { + $output .= 'dn: ' . $entry['dn'] . $lineEnding; + unset($entry['dn']); + ksort($entry); + foreach ($entry as $attributeName => $values) { + foreach ($values as $value) { + if ($this->isPlainAscii($value)) { + $output .= $this->wrapLdif($attributeName . ': ' . $value, $lineEnding) . $lineEnding; + } + else { + $output .= $this->wrapLdif($attributeName . ':: ' . base64_encode($value), $lineEnding) . $lineEnding; + } + } + } + $output .= $lineEnding; + } + + return $output; + } + + /** + * Splits the LDIF line if needed. + * + * @param string $content line content + * @param string $lineEnding line ending + */ + private function wrapLdif($content, $lineEnding) { + $line_length = 76; + if (strlen($content) <= $line_length) { + return $content; + } + $wrappedContent = substr($content, 0, $line_length) . $lineEnding; + $contentLeft = substr($content, $line_length); + $line_length = $line_length - 1; + $lines = str_split($contentLeft, $line_length); + foreach ($lines as $line) { + $wrappedContent .= ' ' . $line . $lineEnding; + } + return trim($wrappedContent); + } + + /** + * Checks if the value is plain ASCII. + * + * @param string $content content to check + * @return bool is plain ASCII + */ + private function isPlainAscii($content) { + for ($i=0; $i < strlen($content); $i++) { + if (ord($content[$i]) < 32 || ord($content[$i]) > 127) { + return false; + } + } + return true; + } + +} diff --git a/lam/lib/html.inc b/lam/lib/html.inc index 5b6c8b99..095aea8a 100644 --- a/lam/lib/html.inc +++ b/lam/lib/html.inc @@ -472,6 +472,8 @@ class htmlInputField extends htmlElement { protected $autocompleteMinLength = 1; /** show calendar */ protected $showCalendar = false; + /** show DN selection */ + protected $showDnSelection = false; /** calendar format */ protected $calendarFormat = ''; /** title attribute */ @@ -589,8 +591,19 @@ class htmlInputField extends htmlElement { if (!empty($this->title)) { $title = ' title="' . $this->title . '"'; } + if ($this->showDnSelection) { + echo ''; + } echo ''; + if ($this->showDnSelection) { + echo ''; + echo ''; + } // autocompletion if ($this->autocomplete) { echo "'; + echo $script; + } + + /** + * This will hide the given table rows when the radio is changed to the specified value. + * The given IDs can be of any e.g. input element. Starting from this element + * the first parent "" element will be used to show/hide. + *
+ *
+ *
Example for $tableRowsToHide: + *
array('val1' => array('option1', 'option2'), 'val2' => array('option3')) + * + * @param array $tableRowsToHide array of select value => array of IDs of child elements to hide + */ + public function setTableRowsToHide($tableRowsToHide) { + $this->tableRowsToHide = $tableRowsToHide; + } + + /** + * This will show the given table rows when the radio is changed to the specified value. + * The given IDs can be of any e.g. input element. Starting from this element + * the first parent "" element will be used to show/hide. + *
+ *
+ *
Example for $tableRowsToShow: + *
array('val1' => array('option1', 'option2'), 'val2' => array('option3')) + * + * @param array $tableRowsToShow array of select value => array of IDs of child elements to show + */ + public function setTableRowsToShow($tableRowsToShow) { + $this->tableRowsToShow = $tableRowsToShow; + } + } /** @@ -2292,7 +2424,7 @@ class htmlInputFileUpload extends htmlElement { if (!$this->isEnabled) { $disabled = ' disabled'; } - echo ''; + echo ''; return array($this->name => 'file'); } @@ -3828,7 +3960,7 @@ class htmlResponsiveRow extends htmlElement { if ($this->id !== null) { $idParam = ' id="' . $this->id . '"'; } - echo '
'; + echo '
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 @@ + + + + +
+
+
    +
  • + import +
  • +
  • + export +
  • +
+
+ +
+
+ +
+
+
+ +\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"; + $container = new htmlResponsiveRow(); + $container->add(new htmlTitle(_("Import")), 12); + + $container->add(new htmlDiv('statusImportInprogress', new htmlOutputText(_('Status') . ': ' . _('in progress'))), 12); + $container->add(new htmlDiv('statusImportDone', new htmlOutputText(_('Status') . ': ' . _('done')), array('hidden')), 12); + $container->add(new htmlDiv('statusImportFailed', new htmlOutputText(_('Status') . ': ' . _('failed')), array('hidden')), 12); + $container->addVerticalSpacer('1rem'); + $container->add(new htmlDiv('progressbarImport', new htmlOutputText('')), 12); + $container->addVerticalSpacer('3rem'); + $button = new htmlButton('submitImportCancel', _('Cancel')); + $container->add($button, 12, 12, 12, 'text-center'); + + $newImportButton = new htmlLink(_('New import'), null, null, true); + $container->add($newImportButton, 12, 12, 12, 'text-center hidden newimport'); + + $container->addVerticalSpacer('3rem'); + + $container->add(new htmlDiv('importResults', new htmlOutputText('')), 12); + $container->add(new htmlJavaScript( + 'window.lam.importexport.startImport(\'' . getSecurityTokenName() . '\', \'' . getSecurityTokenValue() . '\');' + ), 12); + + addSecurityTokenToMetaHTML($container); + + parseHtml(null, $container, array(), false, $tabindex, 'user'); + 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"; + $container = new htmlResponsiveRow(); + $container->add(new htmlTitle(_("Export")), 12); + + $baseDnField = new htmlResponsiveInputField(_('Base DN'), 'baseDn', getDefaultBaseDn(), '751', true); + $baseDnField->showDnSelection(); + $container->add($baseDnField, 12); + + $searchScopes = array( + _('Base (base dn only)') => 'base', + _('One (one level beneath base)') => 'one', + _('Sub (entire subtree)') => 'sub' + ); + $searchScopeSelect = new htmlResponsiveSelect('searchScope', $searchScopes, array('sub'), _('Search scope')); + $searchScopeSelect->setHasDescriptiveElements(true); + $searchScopeSelect->setSortElements(false); + $container->add($searchScopeSelect, 12); + $container->add(new htmlResponsiveInputField(_('Search filter'), 'filter', '(objectClass=*)', '752'), 12); + $container->add(new htmlResponsiveInputField(_('Attributes'), 'attributes', '*', '753'), 12); + $container->add(new htmlResponsiveInputCheckbox('includeSystem', false, _('Include system attributes'), '754'), 12); + $container->add(new htmlResponsiveInputCheckbox('saveAsFile', false, _('Save as file')), 12); + + $formats = array( + 'CSV' => 'csv', + 'LDIF' => 'ldif' + ); + $formatSelect = new htmlResponsiveSelect('format', $formats, array('ldif'), _('Export format')); + $formatSelect->setHasDescriptiveElements(true); + $formatSelect->setSortElements(false); + $container->add($formatSelect, 12); + + $endings = array( + 'Windows' => 'windows', + 'Unix' => 'unix' + ); + $endingsSelect = new htmlResponsiveSelect('ending', $endings, array('unix'), _('End of line')); + $endingsSelect->setHasDescriptiveElements(true); + $endingsSelect->setSortElements(false); + $container->add($endingsSelect, 12); + + $container->addVerticalSpacer('3rem'); + $button = new htmlButton('submitExport', _('Submit')); + $container->add($button, 12, 12, 12, 'text-center'); + + addSecurityTokenToMetaHTML($container); + + parseHtml(null, $container, array(), false, $tabindex, 'user'); + 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"; + $container = new htmlResponsiveRow(); + $container->add(new htmlTitle(_("Export")), 12); + + $container->add(new htmlHiddenInput('baseDn', $_POST['baseDn']), 12); + $container->add(new htmlHiddenInput('searchScope', $_POST['searchScope']), 12); + $container->add(new htmlHiddenInput('filter', $_POST['filter']), 12); + $container->add(new htmlHiddenInput('attributes', $_POST['attributes']), 12); + $container->add(new htmlHiddenInput('format', $_POST['format']), 12); + $container->add(new htmlHiddenInput('ending', $_POST['ending']), 12); + $container->add(new htmlHiddenInput('includeSystem', isset($_POST['includeSystem']) && ($_POST['includeSystem'] === 'on') ? 'true' : 'false'), 12); + $container->add(new htmlHiddenInput('saveAsFile', isset($_POST['saveAsFile']) && ($_POST['saveAsFile'] === 'on') ? 'true' : 'false'), 12); + + $container->add(new htmlDiv('statusExportInprogress', new htmlOutputText(_('Status') . ': ' . _('in progress'))), 12); + $container->add(new htmlDiv('statusExportDone', new htmlOutputText(_('Status') . ': ' . _('done')), array('hidden')), 12); + $container->add(new htmlDiv('statusExportFailed', new htmlOutputText(_('Status') . ': ' . _('failed')), array('hidden')), 12); + $container->addVerticalSpacer('1rem'); + $container->add(new htmlDiv('progressbarExport', new htmlOutputText('')), 12); + $container->addVerticalSpacer('3rem'); + $button = new htmlButton('submitExportCancel', _('Cancel')); + $container->add($button, 12, 12, 12, 'text-center'); + + $newExportButton = new htmlLink(_('New export'), null, null, true); + $container->add($newExportButton, 12, 12, 12, 'text-center hidden newexport'); + + $container->addVerticalSpacer('3rem'); + + $exportText = new htmlOutputText(''); + $exportText->setPreformatted(true); + $container->add(new htmlDiv('exportResults', $exportText), 12); + $container->add(new htmlJavaScript( + 'window.lam.importexport.startExport(\'' . getSecurityTokenName() . '\', \'' . getSecurityTokenValue() . '\');' + ), 12); + + addSecurityTokenToMetaHTML($container); + + parseHtml(null, $container, array(), false, $tabindex, 'user'); + 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'])); + } + +}