From 7222f10fe68c800931408b8b05962458a95b5e50 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Tue, 28 Aug 2018 20:20:36 +0200 Subject: [PATCH 01/26] import/export --- lam/graphics/importexport.png | Bin 0 -> 3498 bytes lam/lib/tools/importexport.inc | 131 +++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 lam/graphics/importexport.png create mode 100644 lam/lib/tools/importexport.inc diff --git a/lam/graphics/importexport.png b/lam/graphics/importexport.png new file mode 100644 index 0000000000000000000000000000000000000000..00d75f74aabb1bb3b77ed47edffae7fb22879de9 GIT binary patch literal 3498 zcmV;b4OQ}qP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=00004XF*Lt006O$eEU(80000WV@Og>004R=004l4008;_004mL004C` z008P>0026e000+nl3&F}0008TNkl#chWYMZzD%36t6wxAE1TA_l`yn zsDT_x01yBGApl_jV3iL3y%^GrKtezgF=~}KpXkgEKX^Jm)YVp2UESoU>D~K*ZMprbws_(B zo6U{+zH@gY)9>^BJ<093jN?222!stMQ*qpDPqjR2EV_{rq007V00_-3O?A|#4;M@H zGz&+aToRw1*g!NeGOxpZesbbrev8|((zUG6Yq{?TA=o;qS?*9pZ|s_l3kAkq| zXhL8{=Z#A~OS^K^>u}8QsHn|OId$b=smq!HiAV?_jGy|d8J!JCo~3yxi&1c!+k$jU zb9UzB<)vXQh?uNnPWSR2b0wW=V=yEgo8~4W8w%D{1!0c{V-BF&Hi4@_|Ap>J&xrd# zmW$1Is3im;NdFtX$d2vrl{W?{<9| zob!iNBd`_e_$)CF1{RMnhLJe(AJiS;a?_d%Lbm4IwVi(HzeJ`~VsFSDr77`0`5%4{ Y0G_IzJ5~C#VE_OC07*qoM6N<$f`Ko7cmMzZ literal 0 HcmV?d00001 diff --git a/lam/lib/tools/importexport.inc b/lam/lib/tools/importexport.inc new file mode 100644 index 00000000..4ea94277 --- /dev/null +++ b/lam/lib/tools/importexport.inc @@ -0,0 +1,131 @@ + \ No newline at end of file From 18a22ef1c4a10b6a14df8028575086a47d68ea51 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Tue, 28 Aug 2018 21:15:51 +0200 Subject: [PATCH 02/26] fixed file upload --- lam/lib/html.inc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lam/lib/html.inc b/lam/lib/html.inc index 1d5149e5..5b6c8b99 100644 --- a/lam/lib/html.inc +++ b/lam/lib/html.inc @@ -3988,6 +3988,8 @@ class htmlResponsiveInputFileUpload extends htmlInputFileUpload { private $label; /** help ID */ private $helpID; + /** help module */ + private $helpModule = null; /** render HTML of parent class */ private $renderParentHtml = false; @@ -4001,7 +4003,13 @@ class htmlResponsiveInputFileUpload extends htmlInputFileUpload { function __construct($name, $label, $helpID = null) { parent::__construct($name); $this->label = htmlspecialchars($label); - $this->helpID = $helpID; + if (is_string($helpID)) { + $this->helpID = $helpID; + } + elseif (is_array($helpID)) { + $this->helpID = $helpID[0]; + $this->helpModule = $helpID[1]; + } } /** From a4c867d6b340938bdbdd9e2e9e2512cfbdd80c19 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Wed, 29 Aug 2018 18:59:45 +0200 Subject: [PATCH 03/26] show/hide for radio --- lam/lib/html.inc | 129 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 125 insertions(+), 4 deletions(-) diff --git a/lam/lib/html.inc b/lam/lib/html.inc index 5b6c8b99..317ce4d5 100644 --- a/lam/lib/html.inc +++ b/lam/lib/html.inc @@ -1623,6 +1623,10 @@ class htmlRadio extends htmlElement { private $isEnabled = true; /** on change code */ private $onchangeEvent = null; + /** list of enclosing table rows to hide when checked */ + protected $tableRowsToHide = array(); + /** list of enclosing table rows to show when checked */ + protected $tableRowsToShow = array(); /** * Constructor. @@ -1663,13 +1667,23 @@ class htmlRadio extends htmlElement { if (!$this->isEnabled) { $disabled = ' disabled'; } - $onchange = ''; - if ($this->onchangeEvent != null) { - $onchange = ' onchange="' . $this->onchangeEvent . '"'; + if (($this->tableRowsToHide != null) || ($this->tableRowsToShow != null)) { + $this->printInitialState(); } // print radio list $counter = 0; foreach ($this->elements as $label => $value) { + $showHideOnchange = ''; + if (($this->tableRowsToHide != null) || ($this->tableRowsToShow != null)) { + $showHideOnchange = $this->getOnchangeCodeForShowHideTableRows($counter); + } + $onchange = ''; + if ($this->onchangeEvent != null) { + $onchange = ' onchange="' . $this->onchangeEvent . '"'; + } + elseif (!empty($showHideOnchange)) { + $onchange = ' onchange="' . $showHideOnchange . '"'; + } $onClick = 'onClick=" jQuery(\'input[name=' . $this->name . ']\').prop(\'checked\', false); jQuery(\'#' . $this->name . $counter . '\').prop(\'checked\', true); @@ -1709,6 +1723,104 @@ class htmlRadio extends htmlElement { $this->onchangeEvent = htmlspecialchars($onchangeEvent); } + /** + * Returns the selector to use to find the show/hide elements. + * + * @return string selector + */ + protected function getShowHideSelector() { + return 'tr'; + } + + /** + * Creates the JavaScript code to hide/show table rows based on the select value. + * + * @param int $counter index + * @return String onChange code + */ + private function getOnchangeCodeForShowHideTableRows($counter) { + $onChange = ''; + if ((sizeof($this->tableRowsToHide) == 0) && (sizeof($this->tableRowsToShow) == 0)) { + return; + } + $values = array(); + if (!empty($this->tableRowsToHide)) { + $values = array_merge($values, array_keys($this->tableRowsToHide)); + } + if (!empty($this->tableRowsToShow)) { + $values = array_merge($values, array_keys($this->tableRowsToShow)); + } + $values = array_unique($values); + $selector = $this->getShowHideSelector(); + // build Java script to show/hide depending fields + foreach ($values as $val) { + // build onChange listener + $onChange .= 'if (jQuery(\'#' . $this->name . $counter . '\').val() == \'' . $val . '\') {'; + if (isset($this->tableRowsToShow[$val])) { + for ($i = 0; $i < sizeof($this->tableRowsToShow[$val]); $i++) { + $onChange .= 'jQuery(\'#' . $this->tableRowsToShow[$val][$i] . '\').closest(\'' . $selector . '\').removeClass(\'hidden\');'; + } + } + if (isset($this->tableRowsToHide[$val])) { + for ($i = 0; $i < sizeof($this->tableRowsToHide[$val]); $i++) { + $onChange .= 'jQuery(\'#' . $this->tableRowsToHide[$val][$i] . '\').closest(\'' . $selector . '\').addClass(\'hidden\');'; + } + } + $onChange .= '};'; + } + return $onChange; + } + + private function printInitialState() { + $selector = $this->getShowHideSelector(); + // build script to set initial state + $script = ''; + 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 +2404,7 @@ class htmlInputFileUpload extends htmlElement { if (!$this->isEnabled) { $disabled = ' disabled'; } - echo ''; + echo ''; return array($this->name => 'file'); } @@ -4288,6 +4400,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'; + } + } /** From c9cff54937725c558e7ef09c6555408a45b434fb Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Fri, 31 Aug 2018 20:59:05 +0200 Subject: [PATCH 04/26] import tool --- lam/help/help.inc | 6 +- lam/lib/import.inc | 118 ++++++++++++ lam/templates/lib/500_lam.js | 37 ++++ lam/templates/misc/ajax.php | 11 +- lam/templates/tools/importexport.php | 263 +++++++++++++++++++++++++++ 5 files changed, 433 insertions(+), 2 deletions(-) create mode 100644 lam/lib/import.inc create mode 100644 lam/templates/tools/importexport.php diff --git a/lam/help/help.inc b/lam/help/help.inc index c248c9c1..f3cc2d19 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,10 @@ $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.')), // 800 - 899 // jobs '800' => array( diff --git a/lam/lib/import.inc b/lam/lib/import.inc new file mode 100644 index 00000000..e22603c8 --- /dev/null +++ b/lam/lib/import.inc @@ -0,0 +1,118 @@ +getStatus(); + } + $endTime = $this->getEndTime(); + while ((!empty($entries)) && ($endTime > time())) { + $this->continueImport($entries); + } + return $this->getStatus(); + } + + /** + * Returns the current status as JSON. + * + * @return string JSON status + */ + private function getStatus() { + if (empty($entries)) { + if (isset($_SESSION[Importer::SESSION_KEY_ENTRIES])) { + unset($_SESSION[Importer::SESSION_KEY_ENTRIES]); + } + $status = array( + Importer::STATUS => 'done' + ); + return json_encode($status); + } + $progress = (sizeof($_SESSION[Importer::SESSION_KEY_ENTRIES]) / $_SESSION[Importer::SESSION_KEY_COUNT]) * 100.0; + $progress = floor(100 - $progress); + $status = array( + Importer::STATUS => 'inProgress', + Importer::PROGRESS => $progress, + Importer::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 + */ + private function continueImport(&$entries) { + $entry = array_shift($entries); + } + +} + +?> diff --git a/lam/templates/lib/500_lam.js b/lam/templates/lib/500_lam.js index 862bedb3..be320477 100644 --- a/lam/templates/lib/500_lam.js +++ b/lam/templates/lib/500_lam.js @@ -901,6 +901,43 @@ window.lam.tools.schema.select = function() { }); }; +window.lam.import = window.lam.import || {}; + +/** + * Starts the import process. + * + * @param tokenName name of CSRF token + * @param tokenValue value of CSRF token + */ +window.lam.import.startImport = function(tokenName, tokenValue) { + jQuery(document).ready(function() { + 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.status == 'done') { + jQuery('#progressbarImport').hide(); + jQuery('#btn_submitImportCancel').hide(); + jQuery('#statusImportInprogress').hide(); + jQuery('#statusImportDone').show(); + } + else { + jQuery('#progressbarImport').progressbar({ + value: jsonData.progress + }); + window.lam.import.startImport(tokenName, tokenValue); + } + }); + }); +}; + 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..46b2c458 100644 --- a/lam/templates/misc/ajax.php +++ b/lam/templates/misc/ajax.php @@ -1,5 +1,6 @@ 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 === 'upload') { include_once('../../lib/upload.inc'); $typeManager = new \LAM\TYPES\TypeManager(); $uploader = new \LAM\UPLOAD\Uploader($typeManager->getConfiguredType($_GET['typeId'])); diff --git a/lam/templates/tools/importexport.php b/lam/templates/tools/importexport.php new file mode 100644 index 00000000..b970c794 --- /dev/null +++ b/lam/templates/tools/importexport.php @@ -0,0 +1,263 @@ + + + + +
+
+
    +
  • + import +
  • +
  • + export +
  • +
+
+ +
+
+
+
+
+ +\n"; + $container = new htmlResponsiveRow(); + $container->add(new htmlTitle(_("Import")), 12); + $sources = array( + _('File') => 'file', + _('Text input') => 'text' + ); + $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->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) { + $message = checkImportData(); + if (!empty($message)) { + $container = new htmlResponsiveRow(); + $container->add(new htmlStatusMessage('ERROR', $message), 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('progressbarImport', new htmlOutputText('')), 12); + $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'); + echo ("
\n"); +} + +/** + * Checks if the import data is ok. + * + * @return string 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)) { + return _('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; +} + +include '../../lib/adminFooter.inc'; From 33b35fa23be00a7f741acf9fcff460c6f7fe5206 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sat, 1 Sep 2018 13:36:04 +0200 Subject: [PATCH 05/26] 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)); + } + +} From a44350407eae78e8d5fcc445b9112c23fbb0c04c Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Tue, 4 Sep 2018 19:34:03 +0200 Subject: [PATCH 06/26] import changetype add --- lam/graphics/importexport.png | Bin 3498 -> 855 bytes lam/lib/import.inc | 125 ++++++++++++++++++++++++++++++++++ lam/tests/lib/importTest.php | 41 +++++++++++ 3 files changed, 166 insertions(+) diff --git a/lam/graphics/importexport.png b/lam/graphics/importexport.png index 00d75f74aabb1bb3b77ed47edffae7fb22879de9..f2445164cff0aae5484a4019717394cec09443e5 100644 GIT binary patch delta 832 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 Ws-8Pl`mgetChangeTypeTask($dn, $currentLines, $type); + $currentLines = array(); + $i++; + $line = $entry[$i]; + $lineTypeData = $this->getLineKeyValue($line); + if ($lineTypeData[Importer::KEY] != Importer::CHANGETYPE) { + throw new LAMException(_('Invalid data'), htmlspecialchars($line)); + } + } + $currentLines[] = $line; + } + $subtasks[] = $this->getChangeTypeTask($dn, $currentLines, $type); + return new MultiTask($subtasks); + } + } + + /** + * Returns a task for LDIF changeType entry. + * + * @param string $dn DN + * @param string $lines lines + * @param string $type change type + * @return ImporterTask task + */ + private function getChangeTypeTask($dn, $lines, $type) { + $attributes = array(); + foreach ($lines as $line) { + $lineData = $this->getLineKeyValue($line); + $attributes[$lineData[Importer::KEY]][] = $lineData[Importer::VALUE]; + } + if ($type === 'add') { + return new AddAttributesTask($dn, $attributes); + } + throw new LAMException(_('Invalid data'), htmlspecialchars($dn) . ' - ' . Importer::CHANGETYPE . ': ' . htmlspecialchars($type)); } /** @@ -293,6 +336,9 @@ class Importer { */ 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))); } @@ -356,4 +402,83 @@ class AddEntryTask implements ImporterTask { } +/** + * Combines multiple import tasks. + * + * @author Roland Gruber + */ +class MultiTask implements ImporterTask { + + /** + * @var ImporterTask[] tasks + */ + private $tasks = array(); + + /** + * Constructor + * + * @param ImporterTask[] $tasks tasks + */ + public function __construct($tasks) { + $this->tasks = $tasks; + } + + /** + * {@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)); + } + +} + ?> diff --git a/lam/tests/lib/importTest.php b/lam/tests/lib/importTest.php index ce579bee..caa5f6b5 100644 --- a/lam/tests/lib/importTest.php +++ b/lam/tests/lib/importTest.php @@ -1,5 +1,7 @@ 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)); + $multiTask = $tasks[0]; + $this->assertEquals(MultiTask::class, get_class($multiTask)); + $this->assertEquals(1, sizeof($multiTask->getTasks())); + $this->assertEquals(AddAttributesTask::class, get_class($multiTask->getTasks()[0])); + } + } From 109e7d679ce5bca8150c2e132ce7c2fa973cc1e6 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 6 Sep 2018 20:53:22 +0200 Subject: [PATCH 07/26] add changetype --- lam/lib/import.inc | 20 +++++++++++++++++--- lam/tests/lib/importTest.php | 7 +++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lam/lib/import.inc b/lam/lib/import.inc index 995a527f..ed474d01 100644 --- a/lam/lib/import.inc +++ b/lam/lib/import.inc @@ -267,6 +267,14 @@ class Importer { } 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); + } $changes = array(); $subtasks = array(); $currentLines = array(); @@ -286,7 +294,7 @@ class Importer { $currentLines[] = $line; } $subtasks[] = $this->getChangeTypeTask($dn, $currentLines, $type); - return new MultiTask($subtasks); + return new MultiTask($subtasks, $dn); } } @@ -305,7 +313,7 @@ class Importer { $attributes[$lineData[Importer::KEY]][] = $lineData[Importer::VALUE]; } if ($type === 'add') { - return new AddAttributesTask($dn, $attributes); + return new AddEntryTask($dn, $attributes); } throw new LAMException(_('Invalid data'), htmlspecialchars($dn) . ' - ' . Importer::CHANGETYPE . ': ' . htmlspecialchars($type)); } @@ -414,13 +422,19 @@ class MultiTask implements ImporterTask { */ private $tasks = array(); + /** + * @var string DN + */ + private $dn = null; + /** * Constructor * * @param ImporterTask[] $tasks tasks */ - public function __construct($tasks) { + public function __construct($tasks, $dn) { $this->tasks = $tasks; + $this->dn = $dn; } /** diff --git a/lam/tests/lib/importTest.php b/lam/tests/lib/importTest.php index caa5f6b5..3c8caf62 100644 --- a/lam/tests/lib/importTest.php +++ b/lam/tests/lib/importTest.php @@ -2,6 +2,7 @@ use \LAM\TOOLS\IMPORT_EXPORT\Importer; use LAM\TOOLS\IMPORT_EXPORT\MultiTask; use LAM\TOOLS\IMPORT_EXPORT\AddAttributesTask; +use LAM\TOOLS\IMPORT_EXPORT\AddEntryTask; /* This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/) @@ -157,10 +158,8 @@ class ImporterTest extends PHPUnit_Framework_TestCase { $importer = new Importer(); $tasks = $importer->getTasks($lines); $this->assertEquals(1, sizeof($tasks)); - $multiTask = $tasks[0]; - $this->assertEquals(MultiTask::class, get_class($multiTask)); - $this->assertEquals(1, sizeof($multiTask->getTasks())); - $this->assertEquals(AddAttributesTask::class, get_class($multiTask->getTasks()[0])); + $task = $tasks[0]; + $this->assertEquals(AddEntryTask::class, get_class($task)); } } From 1cbe9d546f34ff8b331f7e09ceb66d250c08ee3f Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 6 Sep 2018 21:19:07 +0200 Subject: [PATCH 08/26] changetype modrdn --- lam/lib/import.inc | 67 ++++++++++++++++++++++++++++++++++++ lam/tests/lib/importTest.php | 58 +++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/lam/lib/import.inc b/lam/lib/import.inc index ed474d01..27674fbb 100644 --- a/lam/lib/import.inc +++ b/lam/lib/import.inc @@ -275,6 +275,9 @@ class Importer { } return new AddEntryTask($dn, $attributes); } + elseif ($type === 'modrdn') { + return $this->createModRdnTask($dn, $entry); + } $changes = array(); $subtasks = array(); $currentLines = array(); @@ -298,6 +301,31 @@ class Importer { } } + /** + * 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) . '
' . $delOldRdnData); + } + $delOldRdn = ($delOldRdnData[Importer::VALUE] === '0') ? false : true; + return new RenameEntryTask($dn, $newRdn, $delOldRdn); + } + /** * Returns a task for LDIF changeType entry. * @@ -410,6 +438,45 @@ class AddEntryTask implements ImporterTask { } +/** + * 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)); + } + +} + /** * Combines multiple import tasks. * diff --git a/lam/tests/lib/importTest.php b/lam/tests/lib/importTest.php index 3c8caf62..a8cfdc74 100644 --- a/lam/tests/lib/importTest.php +++ b/lam/tests/lib/importTest.php @@ -3,6 +3,7 @@ use \LAM\TOOLS\IMPORT_EXPORT\Importer; use LAM\TOOLS\IMPORT_EXPORT\MultiTask; use LAM\TOOLS\IMPORT_EXPORT\AddAttributesTask; use LAM\TOOLS\IMPORT_EXPORT\AddEntryTask; +use LAM\TOOLS\IMPORT_EXPORT\RenameEntryTask; /* This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/) @@ -162,4 +163,61 @@ class ImporterTest extends PHPUnit_Framework_TestCase { $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)); + } + } From fe3c054825e1ae36b57efed7255ef52131ebcbc7 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 6 Sep 2018 21:30:05 +0200 Subject: [PATCH 09/26] changetype delete --- lam/lib/import.inc | 39 ++++++++++++++++++++++++++++++++++++ lam/tests/lib/importTest.php | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/lam/lib/import.inc b/lam/lib/import.inc index 27674fbb..9110d089 100644 --- a/lam/lib/import.inc +++ b/lam/lib/import.inc @@ -278,6 +278,12 @@ class Importer { 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); + } $changes = array(); $subtasks = array(); $currentLines = array(); @@ -477,6 +483,39 @@ class RenameEntryTask implements ImporterTask { } +/** + * 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. * diff --git a/lam/tests/lib/importTest.php b/lam/tests/lib/importTest.php index a8cfdc74..9cfbcff9 100644 --- a/lam/tests/lib/importTest.php +++ b/lam/tests/lib/importTest.php @@ -4,6 +4,7 @@ use LAM\TOOLS\IMPORT_EXPORT\MultiTask; use LAM\TOOLS\IMPORT_EXPORT\AddAttributesTask; use LAM\TOOLS\IMPORT_EXPORT\AddEntryTask; use LAM\TOOLS\IMPORT_EXPORT\RenameEntryTask; +use LAM\TOOLS\IMPORT_EXPORT\DeleteEntryTask; /* This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/) @@ -220,4 +221,40 @@ class ImporterTest extends PHPUnit_Framework_TestCase { $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)); + } + } From 5b81c8e03c808bc9f9066fb9ac99e97c9dffebc4 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sat, 15 Sep 2018 15:03:47 +0200 Subject: [PATCH 10/26] modify - add, delete, replace --- lam/lib/import.inc | 196 ++++++++++++++++++++++++++++++++--- lam/tests/lib/importTest.php | 182 ++++++++++++++++++++++++++++++++ 2 files changed, 363 insertions(+), 15 deletions(-) diff --git a/lam/lib/import.inc b/lam/lib/import.inc index 9110d089..d96475f1 100644 --- a/lam/lib/import.inc +++ b/lam/lib/import.inc @@ -82,6 +82,9 @@ class Importer { $currentEntry[sizeof($currentEntry) - 1] .= substr($line, 1); } } + elseif ($line === '-') { + $currentEntry[] = $line; + } else { $parts = explode(':', $line, 2); if (sizeof($parts) < 2) { @@ -284,6 +287,9 @@ class Importer { } return new DeleteEntryTask($dn); } + elseif ($type !== 'modify') { + throw new LAMException(_('Invalid data'), htmlspecialchars($dn) . ' - changeType: ' . htmlspecialchars($type)); + } $changes = array(); $subtasks = array(); $currentLines = array(); @@ -291,18 +297,14 @@ class Importer { for ($i = 0; $i < $linesCount; $i++) { $line = $entry[$i]; if ($line === '-') { - $subtasks[] = $this->getChangeTypeTask($dn, $currentLines, $type); + $subtasks[] = $this->getChangeTypeTask($dn, $currentLines); $currentLines = array(); - $i++; - $line = $entry[$i]; - $lineTypeData = $this->getLineKeyValue($line); - if ($lineTypeData[Importer::KEY] != Importer::CHANGETYPE) { - throw new LAMException(_('Invalid data'), htmlspecialchars($line)); - } } - $currentLines[] = $line; + else { + $currentLines[] = $line; + } } - $subtasks[] = $this->getChangeTypeTask($dn, $currentLines, $type); + $subtasks[] = $this->getChangeTypeTask($dn, $currentLines); return new MultiTask($subtasks, $dn); } } @@ -326,7 +328,7 @@ class Importer { $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) . '
' . $delOldRdnData); + throw new LAMException(_('Invalid data'), htmlspecialchars($dn) . '
' . $entry[1]); } $delOldRdn = ($delOldRdnData[Importer::VALUE] === '0') ? false : true; return new RenameEntryTask($dn, $newRdn, $delOldRdn); @@ -337,19 +339,31 @@ class Importer { * * @param string $dn DN * @param string $lines lines - * @param string $type change type * @return ImporterTask task */ - private function getChangeTypeTask($dn, $lines, $type) { + 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); - $attributes[$lineData[Importer::KEY]][] = $lineData[Importer::VALUE]; + if ($lineData[Importer::KEY] !== $attributeName) { + throw new LAMException(_('Invalid data'), htmlspecialchars($dn) . ' - ' . htmlspecialchars($type)); + } + $attributes[$attributeName][] = $lineData[Importer::VALUE]; } if ($type === 'add') { - return new AddEntryTask($dn, $attributes); + return new AddAttributesTask($dn, $attributes); } - throw new LAMException(_('Invalid data'), htmlspecialchars($dn) . ' - ' . Importer::CHANGETYPE . ': ' . htmlspecialchars($type)); + 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)); } /** @@ -599,6 +613,158 @@ class AddAttributesTask implements ImporterTask { 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/tests/lib/importTest.php b/lam/tests/lib/importTest.php index 9cfbcff9..991ce0ad 100644 --- a/lam/tests/lib/importTest.php +++ b/lam/tests/lib/importTest.php @@ -5,6 +5,8 @@ use LAM\TOOLS\IMPORT_EXPORT\AddAttributesTask; use LAM\TOOLS\IMPORT_EXPORT\AddEntryTask; use LAM\TOOLS\IMPORT_EXPORT\RenameEntryTask; use LAM\TOOLS\IMPORT_EXPORT\DeleteEntryTask; +use LAM\TOOLS\IMPORT_EXPORT\DeleteAttributesTask; +use LAM\TOOLS\IMPORT_EXPORT\ReplaceAttributesTask; /* This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/) @@ -257,4 +259,184 @@ class ImporterTest extends PHPUnit_Framework_TestCase { $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 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'])); + } + } From 2b9d77534776ab5ec070e06d72b63574b613c155 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sat, 15 Sep 2018 15:07:53 +0200 Subject: [PATCH 11/26] test --- lam/tests/lib/importTest.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lam/tests/lib/importTest.php b/lam/tests/lib/importTest.php index 991ce0ad..b60528fa 100644 --- a/lam/tests/lib/importTest.php +++ b/lam/tests/lib/importTest.php @@ -277,6 +277,26 @@ class ImporterTest extends PHPUnit_Framework_TestCase { $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. */ From cd749730a4dfe522abbc7b106b044629a79fc09f Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sat, 15 Sep 2018 15:10:50 +0200 Subject: [PATCH 12/26] fixed error handling --- lam/templates/tools/importexport.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lam/templates/tools/importexport.php b/lam/templates/tools/importexport.php index dee30865..8005b0b5 100644 --- a/lam/templates/tools/importexport.php +++ b/lam/templates/tools/importexport.php @@ -167,6 +167,7 @@ function printImportTabProcessing(&$tabindex) { $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(); From 0cc31a4391bfdfeb2e7b00ce1c3b48e76b9af42c Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sat, 15 Sep 2018 18:25:53 +0200 Subject: [PATCH 13/26] added checkbox to not stop on error --- lam/lib/import.inc | 9 ++++++++- lam/templates/tools/importexport.php | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lam/lib/import.inc b/lam/lib/import.inc index d96475f1..3c780c54 100644 --- a/lam/lib/import.inc +++ b/lam/lib/import.inc @@ -43,6 +43,7 @@ class Importer { const SESSION_KEY_TASKS = 'import_tasks'; const SESSION_KEY_COUNT = 'import_count'; + const SESSION_KEY_STOP_ON_ERROR = 'import_stop_on_error'; const STATUS = 'status'; const PROGRESS = 'progress'; const DATA = 'data'; @@ -118,6 +119,7 @@ class Importer { 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); @@ -129,7 +131,12 @@ class Importer { $data .= $task->run(); } catch (LAMException $e) { - return $this->stopImport($data, $e); + if ($stopOnError) { + return $this->stopImport($data, $e); + } + else { + $data .= Importer::formatMessage('ERROR', $e->getTitle(), $e->getMessage()); + } } } return $this->getStatus($data); diff --git a/lam/templates/tools/importexport.php b/lam/templates/tools/importexport.php index 8005b0b5..f5aa1ee1 100644 --- a/lam/templates/tools/importexport.php +++ b/lam/templates/tools/importexport.php @@ -74,6 +74,9 @@ if (isset($_SESSION[Importer::SESSION_KEY_TASKS])) { if (isset($_SESSION[Importer::SESSION_KEY_COUNT])) { unset($_SESSION[Importer::SESSION_KEY_COUNT]); } +if (isset($_SESSION[Importer::SESSION_KEY_STOP_ON_ERROR])) { + unset($_SESSION[Importer::SESSION_KEY_STOP_ON_ERROR]); +} include '../../lib/adminHeader.inc'; $tabindex = 1; @@ -142,6 +145,7 @@ function printImportTabContent(&$tabindex) { $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')); @@ -222,6 +226,7 @@ function checkImportData() { $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')); } include '../../lib/adminFooter.inc'; From 0bd7fcacf07afd405dc79c1a5c252fc1b9367efb Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sat, 15 Sep 2018 18:27:38 +0200 Subject: [PATCH 14/26] renamed tool --- lam/lib/tools/importexport.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lam/lib/tools/importexport.inc b/lam/lib/tools/importexport.inc index 4ea94277..c15d69f0 100644 --- a/lam/lib/tools/importexport.inc +++ b/lam/lib/tools/importexport.inc @@ -41,7 +41,7 @@ class ImportExport implements \LAMTool { * @return string name */ function getName() { - return _("Import/Export"); + return _("LDAP import/export"); } /** From 4afd3d940e9581d0b62e505ab2b5a0042f651d4a Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sat, 15 Sep 2018 20:13:57 +0200 Subject: [PATCH 15/26] added export form --- lam/templates/tools/importexport.php | 94 +++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/lam/templates/tools/importexport.php b/lam/templates/tools/importexport.php index f5aa1ee1..a10a12f4 100644 --- a/lam/templates/tools/importexport.php +++ b/lam/templates/tools/importexport.php @@ -12,6 +12,12 @@ use \htmlOutputText; use \htmlJavaScript; use \LAMException; use \htmlLink; +use \htmlResponsiveInputCheckbox; +use \htmlResponsiveSelect; +use \htmlResponsiveInputField; +use \htmlGroup; +use \htmlInputField; +use LAM\TYPES\TypeManager; /* @@ -109,6 +115,14 @@ include '../../lib/adminHeader.inc'; ?>
+
@@ -145,7 +159,7 @@ function printImportTabContent(&$tabindex) { $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->add(new htmlResponsiveInputCheckbox('noStop', false, _('Don\'t stop on errors')), 12); $container->addVerticalSpacer('3rem'); $button = new htmlButton('submitImport', _('Submit')); @@ -229,4 +243,82 @@ function checkImportData() { $_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); + + $container->addLabel(new htmlOutputText(_('Base DN'))); + $baseDnGroup = new htmlGroup(); + $baseDnGroup->addElement(new htmlInputField('baseDn', getDefaultBaseDn())); + $container->addField($baseDnGroup); + + $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=*)'), 12); + $container->add(new htmlResponsiveInputField(_('Attributes'), 'attributes', '*'), 12); + $container->add(new htmlResponsiveInputCheckbox('includeSystem', false, _('Include system attributes')), 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; +} + include '../../lib/adminFooter.inc'; From 98f6c2bf84bf7e0b2c8a016ab581da908c78f6e0 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sun, 23 Sep 2018 10:05:06 +0200 Subject: [PATCH 16/26] export --- lam/templates/tools/importexport.php | 80 +++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/lam/templates/tools/importexport.php b/lam/templates/tools/importexport.php index a10a12f4..cf3ec4bf 100644 --- a/lam/templates/tools/importexport.php +++ b/lam/templates/tools/importexport.php @@ -85,12 +85,20 @@ if (isset($_SESSION[Importer::SESSION_KEY_STOP_ON_ERROR])) { } include '../../lib/adminHeader.inc'; - $tabindex = 1; +$tabindex = 1; +$activeTab = 0; +if (!empty($_GET['tab']) && ($_GET['tab'] === 'export')) { + $activeTab = 1; +} + ?> @@ -135,7 +143,7 @@ include '../../lib/adminHeader.inc'; * @param int $tabindex tabindex */ function printImportTabContent(&$tabindex) { - echo "
\n"; + echo "\n"; $container = new htmlResponsiveRow(); $container->add(new htmlTitle(_("Import")), 12); $sources = array( @@ -187,7 +195,7 @@ function printImportTabProcessing(&$tabindex) { printImportTabContent($tabindex); return; } - echo "\n"; + echo "\n"; $container = new htmlResponsiveRow(); $container->add(new htmlTitle(_("Import")), 12); @@ -249,13 +257,15 @@ function checkImportData() { * @param int $tabindex tabindex */ function printExportTabContent(&$tabindex) { - echo "\n"; + echo "\n"; $container = new htmlResponsiveRow(); $container->add(new htmlTitle(_("Export")), 12); - $container->addLabel(new htmlOutputText(_('Base DN'))); + $container->addLabel(new htmlOutputText(_('Base DN'), true, true)); $baseDnGroup = new htmlGroup(); - $baseDnGroup->addElement(new htmlInputField('baseDn', getDefaultBaseDn())); + $baseDnInput = new htmlInputField('baseDn', getDefaultBaseDn()); + $baseDnInput->setRequired(true); + $baseDnGroup->addElement($baseDnInput); $container->addField($baseDnGroup); $searchScopes = array( @@ -321,4 +331,60 @@ function getDefaultBaseDn() { 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 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'); + + $container->add(new htmlDiv('exportResults', new htmlOutputText('')), 12); + $container->add(new htmlJavaScript( + 'window.lam.export.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'; From 23e58208cf586a65d1815f578e33093efc1b493b Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sun, 23 Sep 2018 10:06:25 +0200 Subject: [PATCH 17/26] added libapache2-mod-fcgid --- lam-packaging/debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lam-packaging/debian/control b/lam-packaging/debian/control index 23febc6e..c67ac0ba 100644 --- a/lam-packaging/debian/control +++ b/lam-packaging/debian/control @@ -12,7 +12,7 @@ Depends: php5 (>= 5.4.26) | php (>= 7), php5-ldap | php-ldap, php5-gd | php-gd | php5-imagick | php-imagick, php5-json | php-json, php5-curl | php-curl, php5 | php-zip, php5 | php-xml, - libapache2-mod-php5 | libapache2-mod-php | php5-fpm | php-fpm, + libapache2-mod-php5 | libapache2-mod-php | libapache2-mod-fcgid | php5-fpm | php-fpm, php-tcpdf, php-phpseclib (>= 2.0), apache2 (>= 2.4.0) | httpd, fonts-dejavu, debconf (>= 0.2.26) | debconf-2.0, ${misc:Depends} Recommends: php-apc | php-opcache From f2d77dc8515859f86be1328f28aa3a78203a689f Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sun, 23 Sep 2018 20:12:27 +0200 Subject: [PATCH 18/26] export --- lam/lib/export.inc | 127 +++++++++++++++++++++++++++ lam/lib/import.inc | 2 +- lam/templates/lib/500_lam.js | 53 ++++++++++- lam/templates/misc/ajax.php | 17 ++++ lam/templates/tools/importexport.php | 16 +++- 5 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 lam/lib/export.inc diff --git a/lam/lib/export.inc b/lam/lib/export.inc new file mode 100644 index 00000000..f084db75 --- /dev/null +++ b/lam/lib/export.inc @@ -0,0 +1,127 @@ +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(); + } + 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')); + } + } + +} diff --git a/lam/lib/import.inc b/lam/lib/import.inc index 3c780c54..6288eece 100644 --- a/lam/lib/import.inc +++ b/lam/lib/import.inc @@ -34,7 +34,7 @@ use \LAMException; include_once('ldap.inc'); /** - * Creates LDAP accounts for file upload. + * Imports LDIF files. * * @author Roland Gruber * @package tools diff --git a/lam/templates/lib/500_lam.js b/lam/templates/lib/500_lam.js index e23dbb34..d77f2343 100644 --- a/lam/templates/lib/500_lam.js +++ b/lam/templates/lib/500_lam.js @@ -901,7 +901,7 @@ window.lam.tools.schema.select = function() { }); }; -window.lam.import = window.lam.import || {}; +window.lam.importexport = window.lam.importexport || {}; /** * Starts the import process. @@ -909,7 +909,7 @@ window.lam.import = window.lam.import || {}; * @param tokenName name of CSRF token * @param tokenValue value of CSRF token */ -window.lam.import.startImport = function(tokenName, tokenValue) { +window.lam.importexport.startImport = function(tokenName, tokenValue) { jQuery(document).ready(function() { jQuery('#progressbarImport').progressbar(); var output = jQuery('#importResults'); @@ -949,6 +949,55 @@ window.lam.import.startImport = function(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(); + } + else { + jQuery('#progressbarExport').hide(); + jQuery('#btn_submitExportCancel').hide(); + jQuery('#statusExportInprogress').hide(); + jQuery('#statusExportFailed').show(); + jQuery('.newexport').show(); + } + }); + }); +}; + 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 a246e67c..e8e53a8e 100644 --- a/lam/templates/misc/ajax.php +++ b/lam/templates/misc/ajax.php @@ -1,6 +1,7 @@ doExport(); + ob_end_clean(); + echo $jsonOut; + } elseif ($function === 'upload') { include_once('../../lib/upload.inc'); $typeManager = new \LAM\TYPES\TypeManager(); diff --git a/lam/templates/tools/importexport.php b/lam/templates/tools/importexport.php index cf3ec4bf..0e3f4f67 100644 --- a/lam/templates/tools/importexport.php +++ b/lam/templates/tools/importexport.php @@ -17,6 +17,7 @@ use \htmlResponsiveSelect; use \htmlResponsiveInputField; use \htmlGroup; use \htmlInputField; +use \htmlHiddenInput; use LAM\TYPES\TypeManager; /* @@ -215,7 +216,7 @@ function printImportTabProcessing(&$tabindex) { $container->add(new htmlDiv('importResults', new htmlOutputText('')), 12); $container->add(new htmlJavaScript( - 'window.lam.import.startImport(\'' . getSecurityTokenName() . '\', \'' . getSecurityTokenValue() . '\');' + 'window.lam.importexport.startImport(\'' . getSecurityTokenName() . '\', \'' . getSecurityTokenValue() . '\');' ), 12); addSecurityTokenToMetaHTML($container); @@ -273,7 +274,7 @@ function printExportTabContent(&$tabindex) { _('One (one level beneath base)') => 'one', _('Sub (entire subtree)') => 'sub' ); - $searchScopeSelect = new htmlResponsiveSelect('searchscope', $searchScopes, array('sub'), _('Search scope')); + $searchScopeSelect = new htmlResponsiveSelect('searchScope', $searchScopes, array('sub'), _('Search scope')); $searchScopeSelect->setHasDescriptiveElements(true); $searchScopeSelect->setSortElements(false); $container->add($searchScopeSelect, 12); @@ -351,6 +352,15 @@ function printExportTabProcessing(&$tabindex) { $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); @@ -367,7 +377,7 @@ function printExportTabProcessing(&$tabindex) { $container->add(new htmlDiv('exportResults', new htmlOutputText('')), 12); $container->add(new htmlJavaScript( - 'window.lam.export.startExport(\'' . getSecurityTokenName() . '\', \'' . getSecurityTokenValue() . '\');' + 'window.lam.importexport.startExport(\'' . getSecurityTokenName() . '\', \'' . getSecurityTokenValue() . '\');' ), 12); addSecurityTokenToMetaHTML($container); From 1d7db3794bc3917ae17e913e17d40ecc5e592d2f Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Thu, 4 Oct 2018 21:07:55 +0200 Subject: [PATCH 19/26] LDIF export --- lam/help/help.inc | 8 ++ lam/lib/account.inc | 26 +++++ lam/lib/export.inc | 151 +++++++++++++++++++++++++++ lam/templates/lib/500_lam.js | 13 +++ lam/templates/tools/importexport.php | 17 ++- 5 files changed, 205 insertions(+), 10 deletions(-) diff --git a/lam/help/help.inc b/lam/help/help.inc index f3cc2d19..c9d29e82 100644 --- a/lam/help/help.inc +++ b/lam/help/help.inc @@ -342,6 +342,14 @@ $helpArray = array ( // 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 d43a17b9..92ce6196 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 index f084db75..7d53a646 100644 --- a/lam/lib/export.inc +++ b/lam/lib/export.inc @@ -43,6 +43,8 @@ class Exporter { const DATA = 'data'; const STATUS = 'status'; + const FILE = 'file'; + const OUTPUT = 'output'; private $baseDn = null; private $searchScope = null; @@ -84,6 +86,8 @@ class Exporter { public function doExport() { try { $this->checkParameters(); + $results = $this->getLDAPData(); + return $this->writeDataAndReturnJson($results); } catch (LAMException $e) { $data = Exporter::formatMessage('ERROR', $e->getTitle(), $e->getMessage()); @@ -124,4 +128,151 @@ class Exporter { } } + /** + * 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), + 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) { + return 'CSV'; + } + + /** + * 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/templates/lib/500_lam.js b/lam/templates/lib/500_lam.js index d77f2343..db88a714 100644 --- a/lam/templates/lib/500_lam.js +++ b/lam/templates/lib/500_lam.js @@ -986,6 +986,12 @@ window.lam.importexport.startExport = function(tokenName, tokenValue) { 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(); @@ -994,6 +1000,13 @@ window.lam.importexport.startExport = function(tokenName, tokenValue) { jQuery('#statusExportFailed').show(); jQuery('.newexport').show(); } + }) + .fail(function() { + jQuery('#progressbarExport').hide(); + jQuery('#btn_submitExportCancel').hide(); + jQuery('#statusExportInprogress').hide(); + jQuery('#statusExportFailed').show(); + jQuery('.newexport').show(); }); }); }; diff --git a/lam/templates/tools/importexport.php b/lam/templates/tools/importexport.php index 0e3f4f67..f0b974f1 100644 --- a/lam/templates/tools/importexport.php +++ b/lam/templates/tools/importexport.php @@ -262,12 +262,7 @@ function printExportTabContent(&$tabindex) { $container = new htmlResponsiveRow(); $container->add(new htmlTitle(_("Export")), 12); - $container->addLabel(new htmlOutputText(_('Base DN'), true, true)); - $baseDnGroup = new htmlGroup(); - $baseDnInput = new htmlInputField('baseDn', getDefaultBaseDn()); - $baseDnInput->setRequired(true); - $baseDnGroup->addElement($baseDnInput); - $container->addField($baseDnGroup); + $container->add(new htmlResponsiveInputField(_('Base DN'), 'baseDn', getDefaultBaseDn(), '751', true), 12); $searchScopes = array( _('Base (base dn only)') => 'base', @@ -278,9 +273,9 @@ function printExportTabContent(&$tabindex) { $searchScopeSelect->setHasDescriptiveElements(true); $searchScopeSelect->setSortElements(false); $container->add($searchScopeSelect, 12); - $container->add(new htmlResponsiveInputField(_('Search filter'), 'filter', '(objectClass=*)'), 12); - $container->add(new htmlResponsiveInputField(_('Attributes'), 'attributes', '*'), 12); - $container->add(new htmlResponsiveInputCheckbox('includeSystem', false, _('Include system attributes')), 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( @@ -375,7 +370,9 @@ function printExportTabProcessing(&$tabindex) { $container->addVerticalSpacer('3rem'); - $container->add(new htmlDiv('exportResults', new htmlOutputText('')), 12); + $exportText = new htmlOutputText(''); + $exportText->setPreformatted(true); + $container->add(new htmlDiv('exportResults', $exportText), 12); $container->add(new htmlJavaScript( 'window.lam.importexport.startExport(\'' . getSecurityTokenName() . '\', \'' . getSecurityTokenValue() . '\');' ), 12); From ef41215d225519126476ec787009ae32258b7b7c Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sat, 6 Oct 2018 10:45:44 +0200 Subject: [PATCH 20/26] added CSV export --- lam/lib/export.inc | 47 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/lam/lib/export.inc b/lam/lib/export.inc index 7d53a646..43a9381b 100644 --- a/lam/lib/export.inc +++ b/lam/lib/export.inc @@ -184,7 +184,7 @@ class Exporter { )); } return json_encode(array( - Exporter::OUTPUT => htmlspecialchars($output), + Exporter::OUTPUT => htmlspecialchars($output, ENT_NOQUOTES), Exporter::STATUS => 'done' )); } @@ -196,7 +196,50 @@ class Exporter { * @param string $lineEnding line ending */ private function getCsvOutput(&$entries, $lineEnding) { - return 'CSV'; + $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) . '"'; } /** From a202ed882484b7e0f7320b6e609c6b16952a9225 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sat, 6 Oct 2018 19:31:16 +0200 Subject: [PATCH 21/26] disable tree import/export --- lam/templates/3rdParty/pla/config/config.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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, From 89df814e7733665b3eb45e6e77b9076154b9be65 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Sat, 6 Oct 2018 19:47:33 +0200 Subject: [PATCH 22/26] DN chooser --- lam/HISTORY | 5 ++++ lam/lib/html.inc | 20 +++++++++++++ lam/templates/lib/500_lam.js | 45 ++++++++++++++++++++++++++++ lam/templates/tools/importexport.php | 4 ++- 4 files changed, 73 insertions(+), 1 deletion(-) 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/lib/html.inc b/lam/lib/html.inc index 317ce4d5..34d5d65f 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 "