From 33b35fa23be00a7f741acf9fcff460c6f7fe5206 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sat, 1 Sep 2018 13:36:04 +0200 Subject: [PATCH] import full entries --- lam/lib/import.inc | 273 +++++++++++++++++++++++++-- lam/templates/lib/500_lam.js | 11 ++ lam/templates/misc/ajax.php | 2 + lam/templates/tools/importexport.php | 89 +++------ lam/tests/lib/importTest.php | 125 ++++++++++++ 5 files changed, 421 insertions(+), 79 deletions(-) create mode 100644 lam/tests/lib/importTest.php diff --git a/lam/lib/import.inc b/lam/lib/import.inc index e22603c8..40d72724 100644 --- a/lam/lib/import.inc +++ b/lam/lib/import.inc @@ -1,5 +1,7 @@ extractImportChunks($lines); + $tasks = $this->convertToTasks($chunks); + return $tasks; + } /** * Processes the import data stored in session. */ public function doImport() { - $entries = &$_SESSION[Importer::SESSION_KEY_ENTRIES]; + $data = ''; + $tasks = &$_SESSION[Importer::SESSION_KEY_TASKS]; // check if any actions are needed at all - if (empty($entries)) { - return $this->getStatus(); + if (empty($tasks)) { + return $this->getStatus($data); } $endTime = $this->getEndTime(); - while ((!empty($entries)) && ($endTime > time())) { - $this->continueImport($entries); + while ((!empty($tasks)) && ($endTime > time())) { + $task = array_shift($tasks); + try { + $data .= $task->run(); + } + catch (LAMException $e) { + return $this->stopImport($data, $e); + } } - return $this->getStatus(); + 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() { - if (empty($entries)) { - if (isset($_SESSION[Importer::SESSION_KEY_ENTRIES])) { - unset($_SESSION[Importer::SESSION_KEY_ENTRIES]); + 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::STATUS => 'done', + Importer::DATA => $data ); return json_encode($status); } - $progress = (sizeof($_SESSION[Importer::SESSION_KEY_ENTRIES]) / $_SESSION[Importer::SESSION_KEY_COUNT]) * 100.0; + $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 => '' + Importer::DATA => $data ); return json_encode($status); } @@ -108,9 +199,159 @@ class Importer { * Continues the import with processing of a single entry. * * @param array[] $entries import entries + * @return ImporterTask[] tasks */ - private function continueImport(&$entries) { - $entry = array_shift($entries); + 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); + } + } + + /** + * 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 (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)); } } diff --git a/lam/templates/lib/500_lam.js b/lam/templates/lib/500_lam.js index be320477..e23dbb34 100644 --- a/lam/templates/lib/500_lam.js +++ b/lam/templates/lib/500_lam.js @@ -911,6 +911,7 @@ window.lam.import = window.lam.import || {}; */ window.lam.import.startImport = function(tokenName, tokenValue) { jQuery(document).ready(function() { + jQuery('#progressbarImport').progressbar(); var output = jQuery('#importResults'); var data = { jsonInput: '' @@ -922,11 +923,21 @@ window.lam.import.startImport = function(tokenName, tokenValue) { 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({ diff --git a/lam/templates/misc/ajax.php b/lam/templates/misc/ajax.php index 46b2c458..a246e67c 100644 --- a/lam/templates/misc/ajax.php +++ b/lam/templates/misc/ajax.php @@ -31,6 +31,8 @@ use \LAM\TOOLS\IMPORT_EXPORT\Importer; /** security functions */ include_once("../../lib/security.inc"); +/** LDIF import */ +include_once("../../lib/import.inc"); // start session if (isset($_GET['selfservice'])) { diff --git a/lam/templates/tools/importexport.php b/lam/templates/tools/importexport.php index b970c794..dee30865 100644 --- a/lam/templates/tools/importexport.php +++ b/lam/templates/tools/importexport.php @@ -10,6 +10,8 @@ use \htmlStatusMessage; use \htmlDiv; use \htmlOutputText; use \htmlJavaScript; +use \LAMException; +use \htmlLink; /* @@ -66,8 +68,8 @@ if (!empty($_POST)) { } // clean old data -if (isset($_SESSION[Importer::SESSION_KEY_ENTRIES])) { - unset($_SESSION[Importer::SESSION_KEY_ENTRIES]); +if (isset($_SESSION[Importer::SESSION_KEY_TASKS])) { + unset($_SESSION[Importer::SESSION_KEY_TASKS]); } if (isset($_SESSION[Importer::SESSION_KEY_COUNT])) { unset($_SESSION[Importer::SESSION_KEY_COUNT]); @@ -157,13 +159,14 @@ function printImportTabContent(&$tabindex) { * @param int $tabindex tabindex */ function printImportTabProcessing(&$tabindex) { - $message = checkImportData(); - if (!empty($message)) { + try { + checkImportData(); + } + catch (LAMException $e) { $container = new htmlResponsiveRow(); - $container->add(new htmlStatusMessage('ERROR', $message), 12); + $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(); @@ -171,17 +174,23 @@ function printImportTabProcessing(&$tabindex) { $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.import.startImport(\'' . getSecurityTokenName() . '\', \'' . getSecurityTokenValue() . '\');' ), 12); - $container->addVerticalSpacer('3rem'); - - $button = new htmlButton('submitImportCancel', _('Cancel')); - $container->add($button, 12, 12, 12, 'text-center'); - addSecurityTokenToMetaHTML($container); parseHtml(null, $container, array(), false, $tabindex, 'user'); @@ -191,7 +200,7 @@ function printImportTabProcessing(&$tabindex) { /** * Checks if the import data is ok. * - * @return string error message if not valid + * @throws LAMException error message if not valid */ function checkImportData() { $source = $_POST['source']; @@ -205,59 +214,13 @@ function checkImportData() { fclose($handle); } if (empty($ldif)) { - return _('You must either upload a file or provide an import in the text box.'); + 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); - $entriesData = extractImportEntries($lines); - if (!is_array($entriesData)) { - return $entriesData; - } - $_SESSION[Importer::SESSION_KEY_ENTRIES] = $entriesData; - $_SESSION[Importer::SESSION_KEY_COUNT] = sizeof($entriesData); -} - -/** - * Extracts the single entries in the file. - * - * @param string[] $lines LDIF lines - * @return string|array array of string[] - */ -function extractImportEntries($lines) { - $entries = array(); - $currentEntry = array(); - foreach ($lines as $line) { - if (substr(trim($line), 0, 1) === '#') { - // skip comments - continue; - } - if (empty(trim($line))) { - // end of entry - if (!empty($currentEntry)) { - $entries[] = $currentEntry; - $currentEntry = array(); - } - } - elseif (substr($line, 0, 1) === ' ') { - // append to last line if starting with a space - if (empty($currentEntry)) { - return _('Invalid data:') . ' ' . htmlspecialchars($line); - } - else { - $currentEntry[sizeof($currentEntry) - 1] .= substr($line, 1); - } - } - else { - $parts = explode(':', $line, 2); - if (sizeof($parts) < 2) { - return _('Invalid data:') . ' ' . htmlspecialchars($line); - } - $currentEntry[] = $line; - } - } - if (!empty($currentEntry)) { - $entries[] = $currentEntry; - } - return $entries; + $importer = new Importer(); + $tasks = $importer->getTasks($lines); + $_SESSION[Importer::SESSION_KEY_TASKS] = $tasks; + $_SESSION[Importer::SESSION_KEY_COUNT] = sizeof($tasks); } include '../../lib/adminFooter.inc'; diff --git a/lam/tests/lib/importTest.php b/lam/tests/lib/importTest.php new file mode 100644 index 00000000..ce579bee --- /dev/null +++ b/lam/tests/lib/importTest.php @@ -0,0 +1,125 @@ +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)); + } + +}