Merge branch 'master' into bugfix/hot-all-hostgroups-are-shown-10316

This commit is contained in:
Johannes Meyer 2015-11-12 09:23:49 +01:00
commit 689cae8701
40 changed files with 1163 additions and 870 deletions

View File

@ -37,7 +37,16 @@ userpassword: password
dn: cn=Users,ou=groups,dc=icinga,dc=org
objectClass: groupOfUniqueNames
cn: Users
uniqueMember: cn=Jon Doe,ou=people,dc=icinga,dc=org
uniqueMember: cn=John Doe,ou=people,dc=icinga,dc=org
uniqueMember: cn=Jane Smith,ou=people,dc=icinga,dc=org
uniqueMember: cn=John Q. Public,ou=people,dc=icinga,dc=org
uniqueMember: cn=Richard Roe,ou=people,dc=icinga,dc=org
dn: cn=PosixUsers,ou=groups,dc=icinga,dc=org
objectClass: posixGroup
cn: PosixUsers
gidNumber: 2001
memberUid: jdoe
memberUid: jsmith
memberUid: jqpublic
memberUid: rroe

View File

@ -88,6 +88,7 @@ class icingaweb2_dev (
icingaweb2::config::general { 'authentication':
source => $name,
replace => false,
}
icingaweb2::config::general { [ 'config', 'resources', 'roles' ]:

View File

@ -44,7 +44,7 @@ class GroupController extends AuthBackendController
'backend',
array(
'autosubmit' => true,
'label' => $this->translate('Usergroup Backend'),
'label' => $this->translate('User Group Backend'),
'multiOptions' => array_combine($backendNames, $backendNames),
'value' => $this->params->get('backend')
)
@ -66,7 +66,7 @@ class GroupController extends AuthBackendController
$this->setupLimitControl();
$this->setupSortControl(
array(
'group_name' => $this->translate('Group'),
'group_name' => $this->translate('User Group'),
'created_at' => $this->translate('Created at'),
'last_modified' => $this->translate('Last modified')
),
@ -97,7 +97,7 @@ class GroupController extends AuthBackendController
->from('group_membership', array('user_name'))
->where('group_name', $groupName);
$this->setupFilterControl($members, null, array('user'));
$this->setupFilterControl($members, null, array('user'), array('group'));
$this->setupPaginationControl($members);
$this->setupLimitControl();
$this->setupSortControl(
@ -134,7 +134,7 @@ class GroupController extends AuthBackendController
$removeForm->addElement('button', 'btn_submit', array(
'escape' => false,
'type' => 'submit',
'class' => 'link-like spinner',
'class' => 'link-button spinner',
'value' => 'btn_submit',
'decorators' => array('ViewHelper'),
'label' => $this->view->icon('trash'),

View File

@ -44,7 +44,7 @@ class UserController extends AuthBackendController
'backend',
array(
'autosubmit' => true,
'label' => $this->translate('Authentication Backend'),
'label' => $this->translate('User Backend'),
'multiOptions' => array_combine($backendNames, $backendNames),
'value' => $this->params->get('backend')
)
@ -99,7 +99,8 @@ class UserController extends AuthBackendController
$this->setupFilterControl(
$memberships,
array('group_name' => t('User Group')),
array('group_name')
array('group'),
array('user')
);
$this->setupPaginationControl($memberships);
$this->setupLimitControl();
@ -140,7 +141,7 @@ class UserController extends AuthBackendController
$removeForm->addElement('button', 'btn_submit', array(
'escape' => false,
'type' => 'submit',
'class' => 'link-like spinner',
'class' => 'link-button spinner',
'value' => 'btn_submit',
'decorators' => array('ViewHelper'),
'label' => $this->view->icon('trash'),
@ -259,6 +260,7 @@ class UserController extends AuthBackendController
$alreadySeen[$groupName] = null;
$groups[] = (object) array(
'group_name' => $groupName,
'group' => $groupName,
'backend' => $backend
);
}

View File

@ -71,7 +71,7 @@ class LoginForm extends Form
if ($this->created) {
$redirect = $this->getElement('redirect')->getValue();
}
if (empty($redirect) || strpos($redirect, 'authentication/logout') !== 0) {
if (empty($redirect) || strpos($redirect, 'authentication/logout') !== false) {
$redirect = static::REDIRECT_URL;
}
return Url::fromPath($redirect);

View File

@ -4,30 +4,24 @@ use Icinga\Data\Extensible;
use Icinga\Data\Reducible;
if (! $this->compact): ?>
<div class="controls">
<?= $this->tabs; ?>
<div class="grid dont-print">
<div class="col-1-3 text-left">
<?= $this->limiter ?>
<div class="controls separated dont-print">
<?= $tabs; ?>
<div class="grid">
<?= $this->limiter ?>
<?= $this->paginator ?>
<?= $this->sortBox ?>
</div>
<div class="col-1-3">
<?= $this->paginator ?>
<div>
<?= $this->backendSelection; ?>
<?= $this->filterEditor; ?>
</div>
<div class="col-1-3 text-right">
<?= $this->sortBox ?>
</div>
</div>
<div>
<?= $this->backendSelection; ?>
<?= $this->filterEditor; ?>
</div>
</div>
<?php endif ?>
<div class="content groups">
<div class="content">
<?php
if (! isset($backend)) {
echo $this->translate('No backend found which is able to list groups') . '</div>';
echo $this->translate('No backend found which is able to list user groups') . '</div>';
return;
} else {
$extensible = $this->hasPermission('config/authentication/groups/add') && $backend instanceof Extensible;
@ -37,64 +31,68 @@ if (! isset($backend)) {
<?php if ($extensible): ?>
<?= $this->qlink(
$this->translate('Add a New User Group') ,
$this->translate('Add a New User Group'),
'group/add',
array('backend' => $backend->getName()),
array(
'class' => 'button-link',
'data-base-target' => '_next',
'icon' => 'plus',
'title' => $this->translate('Create a new user group')
'icon' => 'plus'
)
) ?>
<?php endif ?>
<?php
// @TODO(el): Remove $firstRow stuff
$firstRow = true;
foreach ($groups as $group): ?>
<?php if ($firstRow): ?>
<?php $firstRow = false; ?>
<table data-base-target="_next" class="action-table listing-table">
<thead>
<tr>
<th class="group-name"><?= $this->translate('Group'); ?></th>
<?php if ($reducible): ?>
<th class="group-remove"><?= $this->translate('Remove'); ?></th>
<?php endif ?>
</tr>
</thead>
<tbody>
<?php endif ?>
<tr>
<td class="group-name"><?= $this->qlink($group->group_name, 'group/show', array(
'backend' => $backend->getName(),
'group' => $group->group_name
), array(
'title' => sprintf($this->translate('Show detailed information for group %s'), $group->group_name)
)); ?></td>
<?php if ($reducible): ?>
<td class="group-remove">
<?= $this->qlink(
null,
'group/remove',
array(
'backend' => $backend->getName(),
'group' => $group->group_name
),
array(
'title' => sprintf($this->translate('Remove group %s'), $group->group_name),
'icon' => 'trash'
)
); ?>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
<?php if ($groups->hasResult()): ?>
</tbody>
</table>
<?php else: ?>
<p><?= $this->translate('No groups found matching the filter'); ?></p>
<?php endif ?>
<?php if (! $groups->hasResult()): ?>
<p><?= $this->translate('No user groups found matching the filter'); ?></p>
</div>
<?php endif ?>
<table data-base-target="_next" class="action-table listing-table">
<thead>
<tr>
<th><?= $this->translate('User Group'); ?></th>
<?php if ($reducible): ?>
<th><?= $this->translate('Remove'); ?></th>
<?php endif ?>
</tr>
</thead>
<tbody>
<?php foreach ($groups as $group): ?>
<tr>
<td>
<?= $this->qlink(
$group->group_name,
'group/show',
array(
'backend' => $backend->getName(),
'group' => $group->group_name
),
array(
'title' => sprintf(
$this->translate('Show detailed information for user group %s'),
$group->group_name
)
)
); ?>
</td>
<?php if ($reducible): ?>
<td class="icon-col">
<?= $this->qlink(
null,
'group/remove',
array(
'backend' => $backend->getName(),
'group' => $group->group_name
),
array(
'title' => sprintf($this->translate('Remove user group %s'), $group->group_name),
'icon' => 'trash'
)
); ?>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>

View File

@ -2,7 +2,6 @@
use Icinga\Data\Extensible;
use Icinga\Data\Updatable;
use Icinga\Data\Selectable;
$extensible = $this->hasPermission('config/authentication/groups/add') && $backend instanceof Extensible;
@ -24,92 +23,86 @@ if ($this->hasPermission('config/authentication/groups/edit') && $backend instan
}
?>
<div class="controls">
<?php if (! $this->compact): ?>
<?= $tabs; ?>
<?php endif ?>
<h2 class="clearfix"><?= $this->escape($group->group_name) ?><span class="pull-right"><?= $editLink ?></span></h2>
<table class="name-value-table">
<tr>
<th><?= $this->translate('Created at'); ?></th>
<td><?= $group->created_at === null ? '-' : $this->formatDateTime($group->created_at); ?></td>
</tr>
<tr>
<th><?= $this->translate('Last modified'); ?></th>
<td><?= $group->last_modified === null ? '-' : $this->formatDateTime($group->last_modified); ?></td>
</tr>
</table>
<h2><?= $this->translate('Members'); ?></h2>
<div class="controls separated dont-print">
<?php if (! $this->compact): ?>
<?= $this->sortBox; ?>
<?= $tabs; ?>
<?php endif ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<h2 class="clearfix"><?= $this->escape($group->group_name) ?><span class="pull-right"><?= $editLink ?></span></h2>
<table class="name-value-table">
<tr>
<th><?= $this->translate('Created at'); ?></th>
<td><?= $group->created_at === null ? '-' : $this->formatDateTime($group->created_at); ?></td>
</tr>
<tr>
<th><?= $this->translate('Last modified'); ?></th>
<td><?= $group->last_modified === null ? '-' : $this->formatDateTime($group->last_modified); ?></td>
</tr>
</table>
<?php if (! $this->compact): ?>
<?= $this->filterEditor; ?>
<h2><?= $this->translate('Members'); ?></h2>
<div class="grid">
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<?= $this->sortBox; ?>
</div>
<?= $this->filterEditor; ?>
<?php endif ?>
</div>
<div class="content members" data-base-target="_next">
<div class="content">
<?php if ($extensible): ?>
<?= $this->qlink(
$this->translate('Add User to Group') ,
$this->translate('Add New Member'),
'group/addmember',
null,
array(
'class' => 'button-link',
'data-base-target' => '_next',
'icon' => 'plus',
'title' => $this->translate('Add user to group')
'backend' => $backend->getName(),
'group' => $group->group_name
),
array(
'icon' => 'plus',
'class' => 'button-link'
)
) ?>
<?php endif ?>
<?php
$firstRow = true;
foreach ($members as $member): ?>
<?php if ($firstRow): ?>
<?php $firstRow = false; ?>
<table data-base-target="_next" class="action-table listing-table">
<thead>
<tr>
<th class="member-name"><?= $this->translate('Username'); ?></th>
<?php if (isset($removeForm)): ?>
<th class="member-remove"><?= $this->translate('Remove'); ?></th>
<?php endif ?>
</tr>
</thead>
<tbody>
<?php endif ?>
<tr>
<td class="member-name">
<?php if (
$this->hasPermission('config/authentication/users/show')
&& method_exists($backend, 'getUserBackend')
&& ($userBackend = $backend->getUserBackend()) !== null
&& $userBackend instanceof Selectable
): ?>
<?= $this->qlink($member->user_name, 'user/show', array(
'backend' => $userBackend->getName(),
'user' => $member->user_name
), array(
'title' => sprintf($this->translate('Show detailed information about %s'), $member->user_name)
)); ?>
<?php else: ?>
<?= $this->escape($member->user_name); ?>
<?php endif ?>
</td>
<?php if (isset($removeForm)): ?>
<td class="icon-col" data-base-target="_self">
<?php $removeForm->getElement('user_name')->setValue($member->user_name); echo $removeForm; ?>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
<?php if ($members->hasResult()): ?>
</tbody>
</table>
<?php else: ?>
<?php if (! $members->hasResult()): ?>
<p><?= $this->translate('No group member found matching the filter'); ?></p>
<?php endif ?>
</div>
<?php return; endif ?>
<table data-base-target="_next" class="action-table listing-table">
<thead>
<tr>
<th><?= $this->translate('Username'); ?></th>
<?php if (isset($removeForm)): ?>
<th><?= $this->translate('Remove'); ?></th>
<?php endif ?>
</tr>
</thead>
<tbody>
<?php foreach ($members as $member): ?>
<tr>
<td>
<?php if (
$this->hasPermission('config/authentication/users/show')
&& ($userBackend = $backend->getUserBackendName($member->user_name)) !== null
): ?>
<?= $this->qlink($member->user_name, 'user/show', array(
'backend' => $userBackend,
'user' => $member->user_name
), array(
'title' => sprintf($this->translate('Show detailed information about %s'), $member->user_name)
)); ?>
<?php else: ?>
<?= $this->escape($member->user_name); ?>
<?php endif ?>
</td>
<?php if (isset($removeForm)): ?>
<td class="icon-col" data-base-target="_self">
<?php $removeForm->getElement('user_name')->setValue($member->user_name); echo $removeForm; ?>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>

View File

@ -1,20 +1,15 @@
<?php
use Icinga\Data\Extensible;
use Icinga\Data\Reducible;
if (! $this->compact): ?>
<div class="controls">
<?= $this->tabs ?>
<div class="grid dont-print">
<div class="col-1-3 text-left">
<?= $this->limiter ?>
</div>
<div class="col-1-3">
<?= $this->paginator ?>
</div>
<div class="col-1-3 text-right">
<?= $this->sortBox ?>
</div>
<div class="controls separated dont-print">
<?= $tabs ?>
<div class="grid">
<?= $this->limiter ?>
<?= $this->paginator ?>
<?= $this->sortBox ?>
</div>
<div>
<?= $this->backendSelection ?>
@ -22,20 +17,18 @@ if (! $this->compact): ?>
</div>
</div>
<?php endif ?>
<div class="content">
<?php if (! isset($backend)): ?>
<p><?= $this->translate('No backend found which is able to list users.') ?></p>
</div>
<?php return; endif ?>
<?php if (! $users->hasResult()): ?>
<p><?= $this->translate('No users found matching the filter.') ?></p>
</div>
<?php return; endif ?>
<?php
$extensible = $this->hasPermission('config/authentication/users/add') && $backend instanceof Extensible;
$reducible = $this->hasPermission('config/authentication/users/remove') && $backend instanceof Reducible;
if (! isset($backend)) {
echo $this->translate('No backend found which is able to list users') . '</div>';
return;
} else {
$extensible = $this->hasPermission('config/authentication/users/add') && $backend instanceof Extensible;
$reducible = $this->hasPermission('config/authentication/users/remove') && $backend instanceof Reducible;
}
?>
<?php if ($extensible): ?>
<?= $this->qlink(
$this->translate('Add a New User') ,
@ -44,12 +37,17 @@ $reducible = $this->hasPermission('config/authentication/users/remove') && $back
array(
'class' => 'button-link',
'data-base-target' => '_next',
'icon' => 'plus',
'title' => $this->translate('Create a new user')
'icon' => 'plus'
)
) ?>
<?php endif ?>
<table class="action-table listing-table" data-base-target="_next">
<?php if (! $users->hasResult()): ?>
<p><?= $this->translate('No users found matching the filter') ?></p>
</div>
<?php return; endif ?>
<table data-base-target="_next" class="action-table listing-table">
<thead>
<tr>
<th><?= $this->translate('Username') ?></th>
@ -91,4 +89,4 @@ $reducible = $this->hasPermission('config/authentication/users/remove') && $back
<?php endforeach ?>
</tbody>
</table>
</div>
</div>

View File

@ -22,93 +22,90 @@ if ($this->hasPermission('config/authentication/users/edit') && $backend instanc
}
?>
<div class="controls">
<div class="controls separated dont-print">
<?php if (! $this->compact): ?>
<?= $tabs; ?>
<?= $tabs; ?>
<?php endif ?>
<h2 class="clearfix"><?= $this->escape($user->user_name) ?><span class="pull-right"><?= $editLink ?></span></h2>
<table class="name-value-table">
<tr>
<th><?= $this->translate('State'); ?></th>
<td><?= $user->is_active === null ? '-' : ($user->is_active ? $this->translate('Active') : $this->translate('Inactive')); ?></td>
</tr>
<tr>
<th><?= $this->translate('Created at'); ?></th>
<td><?= $user->created_at === null ? '-' : $this->formatDateTime($user->created_at); ?></td>
</tr>
<tr>
<th><?= $this->translate('Last modified'); ?></th>
<td><?= $user->last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?></td>
</tr>
</table>
<h2><?= $this->translate('Group Memberships'); ?></h2>
<h2 class="clearfix"><?= $this->escape($user->user_name) ?><span class="pull-right"><?= $editLink ?></span></h2>
<table class="name-value-table">
<tr>
<th><?= $this->translate('State'); ?></th>
<td><?= $user->is_active === null ? '-' : ($user->is_active ? $this->translate('Active') : $this->translate('Inactive')); ?></td>
</tr>
<tr>
<th><?= $this->translate('Created at'); ?></th>
<td><?= $user->created_at === null ? '-' : $this->formatDateTime($user->created_at); ?></td>
</tr>
<tr>
<th><?= $this->translate('Last modified'); ?></th>
<td><?= $user->last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?></td>
</tr>
</table>
<?php if (! $this->compact): ?>
<?= $this->sortBox; ?>
<?php endif ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<?php if (! $this->compact): ?>
<?= $this->filterEditor; ?>
<h2><?= $this->translate('Group Memberships'); ?></h2>
<div class="grid">
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<?= $this->sortBox; ?>
</div>
<?= $this->filterEditor; ?>
<?php endif ?>
</div>
<div class="content memberships">
<?php if ($showCreateMembershipLink): ?>
<div class="content">
<?php if ($showCreateMembershipLink): ?>
<?= $this->qlink(
$this->translate('Add User to Group') ,
$this->translate('Create New Membership'),
'user/createmembership',
array('backend' => $backend->getName()),
array(
'class' => 'button-link',
'data-base-target' => '_next',
'icon' => 'plus',
'title' => $this->translate('Add user to user group')
'backend' => $backend->getName(),
'user' => $user->user_name
),
array(
'icon' => 'plus',
'class' => 'button-link'
)
) ?>
<?php endif ?>
<?php
// @TODO(el): Remove that $firstRow thingy
$firstRow = true;
foreach ($memberships as $membership): ?>
<?php if ($firstRow): ?>
<?php $firstRow = false; ?>
<table data-base-target="_next" class="action-table listing-table">
<thead>
<tr>
<th class="membership-group"><?= $this->translate('Group'); ?></th>
<th class="membership-cancel"><?= $this->translate('Cancel', 'group.membership'); ?></th>
</tr>
</thead>
<tbody>
<?php endif ?>
<tr>
<td class="membership-group">
<?php if ($this->hasPermission('config/authentication/groups/show') && $membership->backend instanceof Selectable): ?>
<?= $this->qlink($membership->group_name, 'group/show', array(
'backend' => $membership->backend->getName(),
'group' => $membership->group_name
), array(
'title' => sprintf($this->translate('Show detailed information for group %s'), $membership->group_name)
)); ?>
<?php else: ?>
<?= $this->escape($membership->group_name); ?>
<?php endif ?>
</td>
<td class="membership-cancel" data-base-target="_self">
<?php if (isset($removeForm) && $membership->backend instanceof Reducible): ?>
<?= $removeForm->setAction($this->url('group/removemember', array(
'backend' => $membership->backend->getName(),
'group' => $membership->group_name
))); ?>
<?php else: ?>
-
<?php endif ?>
</td>
</tr>
<?php endforeach ?>
<?php if ($memberships->hasResult()): ?>
</tbody>
</table>
<?php else: ?>
<p><?= $this->translate('No memberships found matching the filter'); ?></p>
<?php endif ?>
<?php if (! $memberships->hasResult()): ?>
<p><?= $this->translate('No memberships found matching the filter'); ?></p>
</div>
<?php return; endif ?>
<table data-base-target="_next" class="action-table listing-table">
<thead>
<tr>
<th><?= $this->translate('Group'); ?></th>
<th><?= $this->translate('Cancel', 'group.membership'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($memberships as $membership): ?>
<tr>
<td>
<?php if ($this->hasPermission('config/authentication/groups/show') && $membership->backend instanceof Selectable): ?>
<?= $this->qlink($membership->group_name, 'group/show', array(
'backend' => $membership->backend->getName(),
'group' => $membership->group_name
), array(
'title' => sprintf($this->translate('Show detailed information for group %s'), $membership->group_name)
)); ?>
<?php else: ?>
<?= $this->escape($membership->group_name); ?>
<?php endif ?>
</td>
<td class="icon-col" data-base-target="_self">
<?php if (isset($removeForm) && $membership->backend instanceof Reducible): ?>
<?= $removeForm->setAction($this->url('group/removemember', array(
'backend' => $membership->backend->getName(),
'group' => $membership->group_name
))); ?>
<?php else: ?>
-
<?php endif ?>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
</div>

View File

@ -495,6 +495,6 @@ The first release candidate of Icinga Web 2 introduces the following non-backwar
the file remains unchanged.
* The location of a user's preferences has been changed from
**<config-dir>/preferences/<username>.ini** to
**<config-dir>/preferences/<username>/config.ini**.
**&lt;config-dir&gt;/preferences/&lt;username&gt;.ini** to
**&lt;config-dir&gt;/preferences/&lt;username&gt;/config.ini**.
The content of the file remains unchanged.

View File

@ -25,7 +25,7 @@ Packager: Icinga Team <info@icinga.org>
%if 0%{?suse_version}
%define wwwconfigdir %{_sysconfdir}/apache2/conf.d
%define wwwuser wwwrun
%define zend php5-ZendFramework
%define zend %{name}-vendor-Zend
%if 0%{?suse_version} == 1110
%define php php53
Requires: apache2-mod_php53
@ -83,7 +83,6 @@ Requires: %{php}-gd %{php}-intl
%{?rhel:Requires: php-pecl-imagick}
%{?suse_version:Requires: %{php}-gettext %{php}-json %{php}-openssl %{php}-posix}
Requires: %{zend}
Obsoletes: %{name}-vendor-zend
Requires: %{zend}-Db-Adapter-Pdo-Mysql
Requires: %{zend}-Db-Adapter-Pdo-Pgsql

View File

@ -136,6 +136,16 @@ class Logger
return $this;
}
/**
* Return the logging level being used
*
* @return int
*/
public function getLevel()
{
return $this->level;
}
/**
* Register the given message as config error
*

View File

@ -130,6 +130,7 @@ class DbUserBackend extends DbRepository implements UserBackendInterface, Inspec
*/
public function insert($table, array $bind)
{
$this->requireTable($table);
$bind['created_at'] = date('Y-m-d H:i:s');
$this->ds->insert(
$this->prependTablePrefix($table),
@ -150,6 +151,7 @@ class DbUserBackend extends DbRepository implements UserBackendInterface, Inspec
*/
public function update($table, array $bind, Filter $filter = null)
{
$this->requireTable($table);
$bind['last_modified'] = date('Y-m-d H:i:s');
if ($filter) {
$filter = $this->requireFilter($table, $filter);

View File

@ -12,7 +12,6 @@ use Icinga\Exception\ProgrammingError;
use Icinga\Repository\LdapRepository;
use Icinga\Repository\RepositoryQuery;
use Icinga\Protocol\Ldap\LdapException;
use Icinga\Protocol\Ldap\Expression;
use Icinga\User;
class LdapUserBackend extends LdapRepository implements UserBackendInterface, Inspectable
@ -102,15 +101,13 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
/**
* Set the objectClass where to look for users
*
* Sets also the base table name for the underlying repository.
*
* @param string $userClass
*
* @return $this
*/
public function setUserClass($userClass)
{
$this->baseTable = $this->userClass = $this->getNormedAttribute($userClass);
$this->userClass = $this->getNormedAttribute($userClass);
return $this;
}
@ -194,21 +191,21 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
}
/**
* Return a new query for the given columns
* Initialize this repository's virtual tables
*
* @param array $columns The desired columns, if null all columns will be queried
* @return array
*
* @return RepositoryQuery
* @throws ProgrammingError In case $this->userClass has not been set yet
*/
public function select(array $columns = null)
protected function initializeVirtualTables()
{
$query = parent::select($columns);
$query->getQuery()->setBase($this->baseDn);
if ($this->filter) {
$query->getQuery()->where(new Expression($this->filter));
if ($this->userClass === null) {
throw new ProgrammingError('It is required to set the object class where to find users first');
}
return $query;
return array(
'user' => $this->userClass
);
}
/**
@ -216,13 +213,10 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
*
* @return array
*
* @throws ProgrammingError In case either $this->userNameAttribute or $this->userClass has not been set yet
* @throws ProgrammingError In case $this->userNameAttribute has not been set yet
*/
protected function initializeQueryColumns()
{
if ($this->userClass === null) {
throw new ProgrammingError('It is required to set the objectClass where to look for users first');
}
if ($this->userNameAttribute === null) {
throw new ProgrammingError('It is required to set a attribute name where to find a user\'s name first');
}
@ -240,7 +234,7 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
}
return array(
$this->userClass => array(
'user' => array(
'user' => $this->userNameAttribute,
'user_name' => $this->userNameAttribute,
'is_active' => $isActiveAttribute,
@ -269,15 +263,9 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
* Initialize this repository's conversion rules
*
* @return array
*
* @throws ProgrammingError In case $this->userClass has not been set yet
*/
protected function initializeConversionRules()
{
if ($this->userClass === null) {
throw new ProgrammingError('It is required to set the objectClass where to look for users first');
}
if ($this->ds->getCapabilities()->isActiveDirectory()) {
$stateConverter = 'user_account_control';
} else {
@ -285,7 +273,7 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
}
return array(
$this->userClass => array(
'user' => array(
'is_active' => $stateConverter,
'created_at' => 'generalized_time',
'last_modified' => 'generalized_time'
@ -330,14 +318,46 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
}
/**
* @param Inspection $info Optional inspection to fill with diagnostic info
* Validate that the requested table exists
*
* @throws AuthenticationException When authentication is not possible
* @param string $table The table to validate
* @param RepositoryQuery $query An optional query to pass as context
*
* @return string
*
* @throws ProgrammingError In case the given table does not exist
*/
public function assertAuthenticationPossible(Inspection $insp = null)
public function requireTable($table, RepositoryQuery $query = null)
{
if ($query !== null) {
$query->getQuery()->setBase($this->baseDn);
if ($this->filter) {
$query->getQuery()->setNativeFilter($this->filter);
}
}
return parent::requireTable($table, $query);
}
/**
* Validate that the given column is a valid query target and return it or the actual name if it's an alias
*
* @param string $table The table where to look for the column or alias
* @param string $name The name or alias of the column to validate
* @param RepositoryQuery $query An optional query to pass as context
*
* @return string The given column's name
*
* @throws QueryException In case the given column is not a valid query column
*/
public function requireQueryColumn($table, $name, RepositoryQuery $query = null)
{
$column = parent::requireQueryColumn($table, $name, $query);
if ($name === 'user_name' && $query !== null) {
$query->getQuery()->setUnfoldAttribute('user_name');
}
return $column;
}
/**
@ -359,17 +379,16 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
->getQuery()
->setUsePagedResults(false)
->fetchDn();
if ($userDn === null) {
return false;
}
$testCredentialsResult = $this->ds->testCredentials($userDn, $password);
if ($testCredentialsResult) {
$validCredentials = $this->ds->testCredentials($userDn, $password);
if ($validCredentials) {
$user->setAdditional('ldap_dn', $userDn);
}
return $testCredentialsResult;
return $validCredentials;
} catch (LdapException $e) {
throw new AuthenticationException(
'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',

View File

@ -217,6 +217,18 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa
return $memberships;
}
/**
* Return the name of the backend that is providing the given user
*
* @param string $username Currently unused
*
* @return null|string The name of the backend or null in case this information is not available
*/
public function getUserBackendName($username)
{
return null; // TODO(10373): Store this to the database when inserting and fetch it here
}
/**
* Join group into group_membership
*
@ -238,7 +250,7 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa
*/
protected function joinGroupMembership(RepositoryQuery $query)
{
$query->getQuery()->join(
$query->getQuery()->joinLeft(
$this->requireTable('group_membership'),
'g.id = gm.group_id',
array()

View File

@ -5,10 +5,11 @@ namespace Icinga\Authentication\UserGroup;
use Icinga\Authentication\User\UserBackend;
use Icinga\Authentication\User\LdapUserBackend;
use Icinga\Application\Logger;
use Icinga\Data\ConfigObject;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\ProgrammingError;
use Icinga\Protocol\Ldap\Expression;
use Icinga\Protocol\Ldap\LdapException;
use Icinga\Repository\LdapRepository;
use Icinga\Repository\RepositoryQuery;
use Icinga\User;
@ -71,6 +72,13 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
*/
protected $groupMemberAttribute;
/**
* Whether the attribute name where to find a group's member holds ambiguous values
*
* @var bool
*/
protected $ambiguousMemberAttribute;
/**
* The custom LDAP filter to apply on a user query
*
@ -211,15 +219,13 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
/**
* Set the objectClass where to look for groups
*
* Sets also the base table name for the underlying repository.
*
* @param string $groupClass
*
* @return $this
*/
public function setGroupClass($groupClass)
{
$this->baseTable = $this->groupClass = $this->getNormedAttribute($groupClass);
$this->groupClass = $this->getNormedAttribute($groupClass);
return $this;
}
@ -359,22 +365,55 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
}
/**
* Return a new query for the given columns
* Return whether the attribute name where to find a group's member holds ambiguous values
*
* @param array $columns The desired columns, if null all columns will be queried
* @return bool
*
* @return RepositoryQuery
* @throws ProgrammingError In case either $this->groupClass or $this->groupMemberAttribute
* has not been set yet
*/
public function select(array $columns = null)
protected function isMemberAttributeAmbiguous()
{
$query = parent::select($columns);
$query->getQuery()->setBase($this->groupBaseDn);
if ($this->groupFilter) {
// TODO(jom): This should differentiate between groups and their memberships
$query->getQuery()->where(new Expression($this->groupFilter));
if ($this->ambiguousMemberAttribute === null) {
if ($this->groupClass === null) {
throw new ProgrammingError(
'It is required to set the objectClass where to look for groups first'
);
} elseif ($this->groupMemberAttribute === null) {
throw new ProgrammingError(
'It is required to set a attribute name where to find a group\'s members first'
);
}
$sampleValue = $this->ds
->select()
->from($this->groupClass, array($this->groupMemberAttribute))
->setUnfoldAttribute($this->groupMemberAttribute)
->setBase($this->groupBaseDn)
->fetchOne();
$this->ambiguousMemberAttribute = !$this->isRelatedDn($sampleValue);
}
return $query;
return $this->ambiguousMemberAttribute;
}
/**
* Initialize this repository's virtual tables
*
* @return array
*
* @throws ProgrammingError In case $this->groupClass has not been set yet
*/
protected function initializeVirtualTables()
{
if ($this->groupClass === null) {
throw new ProgrammingError('It is required to set the object class where to find groups first');
}
return array(
'group' => $this->groupClass,
'group_membership' => $this->groupClass
);
}
/**
@ -382,16 +421,17 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
*
* @return array
*
* @throws ProgrammingError In case either $this->groupNameAttribute or $this->groupClass has not been set yet
* @throws ProgrammingError In case either $this->groupNameAttribute or
* $this->groupMemberAttribute has not been set yet
*/
protected function initializeQueryColumns()
{
if ($this->groupClass === null) {
throw new ProgrammingError('It is required to set the objectClass where to look for groups first');
}
if ($this->groupNameAttribute === null) {
throw new ProgrammingError('It is required to set a attribute name where to find a group\'s name first');
}
if ($this->groupMemberAttribute === null) {
throw new ProgrammingError('It is required to set a attribute name where to find a group\'s members first');
}
if ($this->ds->getCapabilities()->isActiveDirectory()) {
$createdAtAttribute = 'whenCreated';
@ -409,7 +449,7 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
'created_at' => $createdAtAttribute,
'last_modified' => $lastModifiedAttribute
);
return array($this->groupClass => $columns, $this->groupClass => $columns);
return array('group' => $columns, 'group_membership' => $columns);
}
/**
@ -420,7 +460,7 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
protected function initializeFilterColumns()
{
return array(
t('Username') => 'user',
t('Username') => 'user_name',
t('User Group') => 'group_name',
t('Created At') => 'created_at',
t('Last Modified') => 'last_modified'
@ -431,31 +471,68 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
* Initialize this repository's conversion rules
*
* @return array
*
* @throws ProgrammingError In case $this->groupClass has not been set yet
*/
protected function initializeConversionRules()
{
if ($this->groupClass === null) {
throw new ProgrammingError('It is required to set the objectClass where to look for groups first');
}
if ($this->groupMemberAttribute === null) {
throw new ProgrammingError('It is required to set a attribute name where to find a group\'s members first');
}
$rules = array(
$this->groupClass => array(
'group' => array(
'created_at' => 'generalized_time',
'last_modified' => 'generalized_time'
),
'group_membership' => array(
'created_at' => 'generalized_time',
'last_modified' => 'generalized_time'
)
);
if (! $this->isAmbiguous($this->groupClass, $this->groupMemberAttribute)) {
$rules[$this->groupClass][] = 'user_name';
if (! $this->isMemberAttributeAmbiguous()) {
$rules['group_membership']['user_name'] = 'user_name';
$rules['group_membership']['user'] = 'user_name';
$rules['group']['user_name'] = 'user_name';
$rules['group']['user'] = 'user_name';
}
return $rules;
}
/**
* Return the distinguished name for the given uid or gid
*
* @param string $name
*
* @return string
*/
protected function persistUserName($name)
{
try {
$userDn = $this->ds
->select()
->from($this->userClass, array())
->where($this->userNameAttribute, $name)
->setBase($this->userBaseDn)
->setUsePagedResults(false)
->fetchDn();
if ($userDn) {
return $userDn;
}
$groupDn = $this->ds
->select()
->from($this->groupClass, array())
->where($this->groupNameAttribute, $name)
->setBase($this->groupBaseDn)
->setUsePagedResults(false)
->fetchDn();
if ($groupDn) {
return $groupDn;
}
} catch (LdapException $_) {
// pass
}
Logger::debug('Unable to persist uid or gid "%s" in repository "%s". No DN found.', $name, $this->getName());
return $name;
}
/**
* Return the uid for the given distinguished name
*
@ -475,11 +552,8 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
/**
* Validate that the requested table exists
*
* This will return $this->groupClass in case $table equals "group" or "group_membership".
*
* @param string $table The table to validate
* @param RepositoryQuery $query An optional query to pass as context
* (unused by the base implementation)
*
* @return string
*
@ -487,12 +561,14 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
*/
public function requireTable($table, RepositoryQuery $query = null)
{
$table = parent::requireTable($table, $query);
if ($table === 'group' || $table === 'group_membership') {
$table = $this->groupClass;
if ($query !== null) {
$query->getQuery()->setBase($this->groupBaseDn);
if ($table === 'group' && $this->groupFilter) {
$query->getQuery()->setNativeFilter($this->groupFilter);
}
}
return $table;
return parent::requireTable($table, $query);
}
/**
@ -525,7 +601,7 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
*/
public function getMemberships(User $user)
{
if ($this->isAmbiguous($this->groupClass, $this->groupMemberAttribute)) {
if ($this->isMemberAttributeAmbiguous()) {
$queryValue = $user->getUsername();
} elseif (($queryValue = $user->getAdditional('ldap_dn')) === null) {
$userQuery = $this->ds
@ -535,7 +611,7 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
->setBase($this->userBaseDn)
->setUsePagedResults(false);
if ($this->userFilter) {
$userQuery->where(new Expression($this->userFilter));
$userQuery->setNativeFilter($this->userFilter);
}
if (($queryValue = $userQuery->fetchDn()) === null) {
@ -549,7 +625,7 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
->where($this->groupMemberAttribute, $queryValue)
->setBase($this->groupBaseDn);
if ($this->groupFilter) {
$groupQuery->where(new Expression($this->groupFilter));
$groupQuery->setNativeFilter($this->groupFilter);
}
$groups = array();
@ -560,6 +636,21 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
return $groups;
}
/**
* Return the name of the backend that is providing the given user
*
* @param string $username Unused
*
* @return null|string The name of the backend or null in case this information is not available
*/
public function getUserBackendName($username)
{
$userBackend = $this->getUserBackend();
if ($userBackend !== null) {
return $userBackend->getName();
}
}
/**
* Apply the given configuration on this backend
*
@ -607,7 +698,7 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
return $this
->setGroupBaseDn($config->base_dn)
->setUserBaseDn($config->get('user_base_dn', $this->getGroupBaseDn()))
->setUserBaseDn($config->get('user_base_dn', $defaults->get('user_base_dn', $this->getGroupBaseDn())))
->setGroupClass($config->get('group_class', $defaults->group_class))
->setUserClass($config->get('user_class', $defaults->user_class))
->setGroupNameAttribute($config->get('group_name_attribute', $defaults->group_name_attribute))

View File

@ -34,4 +34,13 @@ interface UserGroupBackendInterface
* @return array
*/
public function getMemberships(User $user);
/**
* Return the name of the backend that is providing the given user
*
* @param string $username
*
* @return null|string The name of the backend or null in case this information is not available
*/
public function getUserBackendName($username);
}

View File

@ -28,9 +28,9 @@ abstract class Command
protected $commandName;
protected $actionName;
private $config;
protected $config;
private $configs;
protected $configs;
protected $defaultActionName = 'default';
@ -127,7 +127,7 @@ abstract class Command
public function fail($msg)
{
throw new IcingaException($msg);
throw new IcingaException('%s', $msg);
}
public function getDefaultActionName()

View File

@ -130,6 +130,7 @@ class DbConnection implements Selectable, Extensible, Updatable, Reducible, Insp
'username' => $this->config->username,
'password' => $this->config->password,
'dbname' => $this->config->dbname,
'charset' => $this->config->charset,
'persistent' => (bool) $this->config->get('persistent', false),
'options' => & $genericAdapterOptions,
'driver_options' => & $driverOptions

View File

@ -288,9 +288,14 @@ class DbQuery extends SimpleQuery
$expression = $this->valueToTimestamp($expression);
}
if (is_array($expression) && $sign === '=') {
// TODO: Should we support this? Doesn't work for blub*
return $col . ' IN (' . $this->escapeForSql($expression) . ')';
if (is_array($expression)) {
if ($sign === '=') {
return $col . ' IN (' . $this->escapeForSql($expression) . ')';
} elseif ($sign === '!=') {
return $col . ' NOT IN (' . $this->escapeForSql($expression) . ')';
}
throw new QueryException('Unable to render array expressions with operators other than equal or not equal');
} elseif ($sign === '=' && strpos($expression, '*') !== false) {
if ($expression === '*') {
// We'll ignore such filters as it prevents index usage and because "*" means anything, anything means

View File

@ -125,17 +125,26 @@ abstract class FilterChain extends Filter
return $this;
}
public function listFilteredColumns()
/**
* List and return all column names referenced in this filter
*
* @param array $columns The columns listed so far
*
* @return array
*/
public function listFilteredColumns(array $columns = array())
{
$columns = array();
foreach ($this->filters as $filter) {
if ($filter instanceof FilterExpression) {
$columns[] = $filter->getColumn();
$column= $filter->getColumn();
if (! in_array($column, $columns, true)) {
$columns[] = $column;
}
} else {
$columns += $filter->listFilteredColumns();
$columns = $filter->listFilteredColumns($columns);
}
}
// array_unique?
return $columns;
}

View File

@ -1,30 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Protocol\Ldap;
class Expression
{
protected $value;
public function __construct($value)
{
$this->value = $value;
}
public function setValue($value)
{
$this->value = $value;
return $this;
}
public function getValue()
{
return $this->value;
}
public function __toString()
{
return (string) $this->getValue();
}
}

View File

@ -308,12 +308,8 @@ class LdapCapabilities
ldap_error($ds)
);
}
$cap = new LdapCapabilities(
$connection->cleanupAttributes(
ldap_get_attributes($ds, $entry),
array_flip($fields)
)
);
$cap = new LdapCapabilities($connection->cleanupAttributes(ldap_get_attributes($ds, $entry), $fields));
return $cap;
}

View File

@ -7,13 +7,14 @@ use Exception;
use ArrayIterator;
use Icinga\Application\Config;
use Icinga\Application\Logger;
use Icinga\Application\Platform;
use Icinga\Data\ConfigObject;
use Icinga\Data\Inspectable;
use Icinga\Data\Inspection;
use Icinga\Data\Selectable;
use Icinga\Data\Sortable;
use Icinga\Exception\InspectionException;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Filter\FilterChain;
use Icinga\Data\Filter\FilterExpression;
use Icinga\Exception\ProgrammingError;
use Icinga\Protocol\Ldap\LdapException;
@ -377,14 +378,7 @@ class LdapConnection implements Selectable, Inspectable
}
$ds = $this->getConnection();
$results = @ldap_search(
$ds,
$query->getBase() ?: $this->getDn(),
(string) $query,
array('dn'),
0,
0
);
$results = $this->ldapSearch($query, array('dn'));
if ($results === false) {
if (ldap_errno($ds) !== self::LDAP_NO_SUCH_OBJECT) {
@ -482,8 +476,28 @@ class LdapConnection implements Selectable, Inspectable
*/
public function fetchOne(LdapQuery $query, array $fields = null)
{
$row = (array) $this->fetchRow($query, $fields);
return array_shift($row) ?: false;
$row = $this->fetchRow($query, $fields);
if ($row === false) {
return false;
}
$values = get_object_vars($row);
if (empty($values)) {
return false;
}
if ($fields === null) {
// Fetch the desired columns from the query if not explicitly overriden in the method's parameter
$fields = $query->getColumns();
}
if (empty($fields)) {
// The desired columns may be empty independently whether provided by the query or the method's parameter
return array_shift($values);
}
$alias = key($fields);
return $values[is_string($alias) ? $alias : $fields[$alias]];
}
/**
@ -694,19 +708,27 @@ class LdapConnection implements Selectable, Inspectable
));
} else {
foreach ($query->getOrder() as $rule) {
if (! in_array($rule[0], $fields)) {
if (! in_array($rule[0], $fields, true)) {
$fields[] = $rule[0];
}
}
}
}
$results = @ldap_search(
$ds,
$query->getBase() ?: $this->rootDn,
(string) $query,
$unfoldAttribute = $query->getUnfoldAttribute();
if ($unfoldAttribute) {
foreach ($query->getFilter()->listFilteredColumns() as $filterColumn) {
$fieldKey = array_search($filterColumn, $fields, true);
if ($fieldKey === false || is_string($fieldKey)) {
$fields[] = $filterColumn;
}
}
}
$results = $this->ldapSearch(
$query,
array_values($fields),
0, // Attributes and values
0,
$serverSorting && $limit ? $offset + $limit : 0
);
if ($results === false) {
@ -727,25 +749,21 @@ class LdapConnection implements Selectable, Inspectable
$count = 0;
$entries = array();
$entry = ldap_first_entry($ds, $results);
$unfoldAttribute = $query->getUnfoldAttribute();
do {
if ($unfoldAttribute) {
$rows = $this->cleanupAttributes(
ldap_get_attributes($ds, $entry),
array_flip($fields),
$unfoldAttribute
);
$rows = $this->cleanupAttributes(ldap_get_attributes($ds, $entry), $fields, $unfoldAttribute);
if (is_array($rows)) {
// TODO: Register the DN the same way as a section name in the ArrayDatasource!
foreach ($rows as $row) {
$count += 1;
if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[] = $row;
}
if ($query->getFilter()->matches($row)) {
$count += 1;
if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[] = $row;
}
if ($serverSorting && $limit > 0 && $limit === count($entries)) {
break;
if ($serverSorting && $limit > 0 && $limit === count($entries)) {
break;
}
}
}
} else {
@ -759,7 +777,7 @@ class LdapConnection implements Selectable, Inspectable
if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes(
ldap_get_attributes($ds, $entry),
array_flip($fields)
$fields
);
}
}
@ -799,8 +817,6 @@ class LdapConnection implements Selectable, Inspectable
$limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() : 0;
$queryString = (string) $query;
$base = $query->getBase() ?: $this->rootDn;
if ($fields === null) {
$fields = $query->getColumns();
@ -811,16 +827,25 @@ class LdapConnection implements Selectable, Inspectable
$serverSorting = false;//$this->getCapabilities()->hasOid(LdapCapabilities::LDAP_SERVER_SORT_OID);
if (! $serverSorting && $query->hasOrder()) {
foreach ($query->getOrder() as $rule) {
if (! in_array($rule[0], $fields)) {
if (! in_array($rule[0], $fields, true)) {
$fields[] = $rule[0];
}
}
}
$unfoldAttribute = $query->getUnfoldAttribute();
if ($unfoldAttribute) {
foreach ($query->getFilter()->listFilteredColumns() as $filterColumn) {
$fieldKey = array_search($filterColumn, $fields, true);
if ($fieldKey === false || is_string($fieldKey)) {
$fields[] = $filterColumn;
}
}
}
$count = 0;
$cookie = '';
$entries = array();
$unfoldAttribute = $query->getUnfoldAttribute();
do {
// Do not request the pagination control as a critical extension, as we want the
// server to return results even if the paged search request cannot be satisfied
@ -835,12 +860,10 @@ class LdapConnection implements Selectable, Inspectable
));
}
$results = @ldap_search(
$ds,
$base,
$queryString,
$results = $this->ldapSearch(
$query,
array_values($fields),
0, // Attributes and values
0,
$serverSorting && $limit ? $offset + $limit : 0
);
if ($results === false) {
@ -850,14 +873,15 @@ class LdapConnection implements Selectable, Inspectable
throw new LdapException(
'LDAP query "%s" (base %s) failed. Error: %s',
$queryString,
$base,
(string) $query,
$query->getBase() ?: $this->getDn(),
ldap_error($ds)
);
} elseif (ldap_count_entries($ds, $results) === 0) {
if (in_array(
ldap_errno($ds),
array(static::LDAP_SIZELIMIT_EXCEEDED, static::LDAP_ADMINLIMIT_EXCEEDED)
array(static::LDAP_SIZELIMIT_EXCEEDED, static::LDAP_ADMINLIMIT_EXCEEDED),
true
)) {
Logger::warning(
'Unable to request more than %u results. Does the server allow paged search requests? (%s)',
@ -872,22 +896,19 @@ class LdapConnection implements Selectable, Inspectable
$entry = ldap_first_entry($ds, $results);
do {
if ($unfoldAttribute) {
$rows = $this->cleanupAttributes(
ldap_get_attributes($ds, $entry),
array_flip($fields),
$unfoldAttribute
);
$rows = $this->cleanupAttributes(ldap_get_attributes($ds, $entry), $fields, $unfoldAttribute);
if (is_array($rows)) {
// TODO: Register the DN the same way as a section name in the ArrayDatasource!
foreach ($rows as $row) {
$count += 1;
if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[] = $row;
}
if ($query->getFilter()->matches($row)) {
$count += 1;
if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[] = $row;
}
if ($serverSorting && $limit > 0 && $limit === count($entries)) {
break;
if ($serverSorting && $limit > 0 && $limit === count($entries)) {
break;
}
}
}
} else {
@ -901,7 +922,7 @@ class LdapConnection implements Selectable, Inspectable
if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes(
ldap_get_attributes($ds, $entry),
array_flip($fields)
$fields
);
}
}
@ -932,7 +953,8 @@ class LdapConnection implements Selectable, Inspectable
// 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($ds, 0, false, $cookie);
ldap_search($ds, $base, $queryString); // Returns no entries, due to the page size
// Returns no entries, due to the page size
ldap_search($ds, $query->getBase() ?: $this->getDn(), (string) $query);
}
if (! $serverSorting && $query->hasOrder()) {
@ -963,8 +985,17 @@ class LdapConnection implements Selectable, Inspectable
// necessary to create another array to map attributes case insensitively to their requested counterparts.
// This does also apply the virtual alias handling. (Since an LDAP server does not handle such)
$loweredFieldMap = array();
foreach ($requestedFields as $name => $alias) {
$loweredFieldMap[strtolower($name)] = is_string($alias) ? $alias : $name;
foreach ($requestedFields as $alias => $name) {
$loweredName = strtolower($name);
if (isset($loweredFieldMap[$loweredName])) {
if (! is_array($loweredFieldMap[$loweredName])) {
$loweredFieldMap[$loweredName] = array($loweredFieldMap[$loweredName]);
}
$loweredFieldMap[$loweredName][] = is_string($alias) ? $alias : $name;
} else {
$loweredFieldMap[$loweredName] = is_string($alias) ? $alias : $name;
}
}
$cleanedAttributes = array();
@ -982,12 +1013,18 @@ class LdapConnection implements Selectable, Inspectable
$requestedAttributeName = isset($loweredFieldMap[strtolower($attribute_name)])
? $loweredFieldMap[strtolower($attribute_name)]
: $attribute_name;
$cleanedAttributes[$requestedAttributeName] = $attribute_value;
if (is_array($requestedAttributeName)) {
foreach ($requestedAttributeName as $requestedName) {
$cleanedAttributes[$requestedName] = $attribute_value;
}
} else {
$cleanedAttributes[$requestedAttributeName] = $attribute_value;
}
}
// The result may not contain all requested fields, so populate the cleaned
// result with the missing fields and their value being set to null
foreach ($requestedFields as $name => $alias) {
foreach ($requestedFields as $alias => $name) {
if (! is_string($alias)) {
$alias = $name;
}
@ -1003,6 +1040,14 @@ class LdapConnection implements Selectable, Inspectable
&& isset($cleanedAttributes[$unfoldAttribute])
&& is_array($cleanedAttributes[$unfoldAttribute])
) {
$siblings = array();
foreach ($loweredFieldMap as $loweredName => $requestedNames) {
if (is_array($requestedNames) && in_array($unfoldAttribute, $requestedNames, true)) {
$siblings = array_diff($requestedNames, array($unfoldAttribute));
break;
}
}
$values = $cleanedAttributes[$unfoldAttribute];
unset($cleanedAttributes[$unfoldAttribute]);
$baseRow = (object) $cleanedAttributes;
@ -1010,6 +1055,10 @@ class LdapConnection implements Selectable, Inspectable
foreach ($values as $value) {
$row = clone $baseRow;
$row->{$unfoldAttribute} = $value;
foreach ($siblings as $sibling) {
$row->{$sibling} = $value;
}
$rows[] = $row;
}
@ -1119,6 +1168,77 @@ class LdapConnection implements Selectable, Inspectable
return $ds;
}
/**
* Perform a LDAP search and return the result
*
* @param LdapQuery $query
* @param array $attributes An array of the required attributes
* @param int $attrsonly Should be set to 1 if only attribute types are wanted
* @param int $sizelimit Enables you to limit the count of entries fetched
* @param int $timelimit Sets the number of seconds how long is spend on the search
* @param int $deref
*
* @return resource|bool A search result identifier or false on error
*/
public function ldapSearch(
LdapQuery $query,
array $attributes = null,
$attrsonly = 0,
$sizelimit = 0,
$timelimit = 0,
$deref = LDAP_DEREF_NEVER
) {
$queryString = (string) $query;
$baseDn = $query->getBase() ?: $this->getDn();
if (Logger::getInstance()->getLevel() === Logger::DEBUG) {
// We're checking the level by ourself to avoid rendering the ldapsearch commandline for nothing
$starttlsParam = $this->encryption === static::STARTTLS ? ' -ZZ' : '';
$ldapUrl = ($this->encryption === static::LDAPS ? 'ldaps://' : 'ldap://')
. $this->hostname
. ($this->port ? ':' . $this->port : '');
if ($this->bound) {
$bindParams = ' -D "' . $this->bindDn . '"' . ($this->bindPw ? ' -w "' . $this->bindPw . '"' : '');
}
if ($deref === LDAP_DEREF_NEVER) {
$derefName = 'never';
} elseif ($deref === LDAP_DEREF_ALWAYS) {
$derefName = 'always';
} elseif ($deref === LDAP_DEREF_SEARCHING) {
$derefName = 'search';
} else { // $deref === LDAP_DEREF_FINDING
$derefName = 'find';
}
Logger::debug("Issueing LDAP search. Use '%s' to reproduce.", sprintf(
'ldapsearch -P 3%s -H "%s"%s -b "%s" -s "sub" -z %u -l %u -a "%s"%s%s%s',
$starttlsParam,
$ldapUrl,
$bindParams,
$baseDn,
$sizelimit,
$timelimit,
$derefName,
$attrsonly ? ' -A' : '',
$queryString ? ' "' . $queryString . '"' : '',
$attributes ? ' "' . join('" "', $attributes) . '"' : ''
));
}
return @ldap_search(
$this->getConnection(),
$baseDn,
$queryString,
$attributes,
$attrsonly,
$sizelimit,
$timelimit,
$deref
);
}
/**
* Create an LDAP entry
*
@ -1184,6 +1304,93 @@ class LdapConnection implements Selectable, Inspectable
return $dir;
}
/**
* Render and return a valid LDAP filter representation of the given filter
*
* @param Filter $filter
* @param int $level
*
* @return string
*/
public function renderFilter(Filter $filter, $level = 0)
{
if ($filter->isExpression()) {
/** @var $filter FilterExpression */
return $this->renderFilterExpression($filter);
}
/** @var $filter FilterChain */
$parts = array();
foreach ($filter->filters() as $filterPart) {
$part = $this->renderFilter($filterPart, $level + 1);
if ($part) {
$parts[] = $part;
}
}
if (empty($parts)) {
return '';
}
$format = '%1$s(%2$s)';
if (count($parts) === 1) {
$format = '%2$s';
}
if ($level === 0) {
$format = '(' . $format . ')';
}
return sprintf($format, $filter->getOperatorSymbol(), implode(')(', $parts));
}
/**
* Render and return a valid LDAP filter expression of the given filter
*
* @param FilterExpression $filter
*
* @return string
*/
protected function renderFilterExpression(FilterExpression $filter)
{
$column = $filter->getColumn();
$sign = $filter->getSign();
$expression = $filter->getExpression();
$format = '%1$s%2$s%3$s';
if ($expression === null || $expression === true) {
$expression = '*';
} elseif (is_array($expression)) {
$seqFormat = '|(%s)';
if ($sign === '!=') {
$seqFormat = '!(' . $seqFormat . ')';
$sign = '=';
}
$seqParts = array();
foreach ($expression as $expressionValue) {
$seqParts[] = sprintf(
$format,
LdapUtils::quoteForSearch($column),
$sign,
LdapUtils::quoteForSearch($expressionValue, true)
);
}
return sprintf($seqFormat, implode(')(', $seqParts));
}
if ($sign === '!=') {
$format = '!(%1$s=%3$s)';
}
return sprintf(
$format,
LdapUtils::quoteForSearch($column),
$sign,
LdapUtils::quoteForSearch($expression, true)
);
}
/**
* Inspect if this LDAP Connection is working as expected
*

View File

@ -4,23 +4,12 @@
namespace Icinga\Protocol\Ldap;
use Icinga\Data\SimpleQuery;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\NotImplementedError;
/**
* LDAP query class
*/
class LdapQuery extends SimpleQuery
{
/**
* 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;
/**
* The base dn being used for this query
*
@ -42,12 +31,18 @@ class LdapQuery extends SimpleQuery
*/
protected $unfoldAttribute;
/**
* This query's native LDAP filter
*
* @var string
*/
protected $nativeFilter;
/**
* Initialize this query
*/
protected function init()
{
$this->filters = array();
$this->usePagedResults = false;
}
@ -120,6 +115,29 @@ class LdapQuery extends SimpleQuery
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
*
@ -127,53 +145,10 @@ class LdapQuery extends SimpleQuery
*/
public function from($target, array $fields = null)
{
$this->filters['objectClass'] = $target;
$this->where('objectClass', $target);
return parent::from($target, $fields);
}
/**
* Add a new filter to the query
*
* @param string $condition Column to search in
* @param mixed $value Value to look for (asterisk wildcards are allowed)
*
* @return $this
*/
public function where($condition, $value = null)
{
// TODO: Adjust this once support for Icinga\Data\Filter is available
if ($condition instanceof Expression) {
$this->filters[] = $condition;
} else {
$this->filters[$condition] = $value;
}
return $this;
}
public function getFilter()
{
throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
}
public function addFilter(Filter $filter)
{
// TODO: This should be considered a quick fix only.
// Drop this entirely once support for Icinga\Data\Filter is available
if ($filter->isExpression()) {
$this->where($filter->getColumn(), $filter->getExpression());
} elseif ($filter->isChain()) {
foreach ($filter->filters() as $chainOrExpression) {
$this->addFilter($chainOrExpression);
}
}
}
public function setFilter(Filter $filter)
{
throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
}
/**
* Fetch result as tree
*
@ -225,36 +200,18 @@ class LdapQuery extends SimpleQuery
}
/**
* Return the LDAP filter to be applied on this query
* Render and return this query's filter
*
* @return string
*
* @throws LdapException In case the objectClass filter does not exist
*/
protected function renderFilter()
public function renderFilter()
{
if (! isset($this->filters['objectClass'])) {
throw new LdapException('Object class is mandatory');
$filter = $this->ds->renderFilter($this->filter);
if ($this->nativeFilter) {
$filter = '(&(' . $this->nativeFilter . ')' . $filter . ')';
}
$parts = array();
foreach ($this->filters as $key => $value) {
if ($value instanceof Expression) {
$parts[] = (string) $value;
} else {
$parts[] = sprintf(
'%s=%s',
LdapUtils::quoteForSearch($key),
LdapUtils::quoteForSearch($value, true)
);
}
}
if (count($parts) > 1) {
return '(&(' . implode(')(', $parts) . '))';
} else {
return '(' . $parts[0] . ')';
}
return $filter;
}
/**

View File

@ -6,6 +6,7 @@ namespace Icinga\Repository;
use Icinga\Data\Db\DbConnection;
use Icinga\Data\Extensible;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Filter\FilterExpression;
use Icinga\Data\Reducible;
use Icinga\Data\Updatable;
use Icinga\Exception\IcingaException;
@ -93,20 +94,20 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
protected $statementColumnAliasMap;
/**
* List of columns where the COLLATE SQL-instruction has been removed
* List of column names or aliases mapped to their table where the COLLATE SQL-instruction has been removed
*
* This list is being populated in case of a PostgreSQL backend only,
* to ensure case-insensitive string comparison in WHERE clauses.
*
* @var array
*/
protected $columnsWithoutCollation;
protected $caseInsensitiveColumns;
/**
* Create a new DB repository object
*
* In case $this->queryColumns has already been initialized, this initializes
* $this->columnsWithoutCollation in case of a PostgreSQL connection.
* $this->caseInsensitiveColumns in case of a PostgreSQL connection.
*
* @param DbConnection $ds The datasource to use
*/
@ -114,7 +115,6 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
{
parent::__construct($ds);
$this->columnsWithoutCollation = array();
if ($ds->getDbType() === 'pgsql' && $this->queryColumns !== null) {
$this->queryColumns = $this->removeCollateInstruction($this->queryColumns);
}
@ -123,7 +123,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
/**
* Return the query columns being provided
*
* Initializes $this->columnsWithoutCollation in case of a PostgreSQL connection.
* Initializes $this->caseInsensitiveColumns in case of a PostgreSQL connection.
*
* @return array
*/
@ -174,11 +174,12 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
*/
protected function removeCollateInstruction($queryColumns)
{
foreach ($queryColumns as & $columns) {
foreach ($columns as & $column) {
foreach ($queryColumns as $table => & $columns) {
foreach ($columns as $alias => & $column) {
// Using a regex here because COLLATE may occur anywhere in the string
$column = preg_replace('/ COLLATE .+$/', '', $column, -1, $count);
if ($count > 0) {
$this->columnsWithoutCollation[] = $column;
$this->caseInsensitiveColumns[$table][is_string($alias) ? $alias : $column] = true;
}
}
}
@ -254,17 +255,24 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
* Return the given table with its alias being applied
*
* @param array|string $table
* @param string $virtualTable
*
* @return array|string
*/
protected function applyTableAlias($table)
protected function applyTableAlias($table, $virtualTable = null)
{
$tableAliases = $this->getTableAliases();
if (is_array($table) || !isset($tableAliases[($nonPrefixedTable = $this->removeTablePrefix($table))])) {
return $table;
if (! is_array($table)) {
if ($virtualTable !== null && isset($tableAliases[$virtualTable])) {
return array($tableAliases[$virtualTable] => $table);
}
if (isset($tableAliases[($nonPrefixedTable = $this->removeTablePrefix($table))])) {
return array($tableAliases[$nonPrefixedTable] => $table);
}
}
return array($tableAliases[$nonPrefixedTable] => $table);
return $table;
}
/**
@ -294,10 +302,16 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
*
* @param string $table
* @param array $bind
*
* @return int The number of affected rows
*/
public function insert($table, array $bind)
{
$this->ds->insert($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind));
$this->requireTable($table);
return $this->ds->insert(
$this->prependTablePrefix($table),
$this->requireStatementColumns($table, $bind)
);
}
/**
@ -306,14 +320,22 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
* @param string $table
* @param array $bind
* @param Filter $filter
*
* @return int The number of affected rows
*/
public function update($table, array $bind, Filter $filter = null)
{
$this->requireTable($table);
if ($filter) {
$filter = $this->requireFilter($table, $filter);
}
$this->ds->update($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind), $filter);
return $this->ds->update(
$this->prependTablePrefix($table),
$this->requireStatementColumns($table, $bind),
$filter
);
}
/**
@ -321,14 +343,18 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
*
* @param string $table
* @param Filter $filter
*
* @return int The number of affected rows
*/
public function delete($table, Filter $filter = null)
{
$this->requireTable($table);
if ($filter) {
$filter = $this->requireFilter($table, $filter);
}
$this->ds->delete($this->prependTablePrefix($table), $filter);
return $this->ds->delete($this->prependTablePrefix($table), $filter);
}
/**
@ -468,8 +494,8 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
* This does not check whether any conversion for the given table is available if $column is not given, as it
* may be possible that columns from another table where joined in which would otherwise not being converted.
*
* @param array|string $table
* @param string $column
* @param string $table
* @param string $column
*
* @return bool
*/
@ -477,10 +503,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
{
if ($column !== null) {
if ($this->validateQueryColumnAssociation($table, $column)) {
return parent::providesValueConversion(
$this->removeTablePrefix($this->clearTableAlias($table)),
$column
);
return parent::providesValueConversion($table, $column);
}
if (($tableName = $this->findTableName($column))) {
@ -513,11 +536,9 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
protected function getConverter($table, $name, $context, RepositoryQuery $query = null)
{
if (
($query !== null && $this->validateQueryColumnAssociation($table, $name))
|| ($query === null && $this->validateStatementColumnAssociation($table, $name))
! ($query !== null && $this->validateQueryColumnAssociation($table, $name))
&& !($query === null && $this->validateStatementColumnAssociation($table, $name))
) {
$table = $this->removeTablePrefix($this->clearTableAlias($table));
} else {
$table = $this->findTableName($name);
if (! $table) {
throw new ProgrammingError('Column name validation seems to have failed. Did you require the column?');
@ -542,86 +563,46 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
*/
public function requireTable($table, RepositoryQuery $query = null)
{
$virtualTable = null;
$statementColumns = $this->getStatementColumns();
if (! isset($statementColumns[$table])) {
$table = parent::requireTable($table);
}
return $this->prependTablePrefix($this->applyTableAlias($table));
}
/**
* Recurse the given filter, require each column for the given table and convert all values
*
* In case of a PostgreSQL connection, this applies LOWER() on the column and strtolower()
* on the value if a COLLATE SQL-instruction is part of the resolved column.
*
* @param string $table The table being filtered
* @param Filter $filter The filter to recurse
* @param RepositoryQuery $query An optional query to pass as context
* (Directly passed through to $this->requireFilterColumn)
* @param bool $clone Whether to clone $filter first
*
* @return Filter The udpated filter
*/
public function requireFilter($table, Filter $filter, RepositoryQuery $query = null, $clone = true)
{
$filter = parent::requireFilter($table, $filter, $query, $clone);
if ($filter->isExpression()) {
$column = $filter->getColumn();
if (in_array($column, $this->columnsWithoutCollation) && strpos($column, 'LOWER') !== 0) {
$filter->setColumn('LOWER(' . $column . ')');
$expression = $filter->getExpression();
if (is_array($expression)) {
$filter->setExpression(array_map('strtolower', $expression));
} else {
$filter->setExpression(strtolower($expression));
}
$newTable = parent::requireTable($table);
if ($newTable !== $table) {
$virtualTable = $table;
}
$table = $newTable;
}
return $filter;
return $this->prependTablePrefix($this->applyTableAlias($table, $virtualTable));
}
/**
* Return this repository's query columns of the given table mapped to their respective aliases
* Return the alias for the given table or null if none has been defined
*
* @param array|string $table
*
* @return array
*
* @throws ProgrammingError In case $table does not exist
*/
public function requireAllQueryColumns($table)
{
return parent::requireAllQueryColumns($this->removeTablePrefix($this->clearTableAlias($table)));
}
/**
* Return the query column name for the given alias or null in case the alias does not exist
*
* @param array|string $table
* @param string $alias
* @param string $table
*
* @return string|null
*/
public function resolveQueryColumnAlias($table, $alias)
public function resolveTableAlias($table)
{
return parent::resolveQueryColumnAlias($this->removeTablePrefix($this->clearTableAlias($table)), $alias);
$tableAliases = $this->getTableAliases();
if (isset($tableAliases[$table])) {
return $tableAliases[$table];
}
}
/**
* Return the alias for the given query column name or null in case the query column name does not exist
*
* @param array|string $table
* @param string $column
* @param string $table
* @param string $column
*
* @return string|null
*/
public function reassembleQueryColumnAlias($table, $column)
{
$alias = parent::reassembleQueryColumnAlias($this->removeTablePrefix($this->clearTableAlias($table)), $column);
$alias = parent::reassembleQueryColumnAlias($table, $column);
if (
$alias === null
&& !$this->validateQueryColumnAssociation($table, $column)
@ -633,29 +614,13 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
return $alias;
}
/**
* Return whether the given query column name or alias is available in the given table
*
* @param array|string $table
* @param string $column
*
* @return bool
*/
public function validateQueryColumnAssociation($table, $column)
{
return parent::validateQueryColumnAssociation(
$this->removeTablePrefix($this->clearTableAlias($table)),
$column
);
}
/**
* Validate that the given column is a valid query target and return it or the actual name if it's an alias
*
* Attempts to join the given column from a different table if its association to the given table cannot be
* verified.
*
* @param array|string $table The table where to look for the column or alias
* @param string $table The table where to look for the column or alias
* @param string $name The name or alias of the column to validate
* @param RepositoryQuery $query An optional query to pass as context,
* if not given no join will be attempted
@ -667,7 +632,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
public function requireQueryColumn($table, $name, RepositoryQuery $query = null)
{
if ($query === null || $this->validateQueryColumnAssociation($table, $name)) {
return parent::requireQueryColumn($this->removeTablePrefix($this->clearTableAlias($table)), $name, $query);
return parent::requireQueryColumn($table, $name, $query);
}
return $this->joinColumn($name, $table, $query);
@ -677,35 +642,65 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
* Validate that the given column is a valid filter target and return it or the actual name if it's an alias
*
* Attempts to join the given column from a different table if its association to the given table cannot be
* verified.
* verified. In case of a PostgreSQL connection and if a COLLATE SQL-instruction is part of the resolved column,
* this applies LOWER() on the column and, if given, strtolower() on the filter's expression.
*
* @param array|string $table The table where to look for the column or alias
* @param string $table The table where to look for the column or alias
* @param string $name The name or alias of the column to validate
* @param RepositoryQuery $query An optional query to pass as context,
* if not given the column is considered being used for a statement filter
* @param FilterExpression $filter An optional filter to pass as context
*
* @return string The given column's name
*
* @throws QueryException In case the given column is not a valid filter column
*/
public function requireFilterColumn($table, $name, RepositoryQuery $query = null)
public function requireFilterColumn($table, $name, RepositoryQuery $query = null, FilterExpression $filter = null)
{
$joined = false;
if ($query === null) {
return $this->requireStatementColumn($table, $name);
$column = $this->requireStatementColumn($table, $name);
} elseif ($this->validateQueryColumnAssociation($table, $name)) {
$column = parent::requireFilterColumn($table, $name, $query, $filter);
} else {
$column = $this->joinColumn($name, $table, $query);
$joined = true;
}
if ($this->validateQueryColumnAssociation($table, $name)) {
return parent::requireFilterColumn($this->removeTablePrefix($this->clearTableAlias($table)), $name, $query);
if (! empty($this->caseInsensitiveColumns)) {
if ($joined) {
$table = $this->findTableName($name);
}
if ($column === $name) {
if ($query === null) {
$name = $this->reassembleStatementColumnAlias($table, $name);
} else {
$name = $this->reassembleQueryColumnAlias($table, $name);
}
}
if (isset($this->caseInsensitiveColumns[$table][$name])) {
$column = 'LOWER(' . $column . ')';
if ($filter !== null) {
$expression = $filter->getExpression();
if (is_array($expression)) {
$filter->setExpression(array_map('strtolower', $expression));
} else {
$filter->setExpression(strtolower($expression));
}
}
}
}
return $this->joinColumn($name, $table, $query);
return $column;
}
/**
* Return the statement column name for the given alias or null in case the alias does not exist
*
* @param array|string $table
* @param string $alias
* @param string $table
* @param string $alias
*
* @return string|null
*/
@ -716,7 +711,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
return $statementAliasColumnMap[$alias];
}
$prefixedAlias = $this->removeTablePrefix($this->clearTableAlias($table)) . '.' . $alias;
$prefixedAlias = $table . '.' . $alias;
if (isset($statementAliasColumnMap[$prefixedAlias])) {
return $statementAliasColumnMap[$prefixedAlias];
}
@ -725,8 +720,8 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
/**
* Return the alias for the given statement column name or null in case the statement column does not exist
*
* @param array|string $table
* @param string $column
* @param string $table
* @param string $column
*
* @return string|null
*/
@ -737,7 +732,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
return $statementColumnAliasMap[$column];
}
$prefixedColumn = $this->removeTablePrefix($this->clearTableAlias($table)) . '.' . $column;
$prefixedColumn = $table . '.' . $column;
if (isset($statementColumnAliasMap[$prefixedColumn])) {
return $statementColumnAliasMap[$prefixedColumn];
}
@ -746,34 +741,32 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
/**
* Return whether the given alias or statement column name is available in the given table
*
* @param array|string $table
* @param string $alias
* @param string $table
* @param string $alias
*
* @return bool
*/
public function validateStatementColumnAssociation($table, $alias)
{
$tableName = $this->removeTablePrefix($this->clearTableAlias($table));
$statementAliasTableMap = $this->getStatementAliasTableMap();
if (isset($statementAliasTableMap[$alias])) {
return $statementAliasTableMap[$alias] === $tableName;
return $statementAliasTableMap[$alias] === $table;
}
$statementColumnTableMap = $this->getStatementColumnTableMap();
if (isset($statementColumnTableMap[$alias])) {
return $statementColumnTableMap[$alias] === $tableName;
return $statementColumnTableMap[$alias] === $table;
}
$prefixedAlias = $tableName . '.' . $alias;
$prefixedAlias = $table . '.' . $alias;
return isset($statementAliasTableMap[$prefixedAlias]) || isset($statementColumnTableMap[$prefixedAlias]);
}
/**
* Return whether the given column name or alias of the given table is a valid statement column
*
* @param array|string $table The table where to look for the column or alias
* @param string $name The column name or alias to check
* @param string $table The table where to look for the column or alias
* @param string $name The column name or alias to check
*
* @return bool
*/
@ -784,7 +777,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
&& $this->reassembleStatementColumnAlias($table, $name) === null)
|| !$this->validateStatementColumnAssociation($table, $name)
) {
return parent::hasStatementColumn($this->removeTablePrefix($this->clearTableAlias($table)), $name);
return parent::hasStatementColumn($table, $name);
}
return true;
@ -793,8 +786,8 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
/**
* Validate that the given column is a valid statement column and return it or the actual name if it's an alias
*
* @param array|string $table The table for which to require the column
* @param string $name The name or alias of the column to validate
* @param string $table The table for which to require the column
* @param string $name The name or alias of the column to validate
*
* @return string The given column's name
*
@ -807,15 +800,11 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
} elseif (($alias = $this->reassembleStatementColumnAlias($table, $name)) !== null) {
$column = $name;
} else {
return parent::requireStatementColumn($this->removeTablePrefix($this->clearTableAlias($table)), $name);
return parent::requireStatementColumn($table, $name);
}
if (! $this->validateStatementColumnAssociation($table, $alias)) {
throw new StatementException(
'Statement column "%s" not found in table "%s"',
$name,
$this->removeTablePrefix($this->clearTableAlias($table))
);
throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
}
return $column;
@ -829,7 +818,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
* The method is called with the same parameters but in reversed order.
*
* @param string $name The alias or column name to join into $target
* @param array|string $target The table to join $name into
* @param string $target The table to join $name into
* @param RepositoryQUery $query The query to apply the JOIN-clause on
*
* @return string The resolved alias or $name
@ -843,7 +832,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
throw new ProgrammingError(
'Unable to find a valid table for column "%s" to join into "%s"',
$name,
$this->removeTablePrefix($this->clearTableAlias($target))
$target
);
}
@ -851,8 +840,10 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
$column = $name;
}
$prefixedTableName = $this->prependTablePrefix($tableName);
if ($query->getQuery()->hasJoinedTable($prefixedTableName)) {
if (($joinIdentifier = $this->resolveTableAlias($tableName)) === null) {
$joinIdentifier = $this->prependTablePrefix($tableName);
}
if ($query->getQuery()->hasJoinedTable($joinIdentifier)) {
return $column;
}
@ -861,7 +852,7 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
throw new ProgrammingError(
'Unable to join table "%s" into "%s". Method "%s" not found',
$tableName,
$this->removeTablePrefix($this->clearTableAlias($target)),
$target,
$joinMethod
);
}

View File

@ -42,15 +42,6 @@ abstract class LdapRepository extends Repository
'groupofuniquenames' => 'groupOfUniqueNames'
);
/**
* Object attributes whose value is not distinguished name
*
* @var array
*/
protected $ambiguousAttributes = array(
'posixGroup' => 'memberUid'
);
/**
* Create a new LDAP repository object
*
@ -79,15 +70,19 @@ abstract class LdapRepository extends Repository
}
/**
* Return whether the given object attribute's value is not a distinguished name
* Return whether the given object DN is related to the given base DN
*
* @param string $objectClass
* @param string $attributeName
* Will use the current connection's root DN if $baseDn is not given.
*
* @param string $dn The object DN to check
* @param string $baseDn The base DN to compare the object DN with
*
* @return bool
*/
protected function isAmbiguous($objectClass, $attributeName)
protected function isRelatedDn($dn, $baseDn = null)
{
return isset($this->ambiguousAttributes[$objectClass][$attributeName]);
$normalizedDn = strtolower(join(',', array_map('trim', explode(',', $dn))));
$normalizedBaseDn = strtolower(join(',', array_map('trim', explode(',', $baseDn ?: $this->ds->getDn()))));
return strpos($normalizedDn, $normalizedBaseDn) !== false;
}
}

View File

@ -6,6 +6,7 @@ namespace Icinga\Repository;
use DateTime;
use Icinga\Application\Logger;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Filter\FilterExpression;
use Icinga\Data\Selectable;
use Icinga\Exception\ProgrammingError;
use Icinga\Exception\QueryException;
@ -52,6 +53,16 @@ abstract class Repository implements Selectable
*/
protected $baseTable;
/**
* The virtual tables being provided
*
* This may be initialized by concrete repository implementations with an array
* where a key is the name of a virtual table and its value the real table name.
*
* @var array
*/
protected $virtualTables;
/**
* The query columns being provided
*
@ -255,6 +266,32 @@ abstract class Repository implements Selectable
return $this->baseTable;
}
/**
* Return the virtual tables being provided
*
* Calls $this->initializeVirtualTables() in case $this->virtualTables is null.
*
* @return array
*/
public function getVirtualTables()
{
if ($this->virtualTables === null) {
$this->virtualTables = $this->initializeVirtualTables();
}
return $this->virtualTables;
}
/**
* Overwrite this in your repository implementation in case you need to initialize the virtual tables lazily
*
* @return array
*/
protected function initializeVirtualTables()
{
return array();
}
/**
* Return the query columns being provided
*
@ -581,7 +618,7 @@ abstract class Repository implements Selectable
{
$converter = $this->getConverter($table, $name, 'persist', $query);
if ($converter !== null) {
$value = $this->$converter($value);
$value = $this->$converter($value, $name, $table, $query);
}
return $value;
@ -603,7 +640,7 @@ abstract class Repository implements Selectable
{
$converter = $this->getConverter($table, $name, 'retrieve', $query);
if ($converter !== null) {
$value = $this->$converter($value);
$value = $this->$converter($value, $name, $table, $query);
}
return $value;
@ -792,7 +829,7 @@ abstract class Repository implements Selectable
}
/**
* Validate that the requested table exists
* Validate that the requested table exists and resolve it's real name if necessary
*
* @param string $table The table to validate
* @param RepositoryQuery $query An optional query to pass as context
@ -809,6 +846,11 @@ abstract class Repository implements Selectable
throw new ProgrammingError('Table "%s" not found', $table);
}
$virtualTables = $this->getVirtualTables();
if (isset($virtualTables[$table])) {
$table = $virtualTables[$table];
}
return $table;
}
@ -831,8 +873,8 @@ abstract class Repository implements Selectable
if ($filter->isExpression()) {
$column = $filter->getColumn();
$filter->setColumn($this->requireFilterColumn($table, $column, $query));
$filter->setExpression($this->persistColumn($table, $column, $filter->getExpression()));
$filter->setColumn($this->requireFilterColumn($table, $column, $query, $filter));
$filter->setExpression($this->persistColumn($table, $column, $filter->getExpression(), $query));
} elseif ($filter->isChain()) {
foreach ($filter->filters() as $chainOrExpression) {
$this->requireFilter($table, $chainOrExpression, $query, false);
@ -862,7 +904,7 @@ abstract class Repository implements Selectable
$columns = array();
foreach ($queryColumns[$table] as $alias => $column) {
if (! in_array(is_string($alias) ? $alias : $column, $blacklist)) {
$columns[$alias] = $column;
$columns[$alias] = $this->resolveQueryColumnAlias($table, $alias);
}
}
@ -1008,12 +1050,13 @@ abstract class Repository implements Selectable
* @param string $table The table where to look for the column or alias
* @param string $name The name or alias of the column to validate
* @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation)
* @param FilterExpression $filter An optional filter to pass as context (unused by the base implementation)
*
* @return string The given column's name
*
* @throws QueryException In case the given column is not a valid filter column
*/
public function requireFilterColumn($table, $name, RepositoryQuery $query = null)
public function requireFilterColumn($table, $name, RepositoryQuery $query = null, FilterExpression $filter = null)
{
if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) {
$alias = $name;

View File

@ -47,6 +47,13 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
*/
protected $iterator;
/**
* This query's custom aliases
*
* @var array
*/
protected $customAliases;
/**
* Create a new repository query
*
@ -79,8 +86,8 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
*/
public function from($target, array $columns = null)
{
$target = $this->repository->requireTable($target, $this);
$this->query = $this->repository->getDataSource()->select()->from($target);
$this->query = $this->repository->getDataSource()->select();
$this->query->from($this->repository->requireTable($target, $this));
$this->query->columns($this->prepareQueryColumns($target, $columns));
$this->target = $target;
return $this;
@ -123,6 +130,7 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
*/
protected function prepareQueryColumns($target, array $desiredColumns = null)
{
$this->customAliases = array();
if (empty($desiredColumns)) {
$columns = $this->repository->requireAllQueryColumns($target);
} else {
@ -130,9 +138,15 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
foreach ($desiredColumns as $customAlias => $columnAlias) {
$resolvedColumn = $this->repository->requireQueryColumn($target, $columnAlias, $this);
if ($resolvedColumn !== $columnAlias) {
$columns[is_string($customAlias) ? $customAlias : $columnAlias] = $resolvedColumn;
if (is_string($customAlias)) {
$columns[$customAlias] = $resolvedColumn;
$this->customAliases[$customAlias] = $columnAlias;
} else {
$columns[$columnAlias] = $resolvedColumn;
}
} elseif (is_string($customAlias)) {
$columns[$customAlias] = $columnAlias;
$this->customAliases[$customAlias] = $columnAlias;
} else {
$columns[] = $columnAlias;
}
@ -142,6 +156,24 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
return $columns;
}
/**
* Return the native column alias for the given custom alias
*
* If no custom alias is found with the given name, it is returned unchanged.
*
* @param string $customAlias
*
* @return string
*/
protected function getNativeAlias($customAlias)
{
if (isset($this->customAliases[$customAlias])) {
return $this->customAliases[$customAlias];
}
return $customAlias;
}
/**
* Return this query's available filter columns with their optional label as key
*
@ -174,10 +206,7 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
*/
public function where($column, $value = null)
{
$this->query->where(
$this->repository->requireFilterColumn($this->target, $column, $this),
$this->repository->persistColumn($this->target, $column, $value, $this)
);
$this->addFilter(Filter::where($column, $value));
return $this;
}
@ -466,7 +495,7 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
$result = $this->query->fetchOne();
if ($result !== false && $this->repository->providesValueConversion($this->target)) {
$columns = $this->getColumns();
$column = isset($columns[0]) ? $columns[0] : key($columns);
$column = isset($columns[0]) ? $columns[0] : $this->getNativeAlias(key($columns));
return $this->repository->retrieveColumn($this->target, $column, $result, $this);
}
@ -491,7 +520,12 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
$alias = $column;
}
$result->$alias = $this->repository->retrieveColumn($this->target, $alias, $result->$alias, $this);
$result->$alias = $this->repository->retrieveColumn(
$this->target,
$this->getNativeAlias($alias),
$result->$alias,
$this
);
}
}
@ -513,7 +547,7 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
$columns = $this->getColumns();
$aliases = array_keys($columns);
$column = is_int($aliases[0]) ? $columns[0] : $aliases[0];
$column = is_int($aliases[0]) ? $columns[0] : $this->getNativeAlias($aliases[0]);
if ($this->repository->providesValueConversion($this->target, $column)) {
foreach ($results as & $value) {
$value = $this->repository->retrieveColumn($this->target, $column, $value, $this);
@ -541,8 +575,11 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
$columns = $this->getColumns();
$aliases = array_keys($columns);
$colOne = $aliases[0] !== 0 ? $aliases[0] : $columns[0];
$colTwo = count($aliases) < 2 ? $colOne : ($aliases[1] !== 1 ? $aliases[1] : $columns[1]);
$colOne = $aliases[0] !== 0 ? $this->getNativeAlias($aliases[0]) : $columns[0];
$colTwo = count($aliases) < 2 ? $colOne : (
$aliases[1] !== 1 ? $this->getNativeAlias($aliases[1]) : $columns[1]
);
if (
$this->repository->providesValueConversion($this->target, $colOne)
|| $this->repository->providesValueConversion($this->target, $colTwo)
@ -587,21 +624,28 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
$alias = $column;
}
$row->$alias = $this->repository->retrieveColumn($this->target, $alias, $row->$alias, $this);
$row->$alias = $this->repository->retrieveColumn(
$this->target,
$this->getNativeAlias($alias),
$row->$alias,
$this
);
}
foreach (($this->getOrder() ?: array()) as $rule) {
$nativeAlias = $this->getNativeAlias($rule[0]);
if (! array_key_exists($rule[0], $flippedColumns) && property_exists($row, $rule[0])) {
if ($this->repository->providesValueConversion($this->target, $rule[0])) {
if ($this->repository->providesValueConversion($this->target, $nativeAlias)) {
$updateOrder = true;
$row->{$rule[0]} = $this->repository->retrieveColumn(
$this->target,
$rule[0],
$row->{$rule[0]}
$nativeAlias,
$row->{$rule[0]},
$this
);
}
} elseif (array_key_exists($rule[0], $flippedColumns)) {
if ($this->repository->providesValueConversion($this->target, $rule[0])) {
if ($this->repository->providesValueConversion($this->target, $nativeAlias)) {
$updateOrder = true;
}
}
@ -677,7 +721,12 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
$alias = $column;
}
$row->$alias = $this->repository->retrieveColumn($this->target, $alias, $row->$alias, $this);
$row->$alias = $this->repository->retrieveColumn(
$this->target,
$this->getNativeAlias($alias),
$row->$alias,
$this
);
}
}

View File

@ -186,6 +186,7 @@ class Controller extends ModuleActionController
* @param Filterable $filterable The filterable to create a filter editor for
* @param array $filterColumns The filter columns to offer to the user
* @param array $searchColumns The search columns to utilize for quick searches
* @param array $preserveParams The url parameters to preserve
*
* @return $this
*
@ -194,25 +195,27 @@ class Controller extends ModuleActionController
protected function setupFilterControl(
Filterable $filterable,
array $filterColumns = null,
array $searchColumns = null
array $searchColumns = null,
array $preserveParams = null
) {
$editor = Widget::create('filterEditor')
$defaultPreservedParams = array(
'limit', // setupPaginationControl()
'sort', // setupSortControl()
'dir', // setupSortControl()
'backend', // Framework
'view', // Framework
'_dev' // Framework
);
$editor = Widget::create('filterEditor');
call_user_func_array(
array($editor, 'preserveParams'),
array_merge($defaultPreservedParams, $preserveParams ?: array())
);
$editor
->setQuery($filterable)
->preserveParams(
'limit',
'sort',
'dir',
'format',
'view',
'user',
'group',
'backend',
'stateType',
'addColumns',
'problems',
'_dev'
)
->ignoreParams('page')
->ignoreParams('page') // setupPaginationControl()
->setColumns($filterColumns)
->setSearchColumns($searchColumns)
->handleRequest($this->getRequest());

View File

@ -171,7 +171,7 @@ class UrlParams
protected function urlEncode($value)
{
return rawurlencode((string) $value);
return rawurlencode($value instanceof Url ? $value->getAbsoluteUrl() : (string) $value);
}
/**

View File

@ -574,7 +574,12 @@ class ListController extends Controller
*/
protected function filterQuery(DataView $dataView)
{
$this->setupFilterControl($dataView);
$this->setupFilterControl($dataView, null, null, array(
'format', // handleFormatRequest()
'stateType', // hostsAction() and servicesAction()
'addColumns', // addColumns()
'problems' // servicegridAction()
));
$this->handleFormatRequest($dataView);
return $dataView;
}

View File

@ -73,8 +73,8 @@ $stateBadges
'host_state' => 2,
'host_handled' => 1
),
'List %u host that is currently in state UNREACHABLE (Acknowledged) in the host group "%s"',
'List %u hosts which are currently in state UNREACHABLE (Acknowledged) in the host group "%s"',
'List %u host that is currently in state UNREACHABLE (Acknowledged)',
'List %u hosts which are currently in state UNREACHABLE (Acknowledged)',
array($stats->hosts_unreachable_handled)
)
->add(
@ -83,8 +83,8 @@ $stateBadges
array(
'host_state' => 99
),
'List %u host that is currently in state UNREACHABLE (Acknowledged)',
'List %u hosts which are currently in state UNREACHABLE (Acknowledged)',
'List %u host that is currently in state PENDING',
'List %u hosts which are currently in state PENDING',
array($stats->hosts_pending)
);
echo $stateBadges->render();

View File

@ -75,7 +75,7 @@ class HostserviceproblemsummaryQuery extends IdoQuery
's.service_object_id = so.object_id AND so.is_active = 1',
array()
);
$this->select->group(array('so.object_id'));
$this->select->group('so.name1');
$this->joinedVirtualTables['services'] = true;
}

View File

@ -55,14 +55,14 @@ abstract class IdoQuery extends DbQuery
protected $prefix;
/**
* An array to map aliases to table names
* An array to map aliases to column names
*
* @var array
*/
protected $idxAliasColumn;
/**
* An array to map aliases to column names
* An array to map aliases to table names
*
* @var array
*/
@ -400,14 +400,14 @@ abstract class IdoQuery extends DbQuery
protected static $idoVersion;
/**
* List of columns where the COLLATE SQL-instruction has been removed
* List of column aliases mapped to their table where the COLLATE SQL-instruction has been removed
*
* This list is being populated in case of a PostgreSQL backend only,
* to ensure case-insensitive string comparison in WHERE clauses.
*
* @var array
*/
protected $columnsWithoutCollation = array();
protected $caseInsensitiveColumns;
/**
* Return true when the column is an aggregate column
@ -421,26 +421,32 @@ abstract class IdoQuery extends DbQuery
}
/**
* Order the result by the given column
* Order the result by the given alias
*
* @param string $columnOrAlias The column or column alias to order by
* @param int $dir The sort direction or null to use default direction
* @param string $alias The column alias to order by
* @param int $dir The sort direction or null to use the default direction
*
* @return $this Fluent interface
* @return $this
*/
public function order($columnOrAlias, $dir = null)
public function order($alias, $dir = null)
{
$this->requireColumn($columnOrAlias);
$this->orderColumns[$columnOrAlias] = $columnOrAlias;
if ($this->isCustomvar($columnOrAlias)) {
$columnOrAlias = $this->getCustomvarColumnName($columnOrAlias);
} elseif ($this->hasAliasName($columnOrAlias)) {
$columnOrAlias = $this->aliasToColumnName($columnOrAlias);
$this->requireColumn($alias);
if ($this->isCustomvar($alias)) {
$column = $this->getCustomvarColumnName($alias);
} elseif ($this->hasAliasName($alias)) {
$column = $this->aliasToColumnName($alias);
$table = $this->aliasToTableName($alias);
if (isset($this->caseInsensitiveColumns[$table][$alias])) {
$column = 'LOWER(' . $column . ')';
}
} else {
Logger::info('Can\'t order by column ' . $columnOrAlias);
Logger::info('Can\'t order by column ' . $alias);
return $this;
}
return parent::order($columnOrAlias, $dir);
$this->orderColumns[$alias] = $alias;
return parent::order($column, $dir);
}
/**
@ -485,22 +491,25 @@ abstract class IdoQuery extends DbQuery
if ($filter->getExpression() === '*') {
return; // Wildcard only filters are ignored so stop early here to avoid joining a table for nothing
}
$alias = $filter->getColumn();
$this->requireColumn($alias);
if ($this->isCustomvar($alias)) {
$column = $this->getCustomvarColumnName($alias);
} else {
$column = $this->aliasToColumnName($alias);
}
if (isset($this->columnsWithoutCollation[$alias])) {
$expression = $filter->getExpression();
if (is_array($expression)) {
$filter->setExpression(array_map('strtolower', $expression));
} else {
$filter->setExpression(strtolower($expression));
if (isset($this->caseInsensitiveColumns[$this->aliasToTableName($alias)][$alias])) {
$column = 'LOWER(' . $column . ')';
$expression = $filter->getExpression();
if (is_array($expression)) {
$filter->setExpression(array_map('strtolower', $expression));
} else {
$filter->setExpression(strtolower($expression));
}
}
}
$filter->setColumn($column);
} else {
foreach ($filter->filters() as $filter) {
@ -524,6 +533,7 @@ abstract class IdoQuery extends DbQuery
if ($value === '*') {
return $this; // Wildcard only filters are ignored so stop early here to avoid joining a table for nothing
}
$this->requireColumn($condition);
$col = $this->getMappedField($condition);
if ($col === null) {
@ -551,27 +561,37 @@ abstract class IdoQuery extends DbQuery
}
/**
* Return whether the given alias or column name provides case insensitive value comparison
* Return whether the given alias provides case insensitive value comparison
*
* @param string $aliasOrColumn
* @param string $alias
*
* @return bool
*/
public function isCaseInsensitive($aliasOrColumn)
public function isCaseInsensitive($alias)
{
if ($this->isCustomVar($aliasOrColumn)) {
if ($this->isCustomVar($alias)) {
return false;
}
$column = $this->getMappedField($aliasOrColumn) ?: $aliasOrColumn;
$column = $this->getMappedField($alias);
if (! $column) {
return false;
}
if (! empty($this->columnsWithoutCollation)) {
return in_array($column, $this->columnsWithoutCollation) || strpos($column, 'LOWER') !== 0;
if (empty($this->caseInsensitiveColumns)) {
return preg_match('/ COLLATE .+$/', $column) === 1;
}
return preg_match('/ COLLATE .+$/', $column) === 1;
if (strpos($column, 'LOWER') === 0) {
return true;
}
$table = $this->aliasToTableName($alias);
if (! $table) {
return false;
}
return isset($this->caseInsensitiveColumns[$table][$alias]);
}
/**
@ -601,12 +621,14 @@ abstract class IdoQuery extends DbQuery
{
$this->customVarsJoinTemplate =
'%1$s = %2$s.object_id AND LOWER(%2$s.varname) = %3$s';
foreach ($this->columnMap as $table => &$columns) {
foreach ($columns as $alias => &$column) {
if (false !== $pos = strpos($column, ' COLLATE')) {
$column = 'LOWER(' . substr($column, 0, $pos) . ')';
$this->columnsWithoutCollation[$alias] = true;
foreach ($this->columnMap as $table => & $columns) {
foreach ($columns as $alias => & $column) {
// Using a regex here because COLLATE may occur anywhere in the string
$column = preg_replace('/ COLLATE .+$/', '', $column, -1, $count);
if ($count > 0) {
$this->caseInsensitiveColumns[$table][$alias] = true;
}
$column = preg_replace(
'/inet_aton\(([[:word:].]+)\)/i',
'(CASE WHEN $1 ~ \'(?:[0-9]{1,3}\\\\.){3}[0-9]{1,3}\' THEN $1::inet - \'0.0.0.0\' ELSE NULL END)',

View File

@ -18,6 +18,7 @@ class HostStatus extends DataView
'host_address',
'host_address6',
'host_state',
'host_hard_state',
'host_state_type',
'host_handled',
'host_unhandled',

View File

@ -15,6 +15,7 @@ class ServiceStatus extends DataView
'host_name',
'host_display_name',
'host_state',
'host_hard_state',
'host_state_type',
'host_last_state_change',
'host_address',
@ -24,6 +25,7 @@ class ServiceStatus extends DataView
'service_description',
'service_display_name',
'service_state',
'service_hard_state',
'service_in_downtime',
'service_acknowledged',
'service_handled',

View File

@ -44,41 +44,15 @@ class MacroTest extends BaseTestCase
$this->assertEquals(Macro::resolveMacros('$service.description$', $svcMock), $svcMock->service_description);
}
public function testCustomvars()
{
$objectMock = Mockery::mock('object');
$objectMock->customvars = array(
'customvar' => 'test'
);
$this->assertEquals(Macro::resolveMacros('$CUSTOMVAR$', $objectMock), $objectMock->customvars['customvar']);
}
public function testFaultyMacros()
{
$hostMock = Mockery::mock('host');
$hostMock->host_name = 'test';
$hostMock->customvars = array(
'host' => 'te',
'name' => 'st'
);
$hostMock->host = 'te';
$this->assertEquals(
Macro::resolveMacros('$$HOSTNAME$ $ HOSTNAME$ $HOST$NAME$', $hostMock),
'$test $ HOSTNAME$ teNAME$'
);
}
public function testMacrosWithSpecialCharacters()
{
$objectMock = Mockery::mock('object');
$objectMock->customvars = array(
'v€ry_sp3c|@l' => 'not too special!'
);
$this->assertEquals(
Macro::resolveMacros('$V€RY_SP3C|@L$', $objectMock),
$objectMock->customvars['v€ry_sp3c|@l']
'$test $ HOSTNAME$ teNAME$',
Macro::resolveMacros('$$HOSTNAME$ $ HOSTNAME$ $host$NAME$', $hostMock)
);
}
}

View File

@ -233,83 +233,6 @@ div.content.users {
}
}
div.content.memberships {
table.membership-list {
th.membership-cancel {
width: 8em;
padding-right: 0.5em;
text-align: right;
}
td.membership-cancel {
text-align: right;
form button.link-like {
color: inherit;
}
}
}
p {
margin-top: 0;
}
a.membership-create {
display: block;
margin-top: 1em;
}
}
div.content.groups {
table.group-list {
th.group-remove {
width: 8em;
padding-right: 0.5em;
text-align: right;
}
td.group-remove {
text-align: right;
}
}
p {
margin-top: 0;
}
a.group-add {
display: block;
margin-top: 1em;
}
}
div.content.members {
table.member-list {
th.member-remove {
width: 8em;
padding-right: 0.5em;
text-align: right;
}
td.member-remove {
text-align: right;
form button.link-like {
color: inherit;
}
}
}
p {
margin-top: 0;
}
a.member-add {
display: block;
margin-top: 1em;
}
}
form.backend-selection {
float: right;