lam/lam/lib/treeview.inc
2022-05-29 17:48:54 +02:00

1610 lines
59 KiB
PHP

<?php
namespace LAM\TOOLS\TREEVIEW;
use htmlButton;
use htmlDiv;
use htmlElement;
use htmlForm;
use htmlGroup;
use htmlHiddenInput;
use htmlImage;
use htmlInputField;
use htmlInputFileUpload;
use htmlInputTextarea;
use htmlLink;
use htmlList;
use htmlOutputText;
use htmlResponsiveInputField;
use htmlResponsiveRow;
use htmlResponsiveSelect;
use htmlResponsiveTable;
use htmlSelect;
use htmlSortableList;
use htmlStatusMessage;
use htmlSubTitle;
use htmlTable;
use htmlTitle;
use LAM\SCHEMA\AttributeType;
use LAM\SCHEMA\ObjectClass;
use LAMException;
use function LAM\SCHEMA\get_schema_attributes;
use function LAM\SCHEMA\get_schema_objectclasses;
/*
This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
Copyright (C) 2021 - 2022 Roland Gruber
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/**
* Tree view functions.
*
* @author Roland Gruber
*/
include_once 'account.inc';
include_once 'tools.inc';
include_once 'tools/treeview.inc';
/**
* Tree view functions.
*
* @package LAM\TOOLS\TREEVIEW
*/
class TreeView {
/**
* @var array schema attributes
*/
private $schemaAttributes = null;
/**
* @var array schema object classes
*/
private $schemaObjectClasses = null;
/**
* Returns the JSON to answer an AJAX request.
*
* @return string JSON data
*/
public function answerAjaxCall(): string {
if (empty($_GET['command'])) {
logNewMessage(LOG_ERR, 'No command given for tree view.');
die();
}
$command = $_GET['command'];
if (empty($_POST['dn'])) {
logNewMessage(LOG_ERR, 'No dn for tree view.');
die;
}
$dn = ($_POST['dn'] === '#') ? '#' : base64_decode($_POST['dn']);
switch ($command) {
case 'getNodes':
return $this->getNodes($dn);
case 'getNodeContent':
$this->validateDn($dn);
return $this->getNodeContent($dn);
case 'getInternalAttributesContent':
$this->validateDn($dn);
return $this->getInternalAttributesContent($dn);
case 'getPossibleNewAttributes':
return $this->getPossibleNewAttributeNameOptionsJson();
case 'saveAttributes':
$this->ensureWriteAccess();
$this->validateDn($dn);
return $this->saveAttributes($dn);
case 'createNewNode':
$this->ensureWriteAccess();
$this->validateDn($dn);
return $this->createNewNode($dn);
case 'deleteNode':
$this->ensureWriteAccess();
$this->validateDn($dn);
return $this->deleteNode($dn);
case 'search':
$this->validateDn($dn);
return $this->search($dn);
case 'searchResults':
$this->validateDn($dn);
return $this->searchResults($dn);
case 'paste':
$this->ensureWriteAccess();
$this->validateDn($dn);
return $this->paste($dn);
default:
logNewMessage(LOG_ERR, 'Invalid command for tree view: ' . $command);
die;
}
return json_encode(array());
}
/**
* Returns the JSON for the possible new attributes select.
*/
private function getPossibleNewAttributeNameOptionsJson() {
$objectClasses = $_POST['objectClasses'];
$attributeNames = $this->getPossibleNewAttributeNameOptions($objectClasses, true);
natcasesort($attributeNames);
return json_encode(array('data' => $attributeNames));
}
/**
* Lists LDAP nodes.
*
* @param string $dn DN
* @return string JSON data
*/
private function getNodes(string $dn): string {
if ($dn === '#') {
return $this->getRootNodes();
}
return $this->getSubNodes($dn);
}
/**
* Returns a list of root nodes for the tree view.
*
* @return string JSON data
*/
private function getRootNodes(): string {
$rootDns = TreeViewTool::getRootDns();
$result = array();
foreach ($rootDns as $rootDn) {
logNewMessage(LOG_DEBUG, 'Getting tree nodes for ' . $rootDn);
$rootData = ldapGetDN($rootDn, array('objectClass'));
if ($rootData === null) {
continue;
}
$children = ldapListDN($rootDn, '(objectClass=*)', array('objectClass'));
$nodeData = $this->createNodeData($rootData, true, true, $children);
$result[] = $nodeData;
}
return json_encode($result);
}
/**
* Returns the subnodes of the given DN.
*
* @param string $dn DN
* @return string JSON data
*/
private function getSubNodes(string $dn): string {
$this->validateDn($dn);
logNewMessage(LOG_DEBUG, 'Getting tree nodes for ' . $dn);
$children = ldapListDN($dn, '(objectClass=*)', array('objectClass'));
$childNodes = array();
foreach ($children as $child) {
$childNodes[] = $this->createNodeData($child);
}
$this->sortNodes($childNodes);
return json_encode($childNodes);
}
/**
* Creates the node data for the tree view.
*
* @param array $attributes LDAP data
* @param bool $open open node
* @param array $children child LDAP data
* @return array nodes
*/
private function createNodeData(array $attributes, bool $open = false, $noShortenFirst = false, array $children = array()): array {
$text = ($noShortenFirst) ? $attributes['dn'] : extractRDN($attributes['dn']);
$data = array(
'id' => base64_encode($attributes['dn']),
'text' => unescapeLdapSpecialCharacters($text),
'icon' => $this->getNodeIcon($attributes),
'children' => true
);
if (!empty($children)) {
$childData = array();
foreach ($children as $child) {
$childData[] = $this->createNodeData($child);
}
$this->sortNodes($childData);
$data['children'] = $childData;
}
if ($open) {
$data['state'] = array(
'opened' => true
);
}
return $data;
}
/**
* Returns the node's icon.
*
* @param array $attributes LDAP data
* @return string icon
*/
private function getNodeIcon(array $attributes): string {
$base = '../../graphics/';
$icon = 'object.svg';
$objectClasses = array_map('strtolower', $attributes['objectclass']);
$rdn = extractRDNValue($attributes['dn']);
if (in_array('sambasamaccount', $objectClasses) &&
'$' == $rdn[ strlen($rdn) - 1 ]) {
$icon = 'samba.svg';
}
if (in_array('sambasamaccount', $objectClasses)) {
$icon = 'samba.svg';
}
elseif (in_array('person', $objectClasses) ||
in_array('organizationalperson', $objectClasses) ||
in_array('inetorgperson', $objectClasses) ||
in_array('account', $objectClasses) ||
in_array('posixaccount', $objectClasses) ||
in_array('organizationalrole', $objectClasses)) {
$icon = 'user.svg';
}
elseif (in_array('organization', $objectClasses)) {
$icon = 'world-color.svg';
}
elseif (in_array('organizationalunit', $objectClasses)) {
$icon = 'folder.svg';
}
elseif (in_array('dcobject', $objectClasses) ||
in_array('domainrelatedobject', $objectClasses) ||
in_array('domain', $objectClasses) ||
in_array('builtindomain', $objectClasses)) {
$icon = 'world-color.svg';
}
elseif (in_array('alias', $objectClasses)) {
$icon = 'link.svg';
}
elseif (in_array('document', $objectClasses)) {
$icon = 'txt.svg';
}
elseif (in_array('country', $objectClasses)) {
$icon = 'world-color.svg';
}
elseif (in_array('locality', $objectClasses)) {
$icon = 'location.svg';
}
elseif (in_array('posixgroup', $objectClasses) ||
in_array('groupofnames', $objectClasses) ||
in_array('groupofuniquenames', $objectClasses) ||
in_array('group', $objectClasses)) {
$icon = 'group.svg';
}
elseif (in_array('iphost', $objectClasses)) {
$icon = 'computer-small.svg';
}
elseif (in_array('device', $objectClasses)) {
$icon = 'device.svg';
}
elseif (in_array('server', $objectClasses)) {
$icon = 'computer-small.svg';
}
elseif (in_array('volume', $objectClasses)) {
$icon = 'hard-drive.svg';
}
elseif (in_array('container', $objectClasses)) {
$icon = 'folder.svg';
}
return $base . $icon;
}
/**
* Sorts the given node array by DN.
*
* @param array $nodes nodes
*/
private function sortNodes(array &$nodes): void {
usort($nodes, 'LAM\TOOLS\TREEVIEW\compareNodeByIdAsDn');
}
/**
* Returns the options for the drop-down to add a new attribute.
*
* @param array $objectClasses object classes
* @param bool $includeMustAttributes include required attributes
* @return array list of options
*/
private function getPossibleNewAttributeNameOptions(array $objectClasses, bool $includeMustAttributes = false): array {
$schemaObjectClasses = $this->getSchemaObjectClasses();
$schemaAttributes = $this->getSchemaAttributes();
$possibleNewAttributes = array();
foreach ($objectClasses as $objectClass) {
$objectClass = strtolower($objectClass);
if (empty($schemaObjectClasses[$objectClass])) {
continue;
}
$attributes = $schemaObjectClasses[$objectClass]->getMayAttrs();
if ($includeMustAttributes) {
$attributes = array_merge($attributes, $schemaObjectClasses[$objectClass]->getMustAttrs());
}
foreach ($attributes as $attribute) {
$attributeName = $attribute->getName();
if (!isset($attributes[strtolower($attributeName)])) {
$schemaAttribute = $schemaAttributes[strtolower($attributeName)];
$single = $schemaAttribute->getIsSingleValue() ? 'single' : 'multi';
$type = $this->isMultiLineAttribute($attributeName, $schemaAttribute) ? 'textarea' : 'input';
if ($this->isPasswordAttribute($attributeName)) {
$type = 'password';
}
elseif ($this->isJpegAttribute($attributeName, $schemaAttribute)) {
$type = 'jpeg';
}
$possibleNewAttributes[$attributeName] = $attributeName . '__#__' . $single . '__#__' . $type;
}
}
}
ksort($possibleNewAttributes);
return $possibleNewAttributes;
}
/**
* Returns the node content with the attribute listing.
*
* @param htmlStatusMessage|null $message message to display
* @return string JSON
*/
private function getNodeContent(string $dn, ?htmlStatusMessage $message = null): string {
$row = new htmlResponsiveRow();
if ($message !== null) {
$row->add($message);
$row->addVerticalSpacer('1rem');
}
$row->add(new htmlTitle(unescapeLdapSpecialCharacters($dn)), 12);
$row->add(new htmlDiv('ldap_actionarea_messages', new htmlOutputText('')), 12);
$row->add(new htmlSubTitle(_('Attributes')), 12);
$attributes = ldapGetDN($dn, array('*'));
unset($attributes['dn']);
ksort($attributes);
$schemaAttributes = $this->getSchemaAttributes();
$objectClasses = $attributes['objectclass'];
$highlighted = (empty($_POST['highlight'])) ? array() : $_POST['highlight'];
foreach ($attributes as $attributeName => $values) {
$schemaAttribute = null;
if (isset($schemaAttributes[$attributeName])) {
$schemaAttribute = $schemaAttributes[$attributeName];
$attributeName = $schemaAttribute->getName();
}
$this->addAttributeContent($row, $attributeName, $values, $schemaAttribute, $objectClasses, $dn, $highlighted);
}
$row->addVerticalSpacer('1rem');
// add new attributes
$possibleNewAttributes = $this->getPossibleNewAttributeNameOptions($objectClasses);
logNewMessage(LOG_DEBUG, 'Possible new attributes for ' . $dn . ': ' . implode('; ', $possibleNewAttributes));
if (!empty($possibleNewAttributes)) {
$possibleNewAttributes = array('' => '') + $possibleNewAttributes;
$row->add(new htmlSubTitle(_('Add new attribute')), 12);
$newAttributeSelect = new htmlResponsiveSelect('newAttribute', $possibleNewAttributes, array(), _('Attribute'));
$newAttributeSelect->setHasDescriptiveElements(true);
$newAttributeSelect->setTransformSingleSelect(false);
$newAttributeSelect->setOnchangeEvent('window.lam.treeview.addAttributeField(event, this);');
$row->add($newAttributeSelect, 12);
$newAttributesContentSingleInput = new htmlResponsiveRow();
$newAttributesContentSingleInput->addLabel(new htmlOutputText('PLACEHOLDER_SINGLE_INPUT_LABEL'));
$newAttributesContentSingleInput->addField($this->getAttributeContentField('placeholder' . getRandomNumber(), array(''), false, false, false, null));
$row->add(new htmlDiv('new-attributes-single-input', $newAttributesContentSingleInput, array('hidden')), 12);
$newAttributesContentMultiInput = new htmlResponsiveRow();
$newAttributesContentMultiInput->addLabel(new htmlOutputText('PLACEHOLDER_MULTI_INPUT_LABEL'));
$newAttributesContentMultiInput->addField($this->getAttributeContentField('placeholder' . getRandomNumber(), array(''), false, true, false, null));
$row->add(new htmlDiv('new-attributes-multi-input', $newAttributesContentMultiInput, array('hidden')), 12);
$newAttributesContentSingleTextarea = new htmlResponsiveRow();
$newAttributesContentSingleTextarea->addLabel(new htmlOutputText('PLACEHOLDER_SINGLE_TEXTAREA_LABEL'));
$newAttributesContentSingleTextarea->addField($this->getAttributeContentField('placeholder' . getRandomNumber(), array(''), false, false, true, null));
$row->add(new htmlDiv('new-attributes-single-textarea', $newAttributesContentSingleTextarea, array('hidden')), 12);
$newAttributesContentMultiTextarea = new htmlResponsiveRow();
$newAttributesContentMultiTextarea->addLabel(new htmlOutputText('PLACEHOLDER_MULTI_TEXTAREA_LABEL'));
$newAttributesContentMultiTextarea->addField($this->getAttributeContentField('placeholder' . getRandomNumber(), array(''), false, true, true, null));
$row->add(new htmlDiv('new-attributes-multi-textarea', $newAttributesContentMultiTextarea, array('hidden')), 12);
$newAttributesContentSinglePassword = new htmlResponsiveRow();
$newAttributesContentSinglePassword->addLabel(new htmlOutputText('PLACEHOLDER_SINGLE_PASSWORD_LABEL'));
$newAttributesContentSinglePassword->addField($this->getAttributeContentField('userpassword' . getRandomNumber(), array(''), false, false, false, null));
$row->add(new htmlDiv('new-attributes-single-password', $newAttributesContentSinglePassword, array('hidden')), 12);
$newAttributesContentMultiPassword = new htmlResponsiveRow();
$newAttributesContentMultiPassword->addLabel(new htmlOutputText('PLACEHOLDER_MULTI_PASSWORD_LABEL'));
$newAttributesContentMultiPassword->addField($this->getAttributeContentField('userpassword' . getRandomNumber(), array(''), false, true, false, null));
$row->add(new htmlDiv('new-attributes-multi-password', $newAttributesContentMultiPassword, array('hidden')), 12);
$newAttributesContentSingleJpeg = new htmlResponsiveRow();
$newAttributesContentSingleJpeg->addLabel(new htmlOutputText('PLACEHOLDER_SINGLE_JPEG_LABEL'));
$newAttributesContentSingleJpeg->addField($this->getAttributeContentField('jpegphoto' . getRandomNumber(), array(''), false, false, false, null));
$row->add(new htmlDiv('new-attributes-single-jpeg', $newAttributesContentSingleJpeg, array('hidden')), 12);
$newAttributesContentMultiJpeg = new htmlResponsiveRow();
$newAttributesContentMultiJpeg->addLabel(new htmlOutputText('PLACEHOLDER_MULTI_JPEG_LABEL'));
$newAttributesContentMultiJpeg->addField($this->getAttributeContentField('jpegphoto' . getRandomNumber(), array(''), false, true, true, null));
$row->add(new htmlDiv('new-attributes-multi-jpeg', $newAttributesContentMultiJpeg, array('hidden')), 12);
}
if (checkIfWriteAccessIsAllowed()) {
$row->addVerticalSpacer('2rem');
$saveButton = new htmlButton('savebutton', _('Save'));
$saveButton->setOnClick('window.lam.treeview.saveAttributes(event, '
. "'" . getSecurityTokenName() . "', "
. "'" . getSecurityTokenValue() . "', "
. "'" . base64_encode($dn) . "');");
$saveButton->setCSSClasses(array('lam-primary'));
$row->add($saveButton, 12, 12, 12, 'text-center');
}
$internalAttributesContent = new htmlResponsiveRow();
$internalAttributesContent->add(new htmlSubTitle(_('Internal attributes')), 12);
$internalAttributesButton = new htmlLink(_('Show internal attributes'), '#', null, true);
$internalAttributesButton->setOnClick('window.lam.treeview.getInternalAttributesContent(event,
"' . getSecurityTokenName() . '",
"' . getSecurityTokenValue() . '",
"' . base64_encode($dn) . '");');
$internalAttributesButton->setId('internalAttributesButton');
$internalAttributesContent->add($internalAttributesButton, 12);
$internalAttributesDiv = new htmlDiv('actionarea-internal-attributes', $internalAttributesContent);
$row->add($internalAttributesDiv, 12);
$tabindex = 1;
ob_start();
parseHtml(null, $row, array(), false, $tabindex, 'none');
$content = ob_get_contents();
ob_end_clean();
return json_encode(array('content' => $content));
}
/**
* Adds the content part for one attribute.
*
* @param htmlResponsiveRow $row container where to add content
* @param string $attributeName attribute name
* @param array $values values
* @param AttributeType|null $schemaAttribute schema attribute
* @param string[] $objectClasses object classes
* @param string|null $dn DN
* @param array $highlighted list of highlighted attribute names
*/
private function addAttributeContent(htmlResponsiveRow $row, string $attributeName, array $values,
?AttributeType $schemaAttribute, array $objectClasses, ?string $dn,
array $highlighted): void {
$schemaObjectClasses = $this->getSchemaObjectClasses();
$label = new htmlOutputText($attributeName);
$rdnAttribute = ($dn !== null) ? strtolower(extractRDNAttribute($dn)) : '';
$attributeNameLowerCase = strtolower($attributeName);
$required = ($attributeNameLowerCase === $rdnAttribute) || ($attributeNameLowerCase === 'objectclass');
if (($schemaAttribute !== null) && $this->isAttributeRequired($schemaObjectClasses, $schemaAttribute, $objectClasses)) {
$required = true;
}
$label->setMarkAsRequired($required);
$isHighlighted = in_array_ignore_case($attributeName, $highlighted);
$cssClasses = ($isHighlighted) ? 'tree-highlight' : '';
$row->addLabel($label, $cssClasses);
if (($schemaAttribute !== null) && !empty($schemaAttribute->getDescription())) {
$label->setTitle($schemaAttribute->getDescription());
}
$isMultiValue = $this->isMultiValueAttribute($values, $schemaAttribute);
$isMultiLine = $this->isMultiLineAttribute($attributeName, $schemaAttribute);
$row->addField($this->getAttributeContentField($attributeName, $values, $required, $isMultiValue, $isMultiLine, $schemaAttribute), $cssClasses);
$row->addVerticalSpacer('0.5rem');
}
/**
* Returns if the given attribute is a multi-value one.
*
* @param array|null $values attribute values
* @param AttributeType|null $schemaAttribute schema attribute
* @return bool is multi-value
*/
private function isMultiValueAttribute(?array $values, ?AttributeType $schemaAttribute): bool {
return (sizeof($values) > 1) || (($schemaAttribute !== null) && ($schemaAttribute->getIsSingleValue() !== true));
}
/**
* Returns the input fields for the attribute.
*
* @param string $attributeName attribute name
* @param array $values values
* @param bool $required is required
* @param bool $isMultiValue multi-value attribute
* @param bool $isMultiLine textarea attribute
* @param AttributeType|null $schemaAttribute schema attribute
* @return htmlElement content
*/
private function getAttributeContentField(string $attributeName, array $values, bool $required, bool $isMultiValue,
bool $isMultiLine, ?AttributeType $schemaAttribute): htmlElement {
if (!$isMultiValue) {
$field = $this->getAttributeInputField($attributeName, $values[0], $required, $isMultiLine, true, $schemaAttribute, 0);
return $this->addExtraAttributeContent($field, $attributeName, $schemaAttribute);
}
$valueListContents = array();
$autoCompleteValues = array();
$onInput = null;
$safeAttributeName = htmlspecialchars(strtolower($attributeName));
if ($safeAttributeName === 'objectclass') {
$schemaObjectClasses = $this->getSchemaObjectClasses();
foreach ($schemaObjectClasses as $schemaObjectClass) {
if (!in_array_ignore_case($schemaObjectClass->getName(), $values)) {
$autoCompleteValues[] = $schemaObjectClass->getName();
}
}
$onInput = "window.lam.treeview.updatePossibleNewAttributes('" . getSecurityTokenName() . "', '" . getSecurityTokenValue() . "');";
}
foreach ($values as $index => $value) {
$inputField = $this->getAttributeInputField($attributeName, $value, $required, $isMultiLine, false, $schemaAttribute, $index);
$cssClasses = $inputField->getCSSClasses();
$cssClasses[] = 'lam-attr-' . $safeAttributeName;
$inputField->setCSSClasses($cssClasses);
if (!empty($autoCompleteValues) && ($inputField instanceof htmlInputField)) {
$inputField->enableAutocompletion($autoCompleteValues);
$inputField->setId($attributeName . $index);
}
if ($onInput !== null) {
$inputField->setOnInput($onInput);
}
$valueLine = new htmlTable();
$valueLine->setCSSClasses(array('fullwidth'));
$valueLine->addElement($inputField);
if (checkIfWriteAccessIsAllowed()) {
if (!$this->isJpegAttribute($attributeName, $schemaAttribute)) {
$addButton = new htmlLink(null, '#', '../../graphics/add.svg');
$addButton->setCSSClasses(array('margin2'));
$addButton->setOnClick('window.lam.treeview.addValue(event, this);');
$valueLine->addElement($addButton);
}
if (!$this->isJpegAttribute($attributeName, $schemaAttribute) || ($value !== '')) {
$clearButton = new htmlLink(null, '#', '../../graphics/del.svg');
$clearButton->setCSSClasses(array('margin2'));
$clearButton->setOnClick('window.lam.treeview.clearValue(event, this);');
$valueLine->addElement($clearButton);
}
}
$valueListContents[] = $valueLine;
}
$listClasses = array('nowrap unstyled-list');
if ($this->isOrderedAttribute($values)) {
$listId = 'attributeList_' . $safeAttributeName;
$valueList = new htmlSortableList($valueListContents, $listId);
$listClasses[] = 'tree-attribute-sorted-list';
$valueList->setOnUpdate('function() {window.lam.treeview.updateAttributePositionData(\'' . $listId . '\');}');
}
else {
$valueList = new htmlList($valueListContents, 'attributeList_' . $safeAttributeName);
}
$valueList->setCSSClasses($listClasses);
return $this->addExtraAttributeContent($valueList, $attributeName, $schemaAttribute);
}
/**
* Returns if the attribute has ordered values.
*
* @param array $values values
* @return bool is ordered
*/
private function isOrderedAttribute(array $values): bool {
if (empty($values)) {
return false;
}
$regex = '/^\\{[0-9]+\\}/';
foreach ($values as $value) {
if (!preg_match($regex, $value)) {
return false;
}
}
return true;
}
/**
* Adds any entry content to the element.
*
* @param htmlElement $element original element
* @param string $attributeName attribute name
* @param AttributeType|null $schemaAttribute schema attribute
* @return htmlElement enhanced element
*/
private function addExtraAttributeContent(htmlElement $element, string $attributeName, ?AttributeType $schemaAttribute): htmlElement {
if ($this->isJpegAttribute($attributeName, $schemaAttribute)) {
$row = new htmlResponsiveRow();
$row->add($element);
$upload = new htmlInputFileUpload('lam_attr_' . $attributeName);
$upload->addDataAttribute('attr-name', $attributeName);
$upload->setCSSClasses(array('image-upload'));
$row->add($upload);
return $row;
}
return $element;
}
/**
* Returns an input field for an LDAP attribute.
*
* @param string $attributeName attribute name
* @param string $value value
* @param bool $required required
* @param bool $isMultiLine is multi-line attribute
* @param bool $isSingleValue is single value attribute
* @param AttributeType|null $schemaAttribute schema attribute
* @param int $index value position
* @return htmlElement input field
*/
private function getAttributeInputField(string $attributeName, string $value, bool $required, bool $isMultiLine,
bool $isSingleValue, ?AttributeType $schemaAttribute, int $index): htmlElement {
if ($this->isPasswordAttribute($attributeName)) {
return $this->getAttributePasswordInputField($attributeName, $value, $required, $isSingleValue);
}
if ($this->isJpegAttribute($attributeName, $schemaAttribute)) {
return $this->getAttributeJpegInputField($attributeName, $value, $required, $index);
}
if (($schemaAttribute !== null) && $schemaAttribute->isBinary()) {
$value = base64_encode($value);
}
if ($isMultiLine) {
$inputField = new htmlInputTextarea('lam_attr_' . $attributeName, $value, 50, 5);
}
else {
$inputField = new htmlInputField('lam_attr_' . $attributeName, $value);
}
$inputField->addDataAttribute('value-orig', $value);
$inputField->addDataAttribute('attr-name', $attributeName);
$cssClass = ($isSingleValue) ? 'single-input' : 'multi-input';
$inputField->setCSSClasses(array($cssClass));
if ($required) {
$inputField->setRequired(true);
}
return $inputField;
}
/**
* Returns if the given attribute is a (hashable) password.
*
* @param string $attributeName attribute name
* @return bool is password
*/
private function isPasswordAttribute(string $attributeName): bool {
return stripos($attributeName, 'userpassword') === 0;
}
/**
* Returns if the given attribute is a JPG image.
*
* @param string $attributeName attribute name
* @param AttributeType|null $schemaAttribute schema attribute
* @return bool is password
*/
private function isJpegAttribute(string $attributeName, ?AttributeType $schemaAttribute): bool {
if (stripos($attributeName, 'jpegphoto') === 0) {
return true;
}
if (($schemaAttribute !== null) && ($schemaAttribute->getType() === 'JPEG')) {
return true;
}
return false;
}
/**
* Returns an input field for a password attribute.
*
* @param string $attributeName attribute name
* @param string $value value
* @param bool $required required
* @param bool $isSingleValue is single value attribute
* @return htmlElement input field
*/
private function getAttributePasswordInputField(string $attributeName, string $value, bool $required, bool $isSingleValue): htmlElement {
$inputField = new htmlInputField('lam_attr_' . $attributeName, $value);
$inputField->addDataAttribute('value-orig', $value);
$inputField->addDataAttribute('attr-name', $attributeName);
$cssClass = ($isSingleValue) ? 'single-input' : 'multi-input';
$inputField->setCSSClasses(array($cssClass));
if ($required) {
$inputField->setRequired(true);
}
$inputField->setIsPassword(true);
$group = new htmlGroup();
$table = new htmlTable();
$table->addElement($inputField);
$hashes = getSupportedHashTypes();
$selectedHash = array(getHashType($value));
$hashSelect = new htmlSelect('lam_hash_' . $attributeName, $hashes, $selectedHash);
$hashSelect->addDataAttribute('attr-name', $attributeName);
$hashSelect->setCSSClasses(array('hash-select'));
$table->addElement($hashSelect);
$checkPassword = new htmlLink(null, '#', '../../graphics/light.svg');
$checkPassword->setTitle(_('Check password'));
$checkPassword->setOnClick("window.lam.treeview.checkPassword(event, this," .
" '" . getSecurityTokenName() . "', '" . getSecurityTokenValue() . "'," .
" '" . _('Check password') . "', '" . _('Check') . "', '" . _('Cancel') . "');");
$table->addElement($checkPassword);
$group->addElement($table);
return $group;
}
/**
* Returns an input field for a JPG image attribute.
*
* @param string $attributeName attribute name
* @param string $value value
* @param bool $required required
* @return htmlElement input field
*/
private function getAttributeJpegInputField(string $attributeName, string $value, bool $required, int $index): htmlElement {
$imgNumber = getRandomNumber();
$jpeg_filename = 'jpg' . $imgNumber . '.jpg';
$outJpeg = @fopen(__DIR__ . '/../tmp/' . $jpeg_filename, "wb");
fwrite($outJpeg, $value);
fclose ($outJpeg);
$photoFile = '../../tmp/' . $jpeg_filename;
$image = new htmlImage($photoFile);
$image->enableLightbox();
$image->setCSSClasses(array('thumbnail', 'image-input'));
$image->addDataAttribute('attr-name', $attributeName);
$image->addDataAttribute('index', $index);
return $image;
}
/**
* Returns if the given attribute is multi-line.
*
* @param string $attributeName attribute name
* @param AttributeType|null $schemaAttribute schema attribute
* @return bool is multi-line
*/
private function isMultiLineAttribute(string $attributeName, ?AttributeType $schemaAttribute): bool {
$knownAttributes = array('postalAddress1', 'homePostalAddress', 'personalSignature', 'description', 'mailReplyText');
if (in_array_ignore_case($attributeName, $knownAttributes)) {
return true;
}
if ($schemaAttribute === null) {
return false;
}
$knownSyntaxOIDs = array(
// octet string syntax OID:
'1.3.6.1.4.1.1466.115.121.1.40',
// postal address syntax OID:
'1.3.6.1.4.1.1466.115.121.1.41');
return in_array($schemaAttribute->getSyntaxOID(), $knownSyntaxOIDs);
}
/**
* Returns if the attribute is required for the given list of object classes.
*
* @param ObjectClass[] $schemaObjectClasses object class definitions
* @param AttributeType $schemaAttribute schema attribute
* @param array $objectClasses list of object classes
* @return bool is required
*/
private function isAttributeRequired(array $schemaObjectClasses, AttributeType $schemaAttribute, array $objectClasses): bool {
foreach ($objectClasses as $objectClass) {
$objectClass = strtolower($objectClass);
if (!isset($schemaObjectClasses[$objectClass])) {
continue;
}
$schemaObjectClass = $schemaObjectClasses[$objectClass];
$requiredAttributes = $schemaObjectClass->getMustAttrNames();
if (in_array_ignore_case($schemaAttribute->getName(), $requiredAttributes)) {
return true;
}
if (!empty($schemaObjectClass->getSupClasses())) {
if ($this->isAttributeRequired($schemaObjectClasses, $schemaAttribute, $schemaObjectClass->getSupClasses())) {
return true;
}
}
}
return false;
}
/**
* Returns the internal attributes.
*
* @param string $dn DN
* @return string JSON
*/
private function getInternalAttributesContent(string $dn): string {
$row = new htmlResponsiveRow();
$row->add(new htmlSubTitle(_('Internal attributes')), 12);
$attributes = ldapGetDN($dn, array('+', 'creatorsName', 'createTimestamp', 'modifiersName',
'modifyTimestamp', 'hasSubordinates', 'pwdChangedTime', 'passwordRetryCount', 'accountUnlockTime', 'nsAccountLock',
'nsRoleDN', 'passwordExpirationTime'));
unset($attributes['dn']);
ksort($attributes);
foreach ($attributes as $attributeName => $values) {
$row->addLabel(new htmlOutputText($this->getProperAttributeName($attributeName)));
$row->addField(new htmlOutputText(implode(', ', $values)));
$row->addVerticalSpacer('0.5rem');
}
$tabindex = 1;
ob_start();
parseHtml(null, $row, array(), false, $tabindex, 'none');
$content = ob_get_contents();
ob_end_clean();
return json_encode(array('content' => $content));
}
/**
* Returns the node content with the attribute listing.
*
* @return string JSON
*/
private function saveAttributes(string $dn): string {
$schemaAttributes = $this->getSchemaAttributes();
$attributes = ldapGetDN($dn, array('*'));
unset($attributes['dn']);
$attributes = array_change_key_case($attributes,CASE_LOWER);
$changes = json_decode($_POST['changes'], true);
$changes = array_change_key_case($changes, CASE_LOWER);
logNewMessage(LOG_DEBUG, 'LDAP changes for ' . $dn . ': ' . print_r($changes, true));
$ldapChanges = array();
if (!empty($changes)) {
foreach ($changes as $attrName => $change) {
$schemaAttribute = isset($schemaAttributes[$attrName]) ? $schemaAttributes[$attrName] : null;
if (isset($change['new'])) {
$newValues = $change['new'];
if (isset($change['hash'])) {
$newValues = $this->applyPasswordHash($newValues, $change['hash']);
}
elseif (($schemaAttribute !== null) && $schemaAttribute->isBinary()) {
$newValues = $this->decodeBinaryAttributeValues($newValues);
}
$ldapChanges[$attrName] = $newValues;
}
if (isset($change['delete']) && isset($attributes[$attrName])) {
$oldValues = $attributes[$attrName];
$changed = false;
foreach ($change['delete'] as $index) {
if (isset($oldValues[$index])) {
unset($oldValues[$index]);
$changed = true;
}
}
if ($changed) {
if (($schemaAttribute !== null) && $schemaAttribute->isBinary()) {
$oldValues = $this->decodeBinaryAttributeValues($oldValues);
}
$ldapChanges[$attrName] = array_values($oldValues);
}
}
if (!empty($change['upload'])) {
if (empty($ldapChanges[$attrName]) && !empty($attributes[$attrName])) {
$ldapChanges[$attrName] = $attributes[$attrName];
}
$oldValues = !empty($attributes[$attrName]) ? $attributes[$attrName] : array();
if ($this->isMultiValueAttribute($oldValues, $schemaAttribute)) {
$ldapChanges[$attrName][] = base64_decode($change['upload']);
}
else {
$ldapChanges[$attrName][0] = base64_decode($change['upload']);
}
}
}
}
$message = new htmlStatusMessage('INFO', _('You made no changes'));
$jsonData = array();
// rename DN if RDN attribute changed
$rdnAttribute = strtolower(extractRDNAttribute($dn));
$rdnValue = extractRDNValue($dn);
if (isset($ldapChanges[$rdnAttribute]) && isset($ldapChanges[$rdnAttribute][0]) && !in_array($rdnValue, $ldapChanges[$rdnAttribute])) {
$pos = 0;
$oldPos = array_search($rdnValue, $changes[$rdnAttribute]['old']);
if (($oldPos !== false) && isset($ldapChanges[$rdnAttribute][$oldPos])) {
$pos = $oldPos;
}
$newRdnValue = $ldapChanges[$rdnAttribute][$pos];
$newRdn = $rdnAttribute . '=' . ldap_escape($newRdnValue, '', LDAP_ESCAPE_DN);
$parent = extractDNSuffix($dn);
$renameOk = ldap_rename($_SESSION['ldap']->server(), $dn, $newRdn, $parent, $_SESSION['ldap']->isActiveDirectory());
$newDn = $newRdn . ',' . $parent;
if ($renameOk) {
logNewMessage(LOG_DEBUG, 'Renamed ' . $dn . ' to ' . $newDn);
$dn = $newDn;
$jsonData['newDn'] = base64_encode($newDn);
$jsonData['parent'] = base64_encode($parent);
unset($ldapChanges[$rdnAttribute]);
$message = new htmlStatusMessage('INFO', _('All changes were successful.'));
}
else {
logNewMessage(LOG_ERR, 'Renaming ' . $dn . ' to ' . $newDn . ' failed: ' . getExtendedLDAPErrorMessage($_SESSION['ldap']->server()));
$message = new htmlStatusMessage('ERROR', sprintf(_('Was unable to rename DN: %s.'), $dn), getExtendedLDAPErrorMessage($_SESSION['ldap']->server()));
// skip other changes
$ldapChanges = array();
}
}
if (!empty($ldapChanges)) {
$saved = ldap_modify($_SESSION['ldap']->server(), $dn, $ldapChanges);
if ($saved) {
$message = new htmlStatusMessage('INFO', _('All changes were successful.'));
}
else {
$message = new htmlStatusMessage('ERROR', sprintf(_('Was unable to modify attributes of DN: %s.'), $dn), getDefaultLDAPErrorString($_SESSION['ldap']->server()));
}
}
$tabIndex = 1;
ob_start();
parseHtml(null, $message, array(), true, $tabIndex, 'none');
$messageContent = ob_get_contents();
ob_end_clean();
$jsonData['result'] = $messageContent;
return json_encode($jsonData);
}
/**
* Base 64 decodes attribute values.
*
* @param string[] $encoded encoded data
* @return string[] binary data
*/
private function decodeBinaryAttributeValues(array $encoded): array {
$binaryValues = array();
foreach ($encoded as $value) {
$decoded = base64_decode($value, true);
if ($decoded !== false) {
$binaryValues[] = $decoded;
}
else {
$binaryValues[] = $value;
}
}
return $binaryValues;
}
/**
* Applies password hashing on the provided values.
*
* @param array $values values
* @param array $hash hash types
* @return array hashed values
*/
private function applyPasswordHash(array $values, array $hash): array {
$result = array();
for ($i = 0; $i < sizeof($values); $i++) {
$oldType = getHashType($values[$i]);
if (($oldType === 'PLAIN') && ($hash[$i] !== 'PLAIN')) {
$result[] = pwd_hash($values[$i], true, $hash[$i]);
}
else {
$result[] = $values[$i];
}
}
return $result;
}
/**
* Displays the content to create a new subnode.
*
* @param string $dn DN
* @return string JSON data
*/
private function createNewNode(string $dn): string {
$step = $_GET['step'];
switch ($step) {
case 'getObjectClasses':
return $this->createNewNodeGetObjectClassesStep($dn);
case 'checkObjectClasses':
return $this->createNewNodeCheckObjectClassesStep($dn);
case 'checkAttributes':
return $this->createNewNodeCheckAttributesStep($dn);
}
logNewMessage(LOG_ERR, 'Invalid create new node step: ' . $step);
}
/**
* Returns the content to select the object classes.
*
* @param string $dn DN
* @param string|null $errorMessage error if any
* @return string JSON data
*/
private function createNewNodeGetObjectClassesStep(string $dn, string $errorMessage = null): string {
$row = new htmlResponsiveRow();
$row->add(new htmlTitle(_('Create a child entry')));
if ($errorMessage !== null) {
$row->add(new htmlStatusMessage('ERROR', $errorMessage));
}
$row->addLabel(new htmlOutputText(_('Parent')));
$row->addField(new htmlOutputText($dn));
$row->addVerticalSpacer('1rem');
$schemaObjectClasses = $this->getSchemaObjectClasses();
$objectClassOptions = array();
$selectCssClasses = array();
foreach ($schemaObjectClasses as $schemaObjectClass) {
$name = $schemaObjectClass->getName();
$objectClassOptions[$name] = $name;
if ($schemaObjectClass->getType() === 'structural') {
$selectCssClasses[$name] = 'bold';
}
}
$objectClassSelect = new htmlResponsiveSelect('objectClasses', $objectClassOptions, array(), _('Object classes'), null, 10);
$objectClassSelect->setHasDescriptiveElements(true);
$objectClassSelect->setMultiSelect(true);
$objectClassSelect->setOptionCssClasses($selectCssClasses);
$row->add($objectClassSelect, 12);
$row->addVerticalSpacer('0.5rem');
$filterGroup = new htmlGroup();
$filterGroup->addElement(new htmlOutputText(_('Filter') . ' '));
$filterInput = new htmlInputField('filter', '');
$filterInput->filterSelectBox('objectClasses');
$filterGroup->addElement($filterInput);
$row->addLabel(new htmlOutputText('&nbsp;', false));
$row->addField($filterGroup);
$row->addVerticalSpacer('1rem');
$nextButton = new htmlButton('next', _('Next'));
$nextButton->setCSSClasses(array('lam-primary'));
$nextButton->setOnClick('window.lam.treeview.createNodeSelectObjectClassesStep(event, \'' . getSecurityTokenName() . '\', \'' . getSecurityTokenValue() . '\');');
$row->add($nextButton, 12, 12, 12, 'text-center');
$row->addVerticalSpacer('2rem');
$row->add(new htmlOutputText(_('Hint: You must choose exactly one structural object class (shown in bold above)')), 12);
$row->add(new htmlHiddenInput('parentDn', base64_encode($dn)), 12);
ob_start();
$tabIndex = 1;
parseHtml(null, $row, array(), false, $tabIndex, '');
$content = ob_get_contents();
ob_end_clean();
return json_encode(array('content' => $content));
}
/**
* Returns the content to select the object classes.
*
* @param string $dn DN
* @param string|null $errorMessage error if any
* @param string|null $rdnAttribute RDN attribute name
* @param array|null $attributes attribute values
* @return string JSON data
*/
private function createNewNodeCheckObjectClassesStep(string $dn, ?string $errorMessage = null, ?string $rdnAttribute = null, ?array $attributes = null): string {
if ($attributes === null) {
$objectClasses = $_POST['objectClasses'];
}
else {
$objectClasses = $attributes['objectClass'];
}
$structuralObjectClassesCount = 0;
$schemaObjectClasses = $this->getSchemaObjectClasses();
foreach ($objectClasses as $objectClass) {
$objectClassLower = strtolower($objectClass);
if (!isset($schemaObjectClasses[$objectClassLower])) {
logNewMessage(LOG_ERR, 'Tree view new node, invalid object class: ' . $objectClass);
return $this->createNewNodeGetObjectClassesStep($dn, _('Invalid object class.'));
}
if ($schemaObjectClasses[$objectClassLower]->getType() === 'structural') {
$structuralObjectClassesCount++;
}
}
if ($structuralObjectClassesCount === 0) {
return $this->createNewNodeGetObjectClassesStep($dn, _('No structural object class selected.'));
}
elseif ($structuralObjectClassesCount > 1) {
return $this->createNewNodeGetObjectClassesStep($dn, _('Multiple structural object classes selected.'));
}
$row = new htmlResponsiveRow();
$row->add(new htmlTitle(_('Create a child entry')));
if ($errorMessage !== null) {
$row->add(new htmlStatusMessage('ERROR', $errorMessage));
$row->addVerticalSpacer('0.5rem');
}
$row->addLabel(new htmlOutputText(_('Parent')));
$row->addField(new htmlOutputText($dn));
$row->addVerticalSpacer('1rem');
$row->addLabel(new htmlOutputText(_('Object classes')));
$row->addField(new htmlOutputText(implode(', ', $objectClasses)));
$row->addVerticalSpacer('1rem');
$schemaAttributes = $this->getSchemaAttributes();
$mustAttributes = array();
$mayAttributes = array();
foreach ($objectClasses as $objectClass) {
$classMustAttributeNames = $this->getMustAttributeNamesRecursive($schemaObjectClasses, $objectClass);
foreach ($classMustAttributeNames as $classMustAttributeName) {
$attrNameLower = strtolower($classMustAttributeName);
if (!isset($schemaAttributes[$attrNameLower])) {
logNewMessage(LOG_ERR, 'Tree view new node, invalid attribute: ' . $classMustAttributeName);
return $this->createNewNodeGetObjectClassesStep($dn, _('Invalid object class.'));
}
$mustAttributes[$attrNameLower] = $schemaAttributes[$attrNameLower];
}
}
foreach ($objectClasses as $objectClass) {
$classMayAttributeNames = $this->getMayAttributeNamesRecursive($schemaObjectClasses, $objectClass);
foreach ($classMayAttributeNames as $classMayAttributeName) {
$attrNameLower = strtolower($classMayAttributeName);
if (!isset($schemaAttributes[$attrNameLower])) {
logNewMessage(LOG_ERR, 'Tree view new node, invalid attribute: ' . $classMayAttributeName);
return $this->createNewNodeGetObjectClassesStep($dn, _('Invalid object class.'));
}
if (array_key_exists($attrNameLower, $mustAttributes)) {
continue;
}
$mayAttributes[$attrNameLower] = $schemaAttributes[$attrNameLower];
}
}
if (isset($mustAttributes['objectclass'])) {
unset($mustAttributes['objectclass']);
}
ksort($mustAttributes);
ksort($mayAttributes);
$allAttributes = array_merge($mustAttributes, $mayAttributes);
ksort($allAttributes);
$rdnOptions = array();
foreach ($allAttributes as $attribute) {
$rdnOptions[] = $attribute->getName();
}
$rdnOptionsSelected = array();
if ($rdnAttribute !== null) {
$rdnOptionsSelected[] = $rdnAttribute;
}
elseif (isset($allAttributes['cn'])) {
$rdnOptionsSelected[] = $allAttributes['cn']->getName();
}
elseif (isset($allAttributes['ou'])) {
$rdnOptionsSelected[] = $allAttributes['ou']->getName();
}
$row->add(new htmlResponsiveSelect('rdn', $rdnOptions, $rdnOptionsSelected, _('RDN identifier')));
if (!empty($mustAttributes)) {
$row->add(new htmlSubTitle(_('Required attributes')));
}
foreach ($mustAttributes as $mustAttribute) {
$values = empty($attributes[$mustAttribute->getName()]) ? array('') : $attributes[$mustAttribute->getName()];
$this->addAttributeContent($row, $mustAttribute->getName(), $values, $mustAttribute, $objectClasses, null, array());
}
if (!empty($mayAttributes)) {
$row->add(new htmlSubTitle(_('Optional attributes')));
}
foreach ($mayAttributes as $mayAttribute) {
$values = empty($attributes[$mayAttribute->getName()]) ? array('') : $attributes[$mayAttribute->getName()];
$this->addAttributeContent($row, $mayAttribute->getName(), $values, $mayAttribute, $objectClasses, null, array());
}
$row->addVerticalSpacer('1rem');
$nextButton = new htmlButton('save', _('Create'));
$nextButton->setCSSClasses(array('lam-primary'));
$nextButton->setOnClick('window.lam.treeview.createNodeEnterAttributesStep(event, \'' . getSecurityTokenName() . '\', \'' . getSecurityTokenValue() . '\');');
$row->add($nextButton, 12, 12, 12, 'text-center');
$row->add(new htmlHiddenInput('objectClasses', implode(',', $objectClasses)));
$row->add(new htmlHiddenInput('parentDn', base64_encode($dn)));
ob_start();
$tabIndex = 1;
parseHtml(null, $row, array(), false, $tabIndex, '');
$content = ob_get_contents();
ob_end_clean();
return json_encode(array('content' => $content));
}
/**
* Gets a recursive list of must attribute names.
*
* @param ObjectClass[] $objectClasses schema object classes
* @param string $objectClass object class
* @return array attribute names
*/
private function getMustAttributeNamesRecursive(array $objectClasses, string $objectClass): array {
$objectClassLower = strtolower($objectClass);
$objectClassObject = $objectClasses[$objectClassLower];
$attributeNames = $objectClassObject->getMustAttrNames();
if ($attributeNames === null) {
$attributeNames = array();
}
if (!empty($objectClassObject->getSupClasses())) {
foreach ($objectClassObject->getSupClasses() as $superClass) {
$attributeNames = array_merge($attributeNames, $this->getMustAttributeNamesRecursive($objectClasses, $superClass));
}
}
$attributeNames = array_map('strtolower', $attributeNames);
$attributeNames = array_unique($attributeNames);
return $attributeNames;
}
/**
* Gets a recursive list of may attribute names.
*
* @param ObjectClass[] $objectClasses schema object classes
* @param string $objectClass object class
* @return array attribute names
*/
private function getMayAttributeNamesRecursive(array $objectClasses, string $objectClass): array {
$objectClassLower = strtolower($objectClass);
$objectClassObject = $objectClasses[$objectClassLower];
$attributeNames = $objectClassObject->getMayAttrNames();
if ($attributeNames === null) {
$attributeNames = array();
}
if (!empty($objectClassObject->getSupClasses())) {
foreach ($objectClassObject->getSupClasses() as $superClass) {
$attributeNames = array_merge($attributeNames, $this->getMayAttributeNamesRecursive($objectClasses, $superClass));
}
}
$attributeNames = array_map('strtolower', $attributeNames);
$attributeNames = array_unique($attributeNames);
return $attributeNames;
}
/**
* Returns the content for the check attributes step of node creation.
*
* @param string $dn DN
* @param string|null $errorMessage error if any
* @return string JSON data
*/
private function createNewNodeCheckAttributesStep(string $dn, string $errorMessage = null): string {
$objectClasses = $_POST['objectClasses'];
$rdnAttribute = $_POST['rdn'];
$attributeChanges = json_decode($_POST['attributes'], true);
$attributes = array('objectClass' => explode(',', $objectClasses));
foreach ($attributeChanges as $attributeName => $attributeChange) {
if (isset($attributeChange['new'])) {
$attributes[$attributeName] = $attributeChange['new'];
if (isset($attributeChange['hash'])) {
$attributes[$attributeName] = $this->applyPasswordHash($attributes[$attributeName], $attributeChange['hash']);
}
}
if (isset($attributeChange['upload'])) {
$attributes[$attributeName][] = base64_decode($attributeChange['upload']);
}
}
if (!isset($attributes[$rdnAttribute][0])) {
return $this->createNewNodeCheckObjectClassesStep($dn, _('The RDN field is empty.'), $rdnAttribute, $attributes);
}
$rdn = $rdnAttribute . '=' . ldap_escape($attributes[$rdnAttribute][0], '', LDAP_ESCAPE_DN);
$newDn = $rdn . ',' . $dn;
$success = ldap_add($_SESSION['ldap']->server(), $newDn, $attributes);
if (!$success) {
return $this->createNewNodeCheckObjectClassesStep($dn, getExtendedLDAPErrorMessage($_SESSION['ldap']->server()), $rdnAttribute, $attributes);
}
return $this->getNodeContent($newDn, new htmlStatusMessage('INFO',
sprintf(_('Creation successful. DN <b>%s</b> has been created.'),
htmlspecialchars(unescapeLdapSpecialCharacters($newDn)))));
}
/**
* Deletes a node in LDAP.
*
* @param string $dn DN
* @return string JSON
*/
private function deleteNode(string $dn): string {
$errors = deleteDN($dn, true);
foreach ($errors as $error) {
logNewMessage(LOG_ERR, 'Tree view delete node failed: ' . $error[0] . ' ' . $error[1]);
}
if (!empty($errors)) {
return json_encode(array('errors' => $errors));
}
return json_encode(array());
}
/**
* Stops processing if DN is invalid.
*
* @param string $dn DN
*/
private function validateDn(string $dn): void {
$dn = strtolower($dn);
$rootDns = TreeViewTool::getRootDns();
foreach ($rootDns as $rootDn) {
$rootDn = strtolower($rootDn);
if (substr($dn, -1 * strlen($rootDn)) === $rootDn) {
return;
}
}
logNewMessage(LOG_ERR, 'Invalid DN for tree view: ' . $dn);
die();
}
/**
* Returns the proper spelling of the attribute name.
*
* @param string $attributeName attribute name in lower-case
* @return string proper attribute name
*/
private function getProperAttributeName(string $attributeName): string {
$schemaAttributes = $this->getSchemaAttributes();
if (isset($schemaAttributes[$attributeName])) {
return $schemaAttributes[$attributeName]->getName();
}
return $attributeName;
}
/**
* Returns the schema attributes.
*
* @return AttributeType[] attributes
*/
private function getSchemaAttributes(): array {
if ($this->schemaAttributes === null) {
$this->schemaAttributes = get_schema_attributes(null);
}
return $this->schemaAttributes;
}
/**
* Returns the schema object classes.
*
* @return ObjectClass[] object classes
*/
private function getSchemaObjectClasses(): array {
if ($this->schemaObjectClasses === null) {
$this->schemaObjectClasses = get_schema_objectclasses();
}
return $this->schemaObjectClasses;
}
/**
* Stops processing if no write access is allowed.
*/
private function ensureWriteAccess(): void {
if (!checkIfWriteAccessIsAllowed()) {
logNewMessage(LOG_ERR, 'Write operation denied for tree view.');
die();
}
}
/**
* Renders the search mask.
*
* @param string $dn DN
* @return string JSON
*/
private function search(string $dn): string {
$row = new htmlResponsiveRow();
$row->add(new htmlTitle(_('Search')));
$row->addLabel(new htmlOutputText(_('Base DN')));
$row->addField(new htmlOutputText(unescapeLdapSpecialCharacters($dn)));
$row->addVerticalSpacer('1rem');
$scopeOptions = array(
_('Sub (entire subtree)') => 'sub',
_('One (one level beneath base)') => 'one',
);
$scopeSelect = new htmlResponsiveSelect('scope', $scopeOptions, array(), _('Search scope'));
$scopeSelect->setSortElements(false);
$scopeSelect->setHasDescriptiveElements(true);
$row->add($scopeSelect);
$filterInput = new htmlResponsiveInputField(_('Search filter'), 'filter', '(objectClass=*)', null, true);
$row->add($filterInput);
$row->add(new htmlResponsiveInputField(_('Attributes'), 'attributes', 'cn, givenName, sn, uid', null, true));
$row->add(new htmlResponsiveInputField(_('Order by'), 'orderBy', 'dn'));
$resultCountInput = new htmlResponsiveInputField(_('LDAP search limit'), 'limit', '50');
$resultCountInput->setValidationRule(htmlElement::VALIDATE_NUMERIC);
$row->add($resultCountInput);
$displayFormat = array(
_('List') => 'list',
_('Table') => 'table'
);
$formatSelect = new htmlResponsiveSelect('format', $displayFormat, array('list'), _('Display format'));
$formatSelect->setHasDescriptiveElements(true);
$row->add($formatSelect);
$row->addVerticalSpacer('2rem');
$nextButton = new htmlButton('search', _('Search'));
$nextButton->setCSSClasses(array('lam-primary'));
$nextButton->setOnClick('window.lam.treeview.searchResults(event, \'' . getSecurityTokenName() . '\', \'' . getSecurityTokenValue() . '\', \'' . base64_encode($dn) . '\');');
$row->add($nextButton, 12, 12, 12, 'text-center');
ob_start();
$tabIndex = 1;
parseHtml(null, $row, array(), false, $tabIndex, '');
$content = ob_get_contents();
ob_end_clean();
return json_encode(array('content' => $content));
}
/**
* Renders the search results.
*
* @param string $dn DN
* @return string JSON
*/
private function searchResults(string $dn): string {
$scope = $_POST['scope'];
if (!in_array($scope, array('sub', 'one'))) {
logNewMessage(LOG_ERR, 'Invalid search scope: ' . $scope);
die();
}
$format = $_POST['format'];
if (!in_array($format, array('list', 'table'))) {
logNewMessage(LOG_ERR, 'Invalid search format: ' . $format);
die();
}
$filter = empty($_POST['filter']) ? '(objectClass=*)' : $_POST['filter'];
$attributes = preg_split('/,[ ]*/', $_POST['attributes']);
$searchAttributes = $attributes;
if (!in_array_ignore_case('objectClass', $searchAttributes)) {
$searchAttributes[] = 'objectClass';
}
global $lamOrderByAttribute;
$lamOrderByAttribute = empty($_POST['orderBy']) ? 'dn' : strtolower($_POST['orderBy']);
$limit = empty($_POST['limit']) ? 0 : intval($_POST['limit']);
$results = array();
switch ($scope) {
case 'sub':
$results = searchLDAP($dn, $filter, $searchAttributes, $limit);
break;
case 'one':
$results = ldapListDN($dn, $filter, $searchAttributes, null, $limit);
break;
}
usort($results, 'LAM\TOOLS\TREEVIEW\compareByAttributes');
$row = $this->searchResultsHeader($dn, $filter);
if ($format === 'list') {
return $this->searchResultsAsList($results, $attributes, $row);
}
return $this->searchResultsAsTable($results, $attributes, $row);
}
/**
* Creates the header part of the search results.
*
* @param string $dn search base
* @param string $filter LDAP filter
* @return htmlResponsiveRow content
*/
private function searchResultsHeader(string $dn, string $filter): htmlResponsiveRow {
$row = new htmlResponsiveRow();
$row->add(new htmlTitle(_('Search Results')));
$row->addLabel(new htmlOutputText(_('Base DN')));
$row->addField(new htmlOutputText(unescapeLdapSpecialCharacters($dn)));
$row->addVerticalSpacer('0.5rem');
$row->addLabel(new htmlOutputText(_('Search filter')));
$row->addField(new htmlOutputText($filter));
$row->addVerticalSpacer('2rem');
return $row;
}
/**
* Returns the search results as list.
*
* @param array $results results
* @param array $attributes attribute list to show
* @param htmlResponsiveRow $row content
* @return string JSON
*/
private function searchResultsAsList(array $results, array $attributes, htmlResponsiveRow $row): string {
foreach ($results as $result) {
$row->add(new htmlSubTitle(getAbstractDN($result['dn']), $this->getNodeIcon($result)));
$row->addLabel(new htmlOutputText('dn'));
$row->addField(new htmlLink(unescapeLdapSpecialCharacters($result['dn']), 'treeView.php?dn=' . base64_encode($result['dn'])));
$result = array_change_key_case($result, CASE_LOWER);
foreach ($attributes as $attribute) {
$attributeLower = strtolower($attribute);
if (!empty($result[$attributeLower])) {
$row->addLabel(new htmlOutputText($attribute));
$row->addField(new htmlOutputText(implode(', ', $result[$attributeLower])));
}
}
$row->addVerticalSpacer('1rem');
}
ob_start();
$tabIndex = 1;
parseHtml(null, $row, array(), false, $tabIndex, '');
$content = ob_get_contents();
ob_end_clean();
return json_encode(array('content' => $content));
}
/**
* Returns the search results as table.
*
* @param array $results results
* @param array $attributes attribute list to show
* @param htmlResponsiveRow $row content
* @return string JSON
*/
private function searchResultsAsTable(array $results, array $attributes, htmlResponsiveRow $row) {
$titles = array_merge(array('', 'dn'), $attributes);
$data = array();
foreach ($results as $result) {
$dataEntry = array($this->getNodeIcon($result), new htmlLink(unescapeLdapSpecialCharacters($result['dn']), 'treeView.php?dn=' . base64_encode($result['dn'])));
$result = array_change_key_case($result, CASE_LOWER);
foreach ($attributes as $attribute) {
$attributeLower = strtolower($attribute);
if (!empty($result[$attributeLower])) {
$dataEntry[] = new htmlOutputText(implode(', ', $result[$attributeLower]));
}
else {
$dataEntry[] = new htmlOutputText('');
}
}
$data[] = $dataEntry;
}
$table = new htmlResponsiveTable($titles, $data);
$table->setCSSClasses(array('colored--table'));
$row->add($table);
ob_start();
$tabIndex = 1;
parseHtml(null, $row, array(), false, $tabIndex, '');
$content = ob_get_contents();
ob_end_clean();
return json_encode(array('content' => $content));
}
/**
* Performs paste operations.
*
* @param string $dn
* @return string
*/
private function paste(string $dn): string {
$targetDn = base64_decode($_POST['targetDn']);
$this->validateDn($targetDn);
$action = $_POST['action'];
if (!in_array($action, array('COPY', 'CUT'))) {
logNewMessage(LOG_ERR, 'Invalid tree paste action: ' . $action);
die();
}
try {
$entryAndChildren = ldapListDN($dn);
$childrenCount = sizeof($entryAndChildren);
logNewMessage(LOG_DEBUG, 'Paste operation for entry with ' . $childrenCount . ' child entries.');
if (($childrenCount === 0) && ($action === 'CUT')) {
// do LDAP move for entries without children and CUT operation
moveDn($dn, $targetDn);
return json_encode(array());
}
copyDnRecursive($dn, $targetDn);
if ($action === 'CUT') {
$errors = deleteDN($dn, true);
if (!empty($errors)) {
$row = new htmlResponsiveRow();
foreach ($errors as $error) {
$row->add(new htmlStatusMessage($error[0], $error[1], isset($error[2]) ? $error[2] : null));
}
ob_start();
$tabIndex = 1;
parseHtml(null, $row, array(), false, $tabIndex, '');
$content = ob_get_contents();
ob_end_clean();
return json_encode(array('error' => $content));
}
}
}
catch (LAMException $e) {
$message = new htmlStatusMessage('ERROR', $e->getTitle(), $e->getMessage());
ob_start();
$tabIndex = 1;
parseHtml(null, $message, array(), false, $tabIndex, '');
$content = ob_get_contents();
ob_end_clean();
return json_encode(array('error' => $content));
}
return json_encode(array());
}
}
/**
* Compares two nodes by interpreting their ID as DN.
*
* @param $a first node
* @param $b second node
* @return int result
*/
function compareNodeByIdAsDn($a, $b): int {
return strnatcasecmp(extractRDN(base64_decode($a['id'])), extractRDN(base64_decode($b['id'])));
}
/**
* Compares two arrays with LDAP attributes by global $lamOrderByAttribute.
*
* @param $a first node
* @param $b second node
* @return int result
*/
function compareByAttributes($a, $b): int {
global $lamOrderByAttribute;
if ($lamOrderByAttribute === 'dn') {
return compareDN($a['dn'], $b['dn']);
}
$a = array_change_key_case($a, CASE_LOWER);
$b = array_change_key_case($b, CASE_LOWER);
if (!isset($a[$lamOrderByAttribute]) && !isset($b[$lamOrderByAttribute])) {
return 0;
}
if (!empty($a[$lamOrderByAttribute]) && empty($b[$lamOrderByAttribute])) {
return 1;
}
if (empty($a[$lamOrderByAttribute]) && !empty($b[$lamOrderByAttribute])) {
return -1;
}
natcasesort($a[$lamOrderByAttribute]);
natcasesort($b[$lamOrderByAttribute]);
$maxA = array_pop($a[$lamOrderByAttribute]);
$maxB = array_pop($b[$lamOrderByAttribute]);
return strnatcasecmp($maxA, $maxB);
}