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 dn: cn=Users,ou=groups,dc=icinga,dc=org
objectClass: groupOfUniqueNames objectClass: groupOfUniqueNames
cn: Users 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=Jane Smith,ou=people,dc=icinga,dc=org
uniqueMember: cn=John Q. Public,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 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': icingaweb2::config::general { 'authentication':
source => $name, source => $name,
replace => false,
} }
icingaweb2::config::general { [ 'config', 'resources', 'roles' ]: icingaweb2::config::general { [ 'config', 'resources', 'roles' ]:

View File

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

View File

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

View File

@ -71,7 +71,7 @@ class LoginForm extends Form
if ($this->created) { if ($this->created) {
$redirect = $this->getElement('redirect')->getValue(); $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; $redirect = static::REDIRECT_URL;
} }
return Url::fromPath($redirect); return Url::fromPath($redirect);

View File

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

View File

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

View File

@ -1,41 +1,34 @@
<?php <?php
use Icinga\Data\Extensible; use Icinga\Data\Extensible;
use Icinga\Data\Reducible; use Icinga\Data\Reducible;
if (! $this->compact): ?> if (! $this->compact): ?>
<div class="controls"> <div class="controls separated dont-print">
<?= $this->tabs ?> <?= $tabs ?>
<div class="grid dont-print"> <div class="grid">
<div class="col-1-3 text-left">
<?= $this->limiter ?> <?= $this->limiter ?>
</div>
<div class="col-1-3">
<?= $this->paginator ?> <?= $this->paginator ?>
</div>
<div class="col-1-3 text-right">
<?= $this->sortBox ?> <?= $this->sortBox ?>
</div> </div>
</div>
<div> <div>
<?= $this->backendSelection ?> <?= $this->backendSelection ?>
<?= $this->filterEditor ?> <?= $this->filterEditor ?>
</div> </div>
</div> </div>
<?php endif ?> <?php endif ?>
<div class="content"> <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 <?php
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; $extensible = $this->hasPermission('config/authentication/users/add') && $backend instanceof Extensible;
$reducible = $this->hasPermission('config/authentication/users/remove') && $backend instanceof Reducible; $reducible = $this->hasPermission('config/authentication/users/remove') && $backend instanceof Reducible;
}
?> ?>
<?php if ($extensible): ?> <?php if ($extensible): ?>
<?= $this->qlink( <?= $this->qlink(
$this->translate('Add a New User') , $this->translate('Add a New User') ,
@ -44,12 +37,17 @@ $reducible = $this->hasPermission('config/authentication/users/remove') && $back
array( array(
'class' => 'button-link', 'class' => 'button-link',
'data-base-target' => '_next', 'data-base-target' => '_next',
'icon' => 'plus', 'icon' => 'plus'
'title' => $this->translate('Create a new user')
) )
) ?> ) ?>
<?php endif ?> <?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> <thead>
<tr> <tr>
<th><?= $this->translate('Username') ?></th> <th><?= $this->translate('Username') ?></th>

View File

@ -22,7 +22,7 @@ if ($this->hasPermission('config/authentication/users/edit') && $backend instanc
} }
?> ?>
<div class="controls"> <div class="controls separated dont-print">
<?php if (! $this->compact): ?> <?php if (! $this->compact): ?>
<?= $tabs; ?> <?= $tabs; ?>
<?php endif ?> <?php endif ?>
@ -41,47 +41,48 @@ if ($this->hasPermission('config/authentication/users/edit') && $backend instanc
<td><?= $user->last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?></td> <td><?= $user->last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?></td>
</tr> </tr>
</table> </table>
<h2><?= $this->translate('Group Memberships'); ?></h2>
<?php if (! $this->compact): ?> <?php if (! $this->compact): ?>
<?= $this->sortBox; ?> <h2><?= $this->translate('Group Memberships'); ?></h2>
<?php endif ?> <div class="grid">
<?= $this->limiter; ?> <?= $this->limiter; ?>
<?= $this->paginator; ?> <?= $this->paginator; ?>
<?php if (! $this->compact): ?> <?= $this->sortBox; ?>
</div>
<?= $this->filterEditor; ?> <?= $this->filterEditor; ?>
<?php endif ?> <?php endif ?>
</div> </div>
<div class="content memberships"> <div class="content">
<?php if ($showCreateMembershipLink): ?> <?php if ($showCreateMembershipLink): ?>
<?= $this->qlink( <?= $this->qlink(
$this->translate('Add User to Group') , $this->translate('Create New Membership'),
'user/createmembership', 'user/createmembership',
array('backend' => $backend->getName()),
array( array(
'class' => 'button-link', 'backend' => $backend->getName(),
'data-base-target' => '_next', 'user' => $user->user_name
),
array(
'icon' => 'plus', 'icon' => 'plus',
'title' => $this->translate('Add user to user group') 'class' => 'button-link'
) )
) ?> ) ?>
<?php endif ?> <?php endif ?>
<?php
// @TODO(el): Remove that $firstRow thingy <?php if (! $memberships->hasResult()): ?>
$firstRow = true; <p><?= $this->translate('No memberships found matching the filter'); ?></p>
foreach ($memberships as $membership): ?> </div>
<?php if ($firstRow): ?> <?php return; endif ?>
<?php $firstRow = false; ?>
<table data-base-target="_next" class="action-table listing-table"> <table data-base-target="_next" class="action-table listing-table">
<thead> <thead>
<tr> <tr>
<th class="membership-group"><?= $this->translate('Group'); ?></th> <th><?= $this->translate('Group'); ?></th>
<th class="membership-cancel"><?= $this->translate('Cancel', 'group.membership'); ?></th> <th><?= $this->translate('Cancel', 'group.membership'); ?></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php endif ?> <?php foreach ($memberships as $membership): ?>
<tr> <tr>
<td class="membership-group"> <td>
<?php if ($this->hasPermission('config/authentication/groups/show') && $membership->backend instanceof Selectable): ?> <?php if ($this->hasPermission('config/authentication/groups/show') && $membership->backend instanceof Selectable): ?>
<?= $this->qlink($membership->group_name, 'group/show', array( <?= $this->qlink($membership->group_name, 'group/show', array(
'backend' => $membership->backend->getName(), 'backend' => $membership->backend->getName(),
@ -93,7 +94,7 @@ foreach ($memberships as $membership): ?>
<?= $this->escape($membership->group_name); ?> <?= $this->escape($membership->group_name); ?>
<?php endif ?> <?php endif ?>
</td> </td>
<td class="membership-cancel" data-base-target="_self"> <td class="icon-col" data-base-target="_self">
<?php if (isset($removeForm) && $membership->backend instanceof Reducible): ?> <?php if (isset($removeForm) && $membership->backend instanceof Reducible): ?>
<?= $removeForm->setAction($this->url('group/removemember', array( <?= $removeForm->setAction($this->url('group/removemember', array(
'backend' => $membership->backend->getName(), 'backend' => $membership->backend->getName(),
@ -105,10 +106,6 @@ foreach ($memberships as $membership): ?>
</td> </td>
</tr> </tr>
<?php endforeach ?> <?php endforeach ?>
<?php if ($memberships->hasResult()): ?>
</tbody> </tbody>
</table> </table>
<?php else: ?>
<p><?= $this->translate('No memberships found matching the filter'); ?></p>
<?php endif ?>
</div> </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 file remains unchanged.
* The location of a user's preferences has been changed from * The location of a user's preferences has been changed from
**<config-dir>/preferences/<username>.ini** to **&lt;config-dir&gt;/preferences/&lt;username&gt;.ini** to
**<config-dir>/preferences/<username>/config.ini**. **&lt;config-dir&gt;/preferences/&lt;username&gt;/config.ini**.
The content of the file remains unchanged. The content of the file remains unchanged.

View File

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

View File

@ -136,6 +136,16 @@ class Logger
return $this; return $this;
} }
/**
* Return the logging level being used
*
* @return int
*/
public function getLevel()
{
return $this->level;
}
/** /**
* Register the given message as config error * 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) public function insert($table, array $bind)
{ {
$this->requireTable($table);
$bind['created_at'] = date('Y-m-d H:i:s'); $bind['created_at'] = date('Y-m-d H:i:s');
$this->ds->insert( $this->ds->insert(
$this->prependTablePrefix($table), $this->prependTablePrefix($table),
@ -150,6 +151,7 @@ class DbUserBackend extends DbRepository implements UserBackendInterface, Inspec
*/ */
public function update($table, array $bind, Filter $filter = null) public function update($table, array $bind, Filter $filter = null)
{ {
$this->requireTable($table);
$bind['last_modified'] = date('Y-m-d H:i:s'); $bind['last_modified'] = date('Y-m-d H:i:s');
if ($filter) { if ($filter) {
$filter = $this->requireFilter($table, $filter); $filter = $this->requireFilter($table, $filter);

View File

@ -12,7 +12,6 @@ use Icinga\Exception\ProgrammingError;
use Icinga\Repository\LdapRepository; use Icinga\Repository\LdapRepository;
use Icinga\Repository\RepositoryQuery; use Icinga\Repository\RepositoryQuery;
use Icinga\Protocol\Ldap\LdapException; use Icinga\Protocol\Ldap\LdapException;
use Icinga\Protocol\Ldap\Expression;
use Icinga\User; use Icinga\User;
class LdapUserBackend extends LdapRepository implements UserBackendInterface, Inspectable 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 * Set the objectClass where to look for users
* *
* Sets also the base table name for the underlying repository.
*
* @param string $userClass * @param string $userClass
* *
* @return $this * @return $this
*/ */
public function setUserClass($userClass) public function setUserClass($userClass)
{ {
$this->baseTable = $this->userClass = $this->getNormedAttribute($userClass); $this->userClass = $this->getNormedAttribute($userClass);
return $this; 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); if ($this->userClass === null) {
$query->getQuery()->setBase($this->baseDn); throw new ProgrammingError('It is required to set the object class where to find users first');
if ($this->filter) {
$query->getQuery()->where(new Expression($this->filter));
} }
return $query; return array(
'user' => $this->userClass
);
} }
/** /**
@ -216,13 +213,10 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
* *
* @return array * @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() 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) { if ($this->userNameAttribute === null) {
throw new ProgrammingError('It is required to set a attribute name where to find a user\'s name first'); 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( return array(
$this->userClass => array( 'user' => array(
'user' => $this->userNameAttribute, 'user' => $this->userNameAttribute,
'user_name' => $this->userNameAttribute, 'user_name' => $this->userNameAttribute,
'is_active' => $isActiveAttribute, 'is_active' => $isActiveAttribute,
@ -269,15 +263,9 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
* Initialize this repository's conversion rules * Initialize this repository's conversion rules
* *
* @return array * @return array
*
* @throws ProgrammingError In case $this->userClass has not been set yet
*/ */
protected function initializeConversionRules() 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()) { if ($this->ds->getCapabilities()->isActiveDirectory()) {
$stateConverter = 'user_account_control'; $stateConverter = 'user_account_control';
} else { } else {
@ -285,7 +273,7 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
} }
return array( return array(
$this->userClass => array( 'user' => array(
'is_active' => $stateConverter, 'is_active' => $stateConverter,
'created_at' => 'generalized_time', 'created_at' => 'generalized_time',
'last_modified' => 'generalized_time' 'last_modified' => 'generalized_time'
@ -330,14 +318,46 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
} }
/** /**
* Validate that the requested table exists
* @param Inspection $info Optional inspection to fill with diagnostic info
* *
* @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() ->getQuery()
->setUsePagedResults(false) ->setUsePagedResults(false)
->fetchDn(); ->fetchDn();
if ($userDn === null) { if ($userDn === null) {
return false; return false;
} }
$testCredentialsResult = $this->ds->testCredentials($userDn, $password); $validCredentials = $this->ds->testCredentials($userDn, $password);
if ($testCredentialsResult) { if ($validCredentials) {
$user->setAdditional('ldap_dn', $userDn); $user->setAdditional('ldap_dn', $userDn);
} }
return $testCredentialsResult; return $validCredentials;
} catch (LdapException $e) { } catch (LdapException $e) {
throw new AuthenticationException( throw new AuthenticationException(
'Failed to authenticate user "%s" against backend "%s". An exception was thrown:', '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 $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 * Join group into group_membership
* *
@ -238,7 +250,7 @@ class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterfa
*/ */
protected function joinGroupMembership(RepositoryQuery $query) protected function joinGroupMembership(RepositoryQuery $query)
{ {
$query->getQuery()->join( $query->getQuery()->joinLeft(
$this->requireTable('group_membership'), $this->requireTable('group_membership'),
'g.id = gm.group_id', 'g.id = gm.group_id',
array() array()

View File

@ -5,10 +5,11 @@ namespace Icinga\Authentication\UserGroup;
use Icinga\Authentication\User\UserBackend; use Icinga\Authentication\User\UserBackend;
use Icinga\Authentication\User\LdapUserBackend; use Icinga\Authentication\User\LdapUserBackend;
use Icinga\Application\Logger;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Exception\ConfigurationError; use Icinga\Exception\ConfigurationError;
use Icinga\Exception\ProgrammingError; use Icinga\Exception\ProgrammingError;
use Icinga\Protocol\Ldap\Expression; use Icinga\Protocol\Ldap\LdapException;
use Icinga\Repository\LdapRepository; use Icinga\Repository\LdapRepository;
use Icinga\Repository\RepositoryQuery; use Icinga\Repository\RepositoryQuery;
use Icinga\User; use Icinga\User;
@ -71,6 +72,13 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
*/ */
protected $groupMemberAttribute; 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 * 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 * Set the objectClass where to look for groups
* *
* Sets also the base table name for the underlying repository.
*
* @param string $groupClass * @param string $groupClass
* *
* @return $this * @return $this
*/ */
public function setGroupClass($groupClass) public function setGroupClass($groupClass)
{ {
$this->baseTable = $this->groupClass = $this->getNormedAttribute($groupClass); $this->groupClass = $this->getNormedAttribute($groupClass);
return $this; 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); if ($this->ambiguousMemberAttribute === null) {
$query->getQuery()->setBase($this->groupBaseDn); if ($this->groupClass === null) {
if ($this->groupFilter) { throw new ProgrammingError(
// TODO(jom): This should differentiate between groups and their memberships 'It is required to set the objectClass where to look for groups first'
$query->getQuery()->where(new Expression($this->groupFilter)); );
} elseif ($this->groupMemberAttribute === null) {
throw new ProgrammingError(
'It is required to set a attribute name where to find a group\'s members first'
);
} }
return $query; $sampleValue = $this->ds
->select()
->from($this->groupClass, array($this->groupMemberAttribute))
->setUnfoldAttribute($this->groupMemberAttribute)
->setBase($this->groupBaseDn)
->fetchOne();
$this->ambiguousMemberAttribute = !$this->isRelatedDn($sampleValue);
}
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 * @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() 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) { if ($this->groupNameAttribute === null) {
throw new ProgrammingError('It is required to set a attribute name where to find a group\'s name first'); 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()) { if ($this->ds->getCapabilities()->isActiveDirectory()) {
$createdAtAttribute = 'whenCreated'; $createdAtAttribute = 'whenCreated';
@ -409,7 +449,7 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
'created_at' => $createdAtAttribute, 'created_at' => $createdAtAttribute,
'last_modified' => $lastModifiedAttribute '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() protected function initializeFilterColumns()
{ {
return array( return array(
t('Username') => 'user', t('Username') => 'user_name',
t('User Group') => 'group_name', t('User Group') => 'group_name',
t('Created At') => 'created_at', t('Created At') => 'created_at',
t('Last Modified') => 'last_modified' t('Last Modified') => 'last_modified'
@ -431,31 +471,68 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
* Initialize this repository's conversion rules * Initialize this repository's conversion rules
* *
* @return array * @return array
*
* @throws ProgrammingError In case $this->groupClass has not been set yet
*/ */
protected function initializeConversionRules() 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( $rules = array(
$this->groupClass => array( 'group' => array(
'created_at' => 'generalized_time',
'last_modified' => 'generalized_time'
),
'group_membership' => array(
'created_at' => 'generalized_time', 'created_at' => 'generalized_time',
'last_modified' => 'generalized_time' 'last_modified' => 'generalized_time'
) )
); );
if (! $this->isAmbiguous($this->groupClass, $this->groupMemberAttribute)) { if (! $this->isMemberAttributeAmbiguous()) {
$rules[$this->groupClass][] = 'user_name'; $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 $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 * Return the uid for the given distinguished name
* *
@ -475,11 +552,8 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
/** /**
* Validate that the requested table exists * 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 string $table The table to validate
* @param RepositoryQuery $query An optional query to pass as context * @param RepositoryQuery $query An optional query to pass as context
* (unused by the base implementation)
* *
* @return string * @return string
* *
@ -487,12 +561,14 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
*/ */
public function requireTable($table, RepositoryQuery $query = null) public function requireTable($table, RepositoryQuery $query = null)
{ {
$table = parent::requireTable($table, $query); if ($query !== null) {
if ($table === 'group' || $table === 'group_membership') { $query->getQuery()->setBase($this->groupBaseDn);
$table = $this->groupClass; 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) public function getMemberships(User $user)
{ {
if ($this->isAmbiguous($this->groupClass, $this->groupMemberAttribute)) { if ($this->isMemberAttributeAmbiguous()) {
$queryValue = $user->getUsername(); $queryValue = $user->getUsername();
} elseif (($queryValue = $user->getAdditional('ldap_dn')) === null) { } elseif (($queryValue = $user->getAdditional('ldap_dn')) === null) {
$userQuery = $this->ds $userQuery = $this->ds
@ -535,7 +611,7 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
->setBase($this->userBaseDn) ->setBase($this->userBaseDn)
->setUsePagedResults(false); ->setUsePagedResults(false);
if ($this->userFilter) { if ($this->userFilter) {
$userQuery->where(new Expression($this->userFilter)); $userQuery->setNativeFilter($this->userFilter);
} }
if (($queryValue = $userQuery->fetchDn()) === null) { if (($queryValue = $userQuery->fetchDn()) === null) {
@ -549,7 +625,7 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
->where($this->groupMemberAttribute, $queryValue) ->where($this->groupMemberAttribute, $queryValue)
->setBase($this->groupBaseDn); ->setBase($this->groupBaseDn);
if ($this->groupFilter) { if ($this->groupFilter) {
$groupQuery->where(new Expression($this->groupFilter)); $groupQuery->setNativeFilter($this->groupFilter);
} }
$groups = array(); $groups = array();
@ -560,6 +636,21 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
return $groups; 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 * Apply the given configuration on this backend
* *
@ -607,7 +698,7 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
return $this return $this
->setGroupBaseDn($config->base_dn) ->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)) ->setGroupClass($config->get('group_class', $defaults->group_class))
->setUserClass($config->get('user_class', $defaults->user_class)) ->setUserClass($config->get('user_class', $defaults->user_class))
->setGroupNameAttribute($config->get('group_name_attribute', $defaults->group_name_attribute)) ->setGroupNameAttribute($config->get('group_name_attribute', $defaults->group_name_attribute))

View File

@ -34,4 +34,13 @@ interface UserGroupBackendInterface
* @return array * @return array
*/ */
public function getMemberships(User $user); 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 $commandName;
protected $actionName; protected $actionName;
private $config; protected $config;
private $configs; protected $configs;
protected $defaultActionName = 'default'; protected $defaultActionName = 'default';
@ -127,7 +127,7 @@ abstract class Command
public function fail($msg) public function fail($msg)
{ {
throw new IcingaException($msg); throw new IcingaException('%s', $msg);
} }
public function getDefaultActionName() public function getDefaultActionName()

View File

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

View File

@ -288,9 +288,14 @@ class DbQuery extends SimpleQuery
$expression = $this->valueToTimestamp($expression); $expression = $this->valueToTimestamp($expression);
} }
if (is_array($expression) && $sign === '=') { if (is_array($expression)) {
// TODO: Should we support this? Doesn't work for blub* if ($sign === '=') {
return $col . ' IN (' . $this->escapeForSql($expression) . ')'; 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) { } elseif ($sign === '=' && strpos($expression, '*') !== false) {
if ($expression === '*') { if ($expression === '*') {
// We'll ignore such filters as it prevents index usage and because "*" means anything, anything means // 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; 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) { foreach ($this->filters as $filter) {
if ($filter instanceof FilterExpression) { if ($filter instanceof FilterExpression) {
$columns[] = $filter->getColumn(); $column= $filter->getColumn();
if (! in_array($column, $columns, true)) {
$columns[] = $column;
}
} else { } else {
$columns += $filter->listFilteredColumns(); $columns = $filter->listFilteredColumns($columns);
} }
} }
// array_unique?
return $columns; 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) ldap_error($ds)
); );
} }
$cap = new LdapCapabilities(
$connection->cleanupAttributes( $cap = new LdapCapabilities($connection->cleanupAttributes(ldap_get_attributes($ds, $entry), $fields));
ldap_get_attributes($ds, $entry),
array_flip($fields)
)
);
return $cap; return $cap;
} }

View File

@ -7,13 +7,14 @@ use Exception;
use ArrayIterator; use ArrayIterator;
use Icinga\Application\Config; use Icinga\Application\Config;
use Icinga\Application\Logger; use Icinga\Application\Logger;
use Icinga\Application\Platform;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Data\Inspectable; use Icinga\Data\Inspectable;
use Icinga\Data\Inspection; use Icinga\Data\Inspection;
use Icinga\Data\Selectable; use Icinga\Data\Selectable;
use Icinga\Data\Sortable; 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\Exception\ProgrammingError;
use Icinga\Protocol\Ldap\LdapException; use Icinga\Protocol\Ldap\LdapException;
@ -377,14 +378,7 @@ class LdapConnection implements Selectable, Inspectable
} }
$ds = $this->getConnection(); $ds = $this->getConnection();
$results = @ldap_search( $results = $this->ldapSearch($query, array('dn'));
$ds,
$query->getBase() ?: $this->getDn(),
(string) $query,
array('dn'),
0,
0
);
if ($results === false) { if ($results === false) {
if (ldap_errno($ds) !== self::LDAP_NO_SUCH_OBJECT) { 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) public function fetchOne(LdapQuery $query, array $fields = null)
{ {
$row = (array) $this->fetchRow($query, $fields); $row = $this->fetchRow($query, $fields);
return array_shift($row) ?: false; 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 { } else {
foreach ($query->getOrder() as $rule) { foreach ($query->getOrder() as $rule) {
if (! in_array($rule[0], $fields)) { if (! in_array($rule[0], $fields, true)) {
$fields[] = $rule[0]; $fields[] = $rule[0];
} }
} }
} }
} }
$results = @ldap_search( $unfoldAttribute = $query->getUnfoldAttribute();
$ds, if ($unfoldAttribute) {
$query->getBase() ?: $this->rootDn, foreach ($query->getFilter()->listFilteredColumns() as $filterColumn) {
(string) $query, $fieldKey = array_search($filterColumn, $fields, true);
if ($fieldKey === false || is_string($fieldKey)) {
$fields[] = $filterColumn;
}
}
}
$results = $this->ldapSearch(
$query,
array_values($fields), array_values($fields),
0, // Attributes and values 0,
$serverSorting && $limit ? $offset + $limit : 0 $serverSorting && $limit ? $offset + $limit : 0
); );
if ($results === false) { if ($results === false) {
@ -727,18 +749,13 @@ class LdapConnection implements Selectable, Inspectable
$count = 0; $count = 0;
$entries = array(); $entries = array();
$entry = ldap_first_entry($ds, $results); $entry = ldap_first_entry($ds, $results);
$unfoldAttribute = $query->getUnfoldAttribute();
do { do {
if ($unfoldAttribute) { if ($unfoldAttribute) {
$rows = $this->cleanupAttributes( $rows = $this->cleanupAttributes(ldap_get_attributes($ds, $entry), $fields, $unfoldAttribute);
ldap_get_attributes($ds, $entry),
array_flip($fields),
$unfoldAttribute
);
if (is_array($rows)) { if (is_array($rows)) {
// TODO: Register the DN the same way as a section name in the ArrayDatasource! // TODO: Register the DN the same way as a section name in the ArrayDatasource!
foreach ($rows as $row) { foreach ($rows as $row) {
if ($query->getFilter()->matches($row)) {
$count += 1; $count += 1;
if (! $serverSorting || $offset === 0 || $offset < $count) { if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[] = $row; $entries[] = $row;
@ -748,6 +765,7 @@ class LdapConnection implements Selectable, Inspectable
break; break;
} }
} }
}
} else { } else {
$count += 1; $count += 1;
if (! $serverSorting || $offset === 0 || $offset < $count) { if (! $serverSorting || $offset === 0 || $offset < $count) {
@ -759,7 +777,7 @@ class LdapConnection implements Selectable, Inspectable
if (! $serverSorting || $offset === 0 || $offset < $count) { if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes( $entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes(
ldap_get_attributes($ds, $entry), ldap_get_attributes($ds, $entry),
array_flip($fields) $fields
); );
} }
} }
@ -799,8 +817,6 @@ class LdapConnection implements Selectable, Inspectable
$limit = $query->getLimit(); $limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() : 0; $offset = $query->hasOffset() ? $query->getOffset() : 0;
$queryString = (string) $query;
$base = $query->getBase() ?: $this->rootDn;
if ($fields === null) { if ($fields === null) {
$fields = $query->getColumns(); $fields = $query->getColumns();
@ -811,16 +827,25 @@ class LdapConnection implements Selectable, Inspectable
$serverSorting = false;//$this->getCapabilities()->hasOid(LdapCapabilities::LDAP_SERVER_SORT_OID); $serverSorting = false;//$this->getCapabilities()->hasOid(LdapCapabilities::LDAP_SERVER_SORT_OID);
if (! $serverSorting && $query->hasOrder()) { if (! $serverSorting && $query->hasOrder()) {
foreach ($query->getOrder() as $rule) { foreach ($query->getOrder() as $rule) {
if (! in_array($rule[0], $fields)) { if (! in_array($rule[0], $fields, true)) {
$fields[] = $rule[0]; $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; $count = 0;
$cookie = ''; $cookie = '';
$entries = array(); $entries = array();
$unfoldAttribute = $query->getUnfoldAttribute();
do { do {
// Do not request the pagination control as a critical extension, as we want the // 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 // 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( $results = $this->ldapSearch(
$ds, $query,
$base,
$queryString,
array_values($fields), array_values($fields),
0, // Attributes and values 0,
$serverSorting && $limit ? $offset + $limit : 0 $serverSorting && $limit ? $offset + $limit : 0
); );
if ($results === false) { if ($results === false) {
@ -850,14 +873,15 @@ class LdapConnection implements Selectable, Inspectable
throw new LdapException( throw new LdapException(
'LDAP query "%s" (base %s) failed. Error: %s', 'LDAP query "%s" (base %s) failed. Error: %s',
$queryString, (string) $query,
$base, $query->getBase() ?: $this->getDn(),
ldap_error($ds) ldap_error($ds)
); );
} elseif (ldap_count_entries($ds, $results) === 0) { } elseif (ldap_count_entries($ds, $results) === 0) {
if (in_array( if (in_array(
ldap_errno($ds), ldap_errno($ds),
array(static::LDAP_SIZELIMIT_EXCEEDED, static::LDAP_ADMINLIMIT_EXCEEDED) array(static::LDAP_SIZELIMIT_EXCEEDED, static::LDAP_ADMINLIMIT_EXCEEDED),
true
)) { )) {
Logger::warning( Logger::warning(
'Unable to request more than %u results. Does the server allow paged search requests? (%s)', 'Unable to request more than %u results. Does the server allow paged search requests? (%s)',
@ -872,15 +896,11 @@ class LdapConnection implements Selectable, Inspectable
$entry = ldap_first_entry($ds, $results); $entry = ldap_first_entry($ds, $results);
do { do {
if ($unfoldAttribute) { if ($unfoldAttribute) {
$rows = $this->cleanupAttributes( $rows = $this->cleanupAttributes(ldap_get_attributes($ds, $entry), $fields, $unfoldAttribute);
ldap_get_attributes($ds, $entry),
array_flip($fields),
$unfoldAttribute
);
if (is_array($rows)) { if (is_array($rows)) {
// TODO: Register the DN the same way as a section name in the ArrayDatasource! // TODO: Register the DN the same way as a section name in the ArrayDatasource!
foreach ($rows as $row) { foreach ($rows as $row) {
if ($query->getFilter()->matches($row)) {
$count += 1; $count += 1;
if (! $serverSorting || $offset === 0 || $offset < $count) { if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[] = $row; $entries[] = $row;
@ -890,6 +910,7 @@ class LdapConnection implements Selectable, Inspectable
break; break;
} }
} }
}
} else { } else {
$count += 1; $count += 1;
if (! $serverSorting || $offset === 0 || $offset < $count) { if (! $serverSorting || $offset === 0 || $offset < $count) {
@ -901,7 +922,7 @@ class LdapConnection implements Selectable, Inspectable
if (! $serverSorting || $offset === 0 || $offset < $count) { if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes( $entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes(
ldap_get_attributes($ds, $entry), 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 // pagedResultsControl with the size set to zero (0) and the cookie set to the last cookie returned by
// the server: https://www.ietf.org/rfc/rfc2696.txt // the server: https://www.ietf.org/rfc/rfc2696.txt
ldap_control_paged_result($ds, 0, false, $cookie); 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()) { 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. // 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) // This does also apply the virtual alias handling. (Since an LDAP server does not handle such)
$loweredFieldMap = array(); $loweredFieldMap = array();
foreach ($requestedFields as $name => $alias) { foreach ($requestedFields as $alias => $name) {
$loweredFieldMap[strtolower($name)] = is_string($alias) ? $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(); $cleanedAttributes = array();
@ -982,12 +1013,18 @@ class LdapConnection implements Selectable, Inspectable
$requestedAttributeName = isset($loweredFieldMap[strtolower($attribute_name)]) $requestedAttributeName = isset($loweredFieldMap[strtolower($attribute_name)])
? $loweredFieldMap[strtolower($attribute_name)] ? $loweredFieldMap[strtolower($attribute_name)]
: $attribute_name; : $attribute_name;
if (is_array($requestedAttributeName)) {
foreach ($requestedAttributeName as $requestedName) {
$cleanedAttributes[$requestedName] = $attribute_value;
}
} else {
$cleanedAttributes[$requestedAttributeName] = $attribute_value; $cleanedAttributes[$requestedAttributeName] = $attribute_value;
} }
}
// The result may not contain all requested fields, so populate the cleaned // The result may not contain all requested fields, so populate the cleaned
// result with the missing fields and their value being set to null // 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)) { if (! is_string($alias)) {
$alias = $name; $alias = $name;
} }
@ -1003,6 +1040,14 @@ class LdapConnection implements Selectable, Inspectable
&& isset($cleanedAttributes[$unfoldAttribute]) && isset($cleanedAttributes[$unfoldAttribute])
&& is_array($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]; $values = $cleanedAttributes[$unfoldAttribute];
unset($cleanedAttributes[$unfoldAttribute]); unset($cleanedAttributes[$unfoldAttribute]);
$baseRow = (object) $cleanedAttributes; $baseRow = (object) $cleanedAttributes;
@ -1010,6 +1055,10 @@ class LdapConnection implements Selectable, Inspectable
foreach ($values as $value) { foreach ($values as $value) {
$row = clone $baseRow; $row = clone $baseRow;
$row->{$unfoldAttribute} = $value; $row->{$unfoldAttribute} = $value;
foreach ($siblings as $sibling) {
$row->{$sibling} = $value;
}
$rows[] = $row; $rows[] = $row;
} }
@ -1119,6 +1168,77 @@ class LdapConnection implements Selectable, Inspectable
return $ds; 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 * Create an LDAP entry
* *
@ -1184,6 +1304,93 @@ class LdapConnection implements Selectable, Inspectable
return $dir; 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 * Inspect if this LDAP Connection is working as expected
* *

View File

@ -4,23 +4,12 @@
namespace Icinga\Protocol\Ldap; namespace Icinga\Protocol\Ldap;
use Icinga\Data\SimpleQuery; use Icinga\Data\SimpleQuery;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\NotImplementedError;
/** /**
* LDAP query class * LDAP query class
*/ */
class LdapQuery extends SimpleQuery 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 * The base dn being used for this query
* *
@ -42,12 +31,18 @@ class LdapQuery extends SimpleQuery
*/ */
protected $unfoldAttribute; protected $unfoldAttribute;
/**
* This query's native LDAP filter
*
* @var string
*/
protected $nativeFilter;
/** /**
* Initialize this query * Initialize this query
*/ */
protected function init() protected function init()
{ {
$this->filters = array();
$this->usePagedResults = false; $this->usePagedResults = false;
} }
@ -120,6 +115,29 @@ class LdapQuery extends SimpleQuery
return $this->unfoldAttribute; 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 * 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) public function from($target, array $fields = null)
{ {
$this->filters['objectClass'] = $target; $this->where('objectClass', $target);
return parent::from($target, $fields); 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 * 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 * @return string
*
* @throws LdapException In case the objectClass filter does not exist
*/ */
protected function renderFilter() public function renderFilter()
{ {
if (! isset($this->filters['objectClass'])) { $filter = $this->ds->renderFilter($this->filter);
throw new LdapException('Object class is mandatory'); if ($this->nativeFilter) {
$filter = '(&(' . $this->nativeFilter . ')' . $filter . ')';
} }
$parts = array(); return $filter;
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] . ')';
}
} }
/** /**

View File

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

View File

@ -42,15 +42,6 @@ abstract class LdapRepository extends Repository
'groupofuniquenames' => 'groupOfUniqueNames' 'groupofuniquenames' => 'groupOfUniqueNames'
); );
/**
* Object attributes whose value is not distinguished name
*
* @var array
*/
protected $ambiguousAttributes = array(
'posixGroup' => 'memberUid'
);
/** /**
* Create a new LDAP repository object * 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 * Will use the current connection's root DN if $baseDn is not given.
* @param string $attributeName *
* @param string $dn The object DN to check
* @param string $baseDn The base DN to compare the object DN with
* *
* @return bool * @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 DateTime;
use Icinga\Application\Logger; use Icinga\Application\Logger;
use Icinga\Data\Filter\Filter; use Icinga\Data\Filter\Filter;
use Icinga\Data\Filter\FilterExpression;
use Icinga\Data\Selectable; use Icinga\Data\Selectable;
use Icinga\Exception\ProgrammingError; use Icinga\Exception\ProgrammingError;
use Icinga\Exception\QueryException; use Icinga\Exception\QueryException;
@ -52,6 +53,16 @@ abstract class Repository implements Selectable
*/ */
protected $baseTable; 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 * The query columns being provided
* *
@ -255,6 +266,32 @@ abstract class Repository implements Selectable
return $this->baseTable; 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 * Return the query columns being provided
* *
@ -581,7 +618,7 @@ abstract class Repository implements Selectable
{ {
$converter = $this->getConverter($table, $name, 'persist', $query); $converter = $this->getConverter($table, $name, 'persist', $query);
if ($converter !== null) { if ($converter !== null) {
$value = $this->$converter($value); $value = $this->$converter($value, $name, $table, $query);
} }
return $value; return $value;
@ -603,7 +640,7 @@ abstract class Repository implements Selectable
{ {
$converter = $this->getConverter($table, $name, 'retrieve', $query); $converter = $this->getConverter($table, $name, 'retrieve', $query);
if ($converter !== null) { if ($converter !== null) {
$value = $this->$converter($value); $value = $this->$converter($value, $name, $table, $query);
} }
return $value; 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 string $table The table to validate
* @param RepositoryQuery $query An optional query to pass as context * @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); throw new ProgrammingError('Table "%s" not found', $table);
} }
$virtualTables = $this->getVirtualTables();
if (isset($virtualTables[$table])) {
$table = $virtualTables[$table];
}
return $table; return $table;
} }
@ -831,8 +873,8 @@ abstract class Repository implements Selectable
if ($filter->isExpression()) { if ($filter->isExpression()) {
$column = $filter->getColumn(); $column = $filter->getColumn();
$filter->setColumn($this->requireFilterColumn($table, $column, $query)); $filter->setColumn($this->requireFilterColumn($table, $column, $query, $filter));
$filter->setExpression($this->persistColumn($table, $column, $filter->getExpression())); $filter->setExpression($this->persistColumn($table, $column, $filter->getExpression(), $query));
} elseif ($filter->isChain()) { } elseif ($filter->isChain()) {
foreach ($filter->filters() as $chainOrExpression) { foreach ($filter->filters() as $chainOrExpression) {
$this->requireFilter($table, $chainOrExpression, $query, false); $this->requireFilter($table, $chainOrExpression, $query, false);
@ -862,7 +904,7 @@ abstract class Repository implements Selectable
$columns = array(); $columns = array();
foreach ($queryColumns[$table] as $alias => $column) { foreach ($queryColumns[$table] as $alias => $column) {
if (! in_array(is_string($alias) ? $alias : $column, $blacklist)) { 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 $table The table where to look for the column or alias
* @param string $name The name or alias of the column to validate * @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 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 * @return string The given column's name
* *
* @throws QueryException In case the given column is not a valid filter column * @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) { if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) {
$alias = $name; $alias = $name;

View File

@ -47,6 +47,13 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
*/ */
protected $iterator; protected $iterator;
/**
* This query's custom aliases
*
* @var array
*/
protected $customAliases;
/** /**
* Create a new repository query * Create a new repository query
* *
@ -79,8 +86,8 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
*/ */
public function from($target, array $columns = null) public function from($target, array $columns = null)
{ {
$target = $this->repository->requireTable($target, $this); $this->query = $this->repository->getDataSource()->select();
$this->query = $this->repository->getDataSource()->select()->from($target); $this->query->from($this->repository->requireTable($target, $this));
$this->query->columns($this->prepareQueryColumns($target, $columns)); $this->query->columns($this->prepareQueryColumns($target, $columns));
$this->target = $target; $this->target = $target;
return $this; return $this;
@ -123,6 +130,7 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
*/ */
protected function prepareQueryColumns($target, array $desiredColumns = null) protected function prepareQueryColumns($target, array $desiredColumns = null)
{ {
$this->customAliases = array();
if (empty($desiredColumns)) { if (empty($desiredColumns)) {
$columns = $this->repository->requireAllQueryColumns($target); $columns = $this->repository->requireAllQueryColumns($target);
} else { } else {
@ -130,9 +138,15 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
foreach ($desiredColumns as $customAlias => $columnAlias) { foreach ($desiredColumns as $customAlias => $columnAlias) {
$resolvedColumn = $this->repository->requireQueryColumn($target, $columnAlias, $this); $resolvedColumn = $this->repository->requireQueryColumn($target, $columnAlias, $this);
if ($resolvedColumn !== $columnAlias) { 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)) { } elseif (is_string($customAlias)) {
$columns[$customAlias] = $columnAlias; $columns[$customAlias] = $columnAlias;
$this->customAliases[$customAlias] = $columnAlias;
} else { } else {
$columns[] = $columnAlias; $columns[] = $columnAlias;
} }
@ -142,6 +156,24 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
return $columns; 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 * 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) public function where($column, $value = null)
{ {
$this->query->where( $this->addFilter(Filter::where($column, $value));
$this->repository->requireFilterColumn($this->target, $column, $this),
$this->repository->persistColumn($this->target, $column, $value, $this)
);
return $this; return $this;
} }
@ -466,7 +495,7 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
$result = $this->query->fetchOne(); $result = $this->query->fetchOne();
if ($result !== false && $this->repository->providesValueConversion($this->target)) { if ($result !== false && $this->repository->providesValueConversion($this->target)) {
$columns = $this->getColumns(); $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); return $this->repository->retrieveColumn($this->target, $column, $result, $this);
} }
@ -491,7 +520,12 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
$alias = $column; $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)) { if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
$columns = $this->getColumns(); $columns = $this->getColumns();
$aliases = array_keys($columns); $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)) { if ($this->repository->providesValueConversion($this->target, $column)) {
foreach ($results as & $value) { foreach ($results as & $value) {
$value = $this->repository->retrieveColumn($this->target, $column, $value, $this); $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)) { if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
$columns = $this->getColumns(); $columns = $this->getColumns();
$aliases = array_keys($columns); $aliases = array_keys($columns);
$colOne = $aliases[0] !== 0 ? $aliases[0] : $columns[0]; $colOne = $aliases[0] !== 0 ? $this->getNativeAlias($aliases[0]) : $columns[0];
$colTwo = count($aliases) < 2 ? $colOne : ($aliases[1] !== 1 ? $aliases[1] : $columns[1]); $colTwo = count($aliases) < 2 ? $colOne : (
$aliases[1] !== 1 ? $this->getNativeAlias($aliases[1]) : $columns[1]
);
if ( if (
$this->repository->providesValueConversion($this->target, $colOne) $this->repository->providesValueConversion($this->target, $colOne)
|| $this->repository->providesValueConversion($this->target, $colTwo) || $this->repository->providesValueConversion($this->target, $colTwo)
@ -587,21 +624,28 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
$alias = $column; $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) { foreach (($this->getOrder() ?: array()) as $rule) {
$nativeAlias = $this->getNativeAlias($rule[0]);
if (! array_key_exists($rule[0], $flippedColumns) && property_exists($row, $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; $updateOrder = true;
$row->{$rule[0]} = $this->repository->retrieveColumn( $row->{$rule[0]} = $this->repository->retrieveColumn(
$this->target, $this->target,
$rule[0], $nativeAlias,
$row->{$rule[0]} $row->{$rule[0]},
$this
); );
} }
} elseif (array_key_exists($rule[0], $flippedColumns)) { } elseif (array_key_exists($rule[0], $flippedColumns)) {
if ($this->repository->providesValueConversion($this->target, $rule[0])) { if ($this->repository->providesValueConversion($this->target, $nativeAlias)) {
$updateOrder = true; $updateOrder = true;
} }
} }
@ -677,7 +721,12 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
$alias = $column; $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 Filterable $filterable The filterable to create a filter editor for
* @param array $filterColumns The filter columns to offer to the user * @param array $filterColumns The filter columns to offer to the user
* @param array $searchColumns The search columns to utilize for quick searches * @param array $searchColumns The search columns to utilize for quick searches
* @param array $preserveParams The url parameters to preserve
* *
* @return $this * @return $this
* *
@ -194,25 +195,27 @@ class Controller extends ModuleActionController
protected function setupFilterControl( protected function setupFilterControl(
Filterable $filterable, Filterable $filterable,
array $filterColumns = null, 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) ->setQuery($filterable)
->preserveParams( ->ignoreParams('page') // setupPaginationControl()
'limit',
'sort',
'dir',
'format',
'view',
'user',
'group',
'backend',
'stateType',
'addColumns',
'problems',
'_dev'
)
->ignoreParams('page')
->setColumns($filterColumns) ->setColumns($filterColumns)
->setSearchColumns($searchColumns) ->setSearchColumns($searchColumns)
->handleRequest($this->getRequest()); ->handleRequest($this->getRequest());

View File

@ -171,7 +171,7 @@ class UrlParams
protected function urlEncode($value) 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) 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); $this->handleFormatRequest($dataView);
return $dataView; return $dataView;
} }

View File

@ -73,8 +73,8 @@ $stateBadges
'host_state' => 2, 'host_state' => 2,
'host_handled' => 1 'host_handled' => 1
), ),
'List %u host that is 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) in the host group "%s"', 'List %u hosts which are currently in state UNREACHABLE (Acknowledged)',
array($stats->hosts_unreachable_handled) array($stats->hosts_unreachable_handled)
) )
->add( ->add(
@ -83,8 +83,8 @@ $stateBadges
array( array(
'host_state' => 99 'host_state' => 99
), ),
'List %u host that is currently in state UNREACHABLE (Acknowledged)', 'List %u host that is currently in state PENDING',
'List %u hosts which are currently in state UNREACHABLE (Acknowledged)', 'List %u hosts which are currently in state PENDING',
array($stats->hosts_pending) array($stats->hosts_pending)
); );
echo $stateBadges->render(); 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', 's.service_object_id = so.object_id AND so.is_active = 1',
array() array()
); );
$this->select->group(array('so.object_id')); $this->select->group('so.name1');
$this->joinedVirtualTables['services'] = true; $this->joinedVirtualTables['services'] = true;
} }

View File

@ -55,14 +55,14 @@ abstract class IdoQuery extends DbQuery
protected $prefix; protected $prefix;
/** /**
* An array to map aliases to table names * An array to map aliases to column names
* *
* @var array * @var array
*/ */
protected $idxAliasColumn; protected $idxAliasColumn;
/** /**
* An array to map aliases to column names * An array to map aliases to table names
* *
* @var array * @var array
*/ */
@ -400,14 +400,14 @@ abstract class IdoQuery extends DbQuery
protected static $idoVersion; 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, * This list is being populated in case of a PostgreSQL backend only,
* to ensure case-insensitive string comparison in WHERE clauses. * to ensure case-insensitive string comparison in WHERE clauses.
* *
* @var array * @var array
*/ */
protected $columnsWithoutCollation = array(); protected $caseInsensitiveColumns;
/** /**
* Return true when the column is an aggregate column * 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 string $alias The column alias to order by
* @param int $dir The sort direction or null to use default direction * @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->requireColumn($alias);
$this->orderColumns[$columnOrAlias] = $columnOrAlias;
if ($this->isCustomvar($columnOrAlias)) { if ($this->isCustomvar($alias)) {
$columnOrAlias = $this->getCustomvarColumnName($columnOrAlias); $column = $this->getCustomvarColumnName($alias);
} elseif ($this->hasAliasName($columnOrAlias)) { } elseif ($this->hasAliasName($alias)) {
$columnOrAlias = $this->aliasToColumnName($columnOrAlias); $column = $this->aliasToColumnName($alias);
$table = $this->aliasToTableName($alias);
if (isset($this->caseInsensitiveColumns[$table][$alias])) {
$column = 'LOWER(' . $column . ')';
}
} else { } else {
Logger::info('Can\'t order by column ' . $columnOrAlias); Logger::info('Can\'t order by column ' . $alias);
return $this; 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() === '*') { if ($filter->getExpression() === '*') {
return; // Wildcard only filters are ignored so stop early here to avoid joining a table for nothing return; // Wildcard only filters are ignored so stop early here to avoid joining a table for nothing
} }
$alias = $filter->getColumn(); $alias = $filter->getColumn();
$this->requireColumn($alias); $this->requireColumn($alias);
if ($this->isCustomvar($alias)) { if ($this->isCustomvar($alias)) {
$column = $this->getCustomvarColumnName($alias); $column = $this->getCustomvarColumnName($alias);
} else { } else {
$column = $this->aliasToColumnName($alias); $column = $this->aliasToColumnName($alias);
} if (isset($this->caseInsensitiveColumns[$this->aliasToTableName($alias)][$alias])) {
if (isset($this->columnsWithoutCollation[$alias])) { $column = 'LOWER(' . $column . ')';
$expression = $filter->getExpression(); $expression = $filter->getExpression();
if (is_array($expression)) { if (is_array($expression)) {
$filter->setExpression(array_map('strtolower', $expression)); $filter->setExpression(array_map('strtolower', $expression));
} else { } else {
$filter->setExpression(strtolower($expression)); $filter->setExpression(strtolower($expression));
}
}
}
}
}
$filter->setColumn($column); $filter->setColumn($column);
} else { } else {
foreach ($filter->filters() as $filter) { foreach ($filter->filters() as $filter) {
@ -524,6 +533,7 @@ abstract class IdoQuery extends DbQuery
if ($value === '*') { if ($value === '*') {
return $this; // Wildcard only filters are ignored so stop early here to avoid joining a table for nothing return $this; // Wildcard only filters are ignored so stop early here to avoid joining a table for nothing
} }
$this->requireColumn($condition); $this->requireColumn($condition);
$col = $this->getMappedField($condition); $col = $this->getMappedField($condition);
if ($col === null) { if ($col === null) {
@ -551,29 +561,39 @@ 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 * @return bool
*/ */
public function isCaseInsensitive($aliasOrColumn) public function isCaseInsensitive($alias)
{ {
if ($this->isCustomVar($aliasOrColumn)) { if ($this->isCustomVar($alias)) {
return false; return false;
} }
$column = $this->getMappedField($aliasOrColumn) ?: $aliasOrColumn; $column = $this->getMappedField($alias);
if (! $column) { if (! $column) {
return false; return false;
} }
if (! empty($this->columnsWithoutCollation)) { if (empty($this->caseInsensitiveColumns)) {
return in_array($column, $this->columnsWithoutCollation) || strpos($column, 'LOWER') !== 0;
}
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]);
}
/** /**
* Apply oracle specific query initialization * Apply oracle specific query initialization
*/ */
@ -603,10 +623,12 @@ abstract class IdoQuery extends DbQuery
'%1$s = %2$s.object_id AND LOWER(%2$s.varname) = %3$s'; '%1$s = %2$s.object_id AND LOWER(%2$s.varname) = %3$s';
foreach ($this->columnMap as $table => & $columns) { foreach ($this->columnMap as $table => & $columns) {
foreach ($columns as $alias => & $column) { foreach ($columns as $alias => & $column) {
if (false !== $pos = strpos($column, ' COLLATE')) { // Using a regex here because COLLATE may occur anywhere in the string
$column = 'LOWER(' . substr($column, 0, $pos) . ')'; $column = preg_replace('/ COLLATE .+$/', '', $column, -1, $count);
$this->columnsWithoutCollation[$alias] = true; if ($count > 0) {
$this->caseInsensitiveColumns[$table][$alias] = true;
} }
$column = preg_replace( $column = preg_replace(
'/inet_aton\(([[:word:].]+)\)/i', '/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)', '(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_address',
'host_address6', 'host_address6',
'host_state', 'host_state',
'host_hard_state',
'host_state_type', 'host_state_type',
'host_handled', 'host_handled',
'host_unhandled', 'host_unhandled',

View File

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

View File

@ -44,41 +44,15 @@ class MacroTest extends BaseTestCase
$this->assertEquals(Macro::resolveMacros('$service.description$', $svcMock), $svcMock->service_description); $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() public function testFaultyMacros()
{ {
$hostMock = Mockery::mock('host'); $hostMock = Mockery::mock('host');
$hostMock->host_name = 'test'; $hostMock->host_name = 'test';
$hostMock->customvars = array( $hostMock->host = 'te';
'host' => 'te',
'name' => 'st'
);
$this->assertEquals( $this->assertEquals(
Macro::resolveMacros('$$HOSTNAME$ $ HOSTNAME$ $HOST$NAME$', $hostMock), '$test $ HOSTNAME$ teNAME$',
'$test $ HOSTNAME$ teNAME$' Macro::resolveMacros('$$HOSTNAME$ $ HOSTNAME$ $host$NAME$', $hostMock)
);
}
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']
); );
} }
} }

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 { form.backend-selection {
float: right; float: right;