= $this->translate('Group'); ?> | + += $this->translate('Remove'); ?> | + +
---|---|
= $this->qlink($group->group_name, 'group/show', array( + 'backend' => $backend->getName(), + 'group' => $group->group_name + ), array( + 'title' => sprintf($this->translate('Show detailed information for group %s'), $group->group_name) + )); ?> | + ++ = $this->qlink( + null, + 'group/remove', + array( + 'backend' => $backend->getName(), + 'group' => $group->group_name + ), + array( + 'title' => sprintf($this->translate('Remove group %s'), $group->group_name), + 'icon' => 'trash' + ) + ); ?> + | + +
= $this->translate('No groups found matching the filter'); ?>
+ + += $this->qlink($this->translate('Add a new group'), 'group/add', array('backend' => $backend->getName()), array( + 'icon' => 'plus', + 'data-base-target' => '_next', + 'class' => 'group-add' +)); ?> + += $this->escape($group->group_name); ?>
= $editLink; ?> += $this->translate('Created at'); ?>: = $group->created_at === null ? '-' : $this->formatDateTime($group->created_at); ?>
+= $this->translate('Last modified'); ?>: = $group->last_modified === null ? '-' : $this->formatDateTime($group->last_modified); ?>
+= $this->translate('Username'); ?> | + += $this->translate('Remove'); ?> | + +
---|---|
= $this->escape($member->user_name); ?> | + ++ getElement('user_name')->setValue($member->user_name); echo $removeForm; ?> + | + +
= $this->translate('No group member found matching the filter'); ?>
+ + + = $this->qlink($this->translate('Add a new member'), 'group/addmember', array( + 'backend' => $backend->getName(), + 'group' => $group->group_name + ), array( + 'icon' => 'plus', + 'data-base-target' => '_next', + 'class' => 'member-add' + )); ?> + += $this->translate('Username'); ?> | + += $this->translate('Remove'); ?> | + +
---|---|
= $this->qlink($user->user_name, 'user/show', array( + 'backend' => $backend->getName(), + 'user' => $user->user_name + ), array( + 'title' => sprintf($this->translate('Show detailed information about %s'), $user->user_name) + )); ?> | + ++ = $this->qlink( + null, + 'user/remove', + array( + 'backend' => $backend->getName(), + 'user' => $user->user_name + ), + array( + 'title' => sprintf($this->translate('Remove user %s'), $user->user_name), + 'icon' => 'trash' + ) + ); ?> + | + +
= $this->translate('No users found matching the filter'); ?>
+ + += $this->qlink($this->translate('Add a new user'), 'user/add', array('backend' => $backend->getName()), array( + 'icon' => 'plus', + 'data-base-target' => '_next', + 'class' => 'user-add' +)); ?> + + \ No newline at end of file diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml new file mode 100644 index 000000000..82f4c53f9 --- /dev/null +++ b/application/views/scripts/user/show.phtml @@ -0,0 +1,95 @@ +hasPermission('config/authentication/users/edit') && $backend instanceof Updatable) { + $editLink = $this->qlink( + null, + 'user/edit', + array( + 'backend' => $backend->getName(), + 'user' => $user->user_name + ), + array( + 'title' => sprintf($this->translate('Edit user %s'), $user->user_name), + 'class' => 'user-edit', + 'icon' => 'edit' + ) + ); +} + +?> += $this->escape($user->user_name); ?>
= $editLink; ?> += $this->translate('State'); ?>: = $user->is_active === null ? '-' : ($user->is_active ? $this->translate('Active') : $this->translate('Inactive')); ?>
+= $this->translate('Created at'); ?>: = $user->created_at === null ? '-' : $this->formatDateTime($user->created_at); ?>
+= $this->translate('Last modified'); ?>: = $user->last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?>
+= $this->translate('Group'); ?> | += $this->translate('Cancel', 'group.membership'); ?> | +
---|---|
+ hasPermission('config/authentication/groups/show') && $membership->backend instanceof Selectable): ?> + = $this->qlink($membership->group_name, 'group/show', array( + 'backend' => $membership->backend->getName(), + 'group' => $membership->group_name + ), array( + 'title' => sprintf($this->translate('Show detailed information for group %s'), $membership->group_name) + )); ?> + + = $this->escape($membership->group_name); ?> + + | ++ backend instanceof Reducible): ?> + = $removeForm->setAction($this->url('group/removemember', array( + 'backend' => $membership->backend->getName(), + 'group' => $membership->group_name + ))); ?> + + - + + | +
= $this->translate('No memberships found matching the filter'); ?>
+ + += $this->qlink($this->translate('Create new membership'), 'user/createmembership', array( + 'backend' => $backend->getName(), + 'user' => $user->user_name +), array( + 'icon' => 'plus', + 'data-base-target' => '_next', + 'class' => 'membership-create' +)); ?> + += $this->translate('Backend'); ?> | += $this->translate('Remove'); ?> | +
---|---|
+ = $this->qlink( + $backendName, + 'usergroupbackend/edit', + array('backend' => $backendName), + array('title' => sprintf($this->translate('Edit user group backend %s'), $backendName)) + ); ?> + | += $this->qlink( + null, + 'usergroupbackend/remove', + array('backend' => $backendName), + array( + 'title' => sprintf($this->translate('Remove user group backend %s'), $backendName), + 'icon' => 'trash' + ) + ); ?> | +
-
- *
- Connection credentials are correct and the bind is possible - *
- At least one user exists - *
- The specified userClass has the property specified by userNameAttribute - *
-
+ *
- Connection credentials are correct and the bind is possible + *
- At least one user exists + *
- The specified userClass has the property specified by userNameAttribute + *
- * $connection->select()->from('user')->where('sAMAccountName = ?', 'icinga');
- *
- *
- * @copyright Copyright (c) 2013 Icinga-Web Team
+ * array(
+ * 'table_name' => array(
+ * 'column1',
+ * 'alias1' => 'column2',
+ * 'alias2' => 'column3'
+ * )
+ * )
+ *
+ *
+ * @var array
+ */
+ protected $statementColumns;
+
+ /**
+ * An array to map table names to statement columns/aliases
+ *
+ * @var array
+ */
+ protected $statementTableMap;
+
+ /**
+ * A flattened array to map statement columns to aliases
+ *
+ * @var array
+ */
+ protected $statementColumnMap;
+
+ /**
+ * List of columns where the COLLATE SQL-instruction has been removed
+ *
+ * This list is being populated in case of a PostgreSQL backend only,
+ * to ensure case-insensitive string comparison in WHERE clauses.
+ *
+ * @var array
+ */
+ protected $columnsWithoutCollation;
+
+ /**
+ * Create a new DB repository object
+ *
+ * In case $this->queryColumns has already been initialized, this initializes
+ * $this->columnsWithoutCollation in case of a PostgreSQL connection.
+ *
+ * @param DbConnection $ds The datasource to use
+ */
+ public function __construct(DbConnection $ds)
+ {
+ parent::__construct($ds);
+
+ $this->columnsWithoutCollation = array();
+ if ($ds->getDbType() === 'pgsql' && $this->queryColumns !== null) {
+ $this->queryColumns = $this->removeCollateInstruction($this->queryColumns);
+ }
+ }
+
+ /**
+ * Return the query columns being provided
+ *
+ * Initializes $this->columnsWithoutCollation in case of a PostgreSQL connection.
+ *
+ * @return array
+ */
+ public function getQueryColumns()
+ {
+ if ($this->queryColumns === null) {
+ $this->queryColumns = parent::getQueryColumns();
+ if ($this->ds->getDbType() === 'pgsql') {
+ $this->queryColumns = $this->removeCollateInstruction($this->queryColumns);
+ }
+ }
+
+ return $this->queryColumns;
+ }
+
+ /**
+ * Return the table aliases to be applied
+ *
+ * Calls $this->initializeTableAliases() in case $this->tableAliases is null.
+ *
+ * @return array
+ */
+ public function getTableAliases()
+ {
+ if ($this->tableAliases === null) {
+ $this->tableAliases = $this->initializeTableAliases();
+ }
+
+ return $this->tableAliases;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the table aliases lazily
+ *
+ * @return array
+ */
+ protected function initializeTableAliases()
+ {
+ return array();
+ }
+
+ /**
+ * Remove each COLLATE SQL-instruction from all given query columns
+ *
+ * @param array $queryColumns
+ *
+ * @return array $queryColumns, the updated version
+ */
+ protected function removeCollateInstruction($queryColumns)
+ {
+ foreach ($queryColumns as & $columns) {
+ foreach ($columns as & $column) {
+ $column = preg_replace('/ COLLATE .+$/', '', $column, -1, $count);
+ if ($count > 0) {
+ $this->columnsWithoutCollation[] = $column;
+ }
+ }
+ }
+
+ return $queryColumns;
+ }
+
+ /**
+ * Return the given table with the datasource's prefix being prepended
+ *
+ * @param array|string $table
+ *
+ * @return array|string
+ *
+ * @throws IcingaException In case $table is not of a supported type
+ */
+ protected function prependTablePrefix($table)
+ {
+ $prefix = $this->ds->getTablePrefix();
+ if (! $prefix) {
+ return $table;
+ }
+
+ if (is_array($table)) {
+ foreach ($table as & $tableName) {
+ if (strpos($tableName, $prefix) === false) {
+ $tableName = $prefix . $tableName;
+ }
+ }
+ } elseif (is_string($table)) {
+ $table = (strpos($table, $prefix) === false ? $prefix : '') . $table;
+ } else {
+ throw new IcingaException('Table prefix handling for type "%s" is not supported', type($table));
+ }
+
+ return $table;
+ }
+
+ /**
+ * Remove the datasource's prefix from the given table name and return the remaining part
+ *
+ * @param array|string $table
+ *
+ * @return array|string
+ *
+ * @throws IcingaException In case $table is not of a supported type
+ */
+ protected function removeTablePrefix($table)
+ {
+ $prefix = $this->ds->getTablePrefix();
+ if (! $prefix) {
+ return $table;
+ }
+
+ if (is_array($table)) {
+ foreach ($table as & $tableName) {
+ if (strpos($tableName, $prefix) === 0) {
+ $tableName = str_replace($prefix, '', $tableName);
+ }
+ }
+ } elseif (is_string($table)) {
+ if (strpos($table, $prefix) === 0) {
+ $table = str_replace($prefix, '', $table);
+ }
+ } else {
+ throw new IcingaException('Table prefix handling for type "%s" is not supported', type($table));
+ }
+
+ return $table;
+ }
+
+ /**
+ * Return the given table with its alias being applied
+ *
+ * @param array|string $table
+ *
+ * @return array|string
+ */
+ protected function applyTableAlias($table)
+ {
+ $tableAliases = $this->getTableAliases();
+ if (is_array($table) || !isset($tableAliases[($nonPrefixedTable = $this->removeTablePrefix($table))])) {
+ return $table;
+ }
+
+ return array($tableAliases[$nonPrefixedTable] => $table);
+ }
+
+ /**
+ * Return the given table with its alias being cleared
+ *
+ * @param array|string $table
+ *
+ * @return string
+ *
+ * @throws IcingaException In case $table is not of a supported type
+ */
+ protected function clearTableAlias($table)
+ {
+ if (is_string($table)) {
+ return $table;
+ }
+
+ if (is_array($table)) {
+ return reset($table);
+ }
+
+ throw new IcingaException('Table alias handling for type "%s" is not supported', type($table));
+ }
+
+ /**
+ * Insert a table row with the given data
+ *
+ * @param string $table
+ * @param array $bind
+ */
+ public function insert($table, array $bind)
+ {
+ $this->ds->insert($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind));
+ }
+
+ /**
+ * Update table rows with the given data, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param array $bind
+ * @param Filter $filter
+ */
+ public function update($table, array $bind, Filter $filter = null)
+ {
+ if ($filter) {
+ $filter = $this->requireFilter($table, $filter);
+ }
+
+ $this->ds->update($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind), $filter);
+ }
+
+ /**
+ * Delete table rows, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param Filter $filter
+ */
+ public function delete($table, Filter $filter = null)
+ {
+ if ($filter) {
+ $filter = $this->requireFilter($table, $filter);
+ }
+
+ $this->ds->delete($this->prependTablePrefix($table), $filter);
+ }
+
+ /**
+ * Return the statement columns being provided
+ *
+ * Calls $this->initializeStatementColumns() in case $this->statementColumns is null.
+ *
+ * @return array
+ */
+ public function getStatementColumns()
+ {
+ if ($this->statementColumns === null) {
+ $this->statementColumns = $this->initializeStatementColumns();
+ }
+
+ return $this->statementColumns;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the statement columns lazily
+ *
+ * @return array
+ */
+ protected function initializeStatementColumns()
+ {
+ return array();
+ }
+
+ /**
+ * Return an array to map table names to statement columns/aliases
+ *
+ * @return array
+ */
+ protected function getStatementTableMap()
+ {
+ if ($this->statementTableMap === null) {
+ $this->initializeStatementMaps();
+ }
+
+ return $this->statementTableMap;
+ }
+
+ /**
+ * Return a flattened array to map statement columns to aliases
+ *
+ * @return array
+ */
+ protected function getStatementColumnMap()
+ {
+ if ($this->statementColumnMap === null) {
+ $this->initializeStatementMaps();
+ }
+
+ return $this->statementColumnMap;
+ }
+
+ /**
+ * Initialize $this->statementTableMap and $this->statementColumnMap
+ */
+ protected function initializeStatementMaps()
+ {
+ $this->statementTableMap = array();
+ $this->statementColumnMap = array();
+ foreach ($this->getStatementColumns() as $table => $columns) {
+ foreach ($columns as $alias => $column) {
+ $key = is_string($alias) ? $alias : $column;
+ if (array_key_exists($key, $this->statementTableMap)) {
+ if ($this->statementTableMap[$key] !== null) {
+ $existingTable = $this->statementTableMap[$key];
+ $existingColumn = $this->statementColumnMap[$key];
+ $this->statementTableMap[$existingTable . '.' . $key] = $existingTable;
+ $this->statementColumnMap[$existingTable . '.' . $key] = $existingColumn;
+ $this->statementTableMap[$key] = null;
+ $this->statementColumnMap[$key] = null;
+ }
+
+ $this->statementTableMap[$table . '.' . $key] = $table;
+ $this->statementColumnMap[$table . '.' . $key] = $column;
+ } else {
+ $this->statementTableMap[$key] = $table;
+ $this->statementColumnMap[$key] = $column;
+ }
+ }
+ }
+ }
+
+ /**
+ * Return whether this repository is capable of converting values
+ *
+ * This does not check whether any conversion for the given table is available, as it may be possible
+ * that columns from another table where joined in which would otherwise not being converted.
+ *
+ * @param array|string $table
+ *
+ * @return bool
+ */
+ public function providesValueConversion($_)
+ {
+ $conversionRules = $this->getConversionRules();
+ return !empty($conversionRules);
+ }
+
+ /**
+ * Return the name of the conversion method for the given alias or column name and context
+ *
+ * @param array|string $table The datasource's table
+ * @param string $name The alias or column name for which to return a conversion method
+ * @param string $context The context of the conversion: persist or retrieve
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case a conversion rule is found but not any conversion method
+ */
+ protected function getConverter($table, $name, $context)
+ {
+ if (
+ $this->validateQueryColumnAssociation($table, $name)
+ || $this->validateStatementColumnAssociation($table, $name)
+ ) {
+ $table = $this->removeTablePrefix($this->clearTableAlias($table));
+ } else {
+ $table = $this->findTableName($name);
+ if (! $table) {
+ throw new ProgrammingError('Column name validation seems to have failed. Did you require the column?');
+ }
+ }
+
+ return parent::getConverter($table, $name, $context);
+ }
+
+ /**
+ * Validate that the requested table exists
+ *
+ * This will prepend the datasource's table prefix and will apply the table's alias, if any.
+ *
+ * @param string $table The table to validate
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (unused by the base implementation)
+ *
+ * @return array|string
+ *
+ * @throws ProgrammingError In case the given table does not exist
+ */
+ public function requireTable($table, RepositoryQuery $query = null)
+ {
+ $statementColumns = $this->getStatementColumns();
+ if (! isset($statementColumns[$table])) {
+ $table = parent::requireTable($table);
+ }
+
+ return $this->prependTablePrefix($this->applyTableAlias($table));
+ }
+
+ /**
+ * Recurse the given filter, require each column for the given table and convert all values
+ *
+ * In case of a PostgreSQL connection, this applies LOWER() on the column and strtolower()
+ * on the value if a COLLATE SQL-instruction is part of the resolved column.
+ *
+ * @param string $table The table being filtered
+ * @param Filter $filter The filter to recurse
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (Directly passed through to $this->requireFilterColumn)
+ * @param bool $clone Whether to clone $filter first
+ *
+ * @return Filter The udpated filter
+ */
+ public function requireFilter($table, Filter $filter, RepositoryQuery $query = null, $clone = true)
+ {
+ $filter = parent::requireFilter($table, $filter, $query, $clone);
+
+ if ($filter->isExpression()) {
+ $column = $filter->getColumn();
+ if (in_array($column, $this->columnsWithoutCollation) && strpos($column, 'LOWER') !== 0) {
+ $filter->setColumn('LOWER(' . $column . ')');
+ $expression = $filter->getExpression();
+ if (is_array($expression)) {
+ $filter->setExpression(array_map('strtolower', $expression));
+ } else {
+ $filter->setExpression(strtolower($expression));
+ }
+ }
+ }
+
+ 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
+ */
+ public function resolveQueryColumnAlias($table, $alias)
+ {
+ return parent::resolveQueryColumnAlias($this->removeTablePrefix($this->clearTableAlias($table)), $alias);
+ }
+
+ /**
+ * Return whether the given query column name or alias is available in the given table
+ *
+ * @param array|string $table
+ * @param string $column
+ *
+ * @return bool
+ */
+ public function validateQueryColumnAssociation($table, $column)
+ {
+ return parent::validateQueryColumnAssociation(
+ $this->removeTablePrefix($this->clearTableAlias($table)),
+ $column
+ );
+ }
+
+ /**
+ * Validate that the given column is a valid query target and return it or the actual name if it's an alias
+ *
+ * Attempts to join the given column from a different table if its association to the given table cannot be
+ * verified.
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The name or alias of the column to validate
+ * @param RepositoryQuery $query An optional query to pass as context,
+ * if not given no join will be attempted
+ *
+ * @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)
+ {
+ if ($query === null || $this->validateQueryColumnAssociation($table, $name)) {
+ return parent::requireQueryColumn($table, $name, $query);
+ }
+
+ return $this->joinColumn($name, $table, $query);
+ }
+
+ /**
+ * Validate that the given column is a valid filter target and return it or the actual name if it's an alias
+ *
+ * Attempts to join the given column from a different table if its association to the given table cannot be
+ * verified.
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The name or alias of the column to validate
+ * @param RepositoryQuery $query An optional query to pass as context,
+ * if not given the column is considered being used for a statement filter
+ *
+ * @return string The given column's name
+ *
+ * @throws QueryException In case the given column is not a valid filter column
+ */
+ public function requireFilterColumn($table, $name, RepositoryQuery $query = null)
+ {
+ if ($query === null) {
+ return $this->requireStatementColumn($table, $name);
+ }
+
+ if ($this->validateQueryColumnAssociation($table, $name)) {
+ return parent::requireFilterColumn($table, $name, $query);
+ }
+
+ return $this->joinColumn($name, $table, $query);
+ }
+
+ /**
+ * Return the statement column name for the given alias or null in case the alias does not exist
+ *
+ * @param string $table
+ * @param string $alias
+ *
+ * @return string|null
+ */
+ public function resolveStatementColumnAlias($table, $alias)
+ {
+ $statementColumnMap = $this->getStatementColumnMap();
+ if (isset($statementColumnMap[$alias])) {
+ return $statementColumnMap[$alias];
+ }
+
+ $prefixedAlias = $this->removeTablePrefix($table) . '.' . $alias;
+ if (isset($statementColumnMap[$prefixedAlias])) {
+ return $statementColumnMap[$prefixedAlias];
+ }
+ }
+
+ /**
+ * Return whether the given alias or statement column name is available in the given table
+ *
+ * @param string $table
+ * @param string $alias
+ *
+ * @return bool
+ */
+ public function validateStatementColumnAssociation($table, $alias)
+ {
+ $statementTableMap = $this->getStatementTableMap();
+ if (isset($statementTableMap[$alias])) {
+ return $statementTableMap[$alias] === $this->removeTablePrefix($table);
+ }
+
+ $prefixedAlias = $this->removeTablePrefix($table) . '.' . $alias;
+ return isset($statementTableMap[$prefixedAlias]);
+ }
+
+ /**
+ * Return whether the given column name or alias of the given table is a valid statement column
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The column name or alias to check
+ *
+ * @return bool
+ */
+ public function hasStatementColumn($table, $name)
+ {
+ if (
+ $this->resolveStatementColumnAlias($table, $name) === null
+ || !$this->validateStatementColumnAssociation($table, $name)
+ ) {
+ return parent::hasStatementColumn($table, $name);
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate that the given column is a valid statement column and return it or the actual name if it's an alias
+ *
+ * @param string $table The table for which to require the column
+ * @param string $name The name or alias of the column to validate
+ *
+ * @return string The given column's name
+ *
+ * @throws StatementException In case the given column is not a statement column
+ */
+ public function requireStatementColumn($table, $name)
+ {
+ if (($column = $this->resolveStatementColumnAlias($table, $name)) === null) {
+ return parent::requireStatementColumn($table, $name);
+ }
+
+ if (! $this->validateStatementColumnAssociation($table, $name)) {
+ throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
+ }
+
+ return $column;
+ }
+
+ /**
+ * Join alias or column $name into $table using $query
+ *
+ * Attempts to find a valid table for the given alias or column name and a method labelled join
+ * to process the actual join logic. If neither of those is found, ProgrammingError will be thrown.
+ * The method is called with the same parameters but in reversed order.
+ *
+ * @param string $name The alias or column name to join into $target
+ * @param array|string $target The table to join $name into
+ * @param RepositoryQUery $query The query to apply the JOIN-clause on
+ *
+ * @return string The resolved alias or $name
+ *
+ * @throws ProgrammingError In case no valid table or join-method is found
+ */
+ public function joinColumn($name, $target, RepositoryQuery $query)
+ {
+ $tableName = $this->findTableName($name);
+ if (! $tableName) {
+ throw new ProgrammingError(
+ 'Unable to find a valid table for column "%s" to join into "%s"',
+ $name,
+ $this->removeTablePrefix($this->clearTableAlias($target))
+ );
+ }
+
+ $column = $this->resolveQueryColumnAlias($tableName, $name);
+
+ $prefixedTableName = $this->prependTablePrefix($tableName);
+ if ($query->getQuery()->hasJoinedTable($prefixedTableName)) {
+ return $column;
+ }
+
+ $joinMethod = 'join' . String::cname($tableName);
+ if (! method_exists($this, $joinMethod)) {
+ throw new ProgrammingError(
+ 'Unable to join table "%s" into "%s". Method "%s" not found',
+ $tableName,
+ $this->removeTablePrefix($this->clearTableAlias($target)),
+ $joinMethod
+ );
+ }
+
+ $this->$joinMethod($query, $target, $name);
+ return $column;
+ }
+
+ /**
+ * Return the table name for the given alias or column name
+ *
+ * @param string $column
+ *
+ * @return string|null null in case no table is found
+ */
+ protected function findTableName($column)
+ {
+ $aliasTableMap = $this->getAliasTableMap();
+ if (isset($aliasTableMap[$column])) {
+ return $aliasTableMap[$column];
+ }
+
+ // TODO(jom): Elaborate whether it makes sense to throw ProgrammingError
+ // instead (duplicate aliases in different tables?)
+ foreach ($aliasTableMap as $alias => $table) {
+ if (strpos($alias, '.') !== false) {
+ list($_, $alias) = explode('.', $column, 2);
+ if ($alias === $column) {
+ return $table;
+ }
+ }
+ }
+ }
+}
diff --git a/library/Icinga/Repository/IniRepository.php b/library/Icinga/Repository/IniRepository.php
new file mode 100644
index 000000000..3c73464e5
--- /dev/null
+++ b/library/Icinga/Repository/IniRepository.php
@@ -0,0 +1,186 @@
+
+ * Insert, update and delete capabilities
+ *
+ */
+abstract class IniRepository extends Repository implements Extensible, Updatable, Reducible
+{
+ /**
+ * The datasource being used
+ *
+ * @var Config
+ */
+ protected $ds;
+
+ /**
+ * Create a new INI repository object
+ *
+ * @param Config $ds The data source to use
+ *
+ * @throws ProgrammingError In case the given data source does not provide a valid key column
+ */
+ public function __construct(Config $ds)
+ {
+ parent::__construct($ds); // First! Due to init().
+
+ if (! $ds->getConfigObject()->getKeyColumn()) {
+ throw new ProgrammingError('INI repositories require their data source to provide a valid key column');
+ }
+ }
+
+ /**
+ * Insert the given data for the given target
+ *
+ * $data must provide a proper value for the data source's key column.
+ *
+ * @param string $target
+ * @param array $data
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function insert($target, array $data)
+ {
+ $newData = $this->requireStatementColumns($target, $data);
+ $section = $this->extractSectionName($newData);
+
+ if ($this->ds->hasSection($section)) {
+ throw new StatementException(t('Cannot insert. Section "%s" does already exist'), $section);
+ }
+
+ $this->ds->setSection($section, $newData);
+
+ try {
+ $this->ds->saveIni();
+ } catch (Exception $e) {
+ throw new StatementException(t('Failed to insert. An error occurred: %s'), $e->getMessage());
+ }
+ }
+
+ /**
+ * Update the target with the given data and optionally limit the affected entries by using a filter
+ *
+ * @param string $target
+ * @param array $data
+ * @param Filter $filter
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function update($target, array $data, Filter $filter = null)
+ {
+ $newData = $this->requireStatementColumns($target, $data);
+ $keyColumn = $this->ds->getConfigObject()->getKeyColumn();
+ if ($filter === null && isset($newData[$keyColumn])) {
+ throw new StatementException(
+ t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
+ $keyColumn
+ );
+ }
+
+ if ($filter !== null) {
+ $filter = $this->requireFilter($target, $filter);
+ }
+
+ $newSection = null;
+ foreach (iterator_to_array($this->ds) as $section => $config) {
+ if ($filter !== null && !$filter->matches($config)) {
+ continue;
+ }
+
+ if ($newSection !== null) {
+ throw new StatementException(
+ t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
+ $keyColumn
+ );
+ }
+
+ foreach ($newData as $column => $value) {
+ if ($column === $keyColumn) {
+ $newSection = $value;
+ } else {
+ $config->$column = $value;
+ }
+ }
+
+ if ($newSection) {
+ if ($this->ds->hasSection($newSection)) {
+ throw new StatementException(t('Cannot update. Section "%s" does already exist'), $newSection);
+ }
+
+ $this->ds->removeSection($section)->setSection($newSection, $config);
+ } else {
+ $this->ds->setSection($section, $config);
+ }
+ }
+
+ try {
+ $this->ds->saveIni();
+ } catch (Exception $e) {
+ throw new StatementException(t('Failed to update. An error occurred: %s'), $e->getMessage());
+ }
+ }
+
+ /**
+ * Delete entries in the given target, optionally limiting the affected entries by using a filter
+ *
+ * @param string $target
+ * @param Filter $filter
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function delete($target, Filter $filter = null)
+ {
+ if ($filter !== null) {
+ $filter = $this->requireFilter($target, $filter);
+ }
+
+ foreach (iterator_to_array($this->ds) as $section => $config) {
+ if ($filter === null || $filter->matches($config)) {
+ $this->ds->removeSection($section);
+ }
+ }
+
+ try {
+ $this->ds->saveIni();
+ } catch (Exception $e) {
+ throw new StatementException(t('Failed to delete. An error occurred: %s'), $e->getMessage());
+ }
+ }
+
+ /**
+ * Extract and return the section name off of the given $data
+ *
+ * @param array $data
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case no valid section name is available
+ */
+ protected function extractSectionName(array & $data)
+ {
+ $keyColumn = $this->ds->getConfigObject()->getKeyColumn();
+ if (! isset($data[$keyColumn])) {
+ throw new ProgrammingError('$data does not provide a value for key column "%s"', $keyColumn);
+ }
+
+ $section = $data[$keyColumn];
+ unset($data[$keyColumn]);
+ return $section;
+ }
+}
diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php
new file mode 100644
index 000000000..803f4958f
--- /dev/null
+++ b/library/Icinga/Repository/Repository.php
@@ -0,0 +1,861 @@
+
+ * Concrete implementations need to initialize Repository::$queryColumns
+ * The datasource passed to a repository must implement the Selectable interface
+ * The datasource must yield an instance of Queryable when its select() method is called
+ *
+ */
+abstract class Repository implements Selectable
+{
+ /**
+ * The format to use when converting values of type date_time
+ */
+ const DATETIME_FORMAT = 'd/m/Y g:i A';
+
+ /**
+ * The name of this repository
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * The datasource being used
+ *
+ * @var Selectable
+ */
+ protected $ds;
+
+ /**
+ * The base table name this repository is responsible for
+ *
+ * This will be automatically set to the first key of $queryColumns if not explicitly set.
+ *
+ * @var string
+ */
+ protected $baseTable;
+
+ /**
+ * The query columns being provided
+ *
+ * This must be initialized by concrete repository implementations, in the following format
+ *
+ * array(
+ * 'baseTable' => array(
+ * 'column1',
+ * 'alias1' => 'column2',
+ * 'alias2' => 'column3'
+ * )
+ * )
+ *
+ *
+ * @var array
+ */
+ protected $queryColumns;
+
+ /**
+ * The columns (or aliases) which are not permitted to be queried. (by design)
+ *
+ * @var array An array of strings
+ */
+ protected $filterColumns;
+
+ /**
+ * The default sort rules to be applied on a query
+ *
+ * This may be initialized by concrete repository implementations, in the following format
+ *
+ * array(
+ * 'alias_or_column_name' => array(
+ * 'order' => 'asc'
+ * ),
+ * 'alias_or_column_name' => array(
+ * 'columns' => array(
+ * 'once_more_the_alias_or_column_name_as_in_the_parent_key',
+ * 'an_additional_alias_or_column_name_with_a_specific_direction asc'
+ * ),
+ * 'order' => 'desc'
+ * ),
+ * 'alias_or_column_name' => array(
+ * 'columns' => array('a_different_alias_or_column_name_designated_to_act_as_the_only_sort_column')
+ * // Ascendant sort by default
+ * )
+ * )
+ *
+ * Note that it's mandatory to supply the alias name in case there is one.
+ *
+ * @var array
+ */
+ protected $sortRules;
+
+ /**
+ * The value conversion rules to apply on a query or statement
+ *
+ * This may be initialized by concrete repository implementations and describes for which aliases or column
+ * names what type of conversion is available. For entries, where the key is the alias/column and the value
+ * is the type identifier, the repository attempts to find a conversion method for the alias/column first and,
+ * if none is found, then for the type. If an entry only provides a value, which is the alias/column, the
+ * repository only attempts to find a conversion method for the alias/column. The name of a conversion method
+ * is expected to be declared using lowerCamelCase. (e.g. user_name will be translated to persistUserName and
+ * groupname will be translated to retrieveGroupname)
+ *
+ * @var array
+ */
+ protected $conversionRules;
+
+ /**
+ * An array to map table names to aliases
+ *
+ * @var array
+ */
+ protected $aliasTableMap;
+
+ /**
+ * A flattened array to map query columns to aliases
+ *
+ * @var array
+ */
+ protected $aliasColumnMap;
+
+ /**
+ * Create a new repository object
+ *
+ * @param Selectable $ds The datasource to use
+ */
+ public function __construct(Selectable $ds)
+ {
+ $this->ds = $ds;
+ $this->aliasTableMap = array();
+ $this->aliasColumnMap = array();
+
+ $this->init();
+ }
+
+ /**
+ * Initialize this repository
+ *
+ * Supposed to be overwritten by concrete repository implementations.
+ */
+ protected function init()
+ {
+
+ }
+
+ /**
+ * Set this repository's name
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * Return this repository's name
+ *
+ * In case no name has been explicitly set yet, the class name is returned.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name ?: __CLASS__;
+ }
+
+ /**
+ * Return the datasource being used
+ *
+ * @return Selectable
+ */
+ public function getDataSource()
+ {
+ return $this->ds;
+ }
+
+ /**
+ * Return the base table name this repository is responsible for
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case no base table name has been set and
+ * $this->queryColumns does not provide one either
+ */
+ public function getBaseTable()
+ {
+ if ($this->baseTable === null) {
+ $queryColumns = $this->getQueryColumns();
+ reset($queryColumns);
+ $this->baseTable = key($queryColumns);
+ if (is_int($this->baseTable) || !is_array($queryColumns[$this->baseTable])) {
+ throw new ProgrammingError('"%s" is not a valid base table', $this->baseTable);
+ }
+ }
+
+ return $this->baseTable;
+ }
+
+ /**
+ * Return the query columns being provided
+ *
+ * Calls $this->initializeQueryColumns() in case $this->queryColumns is null.
+ *
+ * @return array
+ */
+ public function getQueryColumns()
+ {
+ if ($this->queryColumns === null) {
+ $this->queryColumns = $this->initializeQueryColumns();
+ }
+
+ return $this->queryColumns;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the query columns lazily
+ *
+ * @return array
+ */
+ protected function initializeQueryColumns()
+ {
+ return array();
+ }
+
+ /**
+ * Return the columns (or aliases) which are not permitted to be queried
+ *
+ * Calls $this->initializeFilterColumns() in case $this->filterColumns is null.
+ *
+ * @return array
+ */
+ public function getFilterColumns()
+ {
+ if ($this->filterColumns === null) {
+ $this->filterColumns = $this->initializeFilterColumns();
+ }
+
+ return $this->filterColumns;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the filter columns lazily
+ *
+ * @return array
+ */
+ protected function initializeFilterColumns()
+ {
+ return array();
+ }
+
+ /**
+ * Return the default sort rules to be applied on a query
+ *
+ * Calls $this->initializeSortRules() in case $this->sortRules is null.
+ *
+ * @return array
+ */
+ public function getSortRules()
+ {
+ if ($this->sortRules === null) {
+ $this->sortRules = $this->initializeSortRules();
+ }
+
+ return $this->sortRules;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the sort rules lazily
+ *
+ * @return array
+ */
+ protected function initializeSortRules()
+ {
+ return array();
+ }
+
+ /**
+ * Return the value conversion rules to apply on a query
+ *
+ * Calls $this->initializeConversionRules() in case $this->conversionRules is null.
+ *
+ * @return array
+ */
+ public function getConversionRules()
+ {
+ if ($this->conversionRules === null) {
+ $this->conversionRules = $this->initializeConversionRules();
+ }
+
+ return $this->conversionRules;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the conversion rules lazily
+ *
+ * @return array
+ */
+ protected function initializeConversionRules()
+ {
+ return array();
+ }
+
+ /**
+ * Return an array to map table names to aliases
+ *
+ * @return array
+ */
+ protected function getAliasTableMap()
+ {
+ if (empty($this->aliasTableMap)) {
+ $this->initializeAliasMaps();
+ }
+
+ return $this->aliasTableMap;
+ }
+
+ /**
+ * Return a flattened array to map query columns to aliases
+ *
+ * @return array
+ */
+ protected function getAliasColumnMap()
+ {
+ if (empty($this->aliasColumnMap)) {
+ $this->initializeAliasMaps();
+ }
+
+ return $this->aliasColumnMap;
+ }
+
+ /**
+ * Initialize $this->aliasTableMap and $this->aliasColumnMap
+ *
+ * @throws ProgrammingError In case $this->queryColumns does not provide any column information
+ */
+ protected function initializeAliasMaps()
+ {
+ $queryColumns = $this->getQueryColumns();
+ if (empty($queryColumns)) {
+ throw new ProgrammingError('Repositories are required to initialize $this->queryColumns first');
+ }
+
+ foreach ($queryColumns as $table => $columns) {
+ foreach ($columns as $alias => $column) {
+ if (! is_string($alias)) {
+ $key = $column;
+ } else {
+ $key = $alias;
+ $column = preg_replace('~\n\s*~', ' ', $column);
+ }
+
+ if (array_key_exists($key, $this->aliasTableMap)) {
+ if ($this->aliasTableMap[$key] !== null) {
+ $existingTable = $this->aliasTableMap[$key];
+ $existingColumn = $this->aliasColumnMap[$key];
+ $this->aliasTableMap[$existingTable . '.' . $key] = $existingTable;
+ $this->aliasColumnMap[$existingTable . '.' . $key] = $existingColumn;
+ $this->aliasTableMap[$key] = null;
+ $this->aliasColumnMap[$key] = null;
+ }
+
+ $this->aliasTableMap[$table . '.' . $key] = $table;
+ $this->aliasColumnMap[$table . '.' . $key] = $column;
+ } else {
+ $this->aliasTableMap[$key] = $table;
+ $this->aliasColumnMap[$key] = $column;
+ }
+ }
+ }
+ }
+
+ /**
+ * Return a new query for the given columns
+ *
+ * @param array $columns The desired columns, if null all columns will be queried
+ *
+ * @return RepositoryQuery
+ */
+ public function select(array $columns = null)
+ {
+ $query = new RepositoryQuery($this);
+ $query->from($this->getBaseTable(), $columns);
+ return $query;
+ }
+
+ /**
+ * Return whether this repository is capable of converting values for the given table
+ *
+ * @param string $table
+ *
+ * @return bool
+ */
+ public function providesValueConversion($table)
+ {
+ $conversionRules = $this->getConversionRules();
+ return !empty($conversionRules) && isset($conversionRules[$table]);
+ }
+
+ /**
+ * Convert a value supposed to be transmitted to the data source
+ *
+ * @param string $table The table where to persist the value
+ * @param string $name The alias or column name
+ * @param mixed $value The value to convert
+ *
+ * @return mixed If conversion was possible, the converted value, otherwise the unchanged value
+ */
+ public function persistColumn($table, $name, $value)
+ {
+ $converter = $this->getConverter($table, $name, 'persist');
+ if ($converter !== null) {
+ $value = $this->$converter($value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Convert a value which was fetched from the data source
+ *
+ * @param string $table The table the value has been fetched from
+ * @param string $name The alias or column name
+ * @param mixed $value The value to convert
+ *
+ * @return mixed If conversion was possible, the converted value, otherwise the unchanged value
+ */
+ public function retrieveColumn($table, $name, $value)
+ {
+ $converter = $this->getConverter($table, $name, 'retrieve');
+ if ($converter !== null) {
+ $value = $this->$converter($value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Return the name of the conversion method for the given alias or column name and context
+ *
+ * @param string $table The datasource's table
+ * @param string $name The alias or column name for which to return a conversion method
+ * @param string $context The context of the conversion: persist or retrieve
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case a conversion rule is found but not any conversion method
+ */
+ protected function getConverter($table, $name, $context)
+ {
+ $conversionRules = $this->getConversionRules();
+ if (! isset($conversionRules[$table])) {
+ return;
+ }
+
+ $tableRules = $conversionRules[$table];
+
+ // Check for a conversion method for the alias/column first
+ if (array_key_exists($name, $tableRules) || in_array($name, $tableRules)) {
+ $methodName = $context . join('', array_map('ucfirst', explode('_', $name)));
+ if (method_exists($this, $methodName)) {
+ return $methodName;
+ }
+ }
+
+ // The conversion method for the type is just a fallback, but it is required to exist if defined
+ if (isset($tableRules[$name])) {
+ $identifier = join('', array_map('ucfirst', explode('_', $tableRules[$name])));
+ if (! method_exists($this, $context . $identifier)) {
+ // Do not throw an error in case at least one conversion method exists
+ if (! method_exists($this, ($context === 'persist' ? 'retrieve' : 'persist') . $identifier)) {
+ throw new ProgrammingError(
+ 'Cannot find any conversion method for type "%s"'
+ . '. Add a proper conversion method or remove the type definition',
+ $tableRules[$name]
+ );
+ }
+
+ Logger::debug(
+ 'Conversion method "%s" for type definition "%s" does not exist in repository "%s".',
+ $context . $identifier,
+ $tableRules[$name],
+ $this->getName()
+ );
+ } else {
+ return $context . $identifier;
+ }
+ }
+ }
+
+ /**
+ * Convert a timestamp or DateTime object to a string formatted using static::DATETIME_FORMAT
+ *
+ * @param mixed $value
+ *
+ * @return string
+ */
+ protected function persistDateTime($value)
+ {
+ if (is_numeric($value)) {
+ $value = date(static::DATETIME_FORMAT, $value);
+ } elseif ($value instanceof DateTime) {
+ $value = date(static::DATETIME_FORMAT, $value->getTimestamp()); // Using date here, to ignore any timezone
+ } elseif ($value !== null) {
+ throw new ProgrammingError(
+ 'Cannot persist value "%s" as type date_time. It\'s not a timestamp or DateTime object',
+ $value
+ );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Convert a string formatted using static::DATETIME_FORMAT to a unix timestamp
+ *
+ * @param string $value
+ *
+ * @return int
+ */
+ protected function retrieveDateTime($value)
+ {
+ if (is_numeric($value)) {
+ $value = (int) $value;
+ } elseif (is_string($value)) {
+ $dateTime = DateTime::createFromFormat(static::DATETIME_FORMAT, $value);
+ if ($dateTime === false) {
+ Logger::debug(
+ 'Unable to parse string "%s" as type date_time with format "%s" in repository "%s"',
+ $value,
+ static::DATETIME_FORMAT,
+ $this->getName()
+ );
+ $value = null;
+ } else {
+ $value = $dateTime->getTimestamp();
+ }
+ } elseif ($value !== null) {
+ throw new ProgrammingError(
+ 'Cannot retrieve value "%s" as type date_time. It\'s not a integer or (numeric) string',
+ $value
+ );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Convert the given array to an comma separated string
+ *
+ * @param array|string $value
+ *
+ * @return string
+ */
+ protected function persistCommaSeparatedString($value)
+ {
+ if (is_array($value)) {
+ $value = join(',', array_map('trim', $value));
+ } elseif ($value !== null && !is_string($value)) {
+ throw new ProgrammingError('Cannot persist value "%s" as comma separated string', $value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Convert the given comma separated string to an array
+ *
+ * @param string $value
+ *
+ * @return array
+ */
+ protected function retrieveCommaSeparatedString($value)
+ {
+ if ($value && is_string($value)) {
+ $value = String::trimSplit($value);
+ } elseif ($value !== null) {
+ throw new ProgrammingError('Cannot retrieve value "%s" as array. It\'s not a string', $value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Validate that the requested table exists
+ *
+ * @param string $table The table to validate
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (unused by the base implementation)
+ *
+ * @return string The table's name, may differ from the given one
+ *
+ * @throws ProgrammingError In case the given table does not exist
+ */
+ public function requireTable($table, RepositoryQuery $query = null)
+ {
+ $queryColumns = $this->getQueryColumns();
+ if (! isset($queryColumns[$table])) {
+ throw new ProgrammingError('Table "%s" not found', $table);
+ }
+
+ return $table;
+ }
+
+ /**
+ * Recurse the given filter, require each column for the given table and convert all values
+ *
+ * @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)
+ {
+ if ($clone) {
+ $filter = clone $filter;
+ }
+
+ if ($filter->isExpression()) {
+ $column = $filter->getColumn();
+ $filter->setColumn($this->requireFilterColumn($table, $column, $query));
+ $filter->setExpression($this->persistColumn($table, $column, $filter->getExpression()));
+ } elseif ($filter->isChain()) {
+ foreach ($filter->filters() as $chainOrExpression) {
+ $this->requireFilter($table, $chainOrExpression, $query, false);
+ }
+ }
+
+ return $filter;
+ }
+
+ /**
+ * Return this repository's query columns of the given table mapped to their respective aliases
+ *
+ * @param string $table
+ *
+ * @return array
+ *
+ * @throws ProgrammingError In case $table does not exist
+ */
+ public function requireAllQueryColumns($table)
+ {
+ $queryColumns = $this->getQueryColumns();
+ if (! array_key_exists($table, $queryColumns)) {
+ throw new ProgrammingError('Table name "%s" not found', $table);
+ }
+
+ $filterColumns = $this->getFilterColumns();
+ $columns = array();
+ foreach ($queryColumns[$table] as $alias => $column) {
+ if (! in_array(is_string($alias) ? $alias : $column, $filterColumns)) {
+ $columns[$alias] = $column;
+ }
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Return the query column name for the given alias or null in case the alias does not exist
+ *
+ * @param string $table
+ * @param string $alias
+ *
+ * @return string|null
+ */
+ public function resolveQueryColumnAlias($table, $alias)
+ {
+ $aliasColumnMap = $this->getAliasColumnMap();
+ if (isset($aliasColumnMap[$alias])) {
+ return $aliasColumnMap[$alias];
+ }
+
+ $prefixedAlias = $table . '.' . $alias;
+ if (isset($aliasColumnMap[$prefixedAlias])) {
+ return $aliasColumnMap[$prefixedAlias];
+ }
+ }
+
+ /**
+ * Return whether the given alias or query column name is available in the given table
+ *
+ * @param string $table
+ * @param string $alias
+ *
+ * @return bool
+ */
+ public function validateQueryColumnAssociation($table, $alias)
+ {
+ $aliasTableMap = $this->getAliasTableMap();
+ if (isset($aliasTableMap[$alias])) {
+ return $aliasTableMap[$alias] === $table;
+ }
+
+ $prefixedAlias = $table . '.' . $alias;
+ return isset($aliasTableMap[$prefixedAlias]);
+ }
+
+ /**
+ * Return whether the given column name or alias is a valid query column
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The column name or alias to check
+ *
+ * @return bool
+ */
+ public function hasQueryColumn($table, $name)
+ {
+ if (in_array($name, $this->getFilterColumns())) {
+ return false;
+ }
+
+ return $this->resolveQueryColumnAlias($table, $name) !== null
+ && $this->validateQueryColumnAssociation($table, $name);
+ }
+
+ /**
+ * 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 (unused by the base implementation)
+ *
+ * @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)
+ {
+ if (in_array($name, $this->getFilterColumns())) {
+ throw new QueryException(t('Filter column "%s" cannot be queried'), $name);
+ }
+
+ if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
+ throw new QueryException(t('Query column "%s" not found'), $name);
+ }
+
+ if (! $this->validateQueryColumnAssociation($table, $name)) {
+ throw new QueryException(t('Query column "%s" not found in table "%s"'), $name, $table);
+ }
+
+ return $column;
+ }
+
+ /**
+ * Return whether the given column name or alias is a valid filter column
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The column name or alias to check
+ *
+ * @return bool
+ */
+ public function hasFilterColumn($table, $name)
+ {
+ return $this->resolveQueryColumnAlias($table, $name) !== null
+ && $this->validateQueryColumnAssociation($table, $name);
+ }
+
+ /**
+ * Validate that the given column is a valid filter 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 (unused by the base implementation)
+ *
+ * @return string The given column's name
+ *
+ * @throws QueryException In case the given column is not a valid filter column
+ */
+ public function requireFilterColumn($table, $name, RepositoryQuery $query = null)
+ {
+ if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
+ throw new QueryException(t('Filter column "%s" not found'), $name);
+ }
+
+ if (! $this->validateQueryColumnAssociation($table, $name)) {
+ throw new QueryException(t('Filter column "%s" not found in table "%s"'), $name, $table);
+ }
+
+ return $column;
+ }
+
+ /**
+ * Return whether the given column name or alias of the given table is a valid statement column
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The column name or alias to check
+ *
+ * @return bool
+ */
+ public function hasStatementColumn($table, $name)
+ {
+ return $this->hasQueryColumn($table, $name);
+ }
+
+ /**
+ * Validate that the given column is a valid statement column and return it or the actual name if it's an alias
+ *
+ * @param string $table The table for which to require the column
+ * @param string $name The name or alias of the column to validate
+ *
+ * @return string The given column's name
+ *
+ * @throws StatementException In case the given column is not a statement column
+ */
+ public function requireStatementColumn($table, $name)
+ {
+ if (in_array($name, $this->filterColumns)) {
+ throw new StatementException('Filter column "%s" cannot be referenced in a statement', $name);
+ }
+
+ if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
+ throw new StatementException('Statement column "%s" not found', $name);
+ }
+
+ if (! $this->validateQueryColumnAssociation($table, $name)) {
+ throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
+ }
+
+ return $column;
+ }
+
+ /**
+ * Resolve the given aliases or column names of the given table supposed to be persisted and convert their values
+ *
+ * @param string $table
+ * @param array $data
+ *
+ * @return array
+ */
+ public function requireStatementColumns($table, array $data)
+ {
+ $resolved = array();
+ foreach ($data as $alias => $value) {
+ $resolved[$this->requireStatementColumn($table, $alias)] = $this->persistColumn($table, $alias, $value);
+ }
+
+ return $resolved;
+ }
+}
diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php
new file mode 100644
index 000000000..c5e9ac9ed
--- /dev/null
+++ b/library/Icinga/Repository/RepositoryQuery.php
@@ -0,0 +1,589 @@
+repository = $repository;
+ }
+
+ /**
+ * Return the real query being used
+ *
+ * @return QueryInterface
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Set where to fetch which columns
+ *
+ * This notifies the repository about each desired query column.
+ *
+ * @param mixed $target The target from which to fetch the columns
+ * @param array $columns If null or an empty array, all columns will be fetched
+ *
+ * @return $this
+ */
+ public function from($target, array $columns = null)
+ {
+ $target = $this->repository->requireTable($target, $this);
+ $this->query = $this->repository->getDataSource()->select()->from($target);
+ $this->query->columns($this->prepareQueryColumns($target, $columns));
+ $this->target = $target;
+ return $this;
+ }
+
+ /**
+ * Return the columns to fetch
+ *
+ * @return array
+ */
+ public function getColumns()
+ {
+ return $this->query->getColumns();
+ }
+
+ /**
+ * Set which columns to fetch
+ *
+ * This notifies the repository about each desired query column.
+ *
+ * @param array $columns If null or an empty array, all columns will be fetched
+ *
+ * @return $this
+ */
+ public function columns(array $columns)
+ {
+ $this->query->columns($this->prepareQueryColumns($this->target, $columns));
+ return $this;
+ }
+
+ /**
+ * Resolve the given columns supposed to be fetched
+ *
+ * This notifies the repository about each desired query column.
+ *
+ * @param mixed $target The target where to look for each column
+ * @param array $desiredColumns Pass null or an empty array to require all query columns
+ *
+ * @return array The desired columns indexed by their respective alias
+ */
+ protected function prepareQueryColumns($target, array $desiredColumns = null)
+ {
+ if (empty($desiredColumns)) {
+ $columns = $this->repository->requireAllQueryColumns($target);
+ } else {
+ $columns = array();
+ foreach ($desiredColumns as $customAlias => $columnAlias) {
+ $resolvedColumn = $this->repository->requireQueryColumn($target, $columnAlias, $this);
+ if ($resolvedColumn !== $columnAlias) {
+ $columns[is_string($customAlias) ? $customAlias : $columnAlias] = $resolvedColumn;
+ } elseif (is_string($customAlias)) {
+ $columns[$customAlias] = $columnAlias;
+ } else {
+ $columns[] = $columnAlias;
+ }
+ }
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Filter this query using the given column and value
+ *
+ * This notifies the repository about the required filter column.
+ *
+ * @param string $column
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function where($column, $value = null)
+ {
+ $this->query->where(
+ $this->repository->requireFilterColumn($this->target, $column, $this),
+ $this->repository->persistColumn($this->target, $column, $value)
+ );
+ return $this;
+ }
+
+ /**
+ * Add an additional filter expression to this query
+ *
+ * This notifies the repository about each required filter column.
+ *
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function applyFilter(Filter $filter)
+ {
+ return $this->addFilter($filter);
+ }
+
+ /**
+ * Set a filter for this query
+ *
+ * This notifies the repository about each required filter column.
+ *
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function setFilter(Filter $filter)
+ {
+ $this->query->setFilter($this->repository->requireFilter($this->target, $filter, $this));
+ return $this;
+ }
+
+ /**
+ * Add an additional filter expression to this query
+ *
+ * This notifies the repository about each required filter column.
+ *
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function addFilter(Filter $filter)
+ {
+ $this->query->addFilter($this->repository->requireFilter($this->target, $filter, $this));
+ return $this;
+ }
+
+ /**
+ * Return the current filter
+ *
+ * @return Filter
+ */
+ public function getFilter()
+ {
+ return $this->query->getFilter();
+ }
+
+ /**
+ * Add a sort rule for this query
+ *
+ * If called without a specific column, the repository's defaul sort rules will be applied.
+ * This notifies the repository about each column being required as filter column.
+ *
+ * @param string $field The name of the column by which to sort the query's result
+ * @param string $direction The direction to use when sorting (asc or desc, default is asc)
+ *
+ * @return $this
+ */
+ public function order($field = null, $direction = null)
+ {
+ $sortRules = $this->repository->getSortRules();
+ if ($field === null) {
+ // Use first available sort rule as default
+ if (empty($sortRules)) {
+ // Return early in case of no sort defaults and no given $field
+ return $this;
+ }
+
+ $sortColumns = reset($sortRules);
+ if (! array_key_exists('columns', $sortColumns)) {
+ $sortColumns['columns'] = array(key($sortRules));
+ }
+ if ($direction !== null || !array_key_exists('order', $sortColumns)) {
+ $sortColumns['order'] = $direction ?: static::SORT_ASC;
+ }
+ } elseif (array_key_exists($field, $sortRules)) {
+ $sortColumns = $sortRules[$field];
+ if (! array_key_exists('columns', $sortColumns)) {
+ $sortColumns['columns'] = array($field);
+ }
+ if ($direction !== null || !array_key_exists('order', $sortColumns)) {
+ $sortColumns['order'] = $direction ?: static::SORT_ASC;
+ }
+ } else {
+ $sortColumns = array(
+ 'columns' => array($field),
+ 'order' => $direction
+ );
+ };
+
+ $baseDirection = strtoupper($sortColumns['order']) === static::SORT_DESC ? static::SORT_DESC : static::SORT_ASC;
+
+ foreach ($sortColumns['columns'] as $column) {
+ list($column, $specificDirection) = $this->splitOrder($column);
+
+ try {
+ $this->query->order(
+ $this->repository->requireFilterColumn($this->target, $column, $this),
+ $specificDirection ?: $baseDirection
+ // I would have liked the following solution, but hey, a coder should be allowed to produce crap...
+ // $specificDirection && (! $direction || $column !== $field) ? $specificDirection : $baseDirection
+ );
+ } catch (QueryException $_) {
+ Logger::info('Cannot order by column "%s" in repository "%s"', $column, $this->repository->getName());
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Extract and return the name and direction of the given sort column definition
+ *
+ * @param string $field
+ *
+ * @return array An array of two items: $columnName, $direction
+ */
+ protected function splitOrder($field)
+ {
+ $columnAndDirection = explode(' ', $field, 2);
+ if (count($columnAndDirection) === 1) {
+ $column = $field;
+ $direction = null;
+ } else {
+ $column = $columnAndDirection[0];
+ $direction = strtoupper($columnAndDirection[1]) === static::SORT_DESC
+ ? static::SORT_DESC
+ : static::SORT_ASC;
+ }
+
+ return array($column, $direction);
+ }
+
+ /**
+ * Return whether any sort rules were applied to this query
+ *
+ * @return bool
+ */
+ public function hasOrder()
+ {
+ return $this->query->hasOrder();
+ }
+
+ /**
+ * Return the sort rules applied to this query
+ *
+ * @return array
+ */
+ public function getOrder()
+ {
+ return $this->query->getOrder();
+ }
+
+ /**
+ * Limit this query's results
+ *
+ * @param int $count When to stop returning results
+ * @param int $offset When to start returning results
+ *
+ * @return $this
+ */
+ public function limit($count = null, $offset = null)
+ {
+ $this->query->limit($count, $offset);
+ return $this;
+ }
+
+ /**
+ * Return whether this query does not return all available entries from its result
+ *
+ * @return bool
+ */
+ public function hasLimit()
+ {
+ return $this->query->hasLimit();
+ }
+
+ /**
+ * Return the limit when to stop returning results
+ *
+ * @return int
+ */
+ public function getLimit()
+ {
+ return $this->query->getLimit();
+ }
+
+ /**
+ * Return whether this query does not start returning results at the very first entry
+ *
+ * @return bool
+ */
+ public function hasOffset()
+ {
+ return $this->query->hasOffset();
+ }
+
+ /**
+ * Return the offset when to start returning results
+ *
+ * @return int
+ */
+ public function getOffset()
+ {
+ return $this->query->getOffset();
+ }
+
+ /**
+ * Fetch and return the first column of this query's first row
+ *
+ * @return mixed|false False in case of no result
+ */
+ public function fetchOne()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $result = $this->query->fetchOne();
+ if ($result !== false && $this->repository->providesValueConversion($this->target)) {
+ $columns = $this->getColumns();
+ $column = isset($columns[0]) ? $columns[0] : key($columns);
+ return $this->repository->retrieveColumn($this->target, $column, $result);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Fetch and return the first row of this query's result
+ *
+ * @return object|false False in case of no result
+ */
+ public function fetchRow()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $result = $this->query->fetchRow();
+ if ($result !== false && $this->repository->providesValueConversion($this->target)) {
+ foreach ($this->getColumns() as $alias => $column) {
+ if (! is_string($alias)) {
+ $alias = $column;
+ }
+
+ $result->$alias = $this->repository->retrieveColumn($this->target, $alias, $result->$alias);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Fetch and return the first column of all rows of the result set as an array
+ *
+ * @return array
+ */
+ public function fetchColumn()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $results = $this->query->fetchColumn();
+ if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
+ $columns = $this->getColumns();
+ $aliases = array_keys($columns);
+ $column = is_int($aliases[0]) ? $columns[0] : $aliases[0];
+ foreach ($results as & $value) {
+ $value = $this->repository->retrieveColumn($this->target, $column, $value);
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Fetch and return all rows of this query's result set as an array of key-value pairs
+ *
+ * The first column is the key, the second column is the value.
+ *
+ * @return array
+ */
+ public function fetchPairs()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $results = $this->query->fetchPairs();
+ if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
+ $columns = $this->getColumns();
+ $aliases = array_keys($columns);
+ $newResults = array();
+ foreach ($results as $colOneValue => $colTwoValue) {
+ $colOne = $aliases[0] !== 0 ? $aliases[0] : $columns[0];
+ $colTwo = count($aliases) < 2 ? $colOne : ($aliases[1] !== 1 ? $aliases[1] : $columns[1]);
+ $colOneValue = $this->repository->retrieveColumn($this->target, $colOne, $colOneValue);
+ $newResults[$colOneValue] = $this->repository->retrieveColumn($this->target, $colTwo, $colTwoValue);
+ }
+
+ $results = $newResults;
+ }
+
+ return $results;
+ }
+
+ /**
+ * Fetch and return all results of this query
+ *
+ * @return array
+ */
+ public function fetchAll()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $results = $this->query->fetchAll();
+ if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
+ $columns = $this->getColumns();
+ foreach ($results as $row) {
+ foreach ($columns as $alias => $column) {
+ if (! is_string($alias)) {
+ $alias = $column;
+ }
+
+ $row->$alias = $this->repository->retrieveColumn($this->target, $alias, $row->$alias);
+ }
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Count all results of this query
+ *
+ * @return int
+ */
+ public function count()
+ {
+ return $this->query->count();
+ }
+
+ /**
+ * Start or rewind the iteration
+ */
+ public function rewind()
+ {
+ if ($this->iterator === null) {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $iterator = $this->repository->getDataSource()->query($this->query);
+ if ($iterator instanceof IteratorAggregate) {
+ $this->iterator = $iterator->getIterator();
+ } else {
+ $this->iterator = $iterator;
+ }
+ }
+
+ $this->iterator->rewind();
+ Benchmark::measure('Query result iteration started');
+ }
+
+ /**
+ * Fetch and return the current row of this query's result
+ *
+ * @return object
+ */
+ public function current()
+ {
+ $row = $this->iterator->current();
+ if ($this->repository->providesValueConversion($this->target)) {
+ foreach ($this->getColumns() as $alias => $column) {
+ if (! is_string($alias)) {
+ $alias = $column;
+ }
+
+ $row->$alias = $this->repository->retrieveColumn($this->target, $alias, $row->$alias);
+ }
+ }
+
+ return $row;
+ }
+
+ /**
+ * Return whether the current row of this query's result is valid
+ *
+ * @return bool
+ */
+ public function valid()
+ {
+ if (! $this->iterator->valid()) {
+ Benchmark::measure('Query result iteration finished');
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return the key for the current row of this query's result
+ *
+ * @return mixed
+ */
+ public function key()
+ {
+ return $this->iterator->key();
+ }
+
+ /**
+ * Advance to the next row of this query's result
+ */
+ public function next()
+ {
+ $this->iterator->next();
+ }
+}
diff --git a/library/Icinga/User.php b/library/Icinga/User.php
index f9df0d664..c322a04d0 100644
--- a/library/Icinga/User.php
+++ b/library/Icinga/User.php
@@ -426,7 +426,7 @@ class User
// matches
$any = strpos($requiredPermission, '*');
foreach ($this->permissions as $grantedPermission) {
- if ($any !== false && strpos($grantedPermission, '*') === false) {
+ if ($any !== false) {
$wildcard = $any;
} else {
// If the permit contains a wildcard, grant the permission if it's related to the permit
diff --git a/library/Icinga/Util/String.php b/library/Icinga/Util/String.php
index 0d42d0e76..d67035b3f 100644
--- a/library/Icinga/Util/String.php
+++ b/library/Icinga/Util/String.php
@@ -54,6 +54,26 @@ class String
return $string;
}
+ /**
+ * Add ellipsis in the center of a string when a string is longer than max length
+ *
+ * @param string $string
+ * @param int $maxLength
+ * @param string $ellipsis
+ *
+ * @return string
+ */
+ public static function ellipsisCenter($string, $maxLength, $ellipsis = '...')
+ {
+ $start = ceil($maxLength / 2.0);
+ $end = floor($maxLength / 2.0);
+ if (strlen($string) > $maxLength) {
+ return substr($string, 0, $start - strlen($ellipsis)) . $ellipsis . substr($string, - $end);
+ }
+
+ return $string;
+ }
+
/**
* Find and return all similar strings in $possibilites matching $string with the given minimum $similarity
*
diff --git a/library/Icinga/Web/Controller/AuthBackendController.php b/library/Icinga/Web/Controller/AuthBackendController.php
new file mode 100644
index 000000000..43ed78fd7
--- /dev/null
+++ b/library/Icinga/Web/Controller/AuthBackendController.php
@@ -0,0 +1,198 @@
+hasPermission('config/authentication/users/show')) {
+ $this->redirectNow('user/list');
+ } elseif ($this->hasPermission('config/authentication/groups/show')) {
+ $this->redirectNow('group/list');
+ } elseif ($this->hasPermission('config/authentication/roles/show')) {
+ $this->redirectNow('role/list');
+ } else {
+ throw new SecurityException($this->translate('No permission for authentication configuration'));
+ }
+ }
+
+ /**
+ * Return all user backends implementing the given interface
+ *
+ * @param string $interface The class path of the interface, or null if no interface check should be made
+ *
+ * @return array
+ */
+ protected function loadUserBackends($interface = null)
+ {
+ $backends = array();
+ foreach (Config::app('authentication') as $backendName => $backendConfig) {
+ $candidate = UserBackend::create($backendName, $backendConfig);
+ if (! $interface || $candidate instanceof $interface) {
+ $backends[] = $candidate;
+ }
+ }
+
+ return $backends;
+ }
+
+ /**
+ * Return the given user backend or the first match in order
+ *
+ * @param string $name The name of the backend, or null in case the first match should be returned
+ * @param string $interface The interface the backend should implement, no interface check if null
+ *
+ * @return UserBackendInterface
+ *
+ * @throws Zend_Controller_Action_Exception In case the given backend name is invalid
+ */
+ protected function getUserBackend($name = null, $interface = 'Icinga\Data\Selectable')
+ {
+ if ($name !== null) {
+ $config = Config::app('authentication');
+ if (! $config->hasSection($name)) {
+ $this->httpNotFound(sprintf($this->translate('Authentication backend "%s" not found'), $name));
+ } else {
+ $backend = UserBackend::create($name, $config->getSection($name));
+ if ($interface && !$backend instanceof $interface) {
+ $interfaceParts = explode('\\', strtolower($interface));
+ throw new Zend_Controller_Action_Exception(
+ sprintf(
+ $this->translate('Authentication backend "%s" is not %s'),
+ $name,
+ array_pop($interfaceParts)
+ ),
+ 400
+ );
+ }
+ }
+ } else {
+ $backends = $this->loadUserBackends($interface);
+ $backend = array_shift($backends);
+ }
+
+ return $backend;
+ }
+
+ /**
+ * Return all user group backends implementing the given interface
+ *
+ * @param string $interface The class path of the interface, or null if no interface check should be made
+ *
+ * @return array
+ */
+ protected function loadUserGroupBackends($interface = null)
+ {
+ $backends = array();
+ foreach (Config::app('groups') as $backendName => $backendConfig) {
+ $candidate = UserGroupBackend::create($backendName, $backendConfig);
+ if (! $interface || $candidate instanceof $interface) {
+ $backends[] = $candidate;
+ }
+ }
+
+ return $backends;
+ }
+
+ /**
+ * Return the given user group backend or the first match in order
+ *
+ * @param string $name The name of the backend, or null in case the first match should be returned
+ * @param string $interface The interface the backend should implement, no interface check if null
+ *
+ * @return UserGroupBackendInterface
+ *
+ * @throws Zend_Controller_Action_Exception In case the given backend name is invalid
+ */
+ protected function getUserGroupBackend($name = null, $interface = 'Icinga\Data\Selectable')
+ {
+ if ($name !== null) {
+ $config = Config::app('groups');
+ if (! $config->hasSection($name)) {
+ $this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $name));
+ } else {
+ $backend = UserGroupBackend::create($name, $config->getSection($name));
+ if ($interface && !$backend instanceof $interface) {
+ $interfaceParts = explode('\\', strtolower($interface));
+ throw new Zend_Controller_Action_Exception(
+ sprintf(
+ $this->translate('User group backend "%s" is not %s'),
+ $name,
+ array_pop($interfaceParts)
+ ),
+ 400
+ );
+ }
+ }
+ } else {
+ $backends = $this->loadUserGroupBackends($interface);
+ $backend = array_shift($backends);
+ }
+
+ return $backend;
+ }
+
+ /**
+ * Create the tabs to list users and groups
+ */
+ protected function createListTabs()
+ {
+ $tabs = $this->getTabs();
+
+ if ($this->hasPermission('config/authentication/users/show')) {
+ $tabs->add(
+ 'user/list',
+ array(
+ 'title' => $this->translate('List users of authentication backends'),
+ 'label' => $this->translate('Users'),
+ 'icon' => 'user',
+ 'url' => 'user/list'
+ )
+ );
+ }
+
+ if ($this->hasPermission('config/authentication/groups/show')) {
+ $tabs->add(
+ 'group/list',
+ array(
+ 'title' => $this->translate('List groups of user group backends'),
+ 'label' => $this->translate('Groups'),
+ 'icon' => 'users',
+ 'url' => 'group/list'
+ )
+ );
+ }
+
+ if ($this->hasPermission('config/authentication/roles/show')) {
+ $tabs->add(
+ 'role/list',
+ array(
+ 'title' => $this->translate(
+ 'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
+ ),
+ 'label' => $this->translate('Roles'),
+ 'url' => 'role/list'
+ )
+ );
+ }
+
+ return $tabs;
+ }
+}
diff --git a/library/Icinga/Web/Form/ErrorLabeller.php b/library/Icinga/Web/Form/ErrorLabeller.php
index 455f5f81e..f66260149 100644
--- a/library/Icinga/Web/Form/ErrorLabeller.php
+++ b/library/Icinga/Web/Form/ErrorLabeller.php
@@ -39,7 +39,7 @@ class ErrorLabeller extends Zend_Translate_Adapter
protected function createMessages($element)
{
- $label = $element->getLabel();
+ $label = $element->getLabel() ?: $element->getName();
return array(
Zend_Validate_NotEmpty::IS_EMPTY => sprintf(t('%s is required and must not be empty'), $label),
diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php
index 476d73879..63d92fc9f 100644
--- a/library/Icinga/Web/Menu.php
+++ b/library/Icinga/Web/Menu.php
@@ -233,40 +233,50 @@ class Menu implements RecursiveIterator
));
$section = $this->add(t('System'), array(
- 'icon' => 'wrench',
- 'priority' => 200,
+ 'icon' => 'services',
+ 'priority' => 700,
'renderer' => 'ProblemMenuItemRenderer'
));
- $section->add(t('Configuration'), array(
+ if (Logger::writesToFile()) {
+ $section->add(t('Application Log'), array(
+ 'url' => 'list/applicationlog',
+ 'priority' => 710
+ ));
+ }
+
+ $section = $this->add(t('Configuration'), array(
+ 'icon' => 'wrench',
+ 'permission' => 'config/*',
+ 'priority' => 800
+ ));
+ $section->add(t('Application'), array(
'url' => 'config',
'permission' => 'config/application/*',
- 'priority' => 300
+ 'priority' => 810
+ ));
+ $section->add(t('Authentication'), array(
+ 'url' => 'user',
+ 'permission' => 'config/authentication/*',
+ 'priority' => 820
));
$section->add(t('Modules'), array(
'url' => 'config/modules',
'permission' => 'config/modules',
- 'priority' => 400
+ 'priority' => 890
));
- if (Logger::writesToFile()) {
- $section->add(t('Application Log'), array(
- 'url' => 'list/applicationlog',
- 'priority' => 500
- ));
- }
-
$section = $this->add($auth->getUser()->getUsername(), array(
'icon' => 'user',
- 'priority' => 600
+ 'priority' => 900
));
$section->add(t('Preferences'), array(
'url' => 'preference',
- 'priority' => 601
+ 'priority' => 910
));
$section->add(t('Logout'), array(
'url' => 'authentication/logout',
- 'priority' => 700,
+ 'priority' => 990,
'renderer' => 'ForeignMenuItemRenderer'
));
}
diff --git a/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php b/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php
index 5d85a4428..3f2b71cc3 100644
--- a/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php
+++ b/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php
@@ -4,44 +4,64 @@
namespace Icinga\Web\Paginator\Adapter;
use Zend_Paginator_Adapter_Interface;
-
-/**
- * @see Zend_Paginator_Adapter_Interface
- */
+use Icinga\Data\QueryInterface;
class QueryAdapter implements Zend_Paginator_Adapter_Interface
{
/**
- * Array
+ * The query being paginated
*
- * @var array
+ * @var QueryInterface
*/
- protected $query = null;
+ protected $query;
/**
* Item count
*
- * @var integer
+ * @var int
*/
- protected $count = null;
+ protected $count;
/**
- * Constructor.
+ * Create a new QueryAdapter
*
- * @param array $query Query to paginate
+ * @param QueryInterface $query The query to paginate
*/
- // TODO: This might be ready for (QueryInterface $query)
- public function __construct($query)
+ public function __construct(QueryInterface $query)
{
- $this->query = $query;
+ $this->setQuery($query);
}
/**
- * Returns an array of items for a page.
+ * Set the query to paginate
*
- * @param integer $offset Page offset
- * @param integer $itemCountPerPage Number of items per page
- * @return array
+ * @param QueryInterface $query
+ *
+ * @return $this
+ */
+ public function setQuery(QueryInterface $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Return the query being paginated
+ *
+ * @return QueryInterface
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Fetch and return the rows in the given range of the query result
+ *
+ * @param int $offset Page offset
+ * @param int $itemCountPerPage Number of items per page
+ *
+ * @return array
*/
public function getItems($offset, $itemCountPerPage)
{
@@ -49,15 +69,16 @@ class QueryAdapter implements Zend_Paginator_Adapter_Interface
}
/**
- * Returns the total number of items in the query result.
+ * Return the total number of items in the query result
*
- * @return integer
+ * @return int
*/
public function count()
{
if ($this->count === null) {
$this->count = $this->query->count();
}
+
return $this->count;
}
}
diff --git a/library/Icinga/Web/Widget/FilterEditor.php b/library/Icinga/Web/Widget/FilterEditor.php
index 2d64e4c2b..42561aeee 100644
--- a/library/Icinga/Web/Widget/FilterEditor.php
+++ b/library/Icinga/Web/Widget/FilterEditor.php
@@ -216,25 +216,22 @@ class FilterEditor extends AbstractWidget
$filter = $this->getFilter();
if ($search !== null) {
- if (empty($this->searchColumns)) {
- if (strpos($search, '=') === false) {
- Notification::error(mt('monitoring', 'Cannot search here'));
- return $this;
- } else {
- list($k, $v) = preg_split('/=/', $search);
- $filter = $this->mergeRootExpression($filter, trim($k), '=', ltrim($v));
- }
- } else {
- if (false === $this->resetSearchColumns($filter)) {
+ if (strpos($search, '=') !== false) {
+ list($k, $v) = preg_split('/=/', $search);
+ $filter = $this->mergeRootExpression($filter, trim($k), '=', ltrim($v));
+ } elseif (! empty($this->searchColumns)) {
+ if (! $this->resetSearchColumns($filter)) {
$filter = Filter::matchAll();
}
-
$filters = array();
$search = ltrim($search);
foreach ($this->searchColumns as $searchColumn) {
$filters[] = Filter::expression($searchColumn, '=', "*$search*");
}
- $filter->andFilter(new FilterOr($filters));
+ $filter = $filter->andFilter(new FilterOr($filters));
+ } else {
+ Notification::error(mt('monitoring', 'Cannot search here'));
+ return $this;
}
$url = $this->url()->setQueryString(
diff --git a/modules/doc/configuration.php b/modules/doc/configuration.php
index 031a62e64..392009798 100644
--- a/modules/doc/configuration.php
+++ b/modules/doc/configuration.php
@@ -7,7 +7,7 @@ $section = $this->menuSection($this->translate('Documentation'), array(
'title' => 'Documentation',
'icon' => 'book',
'url' => 'doc',
- 'priority' => 190
+ 'priority' => 700
));
$section->add('Icinga Web 2', array(
@@ -18,7 +18,7 @@ $section->add('Module documentations', array(
));
$section->add($this->translate('Developer - Style'), array(
'url' => 'doc/style/guide',
- 'priority' => 200,
+ 'priority' => 790
));
$this->provideSearchUrl($this->translate('Doc'), 'doc/search', -10);
diff --git a/modules/monitoring/application/controllers/HostsController.php b/modules/monitoring/application/controllers/HostsController.php
index 253a1fb74..eb9db57eb 100644
--- a/modules/monitoring/application/controllers/HostsController.php
+++ b/modules/monitoring/application/controllers/HostsController.php
@@ -50,6 +50,7 @@ class Monitoring_HostsController extends Controller
'host_icon_image',
'host_icon_image_alt',
'host_name',
+ 'host_address',
'host_state',
'host_problem',
'host_handled',
@@ -88,6 +89,7 @@ class Monitoring_HostsController extends Controller
'host_icon_image',
'host_icon_image_alt',
'host_name',
+ 'host_address',
'host_state',
'host_problem',
'host_handled',
@@ -125,14 +127,16 @@ class Monitoring_HostsController extends Controller
$this->view->objects = $this->hostList;
$this->view->unhandledObjects = $this->hostList->getUnhandledObjects();
$this->view->problemObjects = $this->hostList->getProblemObjects();
-
$this->view->acknowledgeUnhandledLink = Url::fromPath('monitoring/hosts/acknowledge-problem')
- ->setQueryString($this->hostList->getUnhandledObjects()->objectsFilter());
+ ->setQueryString($this->hostList->getUnhandledObjects()->objectsFilter()->toQueryString());
$this->view->downtimeUnhandledLink = Url::fromPath('monitoring/hosts/schedule-downtime')
- ->setQueryString($this->hostList->getUnhandledObjects()->objectsFilter());
+ ->setQueryString($this->hostList->getUnhandledObjects()->objectsFilter()->toQueryString());
$this->view->downtimeLink = Url::fromPath('monitoring/hosts/schedule-downtime')
- ->setQueryString($this->hostList->getProblemObjects()->objectsFilter());
+ ->setQueryString($this->hostList->getProblemObjects()->objectsFilter()->toQueryString());
$this->view->acknowledgedObjects = $this->hostList->getAcknowledgedObjects();
+ $this->view->acknowledgeLink = Url::fromPath('monitoring/hosts/acknowledge-problem')
+ ->setQueryString($this->hostList->getUnacknowledgedObjects()->objectsFilter()->toQueryString());
+ $this->view->unacknowledgedObjects = $this->hostList->getUnacknowledgedObjects();
$this->view->objectsInDowntime = $this->hostList->getObjectsInDowntime();
$this->view->inDowntimeLink = Url::fromPath('monitoring/list/hosts')
->setQueryString(
diff --git a/modules/monitoring/application/controllers/ServicesController.php b/modules/monitoring/application/controllers/ServicesController.php
index 8381a640d..1e2bd8912 100644
--- a/modules/monitoring/application/controllers/ServicesController.php
+++ b/modules/monitoring/application/controllers/ServicesController.php
@@ -52,6 +52,7 @@ class Monitoring_ServicesController extends Controller
'host_icon_image',
'host_icon_image_alt',
'host_name',
+ 'host_address',
'host_output',
'host_state',
'host_problem',
@@ -99,6 +100,7 @@ class Monitoring_ServicesController extends Controller
'host_icon_image',
'host_icon_image_alt',
'host_name',
+ 'host_address',
'host_output',
'host_state',
'host_problem',
@@ -146,13 +148,14 @@ class Monitoring_ServicesController extends Controller
$this->view->objects = $this->serviceList;
$this->view->unhandledObjects = $this->serviceList->getUnhandledObjects();
$this->view->problemObjects = $this->serviceList->getProblemObjects();
- $this->view->acknowledgeUnhandledLink = Url::fromPath('monitoring/services/acknowledge-problem')
- ->setQueryString($this->serviceList->getUnhandledObjects()->objectsFilter()->toQueryString());
$this->view->downtimeUnhandledLink = Url::fromPath('monitoring/services/schedule-downtime')
->setQueryString($this->serviceList->getUnhandledObjects()->objectsFilter()->toQueryString());
$this->view->downtimeLink = Url::fromPath('monitoring/services/schedule-downtime')
->setQueryString($this->serviceList->getProblemObjects()->objectsFilter()->toQueryString());
$this->view->acknowledgedObjects = $acknowledgedObjects;
+ $this->view->acknowledgeLink = Url::fromPath('monitoring/services/acknowledge-problem')
+ ->setQueryString($this->serviceList->getUnacknowledgedObjects()->objectsFilter()->toQueryString());
+ $this->view->unacknowledgedObjects = $this->serviceList->getUnacknowledgedObjects();
$this->view->objectsInDowntime = $this->serviceList->getObjectsInDowntime();
$this->view->inDowntimeLink = Url::fromPath('monitoring/list/services')
->setQueryString($this->serviceList->getObjectsInDowntime()
diff --git a/modules/monitoring/application/controllers/ShowController.php b/modules/monitoring/application/controllers/ShowController.php
index a8a4583d1..538a75132 100644
--- a/modules/monitoring/application/controllers/ShowController.php
+++ b/modules/monitoring/application/controllers/ShowController.php
@@ -78,20 +78,6 @@ class Monitoring_ShowController extends Controller
$this->setupPaginationControl($this->view->history, 50);
}
- public function servicesAction()
- {
- $this->setAutorefreshInterval(15);
- $this->getTabs()->activate('services');
- $this->_setParam('service', '');
- // TODO: This used to be a hack and still is. Modifying query string here.
- $_SERVER['QUERY_STRING'] = (string) $this->params->without('service')->set('limit', '');
- $this->view->services = $this->view->action('services', 'list', 'monitoring', array(
- 'view' => 'compact',
- 'sort' => 'service_description',
- ));
- $this->fetchHostStats();
- }
-
protected function fetchHostStats()
{
$this->view->stats = $this->backend->select()->from('statusSummary', array(
@@ -228,19 +214,6 @@ class Monitoring_ShowController extends Controller
)
);
}
- $tabs->add(
- 'services',
- array(
- 'title' => sprintf(
- $this->translate('List all services on host %s'),
- $isService ? $object->getHost()->getName() : $object->getName()
- ),
- 'label' => $this->translate('Services'),
- 'icon' => 'services',
- 'url' => 'monitoring/show/services',
- 'urlParams' => $params,
- )
- );
if ($this->backend->hasQuery('eventHistory')) {
$tabs->add(
'history',
diff --git a/modules/monitoring/application/views/helpers/IconImage.php b/modules/monitoring/application/views/helpers/IconImage.php
index e0e969db4..12108f30b 100644
--- a/modules/monitoring/application/views/helpers/IconImage.php
+++ b/modules/monitoring/application/views/helpers/IconImage.php
@@ -1,6 +1,8 @@
host_icon_image && ! preg_match('/[\'"]/', $object->host_icon_image)) {
return $this->view->img(
- 'img/icons/' . $this->view->resolveMacros($object->host_icon_image, $object),
+ 'img/icons/' . Macro::resolveMacros($object->host_icon_image, $object),
null,
array(
'alt' => $object->host_icon_image_alt,
@@ -48,7 +50,7 @@ class Zend_View_Helper_IconImage extends Zend_View_Helper_Abstract
{
if ($object->service_icon_image && ! preg_match('/[\'"]/', $object->service_icon_image)) {
return $this->view->img(
- 'img/icons/' . $this->view->resolveMacros($object->service_icon_image, $object),
+ 'img/icons/' . Macro::resolveMacros($object->service_icon_image, $object),
null,
array(
'alt' => $object->service_icon_image_alt,
diff --git a/modules/monitoring/application/views/helpers/Perfdata.php b/modules/monitoring/application/views/helpers/Perfdata.php
index 36cbaa319..092aeff0f 100644
--- a/modules/monitoring/application/views/helpers/Perfdata.php
+++ b/modules/monitoring/application/views/helpers/Perfdata.php
@@ -3,6 +3,7 @@
use Icinga\Module\Monitoring\Plugin\Perfdata;
use Icinga\Module\Monitoring\Plugin\PerfdataSet;
+use Icinga\Util\String;
class Zend_View_Helper_Perfdata extends Zend_View_Helper_Abstract
{
@@ -20,44 +21,65 @@ class Zend_View_Helper_Perfdata extends Zend_View_Helper_Abstract
{
$pieChartData = PerfdataSet::fromString($perfdataStr)->asArray();
$results = array();
- $table = array(
- '' . implode(
- ' ',
- array(
- '',
- $this->view->translate('Label'),
- $this->view->translate('Value'),
- $this->view->translate('Min'),
- $this->view->translate('Max'),
- $this->view->translate('Warning'),
- $this->view->translate('Critical')
- )
- ) . ' '
+ $keys = array('', 'label', 'value', 'min', 'max', 'warn', 'crit');
+ $columns = array();
+ $labels = array_combine(
+ $keys,
+ array(
+ '',
+ $this->view->translate('Label'),
+ $this->view->translate('Value'),
+ $this->view->translate('Min'),
+ $this->view->translate('Max'),
+ $this->view->translate('Warning'),
+ $this->view->translate('Critical')
+ )
);
foreach ($pieChartData as $perfdata) {
-
+ if ($perfdata->isVisualizable()) {
+ $columns[''] = '';
+ }
+ foreach ($perfdata->toArray() as $column => $value) {
+ if (empty($value) ||
+ $column === 'min' && floatval($value) === 0.0 ||
+ $column === 'max' && $perfdata->isPercentage() && floatval($value) === 100) {
+ continue;
+ }
+ $columns[$column] = $labels[$column];
+ }
+ }
+ // restore original column array sorting
+ $headers = array();
+ foreach ($keys as $column) {
+ if (isset($columns[$column])) {
+ $headers[$column] = $labels[$column];
+ }
+ }
+ $table = array('' . implode(' ', $headers) . ' ');
+ foreach ($pieChartData as $perfdata) {
if ($compact && $perfdata->isVisualizable()) {
$results[] = $perfdata->asInlinePie($color)->render();
} else {
- $row = '';
-
- $row .= '';
+ $data = array();
if ($perfdata->isVisualizable()) {
- $row .= $perfdata->asInlinePie($color)->render() . ' ';
+ $data []= $perfdata->asInlinePie($color)->render() . ' ';
+ } elseif (isset($columns[''])) {
+ $data []= '';
}
- $row .= ' ';
-
if (! $compact) {
- foreach ($perfdata->toArray() as $value) {
- if ($value === '') {
- $value = '-';
+ foreach ($perfdata->toArray() as $column => $value) {
+ if (! isset($columns[$column])) {
+ continue;
}
- $row .= '' . (string) $value . ' ';
+ $text = $this->view->escape(empty($value) ? '-' : $value);
+ $data []= sprintf(
+ '%s',
+ $text,
+ String::ellipsisCenter($text, 24)
+ );
}
}
-
- $row .= ' ';
- $table[] = $row;
+ $table []= '' . implode(' ', $data) . ' ';
}
}
if ($limit > 0) {
@@ -72,8 +94,14 @@ class Zend_View_Helper_Perfdata extends Zend_View_Helper_Abstract
if ($compact) {
return join('', $results);
} else {
- $pieCharts = empty($table) ? '' : '' . implode("\n", $table) . '
';
- return $pieCharts;
+ if (empty($table)) {
+ return '';
+ }
+ return sprintf(
+ '%s
',
+ isset($columns['']) ? 'perfdata-piecharts' : '',
+ implode("\n", $table)
+ );
}
}
}
diff --git a/modules/monitoring/application/views/scripts/host/show.phtml b/modules/monitoring/application/views/scripts/host/show.phtml
index 32389515b..76d8c1b55 100644
--- a/modules/monitoring/application/views/scripts/host/show.phtml
+++ b/modules/monitoring/application/views/scripts/host/show.phtml
@@ -11,6 +11,7 @@
+ = $this->render('show/components/notes.phtml') ?>
= $this->render('show/components/acknowledgement.phtml') ?>
= $this->render('show/components/comments.phtml') ?>
= $this->render('show/components/notifications.phtml') ?>
diff --git a/modules/monitoring/application/views/scripts/hosts/show.phtml b/modules/monitoring/application/views/scripts/hosts/show.phtml
index 6bbb3c80d..1abc2d21d 100644
--- a/modules/monitoring/application/views/scripts/hosts/show.phtml
+++ b/modules/monitoring/application/views/scripts/hosts/show.phtml
@@ -72,9 +72,10 @@
-
+
= $this->icon('attention-alt') ?>
= $this->translatePlural(
@@ -110,6 +111,24 @@
); ?>
+ 0): ?>
+
+ = $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ 'Acknowledge %u unacknowledged problem hosts',
+ 'Acknowledge %u unacknowledged problem hosts',
+ $unackCount
+ ),
+ $unackCount
+ ),
+ $acknowledgeLink,
+ null,
+ array('icon' => 'ok')
+ ); ?>
+
+
= sprintf(
@@ -136,21 +155,6 @@
array('icon' => 'plug')
); ?>
-
- = $this->qlink(
- sprintf(
- $this->translatePlural(
- 'Acknowledge %u unhandled problem host',
- 'Acknowledge %u unhandled problem hosts',
- $unhandledCount
- ),
- $unhandledCount
- ),
- $acknowledgeUnhandledLink,
- null,
- array('icon' => 'ok')
- ); ?>
-
diff --git a/modules/monitoring/application/views/scripts/list/components/hostssummary.phtml b/modules/monitoring/application/views/scripts/list/components/hostssummary.phtml
index d6166a52d..1690bf0a7 100644
--- a/modules/monitoring/application/views/scripts/list/components/hostssummary.phtml
+++ b/modules/monitoring/application/views/scripts/list/components/hostssummary.phtml
@@ -15,7 +15,15 @@ $this->baseFilter = isset($this->baseFilter) ? $this->baseFilter : null;
$selfUrl = 'monitoring/list/hosts';
$currentUrl = Url::fromRequest()->getRelativeUrl();
?>
compact ? ' data-base-target="col1"' : ''; ?>>
- = sprintf($this->translate('%s hosts:'), $this->stats->hosts_total); ?>
+ = $this->qlink(
+ sprintf($this->translatePlural('%u Host', '%u Hosts', $this->stats->hosts_total), $this->stats->hosts_total),
+ $selfUrl,
+ null,
+ array('title' => sprintf(
+ $this->translatePlural('List %u host', 'List all %u hosts', $this->stats->hosts_total),
+ $this->stats->hosts_total
+ ))
+ ) ?>:
stats->hosts_up): ?>
@@ -156,4 +164,4 @@ $currentUrl = Url::fromRequest()->getRelativeUrl();
-
\ No newline at end of file
+
diff --git a/modules/monitoring/application/views/scripts/list/components/servicesummary.phtml b/modules/monitoring/application/views/scripts/list/components/servicesummary.phtml
index 76a51de92..5776285ee 100644
--- a/modules/monitoring/application/views/scripts/list/components/servicesummary.phtml
+++ b/modules/monitoring/application/views/scripts/list/components/servicesummary.phtml
@@ -17,7 +17,18 @@ $this->baseFilter = isset($this->baseFilter) ? $this->baseFilter : null;
$selfUrl = 'monitoring/list/services';
$currentUrl = Url::fromRequest()->getRelativeUrl();
?>compact ? ' data-base-target="col1"' : ''; ?>>
-= sprintf($this->translate('%s services:'), $this->stats->services_total); ?>
+= $this->qlink(
+ sprintf($this->translatePlural(
+ '%u Service', '%u Services', $this->stats->services_total),
+ $this->stats->services_total
+ ),
+ $selfUrl,
+ null,
+ array('title' => sprintf(
+ $this->translatePlural('List %u service', 'List all %u services', $this->stats->services_total),
+ $this->stats->services_total
+ ))
+) ?>:
stats->services_ok): ?>
@@ -63,7 +74,7 @@ foreach (array(2 => 'critical', 3 => 'unknown', 1 => 'warning') as $stateId => $
} else {
$active = '';
}
-
+
echo '';
if ($this->stats->$unhandled) {
diff --git a/modules/monitoring/application/views/scripts/list/hosts.phtml b/modules/monitoring/application/views/scripts/list/hosts.phtml
index 5535d14fa..f8b77574d 100644
--- a/modules/monitoring/application/views/scripts/list/hosts.phtml
+++ b/modules/monitoring/application/views/scripts/list/hosts.phtml
@@ -69,7 +69,7 @@ if (count($hosts) === 0) {
$this->translatePlural('%u unhandled service', '%u unhandled services', $host->host_unhandled_services),
$host->host_unhandled_services
),
- 'monitoring/show/services',
+ 'monitoring/list/services',
array(
'host' => $host->host_name,
'service_problem' => 1,
diff --git a/modules/monitoring/application/views/scripts/list/servicegrid.phtml b/modules/monitoring/application/views/scripts/list/servicegrid.phtml
index a9cbac445..73fbfe3b5 100644
--- a/modules/monitoring/application/views/scripts/list/servicegrid.phtml
+++ b/modules/monitoring/application/views/scripts/list/servicegrid.phtml
@@ -67,7 +67,7 @@ foreach ($serviceDescriptions as $service_description): ?>
= $this->qlink(
$host_name,
- 'monitoring/show/services?' . $serviceFilter,
+ 'monitoring/list/services?' . $serviceFilter,
array('host' => $host_name),
array('title' => sprintf($this->translate('List all reported services on host %s'), $host_name))
); ?>
diff --git a/modules/monitoring/application/views/scripts/partials/host/servicesummary.phtml b/modules/monitoring/application/views/scripts/partials/host/servicesummary.phtml
index 04408808f..b69682e1b 100644
--- a/modules/monitoring/application/views/scripts/partials/host/servicesummary.phtml
+++ b/modules/monitoring/application/views/scripts/partials/host/servicesummary.phtml
@@ -12,8 +12,7 @@ function urlAddFilterOptional($url, $filter, $optional) {
return $url->setQueryString($f->toQueryString());
}
-$selfUrl = Url::fromPath('monitoring/show/services', array('host' => $object->host_name));
-$currentUrl = Url::fromRequest()->without('limit')->getRelativeUrl();
+$selfUrl = Url::fromPath('monitoring/list/services', array('host' => $object->host_name));
?>compact ? ' data-base-target="col1"' : ''; ?>>
stats->services_total): ?>
= $this->qlink(
@@ -27,15 +26,18 @@ $currentUrl = Url::fromRequest()->without('limit')->getRelativeUrl();
),
$selfUrl,
null,
- array('title' => sprintf(
- $this->translatePlural(
- 'List all %u service on host %s',
- 'List all %u services on host %s',
- $object->stats->services_total
+ array(
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List all %u service on host %s',
+ 'List all %u services on host %s',
+ $object->stats->services_total
+ ),
+ $object->stats->services_total,
+ $object->host_name
),
- $object->stats->services_total,
- $object->host_name
- ))
+ 'data-base-target' => '_next'
+ )
); ?>
= $this->translate('No services configured on this host'); ?>
@@ -43,20 +45,23 @@ $currentUrl = Url::fromRequest()->without('limit')->getRelativeUrl();
stats->services_ok): ?>
-
+
= $this->qlink(
$object->stats->services_ok,
$selfUrl,
array('service_state' => 0),
- array('title' => sprintf(
- $this->translatePlural(
- 'List %u service that is currently in state OK on host %s',
- 'List %u services which are currently in state OK on host %s',
- $object->stats->services_ok
+ array(
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state OK on host %s',
+ 'List %u services which are currently in state OK on host %s',
+ $object->stats->services_ok
+ ),
+ $object->stats->services_ok,
+ $object->host_name
),
- $object->stats->services_ok,
- $object->host_name
- ))
+ 'data-base-target' => '_next'
+ )
); ?>
@@ -68,61 +73,49 @@ foreach (array(2 => 'critical', 3 => 'unknown', 1 => 'warning') as $stateId => $
$unhandled = $pre . '_unhandled';
$paramsHandled = array('service_state' => $stateId, 'service_handled' => 1);
$paramsUnhandled = array('service_state' => $stateId, 'service_handled' => 0);
- if ($object->stats->$unhandled) {
- $compareUrl = $selfUrl->with($paramsUnhandled)->getRelativeUrl();
- } else {
- $compareUrl = $selfUrl->with($paramsHandled)->getRelativeUrl();
- }
-
- if ($compareUrl === $currentUrl) {
- $active = ' active';
- } else {
- $active = '';
- }
-
- echo '';
+ echo '';
if ($object->stats->$unhandled) {
echo $this->qlink(
$object->stats->$unhandled,
$selfUrl,
$paramsUnhandled,
- array('title' => sprintf(
- $this->translatePlural(
- 'List %u service that is currently in state %s on host %s',
- 'List %u services which are currently in state %s on host %s',
- $object->stats->$unhandled
+ array(
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state %s on host %s',
+ 'List %u services which are currently in state %s on host %s',
+ $object->stats->$unhandled
+ ),
+ $object->stats->$unhandled,
+ Service::getStateText($stateId, true),
+ $object->host_name
),
- $object->stats->$unhandled,
- Service::getStateText($stateId, true),
- $object->host_name
- ))
+ 'data-base-target' => '_next'
+ )
);
}
if ($object->stats->$handled) {
-
- if ($selfUrl->with($paramsHandled)->getRelativeUrl() === $currentUrl) {
- $active = ' active';
- } else {
- $active = '';
- }
if ($object->stats->$unhandled) {
- echo '';
+ echo '';
}
echo $this->qlink(
$object->stats->$handled,
$selfUrl,
$paramsHandled,
- array('title' => sprintf(
- $this->translatePlural(
- 'List %u service that is currently in state %s (Acknowledged) on host %s',
- 'List %u services which are currently in state %s (Acknowledged) on host %s',
- $object->stats->$handled
+ array(
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state %s (Acknowledged) on host %s',
+ 'List %u services which are currently in state %s (Acknowledged) on host %s',
+ $object->stats->$handled
+ ),
+ $object->stats->$handled,
+ Service::getStateText($stateId, true),
+ $object->host_name
),
- $object->stats->$handled,
- Service::getStateText($stateId, true),
- $object->host_name
- ))
+ 'data-base-target' => '_next'
+ )
);
if ($object->stats->$unhandled) {
echo "\n";
@@ -133,22 +126,25 @@ foreach (array(2 => 'critical', 3 => 'unknown', 1 => 'warning') as $stateId => $
}
?>
stats->services_pending): ?>
-
+
= $this->qlink(
$object->stats->services_pending,
$selfUrl,
array('service_state' => 99),
- array('title' => sprintf(
- $this->translatePlural(
- 'List %u service that is currently in state PENDING on host %s',
- 'List %u services which are currently in state PENDING on host %s',
- $object->stats->services_pending
+ array(
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state PENDING on host %s',
+ 'List %u services which are currently in state PENDING on host %s',
+ $object->stats->services_pending
+ ),
+ $object->stats->services_pending,
+ $object->host_name
),
- $object->stats->services_pending,
- $object->host_name
- ))
+ 'data-base-target' => '_next'
+ )
) ?>
-
\ No newline at end of file
+
diff --git a/modules/monitoring/application/views/scripts/service/show.phtml b/modules/monitoring/application/views/scripts/service/show.phtml
index 4d33644ce..43dc77030 100644
--- a/modules/monitoring/application/views/scripts/service/show.phtml
+++ b/modules/monitoring/application/views/scripts/service/show.phtml
@@ -11,6 +11,7 @@
+ = $this->render('show/components/notes.phtml') ?>
= $this->render('show/components/acknowledgement.phtml') ?>
= $this->render('show/components/comments.phtml') ?>
= $this->render('show/components/notifications.phtml') ?>
diff --git a/modules/monitoring/application/views/scripts/services/show.phtml b/modules/monitoring/application/views/scripts/services/show.phtml
index 746e24871..fc2e87ad2 100644
--- a/modules/monitoring/application/views/scripts/services/show.phtml
+++ b/modules/monitoring/application/views/scripts/services/show.phtml
@@ -70,9 +70,10 @@
-
+
= $this->icon('attention-alt') ?>
@@ -105,6 +106,23 @@
); ?>
+ 0): ?>
+
+ = $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ 'Acknowledge %u unacknowledged problem service',
+ 'Acknowledge %u unacknowledged problem services',
+ $unackCount
+ ),
+ $unackCount
+ ),
+ $acknowledgeLink,
+ null,
+ array('icon' => 'ok')
+ ); ?>
+
+
= sprintf($this->translate('There are %s unhandled problem services. ' .
@@ -126,20 +144,6 @@
array('icon' => 'plug')
); ?>
-
- = $this->qlink(
- sprintf(
- $this->translatePlural(
- 'Acknowledge %u unhandled problem service',
- 'Acknowledge %u unhandled problem services',
- $unhandledCount
- ),
- $unhandledCount
- ),
- $acknowledgeUnhandledLink,
- null,
- array('icon' => 'ok')
- ); ?>
diff --git a/modules/monitoring/application/views/scripts/show/components/actions.phtml b/modules/monitoring/application/views/scripts/show/components/actions.phtml
index 0681d5d91..329a06fb9 100644
--- a/modules/monitoring/application/views/scripts/show/components/actions.phtml
+++ b/modules/monitoring/application/views/scripts/show/components/actions.phtml
@@ -1,41 +1,16 @@
%s ', $this->translate('opens in new window'));
-$linkText = '%s ' . $newTabInfo . '';
-$localLinkText = '%s';
-
-if ($object->notes_url) {
- if (strpos($object->notes_url, "' ") === false) {
- $links[] = sprintf($linkText, $this->resolveMacros($object->notes_url, $object), 'Notes');
- } else {
- // TODO: We should find out document what's going on here. Looks strange :p
- foreach(explode("' ", $object->notes_url) as $url) {
- $url = strpos($url, "'") === 0 ? substr($url, 1) : $url;
- $url = strrpos($url, "'") === strlen($url) - 1 ? substr($url, 0, strlen($url) - 1) : $url;
- $links[] = sprintf($linkText, $this->resolveMacros($url, $object), 'Notes');
- }
- }
-}
-if ($object->action_url) {
- if (strpos($object->action_url, "' ") === false) {
- $links[] = sprintf($linkText, $this->resolveMacros($object->action_url, $object), 'Action');
- } else {
- // TODO: We should find out document what's going on here. Looks strange :p
- foreach(explode("' ", $object->action_url) as $url) {
- $url = strpos($url, "'") === 0 ? substr($url, 1) : $url;
- $url = strrpos($url, "'") === strlen($url) - 1 ? substr($url, 0, strlen($url) - 1) : $url;
- $links[] = sprintf($linkText, $this->resolveMacros($url, $object), 'Action');
- }
- }
+$links = $object->getActionUrls();
+foreach ($links as $i => $link) {
+ $links[$i] = sprintf('%s ' . $newTabInfo . '', $link, 'Action');
}
if (isset($this->actions)) {
foreach ($this->actions as $id => $action) {
- $links[] = sprintf($localLinkText, $action, $id);
+ $links[] = sprintf('%s', $action, $id);
}
}
@@ -46,5 +21,5 @@ if (empty($links)) {
?>
= $this->translate('Actions') ?>
- = implode("\n ", $links) . "\n" ?>
+ = implode("
", $links) ?>
diff --git a/modules/monitoring/application/views/scripts/show/components/notes.phtml b/modules/monitoring/application/views/scripts/show/components/notes.phtml
new file mode 100644
index 000000000..4a5041e50
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/notes.phtml
@@ -0,0 +1,26 @@
+getNotes());
+$links = $object->getNotesUrls();
+
+if (! empty($links) || ! empty($notes)): ?>
+
+ = $this->translate('Notes') ?>
+
+ ';
+ }
+ // add warning to links that open in new tabs to improve accessibility, as recommended by WCAG20 G201
+ $newTabInfo = sprintf(
+ ' %s ',
+ $this->translate('opens in new window')
+ );
+ $linkText = '%s ' . $newTabInfo . '';
+ foreach ($links as $i => $link) {
+ $links[$i] = sprintf($linkText, $this->escape($link), $this->escape($link));
+ }
+ echo implode('
', $links);
+ ?>
+
+
+
diff --git a/modules/monitoring/application/views/scripts/show/services.phtml b/modules/monitoring/application/views/scripts/show/services.phtml
deleted file mode 100644
index d3a0c3ef8..000000000
--- a/modules/monitoring/application/views/scripts/show/services.phtml
+++ /dev/null
@@ -1,8 +0,0 @@
-
- compact): ?>
- = $this->tabs; ?>
-
-= $this->render('partials/host/object-header.phtml') ?>
-= $this->render('partials/host/servicesummary.phtml') ?>
-
-= $services ?>
diff --git a/modules/monitoring/configuration.php b/modules/monitoring/configuration.php
index e548a36e5..b83de5e23 100644
--- a/modules/monitoring/configuration.php
+++ b/modules/monitoring/configuration.php
@@ -203,7 +203,7 @@ $section->add($this->translate('Alert Summary'), array(
$section = $this->menuSection($this->translate('System'));
$section->add($this->translate('Monitoring Health'), array(
'url' => 'monitoring/process/info',
- 'priority' => 120,
+ 'priority' => 720,
'renderer' => 'Icinga\Module\Monitoring\Web\Menu\BackendAvailabilityMenuItemRenderer'
));
diff --git a/modules/monitoring/doc/configuration.md b/modules/monitoring/doc/configuration.md
new file mode 100644
index 000000000..7636b1a52
--- /dev/null
+++ b/modules/monitoring/doc/configuration.md
@@ -0,0 +1,16 @@
+# Monitoring Module Configuration
+
+## Overview
+
+Apart from its web configuration capabilities, the local configuration is
+stored in `/etc/icingaweb2` by default (depending on your config setup).
+
+ Location | File | Description
+ ------------------------------|-----------------------|---------------------------
+ modules/monitoring | Directory | `monitoring` module specific configuration
+ modules/monitoring | config.ini | Security settings (e.g. protected custom vars) for the `monitoring` module
+ modules/monitoring | backends.ini | Backend type and resources (e.g. Icinga IDO DB)
+ modules/monitoring | [instances.ini](instances.md#instances) | Instances and their transport (e.g. local external command pipe)
+
+
+
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
index 9cd7eff08..1feff0f71 100644
--- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
@@ -416,21 +416,11 @@ abstract class IdoQuery extends DbQuery
} elseif ($dbType === 'pgsql') {
$this->initializeForPostgres();
}
- $this->dbSelect();
+ $this->joinBaseTables();
$this->select->columns($this->columns);
- //$this->joinBaseTables();
$this->prepareAliasIndexes();
}
- protected function dbSelect()
- {
- if ($this->select === null) {
- $this->select = $this->db->select();
- $this->joinBaseTables();
- }
- return clone $this->select;
- }
-
/**
* Join the base tables for this query
*/
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatusQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatusQuery.php
index 2aad7d31c..7181e8b89 100644
--- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatusQuery.php
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatusQuery.php
@@ -43,6 +43,7 @@ class StatusQuery extends IdoQuery
'host_icon_image' => 'h.icon_image',
'host_icon_image_alt' => 'h.icon_image_alt',
'host_action_url' => 'h.action_url',
+ 'host_notes' => 'h.notes',
'host_notes_url' => 'h.notes_url'
),
'hoststatus' => array(
@@ -179,6 +180,7 @@ class StatusQuery extends IdoQuery
'service_icon_image' => 's.icon_image',
'service_icon_image_alt' => 's.icon_image_alt',
'service_action_url' => 's.action_url',
+ 'service_notes' => 's.notes',
'service_notes_url' => 's.notes_url',
'object_type' => '(\'service\')'
),
diff --git a/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php
index a9839565b..f39b0b4c3 100644
--- a/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php
+++ b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php
@@ -108,7 +108,8 @@ abstract class CommandTransport
*/
public static function first()
{
- $config = self::getConfig()->current();
- return self::fromConfig($config);
+ $config = self::getConfig();
+ $config->rewind();
+ return self::fromConfig($config->current());
}
}
diff --git a/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php b/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php
index 197a3fdb1..a26999f18 100644
--- a/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php
+++ b/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php
@@ -4,6 +4,7 @@
namespace Icinga\Module\Monitoring\Command\Transport;
use Exception;
+use RuntimeException;
use Icinga\Application\Logger;
use Icinga\Exception\ConfigurationError;
use Icinga\Module\Monitoring\Command\Exception\TransportException;
@@ -123,11 +124,15 @@ class LocalCommandFile implements CommandTransportInterface
$file->fwrite($commandString . "\n");
$file->fflush();
} catch (Exception $e) {
+ $message = $e->getMessage();
+ if ($e instanceof RuntimeException && ($pos = strrpos($message, ':')) !== false) {
+ // Assume RuntimeException thrown by SplFileObject in the format: __METHOD__ . "({$filename}): Message"
+ $message = substr($message, $pos + 1);
+ }
throw new TransportException(
- 'Can\'t send external Icinga command "%s" to the local command file "%s": %s',
- $commandString,
+ 'Can\'t send external Icinga command to the local command file "%s": %s',
$this->path,
- $e
+ $message
);
}
}
diff --git a/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php b/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
index 866497a5a..c0f670bfe 100644
--- a/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+++ b/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
@@ -201,8 +201,7 @@ class RemoteCommandFile implements CommandTransportInterface
exec($ssh, $output, $status);
if ($status !== 0) {
throw new TransportException(
- 'Can\'t send external Icinga command "%s": %s',
- $ssh,
+ 'Can\'t send external Icinga command: %s',
implode(' ', $output)
);
}
diff --git a/modules/monitoring/library/Monitoring/DataView/DataView.php b/modules/monitoring/library/Monitoring/DataView/DataView.php
index 69d8631c8..ba628f31c 100644
--- a/modules/monitoring/library/Monitoring/DataView/DataView.php
+++ b/modules/monitoring/library/Monitoring/DataView/DataView.php
@@ -3,7 +3,6 @@
namespace Icinga\Module\Monitoring\DataView;
-use ArrayIterator;
use IteratorAggregate;
use Icinga\Data\QueryInterface;
use Icinga\Data\Filter\Filter;
@@ -13,6 +12,7 @@ use Icinga\Data\ConnectionInterface;
use Icinga\Exception\QueryException;
use Icinga\Web\Request;
use Icinga\Web\Url;
+use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
/**
@@ -23,7 +23,7 @@ abstract class DataView implements QueryInterface, IteratorAggregate
/**
* The query used to populate the view
*
- * @var QueryInterface
+ * @var IdoQuery
*/
protected $query;
@@ -61,11 +61,11 @@ abstract class DataView implements QueryInterface, IteratorAggregate
/**
* Return a iterator for all rows of the result set
*
- * @return ArrayIterator
+ * @return IdoQuery
*/
public function getIterator()
{
- return new ArrayIterator($this->fetchAll());
+ return $this->getQuery();
}
/**
@@ -481,15 +481,13 @@ abstract class DataView implements QueryInterface, IteratorAggregate
}
/**
- * Fetch a column of all rows of the result set as an array
- *
- * @param int $columnIndex Index of the column to fetch
+ * Fetch the first column of all rows of the result set as an array
*
* @return array
*/
- public function fetchColumn($columnIndex = 0)
+ public function fetchColumn()
{
- return $this->getQuery()->fetchColumn($columnIndex);
+ return $this->getQuery()->fetchColumn();
}
/**
diff --git a/modules/monitoring/library/Monitoring/Object/Host.php b/modules/monitoring/library/Monitoring/Object/Host.php
index a158625b5..dd9b4c0c1 100644
--- a/modules/monitoring/library/Monitoring/Object/Host.php
+++ b/modules/monitoring/library/Monitoring/Object/Host.php
@@ -7,7 +7,7 @@ use InvalidArgumentException;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
/**
- * A Icinga host
+ * An Icinga host
*/
class Host extends MonitoredObject
{
@@ -119,6 +119,7 @@ class Host extends MonitoredObject
'host_max_check_attempts',
'host_name',
'host_next_check',
+ 'host_notes',
'host_notes_url',
'host_notifications_enabled',
'host_notifications_enabled_changed',
@@ -188,4 +189,16 @@ class Host extends MonitoredObject
}
return $text;
}
+
+ public function getNotesUrls()
+ {
+ return $this->resolveAllStrings(
+ MonitoredObject::parseAttributeUrls($this->host_notes_url)
+ );
+ }
+
+ public function getNotes()
+ {
+ return $this->host_notes;
+ }
}
diff --git a/modules/monitoring/library/Monitoring/Object/HostList.php b/modules/monitoring/library/Monitoring/Object/HostList.php
index cbbafdecf..e48fcc415 100644
--- a/modules/monitoring/library/Monitoring/Object/HostList.php
+++ b/modules/monitoring/library/Monitoring/Object/HostList.php
@@ -112,4 +112,19 @@ class HostList extends ObjectList
->from('hostdowntime', array('host_name'))
->applyFilter(clone $this->filter);
}
+
+ /**
+ * @return ObjectList
+ */
+ public function getUnacknowledgedObjects()
+ {
+ $unhandledObjects = array();
+ foreach ($this as $object) {
+ if (! in_array((int) $object->state, array(0, 99)) &&
+ (bool) $object->host_acknowledged === false) {
+ $unhandledObjects[] = $object;
+ }
+ }
+ return $this->newFromArray($unhandledObjects);
+ }
}
diff --git a/modules/monitoring/application/views/helpers/ResolveMacros.php b/modules/monitoring/library/Monitoring/Object/Macro.php
similarity index 55%
rename from modules/monitoring/application/views/helpers/ResolveMacros.php
rename to modules/monitoring/library/Monitoring/Object/Macro.php
index 7d555f8fd..4a9045b58 100644
--- a/modules/monitoring/application/views/helpers/ResolveMacros.php
+++ b/modules/monitoring/library/Monitoring/Object/Macro.php
@@ -1,36 +1,41 @@
'host_name',
- 'HOSTADDRESS' => 'host_address',
- 'SERVICEDESC' => 'service_description'
+ private static $icingaMacros = array(
+ 'HOSTNAME' => 'host_name',
+ 'HOSTADDRESS' => 'host_address',
+ 'SERVICEDESC' => 'service_description',
+ 'host.name' => 'host_name',
+ 'host.address' => 'host_address',
+ 'service.description' => 'service_description'
);
/**
* Return the given string with macros being resolved
*
* @param string $input The string in which to look for macros
- * @param MonitoredObject|stdClass $object The host or service used to resolve macros
+ * @param MonitoredObject|stdClass $object The host or service used to resolve macros
*
* @return string The substituted or unchanged string
*/
- public function resolveMacros($input, $object)
+ public static function resolveMacros($input, $object)
{
$matches = array();
if (preg_match_all('@\$([^\$\s]+)\$@', $input, $matches)) {
foreach ($matches[1] as $key => $value) {
- $newValue = $this->resolveMacro($value, $object);
+ $newValue = self::resolveMacro($value, $object);
if ($newValue !== $value) {
$input = str_replace($matches[0][$key], $newValue, $input);
}
@@ -44,14 +49,14 @@ class Zend_View_Helper_ResolveMacros extends Zend_View_Helper_Abstract
* Resolve a macro based on the given object
*
* @param string $macro The macro to resolve
- * @param MonitoredObject|stdClass $object The object used to resolve the macro
+ * @param MonitoredObject|stdClass $object The object used to resolve the macro
*
* @return string The new value or the macro if it cannot be resolved
*/
- public function resolveMacro($macro, $object)
+ public static function resolveMacro($macro, $object)
{
- if (array_key_exists($macro, $this->icingaMacros) && $object->{$this->icingaMacros[$macro]} !== false) {
- return $object->{$this->icingaMacros[$macro]};
+ if (array_key_exists($macro, self::$icingaMacros) && $object->{self::$icingaMacros[$macro]} !== false) {
+ return $object->{self::$icingaMacros[$macro]};
}
if (array_key_exists($macro, $object->customvars)) {
return $object->customvars[$macro];
diff --git a/modules/monitoring/library/Monitoring/Object/MonitoredObject.php b/modules/monitoring/library/Monitoring/Object/MonitoredObject.php
index a569341f6..81fe6c069 100644
--- a/modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+++ b/modules/monitoring/library/Monitoring/Object/MonitoredObject.php
@@ -558,4 +558,73 @@ abstract class MonitoredObject implements Filterable
}
return null;
}
+
+ /**
+ * The notes for this monitored object
+ *
+ * @return string The notes as a string
+ */
+ public abstract function getNotes();
+
+ /**
+ * Get all note urls configured for this monitored object
+ *
+ * @return array All note urls as a string
+ */
+ public abstract function getNotesUrls();
+
+ /**
+ * Get all action urls configured for this monitored object
+ *
+ * @return array All note urls as a string
+ */
+ public function getActionUrls()
+ {
+ return $this->resolveAllStrings(
+ MonitoredObject::parseAttributeUrls($this->action_url)
+ );
+ }
+
+ /**
+ * Resolve macros in all given strings in the current object context
+ *
+ * @param array $strs An array of urls as string
+ * @return type
+ */
+ protected function resolveAllStrings(array $strs)
+ {
+ foreach ($strs as $i => $str) {
+ $strs[$i] = Macro::resolveMacros($str, $this);
+ }
+ return $strs;
+ }
+
+ /**
+ * Parse the content of the action_url or notes_url attributes
+ *
+ * Find all occurences of http links, separated by whitespaces and quoted
+ * by single or double-ticks.
+ *
+ * @link http://docs.icinga.org/latest/de/objectdefinitions.html
+ *
+ * @param string $urlString A string containing one or more urls
+ * @return array Array of urls as strings
+ */
+ public static function parseAttributeUrls($urlString)
+ {
+ if (empty($urlString)) {
+ return array();
+ }
+ if (strpos($urlString, "' ") === false) {
+ $links[] = $urlString;
+ } else {
+ // parse notes-url format
+ foreach (explode("' ", $urlString) as $url) {
+ $url = strpos($url, "'") === 0 ? substr($url, 1) : $url;
+ $url = strrpos($url, "'") === strlen($url) - 1 ? substr($url, 0, strlen($url) - 1) : $url;
+ $links[] = $url;
+ }
+ }
+ return $links;
+ }
}
diff --git a/modules/monitoring/library/Monitoring/Object/ObjectList.php b/modules/monitoring/library/Monitoring/Object/ObjectList.php
index 75fe1ba76..c4ecdddb2 100644
--- a/modules/monitoring/library/Monitoring/Object/ObjectList.php
+++ b/modules/monitoring/library/Monitoring/Object/ObjectList.php
@@ -193,6 +193,11 @@ abstract class ObjectList implements Countable, IteratorAggregate
return $this->newFromArray($handledObjects);
}
+ /**
+ * @return ObjectList
+ */
+ public abstract function getUnacknowledgedObjects();
+
/**
* Create a ObjectList from an array of hosts without querying a backend
*
diff --git a/modules/monitoring/library/Monitoring/Object/Service.php b/modules/monitoring/library/Monitoring/Object/Service.php
index 0e8c975f2..cb5615df1 100644
--- a/modules/monitoring/library/Monitoring/Object/Service.php
+++ b/modules/monitoring/library/Monitoring/Object/Service.php
@@ -7,7 +7,7 @@ use InvalidArgumentException;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
/**
- * A Icinga service
+ * An Icinga service
*/
class Service extends MonitoredObject
{
@@ -147,6 +147,7 @@ class Service extends MonitoredObject
'service_last_state_change',
'service_long_output',
'service_next_check',
+ 'service_notes',
'service_notes_url',
'service_notifications_enabled',
'service_notifications_enabled_changed',
@@ -198,4 +199,16 @@ class Service extends MonitoredObject
}
return $text;
}
+
+ public function getNotesUrls()
+ {
+ return $this->resolveAllStrings(
+ MonitoredObject::parseAttributeUrls($this->service_notes_url)
+ );
+ }
+
+ public function getNotes()
+ {
+ return $this->service_notes;
+ }
}
diff --git a/modules/monitoring/library/Monitoring/Object/ServiceList.php b/modules/monitoring/library/Monitoring/Object/ServiceList.php
index f4cc38598..f96876666 100644
--- a/modules/monitoring/library/Monitoring/Object/ServiceList.php
+++ b/modules/monitoring/library/Monitoring/Object/ServiceList.php
@@ -160,4 +160,19 @@ class ServiceList extends ObjectList
->from('servicedowntime', array('host_name', 'service_description'))
->applyFilter(clone $this->filter);
}
+
+ /**
+ * @return ObjectList
+ */
+ public function getUnacknowledgedObjects()
+ {
+ $unhandledObjects = array();
+ foreach ($this as $object) {
+ if (! in_array((int) $object->state, array(0, 99)) &&
+ (bool) $object->service_acknowledged === false) {
+ $unhandledObjects[] = $object;
+ }
+ }
+ return $this->newFromArray($unhandledObjects);
+ }
}
diff --git a/modules/monitoring/library/Monitoring/Plugin/Perfdata.php b/modules/monitoring/library/Monitoring/Plugin/Perfdata.php
index b1c71cb72..df1b6016a 100644
--- a/modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+++ b/modules/monitoring/library/Monitoring/Plugin/Perfdata.php
@@ -444,7 +444,7 @@ class Perfdata
public function toArray()
{
$parts = array(
- $this->getLabel(),
+ 'label' => $this->getLabel(),
'value' => $this->format($this->getvalue()),
'min' => isset($this->minValue) && !$this->isPercentage() ? $this->format($this->minValue) : '',
'max' => isset($this->maxValue) && !$this->isPercentage() ? $this->format($this->maxValue) : '',
diff --git a/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php b/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
index 9d27b1268..7ac509d7b 100644
--- a/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+++ b/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
@@ -181,19 +181,6 @@ abstract class MonitoredObjectController extends Controller
)
);
}
- $tabs->add(
- 'services',
- array(
- 'title' => sprintf(
- $this->translate('List all services on host %s'),
- $isService ? $object->getHost()->getName() : $object->getName()
- ),
- 'label' => $this->translate('Services'),
- 'icon' => 'services',
- 'url' => 'monitoring/show/services',
- 'urlParams' => $params
- )
- );
if ($this->backend->hasQuery('eventHistory')) {
$tabs->add(
'history',
diff --git a/modules/monitoring/test/php/application/views/helpers/ResolveMacrosTest.php b/modules/monitoring/test/php/application/views/helpers/ResolveMacrosTest.php
deleted file mode 100644
index 31bf839a3..000000000
--- a/modules/monitoring/test/php/application/views/helpers/ResolveMacrosTest.php
+++ /dev/null
@@ -1,78 +0,0 @@
-host_name = 'test';
- $hostMock->host_address = '1.1.1.1';
-
- $helper = new Zend_View_Helper_ResolveMacros();
- $this->assertEquals($helper->resolveMacros('$HOSTNAME$', $hostMock), $hostMock->host_name);
- $this->assertEquals($helper->resolveMacros('$HOSTADDRESS$', $hostMock), $hostMock->host_address);
- }
-
- public function testServiceMacros()
- {
- $svcMock = Mockery::mock('service');
- $svcMock->host_name = 'test';
- $svcMock->host_address = '1.1.1.1';
- $svcMock->service_description = 'a service';
-
- $helper = new Zend_View_Helper_ResolveMacros();
- $this->assertEquals($helper->resolveMacros('$HOSTNAME$', $svcMock), $svcMock->host_name);
- $this->assertEquals($helper->resolveMacros('$HOSTADDRESS$', $svcMock), $svcMock->host_address);
- $this->assertEquals($helper->resolveMacros('$SERVICEDESC$', $svcMock), $svcMock->service_description);
- }
-
- public function testCustomvars()
- {
- $objectMock = Mockery::mock('object');
- $objectMock->customvars = array(
- 'CUSTOMVAR' => 'test'
- );
-
- $helper = new Zend_View_Helper_ResolveMacros();
- $this->assertEquals($helper->resolveMacros('$CUSTOMVAR$', $objectMock), $objectMock->customvars['CUSTOMVAR']);
- }
-
- public function testFaultyMacros()
- {
- $hostMock = Mockery::mock('host');
- $hostMock->host_name = 'test';
- $hostMock->customvars = array(
- 'HOST' => 'te',
- 'NAME' => 'st'
- );
-
- $helper = new Zend_View_Helper_ResolveMacros();
- $this->assertEquals(
- $helper->resolveMacros('$$HOSTNAME$ $ HOSTNAME$ $HOST$NAME$', $hostMock),
- '$test $ HOSTNAME$ teNAME$'
- );
- }
-
- public function testMacrosWithSpecialCharacters()
- {
- $objectMock = Mockery::mock('object');
- $objectMock->customvars = array(
- 'V€RY_SP3C|@L' => 'not too special!'
- );
-
- $helper = new Zend_View_Helper_ResolveMacros();
- $this->assertEquals(
- $helper->resolveMacros('$V€RY_SP3C|@L$', $objectMock),
- $objectMock->customvars['V€RY_SP3C|@L']
- );
- }
-}
diff --git a/modules/monitoring/test/php/library/Monitoring/Object/MacroTest.php b/modules/monitoring/test/php/library/Monitoring/Object/MacroTest.php
new file mode 100644
index 000000000..e1b3595e3
--- /dev/null
+++ b/modules/monitoring/test/php/library/Monitoring/Object/MacroTest.php
@@ -0,0 +1,78 @@
+host_name = 'test';
+ $hostMock->host_address = '1.1.1.1';
+
+ $this->assertEquals(Macro::resolveMacros('$HOSTNAME$', $hostMock), $hostMock->host_name);
+ $this->assertEquals(Macro::resolveMacros('$HOSTADDRESS$', $hostMock), $hostMock->host_address);
+ $this->assertEquals(Macro::resolveMacros('$host.name$', $hostMock), $hostMock->host_name);
+ $this->assertEquals(Macro::resolveMacros('$host.address$', $hostMock), $hostMock->host_address);
+ }
+
+ public function testServiceMacros()
+ {
+ $svcMock = Mockery::mock('service');
+ $svcMock->host_name = 'test';
+ $svcMock->host_address = '1.1.1.1';
+ $svcMock->service_description = 'a service';
+
+ $this->assertEquals(Macro::resolveMacros('$HOSTNAME$', $svcMock), $svcMock->host_name);
+ $this->assertEquals(Macro::resolveMacros('$HOSTADDRESS$', $svcMock), $svcMock->host_address);
+ $this->assertEquals(Macro::resolveMacros('$SERVICEDESC$', $svcMock), $svcMock->service_description);
+ $this->assertEquals(Macro::resolveMacros('$host.name$', $svcMock), $svcMock->host_name);
+ $this->assertEquals(Macro::resolveMacros('$host.address$', $svcMock), $svcMock->host_address);
+ $this->assertEquals(Macro::resolveMacros('$service.description$', $svcMock), $svcMock->service_description);
+ }
+
+ public function testCustomvars()
+ {
+ $objectMock = Mockery::mock('object');
+ $objectMock->customvars = array(
+ 'CUSTOMVAR' => 'test'
+ );
+
+ $this->assertEquals(Macro::resolveMacros('$CUSTOMVAR$', $objectMock), $objectMock->customvars['CUSTOMVAR']);
+ }
+
+ public function testFaultyMacros()
+ {
+ $hostMock = Mockery::mock('host');
+ $hostMock->host_name = 'test';
+ $hostMock->customvars = array(
+ 'HOST' => 'te',
+ 'NAME' => 'st'
+ );
+
+ $this->assertEquals(
+ Macro::resolveMacros('$$HOSTNAME$ $ HOSTNAME$ $HOST$NAME$', $hostMock),
+ '$test $ HOSTNAME$ teNAME$'
+ );
+ }
+
+ public function testMacrosWithSpecialCharacters()
+ {
+ $objectMock = Mockery::mock('object');
+ $objectMock->customvars = array(
+ 'V€RY_SP3C|@L' => 'not too special!'
+ );
+
+ $this->assertEquals(
+ Macro::resolveMacros('$V€RY_SP3C|@L$', $objectMock),
+ $objectMock->customvars['V€RY_SP3C|@L']
+ );
+ }
+}
diff --git a/modules/setup/application/forms/AdminAccountPage.php b/modules/setup/application/forms/AdminAccountPage.php
index b123b142d..a7bd8fc6c 100644
--- a/modules/setup/application/forms/AdminAccountPage.php
+++ b/modules/setup/application/forms/AdminAccountPage.php
@@ -8,8 +8,8 @@ use LogicException;
use Icinga\Web\Form;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
-use Icinga\Authentication\Backend\DbUserBackend;
-use Icinga\Authentication\Backend\LdapUserBackend;
+use Icinga\Authentication\User\DbUserBackend;
+use Icinga\Authentication\User\LdapUserBackend;
/**
* Wizard page to define the initial administrative account
@@ -268,13 +268,8 @@ class AdminAccountPage extends Form
if ($this->backendConfig['backend'] === 'db') {
$backend = new DbUserBackend(ResourceFactory::createResource(new ConfigObject($this->resourceConfig)));
} elseif ($this->backendConfig['backend'] === 'ldap') {
- $backend = new LdapUserBackend(
- ResourceFactory::createResource(new ConfigObject($this->resourceConfig)),
- $this->backendConfig['user_class'],
- $this->backendConfig['user_name_attribute'],
- $this->backendConfig['base_dn'],
- $this->backendConfig['filter']
- );
+ $backend = new LdapUserBackend(ResourceFactory::createResource(new ConfigObject($this->resourceConfig)));
+ $backend->setConfig($this->backendConfig);
} else {
throw new LogicException(
sprintf(
@@ -285,10 +280,8 @@ class AdminAccountPage extends Form
}
try {
- $users = $backend->listUsers();
- natsort ($users);
- return $users;
- } catch (Exception $e) {
+ return $backend->select(array('user_name'))->fetchColumn();
+ } catch (Exception $_) {
// No need to handle anything special here. Error means no users found.
return array();
}
diff --git a/modules/setup/application/forms/AuthBackendPage.php b/modules/setup/application/forms/AuthBackendPage.php
index f3bce41fa..bb68792a6 100644
--- a/modules/setup/application/forms/AuthBackendPage.php
+++ b/modules/setup/application/forms/AuthBackendPage.php
@@ -4,9 +4,9 @@
namespace Icinga\Module\Setup\Forms;
use Icinga\Web\Form;
-use Icinga\Forms\Config\Authentication\DbBackendForm;
-use Icinga\Forms\Config\Authentication\LdapBackendForm;
-use Icinga\Forms\Config\Authentication\ExternalBackendForm;
+use Icinga\Forms\Config\UserBackend\DbBackendForm;
+use Icinga\Forms\Config\UserBackend\LdapBackendForm;
+use Icinga\Forms\Config\UserBackend\ExternalBackendForm;
use Icinga\Data\ConfigObject;
/**
@@ -105,7 +105,7 @@ class AuthBackendPage extends Form
}
if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) {
- if ($this->config['type'] === 'ldap' && false === LdapBackendForm::isValidAuthenticationBackend($this)) {
+ if ($this->config['type'] === 'ldap' && false === LdapBackendForm::isValidUserBackend($this)) {
$this->addSkipValidationCheckbox();
return false;
}
diff --git a/modules/setup/library/Setup/Steps/AuthenticationStep.php b/modules/setup/library/Setup/Steps/AuthenticationStep.php
index 9de0c9867..58d9d68d7 100644
--- a/modules/setup/library/Setup/Steps/AuthenticationStep.php
+++ b/modules/setup/library/Setup/Steps/AuthenticationStep.php
@@ -7,7 +7,7 @@ use Exception;
use Icinga\Application\Config;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
-use Icinga\Authentication\Backend\DbUserBackend;
+use Icinga\Authentication\User\DbUserBackend;
use Icinga\Module\Setup\Step;
class AuthenticationStep extends Step
@@ -88,11 +88,12 @@ class AuthenticationStep extends Step
ResourceFactory::createResource(new ConfigObject($this->data['adminAccountData']['resourceConfig']))
);
- if (array_search($this->data['adminAccountData']['username'], $backend->listUsers()) === false) {
- $backend->addUser(
- $this->data['adminAccountData']['username'],
- $this->data['adminAccountData']['password']
- );
+ if ($backend->select()->where('user_name', $this->data['adminAccountData']['username'])->count() === 0) {
+ $backend->insert('user', array(
+ 'user_name' => $this->data['adminAccountData']['username'],
+ 'password' => $this->data['adminAccountData']['password'],
+ 'is_active' => true
+ ));
}
} catch (Exception $e) {
$this->dbError = $e;
diff --git a/public/css/icinga/forms.less b/public/css/icinga/forms.less
index c8b5df161..b5d5bca01 100644
--- a/public/css/icinga/forms.less
+++ b/public/css/icinga/forms.less
@@ -96,11 +96,6 @@ select::-moz-focus-inner {
outline: 0;
}
-input:disabled, select:disabled {
- background-color: #fff;
- border-color: white;
-}
-
form.inline {
margin: 0;
padding: 0;
@@ -263,4 +258,4 @@ form > div.header {
h1, h2, h3, h4, h5, h6 {
display: inline-block;
}
-}
\ No newline at end of file
+}
diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less
index b442723cb..18bc03a57 100644
--- a/public/css/icinga/main-content.less
+++ b/public/css/icinga/main-content.less
@@ -41,6 +41,10 @@ img.icon {
background-position: 1em center;
}
+#notifications > li.info {
+ background-color: @colorFormNotificationInfo;
+}
+
#notifications > li.warning {
background-color: @colorWarningHandled;
}
@@ -144,6 +148,11 @@ table.perfdata {
font-size: 0.9em;
}
+table.perfdata.perfdata-piecharts {
+ left: -2.6em;
+ position: relative;
+}
+
table.perfdata th {
padding: 0;
text-align: left;
@@ -202,3 +211,164 @@ table.benchmark {
border: 1px solid lightgrey;
background-color: #fbfcc5;
}
+
+div.content.users {
+ table.user-list {
+ th.user-remove {
+ width: 8em;
+ padding-right: 0.5em;
+ text-align: right;
+ }
+
+ td.user-remove {
+ text-align: right;
+ }
+ }
+
+ p {
+ margin-top: 0;
+ }
+
+ a.user-add {
+ display: block;
+ margin-top: 1em;
+ }
+}
+
+div.controls div.user-header {
+ border-bottom: 2px solid @colorPetrol;
+ margin-bottom: 1em;
+
+ .user-name {
+ display: inline-block;
+ margin: 0 0 0.3em;
+ font-size: 2em;
+ }
+
+ .user-state, .user-created, .user-modified {
+ margin: 0 0 0.2em;
+ font-size: 0.8em;
+ }
+}
+
+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.controls div.group-header {
+ border-bottom: 2px solid @colorPetrol;
+ margin-bottom: 1em;
+
+ .group-name {
+ display: inline-block;
+ margin: 0 0 0.3em;
+ font-size: 2em;
+ }
+
+ .group-parent, .group-created, .group-modified {
+ margin: 0 0 0.2em;
+ font-size: 0.8em;
+ }
+}
+
+div.content.members {
+ table.member-list {
+ th.member-remove {
+ width: 8em;
+ padding-right: 0.5em;
+ text-align: right;
+ }
+
+ td.member-remove {
+ text-align: right;
+
+ form button.link-like {
+ color: inherit;
+ }
+ }
+ }
+
+ p {
+ margin-top: 0;
+ }
+
+ a.member-add {
+ display: block;
+ margin-top: 1em;
+ }
+}
+
+form.backend-selection {
+ float: right;
+
+ div.element {
+ margin: 0;
+
+ label {
+ width: auto;
+ margin-right: 0.5em;
+ }
+
+ select {
+ width: 11.5em;
+ margin-left: 0;
+ }
+ }
+}
+
+table.usergroupbackend-list {
+ th.backend-remove {
+ width: 8em;
+ text-align: right;
+ }
+
+ td.backend-remove {
+ text-align: right;
+ }
+}
diff --git a/public/css/icinga/monitoring-colors.less b/public/css/icinga/monitoring-colors.less
index 2b2b70b27..7f1499f02 100644
--- a/public/css/icinga/monitoring-colors.less
+++ b/public/css/icinga/monitoring-colors.less
@@ -191,7 +191,6 @@ tr[href]:hover {
}
tr.state[href]:hover td.state {
- color: white;
background-color: #eee;
}
diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js
index cc927feb6..84354c960 100644
--- a/public/js/icinga/events.js
+++ b/public/js/icinga/events.js
@@ -274,6 +274,10 @@
}
}
+ // Disable all form controls to prevent resubmission except for our search input
+ // Note that disabled form inputs will not be enabled via JavaScript again
+ $form.find(':input:not(#search):not(:disabled)').prop('disabled', true);
+
icinga.loader.loadUrl(url, $target, data, method);
return false;
diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js
index 99667ac1c..082b41e87 100644
--- a/public/js/icinga/loader.js
+++ b/public/js/icinga/loader.js
@@ -310,7 +310,7 @@
} else {
if (req.$target.attr('id') === 'col2') { // TODO: multicol
- if ($('#col1').data('icingaUrl') === redirect) {
+ if ($('#col1').data('icingaUrl').split('?')[0] === redirect.split('?')[0]) {
icinga.ui.layout1col();
req.$target = $('#col1');
delete(this.requests['col2']);
diff --git a/test/php/application/forms/Config/Authentication/DbBackendFormTest.php b/test/php/application/forms/Config/UserBackend/DbBackendFormTest.php
similarity index 72%
rename from test/php/application/forms/Config/Authentication/DbBackendFormTest.php
rename to test/php/application/forms/Config/UserBackend/DbBackendFormTest.php
index fb93f6050..d58ff8a33 100644
--- a/test/php/application/forms/Config/Authentication/DbBackendFormTest.php
+++ b/test/php/application/forms/Config/UserBackend/DbBackendFormTest.php
@@ -1,7 +1,7 @@
setUpResourceFactoryMock();
- Mockery::mock('overload:Icinga\Authentication\Backend\DbUserBackend')
- ->shouldReceive('count')
+ Mockery::mock('overload:Icinga\Authentication\User\DbUserBackend')
+ ->shouldReceive('select->where->count')
->andReturn(2);
// Passing array(null) is required to make Mockery call the constructor...
- $form = Mockery::mock('Icinga\Forms\Config\Authentication\DbBackendForm[getView]', array(null));
+ $form = Mockery::mock('Icinga\Forms\Config\UserBackend\DbBackendForm[getView]', array(null));
$form->shouldReceive('getView->escape')
->with(Mockery::type('string'))
->andReturnUsing(function ($s) { return $s; });
@@ -41,8 +41,8 @@ class DbBackendFormTest extends BaseTestCase
$form->populate(array('resource' => 'test_db_backend'));
$this->assertTrue(
- DbBackendForm::isValidAuthenticationBackend($form),
- 'DbBackendForm claims that a valid authentication backend with users is not valid'
+ DbBackendForm::isValidUserBackend($form),
+ 'DbBackendForm claims that a valid user backend with users is not valid'
);
}
@@ -53,12 +53,12 @@ class DbBackendFormTest extends BaseTestCase
public function testInvalidBackendIsNotValid()
{
$this->setUpResourceFactoryMock();
- Mockery::mock('overload:Icinga\Authentication\Backend\DbUserBackend')
+ Mockery::mock('overload:Icinga\Authentication\User\DbUserBackend')
->shouldReceive('count')
->andReturn(0);
// Passing array(null) is required to make Mockery call the constructor...
- $form = Mockery::mock('Icinga\Forms\Config\Authentication\DbBackendForm[getView]', array(null));
+ $form = Mockery::mock('Icinga\Forms\Config\UserBackend\DbBackendForm[getView]', array(null));
$form->shouldReceive('getView->escape')
->with(Mockery::type('string'))
->andReturnUsing(function ($s) { return $s; });
@@ -67,8 +67,8 @@ class DbBackendFormTest extends BaseTestCase
$form->populate(array('resource' => 'test_db_backend'));
$this->assertFalse(
- DbBackendForm::isValidAuthenticationBackend($form),
- 'DbBackendForm claims that an invalid authentication backend without users is valid'
+ DbBackendForm::isValidUserBackend($form),
+ 'DbBackendForm claims that an invalid user backend without users is valid'
);
}
diff --git a/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php b/test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php
similarity index 74%
rename from test/php/application/forms/Config/Authentication/LdapBackendFormTest.php
rename to test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php
index 56ea08987..f7373a7ae 100644
--- a/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php
+++ b/test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php
@@ -1,7 +1,7 @@
setUpResourceFactoryMock();
- Mockery::mock('overload:Icinga\Authentication\Backend\LdapUserBackend')
- ->shouldReceive('assertAuthenticationPossible')->andReturnNull();
+ Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend')
+ ->shouldReceive('assertAuthenticationPossible')->andReturnNull()
+ ->shouldReceive('setConfig')->andReturnNull();
// Passing array(null) is required to make Mockery call the constructor...
- $form = Mockery::mock('Icinga\Forms\Config\Authentication\LdapBackendForm[getView]', array(null));
+ $form = Mockery::mock('Icinga\Forms\Config\UserBackend\LdapBackendForm[getView]', array(null));
$form->shouldReceive('getView->escape')
->with(Mockery::type('string'))
->andReturnUsing(function ($s) { return $s; });
@@ -41,8 +42,8 @@ class LdapBackendFormTest extends BaseTestCase
$form->populate(array('resource' => 'test_ldap_backend'));
$this->assertTrue(
- LdapBackendForm::isValidAuthenticationBackend($form),
- 'LdapBackendForm claims that a valid authentication backend with users is not valid'
+ LdapBackendForm::isValidUserBackend($form),
+ 'LdapBackendForm claims that a valid user backend with users is not valid'
);
}
@@ -53,11 +54,11 @@ class LdapBackendFormTest extends BaseTestCase
public function testInvalidBackendIsNotValid()
{
$this->setUpResourceFactoryMock();
- Mockery::mock('overload:Icinga\Authentication\Backend\LdapUserBackend')
+ Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend')
->shouldReceive('assertAuthenticationPossible')->andThrow(new AuthenticationException);
// Passing array(null) is required to make Mockery call the constructor...
- $form = Mockery::mock('Icinga\Forms\Config\Authentication\LdapBackendForm[getView]', array(null));
+ $form = Mockery::mock('Icinga\Forms\Config\UserBackend\LdapBackendForm[getView]', array(null));
$form->shouldReceive('getView->escape')
->with(Mockery::type('string'))
->andReturnUsing(function ($s) { return $s; });
@@ -66,8 +67,8 @@ class LdapBackendFormTest extends BaseTestCase
$form->populate(array('resource' => 'test_ldap_backend'));
$this->assertFalse(
- LdapBackendForm::isValidAuthenticationBackend($form),
- 'LdapBackendForm claims that an invalid authentication backend without users is valid'
+ LdapBackendForm::isValidUserBackend($form),
+ 'LdapBackendForm claims that an invalid user backend without users is valid'
);
}
diff --git a/test/php/application/forms/Config/AuthenticationBackendReorderFormTest.php b/test/php/application/forms/Config/UserBackendReorderFormTest.php
similarity index 64%
rename from test/php/application/forms/Config/AuthenticationBackendReorderFormTest.php
rename to test/php/application/forms/Config/UserBackendReorderFormTest.php
index 23563f31b..240d578be 100644
--- a/test/php/application/forms/Config/AuthenticationBackendReorderFormTest.php
+++ b/test/php/application/forms/Config/UserBackendReorderFormTest.php
@@ -5,10 +5,10 @@ namespace Tests\Icinga\Forms\Config;
use Icinga\Test\BaseTestCase;
use Icinga\Application\Config;
-use Icinga\Forms\Config\AuthenticationBackendConfigForm;
-use Icinga\Forms\Config\AuthenticationBackendReorderForm;
+use Icinga\Forms\Config\UserBackendConfigForm;
+use Icinga\Forms\Config\UserBackendReorderForm;
-class AuthenticationBackendConfigFormWithoutSave extends AuthenticationBackendConfigForm
+class UserBackendConfigFormWithoutSave extends UserBackendConfigForm
{
public static $newConfig;
@@ -19,11 +19,11 @@ class AuthenticationBackendConfigFormWithoutSave extends AuthenticationBackendCo
}
}
-class AuthenticationBackendReorderFormProvidingConfigFormWithoutSave extends AuthenticationBackendReorderForm
+class UserBackendReorderFormProvidingConfigFormWithoutSave extends UserBackendReorderForm
{
public function getConfigForm()
{
- $form = new AuthenticationBackendConfigFormWithoutSave();
+ $form = new UserBackendConfigFormWithoutSave();
$form->setIniConfig($this->config);
return $form;
}
@@ -45,7 +45,7 @@ class AuthenticationBackendReorderFormTest extends BaseTestCase
->shouldReceive('isPost')->andReturn(true)
->shouldReceive('getPost')->andReturn(array('backend_newpos' => 'test3|1'));
- $form = new AuthenticationBackendReorderFormProvidingConfigFormWithoutSave();
+ $form = new UserBackendReorderFormProvidingConfigFormWithoutSave();
$form->setIniConfig($config);
$form->setTokenDisabled();
$form->setUidDisabled();
@@ -53,8 +53,8 @@ class AuthenticationBackendReorderFormTest extends BaseTestCase
$this->assertEquals(
array('test1', 'test3', 'test2'),
- AuthenticationBackendConfigFormWithoutSave::$newConfig->keys(),
- 'Moving elements with AuthenticationBackendReorderForm does not seem to properly work'
+ UserBackendConfigFormWithoutSave::$newConfig->keys(),
+ 'Moving elements with UserBackendReorderForm does not seem to properly work'
);
}
}
diff --git a/test/php/library/Icinga/Data/ConfigObjectTest.php b/test/php/library/Icinga/Data/ConfigObjectTest.php
index eabdf80cf..820d17b83 100644
--- a/test/php/library/Icinga/Data/ConfigObjectTest.php
+++ b/test/php/library/Icinga/Data/ConfigObjectTest.php
@@ -60,14 +60,6 @@ class ConfigObjectTest extends BaseTestCase
);
}
- public function testWhetherConfigObjectsAreCountable()
- {
- $config = new ConfigObject(array('a' => 'b', 'c' => array('d' => 'e')));
-
- $this->assertInstanceOf('Countable', $config, 'ConfigObject objects do not implement interface `Countable\'');
- $this->assertEquals(2, count($config), 'ConfigObject objects do not count properties and sections correctly');
- }
-
public function testWhetherConfigObjectsAreTraversable()
{
$config = new ConfigObject(array('a' => 'b', 'c' => 'd'));
@@ -124,7 +116,7 @@ class ConfigObjectTest extends BaseTestCase
}
/**
- * @expectedException LogicException
+ * @expectedException \Icinga\Exception\ProgrammingError
*/
public function testWhetherItIsNotPossibleToAppendProperties()
{
@@ -142,9 +134,6 @@ class ConfigObjectTest extends BaseTestCase
$this->assertFalse(isset($config->c), 'ConfigObjects do not allow to unset sections');
}
- /**
- * @depends testWhetherConfigObjectsAreCountable
- */
public function testWhetherOneCanCheckIfAConfigObjectHasAnyPropertiesOrSections()
{
$config = new ConfigObject();
diff --git a/test/php/library/Icinga/File/Ini/IniWriterTest.php b/test/php/library/Icinga/File/Ini/IniWriterTest.php
index 4dc94a63a..2d9659675 100644
--- a/test/php/library/Icinga/File/Ini/IniWriterTest.php
+++ b/test/php/library/Icinga/File/Ini/IniWriterTest.php
@@ -521,12 +521,10 @@ EOD
key1 = "1"
key2 = "2"
-
[two]
a.b = "c"
d.e = "f"
-
[three]
key = "value"
foo.bar = "raboof"
@@ -537,12 +535,10 @@ EOD;
key = "value"
foo.bar = "raboof"
-
[two]
a.b = "c"
d.e = "f"
-
[one]
key1 = "1"
key2 = "2"
@@ -589,7 +585,6 @@ EOD;
; comment 1
[one]
-
; comment 2
[two]
EOD;
@@ -598,7 +593,6 @@ EOD;
; comment 2
[two]
-
; comment 1
[one]
EOD;
@@ -637,8 +631,8 @@ EOD;
);
$this->assertEquals(
- $config,
- $writer->render(),
+ trim($config),
+ trim($writer->render()),
'IniWriter does not preserve comments on empty lines'
);
}
@@ -667,8 +661,8 @@ EOD;
);
$this->assertEquals(
- $config,
- $writer->render(),
+ trim($config),
+ trim($writer->render()),
'IniWriter does not preserve comments on property lines'
);
}
@@ -686,8 +680,8 @@ EOD;
);
$this->assertEquals(
- $config,
- $writer->render(),
+ trim($config),
+ trim($writer->render()),
'IniWriter does not preserve comments on empty section lines'
);
}
@@ -719,8 +713,8 @@ EOD;
);
$this->assertEquals(
- $config,
- $writer->render(),
+ trim($config),
+ trim($writer->render()),
'IniWriter does not preserve comments on property lines'
);
}
diff --git a/test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php b/test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php
deleted file mode 100644
index 182004703..000000000
--- a/test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php
+++ /dev/null
@@ -1,258 +0,0 @@
-getAttributesMock;
- }
-
- function ldap_start_tls()
- {
- global $self;
- $self->startTlsCalled = true;
- }
-
- function ldap_set_option($ds, $option, $value)
- {
- global $self;
- $self->activatedOptions[$option] = $value;
- return true;
- }
-
- function ldap_set($ds, $option)
- {
- global $self;
- $self->activatedOptions[] = $option;
- }
-
- function ldap_control_paged_result()
- {
- global $self;
- $self->pagedResultsCalled = true;
- return true;
- }
-
- function ldap_control_paged_result_response()
- {
- return true;
- }
-
- function ldap_get_dn()
- {
- return NULL;
- }
-
- function ldap_free_result()
- {
- return NULL;
- }
- }
-
- private function node(&$element, $name)
- {
- $element['count']++;
- $element[$name] = array('count' => 0);
- $element[] = $name;
- }
-
- private function addEntry(&$element, $name, $entry)
- {
- $element[$name]['count']++;
- $element[$name][] = $entry;
- }
-
- private function mockQuery()
- {
- return Mockery::mock('overload:Icinga\Protocol\Ldap\Query')
- ->shouldReceive(array(
- 'from' => Mockery::self(),
- 'create' => array('count' => 1),
- 'listFields' => array('count' => 1),
- 'getLimit' => 1,
- 'hasOffset' => false,
- 'hasBase' => false,
- 'getSortColumns' => array(),
- 'getUsePagedResults' => true
- ));
- }
-
- private function connectionFetchAll()
- {
- $this->mockQuery();
- $this->connection->connect();
- $this->connection->fetchAll(Mockery::self());
- }
-
- public function setUp()
- {
- $this->pagedResultsCalled = false;
- $this->startTlsCalled = false;
- $this->activatedOptions = array();
-
- $this->mockLdapFunctions();
-
- $config = new ConfigObject(
- array(
- 'hostname' => 'localhost',
- 'root_dn' => 'dc=example,dc=com',
- 'bind_dn' => 'cn=user,ou=users,dc=example,dc=com',
- 'bind_pw' => '***'
- )
- );
- $this->connection = new Connection($config);
-
- $caps = array('count' => 0);
- $this->node($caps, 'defaultNamingContext');
- $this->node($caps, 'namingContexts');
- $this->node($caps, 'supportedCapabilities');
- $this->node($caps, 'supportedControl');
- $this->node($caps, 'supportedLDAPVersion');
- $this->node($caps, 'supportedExtension');
- $this->getAttributesMock = $caps;
- }
-
- public function testUsePageControlWhenAnnounced()
- {
- if (version_compare(PHP_VERSION, '5.4.0') < 0) {
- $this->markTestSkipped('Page control needs at least PHP_VERSION 5.4.0');
- }
-
- $this->addEntry($this->getAttributesMock, 'supportedControl', Capability::LDAP_PAGED_RESULT_OID_STRING);
- $this->connectionFetchAll();
-
- // see ticket #7993
- $this->assertEquals(true, $this->pagedResultsCalled, "Use paged result when capability is present.");
- }
-
- public function testDontUsePagecontrolWhenNotAnnounced()
- {
- if (version_compare(PHP_VERSION, '5.4.0') < 0) {
- $this->markTestSkipped('Page control needs at least PHP_VERSION 5.4.0');
- }
- $this->connectionFetchAll();
-
- // see ticket #8490
- $this->assertEquals(false, $this->pagedResultsCalled, "Don't use paged result when capability is not announced.");
- }
-
- public function testUseLdapV2WhenAnnounced()
- {
- // TODO: Test turned off, see other TODO in Ldap/Connection.
- $this->markTestSkipped('LdapV2 currently turned off.');
-
- $this->addEntry($this->getAttributesMock, 'supportedLDAPVersion', 2);
- $this->connectionFetchAll();
-
- $this->assertArrayHasKey(LDAP_OPT_PROTOCOL_VERSION, $this->activatedOptions, "LDAP version must be set");
- $this->assertEquals($this->activatedOptions[LDAP_OPT_PROTOCOL_VERSION], 2);
- }
-
- public function testUseLdapV3WhenAnnounced()
- {
- $this->addEntry($this->getAttributesMock, 'supportedLDAPVersion', 3);
- $this->connectionFetchAll();
-
- $this->assertArrayHasKey(LDAP_OPT_PROTOCOL_VERSION, $this->activatedOptions, "LDAP version must be set");
- $this->assertEquals($this->activatedOptions[LDAP_OPT_PROTOCOL_VERSION], 3, "LDAPv3 must be active");
- }
-
- public function testDefaultSettings()
- {
- $this->connectionFetchAll();
-
- $this->assertArrayHasKey(LDAP_OPT_PROTOCOL_VERSION, $this->activatedOptions, "LDAP version must be set");
- $this->assertEquals($this->activatedOptions[LDAP_OPT_PROTOCOL_VERSION], 3, "LDAPv3 must be active");
-
- $this->assertArrayHasKey(LDAP_OPT_REFERRALS, $this->activatedOptions, "Following referrals must be turned off");
- $this->assertEquals($this->activatedOptions[LDAP_OPT_REFERRALS], 0, "Following referrals must be turned off");
- }
-
-
- public function testActiveDirectoryDiscovery()
- {
- $this->addEntry($this->getAttributesMock, 'supportedCapabilities', Capability::LDAP_CAP_ACTIVE_DIRECTORY_OID);
- $this->connectionFetchAll();
-
- $this->assertEquals(true, $this->connection->getCapabilities()->hasAdOid(),
- "Server with LDAP_CAP_ACTIVE_DIRECTORY_OID must be recognized as Active Directory.");
- }
-
- public function testDefaultNamingContext()
- {
- $this->addEntry($this->getAttributesMock, 'defaultNamingContext', 'dn=default,dn=contex');
- $this->connectionFetchAll();
-
- $this->assertEquals('dn=default,dn=contex', $this->connection->getCapabilities()->getDefaultNamingContext(),
- 'Default naming context must be correctly recognized.');
- }
-
- public function testDefaultNamingContextFallback()
- {
- $this->addEntry($this->getAttributesMock, 'namingContexts', 'dn=some,dn=other,dn=context');
- $this->addEntry($this->getAttributesMock, 'namingContexts', 'dn=default,dn=context');
- $this->connectionFetchAll();
-
- $this->assertEquals('dn=some,dn=other,dn=context', $this->connection->getCapabilities()->getDefaultNamingContext(),
- 'If defaultNamingContext is missing, the connection must fallback to first namingContext.');
- }
-}
diff --git a/test/php/library/Icinga/Protocol/Ldap/QueryTest.php b/test/php/library/Icinga/Protocol/Ldap/QueryTest.php
index 589dc8f36..44e47df78 100644
--- a/test/php/library/Icinga/Protocol/Ldap/QueryTest.php
+++ b/test/php/library/Icinga/Protocol/Ldap/QueryTest.php
@@ -36,51 +36,11 @@ class QueryTest extends BaseTestCase
return $select;
}
- public function testLimit()
- {
- $select = $this->prepareSelect();
- $this->assertEquals(10, $select->getLimit());
- $this->assertEquals(4, $select->getOffset());
- }
-
- public function testHasLimit()
- {
- $select = $this->emptySelect();
- $this->assertFalse($select->hasLimit());
- $select = $this->prepareSelect();
- $this->assertTrue($select->hasLimit());
- }
-
- public function testHasOffset()
- {
- $select = $this->emptySelect();
- $this->assertFalse($select->hasOffset());
- $select = $this->prepareSelect();
- $this->assertTrue($select->hasOffset());
- }
-
- public function testGetLimit()
- {
- $select = $this->prepareSelect();
- $this->assertEquals(10, $select->getLimit());
- }
-
- public function testGetOffset()
- {
- $select = $this->prepareSelect();
- $this->assertEquals(10, $select->getLimit());
- }
-
public function testFetchTree()
{
$this->markTestIncomplete('testFetchTree is not implemented yet - requires real LDAP');
}
- public function testFrom()
- {
- return $this->testListFields();
- }
-
public function testWhere()
{
$this->markTestIncomplete('testWhere is not implemented yet');
@@ -88,30 +48,13 @@ class QueryTest extends BaseTestCase
public function testOrder()
{
- $select = $this->emptySelect()->order('bla');
- // tested by testGetSortColumns
+ $this->markTestIncomplete('testOrder is not implemented yet, order support for ldap queries is incomplete');
}
- public function testListFields()
- {
- $select = $this->prepareSelect();
- $this->assertEquals(
- array('testIntColumn', 'testStringColumn'),
- $select->listFields()
- );
- }
-
- public function testGetSortColumns()
- {
- $select = $this->prepareSelect();
- $cols = $select->getSortColumns();
- $this->assertEquals('testIntColumn', $cols[0][0]);
- }
-
- public function testCreateQuery()
+ public function testRenderFilter()
{
$select = $this->prepareSelect();
$res = '(&(objectClass=dummyClass)(testIntColumn=1)(testStringColumn=test)(testWildcard=abc*))';
- $this->assertEquals($res, $select->create());
+ $this->assertEquals($res, (string) $select);
}
}
diff --git a/test/php/library/Icinga/UserTest.php b/test/php/library/Icinga/UserTest.php
index dc55dc62d..97cf412c0 100644
--- a/test/php/library/Icinga/UserTest.php
+++ b/test/php/library/Icinga/UserTest.php
@@ -67,13 +67,15 @@ class UserTest extends BaseTestCase
'test',
'test/some/specific',
'test/more/*',
- 'test/wildcard-with-wildcard/*'
+ 'test/wildcard-with-wildcard/*',
+ 'test/even-more/specific-with-wildcard/*'
));
$this->assertTrue($user->can('test'));
$this->assertTrue($user->can('test/some/specific'));
$this->assertTrue($user->can('test/more/everything'));
$this->assertTrue($user->can('test/wildcard-with-wildcard/*'));
$this->assertTrue($user->can('test/wildcard-with-wildcard/sub/sub'));
+ $this->assertTrue($user->can('test/even-more/*'));
$this->assertFalse($user->can('not/test'));
$this->assertFalse($user->can('test/some/not/so/specific'));
$this->assertFalse($user->can('test/wildcard2/*'));