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