Ldap\Query: Extend SimpleQuery and add missing documentation

refs #8826
refs #8955
This commit is contained in:
Johannes Meyer 2015-05-04 11:26:27 +02:00
parent 99213432f5
commit 5baa0590b1
3 changed files with 135 additions and 308 deletions

View File

@ -210,7 +210,7 @@ class Connection implements Selectable
if (count($rows) > 1) { if (count($rows) > 1) {
throw new LdapException( throw new LdapException(
'Cannot fetch single DN for %s', 'Cannot fetch single DN for %s',
$query->create() $query
); );
} }
return key($rows); return key($rows);
@ -272,16 +272,20 @@ class Connection implements Selectable
* @return array The matched entries * @return array The matched entries
* @throws LdapException * @throws LdapException
*/ */
protected function runQuery(Query $query, $fields = array()) protected function runQuery(Query $query, array $fields = null)
{ {
$limit = $query->getLimit(); $limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() - 1 : 0; $offset = $query->hasOffset() ? $query->getOffset() - 1 : 0;
if (empty($fields)) {
$fields = $query->getColumns();
}
$results = @ldap_search( $results = @ldap_search(
$this->ds, $this->ds,
$query->hasBase() ? $query->getBase() : $this->root_dn, $query->getBase() ?: $this->root_dn,
$query->create(), (string) $query,
empty($fields) ? $query->listFields() : $fields, array_values($fields),
0, // Attributes and values 0, // Attributes and values
$limit ? $offset + $limit : 0 $limit ? $offset + $limit : 0
); );
@ -292,8 +296,8 @@ class Connection implements Selectable
throw new LdapException( throw new LdapException(
'LDAP query "%s" (base %s) failed. Error: %s', 'LDAP query "%s" (base %s) failed. Error: %s',
$query->create(), $query,
$query->hasBase() ? $query->getBase() : $this->root_dn, $query->getBase() ?: $this->root_dn,
ldap_error($this->ds) ldap_error($this->ds)
); );
} elseif (ldap_count_entries($this->ds, $results) === 0) { } elseif (ldap_count_entries($this->ds, $results) === 0) {
@ -336,28 +340,29 @@ class Connection implements Selectable
* *
* @param Query $query The query to execute * @param Query $query The query to execute
* @param array $fields The fields that will be fetched from the matches * @param array $fields The fields that will be fetched from the matches
* @param int $page_size The maximum page size, defaults to Connection::PAGE_SIZE * @param int $pageSize The maximum page size, defaults to Connection::PAGE_SIZE
* *
* @return array The matched entries * @return array The matched entries
* @throws LdapException * @throws LdapException
* @throws ProgrammingError When executed without available page controls (check with pageControlAvailable() ) * @throws ProgrammingError When executed without available page controls (check with pageControlAvailable() )
*/ */
protected function runPagedQuery(Query $query, $fields = array(), $pageSize = null) protected function runPagedQuery(Query $query, array $fields = null, $pageSize = null)
{ {
if (! $this->pageControlAvailable($query)) { if (! $this->pageControlAvailable($query)) {
throw new ProgrammingError('LDAP: Page control not available.'); throw new ProgrammingError('LDAP: Page control not available.');
} }
if (! isset($pageSize)) { if (! isset($pageSize)) {
$pageSize = static::PAGE_SIZE; $pageSize = static::PAGE_SIZE;
} }
$limit = $query->getLimit(); $limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() - 1 : 0; $offset = $query->hasOffset() ? $query->getOffset() - 1 : 0;
$queryString = $query->create(); $queryString = (string) $query;
$base = $query->hasBase() ? $query->getBase() : $this->root_dn; $base = $query->getBase() ?: $this->root_dn;
if (empty($fields)) { if (empty($fields)) {
$fields = $query->listFields(); $fields = $query->getColumns();
} }
$count = 0; $count = 0;
@ -368,7 +373,14 @@ class Connection implements Selectable
// possibillity server to return an answer in case the pagination extension is missing. // possibillity server to return an answer in case the pagination extension is missing.
ldap_control_paged_result($this->ds, $pageSize, false, $cookie); ldap_control_paged_result($this->ds, $pageSize, false, $cookie);
$results = @ldap_search($this->ds, $base, $queryString, $fields, 0, $limit ? $offset + $limit : 0); $results = @ldap_search(
$this->ds,
$base,
$queryString,
array_values($fields),
0, // Attributes and values
$limit ? $offset + $limit : 0
);
if ($results === false) { if ($results === false) {
if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) { if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) {
break; break;
@ -426,7 +438,7 @@ class Connection implements Selectable
// pagedResultsControl with the size set to zero (0) and the cookie set to the last cookie returned by // pagedResultsControl with the size set to zero (0) and the cookie set to the last cookie returned by
// the server: https://www.ietf.org/rfc/rfc2696.txt // the server: https://www.ietf.org/rfc/rfc2696.txt
ldap_control_paged_result($this->ds, 0, false, $cookie); ldap_control_paged_result($this->ds, 0, false, $cookie);
ldap_search($this->ds, $base, $queryString, $fields); // Returns no entries, due to the page size ldap_search($this->ds, $base, $queryString); // Returns no entries, due to the page size
} else { } else {
// Reset the paged search request so that subsequent requests succeed // Reset the paged search request so that subsequent requests succeed
ldap_control_paged_result($this->ds, 0); ldap_control_paged_result($this->ds, 0);

View File

@ -3,151 +3,151 @@
namespace Icinga\Protocol\Ldap; namespace Icinga\Protocol\Ldap;
use Icinga\Data\SimpleQuery;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\NotImplementedError;
/** /**
* Search class * LDAP query class
*
* @package Icinga\Protocol\Ldap
*/ */
/** class Query extends SimpleQuery
* Search abstraction class
*
* Usage example:
*
* <code>
* $connection->select()->from('user')->where('sAMAccountName = ?', 'icinga');
* </code>
*
* @copyright Copyright (c) 2013 Icinga-Web Team <info@icinga.org>
* @author Icinga-Web Team <info@icinga.org>
* @package Icinga\Protocol\Ldap
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
*/
class Query
{ {
protected $connection; /**
protected $filters = array(); * This query's filters
protected $fields = array(); *
protected $limit_count = 0; * Currently just a basic key/value pair based array. Can be removed once Icinga\Data\Filter is supported.
protected $limit_offset = 0; *
protected $sort_columns = array(); * @var array
protected $count; */
protected $base; protected $filters;
protected $usePagedResults = true;
/** /**
* Constructor * The base dn being used for this query
* *
* @param Connection LDAP Connection object * @var string
* @return void
*/ */
public function __construct(Connection $connection) protected $base;
/**
* Whether this query is permitted to utilize paged results
*
* @var bool
*/
protected $usePagedResults;
/**
* Initialize this query
*/
protected function init()
{ {
$this->connection = $connection; $this->filters = array();
$this->usePagedResults = true;
} }
/**
* Set the base dn to be used for this query
*
* @param string $base
*
* @return $this
*/
public function setBase($base) public function setBase($base)
{ {
$this->base = $base; $this->base = $base;
return $this; return $this;
} }
public function hasBase() /**
{ * Return the base dn being used for this query
return $this->base !== null; *
} * @return string
*/
public function getBase() public function getBase()
{ {
return $this->base; return $this->base;
} }
/**
* Set whether this query is permitted to utilize paged results
*
* @param bool $state
*
* @return $this
*/
public function setUsePagedResults($state = true) public function setUsePagedResults($state = true)
{ {
$this->usePagedResults = (bool) $state; $this->usePagedResults = (bool) $state;
return $this; return $this;
} }
/**
* Return whether this query is permitted to utilize paged results
*
* @return bool
*/
public function getUsePagedResults() public function getUsePagedResults()
{ {
return $this->usePagedResults; return $this->usePagedResults;
} }
/** /**
* Count result set, ignoring limits * Choose an objectClass and the columns you are interested in
* *
* @return int * {@inheritdoc} This creates an objectClass filter.
*/ */
public function count() public function from($target, array $fields = null)
{ {
if ($this->count === null) { $this->filters['objectClass'] = $target;
$this->count = $this->connection->count($this); return parent::from($target, $fields);
}
return $this->count;
} }
/** /**
* Count result set, ignoring limits * Add a new filter to the query
* *
* @return int * @param string $condition Column to search in
* @param mixed $value Value to look for (asterisk wildcards are allowed)
*
* @return $this
*/ */
public function limit($count = null, $offset = null) public function where($condition, $value = null)
{ {
if (! preg_match('~^\d+~', $count . $offset)) { // TODO: Adjust this once support for Icinga\Data\Filter is available
throw new Exception( if ($condition instanceof Expression) {
'Got invalid limit: %s, %s', $this->filters[] = $condition;
$count, } else {
$offset $this->filters[$condition] = $value;
);
} }
$this->limit_count = (int) $count;
$this->limit_offset = (int) $offset;
return $this; return $this;
} }
/** public function getFilter()
* Whether a limit has been set
*
* @return boolean
*/
public function hasLimit()
{ {
return $this->limit_count > 0; throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
} }
/** public function applyFilter(Filter $filter)
* Whether an offset (limit) has been set
*
* @return boolean
*/
public function hasOffset()
{ {
return $this->limit_offset > 0; throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
} }
/** public function addFilter(Filter $filter)
* Retrieve result limit
*
* @return int
*/
public function getLimit()
{ {
return $this->limit_count; throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
} }
/** public function setFilter(Filter $filter)
* Retrieve result offset
*
* @return int
*/
public function getOffset()
{ {
return $this->limit_offset; throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
} }
/** /**
* Fetch result as tree * Fetch result as tree
* *
* @return Node * @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() public function fetchTree()
{ {
@ -179,157 +179,32 @@ class Query
} }
/** /**
* Fetch result as an array of objects * Fetch the distinguished name of the first result
* *
* @return array * @return string|false The distinguished name or false in case it's not possible to fetch a result
*
* @throws Exception In case the query returns multiple results
* (i.e. it's not possible to fetch a unique DN)
*/ */
public function fetchAll() public function fetchDn()
{ {
return $this->connection->fetchAll($this); return $this->ds->fetchDn($this);
} }
/** /**
* Fetch first result row * Return the LDAP filter to be applied on this query
* *
* @return object * @return string
*
* @throws Exception In case the objectClass filter does not exist
*/ */
public function fetchRow() protected function renderFilter()
{ {
return $this->connection->fetchRow($this); if (! isset($this->filters['objectClass'])) {
}
/**
* Fetch first column value from first result row
*
* @return mixed
*/
public function fetchOne()
{
return $this->connection->fetchOne($this);
}
/**
* Fetch a key/value list, first column is key, second is value
*
* @return array
*/
public function fetchPairs()
{
// STILL TODO!!
return $this->connection->fetchPairs($this);
}
/**
* Where to select (which fields) from
*
* This creates an objectClass filter
*
* @return Query
*/
public function from($objectClass, $fields = array())
{
$this->filters['objectClass'] = $objectClass;
$this->fields = $fields;
return $this;
}
/**
* Add a new filter to the query
*
* @param string Column to search in
* @param string Filter text (asterisks are allowed)
* @return Query
*/
public function where($key, $val)
{
$this->filters[$key] = $val;
return $this;
}
/**
* Sort by given column
*
* TODO: Sort direction is not implemented yet
*
* @param string Order column
* @param string Order direction
* @return Query
*/
public function order($column, $direction = 'ASC')
{
$this->sort_columns[] = array($column, $direction);
return $this;
}
/**
* Retrieve a list of the desired fields
*
* @return array
*/
public function listFields()
{
return $this->fields;
}
/**
* Retrieve a list containing current sort columns
*
* @return array
*/
public function getSortColumns()
{
return $this->sort_columns;
}
/**
* Return a pagination adapter for the current query
*
* @return \Zend_Paginator
*/
public function paginate($limit = null, $page = null)
{
if ($page === null || $limit === null) {
$request = \Zend_Controller_Front::getInstance()->getRequest();
if ($page === null) {
$page = $request->getParam('page', 0);
}
if ($limit === null) {
$limit = $request->getParam('limit', 20);
}
}
$paginator = new \Zend_Paginator(
// TODO: Adapter doesn't fit yet:
new \Icinga\Web\Paginator\Adapter\QueryAdapter($this)
);
$paginator->setItemCountPerPage($limit);
$paginator->setCurrentPageNumber($page);
return $paginator;
}
/**
* Add a filter expression to this query
*
* @param Expression $expression
*
* @return Query
*/
public function addFilter(Expression $expression)
{
$this->filters[] = $expression;
return $this;
}
/**
* Returns the LDAP filter that will be applied
*
* @string
*/
public function create()
{
$parts = array();
if (! isset($this->filters['objectClass']) || $this->filters['objectClass'] === null) {
throw new Exception('Object class is mandatory'); throw new Exception('Object class is mandatory');
} }
$parts = array();
foreach ($this->filters as $key => $value) { foreach ($this->filters as $key => $value) {
if ($value instanceof Expression) { if ($value instanceof Expression) {
$parts[] = (string) $value; $parts[] = (string) $value;
@ -341,6 +216,7 @@ class Query
); );
} }
} }
if (count($parts) > 1) { if (count($parts) > 1) {
return '(&(' . implode(')(', $parts) . '))'; return '(&(' . implode(')(', $parts) . '))';
} else { } else {
@ -348,17 +224,13 @@ class Query
} }
} }
/**
* Return the LDAP filter to be applied on this query
*
* @return string
*/
public function __toString() public function __toString()
{ {
return $this->create(); return $this->renderFilter();
}
/**
* Descructor
*/
public function __destruct()
{
// To be on the safe side:
unset($this->connection);
} }
} }

View File

@ -36,51 +36,11 @@ class QueryTest extends BaseTestCase
return $select; return $select;
} }
public function testLimit()
{
$select = $this->prepareSelect();
$this->assertEquals(10, $select->getLimit());
$this->assertEquals(4, $select->getOffset());
}
public function testHasLimit()
{
$select = $this->emptySelect();
$this->assertFalse($select->hasLimit());
$select = $this->prepareSelect();
$this->assertTrue($select->hasLimit());
}
public function testHasOffset()
{
$select = $this->emptySelect();
$this->assertFalse($select->hasOffset());
$select = $this->prepareSelect();
$this->assertTrue($select->hasOffset());
}
public function testGetLimit()
{
$select = $this->prepareSelect();
$this->assertEquals(10, $select->getLimit());
}
public function testGetOffset()
{
$select = $this->prepareSelect();
$this->assertEquals(10, $select->getLimit());
}
public function testFetchTree() public function testFetchTree()
{ {
$this->markTestIncomplete('testFetchTree is not implemented yet - requires real LDAP'); $this->markTestIncomplete('testFetchTree is not implemented yet - requires real LDAP');
} }
public function testFrom()
{
return $this->testListFields();
}
public function testWhere() public function testWhere()
{ {
$this->markTestIncomplete('testWhere is not implemented yet'); $this->markTestIncomplete('testWhere is not implemented yet');
@ -88,30 +48,13 @@ class QueryTest extends BaseTestCase
public function testOrder() public function testOrder()
{ {
$select = $this->emptySelect()->order('bla'); $this->markTestIncomplete('testOrder is not implemented yet, order support for ldap queries is incomplete');
// tested by testGetSortColumns
} }
public function testListFields() public function testRenderFilter()
{
$select = $this->prepareSelect();
$this->assertEquals(
array('testIntColumn', 'testStringColumn'),
$select->listFields()
);
}
public function testGetSortColumns()
{
$select = $this->prepareSelect();
$cols = $select->getSortColumns();
$this->assertEquals('testIntColumn', $cols[0][0]);
}
public function testCreateQuery()
{ {
$select = $this->prepareSelect(); $select = $this->prepareSelect();
$res = '(&(objectClass=dummyClass)(testIntColumn=1)(testStringColumn=test)(testWildcard=abc*))'; $res = '(&(objectClass=dummyClass)(testIntColumn=1)(testStringColumn=test)(testWildcard=abc*))';
$this->assertEquals($res, $select->create()); $this->assertEquals($res, (string) $select);
} }
} }