diff options
Diffstat (limited to '')
-rw-r--r-- | library/Icinga/Protocol/Ldap/LdapQuery.php | 361 |
1 files changed, 361 insertions, 0 deletions
diff --git a/library/Icinga/Protocol/Ldap/LdapQuery.php b/library/Icinga/Protocol/Ldap/LdapQuery.php new file mode 100644 index 0000000..42a236f --- /dev/null +++ b/library/Icinga/Protocol/Ldap/LdapQuery.php @@ -0,0 +1,361 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Protocol\Ldap; + +use Icinga\Data\Filter\Filter; +use LogicException; +use Icinga\Data\SimpleQuery; + +/** + * LDAP query class + */ +class LdapQuery extends SimpleQuery +{ + /** + * The base dn being used for this query + * + * @var string + */ + protected $base; + + /** + * Whether this query is permitted to utilize paged results + * + * @var bool + */ + protected $usePagedResults; + + /** + * The name of the attribute used to unfold the result + * + * @var string + */ + protected $unfoldAttribute; + + /** + * This query's native LDAP filter + * + * @var string + */ + protected $nativeFilter; + + /** + * Only fetch the entry at the base of the search + */ + const SCOPE_BASE = 'base'; + + /** + * Fetch entries one below the base DN + */ + const SCOPE_ONE = 'one'; + + /** + * Fetch all entries below the base DN + */ + const SCOPE_SUB = 'sub'; + + /** + * All available scopes + * + * @var array + */ + public static $scopes = array( + LdapQuery::SCOPE_BASE, + LdapQuery::SCOPE_ONE, + LdapQuery::SCOPE_SUB + ); + + /** + * LDAP search scope (default: SCOPE_SUB) + * + * @var string + */ + protected $scope = LdapQuery::SCOPE_SUB; + + /** + * Initialize this query + */ + protected function init() + { + $this->usePagedResults = false; + } + + /** + * Set the base dn to be used for this query + * + * @param string $base + * + * @return $this + */ + public function setBase($base) + { + $this->base = $base; + return $this; + } + + /** + * Return the base dn being used for this query + * + * @return string + */ + public function getBase() + { + return $this->base; + } + + /** + * Set whether this query is permitted to utilize paged results + * + * @param bool $state + * + * @return $this + */ + public function setUsePagedResults($state = true) + { + $this->usePagedResults = (bool) $state; + return $this; + } + + /** + * Return whether this query is permitted to utilize paged results + * + * @return bool + */ + public function getUsePagedResults() + { + return $this->usePagedResults; + } + + /** + * Set the attribute to be used to unfold the result + * + * @param string $attributeName + * + * @return $this + */ + public function setUnfoldAttribute($attributeName) + { + $this->unfoldAttribute = $attributeName; + return $this; + } + + /** + * Return the attribute to use to unfold the result + * + * @return string + */ + public function getUnfoldAttribute() + { + return $this->unfoldAttribute; + } + + /** + * Set this query's native LDAP filter + * + * @param string $filter + * + * @return $this + */ + public function setNativeFilter($filter) + { + $this->nativeFilter = $filter; + return $this; + } + + /** + * Return this query's native LDAP filter + * + * @return string + */ + public function getNativeFilter() + { + return $this->nativeFilter; + } + + /** + * Choose an objectClass and the columns you are interested in + * + * {@inheritdoc} This creates an objectClass filter. + */ + public function from($target, array $fields = null) + { + $this->where('objectClass', $target); + return parent::from($target, $fields); + } + + public function where($condition, $value = null) + { + $this->addFilter(Filter::expression($condition, '=', $value)); + return $this; + } + + public function addFilter(Filter $filter) + { + $this->makeCaseInsensitive($filter); + return parent::addFilter($filter); + } + + public function setFilter(Filter $filter) + { + $this->makeCaseInsensitive($filter); + return parent::setFilter($filter); + } + + protected function makeCaseInsensitive(Filter $filter) + { + if ($filter->isExpression()) { + /** @var \Icinga\Data\Filter\FilterExpression $filter */ + $filter->setCaseSensitive(false); + } else { + /** @var \Icinga\Data\Filter\FilterChain $filter */ + foreach ($filter->filters() as $subFilter) { + $this->makeCaseInsensitive($subFilter); + } + } + } + + public function compare($a, $b, $orderIndex = 0) + { + if (array_key_exists($orderIndex, $this->order)) { + $column = $this->order[$orderIndex][0]; + $direction = $this->order[$orderIndex][1]; + + $flippedColumns = $this->flippedColumns ?: array_flip($this->columns); + if (array_key_exists($column, $flippedColumns) && is_string($flippedColumns[$column])) { + $column = $flippedColumns[$column]; + } + + if (is_array($a->$column)) { + // rfc2891 states: If a sort key is a multi-valued attribute, and an entry happens to + // have multiple values for that attribute and no other controls are + // present that affect the sorting order, then the server SHOULD use the + // least value (according to the ORDERING rule for that attribute). + $a = clone $a; + $a->$column = array_reduce($a->$column, function ($carry, $item) use ($direction) { + $result = $carry === null ? 0 : strcmp($item, $carry); + return ($direction === self::SORT_ASC ? $result : $result * -1) < 1 ? $item : $carry; + }); + } + + if (is_array($b->$column)) { + $b = clone $b; + $b->$column = array_reduce($b->$column, function ($carry, $item) use ($direction) { + $result = $carry === null ? 0 : strcmp($item, $carry); + return ($direction === self::SORT_ASC ? $result : $result * -1) < 1 ? $item : $carry; + }); + } + } + + return parent::compare($a, $b, $orderIndex); + } + + /** + * Fetch result as tree + * + * @return Root + * + * @todo This is untested waste, not being used anywhere and ignores the query's order and base dn. + * Evaluate whether it's reasonable to properly implement and test it. + */ + public function fetchTree() + { + $result = $this->fetchAll(); + $sorted = array(); + $quotedDn = preg_quote($this->ds->getDn(), '/'); + foreach ($result as $key => & $item) { + $new_key = LdapUtils::implodeDN( + array_reverse( + LdapUtils::explodeDN( + preg_replace('/,' . $quotedDn . '$/', '', $key) + ) + ) + ); + $sorted[$new_key] = $key; + } + unset($groups); + ksort($sorted); + + $tree = Root::forConnection($this->ds); + $root_dn = $tree->getDN(); + foreach ($sorted as $sort_key => & $key) { + if ($key === $root_dn) { + continue; + } + $tree->createChildByDN($key, $result[$key]); + } + return $tree; + } + + /** + * Fetch the distinguished name of the first result + * + * @return string|false The distinguished name or false in case it's not possible to fetch a result + * + * @throws LdapException In case the query returns multiple results + * (i.e. it's not possible to fetch a unique DN) + */ + public function fetchDn() + { + return $this->ds->fetchDn($this); + } + + /** + * Render and return this query's filter + * + * @return string + */ + public function renderFilter() + { + $filter = $this->ds->renderFilter($this->filter); + if ($this->nativeFilter) { + $filter = '(&(' . $this->nativeFilter . ')' . $filter . ')'; + } + + return $filter; + } + + /** + * Return the LDAP filter to be applied on this query + * + * @return string + */ + public function __toString() + { + return $this->renderFilter(); + } + + /** + * Get LDAP search scope + * + * @return string + */ + public function getScope() + { + return $this->scope; + } + + /** + * Set LDAP search scope + * + * Valid: sub one base (Default: sub) + * + * @param string $scope + * + * @return LdapQuery + * + * @throws LogicException If scope value is invalid + */ + public function setScope($scope) + { + if (! in_array($scope, static::$scopes)) { + throw new LogicException( + 'Can\'t set scope %d, it is is invalid. Use one of %s or LdapQuery\'s constants.', + $scope, + implode(', ', static::$scopes) + ); + } + $this->scope = $scope; + return $this; + } +} |