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) {
throw new LdapException(
'Cannot fetch single DN for %s',
$query->create()
$query
);
}
return key($rows);
@ -272,16 +272,20 @@ class Connection implements Selectable
* @return array The matched entries
* @throws LdapException
*/
protected function runQuery(Query $query, $fields = array())
protected function runQuery(Query $query, array $fields = null)
{
$limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() - 1 : 0;
if (empty($fields)) {
$fields = $query->getColumns();
}
$results = @ldap_search(
$this->ds,
$query->hasBase() ? $query->getBase() : $this->root_dn,
$query->create(),
empty($fields) ? $query->listFields() : $fields,
$query->getBase() ?: $this->root_dn,
(string) $query,
array_values($fields),
0, // Attributes and values
$limit ? $offset + $limit : 0
);
@ -292,8 +296,8 @@ class Connection implements Selectable
throw new LdapException(
'LDAP query "%s" (base %s) failed. Error: %s',
$query->create(),
$query->hasBase() ? $query->getBase() : $this->root_dn,
$query,
$query->getBase() ?: $this->root_dn,
ldap_error($this->ds)
);
} elseif (ldap_count_entries($this->ds, $results) === 0) {
@ -336,28 +340,29 @@ class Connection implements Selectable
*
* @param Query $query The query to execute
* @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
* @throws LdapException
* @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)) {
throw new ProgrammingError('LDAP: Page control not available.');
}
if (! isset($pageSize)) {
$pageSize = static::PAGE_SIZE;
}
$limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() - 1 : 0;
$queryString = $query->create();
$base = $query->hasBase() ? $query->getBase() : $this->root_dn;
$queryString = (string) $query;
$base = $query->getBase() ?: $this->root_dn;
if (empty($fields)) {
$fields = $query->listFields();
$fields = $query->getColumns();
}
$count = 0;
@ -368,7 +373,14 @@ class Connection implements Selectable
// possibillity server to return an answer in case the pagination extension is missing.
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 (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) {
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
// the server: https://www.ietf.org/rfc/rfc2696.txt
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 {
// Reset the paged search request so that subsequent requests succeed
ldap_control_paged_result($this->ds, 0);

View File

@ -3,151 +3,151 @@
namespace Icinga\Protocol\Ldap;
use Icinga\Data\SimpleQuery;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\NotImplementedError;
/**
* Search class
*
* @package Icinga\Protocol\Ldap
* LDAP query class
*/
/**
* 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
class Query extends SimpleQuery
{
protected $connection;
protected $filters = array();
protected $fields = array();
protected $limit_count = 0;
protected $limit_offset = 0;
protected $sort_columns = array();
protected $count;
protected $base;
protected $usePagedResults = true;
/**
* This query's filters
*
* Currently just a basic key/value pair based array. Can be removed once Icinga\Data\Filter is supported.
*
* @var array
*/
protected $filters;
/**
* Constructor
* The base dn being used for this query
*
* @param Connection LDAP Connection object
* @return void
* @var string
*/
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)
{
$this->base = $base;
return $this;
}
public function hasBase()
{
return $this->base !== null;
}
/**
* 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;
}
/**
* 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->count = $this->connection->count($this);
}
return $this->count;
$this->filters['objectClass'] = $target;
return parent::from($target, $fields);
}
/**
* 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)) {
throw new Exception(
'Got invalid limit: %s, %s',
$count,
$offset
);
// TODO: Adjust this once support for Icinga\Data\Filter is available
if ($condition instanceof Expression) {
$this->filters[] = $condition;
} else {
$this->filters[$condition] = $value;
}
$this->limit_count = (int) $count;
$this->limit_offset = (int) $offset;
return $this;
}
/**
* Whether a limit has been set
*
* @return boolean
*/
public function hasLimit()
public function getFilter()
{
return $this->limit_count > 0;
throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
}
/**
* Whether an offset (limit) has been set
*
* @return boolean
*/
public function hasOffset()
public function applyFilter(Filter $filter)
{
return $this->limit_offset > 0;
throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
}
/**
* Retrieve result limit
*
* @return int
*/
public function getLimit()
public function addFilter(Filter $filter)
{
return $this->limit_count;
throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
}
/**
* Retrieve result offset
*
* @return int
*/
public function getOffset()
public function setFilter(Filter $filter)
{
return $this->limit_offset;
throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
}
/**
* 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()
{
@ -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);
}
/**
* 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) {
if (! isset($this->filters['objectClass'])) {
throw new Exception('Object class is mandatory');
}
$parts = array();
foreach ($this->filters as $key => $value) {
if ($value instanceof Expression) {
$parts[] = (string) $value;
@ -341,6 +216,7 @@ class Query
);
}
}
if (count($parts) > 1) {
return '(&(' . implode(')(', $parts) . '))';
} else {
@ -348,17 +224,13 @@ class Query
}
}
/**
* Return the LDAP filter to be applied on this query
*
* @return string
*/
public function __toString()
{
return $this->create();
}
/**
* Descructor
*/
public function __destruct()
{
// To be on the safe side:
unset($this->connection);
return $this->renderFilter();
}
}

View File

@ -36,51 +36,11 @@ class QueryTest extends BaseTestCase
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()
{
$this->markTestIncomplete('testFetchTree is not implemented yet - requires real LDAP');
}
public function testFrom()
{
return $this->testListFields();
}
public function testWhere()
{
$this->markTestIncomplete('testWhere is not implemented yet');
@ -88,30 +48,13 @@ class QueryTest extends BaseTestCase
public function testOrder()
{
$select = $this->emptySelect()->order('bla');
// tested by testGetSortColumns
$this->markTestIncomplete('testOrder is not implemented yet, order support for ldap queries is incomplete');
}
public function testListFields()
{
$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()
public function testRenderFilter()
{
$select = $this->prepareSelect();
$res = '(&(objectClass=dummyClass)(testIntColumn=1)(testStringColumn=test)(testWildcard=abc*))';
$this->assertEquals($res, $select->create());
$this->assertEquals($res, (string) $select);
}
}