summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Protocol/Ldap/LdapQuery.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Protocol/Ldap/LdapQuery.php')
-rw-r--r--library/Icinga/Protocol/Ldap/LdapQuery.php361
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..f4e1986
--- /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;
+ }
+
+ 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;
+ }
+}