1<?php 2namespace LAM\TOOLS\MULTI_EDIT; 3use \htmlTable; 4use \htmlTitle; 5use \htmlSelect; 6use \htmlOutputText; 7use \htmlInputField; 8use \htmlSubTitle; 9use \htmlButton; 10use \htmlStatusMessage; 11use \htmlSpacer; 12use \htmlHiddenInput; 13use \htmlGroup; 14use \htmlDiv; 15use \htmlJavaScript; 16use \htmlLink; 17use \htmlInputTextarea; 18use \htmlResponsiveRow; 19use \htmlResponsiveSelect; 20use \htmlResponsiveInputField; 21use \htmlResponsiveTable; 22/* 23 24 This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/) 25 Copyright (C) 2013 - 2020 Roland Gruber 26 27 This program is free software; you can redistribute it and/or modify 28 it under the terms of the GNU General Public License as published by 29 the Free Software Foundation; either version 2 of the License, or 30 (at your option) any later version. 31 32 This program is distributed in the hope that it will be useful, 33 but WITHOUT ANY WARRANTY; without even the implied warranty of 34 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 35 GNU General Public License for more details. 36 37 You should have received a copy of the GNU General Public License 38 along with this program; if not, write to the Free Software 39 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 40 41*/ 42 43/** 44* Multi edit tool that allows LDAP operations on multiple entries. 45* 46* @author Roland Gruber 47* @package tools 48*/ 49 50/** security functions */ 51include_once(__DIR__ . "/../../lib/security.inc"); 52/** access to configuration data */ 53include_once(__DIR__ . "/../../lib/config.inc"); 54/** access LDAP server */ 55include_once(__DIR__ . "/../../lib/ldap.inc"); 56/** used to print status messages */ 57include_once(__DIR__ . "/../../lib/status.inc"); 58 59// start session 60startSecureSession(); 61enforceUserIsLoggedIn(); 62 63// die if no write access 64if (!checkIfWriteAccessIsAllowed()) { 65 die(); 66} 67 68checkIfToolIsActive('toolMultiEdit'); 69 70setlanguage(); 71 72if (!empty($_POST)) { 73 validateSecurityToken(); 74} 75 76define('ADD', 'add'); 77define('MOD', 'mod'); 78define('DEL', 'del'); 79 80define('STAGE_START', 'start'); 81define('STAGE_READ_FINISHED', 'readFinished'); 82define('STAGE_ACTIONS_CALCULATED', 'actionsCalculated'); 83define('STAGE_WRITING', 'writing'); 84define('STAGE_FINISHED', 'finished'); 85 86if (isset($_GET['ajaxStatus'])) { 87 runAjaxActions(); 88} 89else { 90 displayStartPage(); 91} 92 93/** 94 * Displays the main page of the multi edit tool. 95 */ 96function displayStartPage() { 97 // display main page 98 include __DIR__ . '/../../lib/adminHeader.inc'; 99 echo '<div class="user-bright smallPaddingContent">'; 100 echo "<form action=\"multiEdit.php\" method=\"post\">\n"; 101 $errors = array(); 102 $tabindex = 1; 103 $container = new htmlResponsiveRow(); 104 $container->add(new htmlTitle(_("Multi edit")), 12); 105 // LDAP suffix 106 $showRules = array('-' => array('otherSuffix')); 107 $hideRules = array(); 108 $typeManager = new \LAM\TYPES\TypeManager(); 109 $types = $typeManager->getConfiguredTypes(); 110 $suffixes = array(); 111 foreach ($types as $type) { 112 if ($type->isHidden()) { 113 continue; 114 } 115 $suffixes[$type->getAlias()] = $type->getSuffix(); 116 $hideRules[$type->getSuffix()] = array('otherSuffix'); 117 } 118 $treeSuffix = $_SESSION['config']->get_Suffix('tree'); 119 if (!empty($treeSuffix)) { 120 $suffixes[_('Tree view')] = $_SESSION['config']->get_Suffix('tree'); 121 $hideRules[$_SESSION['config']->get_Suffix('tree')] = array('otherSuffix'); 122 } 123 $suffixes = array_flip($suffixes); 124 natcasesort($suffixes); 125 $suffixes = array_flip($suffixes); 126 $suffixes[_('Other')] = '-'; 127 $suffixValues = array_values($suffixes); 128 $valSuffix = empty($_POST['suffix']) ? $suffixValues[0] : $_POST['suffix']; 129 $suffixSelect = new htmlResponsiveSelect('suffix', $suffixes, array($valSuffix), _('LDAP suffix'), '700'); 130 $suffixSelect->setHasDescriptiveElements(true); 131 $suffixSelect->setSortElements(false); 132 $suffixSelect->setTableRowsToShow($showRules); 133 $suffixSelect->setTableRowsToHide($hideRules); 134 $container->add($suffixSelect, 12); 135 $valOtherSuffix = empty($_POST['otherSuffix']) ? '' : $_POST['otherSuffix']; 136 $container->add(new htmlResponsiveInputField(_('Other'), 'otherSuffix', $valOtherSuffix), 12); 137 // LDAP filter 138 $valFilter = empty($_POST['filter']) ? '(objectClass=inetOrgPerson)' : $_POST['filter']; 139 $container->add(new htmlResponsiveInputField(_('LDAP filter'), 'filter', $valFilter, '701'), 12); 140 // operation fields 141 $operationsTitle = new htmlSubTitle(_('Operations')); 142 $operationsTitle->setHelpId('702'); 143 $container->add($operationsTitle, 12); 144 $operationsTitles = array(_('Type'), _('Attribute name'), _('Value')); 145 $data = array(); 146 $opCount = empty($_POST['opcount']) ? '3' : $_POST['opcount']; 147 if (isset($_POST['addFields'])) { 148 $opCount += 3; 149 } 150 $operations = array(_('Add') => ADD, _('Modify') => MOD, _('Delete') => DEL); 151 for ($i = 0; $i < $opCount; $i++) { 152 // operation type 153 $selOp = empty($_POST['op_' . $i]) ? ADD : $_POST['op_' . $i]; 154 $opSelect = new htmlSelect('op_' . $i, $operations, array($selOp)); 155 $opSelect->setHasDescriptiveElements(true); 156 $data[$i][] = $opSelect; 157 // attribute name 158 $attrVal = empty($_POST['attr_' . $i]) ? '' : $_POST['attr_' . $i]; 159 $data[$i][] = new htmlInputField('attr_' . $i, $attrVal); 160 $valVal = empty($_POST['val_' . $i]) ? '' : $_POST['val_' . $i]; 161 $data[$i][] = new htmlInputField('val_' . $i, $valVal); 162 // check input 163 if (($selOp == ADD) && !empty($attrVal) && empty($valVal)) { 164 $errors[] = new htmlStatusMessage('ERROR', _('Please enter a value to add.'), htmlspecialchars($attrVal)); 165 } 166 if (($selOp == MOD) && !empty($attrVal) && empty($valVal)) { 167 $errors[] = new htmlStatusMessage('ERROR', _('Please enter a value to modify.'), htmlspecialchars($attrVal)); 168 } 169 } 170 $operationsTable = new htmlResponsiveTable($operationsTitles, $data); 171 $container->add($operationsTable, 12); 172 // add more fields 173 $container->addVerticalSpacer('1rem'); 174 $container->add(new htmlButton('addFields', _('Add more fields')), 12); 175 $container->add(new htmlHiddenInput('opcount', $opCount), 12); 176 // error messages 177 if (sizeof($errors) > 0) { 178 $container->addVerticalSpacer('5rem'); 179 foreach ($errors as $error) { 180 $error->colspan = 5; 181 $container->add($error, 12); 182 } 183 } 184 // action buttons 185 $container->addVerticalSpacer('2rem'); 186 $buttonGroup = new htmlGroup(); 187 $buttonGroup->colspan = 3; 188 $dryRunButton = new htmlButton('dryRun', _('Dry run')); 189 $dryRunButton->setIconClass('dryRunButton'); 190 $buttonGroup->addElement($dryRunButton); 191 $buttonGroup->addElement(new htmlSpacer('10px', null)); 192 $applyButton = new htmlButton('applyChanges', _('Apply changes')); 193 $applyButton->setIconClass('saveButton'); 194 $buttonGroup->addElement($applyButton); 195 $container->add($buttonGroup, 12); 196 $container->addVerticalSpacer('1rem'); 197 198 // run actions 199 if ((sizeof($errors) == 0) && (isset($_POST['dryRun']) || isset($_POST['applyChanges']))) { 200 runActions($container); 201 } 202 203 addSecurityTokenToMetaHTML($container); 204 205 parseHtml(null, $container, array(), false, $tabindex, 'user'); 206 echo "</form>\n"; 207 echo '</div>'; 208 include __DIR__ . '/../../lib/adminFooter.inc'; 209} 210 211/** 212 * Runs the dry run and change actions. 213 * 214 * @param htmlResponsiveRow $container container 215 */ 216function runActions(htmlResponsiveRow &$container) { 217 // LDAP suffix 218 if ($_POST['suffix'] == '-') { 219 $suffix = trim($_POST['otherSuffix']); 220 } 221 else { 222 $suffix = $_POST['suffix']; 223 } 224 if (empty($suffix)) { 225 $error = new htmlStatusMessage('ERROR', _('LDAP Suffix is invalid!')); 226 $error->colspan = 5; 227 $container->add($error, 12); 228 return; 229 } 230 // LDAP filter 231 $filter = trim($_POST['filter']); 232 // operations 233 $operations = array(); 234 for ($i = 0; $i < $_POST['opcount']; $i++) { 235 if (!empty($_POST['attr_' . $i])) { 236 $operations[] = array($_POST['op_' . $i], strtolower(trim($_POST['attr_' . $i])), trim($_POST['val_' . $i])); 237 } 238 } 239 if (sizeof($operations) == 0) { 240 $error = new htmlStatusMessage('ERROR', _('Please specify at least one operation.')); 241 $error->colspan = 5; 242 $container->add($error, 12); 243 return; 244 } 245 $_SESSION['multiEdit_suffix'] = $suffix; 246 $_SESSION['multiEdit_filter'] = $filter; 247 $_SESSION['multiEdit_operations'] = $operations; 248 $_SESSION['multiEdit_status'] = array('stage' => STAGE_START); 249 $_SESSION['multiEdit_dryRun'] = isset($_POST['dryRun']); 250 // disable all input elements 251 $jsContent = ' 252 jQuery(\'input\').attr(\'disabled\', true); 253 jQuery(\'select\').attr(\'disabled\', true); 254 jQuery(\'button\').attr(\'disabled\', true); 255 '; 256 $container->add(new htmlJavaScript($jsContent), 12); 257 // progress area 258 $container->add(new htmlSubTitle(_('Progress')), 12); 259 $progressBarDiv = new htmlDiv('progressBar', ''); 260 $progressBarDiv->colspan = 5; 261 $container->add($progressBarDiv, 12); 262 $progressDiv = new htmlDiv('progressArea', ''); 263 $progressDiv->colspan = 5; 264 $container->add($progressDiv, 12); 265 // JS block for AJAX status update 266 $ajaxBlock = ' 267 jQuery.get(\'multiEdit.php?ajaxStatus\', null, function(data) {handleReply(data);}, \'json\'); 268 269 function handleReply(data) { 270 jQuery(\'#progressBar\').progressbar({value: data.progress, max: 120}); 271 jQuery(\'#progressArea\').html(data.content); 272 if (data.status != "finished") { 273 jQuery.get(\'multiEdit.php?ajaxStatus\', null, function(data) {handleReply(data);}, \'json\'); 274 } 275 else { 276 jQuery(\'input\').removeAttr(\'disabled\'); 277 jQuery(\'select\').removeAttr(\'disabled\'); 278 jQuery(\'button\').removeAttr(\'disabled\'); 279 jQuery(\'#progressBar\').hide(); 280 } 281 } 282 '; 283 $container->add(new htmlJavaScript($ajaxBlock), 12); 284} 285 286/** 287 * Performs the modify operations. 288 */ 289function runAjaxActions() { 290 $jsonReturn = array( 291 'status' => STAGE_START, 292 'progress' => 0, 293 'content' => '' 294 ); 295 switch ($_SESSION['multiEdit_status']['stage']) { 296 case STAGE_START: 297 $jsonReturn = readLDAPData(); 298 break; 299 case STAGE_READ_FINISHED: 300 $jsonReturn = generateActions(); 301 break; 302 case STAGE_ACTIONS_CALCULATED: 303 case STAGE_WRITING: 304 if ($_SESSION['multiEdit_dryRun']) { 305 $jsonReturn = dryRun(); 306 } 307 else { 308 $jsonReturn = doModify(); 309 } 310 break; 311 } 312 echo json_encode($jsonReturn); 313} 314 315/** 316 * Reads the LDAP entries from the directory. 317 * 318 * @return array status 319 */ 320function readLDAPData() { 321 $suffix = $_SESSION['multiEdit_suffix']; 322 $filter = $_SESSION['multiEdit_filter']; 323 if (empty($filter)) { 324 $filter = '(objectClass=*)'; 325 } 326 $operations = $_SESSION['multiEdit_operations']; 327 $attributes = array(); 328 foreach ($operations as $op) { 329 if (!in_array(strtolower($op[1]), $attributes)) { 330 $attributes[] = strtolower($op[1]); 331 } 332 } 333 // run LDAP query 334 $results = searchLDAP($suffix, $filter, $attributes); 335 // print error message if no data returned 336 if (empty($results)) { 337 $code = ldap_errno($_SESSION['ldap']->server()); 338 if ($code !== 0) { 339 $msg = new htmlStatusMessage('ERROR', _('Encountered an error while performing search.'), getDefaultLDAPErrorString($_SESSION['ldap']->server())); 340 } 341 else { 342 $msg = new htmlStatusMessage('ERROR', _('No objects found!')); 343 } 344 $content = getMessageHTML($msg); 345 return array( 346 'status' => STAGE_FINISHED, 347 'progress' => 120, 348 'content' => $content 349 ); 350 } 351 // save LDAP data 352 $_SESSION['multiEdit_status']['entries'] = $results; 353 $_SESSION['multiEdit_status']['stage'] = STAGE_READ_FINISHED; 354 return array( 355 'status' => STAGE_READ_FINISHED, 356 'progress' => 10, 357 'content' => '' 358 ); 359} 360 361/** 362 * Generates the required actions based on the read LDAP data. 363 * 364 * @return array status 365 */ 366function generateActions() { 367 $actions = array(); 368 foreach ($_SESSION['multiEdit_status']['entries'] as $entry) { 369 $dn = $entry['dn']; 370 foreach ($_SESSION['multiEdit_operations'] as $op) { 371 $opType = $op[0]; 372 $attr = $op[1]; 373 $val = $op[2]; 374 switch ($opType) { 375 case ADD: 376 if (empty($entry[$attr]) || !in_array_ignore_case($val, $entry[$attr])) { 377 $actions[] = array(ADD, $dn, $attr, $val); 378 } 379 break; 380 case MOD: 381 if (empty($entry[$attr])) { 382 // attribute not yet exists, add it 383 $actions[] = array(ADD, $dn, $attr, $val); 384 } 385 elseif (!empty($entry[$attr]) && !in_array_ignore_case($val, $entry[$attr])) { 386 // attribute exists and value is not included, replace old values 387 $actions[] = array(MOD, $dn, $attr, $val); 388 } 389 break; 390 case DEL: 391 if (empty($val) && !empty($entry[$attr])) { 392 $actions[] = array(DEL, $dn, $attr, null); 393 } 394 elseif (!empty($val) && isset($entry[$attr]) && in_array($val, $entry[$attr])) { 395 $actions[] = array(DEL, $dn, $attr, $val); 396 } 397 break; 398 } 399 } 400 } 401 // save actions 402 $_SESSION['multiEdit_status']['actions'] = $actions; 403 $_SESSION['multiEdit_status']['stage'] = STAGE_ACTIONS_CALCULATED; 404 return array( 405 'status' => STAGE_ACTIONS_CALCULATED, 406 'progress' => 20, 407 'content' => '' 408 ); 409} 410 411/** 412 * Prints the dryRun output. 413 * 414 * @return array status 415 */ 416function dryRun() { 417 $pro = isLAMProVersion() ? ' Pro' : ''; 418 $ldif = '# LDAP Account Manager' . $pro . ' ' . LAMVersion() . "\n\nversion: 1\n\n"; 419 $log = ''; 420 // fill LDIF and log file 421 $lastDN = ''; 422 foreach ($_SESSION['multiEdit_status']['actions'] as $action) { 423 $opType = $action[0]; 424 $dn = $action[1]; 425 $attr = $action[2]; 426 $val = $action[3]; 427 if ($lastDN != $dn) { 428 if ($lastDN != '') { 429 $log .= "\r\n"; 430 } 431 $lastDN = $dn; 432 $log .= $dn . "\r\n"; 433 } 434 if ($lastDN != '') { 435 $ldif .= "\n"; 436 } 437 $ldif .= 'dn: ' . $dn . "\n"; 438 $ldif .= 'changetype: modify' . "\n"; 439 switch ($opType) { 440 case ADD: 441 $log .= '+' . $attr . '=' . $val . "\r\n"; 442 $ldif .= 'add: ' . $attr . "\n"; 443 $ldif .= $attr . ': ' . $val . "\n"; 444 break; 445 case DEL: 446 $ldif .= 'delete: ' . $attr . "\n"; 447 if (empty($val)) { 448 $log .= '-' . $attr . "\r\n"; 449 } 450 else { 451 $log .= '-' . $attr . '=' . $val . "\r\n"; 452 $ldif .= $attr . ': ' . $val . "\n"; 453 } 454 break; 455 case MOD: 456 $log .= '*' . $attr . '=' . $val . "\r\n"; 457 $ldif .= 'replace: ' . $attr . "\n"; 458 $ldif .= $attr . ': ' . $val . "\n"; 459 break; 460 } 461 } 462 // build meta HTML 463 $container = new htmlTable(); 464 $container->addElement(new htmlOutputText(_('Dry run finished.')), true); 465 $container->addVerticalSpace('20px'); 466 // store LDIF 467 $filename = 'ldif' . getRandomNumber() . '.ldif'; 468 $out = @fopen(dirname(__FILE__) . '/../../tmp/' . $filename, "wb"); 469 fwrite($out, $ldif); 470 $container->addElement(new htmlOutputText(_('LDIF file')), true); 471 $ldifLink = new htmlLink($filename, '../../tmp/' . $filename); 472 $ldifLink->setTargetWindow('_blank'); 473 $container->addElement($ldifLink, true); 474 $container->addVerticalSpace('20px'); 475 $container->addElement(new htmlOutputText(_('Log output')), true); 476 $container->addElement(new htmlInputTextarea('log', $log, 100, 30), true); 477 // generate HTML 478 fclose ($out); 479 ob_start(); 480 $tabindex = 1; 481 parseHtml(null, $container, array(), true, $tabindex, 'user'); 482 $content = ob_get_contents(); 483 ob_end_clean(); 484 return array( 485 'status' => STAGE_FINISHED, 486 'progress' => 120, 487 'content' => $content 488 ); 489} 490 491/** 492 * Error handler 493 * 494 * @param int $errno error number 495 * @param string $errstr error message 496 * @param string $errfile error file 497 * @param int $errline error line 498 */ 499function multiEditLdapErrorHandler($errno, $errstr, $errfile, $errline) { 500 if ($errno === E_USER_ERROR) { 501 logNewMessage(LOG_ERR, 'Error occurred: ' . $errstr . " ($errfile: $errline)"); 502 $_REQUEST['multiEdit_error'] = true; 503 } 504 elseif ($errno === E_USER_WARNING) { 505 logNewMessage(LOG_WARNING, 'Error occurred: ' . $errstr . " ($errfile: $errline)"); 506 $_REQUEST['multiEdit_error'] = true; 507 } 508} 509 510/** 511 * Runs the actual modifications. 512 * 513 * @return array status 514 */ 515function doModify() { 516 set_error_handler('\LAM\TOOLS\MULTI_EDIT\multiEditLdapErrorHandler'); 517 // initial action index 518 if (!isset($_SESSION['multiEdit_status']['index'])) { 519 $_SESSION['multiEdit_status']['index'] = 0; 520 } 521 // initial content 522 if (!isset($_SESSION['multiEdit_status']['modContent'])) { 523 $_SESSION['multiEdit_status']['modContent'] = ''; 524 } 525 // run 10 modifications in each call 526 $localCount = 0; 527 while (($localCount < 10) && ($_SESSION['multiEdit_status']['index'] < sizeof($_SESSION['multiEdit_status']['actions']))) { 528 $action = $_SESSION['multiEdit_status']['actions'][$_SESSION['multiEdit_status']['index']]; 529 $opType = $action[0]; 530 $dn = $action[1]; 531 $attr = $action[2]; 532 $val = $action[3]; 533 $_SESSION['multiEdit_status']['modContent'] .= htmlspecialchars($dn) . "<br>"; 534 // run LDAP commands 535 $success = false; 536 switch ($opType) { 537 case ADD: 538 $success = ldap_mod_add($_SESSION['ldap']->server(), $dn, array($attr => array($val))); 539 break; 540 case DEL: 541 if (empty($val)) { 542 $success = ldap_modify($_SESSION['ldap']->server(), $dn, array($attr => array())); 543 } 544 else { 545 $success = ldap_mod_del($_SESSION['ldap']->server(), $dn, array($attr => array($val))); 546 } 547 break; 548 case MOD: 549 $success = ldap_modify($_SESSION['ldap']->server(), $dn, array($attr => array($val))); 550 break; 551 } 552 if (!$success || isset($_REQUEST['multiEdit_error'])) { 553 $msg = new htmlStatusMessage('ERROR', getDefaultLDAPErrorString($_SESSION['ldap']->server())); 554 $_SESSION['multiEdit_status']['modContent'] .= getMessageHTML($msg); 555 } 556 $localCount++; 557 $_SESSION['multiEdit_status']['index']++; 558 } 559 // check if finished 560 if ($_SESSION['multiEdit_status']['index'] == sizeof($_SESSION['multiEdit_status']['actions'])) { 561 $_SESSION['multiEdit_status']['modContent'] .= '<br><br>' . _('Finished all operations.'); 562 return array( 563 'status' => STAGE_FINISHED, 564 'progress' => 120, 565 'content' => $_SESSION['multiEdit_status']['modContent'] 566 ); 567 } 568 // return current status 569 return array( 570 'status' => STAGE_WRITING, 571 'progress' => 20 + (($_SESSION['multiEdit_status']['index'] / sizeof($_SESSION['multiEdit_status']['actions'])) * 100), 572 'content' => $_SESSION['multiEdit_status']['modContent'] 573 ); 574} 575 576/** 577 * Returns the HTML code for a htmlStatusMessage 578 * 579 * @param htmlStatusMessage $msg message 580 * @return String HTML code 581 */ 582function getMessageHTML($msg) { 583 $tabindex = 0; 584 ob_start(); 585 parseHtml(null, $msg, array(), true, $tabindex, 'user'); 586 $content = ob_get_contents(); 587 ob_end_clean(); 588 return $content; 589} 590