Merge branch 'master' into feature/restrict-custom-variables-10965

This commit is contained in:
Eric Lippmann 2016-04-13 15:44:12 +02:00
commit 4d488ab354
83 changed files with 2168 additions and 1407 deletions

View File

@ -16,7 +16,7 @@ Icinga Core and any other monitoring backend compatible with the IDO database.
## Installation
For installing Icinga Web 2 please read [doc/installation.md](doc/installation.md).
For installing Icinga Web 2 please read [doc/02-Installation.md](doc/02-Installation.md).
## Support

View File

@ -243,6 +243,20 @@ class DashboardController extends ActionController
$this->createTabs();
if (! $this->dashboard->hasPanes()) {
$this->view->title = 'Dashboard';
} else {
$panes = array_filter(
$this->dashboard->getPanes(),
function ($pane) {
return ! $pane->getDisabled();
}
);
if (empty($panes)) {
$this->view->title = 'Dashboard';
$this->getTabs()->add('dashboard', array(
'active' => true,
'title' => $this->translate('Dashboard'),
'url' => Url::fromRequest()
));
} else {
if ($this->_getParam('pane')) {
$pane = $this->_getParam('pane');
@ -261,6 +275,7 @@ class DashboardController extends ActionController
}
}
}
}
/**
* Setting dialog

View File

@ -18,6 +18,7 @@
</thead>
<tbody>
<?php foreach ($this->dashboard->getPanes() as $pane): ?>
<?php if ($pane->getDisabled()) continue; ?>
<tr style="background-color: #f1f1f1;">
<th colspan="2" style="text-align: left; padding: 0.5em;">
<?= $this->escape($pane->getName()) ?>
@ -43,7 +44,7 @@
</tr>
<?php else: ?>
<?php foreach ($dashlets as $dashlet): ?>
<?php if ($dashlet->getDisabled() === true) continue; ?>
<?php if ($dashlet->getDisabled()) continue; ?>
<tr>
<td>
<?= $this->qlink(

View File

@ -5,16 +5,14 @@ use Icinga\Data\Reducible;
if (! $this->compact): ?>
<div class="controls separated">
<?= $tabs; ?>
<div class="grid">
<?= $this->sortBox ?>
<?= $this->limiter ?>
<?= $this->tabs ?>
<?= $this->paginator ?>
<div class="sort-controls-container">
<?= $this->limiter ?>
<?= $this->sortBox ?>
</div>
<div>
<?= $this->backendSelection; ?>
<?= $this->filterEditor; ?>
</div>
<?= $this->backendSelection ?>
<?= $this->filterEditor ?>
</div>
<?php endif ?>
<div class="content">
@ -45,8 +43,7 @@ if (! isset($backend)) {
<?php if (! $groups->hasResult()): ?>
<p><?= $this->translate('No user groups found matching the filter'); ?></p>
</div>
<?php endif ?>
<?php return; endif ?>
<table data-base-target="_next" class="table-row-selectable common-table">
<thead>
<tr>
@ -85,8 +82,9 @@ if (! isset($backend)) {
'group' => $group->group_name
),
array(
'class' => 'action-link',
'title' => sprintf($this->translate('Remove user group %s'), $group->group_name),
'icon' => 'trash'
'icon' => 'cancel'
)
); ?>
</td>

View File

@ -5,16 +5,14 @@ use Icinga\Data\Reducible;
if (! $this->compact): ?>
<div class="controls separated">
<?= $tabs ?>
<div class="grid">
<?= $this->sortBox ?>
<?= $this->limiter ?>
<?= $this->tabs ?>
<?= $this->paginator ?>
<div class="sort-controls-container">
<?= $this->limiter ?>
<?= $this->sortBox ?>
</div>
<div>
<?= $this->backendSelection ?>
<?= $this->filterEditor ?>
</div>
</div>
<?php endif ?>
<div class="content">

View File

@ -4,7 +4,7 @@ The preferred way of installing Icinga Web 2 is to use the official package repo
system and distribution you are running. But it is also possible to install Icinga Web 2 directly from source.
In case you are upgrading from an older version of Icinga Web 2
please make sure to read the [upgrading](installation.md#upgrading) section
please make sure to read the [upgrading](02-Installation.md#upgrading) section
thoroughly.
## <a id="installing-requirements"></a> Installing Requirements
@ -179,7 +179,7 @@ git clone git://git.icinga.org/icingaweb2.git
### <a id="installing-from-source-requirements"></a> Installing Requirements from Source
You will need to install certain dependencies depending on your setup listed [here](installation.md#installing-requirements).
You will need to install certain dependencies depending on your setup listed [here](02-Installation.md#installing-requirements).
The following example installs Apache2 as web server, MySQL as RDBMS and uses the PHP adapter for MySQL.
Adopt the package requirements to your needs (e.g. adding ldap for authentication) and distribution.
@ -318,7 +318,7 @@ Puppet, Ansible, Chef, etc. modules.
> Read the documentation on the respective linked configuration sections before
> deploying the configuration manually.
>
> If you are unsure about certain settings, use the [setup wizard](installation.md#web-setup-wizard-from-source) once
> If you are unsure about certain settings, use the [setup wizard](02-Installation.md#web-setup-wizard-from-source) once
> and then collect the generated configuration as well as sql dumps.
#### <a id="web-setup-manual-from-source-database"></a> Icinga Web 2 Manual Database Setup
@ -336,7 +336,7 @@ mysql -p icingaweb2 < /usr/share/icingaweb2/etc/schema/mysql.schema.sql
```
Then generate a new password hash as described in the [authentication docs](authentication.md#authentication-configuration-db-setup)
Then generate a new password hash as described in the [authentication docs](05-Authentication.md#authentication-configuration-db-setup)
and use it to insert a new user called `icingaadmin` into the database.
```
@ -349,7 +349,7 @@ quit
#### <a id="web-setup-manual-from-source-config"></a> Icinga Web 2 Manual Configuration
[resources.ini](resources.md#resources) providing the details for the Icinga Web 2 and
[resources.ini](04-Resources.md#resources) providing the details for the Icinga Web 2 and
Icinga 2 IDO database configuration. Example for MySQL:
```
@ -375,7 +375,7 @@ username = "icinga"
password = "icinga"
```
[config.ini](configuration.md#configuration) defining general application settings.
[config.ini](03-Configuration.md#configuration) defining general application settings.
```
vim /etc/icingaweb2/config.ini
@ -391,7 +391,7 @@ type = "db"
resource = "icingaweb2"
```
[authentication.ini](authentication.md#authentication) for e.g. using the previously created database.
[authentication.ini](05-Authentication.md#authentication) for e.g. using the previously created database.
```
vim /etc/icingaweb2/authentication.ini
@ -402,7 +402,7 @@ resource = "icingaweb2"
```
[roles.ini](security.md#security) granting the previously added `icingaadmin` user all permissions.
[roles.ini](06-Security.md#security) granting the previously added `icingaadmin` user all permissions.
```
vim /etc/icingaweb2/roles.ini
@ -415,7 +415,7 @@ permissions = "*"
#### <a id="web-setup-manual-from-source-config-monitoring-module"></a> Icinga Web 2 Manual Configuration Monitoring Module
[config.ini](../modules/monitoring/doc/configuration.md#configuration) defining additional security settings.
**config.ini** defining additional security settings.
```
vim /etc/icingaweb2/modules/monitoring/config.ini
@ -424,7 +424,7 @@ vim /etc/icingaweb2/modules/monitoring/config.ini
protected_customvars = "*pw*,*pass*,community"
```
[backends.ini](../modules/monitoring/doc/configuration.md#configuration) referencing the Icinga 2 DB IDO resource.
**backends.ini** referencing the Icinga 2 DB IDO resource.
```
vim /etc/icingaweb2/modules/monitoring/backends.ini
@ -434,7 +434,7 @@ type = "ido"
resource = "icinga2"
```
[commandtransports.ini](../modules/monitoring/doc/commandtransports.md#commandtransports) defining the Icinga 2 command pipe.
**commandtransports.ini** defining the Icinga command pipe.
```
vim /etc/icingaweb2/modules/monitoring/commandtransports.ini
@ -451,46 +451,20 @@ Finally visit Icinga Web 2 in your browser to login as `icingaadmin` user: `/ici
# <a id="upgrading"></a> Upgrading Icinga Web 2
## <a id="upgrading-to-beta2"></a> Upgrading to Icinga Web 2 Beta 2
## <a id="upgrading-to-2.3.0"></a> Upgrading to Icinga Web 2 2.3.0
Icinga Web 2 Beta 2 introduces access control based on roles for secured actions. If you've already set up Icinga Web 2,
you are required to create the file **roles.ini** beneath Icinga Web 2's configuration directory with the following
content:
```
[administrators]
users = "your_user_name, another_user_name"
permissions = "*"
```
* Icinga Web 2 version 2.3.0 does not introduce any backward incompatible change.
After please log out from Icinga Web 2 and log in again for having all permissions granted.
## <a id="upgrading-to-2.2.0"></a> Upgrading to Icinga Web 2 2.2.0
If you delegated authentication to your web server using the `autologin` backend, you have to switch to the `external`
authentication backend to be able to log in again. The new name better reflects whats going on. A similar change
affects environments that opted for not storing preferences, your new backend is `none`.
* The menu entry `Authorization` beneath `Config` has been renamed to `Authentication`. The role, user backend and user
group backend configuration which was previously found beneath `Authentication` has been moved to `Application`.
## <a id="upgrading-to-beta3"></a> Upgrading to Icinga Web 2 Beta 3
## <a id="upgrading-to-2.1.x"></a> Upgrading to Icinga Web 2 2.1.x
Because Icinga Web 2 Beta 3 does not introduce any backward incompatible change you don't have to change your
configuration files after upgrading to Icinga Web 2 Beta 3.
## <a id="upgrading-to-rc1"></a> Upgrading to Icinga Web 2 Release Candidate 1
The first release candidate of Icinga Web 2 introduces the following non-backward compatible changes:
* The database schema has been adjusted and the tables `icingaweb_group` and
`icingaweb_group_membership` were altered to ensure referential integrity.
Please use the upgrade script located in **etc/schema/** to update your
database schema
* Users who are using PostgreSQL < v9.1 are required to upgrade their
environment to v9.1+ as this is the new minimum required version
for utilizing PostgreSQL as database backend
* The restrictions `monitoring/hosts/filter` and `monitoring/services/filter`
provided by the monitoring module were merged together. The new
restriction is called `monitoring/filter/objects` and supports only a
predefined subset of filter columns. Please see the module's security
related documentation for more details.
* Since Icinga Web 2 version 2.1.3 LDAP user group backends respect the configuration option `group_filter`.
Users who changed the configuration manually and used the option `filter` instead
have to change it back to `group_filter`.
## <a id="upgrading-to-2.0.0"></a> Upgrading to Icinga Web 2 2.0.0
@ -514,13 +488,43 @@ The first release candidate of Icinga Web 2 introduces the following non-backwar
**&lt;config-dir&gt;/preferences/&lt;username&gt;/config.ini**.
The content of the file remains unchanged.
## <a id="upgrading-to-2.1.x"></a> Upgrading to Icinga Web 2 2.1.x
## <a id="upgrading-to-rc1"></a> Upgrading to Icinga Web 2 Release Candidate 1
* Since Icinga Web 2 version 2.1.3 LDAP user group backends respect the configuration option `group_filter`.
Users who changed the configuration manually and used the option `filter` instead
have to change it back to `group_filter`.
The first release candidate of Icinga Web 2 introduces the following non-backward compatible changes:
## <a id="upgrading-to-2.2.0"></a> Upgrading to Icinga Web 2 2.2.0
* The database schema has been adjusted and the tables `icingaweb_group` and
`icingaweb_group_membership` were altered to ensure referential integrity.
Please use the upgrade script located in **etc/schema/** to update your
database schema
* The menu entry `Authorization` beneath `Config` has been renamed to `Authentication`. The role, user backend and user
group backend configuration which was previously found beneath `Authentication` has been moved to `Application`.
* Users who are using PostgreSQL < v9.1 are required to upgrade their
environment to v9.1+ as this is the new minimum required version
for utilizing PostgreSQL as database backend
* The restrictions `monitoring/hosts/filter` and `monitoring/services/filter`
provided by the monitoring module were merged together. The new
restriction is called `monitoring/filter/objects` and supports only a
predefined subset of filter columns. Please see the module's security
related documentation for more details.
## <a id="upgrading-to-beta3"></a> Upgrading to Icinga Web 2 Beta 3
Because Icinga Web 2 Beta 3 does not introduce any backward incompatible change you don't have to change your
configuration files after upgrading to Icinga Web 2 Beta 3.
## <a id="upgrading-to-beta2"></a> Upgrading to Icinga Web 2 Beta 2
Icinga Web 2 Beta 2 introduces access control based on roles for secured actions. If you've already set up Icinga Web 2,
you are required to create the file **roles.ini** beneath Icinga Web 2's configuration directory with the following
content:
```
[administrators]
users = "your_user_name, another_user_name"
permissions = "*"
```
After please log out from Icinga Web 2 and log in again for having all permissions granted.
If you delegated authentication to your web server using the `autologin` backend, you have to switch to the `external`
authentication backend to be able to log in again. The new name better reflects whats going on. A similar change
affects environments that opted for not storing preferences, your new backend is `none`.

15
doc/03-Configuration.md Normal file
View File

@ -0,0 +1,15 @@
# <a id="configuration"></a> Configuration
## Overview
Apart from its web configuration capabilities, the local configuration is
stored in `/etc/icingaweb2` by default (depending on your config setup).
File/Directory | Description
---------------------------------------------------------
config.ini | General configuration (logging, preferences)
[resources.ini](04-Ressources.md) | Global resources (Icinga Web 2 database for preferences and authentication, Icinga IDO database)
roles.ini | User specific roles (e.g. `administrators`) and permissions
[authentication.ini](05-Authentication.md) | Authentication backends (e.g. database)
enabledModules | Contains symlinks to enabled modules
modules | Directory for module specific configuration

View File

@ -18,29 +18,60 @@ The order of entries in the authentication configuration determines the order of
If the current authentication method errors or if the current authentication method does not know the account being
authenticated, the next authentication method will be used.
### <a id="authentication-configuration-external-authentication"></a> External Authentication
## <a id="authentication-configuration-external-authentication"></a> External Authentication
For delegating authentication to the web server simply add `autologin` to your authentication configuration:
````
```
[autologin]
backend = external
````
```
If your web server is not configured for authentication though the `autologin` section has no effect.
If your web server is not configured for authentication though, the `autologin` section has no effect.
### <a id="authentication-configuration-ad-or-ldap-authentication"></a> Active Directory or LDAP Authentication
### <a id="authentication-configuration-external-authentication-example"></a> Example Configuration for Apache and Basic Authentication
The following example will show you how to enable external authentication in Apache
using **Basic access authentication**.
**Creating Users**
To create users for **basic access authentication** you can use the tool `htpasswd`. In this example **.http-users** is
the name of the file containing the user credentials.
The following command creates a new file with the user **icingaadmin**. `htpasswd` will prompt you for a password.
If you want to add more users to the file you have to omit the `-c` switch to not overwrite the file.
```
sudo htpasswd -c /etc/icingaweb2/.http-users icingaadmin
```
**Configuring the Web Server**
Add the following configuration to the **&lt;Directory&gt; Directive** in the **icingaweb.conf** web server
configuration file.
```
AuthType Basic
AuthName "Icinga Web 2"
AuthUserFile /etc/icingaweb2/.http-users
Require valid-user
```
Restart your web server to apply the changes.
## <a id="authentication-configuration-ad-or-ldap-authentication"></a> Active Directory or LDAP Authentication
If you want to authenticate against Active Directory or LDAP, you have to define a
[LDAP resource](resources.md#resources-configuration-ldap) which will be referenced as data source for the Active Directory
or LDAP configuration method.
[LDAP resource](04-Resources.md#resources-configuration-ldap) which will be referenced as data source for the
Active Directory or LDAP configuration method.
#### <a id="authentication-configuration-ldap-authentication"></a> LDAP
### <a id="authentication-configuration-ldap-authentication"></a> LDAP
Directive | Description
------------------------|------------
**backend** | `ldap`
**resource** | The name of the LDAP resource defined in [resources.ini](resources.md#resources).
**resource** | The name of the LDAP resource defined in [resources.ini](04-Resources.md#resources).
**user_class** | LDAP user class.
**user_name_attribute** | LDAP attribute which contains the username.
**filter** | LDAP search filter.
@ -60,12 +91,12 @@ Note that in case the set *user_name_attribute* holds multiple values it is requ
values are unique. Additionally, a user will be logged in using the exact user id used to authenticate
with Icinga Web 2 (e.g. an alias) no matter what the primary user id might actually be.
#### <a id="authentication-configuration-ad-authentication"></a> Active Directory
### <a id="authentication-configuration-ad-authentication"></a> Active Directory
Directive | Description
------------------------|------------
**backend** | `msldap`
**resource** | The name of the LDAP resource defined in [resources.ini](resources.md#resources).
**resource** | The name of the LDAP resource defined in [resources.ini](04-Resources.md#resources).
**Example:**
@ -75,16 +106,16 @@ backend = msldap
resource = my_ad
```
### <a id="authentication-configuration-db-authentication"></a> Database Authentication
## <a id="authentication-configuration-db-authentication"></a> Database Authentication
If you want to authenticate against a MySQL or a PostgreSQL database, you have to define a
[database resource](resources.md#resources-configuration-database) which will be referenced as data source for the database
[database resource](04-Resources.md#resources-configuration-database) which will be referenced as data source for the database
authentication method.
Directive | Description
------------------------|------------
**backend** | `db`
**resource** | The name of the database resource defined in [resources.ini](resources.md#resources).
**resource** | The name of the database resource defined in [resources.ini](04-Resources.md#resources).
**Example:**
@ -94,28 +125,28 @@ backend = db
resource = icingaweb-mysql
```
#### <a id="authentication-configuration-db-setup"></a> Database Setup
### <a id="authentication-configuration-db-setup"></a> Database Setup
For authenticating against a database, you have to import one of the following database schemas:
* **etc/schema/preferences.mysql.sql** (for **MySQL** database)
* **etc/schema/preferences.pgsql.sql** (for **PostgreSQL** databases)
After that you have to define the [database resource](resources.md#resources-configuration-database).
After that you have to define the [database resource](04-Resources.md#resources-configuration-database).
**Manually Creating Users**
Icinga Web 2 uses the MD5 based BSD password algorithm. For generating a password hash, please use the following
command:
````
```
openssl passwd -1 password
````
```
> Note: The switch to `openssl passwd` is the **number one** (`-1`) for using the MD5 based BSD password algorithm.
Insert the user into the database using the generated password hash:
````
```
INSERT INTO icingaweb_user (name, active, password_hash) VALUES ('icingaadmin', 1, 'hash from openssl');
````
```

View File

@ -21,9 +21,8 @@ things to which access can be managed: actions and objects.
### Actions
Actions are all the things an Icinga Web 2 user can do, like changing a certain configuration,
changing permissions or sending a command to the Icinga instance through the
<a href="http://docs.icinga.org/icinga2/latest/doc/module/icinga2/toc#!/icinga2/latest/doc/module/icinga2/chapter/getting-started#setting-up-external-command-pipe">Command Pipe</a>
in the monitoring module. All actions must be be **allowed explicitly** using permissions.
changing permissions or sending a command to the Icinga instance through the Icinga command pipe.
All actions must be be **allowed explicitly** using permissions.
A permission is a simple list of identifiers of actions a user is
allowed to do. Permissions are described in greater detail in the
@ -47,7 +46,7 @@ using Active Directory, and a user **icingaadmin** that is authenticated using a
In the configuration, both can be referenced to by using their user names **icingaadmin** or **jdoe**.
Icinga Web 2 users and groups are not configured by a configuration file, but provided by
an **authentication backend**. For extended information on setting up authentication backends and managing users, please read the chapter [Authentication](authentication.md#authentication).
an **authentication backend**. For extended information on setting up authentication backends and managing users, please read the chapter [Authentication](05-Authentication.md#authentication).
<div class="info-box">
@ -59,7 +58,7 @@ an **authentication backend**. For extended information on setting up authentica
#### Managing Users
When using a [Database
as authentication backend](authentication.md#authentication-configuration-db-authentication), it is possible to create, add and delete users directly in the frontend. This configuration
as authentication backend](05-Authentication.md#authentication-configuration-db-authentication), it is possible to create, add and delete users directly in the frontend. This configuration
can be found at **Configuration > Authentication > Users **.
### Groups
@ -70,12 +69,12 @@ A user can be member of multiple groups and will inherit all permissions and res
Like users, groups are identified solely by their **name** that is provided by
a **group backend**. For extended information on setting up group backends,
please read the chapter [Authentication](authentication.md#authentication).
please read the chapter [Authentication](05-Authentication.md#authentication).
#### Managing Groups
When using a [Database as an authentication backend](#authentication.md#authentication-configuration-db-authentication),
When using a [Database as an authentication backend](05-Authentication.md#authentication-configuration-db-authentication),
it is possible to manage groups and group memberships directly in the frontend. This configuration
can be found at **Configuration > Authentication > Groups **.
@ -158,17 +157,17 @@ through a group) all permissions are added together to get the users actual perm
### Global Permissions
Name | Permits
--------------- ----|--------------------------------------------------------
--------------------------|--------------------------------------------------------
* | Allow everything, including module-specific permissions
config/* | Allow all configuration actions
config/modules | Allow enabling or disabling modules
module/<moduleName> | Allow access to module <moduleName>
module/&lt;moduleName&gt; | Allow access to module &lt;moduleName&gt;
### Monitoring Module Permissions
The built-in monitoring module defines an additional set of permissions, that
is described in detail in the [monitoring module documentation](/icingaweb2/doc/module/doc/chapter/monitoring-security#monitoring-security).
is described in detail in the monitoring module documentation.
## <a id="restrictions"></a> Restrictions
@ -187,7 +186,7 @@ mentioned in the section [Syntax](#syntax).
### Filter Expressions
Filters operate on columns. A complete list of all available filter columns on hosts and services can be found in
the [monitoring module documentation](/icingaweb2/doc/module/doc/chapter/monitoring-security#monitoring-security-restrictions).
the monitoring module documentation.
Any filter expression that is allowed in the filtered view, is also an allowed filter expression.
This means, that it is possible to define negations, wildcards, and even nested

View File

@ -27,13 +27,13 @@ type = ini
### <a id="preferences-configuration-db"></a> Store Preferences in a Database
In order to be more flexible in distributed setups you can store preferences in a MySQL or in a PostgreSQL database.
For storing preferences in a database, you have to define a [database resource](resources.md#resources-configuration-database)
For storing preferences in a database, you have to define a [database resource](04-Resources.md#resources-configuration-database)
which will be referenced as resource for the preferences storage.
Directive | Description
------------------------|------------
**type** | `db`
**resource** | The name of the database resource defined in [resources.ini](resources.md#resources).
**resource** | The name of the database resource defined in [resources.ini](04-Resources.md#resources).
**Example:**
@ -50,4 +50,4 @@ For storing preferences in a database, you have to import one of the following d
* **etc/schema/preferences.mysql.sql** (for **MySQL** database)
* **etc/schema/preferences.pgsql.sql** (for **PostgreSQL** databases)
After that you have to define the [database resource](resources.md#resources-configuration-database).
After that you have to define the [database resource](04-Resources.md#resources-configuration-database).

View File

@ -1,15 +0,0 @@
# <a id="configuration"></a> 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
------------------------------|-----------------------|---------------------------
. | config.ini | General configuration (logging, preferences)
. | resources.ini | Global resources (Icinga Web 2 database for preferences and authentication, icinga ido database)
. | roles.ini | User specific roles (e.g. `administrators`) and permissions
. | [authentication.ini](authentication.md) | Authentication backends (e.g. database)
enabledModules | Symlink | Contains symlinks to enabled modules from `/usr/share/icingaweb2/modules/*`. Defaults to [monitoring](modules/monitoring/doc/configuration.md) and `doc`.
modules | Directory | Module specific configuration

View File

@ -1,86 +0,0 @@
# External Authentication
It is possible to utilize the authentication mechanism of the webserver instead
of the internal authentication of Icinga Web 2 to authenticate users. This might
be useful if you only have very few users and user management over **.htaccess**
is not sufficient or if you are required to use some other authentication
mechanism that is only available by utilizing the webserver.
Icinga Web 2 will entrust the complete authentication process to the
authentication provider of the webserver, if external authentication is used.
So it is very important that the webserver's authentication is configured
correctly as wrong configuration might lead to unauthorized access or a
malfunction in the login-process.
## Using External Authentication
External authentication in Icinga Web 2 requires the following preparations:
1. The external authentication must be set up properly to correctly
authenticate users
2. Icinga Web 2 must be configured to use external authentication
### Preparing the External Authentication Provider
This step depends heavily on the used webserver and authentication mechanism you
want to use. It is not possible to cover all possibillities and you should
probably read the documentation for your webserver to get detailed instructions
on how to set up authentication properly.
In general you need to make sure that:
- All routes require authentication
- Only permitted users are allowed to authenticate
#### Example Configuration for Apache and HTTPDigestAuthentication
The following example will show how to enable external authentication in Apache
using *HTTP Digest Authentication*.
##### Creating users
To create users for digest authentication you can use the tool *htdigest*. In
this example **.icingawebdigest** is the name of the file containing the user
credentials.
This command creates a new file with the user *jdoe*. *htdigest* will prompt
you for a password. If you want to add more users to the file you need to omit
the *-c* parameter in all following commands to not to overwrite the file.
````
sudo htdigest -c /etc/icingaweb2/.icingawebdigest "Icinga Web 2" jdoe
````
##### Configuring the Webserver
The webserver should require authentication for all public Icinga Web 2 files.
````
<Directory "/usr/share/icingaweb2/public">
AuthType digest
AuthName "Icinga Web 2"
AuthDigestProvider file
AuthUserFile /etc/icingaweb2/.icingawebdigest
Require valid-user
</Directory>
````
To get these changes to work, make sure to enable the module for
HTTPDigestAuthentication and restart the webserver.
### Preparing Icinga Web 2
Once external authentication is set up correctly you need to configure Icinga
Web 2. In case you already completed the setup wizard it is likely that you are
now finished.
To get Icinga Web 2 to use external authentication the file
**config/authentication.ini** is required. Just add the following section
called "autologin", or any name of your choice, and save your changes:
````
[autologin]
backend = external
````
Congratulations! You are now logged in when visiting Icinga Web 2.

View File

@ -190,7 +190,7 @@ cp -pv etc/bash_completion.d/icingacli %{buildroot}/%{_sysconfdir}/bash_completi
cp -prv modules/{monitoring,setup,doc,translation} %{buildroot}/%{basedir}/modules
cp -prv library/Icinga %{buildroot}/%{phpdir}
cp -prv library/vendor/{dompdf,HTMLPurifier*,JShrink,lessphp,Parsedown,Zend} %{buildroot}/%{basedir}/library/vendor
cp -prv public/{css,img,js,error_norewrite.html} %{buildroot}/%{basedir}/public
cp -prv public/{css,font,img,js,error_norewrite.html} %{buildroot}/%{basedir}/public
cp -pv packages/files/apache/icingaweb2.conf %{buildroot}/%{wwwconfigdir}/icingaweb2.conf
cp -pv packages/files/bin/icingacli %{buildroot}/%{bindir}
cp -pv packages/files/public/index.php %{buildroot}/%{basedir}/public

View File

@ -0,0 +1,152 @@
<?php
/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
namespace Icinga\Application\Hook\Ticket;
use ArrayAccess;
/**
* A ticket pattern
*
* This class should be used by modules which provide implementations for the Web 2 ticket hook.
* Have a look at the GenericTTS module for a possible use case.
*/
class TicketPattern implements ArrayAccess
{
/**
* The result of a performed ticket match
*
* @var array
*/
protected $match = array();
/**
* The name of the TTS integration
*
* @var string
*/
protected $name;
/**
* The ticket pattern
*
* @var string
*/
protected $pattern;
/**
* {@inheritdoc}
*/
public function offsetExists($offset)
{
return isset($this->match[$offset]);
}
/**
* {@inheritdoc}
*/
public function offsetGet($offset)
{
return array_key_exists($offset, $this->match) ? $this->match[$offset] : null;
}
/**
* {@inheritdoc}
*/
public function offsetSet($offset, $value)
{
if ($offset === null) {
$this->match[] = $value;
} else {
$this->match[$offset] = $value;
}
}
/**
* {@inheritdoc}
*/
public function offsetUnset($offset)
{
unset($this->match[$offset]);
}
/**
* Get the result of a performed ticket match
*
* @return array
*/
public function getMatch()
{
return $this->match;
}
/**
* Set the result of a performed ticket match
*
* @param array $match
*
* @return $this
*/
public function setMatch(array $match)
{
$this->match = $match;
return $this;
}
/**
* Get the name of the TTS integration
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set the name of the TTS integration
*
* @param string $name
*
* @return $this
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get the ticket pattern
*
* @return string
*/
public function getPattern()
{
return $this->pattern;
}
/**
* Set the ticket pattern
*
* @param string $pattern
*
* @return $this
*/
public function setPattern($pattern)
{
$this->pattern = $pattern;
return $this;
}
/**
* Whether the integration is properly configured, i.e. the pattern and the URL are not empty
*
* @return bool
*/
public function isValid()
{
return ! empty($this->pattern);
}
}

View File

@ -3,15 +3,17 @@
namespace Icinga\Application\Hook;
use ArrayIterator;
use ErrorException;
use Exception;
use Icinga\Application\Hook\Ticket\TicketPattern;
use Icinga\Application\Logger;
use Icinga\Exception\IcingaException;
/**
* Base class for ticket hooks
*
* Extend this class if you want to integrate your ticketing solution Icinga Web 2
* Extend this class if you want to integrate your ticketing solution into Icinga Web 2.
*/
abstract class TicketHook
{
@ -39,6 +41,100 @@ abstract class TicketHook
{
}
/**
* Create a link for each matched element in the subject text
*
* @param array|TicketPattern $match Matched element according to {@link getPattern()}
*
* @return string Replacement string
*/
abstract public function createLink($match);
/**
* Get the pattern(s) to search for
*
* Return an array of TicketPattern instances here to support multiple TTS integrations.
*
* @return string|TicketPattern[]
*/
abstract public function getPattern();
/**
* Apply ticket patterns to the given text
*
* @param string $text
* @param TicketPattern[] $ticketPatterns
*
* @return string
*/
private function applyTicketPatterns($text, array $ticketPatterns)
{
$out = '';
$start = 0;
$iterator = new ArrayIterator($ticketPatterns);
$iterator->rewind();
while ($iterator->valid()) {
$ticketPattern = $iterator->current();
try {
preg_match($ticketPattern->getPattern(), $text, $match, PREG_OFFSET_CAPTURE, $start);
} catch (ErrorException $e) {
$this->fail('Can\'t create ticket links: Pattern is invalid: %s', IcingaException::describe($e));
$iterator->next();
continue;
}
if (empty($match)) {
$iterator->next();
continue;
}
// Remove preg_offset from match for the ticket pattern
$carry = array();
array_walk($match, function ($value, $key) use (&$carry) {
$carry[$key] = $value[0];
}, $carry);
$ticketPattern->setMatch($carry);
$offsetLeft = $match[0][1];
$matchLength = strlen($match[0][0]);
$out .= substr($text, $start, $offsetLeft - $start);
try {
$out .= $this->createLink($ticketPattern);
} catch (Exception $e) {
$this->fail('Can\'t create ticket links: %s', IcingaException::describe($e));
return $text;
}
$start = $offsetLeft + $matchLength;
}
$out .= substr($text, $start);
return $out;
}
/**
* Helper function to create a TicketPattern instance
*
* @param string $name Name of the TTS integration
* @param string $pattern Ticket pattern
*
* @return TicketPattern
*/
protected function createTicketPattern($name, $pattern)
{
$ticketPattern = new TicketPattern();
$ticketPattern
->setName($name)
->setPattern($pattern);
return $ticketPattern;
}
/**
* Set the hook as failed w/ the given message
*
@ -63,22 +159,6 @@ abstract class TicketHook
return $this->lastError;
}
/**
* Get the pattern
*
* @return string
*/
abstract public function getPattern();
/**
* Create a link for each matched element in the subject text
*
* @param array $match Array of matched elements according to {@link getPattern()}
*
* @return string Replacement string
*/
abstract public function createLink($match);
/**
* Create links w/ {@link createLink()} in the given text that matches to the subject from {@link getPattern()}
*
@ -101,10 +181,15 @@ abstract class TicketHook
$this->fail('Can\'t create ticket links: Retrieving the pattern failed: %s', IcingaException::describe($e));
return $text;
}
if (empty($pattern)) {
$this->fail('Can\'t create ticket links: Pattern is empty');
return $text;
}
if (is_array($pattern)) {
$text = $this->applyTicketPatterns($text, $pattern);
} else {
try {
$text = preg_replace_callback(
$pattern,
@ -118,6 +203,7 @@ abstract class TicketHook
$this->fail('Can\'t create ticket links: %s', IcingaException::describe($e));
return $text;
}
}
return $text;
}

View File

@ -5,6 +5,7 @@ namespace Icinga\Authentication;
use Icinga\Application\Config;
use Icinga\Application\Logger;
use Icinga\Authentication\Role;
use Icinga\Exception\NotReadableError;
use Icinga\Data\ConfigObject;
use Icinga\User;
@ -43,16 +44,12 @@ class AdmissionLoader
}
/**
* Get user permissions and restrictions
* Apply permissions, restrictions and roles to the given user
*
* @param User $user
*
* @return array
*/
public function getPermissionsAndRestrictions(User $user)
public function applyRoles(User $user)
{
$permissions = array();
$restrictions = array();
$username = $user->getUsername();
try {
$roles = Config::app('roles');
@ -62,14 +59,18 @@ class AdmissionLoader
$username,
$e
);
return array($permissions, $restrictions);
return;
}
$userGroups = $user->getGroups();
foreach ($roles as $role) {
$permissions = array();
$restrictions = array();
$roleObjs = array();
foreach ($roles as $roleName => $role) {
if ($this->match($username, $userGroups, $role)) {
$permissionsFromRole = StringHelper::trimSplit($role->permissions);
$permissions = array_merge(
$permissions,
array_diff(StringHelper::trimSplit($role->permissions), $permissions)
array_diff($permissionsFromRole, $permissions)
);
$restrictionsFromRole = $role->toArray();
unset($restrictionsFromRole['users']);
@ -81,8 +82,16 @@ class AdmissionLoader
}
$restrictions[$name][] = $restriction;
}
$roleObj = new Role();
$roleObjs[] = $roleObj
->setName($roleName)
->setPermissions($permissionsFromRole)
->setRestrictions($restrictionsFromRole);
}
}
return array($permissions, $restrictions);
$user->setPermissions($permissions);
$user->setRestrictions($restrictions);
$user->setRoles($roleObjs);
}
}

View File

@ -160,9 +160,7 @@ class Auth
}
$user->setGroups($groups);
$admissionLoader = new AdmissionLoader();
list($permissions, $restrictions) = $admissionLoader->getPermissionsAndRestrictions($user);
$user->setPermissions($permissions);
$user->setRestrictions($restrictions);
$admissionLoader->applyRoles($user);
$this->user = $user;
if ($persist) {
$this->persistCurrentUser();
@ -242,10 +240,10 @@ class Auth
public function authenticateFromSession()
{
$this->user = Session::getSession()->get('user');
if ($this->user !== null && $this->user->isExternalUser() === true) {
if ($this->user !== null && $this->user->isExternalUser()) {
list($originUsername, $field) = $this->user->getExternalUserInformation();
$username = getenv($field); // usually REMOTE_USER here
if ( !$username || $username !== $originUsername) {
$username = ExternalBackend::getRemoteUser($field);
if ($username === null || $username !== $originUsername) {
$this->removeAuthorization();
}
}

View File

@ -0,0 +1,109 @@
<?php
/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication;
class Role
{
/**
* Name of the role
*
* @var string
*/
protected $name;
/**
* Permissions of the role
*
* @var string[]
*/
protected $permissions = array();
/**
* Restrictions of the role
*
* @var string[]
*/
protected $restrictions = array();
/**
* Get the name of the role
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set the name of the role
*
* @param string $name
*
* @return $this
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get the permissions of the role
*
* @return string[]
*/
public function getPermissions()
{
return $this->permissions;
}
/**
* Set the permissions of the role
*
* @param string[] $permissions
*
* @return $this
*/
public function setPermissions(array $permissions)
{
$this->permissions = $permissions;
return $this;
}
/**
* Get the restrictions of the role
*
* @param string $name Optional name of the restriction
*
* @return string[]|null
*/
public function getRestrictions($name = null)
{
$restrictions = $this->restrictions;
if ($name === null) {
return $restrictions;
}
if (isset($restrictions[$name])) {
return $restrictions[$name];
}
return null;
}
/**
* Set the restrictions of the role
*
* @param string[] $restrictions
*
* @return $this
*/
public function setRestrictions(array $restrictions)
{
$this->restrictions = $restrictions;
return $this;
}
}

View File

@ -35,6 +35,14 @@ class ExternalBackend implements UserBackendInterface
$this->stripUsernameRegexp = $config->get('strip_username_regexp');
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->name;
}
/**
* {@inheritdoc}
*/
@ -45,11 +53,22 @@ class ExternalBackend implements UserBackendInterface
}
/**
* {@inheritdoc}
* Get the remote user from environment or $_SERVER, if any
*
* @param string $variable The name variable where to read the user from
*
* @return string|null
*/
public function getName()
public static function getRemoteUser($variable = 'REMOTE_USER')
{
return $this->name;
$username = getenv($variable);
if ($username !== false) {
return $username;
}
if (array_key_exists($variable, $_SERVER)) {
return $_SERVER[$variable];
}
return null;
}
@ -58,8 +77,8 @@ class ExternalBackend implements UserBackendInterface
*/
public function authenticate(User $user, $password = null)
{
$username = getenv('REMOTE_USER');
if ($username !== false) {
$username = static::getRemoteUser();
if ($username !== null) {
$user->setExternalUserInformation($username, 'REMOTE_USER');
if ($this->stripUsernameRegexp) {

View File

@ -152,11 +152,12 @@ class DbConnection implements Selectable, Extensible, Updatable, Reducible, Insp
*/
$driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] =
'SET SESSION SQL_MODE=\'STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,'
. 'NO_AUTO_CREATE_USER,ANSI_QUOTES,PIPES_AS_CONCAT,NO_ENGINE_SUBSTITUTION\';';
if (array_key_exists('charset', $adapterParamaters) && $adapterParamaters['charset']) {
$driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .= 'SET NAMES ' . $adapterParamaters['charset']. ';';
. 'NO_AUTO_CREATE_USER,ANSI_QUOTES,PIPES_AS_CONCAT,NO_ENGINE_SUBSTITUTION\'';
if (isset($adapterParamaters['charset'])) {
$driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .= ', NAMES ' . $adapterParamaters['charset'];
unset($adapterParamaters['charset']);
}
$driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .=';';
$adapterParamaters['port'] = $this->config->get('port', 3306);
break;

View File

@ -92,6 +92,6 @@ class TreeNodeIterator implements RecursiveIterator
*/
public function isEmpty()
{
return empty($this->children);
return ! $this->children->count();
}
}

View File

@ -3,20 +3,21 @@
namespace Icinga\Protocol\Ldap;
use Exception;
use ArrayIterator;
use Exception;
use LogicException;
use stdClass;
use Icinga\Application\Config;
use Icinga\Application\Logger;
use Icinga\Data\ConfigObject;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Filter\FilterChain;
use Icinga\Data\Filter\FilterExpression;
use Icinga\Data\Inspectable;
use Icinga\Data\Inspection;
use Icinga\Data\Selectable;
use Icinga\Data\Sortable;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Filter\FilterChain;
use Icinga\Data\Filter\FilterExpression;
use Icinga\Exception\ProgrammingError;
use Icinga\Protocol\Ldap\LdapException;
/**
* Encapsulate LDAP connections and query creation
@ -548,6 +549,23 @@ class LdapConnection implements Selectable, Inspectable
return $pairs;
}
/**
* Fetch an LDAP entry by its DN
*
* @param string $dn
* @param array|null $fields
*
* @return StdClass|bool
*/
public function fetchByDn($dn, array $fields = null)
{
return $this->select()
->from('*', $fields)
->setBase($dn)
->setScope('base')
->fetchRow();
}
/**
* Test the given LDAP credentials by establishing a connection and attempting a LDAP bind
*
@ -706,7 +724,7 @@ class LdapConnection implements Selectable, Inspectable
'value' => $this->encodeSortRules($query->getOrder())
)
));
} else {
} elseif (! empty($fields)) {
foreach ($query->getOrder() as $rule) {
if (! in_array($rule[0], $fields, true)) {
$fields[] = $rule[0];
@ -825,7 +843,7 @@ class LdapConnection implements Selectable, Inspectable
$ds = $this->getConnection();
$serverSorting = false;//$this->getCapabilities()->hasOid(LdapCapabilities::LDAP_SERVER_SORT_OID);
if (! $serverSorting && $query->hasOrder()) {
if (! $serverSorting && $query->hasOrder() && ! empty($fields)) {
foreach ($query->getOrder() as $rule) {
if (! in_array($rule[0], $fields, true)) {
$fields[] = $rule[0];
@ -1179,6 +1197,8 @@ class LdapConnection implements Selectable, Inspectable
* @param int $deref
*
* @return resource|bool A search result identifier or false on error
*
* @throws LogicException If the LDAP query search scope is unsupported
*/
public function ldapSearch(
LdapQuery $query,
@ -1190,6 +1210,7 @@ class LdapConnection implements Selectable, Inspectable
) {
$queryString = (string) $query;
$baseDn = $query->getBase() ?: $this->getDn();
$scope = $query->getScope();
if (Logger::getInstance()->getLevel() === Logger::DEBUG) {
// We're checking the level by ourself to avoid rendering the ldapsearch commandline for nothing
@ -1213,11 +1234,12 @@ class LdapConnection implements Selectable, Inspectable
}
Logger::debug("Issueing LDAP search. Use '%s' to reproduce.", sprintf(
'ldapsearch -P 3%s -H "%s"%s -b "%s" -s "sub" -z %u -l %u -a "%s"%s%s%s',
'ldapsearch -P 3%s -H "%s"%s -b "%s" -s "%s" -z %u -l %u -a "%s"%s%s%s',
$starttlsParam,
$ldapUrl,
$bindParams,
$baseDn,
$scope,
$sizelimit,
$timelimit,
$derefName,
@ -1227,7 +1249,21 @@ class LdapConnection implements Selectable, Inspectable
));
}
return @ldap_search(
switch($scope) {
case LdapQuery::SCOPE_SUB:
$function = 'ldap_search';
break;
case LdapQuery::SCOPE_ONE:
$function = 'ldap_list';
break;
case LdapQuery::SCOPE_BASE:
$function = 'ldap_read';
break;
default:
throw new LogicException('LDAP scope %s not supported by ldapSearch', $scope);
}
return @$function(
$this->getConnection(),
$baseDn,
$queryString,

View File

@ -3,6 +3,7 @@
namespace Icinga\Protocol\Ldap;
use LogicException;
use Icinga\Data\SimpleQuery;
/**
@ -38,6 +39,39 @@ class LdapQuery extends SimpleQuery
*/
protected $nativeFilter;
/**
* Only fetch the entry at the base of the search
*/
const SCOPE_BASE = 'base';
/**
* Fetch entries one below the base DN
*/
const SCOPE_ONE = 'one';
/**
* Fetch all entries below the base DN
*/
const SCOPE_SUB = 'sub';
/**
* All available scopes
*
* @var array
*/
public static $scopes = array(
LdapQuery::SCOPE_BASE,
LdapQuery::SCOPE_ONE,
LdapQuery::SCOPE_SUB
);
/**
* LDAP search scope (default: SCOPE_SUB)
*
* @var string
*/
protected $scope = LdapQuery::SCOPE_SUB;
/**
* Initialize this query
*/
@ -223,4 +257,38 @@ class LdapQuery extends SimpleQuery
{
return $this->renderFilter();
}
/**
* Get LDAP search scope
*
* @return string
*/
public function getScope()
{
return $this->scope;
}
/**
* Set LDAP search scope
*
* Valid: sub one base (Default: sub)
*
* @param string $scope
*
* @return LdapQuery
*
* @throws LogicException If scope value is invalid
*/
public function setScope($scope)
{
if (! in_array($scope, static::$scopes)) {
throw new LogicException(
'Can\'t set scope %d, it is is invalid. Use one of %s or LdapQuery\'s constants.',
$scope, implode(', ', static::$scopes)
);
}
$this->scope = $scope;
return $this;
}
}

View File

@ -28,9 +28,11 @@ class LdapUtils
$res = ldap_explode_dn($dn, $with_type ? 0 : 1);
foreach ($res as $k => $v) {
$res[$k] = preg_replace(
'/\\\([0-9a-f]{2})/ei',
"chr(hexdec('\\1'))",
$res[$k] = preg_replace_callback(
'/\\\([0-9a-f]{2})/i',
function ($m) {
return chr(hexdec($m[1]));
},
$v
);
}

View File

@ -6,6 +6,7 @@ namespace Icinga;
use DateTimeZone;
use InvalidArgumentException;
use Icinga\Application\Config;
use Icinga\Authentication\Role;
use Icinga\User\Preferences;
use Icinga\Web\Navigation\Navigation;
@ -91,6 +92,13 @@ class User
*/
protected $groups = array();
/**
* Roles of this user
*
* @var Role[]
*/
protected $roles = array();
/**
* Preferences object
*
@ -229,13 +237,39 @@ class User
}
/**
* Settter for restrictions
* Set the user's restrictions
*
* @param array $restrictions
* @param string[] $restrictions
*
* @return $this
*/
public function setRestrictions(array $restrictions)
{
$this->restrictions = $restrictions;
return $this;
}
/**
* Get the roles of the user
*
* @return Role[]
*/
public function getRoles()
{
return $this->roles;
}
/**
* Set the roles of the user
*
* @param Role[] $roles
*
* @return $this
*/
public function setRoles(array $roles)
{
$this->roles = $roles;
return $this;
}
/**

View File

@ -3,14 +3,22 @@
namespace Icinga\Util;
use ArrayIterator;
use InvalidArgumentException;
use Iterator;
use RecursiveIterator;
/**
* Iterator for traversing a directory
*/
class DirectoryIterator implements Iterator
class DirectoryIterator implements RecursiveIterator
{
/**
* Iterate files first
*
* @var int
*/
const FILES_FIRST = 1;
/**
* Current directory item
*
@ -26,11 +34,18 @@ class DirectoryIterator implements Iterator
protected $extension;
/**
* Directory handle
* Scanned files
*
* @var resource
* @var ArrayIterator
*/
private $handle;
private $files;
/**
* Iterator flags
*
* @var int
*/
protected $flags;
/**
* Current key
@ -46,6 +61,13 @@ class DirectoryIterator implements Iterator
*/
protected $path;
/**
* Directory queue if FILES_FIRST flag is set
*
* @var array
*/
private $queue;
/**
* Whether to skip empty files
*
@ -72,8 +94,9 @@ class DirectoryIterator implements Iterator
*
* @param string $path The path of the directory to traverse
* @param string $extension The file extension to filter for. A leading dot is optional
* @param int $flags Iterator flags
*/
public function __construct($path, $extension = null)
public function __construct($path, $extension = null, $flags = null)
{
if (empty($path)) {
throw new InvalidArgumentException('The path can\'t be empty');
@ -82,6 +105,9 @@ class DirectoryIterator implements Iterator
if (! empty($extension)) {
$this->extension = '.' . ltrim($extension, '.');
}
if ($flags !== null) {
$this->flags = $flags;
}
}
/**
@ -96,6 +122,23 @@ class DirectoryIterator implements Iterator
return is_dir($path) && is_readable($path);
}
/**
* {@inheritdoc}
*/
public function hasChildren()
{
return static::isReadable($this->current);
}
/**
* {@inheritdoc}
*/
public function getChildren()
{
return new static($this->current, $this->extension, $this->flags);
}
/**
* {@inheritdoc}
*/
@ -110,12 +153,14 @@ class DirectoryIterator implements Iterator
public function next()
{
do {
$file = readdir($this->handle);
if ($file === false) {
$key = false;
$this->files->next();
$skip = false;
if (! $this->files->valid()) {
$file = false;
$path = false;
break;
} else {
$skip = false;
$file = $this->files->current();
do {
if ($this->skipHidden && $file[0] === '.') {
$skip = true;
@ -125,7 +170,10 @@ class DirectoryIterator implements Iterator
$path = $this->path . '/' . $file;
if (is_dir($path)) {
if ($this->flags & static::FILES_FIRST === static::FILES_FIRST) {
$this->queue[] = array($path, $file);
$skip = true;
}
break;
}
@ -138,16 +186,18 @@ class DirectoryIterator implements Iterator
$skip = true;
break;
}
$key = $file;
$file = $path;
} while (0);
}
} while ($skip);
$this->current = $file;
/** @noinspection PhpUndefinedVariableInspection */
$this->key = $key;
if ($path === false && ! empty($this->queue)) {
list($path, $file) = array_shift($this->queue);
}
$this->current = $path;
$this->key = $file;
}
/**
@ -171,21 +221,13 @@ class DirectoryIterator implements Iterator
*/
public function rewind()
{
if ($this->handle === null) {
$this->handle = opendir($this->path);
} else {
rewinddir($this->handle);
if ($this->files === null) {
$files = scandir($this->path);
natcasesort($files);
$this->files = new ArrayIterator($files);
}
$this->files->rewind();
$this->queue = array();
$this->next();
}
/**
* Close directory handle if created
*/
public function __destruct()
{
if ($this->handle !== null) {
closedir($this->handle);
}
}
}

View File

@ -100,7 +100,7 @@ class Autosubmit extends Zend_Form_Decorator_Abstract
: t('Upon its value has changed, this field issues an automatic update of this page.');
$content .= $this->getView()->icon('cw', $warning, array(
'aria-hidden' => $isForm ? 'false' : 'true',
'class' => 'spinner'
'class' => 'spinner autosubmit-info'
));
if (! $isForm && $this->getAccessible()) {
$content = '<span id="' . $this->getWarningId() . '" class="sr-only">' . $warning . '</span>' . $content;

View File

@ -70,7 +70,7 @@ class LessCompiler
*/
public function addLessFile($lessFile)
{
$this->lessFiles[] = $lessFile;
$this->lessFiles[] = realpath($lessFile);
return $this;
}
@ -87,7 +87,7 @@ class LessCompiler
if (! isset($this->moduleLessFiles[$moduleName])) {
$this->moduleLessFiles[$moduleName] = array();
}
$this->moduleLessFiles[$moduleName][] = $lessFile;
$this->moduleLessFiles[$moduleName][] = realpath($lessFile);
return $this;
}
@ -98,9 +98,12 @@ class LessCompiler
*/
public function getLessFiles()
{
$lessFiles = iterator_to_array(new RecursiveIteratorIterator(new RecursiveArrayIterator(
$this->lessFiles + $this->moduleLessFiles
)));
$lessFiles = $this->lessFiles;
foreach ($this->moduleLessFiles as $moduleLessFiles) {
$lessFiles = array_merge($lessFiles, $moduleLessFiles);
}
if ($this->theme !== null) {
$lessFiles[] = $this->theme;
}

View File

@ -201,12 +201,6 @@ class Dashboard extends AbstractWidget
{
/** @var $pane Pane */
foreach ($panes as $pane) {
if ($pane->getDisabled()) {
if ($this->hasPane($pane->getTitle()) === true) {
$this->removePane($pane->getTitle());
}
continue;
}
if ($this->hasPane($pane->getTitle()) === true) {
/** @var $current Pane */
$current = $this->panes[$pane->getName()];
@ -231,6 +225,9 @@ class Dashboard extends AbstractWidget
$this->tabs = new Tabs();
foreach ($this->panes as $key => $pane) {
if ($pane->getDisabled()) {
continue;
}
$this->tabs->add(
$key,
array(

View File

@ -40,7 +40,7 @@ class Pane extends UserWidget
*
* @var bool
*/
private $disabled;
private $disabled = false;
/**
* Create a new pane

View File

@ -377,8 +377,8 @@ EOT;
array(
$tabs,
$drop,
$close,
$refresh
$refresh,
$close
),
$this->baseTpl
);

View File

@ -17,7 +17,7 @@ class Parsedown
{
# ~
const version = '1.5.0';
const version = '1.6.0';
# ~
@ -107,12 +107,6 @@ class Parsedown
# ~
protected $DefinitionTypes = array(
'[' => array('Reference'),
);
# ~
protected $unmarkedBlockTypes = array(
'Code',
);
@ -169,7 +163,7 @@ class Parsedown
# ~
if (isset($CurrentBlock['incomplete']))
if (isset($CurrentBlock['continuable']))
{
$Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
@ -185,8 +179,6 @@ class Parsedown
{
$CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
}
unset($CurrentBlock['incomplete']);
}
}
@ -226,7 +218,7 @@ class Parsedown
if (method_exists($this, 'block'.$blockType.'Continue'))
{
$Block['incomplete'] = true;
$Block['continuable'] = true;
}
$CurrentBlock = $Block;
@ -253,7 +245,7 @@ class Parsedown
# ~
if (isset($CurrentBlock['incomplete']) and method_exists($this, 'block'.$CurrentBlock['type'].'Complete'))
if (isset($CurrentBlock['continuable']) and method_exists($this, 'block'.$CurrentBlock['type'].'Complete'))
{
$CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
}
@ -394,16 +386,16 @@ class Parsedown
protected function blockFencedCode($Line)
{
if (preg_match('/^(['.$Line['text'][0].']{3,})[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
{
$Element = array(
'name' => 'code',
'text' => '',
);
if (isset($matches[2]))
if (isset($matches[1]))
{
$class = 'language-'.$matches[2];
$class = 'language-'.$matches[1];
$Element['attributes'] = array(
'class' => $class,
@ -673,7 +665,9 @@ class Parsedown
if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
{
if (in_array($matches[1], $this->textLevelElements))
$element = strtolower($matches[1]);
if (in_array($element, $this->textLevelElements))
{
return;
}
@ -736,8 +730,6 @@ class Parsedown
{
$Block['closed'] = true;
}
$Block['markup'] .= $matches[1];
}
if (isset($Block['interrupted']))
@ -989,15 +981,13 @@ class Parsedown
{
$markup = '';
$unexaminedText = $text;
# $excerpt is based on the first occurrence of a marker
$markerPosition = 0;
while ($excerpt = strpbrk($unexaminedText, $this->inlineMarkerList))
while ($excerpt = strpbrk($text, $this->inlineMarkerList))
{
$marker = $excerpt[0];
$markerPosition += strpos($unexaminedText, $marker);
$markerPosition = strpos($text, $marker);
$Excerpt = array('text' => $excerpt, 'context' => $text);
@ -1010,34 +1000,42 @@ class Parsedown
continue;
}
if (isset($Inline['position']) and $Inline['position'] > $markerPosition) # position is ahead of marker
# makes sure that the inline belongs to "our" marker
if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
{
continue;
}
# sets a default inline position
if ( ! isset($Inline['position']))
{
$Inline['position'] = $markerPosition;
}
# the text that comes before the inline
$unmarkedText = substr($text, 0, $Inline['position']);
# compile the unmarked text
$markup .= $this->unmarkedText($unmarkedText);
# compile the inline
$markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
# remove the examined text
$text = substr($text, $Inline['position'] + $Inline['extent']);
$unexaminedText = $text;
$markerPosition = 0;
continue 2;
}
$unexaminedText = substr($excerpt, 1);
# the marker does not belong to an inline
$markerPosition ++;
$unmarkedText = substr($text, 0, $markerPosition + 1);
$markup .= $this->unmarkedText($unmarkedText);
$text = substr($text, $markerPosition + 1);
}
$markup .= $this->unmarkedText($text);
@ -1199,7 +1197,7 @@ class Parsedown
return;
}
if (preg_match('/^[(]((?:[^ (]|[(][^ )]+[)])+)(?:[ ]+("[^"]+"|\'[^\']+\'))?[)]/', $remainder, $matches))
if (preg_match('/^[(]((?:[^ ()]|[(][^ )]+[)])+)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $remainder, $matches))
{
$Element['attributes']['href'] = $matches[1];
@ -1214,7 +1212,7 @@ class Parsedown
{
if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
{
$definition = $matches[1] ? $matches[1] : $Element['text'];
$definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
$definition = strtolower($definition);
$extent += strlen($matches[0]);
@ -1360,11 +1358,6 @@ class Parsedown
}
}
#
# ~
protected $unmarkedInlineTypes = array("\n" => 'Break', '://' => 'Url');
# ~
protected function unmarkedText($text)
@ -1409,7 +1402,7 @@ class Parsedown
if (isset($Element['handler']))
{
$markup .= $this->$Element['handler']($Element['text']);
$markup .= $this->{$Element['handler']}($Element['text']);
}
else
{
@ -1483,7 +1476,7 @@ class Parsedown
return self::$instances[$name];
}
$instance = new self();
$instance = new static();
self::$instances[$name] = $instance;

View File

@ -1,4 +1,4 @@
RELEASE=1.5.0
RELEASE=1.6.0
PARSEDOWN=parsedown-$RELEASE
curl https://codeload.github.com/erusev/parsedown/tar.gz/${RELEASE} -o ${PARSEDOWN}.tar.gz
tar xfz ${PARSEDOWN}.tar.gz --strip-components 1 ${PARSEDOWN}/Parsedown.php ${PARSEDOWN}/LICENSE.txt

View File

@ -3,6 +3,8 @@
namespace Icinga\Module\Doc\Controllers;
use finfo;
use SplFileInfo;
use Icinga\Application\Icinga;
use Icinga\Module\Doc\DocController;
use Icinga\Module\Doc\Exception\DocException;
@ -88,11 +90,12 @@ class ModuleController extends DocController
{
$module = $this->params->getRequired('moduleName');
$this->assertModuleInstalled($module);
$this->view->moduleName = $module;
$moduleManager = Icinga::app()->getModuleManager();
$name = $moduleManager->getModule($module)->getTitle();
try {
$this->renderToc(
$this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')),
$module,
$name,
'doc/module/chapter',
array('moduleName' => $module)
);
@ -120,6 +123,7 @@ class ModuleController extends DocController
$this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')),
$chapter,
'doc/module/chapter',
'doc/module/img',
array('moduleName' => $module)
);
} catch (DocException $e) {
@ -127,6 +131,60 @@ class ModuleController extends DocController
}
}
/**
* Deliver images
*/
public function imageAction()
{
$module = $this->params->getRequired('moduleName');
$image = $this->params->getRequired('image');
$docPath = $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc'));
$imagePath = realpath($docPath . '/' . $image);
if ($imagePath === false) {
$this->httpNotFound('%s does not exist', $image);
}
$this->_helper->viewRenderer->setNoRender(true);
$this->_helper->layout()->disableLayout();
$imageInfo = new SplFileInfo($imagePath);
$ETag = md5($imageInfo->getMTime() . $imagePath);
$lastModified = gmdate('D, d M Y H:i:s T', $imageInfo->getMTime());
$match = false;
if (isset($_SERER['HTTP_IF_NONE_MATCH'])) {
$ifNoneMatch = explode(', ', stripslashes($_SERVER['HTTP_IF_NONE_MATCH']));
foreach ($ifNoneMatch as $tag) {
if ($tag === $ETag) {
$match = true;
break;
}
}
} elseif (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$lastModifiedSince = stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if ($lastModifiedSince === $lastModified) {
$match = true;
}
}
header('ETag: "' . $ETag . '"');
header('Cache-Control: no-transform,public,max-age=3600');
header('Last-Modified: ' . $lastModified);
// Set additional headers for compatibility reasons (Cache-Control should have precedence) in case
// session.cache_limiter is set to no cache
header('Pragma: cache');
header('Expires: ' . gmdate('D, d M Y H:i:s T', time() + 3600));
if ($match) {
header('HTTP/1.1 304 Not Modified');
} else {
$finfo = new finfo();
header('Content-Type: ' . $finfo->file($imagePath, FILEINFO_MIME_TYPE));
readfile($imagePath);
}
}
/**
* View a module's documentation as PDF
*

View File

@ -1,6 +1,6 @@
# <a id="module-documentation"></a> Writing Module Documentation
![Markdown](/img/doc/doc/markdown.png)
![Markdown](img/markdown.png)
Icinga Web 2 is capable of viewing your module's documentation, if the documentation is written in
[Markdown](http://en.wikipedia.org/wiki/Markdown). Please refer to
@ -50,13 +50,15 @@ This syntax is also supported in Icinga Web 2.
## <a id="images"></a> Including Images
Images must placed in the `img` directory beneath your module's `public` directory, e.g.:
Images must placed in the `doc` directory beneath your module's root directory, e.g.:
example-module/public/img/doc
/path/to/icingaweb2/modules/example-module/doc/img/example.png
Note that the `img` sub directory is not mandatory but good for organizing your directory structure.
Module images can be accessed using the following URL:
{baseURL}/img/{moduleName}/{file} e.g. icingaweb/img/example-module/doc/example.png
{baseURL}/doc/module/{moduleName}/image/{image} e.g. icingaweb2/doc/module/example-module/image/img/example.png
Markdown's image syntax is very similar to Markdown's link syntax, but prefixed with an exclamation mark, e.g.:
@ -64,4 +66,4 @@ Markdown's image syntax is very similar to Markdown's link syntax, but prefixed
URLs to images inside your Markdown documentation files must be specified without the base URL, e.g.:
![Example](/img/example-module/doc/example.png)
![Example](img/example.png)

View File

@ -20,6 +20,9 @@ class DocController extends Controller
if ($this->hasParam('chapter')) {
$this->params->set('chapter', $this->getParam('chapter'));
}
if ($this->hasParam('image')) {
$this->params->set('image', $this->getParam('image'));
}
if ($this->hasParam('moduleName')) {
$this->params->set('moduleName', $this->getParam('moduleName'));
}
@ -31,20 +34,27 @@ class DocController extends Controller
* @param string $path Path to the documentation
* @param string $chapter ID of the chapter
* @param string $url URL to replace links with
* @param string $imageUrl URL to images
* @param array $urlParams Additional URL parameters
*/
protected function renderChapter($path, $chapter, $url, array $urlParams = array())
protected function renderChapter($path, $chapter, $url, $imageUrl = null, array $urlParams = array())
{
$parser = new DocParser($path);
$section = new DocSectionRenderer($parser->getDocTree(), DocSectionRenderer::decodeUrlParam($chapter));
$this->view->section = $section
->setHighlightSearch($this->params->get('highlight-search'))
->setImageUrl($imageUrl)
->setUrl($url)
->setUrlParams($urlParams)
->setHighlightSearch($this->params->get('highlight-search'));
$this->view->title = $chapter;
->setUrlParams($urlParams);
$first = null;
foreach ($section as $first) {
break;
}
$title = $first === null ? ucfirst($chapter) : $first->getTitle();
$this->view->title = $title;
$this->getTabs()->add('toc', array(
'active' => true,
'title' => ucfirst($chapter),
'title' => $title,
'url' => Url::fromRequest()
));
$this->render('chapter', null, true);
@ -66,10 +76,10 @@ class DocController extends Controller
->setUrl($url)
->setUrlParams($urlParams);
$name = ucfirst($name);
$this->view->title = sprintf($this->translate('%s Documentation'), $name);
$title = sprintf($this->translate('%s Documentation'), $name);
$this->getTabs()->add('toc', array(
'active' => true,
'title' => $name,
'title' => $title,
'url' => Url::fromRequest()
));
$this->render('toc', null, true);

View File

@ -4,6 +4,7 @@
namespace Icinga\Module\Doc;
use CachingIterator;
use RecursiveIteratorIterator;
use SplFileObject;
use SplStack;
use Icinga\Data\Tree\SimpleTree;
@ -61,7 +62,7 @@ class DocParser
);
}
$this->path = $path;
$this->docIterator = new DirectoryIterator($path, 'md');
$this->docIterator = new DirectoryIterator($path, 'md', DirectoryIterator::FILES_FIRST);
}
/**
@ -117,9 +118,37 @@ class DocParser
} else {
$id = null;
}
/** @noinspection PhpUndefinedVariableInspection */
return array($header, $id, $level, $headerStyle);
}
/**
* Generate unique section ID
*
* @param string $id
* @param string $filename
* @param SimpleTree $tree
*
* @return string
*/
protected function uuid($id, $filename, SimpleTree $tree)
{
$id = str_replace(' ', '-', $id);
if ($tree->getNode($id) === null) {
return $id;
}
$id = $id . '-' . md5($filename);
$offset = 0;
while ($tree->getNode($id)) {
if ($offset++ === 0) {
$id .= '-' . $offset;
} else {
$id = substr($id, 0, -1) . $offset;
}
}
return $id;
}
/**
* Get the documentation tree
*
@ -128,7 +157,7 @@ class DocParser
public function getDocTree()
{
$tree = new SimpleTree();
foreach ($this->docIterator as $filename) {
foreach (new RecursiveIteratorIterator($this->docIterator) as $filename) {
$file = new SplFileObject($filename);
$lastLine = null;
$stack = new SplStack();
@ -154,9 +183,9 @@ class DocParser
} else {
$noFollow = false;
}
if ($tree->getNode($id) !== null) {
$id = uniqid($id);
}
$id = $this->uuid($id, $filename, $tree);
$section = new DocSection();
$section
->setId($id)
@ -178,10 +207,7 @@ class DocParser
} else {
if ($stack->isEmpty()) {
$title = ucfirst($file->getBasename('.' . pathinfo($file->getFilename(), PATHINFO_EXTENSION)));
$id = $title;
if ($tree->getNode($id) !== null) {
$id = uniqid($id);
}
$id = $this->uuid($title, $filename, $tree);
$section = new DocSection();
$section
->setId($id)

View File

@ -88,14 +88,6 @@ class DocSection extends TreeNode
return $this->content;
}
/**
* {@inheritdoc}
*/
public function setId($id)
{
return parent::setId(str_replace(' ', '-', (string) $id));
}
/**
* Set the header level
*

View File

@ -13,6 +13,13 @@ use Icinga\Web\View;
*/
abstract class DocRenderer extends RecursiveIteratorIterator
{
/**
* URL to images
*
* @var string
*/
protected $imageUrl;
/**
* URL to replace links with
*
@ -34,6 +41,38 @@ abstract class DocRenderer extends RecursiveIteratorIterator
*/
protected $view;
/**
* Get the URL to images
*
* @return string
*/
public function getImageUrl()
{
return $this->imageUrl;
}
/**
* Set the URL to images
*
* @param string $imageUrl
*
* @return $this
*/
public function setImageUrl($imageUrl)
{
$this->imageUrl = (string) $imageUrl;
return $this;
}
/**
* Get the URL to replace links with
*
* @return string
*/
public function getUrl()
{
return $this->url;
}
/**
* Set the URL to replace links with
*
@ -48,13 +87,13 @@ abstract class DocRenderer extends RecursiveIteratorIterator
}
/**
* Get the URL to replace links with
* Get additional URL parameters
*
* @return string
* @return array
*/
public function getUrl()
public function getUrlParams()
{
return $this->url;
return $this->urlParams;
}
/**
@ -71,13 +110,16 @@ abstract class DocRenderer extends RecursiveIteratorIterator
}
/**
* Get additional URL parameters
* Get the view
*
* @return array
* @return View
*/
public function getUrlParams()
public function getView()
{
return $this->urlParams;
if ($this->view === null) {
$this->view = Icinga::app()->getViewRenderer()->view;
}
return $this->view;
}
/**
@ -93,19 +135,6 @@ abstract class DocRenderer extends RecursiveIteratorIterator
return $this;
}
/**
* Get the view
*
* @return View
*/
public function getView()
{
if ($this->view === null) {
$this->view = Icinga::app()->getViewRenderer()->view;
}
return $this->view;
}
/**
* Encode an anchor identifier
*

View File

@ -190,7 +190,20 @@ class DocSectionRenderer extends DocRenderer
$xpath = new DOMXPath($doc);
$img = $xpath->query('//img[1]')->item(0);
/** @var \DOMElement $img */
$img->setAttribute('src', Url::fromPath($img->getAttribute('src'))->getAbsoluteUrl());
$path = $this->getView()->getHelper('Url')->url(
array_merge(
array(
'image' => $img->getAttribute('src')
),
$this->urlParams
),
$this->imageUrl,
false,
false
);
$url = $this->getView()->url($path);
/** @var \Icinga\Web\Url $url */
$img->setAttribute('src', $url->getAbsoluteUrl());
return substr_replace($doc->saveXML($img), '', -2, 1); // Replace '/>' with '>'
}
@ -274,7 +287,7 @@ class DocSectionRenderer extends DocRenderer
$html
);
$html = preg_replace_callback(
'#<blockquote>.+</blockquote>#ms',
'#<blockquote>.+?</blockquote>#ms',
array($this, 'markupNotes'),
$html
);

View File

@ -25,6 +25,7 @@ pre > code {
.chapter a {
border-bottom: 1px @gray-light dotted;
font-weight: @font-weight-bold;
&:hover {
border-bottom: 1px @text-color solid;
@ -60,14 +61,11 @@ pre > code {
a {
&:before {
.rounded-corners();
background-color: @icinga-blue;
color: @text-color-on-icinga-blue;
content: counter(li) ".";
color: @icinga-blue;
content: counters(li,".") " ";
display: inline-block;
font-size: small;
margin-right: 0.25em;
font-weight: @font-weight-bold;
min-width: 1.5em;
padding: 0.25em;
text-align: center;

View File

@ -43,7 +43,22 @@ $docModulePdf = new Zend_Controller_Router_Route(
)
);
$docModuleImg = new Zend_Controller_Router_Route_Regex(
'doc/module/([^/]+)/image/(.+)',
array(
'controller' => 'module',
'action' => 'image',
'module' => 'doc'
),
array(
'moduleName' => 1,
'image' => 2
),
'doc/module/%s/image/%s'
);
$this->addRoute('doc/module/chapter', $docModuleChapter);
$this->addRoute('doc/icingaweb/chapter', $docIcingaWebChapter);
$this->addRoute('doc/module/toc', $docModuleToc);
$this->addRoute('doc/module/pdf', $docModulePdf);
$this->addRoute('doc/module/img', $docModuleImg);

View File

@ -55,6 +55,9 @@ class ListCommand extends Command
$query->limit($limit, $this->params->shift('offset'));
}
foreach ($this->params->getParams() as $col => $filter) {
if (strtolower($col) === 'problems') {
$col = 'service_problem';
}
$query->where($col, $filter);
}
// $query->applyFilters($this->params->getParams());

View File

@ -49,7 +49,8 @@ class HostsController extends Controller
'host_obsessing',
'host_passive_checks_enabled',
'host_problem',
'host_state'
'host_state',
'instance_name'
));
$this->view->baseFilter = $this->hostList->getFilter();
$this->getTabs()->add(

View File

@ -39,10 +39,18 @@ class ListController extends Controller
}
/**
* Display host list
* List hosts
*/
public function hostsAction()
{
$this->addTitleTab(
'hosts',
$this->translate('Hosts'),
$this->translate('List hosts')
);
$this->setAutorefreshInterval(10);
// Handle soft and hard states
if (strtolower($this->params->shift('stateType', 'soft')) === 'hard') {
$stateColumn = 'host_hard_state';
@ -51,9 +59,8 @@ class ListController extends Controller
$stateColumn = 'host_state';
$stateChangeColumn = 'host_last_state_change';
}
$this->addTitleTab('hosts', $this->translate('Hosts'), $this->translate('List hosts'));
$this->setAutorefreshInterval(10);
$query = $this->backend->select()->from('hoststatus', array_merge(array(
$hosts = $this->backend->select()->from('hoststatus', array_merge(array(
'host_icon_image',
'host_icon_image_alt',
'host_name',
@ -71,9 +78,20 @@ class ListController extends Controller
'host_active_checks_enabled',
'host_passive_checks_enabled'
), $this->addColumns()));
$this->applyRestriction('monitoring/filter/objects', $query);
$this->filterQuery($query);
$this->view->hosts = $query;
$this->applyRestriction('monitoring/filter/objects', $hosts);
$this->filterQuery($hosts);
$this->setupPaginationControl($hosts);
$this->setupLimitControl();
$this->setupSortControl(array(
'host_severity' => $this->translate('Severity'),
'host_state' => $this->translate('Current State'),
'host_display_name' => $this->translate('Hostname'),
'host_address' => $this->translate('Address'),
'host_last_check' => $this->translate('Last Check'),
'host_last_state_change' => $this->translate('Last State Change')
), $hosts);
$stats = $this->backend->select()->from('hoststatussummary', array(
'hosts_total',
'hosts_up',
@ -86,28 +104,26 @@ class ListController extends Controller
'hosts_pending',
));
$this->applyRestriction('monitoring/filter/objects', $stats);
$this->view->stats = $stats;
$this->setupLimitControl();
$this->setupPaginationControl($this->view->hosts);
$this->setupSortControl(array(
'host_severity' => $this->translate('Severity'),
'host_state' => $this->translate('Current State'),
'host_display_name' => $this->translate('Hostname'),
'host_address' => $this->translate('Address'),
'host_last_check' => $this->translate('Last Check'),
'host_last_state_change' => $this->translate('Last State Change')
), $query);
$summary = $query->getQuery()->queryServiceProblemSummary();
$summary = $hosts->getQuery()->queryServiceProblemSummary();
$this->applyRestriction('monitoring/filter/objects', $summary);
$this->view->hosts = $hosts;
$this->view->stats = $stats;
$this->view->summary = $summary->fetchPairs();
}
/**
* Display service list
* List services
*/
public function servicesAction()
{
$this->addTitleTab(
'services',
$this->translate('Services'),
$this->translate('List services')
);
// Handle soft and hard states
if (strtolower($this->params->shift('stateType', 'soft')) === 'hard') {
$stateColumn = 'service_hard_state';
@ -117,14 +133,9 @@ class ListController extends Controller
$stateChangeColumn = 'service_last_state_change';
}
$this->addTitleTab('services', $this->translate('Services'), $this->translate('List services'));
$this->view->showHost = true;
if (strpos($this->params->get('host_name', '*'), '*') === false) {
$this->view->showHost = false;
}
$this->setAutorefreshInterval(10);
$columns = array_merge(array(
$services = $this->backend->select()->from('servicestatus', array_merge(array(
'host_name',
'host_display_name',
'host_state',
@ -147,14 +158,12 @@ class ListController extends Controller
'service_notifications_enabled',
'service_active_checks_enabled',
'service_passive_checks_enabled'
), $this->addColumns());
$query = $this->backend->select()->from('servicestatus', $columns);
$this->applyRestriction('monitoring/filter/objects', $query);
$this->filterQuery($query);
$this->view->services = $query;
), $this->addColumns()));
$this->applyRestriction('monitoring/filter/objects', $services);
$this->filterQuery($services);
$this->setupPaginationControl($services);
$this->setupLimitControl();
$this->setupPaginationControl($this->view->services);
$this->setupSortControl(array(
'service_severity' => $this->translate('Service Severity'),
'service_state' => $this->translate('Current Service State'),
@ -166,7 +175,7 @@ class ListController extends Controller
'host_display_name' => $this->translate('Hostname'),
'host_address' => $this->translate('Host Address'),
'host_last_check' => $this->translate('Last Host Check')
), $query);
), $services);
$stats = $this->backend->select()->from('servicestatussummary', array(
'services_critical',
@ -183,18 +192,30 @@ class ListController extends Controller
'services_warning_unhandled'
));
$this->applyRestriction('monitoring/filter/objects', $stats);
$this->view->services = $services;
$this->view->stats = $stats;
if (strpos($this->params->get('host_name', '*'), '*') === false) {
$this->view->showHost = false;
} else {
$this->view->showHost = true;
}
}
/**
* Fetch the current downtimes and put them into the view property `downtimes`
* List downtimes
*/
public function downtimesAction()
{
$this->addTitleTab('downtimes', $this->translate('Downtimes'), $this->translate('List downtimes'));
$this->addTitleTab(
'downtimes',
$this->translate('Downtimes'),
$this->translate('List downtimes')
);
$this->setAutorefreshInterval(12);
$query = $this->backend->select()->from('downtime', array(
$downtimes = $this->backend->select()->from('downtime', array(
'id' => 'downtime_internal_id',
'objecttype' => 'object_type',
'comment' => 'downtime_comment',
@ -215,13 +236,11 @@ class ListController extends Controller
'host_display_name',
'service_display_name'
));
$this->applyRestriction('monitoring/filter/objects', $query);
$this->filterQuery($query);
$this->view->downtimes = $query;
$this->applyRestriction('monitoring/filter/objects', $downtimes);
$this->filterQuery($downtimes);
$this->setupPaginationControl($downtimes);
$this->setupLimitControl();
$this->setupPaginationControl($this->view->downtimes);
$this->setupSortControl(array(
'downtime_is_in_effect' => $this->translate('Is In Effect'),
'host_display_name' => $this->translate('Host'),
@ -233,7 +252,9 @@ class ListController extends Controller
'downtime_scheduled_start' => $this->translate('Scheduled Start'),
'downtime_scheduled_end' => $this->translate('Scheduled End'),
'downtime_duration' => $this->translate('Duration')
), $query);
), $downtimes);
$this->view->downtimes = $downtimes;
if ($this->Auth()->hasPermission('monitoring/command/downtime/delete')) {
$this->view->delDowntimeForm = new DeleteDowntimeCommandForm();
@ -242,7 +263,7 @@ class ListController extends Controller
}
/**
* Display notification overview
* List notifications
*/
public function notificationsAction()
{
@ -251,9 +272,10 @@ class ListController extends Controller
$this->translate('Notifications'),
$this->translate('List notifications')
);
$this->setAutorefreshInterval(15);
$query = $this->backend->select()->from('notification', array(
$notifications = $this->backend->select()->from('notification', array(
'host_name',
'service_description',
'notification_output',
@ -263,22 +285,30 @@ class ListController extends Controller
'host_display_name',
'service_display_name'
));
$this->applyRestriction('monitoring/filter/objects', $query);
$this->filterQuery($query);
$this->view->notifications = $query;
$this->applyRestriction('monitoring/filter/objects', $notifications);
$this->filterQuery($notifications);
$this->setupPaginationControl($notifications);
$this->setupLimitControl();
$this->setupPaginationControl($this->view->notifications);
$this->setupSortControl(array(
'notification_start_time' => $this->translate('Notification Start')
), $query);
), $notifications);
$this->view->notifications = $notifications;
}
/**
* List contacts
*/
public function contactsAction()
{
$this->addTitleTab('contacts', $this->translate('Contacts'), $this->translate('List contacts'));
$this->addTitleTab(
'contacts',
$this->translate('Contacts'),
$this->translate('List contacts')
);
$query = $this->backend->select()->from('contact', array(
$contacts = $this->backend->select()->from('contact', array(
'contact_name',
'contact_alias',
'contact_email',
@ -286,20 +316,19 @@ class ListController extends Controller
'contact_notify_service_timeperiod',
'contact_notify_host_timeperiod'
));
$this->applyRestriction('monitoring/filter/objects', $query);
$this->filterQuery($query);
$this->view->contacts = $query;
$this->applyRestriction('monitoring/filter/objects', $contacts);
$this->filterQuery($contacts);
$this->setupPaginationControl($contacts);
$this->setupLimitControl();
$this->setupPaginationControl($this->view->contacts);
$this->setupSortControl(array(
'contact_name' => $this->translate('Name'),
'contact_alias' => $this->translate('Alias'),
'contact_email' => $this->translate('Email'),
'contact_pager' => $this->translate('Pager Address / Number'),
'contact_notify_service_timeperiod' => $this->translate('Service Notification Timeperiod'),
'contact_notify_host_timeperiod' => $this->translate('Host Notification Timeperiod')
), $query);
'contact_pager' => $this->translate('Pager Address / Number')
), $contacts);
$this->view->contacts = $contacts;
}
public function eventgridAction()
@ -342,6 +371,9 @@ class ListController extends Controller
$this->view->orientation = $orientation;
}
/**
* List contact groups
*/
public function contactgroupsAction()
{
$this->addTitleTab(
@ -350,47 +382,38 @@ class ListController extends Controller
$this->translate('List contact groups')
);
$query = $this->backend->select()->from('contactgroup', array(
$contactGroups = $this->backend->select()->from('contactgroup', array(
'contactgroup_name',
'contactgroup_alias',
'contact_name',
'contact_alias',
'contact_email',
'contact_pager'
'contact_count'
));
$this->applyRestriction('monitoring/filter/objects', $query);
$this->filterQuery($query);
$this->applyRestriction('monitoring/filter/objects', $contactGroups);
$this->filterQuery($contactGroups);
$this->setupPaginationControl($contactGroups);
$this->setupLimitControl();
$this->setupSortControl(array(
'contactgroup_name' => $this->translate('Contactgroup Name'),
'contactgroup_alias' => $this->translate('Contactgroup Alias')
), $query);
), $contactGroups);
// Fetch and prepare all contact groups:
$contactgroups = $query->getQuery()->fetchAll();
$groupData = array();
foreach ($contactgroups as $c) {
if (!array_key_exists($c->contactgroup_name, $groupData)) {
$groupData[$c->contactgroup_name] = array(
'alias' => $c->contactgroup_alias,
'contacts' => array()
);
}
if (isset ($c->contact_name)) {
$groupData[$c->contactgroup_name]['contacts'][] = $c;
}
}
// TODO: Find a better naming
$this->view->groupData = $groupData;
$this->view->contactGroups = $contactGroups;
}
/**
* List all comments
*/
public function commentsAction()
{
$this->addTitleTab('comments', $this->translate('Comments'), $this->translate('List comments'));
$this->addTitleTab(
'comments',
$this->translate('Comments'),
$this->translate('List comments')
);
$this->setAutorefreshInterval(12);
$query = $this->backend->select()->from('comment', array(
$comments = $this->backend->select()->from('comment', array(
'id' => 'comment_internal_id',
'objecttype' => 'object_type',
'comment' => 'comment_data',
@ -404,12 +427,11 @@ class ListController extends Controller
'host_display_name',
'service_display_name'
));
$this->applyRestriction('monitoring/filter/objects', $query);
$this->filterQuery($query);
$this->view->comments = $query;
$this->applyRestriction('monitoring/filter/objects', $comments);
$this->filterQuery($comments);
$this->setupPaginationControl($comments);
$this->setupLimitControl();
$this->setupPaginationControl($this->view->comments);
$this->setupSortControl(
array(
'comment_timestamp' => $this->translate('Comment Timestamp'),
@ -418,15 +440,20 @@ class ListController extends Controller
'comment_type' => $this->translate('Comment Type'),
'comment_expiration' => $this->translate('Expiration')
),
$query
$comments
);
$this->view->comments = $comments;
if ($this->Auth()->hasPermission('monitoring/command/comment/delete')) {
$this->view->delCommentForm = new DeleteCommentCommandForm();
$this->view->delCommentForm->handleRequest();
}
}
/**
* List service groups
*/
public function servicegroupsAction()
{
$this->addTitleTab(
@ -434,9 +461,10 @@ class ListController extends Controller
$this->translate('Service Groups'),
$this->translate('List service groups')
);
$this->setAutorefreshInterval(12);
$query = $this->backend->select()->from('servicegroupsummary', array(
$serviceGroups = $this->backend->select()->from('servicegroupsummary', array(
'servicegroup_alias',
'servicegroup_name',
'services_critical_handled',
@ -449,25 +477,34 @@ class ListController extends Controller
'services_warning_handled',
'services_warning_unhandled'
));
$this->applyRestriction('monitoring/filter/objects', $query);
$this->filterQuery($query);
$this->view->servicegroups = $query;
$this->applyRestriction('monitoring/filter/objects', $serviceGroups);
$this->filterQuery($serviceGroups);
$this->setupPaginationControl($serviceGroups);
$this->setupLimitControl();
$this->setupPaginationControl($this->view->servicegroups);
$this->setupSortControl(array(
'services_severity' => $this->translate('Severity'),
'servicegroup_alias' => $this->translate('Service Group Name'),
'services_total' => $this->translate('Total Services')
), $query);
), $serviceGroups);
$this->view->serviceGroups = $serviceGroups;
}
/**
* List host groups
*/
public function hostgroupsAction()
{
$this->addTitleTab('hostgroups', $this->translate('Host Groups'), $this->translate('List host groups'));
$this->addTitleTab(
'hostgroups',
$this->translate('Host Groups'),
$this->translate('List host groups')
);
$this->setAutorefreshInterval(12);
$query = $this->backend->select()->from('hostgroupsummary', array(
$hostGroups = $this->backend->select()->from('hostgroupsummary', array(
'hostgroup_alias',
'hostgroup_name',
'hosts_down_handled',
@ -487,18 +524,19 @@ class ListController extends Controller
'services_warning_handled',
'services_warning_unhandled'
));
$this->applyRestriction('monitoring/filter/objects', $query);
$this->filterQuery($query);
$this->view->hostgroups = $query;
$this->applyRestriction('monitoring/filter/objects', $hostGroups);
$this->filterQuery($hostGroups);
$this->setupPaginationControl($hostGroups);
$this->setupLimitControl();
$this->setupPaginationControl($this->view->hostgroups);
$this->setupSortControl(array(
'hosts_severity' => $this->translate('Severity'),
'hostgroup_alias' => $this->translate('Host Group Name'),
'hosts_total' => $this->translate('Total Hosts'),
'services_total' => $this->translate('Total Services')
), $query);
), $hostGroups);
$this->view->hostGroups = $hostGroups;
}
public function eventhistoryAction()

View File

@ -41,6 +41,7 @@ class ServicesController extends Controller
'host_name',
'host_problem',
'host_state',
'instance_name',
'service_acknowledged',
'service_active_checks_enabled',
'service_description',

View File

@ -58,6 +58,10 @@ class ToggleInstanceFeaturesCommandForm extends CommandForm
*/
public function createElements(array $formData = array())
{
$notificationDescription = null;
$isIcinga2 = $this->getBackend()->isIcinga2($this->status->program_version);
if (! $isIcinga2) {
if ((bool) $this->status->notifications_enabled) {
if ($this->hasPermission('monitoring/command/feature/instance')) {
$notificationDescription = sprintf(
@ -74,8 +78,7 @@ class ToggleInstanceFeaturesCommandForm extends CommandForm
$this->translate('Notifications will be re-enabled in <strong>%s</strong>'),
$this->getView()->timeUntil($this->status->disable_notif_expire_time)
);
} else {
$notificationDescription = null;
}
}
$toggleDisabled = $this->hasPermission('monitoring/command/feature/instance') ? null : '';
@ -138,7 +141,7 @@ class ToggleInstanceFeaturesCommandForm extends CommandForm
)
);
if (! $this->getBackend()->isIcinga2($this->status->program_version)) {
if (! $isIcinga2) {
$this->addElement(
'checkbox',
ToggleInstanceFeatureCommand::FEATURE_HOST_OBSESSING,

View File

@ -27,7 +27,7 @@ class Zend_View_Helper_IconImage extends Zend_View_Helper_Abstract
public function host($object)
{
if ($object->host_icon_image && ! preg_match('/[\'"]/', $object->host_icon_image)) {
return $this->view->img(
return $this->view->icon(
Macro::resolveMacros($object->host_icon_image, $object),
null,
array(
@ -50,7 +50,7 @@ class Zend_View_Helper_IconImage extends Zend_View_Helper_Abstract
public function service($object)
{
if ($object->service_icon_image && ! preg_match('/[\'"]/', $object->service_icon_image)) {
return $this->view->img(
return $this->view->icon(
Macro::resolveMacros($object->service_icon_image, $object),
null,
array(

View File

@ -51,6 +51,14 @@ class Zend_View_Helper_PluginOutput extends Zend_View_Helper_Abstract
$isHtml = false;
}
$output = $this->fixLinks($output);
// Help browsers to break words in plugin output
$output = trim($output);
// Add space after comma where missing
$output = preg_replace('/,[^\s]/', ', ', $output);
// Add zero width space after ')', ']', ':', '.', '_' and '-' if not surrounded by whitespaces
$output = preg_replace('/([^\s])([\\)\\]:._-])([^\s])/', '$1$2&#8203;$3', $output);
// Add zero width space before '(' and '[' if not surrounded by whitespaces
$output = preg_replace('/([^\s])([([])([^\s])/', '$1&#8203;$2$3', $output);
if (! $raw) {
if ($isHtml) {

View File

@ -1,11 +1,11 @@
<?php if (! $this->compact): ?>
<div class="controls">
<?= $tabs ?>
<?= $this->tabs ?>
<?= $this->render('list/components/selectioninfo.phtml') ?>
<div class="grid">
<?= $this->sortBox ?>
<?= $this->limiter ?>
<?= $this->paginator ?>
<div class="sort-controls-container">
<?= $this->limiter ?>
<?= $this->sortBox ?>
</div>
<?= $this->filterEditor ?>
</div>

View File

@ -2,23 +2,21 @@
if (! $this->compact): ?>
<div class="controls">
<?= $tabs ?>
<div class="grid">
<?= $this->sortBox ?>
<?= $this->limiter ?>
<?= $this->tabs ?>
<?= $this->paginator ?>
<div class="sort-controls-container">
<?= $this->limiter ?>
<?= $this->sortBox ?>
</div>
<?= $this->filterEditor ?>
</div>
<?php endif ?>
<div class="content">
<?php
if (count($groupData) === 0) {
echo $this->translate('No contactgroups found matching the filter') . '</div>';
return;
}
?>
<table class="action table-row-selectable common-table" data-base-target="_next">
<?php if (! $contactGroups->hasResult()): ?>
<p><?= $this->translate('No contact groups found matching the filter') ?></p>
</div>
<?php return; endif ?>
<table class="common-table table-row-selectable" data-base-target="_next">
<thead>
<tr>
<th></th>
@ -27,26 +25,25 @@ if (count($groupData) === 0) {
</tr>
</thead>
<tbody>
<?php foreach ($groupData as $groupName => $groupInfo): ?>
<?php foreach ($contactGroups as $contactGroup): ?>
<tr>
<td class="count-col">
<span class="badge"><?= count($groupInfo['contacts']) ?></span>
<span class="badge"><?= $contactGroup->contact_count ?></span>
</td>
<td>
<?= $this->qlink(
$groupName,
$contactGroup->contactgroup_name,
'monitoring/list/contacts',
array('contactgroup_name' => $groupName),
array('contactgroup_name' => $contactGroup->contactgroup_name),
array('title' => sprintf(
$this->translate('Show detailed information about %s'),
$groupName
$contactGroup->contactgroup_name
))
) ?>
</td>
<td>
<?php if ($groupInfo['alias'] !== $groupName): ?>
<?= $groupInfo['alias'] ?>
<?php if ($contactGroup->contactgroup_name !== $contactGroup->contactgroup_alias): ?>
<?= $contactGroup->contactgroup_alias ?>
<?php endif ?>
</td>
</tr>

View File

@ -1,17 +1,20 @@
<?php if (! $this->compact): ?>
<div class="controls">
<?= $tabs ?>
<div class="grid">
<?= $this->sortBox ?>
<?= $this->limiter ?>
<?= $this->tabs ?>
<?= $this->paginator ?>
<div class="sort-controls-container">
<?= $this->limiter ?>
<?= $this->sortBox ?>
</div>
<?= $this->filterEditor ?>
</div>
<?php endif ?>
<div class="content">
<?php if ($contacts->hasResult()): ?>
<table class="action table-row-selectable common-table" data-base-target="_next">
<?php if (! $contacts->hasResult()): ?>
<p><?= $this->translate('No contacts found matching the filter') ?></p>
</div>
<?php return; endif ?>
<table class="common-table table-row-selectable" data-base-target="_next">
<thead>
<tr>
<th><?= $this->translate('Name') ?></th>
@ -27,53 +30,54 @@
$contact->contact_name,
'monitoring/show/contact',
array('contact_name' => $contact->contact_name),
array('title' => sprintf(
array(
'title' => sprintf(
$this->translate('Show detailed information about %s'),
$contact->contact_alias
), 'class' => 'rowaction')
); ?>
)
)
) ?>
</td>
<td>
<?= $this->translate('Email') ?>:
<a href="mailto:<?= $contact->contact_email; ?>"
title="<?= sprintf($this->translate('Send a mail to %s'), $contact->contact_alias); ?>"
aria-label="<?= sprintf($this->translate('Send a mail to %s'), $contact->contact_alias); ?>">
<?= $this->escape($contact->contact_email); ?>
<a href="mailto:<?= $contact->contact_email ?>"
title="<?= sprintf($this->translate('Send a mail to %s'), $contact->contact_alias) ?>"
aria-label="<?= sprintf($this->translate('Send a mail to %s'), $contact->contact_alias) ?>">
<?= $this->escape($contact->contact_email) ?>
</a>
</td>
<td>
<?php if ($contact->contact_pager): ?>
<?= $this->escape($contact->contact_pager) ?>
<?php endif; ?>
<?php endif ?>
</td>
<?php if ($contact->contact_notify_service_timeperiod): ?>
<td>
<?= $this->escape($contact->contact_notify_service_timeperiod) ?>
</td>
<?php endif; ?>
<?php endif ?>
<?php if ($contact->contact_notify_host_timeperiod): ?>
<td>
<?= $this->escape($contact->contact_notify_host_timeperiod) ?>
</td>
<?php endif; ?>
<?php endif ?>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php if ($contacts->hasMore()): ?>
<?php if ($contacts->hasMore()): ?>
<div class="action-links">
<?= $this->qlink(
$this->translate('Show More'),
$this->url()->without(array('view', 'limit')),
null,
array(
'data-base-target' => '_next',
'class' => 'pull-right action-link'
'class' => 'action-link',
'data-base-target' => '_next'
)
); ?>
<?php endif ?>
<?php else: ?>
<?= $this->translate('No contacts found matching the filter'); ?>
) ?>
</div>
<?php endif ?>
</div>

View File

@ -3,21 +3,25 @@ use Icinga\Module\Monitoring\Object\Host;
use Icinga\Module\Monitoring\Object\Service;
if (! $this->compact): ?>
<div class="controls separated">
<?= $tabs ?>
<div class="controls">
<?= $this->tabs ?>
<?= $this->render('list/components/selectioninfo.phtml') ?>
<div class="grid">
<?= $this->sortBox ?>
<?= $this->limiter ?>
<?= $this->paginator ?>
<div class="sort-controls-container">
<?= $this->limiter ?>
<?= $this->sortBox ?>
</div>
<?= $this->filterEditor ?>
</div>
<?php endif ?>
<div class="content">
<table data-base-target="_next"
class="table-row-selectable state-table multiselect common-table"
data-icinga-multiselect-url="<?= $this->href('monitoring/downtimes/show'); ?>"
<?php if (! $downtimes->hasResult()): ?>
<p><?= $this->translate('No downtimes found matching the filter.') ?></p>
</div>
<?php return; endif ?>
<table class="common-table state-table table-row-selectable multiselect"
data-base-target="_next"
data-icinga-multiselect-url="<?= $this->href('monitoring/downtimes/show') ?>"
data-icinga-multiselect-controllers="<?= $this->href("monitoring/downtimes") ?>"
data-icinga-multiselect-data="downtime_id">
<tbody>
@ -29,25 +33,26 @@ if (! $this->compact): ?>
$this->isService = false;
$this->stateName = Host::getStateText($downtime->host_state);
}
// Set downtime for partials
$this->downtime = $downtime;
?>
<tr href="<?= $this->href('monitoring/downtime/show', array('downtime_id' => $downtime->id)) ?>">
<?= $this->render('partials/downtime/downtime-header.phtml'); ?>
<?= $this->render('partials/downtime/downtime-header.phtml') ?>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php if (! $downtimes->hasResult()): ?>
<?= $this->translate('No downtimes found matching the filter, maybe the downtime already expired.'); ?>
<?php elseif ($downtimes->hasMore()): ?>
<?php if ($downtimes->hasMore()): ?>
<div class="action-links">
<?= $this->qlink(
$this->translate('Show More'),
$this->url()->without(array('view', 'limit')),
null,
array(
'data-base-target' => '_next',
'class' => 'pull-right action-link'
'class' => 'action-link',
'data-base-target' => '_next'
)
); ?>
) ?>
</div>
<?php endif ?>
</div>

View File

@ -4,12 +4,8 @@ use Icinga\Web\Widget\Chart\HistoryColorGrid;
if (! $this->compact): ?>
<div class="controls">
<?= $this->tabs; ?>
<?= $this->sortBox; ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<?= $this->filterEditor; ?>
<?= $form; ?>
<?= $this->tabs ?>
<?= $this->form ?>
</div>
<?php endif ?>
<div class="content" data-base-target="_next">

View File

@ -3,11 +3,11 @@
if (! $this->compact): ?>
<div class="controls">
<?= $this->tabs ?>
<div class="grid">
<?= $this->sortBox ?>
<div class="sort-controls-container">
<?= $this->limiter ?>
<?= $this->filterEditor ?>
<?= $this->sortBox ?>
</div>
<?= $this->filterEditor ?>
</div>
<?php endif ?>
<?= $this->partial(

View File

@ -1,25 +1,25 @@
<?php
use Icinga\Module\Monitoring\Web\Widget\StateBadges;
/** @var \Icinga\Module\Monitoring\DataView\Hostgroup $hostgroups */
if (! $this->compact): ?>
<div class="controls">
<?= $tabs ?>
<div class="grid">
<?= $this->sortBox ?>
<?= $this->limiter ?>
<?= $this->tabs ?>
<?= $this->paginator ?>
<div class="sort-controls-container">
<?= $this->limiter ?>
<?= $this->sortBox ?>
</div>
<?= $this->filterEditor ?>
</div>
<?php endif ?>
<div class="content">
<?php if (! $hostgroups->hasResult()): ?>
<?php /** @var \Icinga\Module\Monitoring\DataView\Hostgroup $hostGroups */ if (! $hostGroups->hasResult()): ?>
<p><?= $this->translate('No host groups found matching the filter.') ?></p>
</div>
<?php return; endif ?>
<table class="table-row-selectable common-table" data-base-target="_next">
<thead>
<table class="common-table table-row-selectable" data-base-target="_next">
<thead>
<tr>
<th></th>
<th><?= $this->translate('Host Group') ?></th>
@ -27,21 +27,21 @@ if (! $this->compact): ?>
<th></th>
<th><?= $this->translate('Service States') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($hostgroups->peekAhead($this->compact) as $hostgroup): ?>
</thead>
<tbody>
<?php foreach ($hostGroups->peekAhead($this->compact) as $hostGroup): ?>
<tr>
<td class="count-col">
<span class="badge"><?= $hostgroup->hosts_total ?></span>
<span class="badge"><?= $hostGroup->hosts_total ?></span>
</td>
<th>
<?= $this->qlink(
$hostgroup->hostgroup_alias,
$hostGroup->hostgroup_alias,
'monitoring/list/hosts',
array('hostgroup_name' => $hostgroup->hostgroup_name),
array('hostgroup_name' => $hostGroup->hostgroup_name),
array('title' => sprintf(
$this->translate('List all hosts in the group "%s"'),
$hostgroup->hostgroup_alias
$hostGroup->hostgroup_alias
))
) ?>
</th>
@ -53,93 +53,93 @@ if (! $this->compact): ?>
->setBaseFilter($this->filterEditor->getFilter())
->add(
StateBadges::STATE_UP,
$hostgroup->hosts_up,
$hostGroup->hosts_up,
array(
'host_state' => 0,
'hostgroup_name' => $hostgroup->hostgroup_name,
'hostgroup_name' => $hostGroup->hostgroup_name,
'sort' => 'host_severity'
),
'List %u host that is currently in state UP in the host group "%s"',
'List %u hosts which are currently in state UP in the host group "%s"',
array($hostgroup->hosts_up, $hostgroup->hostgroup_alias)
array($hostGroup->hosts_up, $hostGroup->hostgroup_alias)
)
->add(
StateBadges::STATE_DOWN,
$hostgroup->hosts_down_unhandled,
$hostGroup->hosts_down_unhandled,
array(
'host_state' => 1,
'host_acknowledged' => 0,
'host_in_downtime' => 0,
'hostgroup_name' => $hostgroup->hostgroup_name,
'hostgroup_name' => $hostGroup->hostgroup_name,
'sort' => 'host_severity'
),
'List %u host that is currently in state DOWN in the host group "%s"',
'List %u hosts which are currently in state DOWN in the host group "%s"',
array($hostgroup->hosts_down_unhandled, $hostgroup->hostgroup_alias)
array($hostGroup->hosts_down_unhandled, $hostGroup->hostgroup_alias)
)
->add(
StateBadges::STATE_DOWN_HANDLED,
$hostgroup->hosts_down_handled,
$hostGroup->hosts_down_handled,
array(
'host_state' => 1,
'host_handled' => 1,
'hostgroup_name' => $hostgroup->hostgroup_name,
'hostgroup_name' => $hostGroup->hostgroup_name,
'sort' => 'host_severity'
),
'List %u host that is currently in state DOWN (Acknowledged) in the host group "%s"',
'List %u hosts which are currently in state DOWN (Acknowledged) in the host group "%s"',
array($hostgroup->hosts_down_handled, $hostgroup->hostgroup_alias)
array($hostGroup->hosts_down_handled, $hostGroup->hostgroup_alias)
)
->add(
StateBadges::STATE_UNREACHABLE,
$hostgroup->hosts_unreachable_unhandled,
$hostGroup->hosts_unreachable_unhandled,
array(
'host_state' => 2,
'host_acknowledged' => 0,
'host_in_downtime' => 0,
'hostgroup_name' => $hostgroup->hostgroup_name,
'hostgroup_name' => $hostGroup->hostgroup_name,
'sort' => 'host_severity'
),
'List %u host that is currently in state UNREACHABLE in the host group "%s"',
'List %u hosts which are currently in state UNREACHABLE in the host group "%s"',
array($hostgroup->hosts_unreachable_unhandled, $hostgroup->hostgroup_alias)
array($hostGroup->hosts_unreachable_unhandled, $hostGroup->hostgroup_alias)
)
->add(
StateBadges::STATE_UNREACHABLE_HANDLED,
$hostgroup->hosts_unreachable_handled,
$hostGroup->hosts_unreachable_handled,
array(
'host_state' => 2,
'host_handled' => 1,
'hostgroup_name' => $hostgroup->hostgroup_name,
'hostgroup_name' => $hostGroup->hostgroup_name,
'sort' => 'host_severity'
),
'List %u host that is currently in state UNREACHABLE (Acknowledged) in the host group "%s"',
'List %u hosts which are currently in state UNREACHABLE (Acknowledged) in the host group "%s"',
array($hostgroup->hosts_unreachable_handled, $hostgroup->hostgroup_alias)
array($hostGroup->hosts_unreachable_handled, $hostGroup->hostgroup_alias)
)
->add(
StateBadges::STATE_PENDING,
$hostgroup->hosts_pending,
$hostGroup->hosts_pending,
array(
'host_state' => 99,
'hostgroup_name' => $hostgroup->hostgroup_name,
'hostgroup_name' => $hostGroup->hostgroup_name,
'sort' => 'host_severity'
),
'List %u host that is currently in state PENDING in the host group "%s"',
'List %u hosts which are currently in state PENDING in the host group "%s"',
array($hostgroup->hosts_pending, $hostgroup->hostgroup_alias)
array($hostGroup->hosts_pending, $hostGroup->hostgroup_alias)
);
echo $stateBadges->render();
?>
</td>
<td class="count-col">
<?= $this->qlink(
$hostgroup->services_total,
$hostGroup->services_total,
'monitoring/list/services',
array('hostgroup_name' => $hostgroup->hostgroup_name),
array('hostgroup_name' => $hostGroup->hostgroup_name),
array('title' => sprintf(
$this->translate('List all services of all hosts in host group "%s"'),
$hostgroup->hostgroup_alias
$hostGroup->hostgroup_alias
), 'class' => 'badge')
) ?>
</td>
@ -150,129 +150,131 @@ if (! $this->compact): ?>
->setUrl('monitoring/list/services')
->add(
StateBadges::STATE_OK,
$hostgroup->services_ok,
$hostGroup->services_ok,
array(
'service_state' => 0,
'hostgroup_name' => $hostgroup->hostgroup_name,
'hostgroup_name' => $hostGroup->hostgroup_name,
'sort' => 'service_severity'
),
'List %u service that is currently in state OK on hosts in the host group "%s"',
'List %u services which are currently in state OK on hosts in the host group "%s"',
array($hostgroup->services_ok, $hostgroup->hostgroup_alias)
array($hostGroup->services_ok, $hostGroup->hostgroup_alias)
)
->add(
StateBadges::STATE_CRITICAL,
$hostgroup->services_critical_unhandled,
$hostGroup->services_critical_unhandled,
array(
'service_state' => 2,
'service_acknowledged' => 0,
'service_in_downtime' => 0,
'host_problem' => 0,
'hostgroup_name' => $hostgroup->hostgroup_name,
'hostgroup_name' => $hostGroup->hostgroup_name,
'sort' => 'service_severity'
),
'List %u service that is currently in state CRITICAL on hosts in the host group "%s"',
'List %u services which are currently in state CRITICAL on hosts in the host group "%s"',
array($hostgroup->services_critical_unhandled, $hostgroup->hostgroup_alias)
array($hostGroup->services_critical_unhandled, $hostGroup->hostgroup_alias)
)
->add(
StateBadges::STATE_CRITICAL_HANDLED,
$hostgroup->services_critical_handled,
$hostGroup->services_critical_handled,
array(
'service_state' => 2,
'service_handled' => 1,
'hostgroup_name' => $hostgroup->hostgroup_name,
'hostgroup_name' => $hostGroup->hostgroup_name,
'sort' => 'service_severity'
),
'List %u service that is currently in state CRITICAL (Acknowledged) on hosts in the host group "%s"',
'List %u services which are currently in state CRITICAL (Acknowledged) on hosts in the host group "%s"',
array($hostgroup->services_critical_unhandled, $hostgroup->hostgroup_alias)
array($hostGroup->services_critical_unhandled, $hostGroup->hostgroup_alias)
)
->add(
StateBadges::STATE_UNKNOWN,
$hostgroup->services_unknown_unhandled,
$hostGroup->services_unknown_unhandled,
array(
'service_state' => 3,
'service_acknowledged' => 0,
'service_in_downtime' => 0,
'host_problem' => 0,
'hostgroup_name' => $hostgroup->hostgroup_name,
'hostgroup_name' => $hostGroup->hostgroup_name,
'sort' => 'service_severity'
),
'List %u service that is currently in state UNKNOWN on hosts in the host group "%s"',
'List %u services which are currently in state UNKNOWN on hosts in the host group "%s"',
array($hostgroup->services_unknown_unhandled, $hostgroup->hostgroup_alias)
array($hostGroup->services_unknown_unhandled, $hostGroup->hostgroup_alias)
)
->add(
StateBadges::STATE_UNKNOWN_HANDLED,
$hostgroup->services_unknown_handled,
$hostGroup->services_unknown_handled,
array(
'service_state' => 3,
'service_handled' => 1,
'hostgroup_name' => $hostgroup->hostgroup_name,
'hostgroup_name' => $hostGroup->hostgroup_name,
'sort' => 'service_severity'
),
'List %u service that is currently in state UNKNOWN (Acknowledged) on hosts in the host group "%s"',
'List %u services which are currently in state UNKNOWN (Acknowledged) on hosts in the host group "%s"',
array($hostgroup->services_unknown_handled, $hostgroup->hostgroup_alias)
array($hostGroup->services_unknown_handled, $hostGroup->hostgroup_alias)
)
->add(
StateBadges::STATE_WARNING,
$hostgroup->services_warning_unhandled,
$hostGroup->services_warning_unhandled,
array(
'service_state' => 1,
'service_acknowledged' => 0,
'service_in_downtime' => 0,
'host_problem' => 0,
'hostgroup_name' => $hostgroup->hostgroup_name,
'hostgroup_name' => $hostGroup->hostgroup_name,
'sort' => 'service_severity'
),
'List %u service that is currently in state WARNING on hosts in the host group "%s"',
'List %u services which are currently in state WARNING on hosts in the host group "%s"',
array($hostgroup->services_warning_unhandled, $hostgroup->hostgroup_alias)
array($hostGroup->services_warning_unhandled, $hostGroup->hostgroup_alias)
)
->add(
StateBadges::STATE_WARNING_HANDLED,
$hostgroup->services_warning_handled,
$hostGroup->services_warning_handled,
array(
'service_state' => 1,
'service_handled' => 1,
'hostgroup_name' => $hostgroup->hostgroup_name,
'hostgroup_name' => $hostGroup->hostgroup_name,
'sort' => 'service_severity'
),
'List %u service that is currently in state WARNING (Acknowledged) on hosts in the host group "%s"',
'List %u services which are currently in state WARNING (Acknowledged) on hosts in the host group "%s"',
array($hostgroup->services_warning_handled, $hostgroup->hostgroup_alias)
array($hostGroup->services_warning_handled, $hostGroup->hostgroup_alias)
)
->add(
StateBadges::STATE_PENDING,
$hostgroup->services_pending,
$hostGroup->services_pending,
array(
'service_state' => 99,
'hostgroup_name' => $hostgroup->hostgroup_name,
'hostgroup_name' => $hostGroup->hostgroup_name,
'sort' => 'service_severity'
),
'List %u service that is currently in state PENDING on hosts in the host group "%s"',
'List %u services which are currently in state PENDING on hosts in the host group "%s"',
array($hostgroup->services_pending, $hostgroup->hostgroup_alias)
array($hostGroup->services_pending, $hostGroup->hostgroup_alias)
);
echo $stateBadges->render();
?>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php if ($hostgroups->hasMore()): ?>
<?= $this->qlink(
<?php endforeach ?>
</tbody>
</table>
<?php if ($hostGroups->hasMore()): ?>
<div class="action-links">
<?= $this->qlink(
$this->translate('Show More'),
$this->url()->without(array('view', 'limit')),
null,
array(
'data-base-target' => '_next',
'class' => 'pull-right action-link'
'class' => 'action-link',
'data-base-target' => '_next'
)
) ?>
) ?>
</div>
<?php endif ?>
</div>

View File

@ -4,14 +4,12 @@ use Icinga\Module\Monitoring\Object\Host;
if (! $this->compact): ?>
<div class="controls">
<?= $this->tabs ?>
<div class="grid">
<?= $this->render('list/components/hostssummary.phtml') ?>
<?= $this->render('list/components/selectioninfo.phtml') ?>
</div>
<div class="grid">
<?= $this->sortBox ?>
<?= $this->limiter ?>
<?= $this->paginator ?>
<div class="sort-controls-container">
<?= $this->limiter ?>
<?= $this->sortBox ?>
</div>
<?= $this->filterEditor ?>
</div>
@ -19,7 +17,7 @@ if (! $this->compact): ?>
<div class="content">
<?php if (! $hosts->hasResult()): ?>
<p><?= $this->translate('No hosts found matching the filter.') ?></p>
</div>
</div>
<?php return; endif ?>
<table data-base-target="_next"
class="table-row-selectable state-table multiselect"
@ -96,14 +94,14 @@ if (! $this->compact): ?>
</tbody>
</table>
<?php if ($hosts->hasMore()): ?>
<div class="text-right">
<div class="action-links">
<?= $this->qlink(
$this->translate('Show More'),
$this->url()->without(array('view', 'limit')),
null,
array(
'data-base-target' => '_next',
'class' => 'action-link'
'class' => 'action-link',
'data-base-target' => '_next'
)
) ?>
</div>

View File

@ -4,11 +4,11 @@ use Icinga\Module\Monitoring\Object\Service;
if (! $this->compact): ?>
<div class="controls">
<?= $tabs ?>
<div class="grid">
<?= $this->sortBox ?>
<?= $this->limiter ?>
<?= $this->tabs ?>
<?= $this->paginator ?>
<div class="sort-controls-container">
<?= $this->limiter ?>
<?= $this->sortBox ?>
</div>
<?= $this->filterEditor ?>
</div>
@ -77,7 +77,7 @@ if (! $this->compact): ?>
</tbody>
</table>
<?php if ($notifications->hasMore()): ?>
<div class="text-right">
<div class="action-links">
<?= $this->qlink(
$this->translate('Show More'),
$this->url(isset($notificationsUrl) ? $notificationsUrl : null)->without(array('view', 'limit')),

View File

@ -2,30 +2,30 @@
if (! $this->compact): ?>
<div class="controls">
<?= $tabs ?>
<div class="grid">
<?= $this->sortBox ?>
<?= $this->limiter ?>
<?= $this->tabs ?>
<?= $this->paginator ?>
<div class="sort-controls-container">
<?= $this->limiter ?>
<?= $this->sortBox ?>
</div>
<?= $this->filterEditor ?>
</div>
<?php endif ?>
<div class="content">
<?php if (! $servicegroups->hasResult()): ?>
<?php if (! $serviceGroups->hasResult()): ?>
<p><?= $this->translate('No service groups found matching the filter.') ?></p>
</div>
<?php return; endif ?>
<table class="table-row-selectable common-table" data-base-target="_next">
<thead>
<table class="table-row-selectable common-table" data-base-target="_next">
<thead>
<tr>
<th></th>
<th><?= $this->translate('Service Group') ?></th>
<th><?= $this->translate('Service States') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($servicegroups->peekAhead($this->compact) as $serviceGroup): ?>
</thead>
<tbody>
<?php foreach ($serviceGroups->peekAhead($this->compact) as $serviceGroup): ?>
<tr>
<td class="count-col">
<span class="badge"><?= $serviceGroup->services_total ?></span>
@ -157,18 +157,20 @@ if (! $this->compact): ?>
?>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php if ($servicegroups->hasMore()): ?>
<?= $this->qlink(
<?php endforeach ?>
</tbody>
</table>
<?php if ($serviceGroups->hasMore()): ?>
<div class="action-links">
<?= $this->qlink(
$this->translate('Show More'),
$this->url()->without(array('view', 'limit')),
null,
array(
'data-base-target' => '_next',
'class' => 'pull-right action-link'
'class' => 'action-link',
'data-base-target' => '_next'
)
) ?>
) ?>
</div>
<?php endif ?>
</div>

View File

@ -5,14 +5,12 @@ use Icinga\Module\Monitoring\Object\Service;
if (! $this->compact): ?>
<div class="controls">
<?= $this->tabs ?>
<div class="grid">
<?= $this->render('list/components/servicesummary.phtml') ?>
<?= $this->render('list/components/selectioninfo.phtml') ?>
</div>
<div class="grid">
<?= $this->sortBox ?>
<?= $this->limiter ?>
<?= $this->paginator ?>
<div class="sort-controls-container">
<?= $this->limiter ?>
<?= $this->sortBox ?>
</div>
<?= $this->filterEditor ?>
</div>
@ -101,16 +99,16 @@ if (! $this->compact): ?>
</tbody>
</table>
<?php if ($services->hasMore()): ?>
<div class="text-right">
<div class="action-links">
<?= $this->qlink(
$this->translate('Show More'),
$this->url()->without(array('view', 'limit')),
null,
array(
'data-base-target' => '_next',
'class' => 'action-link'
'class' => 'action-link',
'data-base-target' => '_next'
)
) ?>
</div>
</div>
<?php endif ?>
</div>

View File

@ -16,12 +16,12 @@ class ContactgroupQuery extends IdoQuery
/**
* {@inheritdoc}
*/
protected $groupBase = array('contactgroups' => array('cg.contactgroup_id', 'cgo.object_id'));
protected $groupBase = array('contactgroups' => array('cg.contactgroup_id'));
/**
* {@inheritdoc}
*/
protected $groupOrigin = array('contacts', 'hosts', 'services');
protected $groupOrigin = array('hosts', 'members', 'services');
/**
* {@inheritdoc}
@ -32,28 +32,8 @@ class ContactgroupQuery extends IdoQuery
'contactgroup_name' => 'cgo.name1',
'contactgroup_alias' => 'cg.alias COLLATE latin1_general_ci'
),
'contacts' => array(
'contact_id' => 'c.contact_id',
'contact' => 'co.name1 COLLATE latin1_general_ci',
'contact_name' => 'co.name1',
'contact_alias' => 'c.alias COLLATE latin1_general_ci',
'contact_email' => 'c.email_address COLLATE latin1_general_ci',
'contact_pager' => 'c.pager_address',
'contact_object_id' => 'c.contact_object_id',
'contact_has_host_notfications' => 'c.host_notifications_enabled',
'contact_has_service_notfications' => 'c.service_notifications_enabled',
'contact_can_submit_commands' => 'c.can_submit_commands',
'contact_notify_service_recovery' => 'c.notify_service_recovery',
'contact_notify_service_warning' => 'c.notify_service_warning',
'contact_notify_service_critical' => 'c.notify_service_critical',
'contact_notify_service_unknown' => 'c.notify_service_unknown',
'contact_notify_service_flapping' => 'c.notify_service_flapping',
'contact_notify_service_downtime' => 'c.notify_service_recovery',
'contact_notify_host_recovery' => 'c.notify_host_recovery',
'contact_notify_host_down' => 'c.notify_host_down',
'contact_notify_host_unreachable' => 'c.notify_host_unreachable',
'contact_notify_host_flapping' => 'c.notify_host_flapping',
'contact_notify_host_downtime' => 'c.notify_host_downtime'
'members' => array(
'contact_count' => 'COUNT(cgm.contactgroup_member_id)'
),
'hostgroups' => array(
'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
@ -99,22 +79,18 @@ class ContactgroupQuery extends IdoQuery
}
/**
* Join contacts
* Join contact group members
*/
protected function joinContacts()
protected function joinMembers()
{
$this->select->joinLeft(
array('cgm' => $this->prefix . 'contactgroup_members'),
'cgm.contactgroup_id = cg.contactgroup_id',
array()
)->joinLeft(
)->join(
array('co' => $this->prefix . 'objects'),
'co.object_id = cgm.contact_object_id AND co.is_active = 1 AND co.objecttype_id = 10',
array()
)->joinLeft(
array('c' => $this->prefix . 'contacts'),
'c.contact_object_id = co.object_id',
array()
);
}

View File

@ -11,31 +11,9 @@ class Contactgroup extends DataView
public function getColumns()
{
return array(
'instance_name',
'contactgroup_name',
'contactgroup_alias',
'contact_object_id',
'contact_id',
'contact_name',
'contact_alias',
'contact_email',
'contact_pager',
'contact_has_host_notfications',
'contact_has_service_notfications',
'contact_can_submit_commands',
'contact_notify_service_recovery',
'contact_notify_service_warning',
'contact_notify_service_critical',
'contact_notify_service_unknown',
'contact_notify_service_flapping',
'contact_notify_service_downtime',
'contact_notify_host_recovery',
'contact_notify_host_down',
'contact_notify_host_unreachable',
'contact_notify_host_flapping',
'contact_notify_host_downtime',
'contact_notify_host_timeperiod',
'contact_notify_service_timeperiod'
'contact_count'
);
}
@ -60,9 +38,10 @@ class Contactgroup extends DataView
public function getStaticFilterColumns()
{
return array(
'contactgroup', 'contact',
'contactgroup',
'host', 'host_name', 'host_display_name', 'host_alias',
'hostgroup', 'hostgroup_alias', 'hostgroup_name',
'instance_name',
'service', 'service_description', 'service_display_name',
'servicegroup', 'servicegroup_alias', 'servicegroup_name'
);

View File

@ -11,58 +11,56 @@ class HostStatus extends DataView
public function getColumns()
{
return array_merge($this->getHookedColumns(), array(
'instance_name',
'host_name',
'host_display_name',
'host_alias',
'host_acknowledged',
'host_acknowledgement_type',
'host_action_url',
'host_active_checks_enabled',
'host_active_checks_enabled_changed',
'host_address',
'host_address6',
'host_state',
'host_hard_state',
'host_state_type',
'host_handled',
'host_unhandled',
'host_in_downtime',
'host_acknowledged',
'host_last_state_change',
'host_last_state_change',
'host_last_notification',
'host_last_check',
'host_next_check',
'host_alias',
'host_check_command',
'host_check_execution_time',
'host_check_latency',
'host_output',
'host_long_output',
'host_check_command',
'host_check_timeperiod',
'host_perfdata',
'host_check_source',
'host_passive_checks_enabled',
'host_passive_checks_enabled_changed',
'host_obsessing',
'host_obsessing_changed',
'host_notifications_enabled',
'host_notifications_enabled_changed',
'host_check_timeperiod',
'host_current_check_attempt',
'host_current_notification_number',
'host_display_name',
'host_event_handler_enabled',
'host_event_handler_enabled_changed',
'host_flap_detection_enabled',
'host_flap_detection_enabled_changed',
'host_active_checks_enabled',
'host_active_checks_enabled_changed',
'host_current_check_attempt',
'host_max_check_attempts',
'host_last_notification',
'host_current_notification_number',
'host_percent_state_change',
'host_is_flapping',
'host_action_url',
'host_notes_url',
'host_percent_state_change',
'host_modified_host_attributes',
'host_severity',
'host_problem',
'host_handled',
'host_hard_state',
'host_in_downtime',
'host_ipv4',
'host_acknowledgement_type'
'host_is_flapping',
'host_is_reachable',
'host_last_check',
'host_last_notification',
'host_last_state_change',
'host_long_output',
'host_max_check_attempts',
'host_modified_host_attributes',
'host_name',
'host_next_check',
'host_notes_url',
'host_notifications_enabled',
'host_notifications_enabled_changed',
'host_obsessing',
'host_obsessing_changed',
'host_output',
'host_passive_checks_enabled',
'host_passive_checks_enabled_changed',
'host_percent_state_change',
'host_perfdata',
'host_problem',
'host_severity',
'host_state',
'host_state_type',
'host_unhandled',
'instance_name'
));
}

View File

@ -11,93 +11,91 @@ class ServiceStatus extends DataView
public function getColumns()
{
return array_merge($this->getHookedColumns(), array(
'instance_name',
'host_name',
'host_display_name',
'host_state',
'host_hard_state',
'host_state_type',
'host_last_state_change',
'host_acknowledged',
'host_action_url',
'host_active_checks_enabled',
'host_address',
'host_address6',
'host_problem',
'host_alias',
'host_check_source',
'host_display_name',
'host_handled',
'service_description',
'service_display_name',
'service_state',
'service_hard_state',
'service_in_downtime',
'service_acknowledged',
'service_handled',
'service_unhandled',
'service_output',
'service_last_state_change',
'service_long_output',
'service_is_flapping',
'service_state_type',
'service_severity',
'service_last_check',
'service_notifications_enabled',
'service_notifications_enabled_changed',
'service_action_url',
'service_notes',
'service_notes_url',
'service_last_check',
'service_next_check',
'service_attempt',
'service_last_notification',
'service_check_command',
'service_current_notification_number',
'host_acknowledged',
'host_output',
'host_long_output',
'host_hard_state',
'host_in_downtime',
'host_ipv4',
'host_is_flapping',
'host_last_check',
'host_notifications_enabled',
'host_unhandled_service_count',
'host_action_url',
'host_notes_url',
'host_display_name',
'host_alias',
'host_ipv4',
'host_severity',
'host_perfdata',
'host_check_source',
'host_active_checks_enabled',
'host_passive_checks_enabled',
'host_last_hard_state',
'host_last_hard_state_change',
'host_last_time_up',
'host_last_state_change',
'host_last_time_down',
'host_last_time_unreachable',
'host_last_time_up',
'host_long_output',
'host_modified_host_attributes',
'service_hard_state',
'service_problem',
'service_perfdata',
'service_check_source',
'service_check_timeperiod',
'host_name',
'host_notes_url',
'host_notifications_enabled',
'host_output',
'host_passive_checks_enabled',
'host_perfdata',
'host_problem',
'host_severity',
'host_state',
'host_state_type',
'host_unhandled_service_count',
'instance_name',
'service_acknowledged',
'service_acknowledgement_type',
'service_action_url',
'service_active_checks_enabled',
'service_active_checks_enabled_changed',
'service_passive_checks_enabled',
'service_passive_checks_enabled_changed',
'service_last_hard_state',
'service_last_hard_state_change',
'service_last_time_ok',
'service_last_time_warning',
'service_last_time_critical',
'service_last_time_unknown',
'service_attempt',
'service_check_command',
'service_check_source',
'service_check_timeperiod',
'service_current_check_attempt',
'service_max_check_attempts',
'service_obsessing',
'service_obsessing_changed',
'service_current_notification_number',
'service_description',
'service_display_name',
'service_event_handler_enabled',
'service_event_handler_enabled_changed',
'service_flap_detection_enabled',
'service_flap_detection_enabled_changed',
'service_modified_service_attributes',
'service_handled',
'service_hard_state',
'service_host_name',
'service_acknowledgement_type',
'service_in_downtime',
'service_is_flapping',
'service_is_reachable',
'service_last_check',
'service_last_hard_state',
'service_last_hard_state_change',
'service_last_notification',
'service_last_state_change',
'service_last_time_critical',
'service_last_time_ok',
'service_last_time_unknown',
'service_last_time_warning',
'service_long_output',
'service_max_check_attempts',
'service_modified_service_attributes',
'service_next_check',
'service_notes',
'service_notes_url',
'service_notifications_enabled',
'service_notifications_enabled_changed',
'service_obsessing',
'service_obsessing_changed',
'service_output',
'service_passive_checks_enabled',
'service_passive_checks_enabled_changed',
'service_perfdata',
'service_problem',
'service_severity',
'service_state',
'service_state_type',
'service_unhandled'
));
}

View File

@ -257,6 +257,7 @@ class StateBadges extends AbstractWidget
$groupItem = new NavigationItem(
uniqid(),
array(
'cssClass' => 'state-badge-group',
'label' => '',
'priority' => $this->priority++
)

View File

@ -45,16 +45,27 @@
padding-right: 0;
}
}
}
// Multi-selection info
.selection-info {
float: right;
font-size: @font-size-small;
padding: @vertical-padding / 2 0;
.state-badge-group li {
margin-right: 1px;
}
&:hover {
cursor: help;
.state-badge-group li:last-child {
margin-right: 0;
}
.state-badge-group .badge {
border-radius: 0;
}
.state-badge-group li:first-child > .badge {
border-top-left-radius: 0.4em;
border-bottom-left-radius: 0.4em;
}
.state-badge-group li:last-child > .badge {
border-top-right-radius: 0.4em;
border-bottom-right-radius: 0.4em;
}
}
@ -79,6 +90,11 @@
}
}
.controls > .hosts-summary,
.controls > .services-summary {
float: left;
}
// State table in the host and service multi-selection and detail views
.host-detail-state,
.service-detail-state {
@ -94,10 +110,6 @@
/* Generic box element */
.boxview a {
text-decoration: none;
}
.boxview > div.box {
text-align: center;
vertical-align: top;
@ -126,7 +138,6 @@
/* Any line of a box entry */
.boxview div.box.entry a {
display: block;
color: inherit;
}
.boxview div.box.badge {
@ -370,13 +381,7 @@ div.timeline {
a {
font-weight: bold;
text-decoration: none;
white-space: nowrap;
&:hover {
text-decoration: underline;
}
}
}

View File

@ -78,13 +78,32 @@
font-size: @font-size-small;
}
// Plugin output in overviews
// Plugin output in detail views
.plugin-output,
// Plugin output in overvies
.overview-plugin-output {
-webkit-hyphens: auto;
-moz-hyphens: auto;
-ms-hyphens: auto;
hyphens: auto;
overflow-wrap: break-word;
word-wrap: break-word;
}
// Plugin output in overvies
.overview-plugin-output {
color: @text-color-light;
font-family: @font-family-fixed;
font-size: @font-size-small;
margin: 0;
white-space: pre-wrap;
// Long text in table cells overflows the table's width if the table's layout is not fixed.
// Thus overflow-wrap will not have any effect. But w/ the following we set a width of any value
// plus a min-width of 100% to consume the full width nonetheless which seems to always
// instruct browsers to not overflow the table. Ridiculous.
min-width: 100%;
width: 1em;
}
// Table for performance data in detail views

View File

@ -284,6 +284,7 @@ class GettextTranslationHelper
'--keyword=translatePlural:1,2',
'--keyword=translatePlural:1,2,4c',
'--keyword=mt:2',
'--keyword=mt:2,3c',
'--keyword=mtp:2,3',
'--keyword=mtp:2,3,5c',
'--keyword=t',

View File

@ -44,7 +44,7 @@
@tr-hover-color: #F5FDFF;
// Font families
@font-family: Calibri, Helvetica, sans-serif;
@font-family: Calibri, "Lucida Grande", sans-serif;
@font-family-fixed: "Liberation Mono", "Lucida Console", Courier, monospace;
@font-family-wide: Tahoma, Verdana, sans-serif;
@ -80,15 +80,19 @@ a {
color: inherit;
text-decoration: none;
&:focus {
outline-color: @icinga-blue;
}
&:hover {
text-decoration: underline;
}
}
a:focus,
button:focus,
input[type="checkbox"]:focus {
outline-color: @icinga-blue;
outline-style: solid; // IE
outline-style: auto;
}
// Default margin for block text
blockquote, p, pre {
margin: 0 0 1em 0;

View File

@ -1,7 +1,49 @@
/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
// TODO(el): Rename .filter to .filter-control
// Hide auto sumbit info in controls but keep information for screen readers
.controls .autosubmit-info {
.sr-only();
}
// Backend selection control in user and group list views
.backend-selection {
float: left;
> .control-group {
padding: 0;
> .control-label-group {
text-align: left;
width: auto;
}
}
}
.backend-selection,
.pagination-control,
.selection-info,
.sort-controls-container {
margin-bottom: 0.5em;
}
.backend-selection,
.pagination-control,
.sort-controls-container {
// Select controls may not respect font-size thus leading to improperly vertical alignment w/o big enough line-height
line-height: 2em;
}
.filter {
// Display filter control on a new line
clear: both;
}
.limiter-control > .control-group {
padding: 0;
// Note that the sort-control form does not have padding as it's utilizing different decorators
> .control-label-group {
text-align: left;
@ -18,7 +60,17 @@
}
}
.limiter-control,
.sort-control {
// Display limiter and sort control both on the same line; floating could be used here too to achieve the same
display: inline-block;
}
.pagination-control {
// Display the pagination-control on a new line
clear: both;
float: left;
li {
&.active {
> a,
@ -57,7 +109,19 @@
}
}
// Multi-selection info
.selection-info {
float: right;
font-size: @font-size-small;
&:hover {
cursor: help;
}
}
.sort-control {
margin-left: 0.25em;
label {
width: auto;
margin-right: 0.5em;
@ -72,10 +136,16 @@
width: 8em;
margin-left: 0;
}
}
> form > i {
.sr-only();
}
.sort-controls-container {
clear: right;
float: right;
}
.sort-direction-control {
margin-left: 0.25em;
width: 1em;
}
html.no-js .sort-control form {

View File

@ -83,6 +83,10 @@ form.inline {
margin-left: 10em;
}
.autosubmit-info {
margin-left: 0.5em;
}
button:hover .icon-cancel {
color: @color-critical;
}

View File

@ -76,7 +76,6 @@
@gutter: 1em;
// x-column-layout
#main {
.clearfix();
@ -109,6 +108,22 @@
}
}
// Mobile menu
#mobile-menu-toggle {
text-align: right;
> button {
background: none;
border: none;
height: 2.5em;
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
appearance: none;
}
}
#sidebar {
background-color: @gray-lighter;
}

View File

@ -3,8 +3,6 @@
// Login page styles
#login {
font-size: 1em;
#icinga-logo {
.fadein();
}

View File

@ -21,6 +21,7 @@
}
.icon-col {
text-align: center;
width: 1em;
}
@ -68,6 +69,16 @@ a:hover > .icon-cancel {
.button-link {
.action-link();
.rounded-corners(3px);
background: @gray-lighter;
display: inline-block;
padding: 0.25em 0.5em;
&:hover {
background: @gray-lightest;
text-decoration: none;
}
}
// List styles
@ -177,41 +188,6 @@ a:hover > .icon-cancel {
width: @name-value-table-name-width;
}
// TODO(el): Fix
.controls {
.limiter-control {
float: right;
padding: @vertical-padding / 2 0;
margin-bottom: 0.25em;
}
.pagination-control {
padding: @vertical-padding / 2 0;
margin-bottom: 0.25em;
}
.sort-control {
float: right;
padding: @vertical-padding / 2 0;
margin-bottom: 0.25em;
margin-left: 0.5em;
}
.sort-direction-control {
margin-bottom: 0.5em;
margin-left: 0.5em;
width: 1em;
}
.filter {
margin-bottom: 0.5em;
}
.selection-info {
margin-left: 0.5em;
}
}
/* Styles for centering content of unknown width and height both horizontally and vertically
*
* Example markup:

View File

@ -56,8 +56,6 @@
}
.rounded-corners(@border-radius: 0.4em) {
-webkit-border-radius: @border-radius;
-moz-border-radius: @border-radius;
border-radius: @border-radius;
-webkit-background-clip: padding-box;

View File

@ -29,10 +29,6 @@
> a {
color: @body-bg-color;
&:hover {
text-decoration: underline;
}
}
&.active > a,

View File

@ -59,3 +59,8 @@
#menu .active > a {
text-decoration: underline;
}
.boxview a:focus {
color: @text-color;
text-decoration: underline;
}

View File

@ -55,6 +55,20 @@
* @returns {string|NULL} The content to be rendered, or NULL, when nothing should be changed
*/
Form.prototype.renderHook = function(content, $container, action, autorefresh) {
if ($container.attr('id') === 'menu') {
var $search = $container.find('#search');
if ($search[0] === document.activeElement) {
return null;
}
var search = $container.find('#search').val();
if (search.length) {
var $content = $('<div></div>').append(content);
$content.find('#search').attr('value', search).addClass('active');
return $content.html();
}
return content;
}
var origFocus = document.activeElement;
var containerId = $container.attr('id');
var icinga = this.icinga;

View File

@ -421,7 +421,7 @@
var $target;
var formerUrl;
var remote = /^(?:[a-z]+:)\/\//;
if (href.match(/^(mailto|javascript):/)) {
if (href.match(/^(mailto|javascript|data):/)) {
return true;
}

View File

@ -100,9 +100,6 @@
var $oldLink = $(this);
if ($oldLink.hasAttr('type') && $oldLink.attr('type').indexOf('css') > -1) {
var base = location.protocol + '//' + location.host;
if (location.port) {
base += location.port;
}
var url = icinga.utils.addUrlParams(
$(this).attr('href'),
{ id: new Date().getTime() }
@ -587,7 +584,7 @@
});
if ($layout.hasClass('minimal-layout')) {
if (! this.mobileMenu) {
if (! this.mobileMenu && $sidebar.length) {
$header.css({
top: $sidebar.outerHeight() + 'px'
});
@ -599,19 +596,18 @@
zIndex: 2
});
$sidebar
.on(
'click',
this.toggleMobileMenu
)
.prepend(
$('<div id="mobile-menu-toggle"><button><i class="icon-menu"></i></button></div>')
)
.css({
paddingBottom: 32,
paddingBottom: $('#mobile-menu-toggle').height(),
top: 0,
zIndex: 3
})
.on('click', this.toggleMobileMenu)
.prepend(
$('<i id="mobile-menu-toggle" class="icon-menu"></i>').css({
cursor: 'pointer',
display: 'block',
height: '32px'
})
);
});
$search.on('keypress', this.closeMobileMenu);
this.mobileMenu = true;