diff --git a/README.md b/README.md index 78aa4fe79..7705853d9 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/application/controllers/DashboardController.php b/application/controllers/DashboardController.php index a2ea68fd3..4a7fda9ee 100644 --- a/application/controllers/DashboardController.php +++ b/application/controllers/DashboardController.php @@ -244,20 +244,35 @@ class DashboardController extends ActionController if (! $this->dashboard->hasPanes()) { $this->view->title = 'Dashboard'; } else { - if ($this->_getParam('pane')) { - $pane = $this->_getParam('pane'); - $this->dashboard->activate($pane); - } - if ($this->dashboard === null) { - $this->view->title = 'Dashboard'; - } else { - $this->view->title = $this->dashboard->getActivePane()->getTitle() . ' :: Dashboard'; - if ($this->hasParam('remove')) { - $this->dashboard->getActivePane()->removeDashlet($this->getParam('remove')); - $this->dashboard->getConfig()->saveIni(); - $this->redirectNow(URL::fromRequest()->remove('remove')); + $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'); + $this->dashboard->activate($pane); + } + if ($this->dashboard === null) { + $this->view->title = 'Dashboard'; + } else { + $this->view->title = $this->dashboard->getActivePane()->getTitle() . ' :: Dashboard'; + if ($this->hasParam('remove')) { + $this->dashboard->getActivePane()->removeDashlet($this->getParam('remove')); + $this->dashboard->getConfig()->saveIni(); + $this->redirectNow(URL::fromRequest()->remove('remove')); + } + $this->view->dashboard = $this->dashboard; } - $this->view->dashboard = $this->dashboard; } } } diff --git a/application/views/scripts/dashboard/settings.phtml b/application/views/scripts/dashboard/settings.phtml index 2d22aaf8e..29eb8a150 100644 --- a/application/views/scripts/dashboard/settings.phtml +++ b/application/views/scripts/dashboard/settings.phtml @@ -18,6 +18,7 @@ dashboard->getPanes() as $pane): ?> + getDisabled()) continue; ?> escape($pane->getName()) ?> @@ -43,7 +44,7 @@ - getDisabled() === true) continue; ?> + getDisabled()) continue; ?> qlink( diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml index cf0645633..84d75018c 100644 --- a/application/views/scripts/group/list.phtml +++ b/application/views/scripts/group/list.phtml @@ -5,16 +5,14 @@ use Icinga\Data\Reducible; if (! $this->compact): ?>
- -
- sortBox ?> + tabs ?> + paginator ?> +
limiter ?> - paginator ?> -
-
- backendSelection; ?> - filterEditor; ?> + sortBox ?>
+ backendSelection ?> + filterEditor ?>
@@ -45,8 +43,7 @@ if (! isset($backend)) { hasResult()): ?>

translate('No user groups found matching the filter'); ?>

- - + @@ -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' ) ); ?> diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml index 182e26e87..7e045d8a9 100644 --- a/application/views/scripts/user/list.phtml +++ b/application/views/scripts/user/list.phtml @@ -5,16 +5,14 @@ use Icinga\Data\Reducible; if (! $this->compact): ?>
- -
- sortBox ?> + tabs ?> + paginator ?> +
limiter ?> - paginator ?> -
-
- backendSelection ?> - filterEditor ?> + sortBox ?>
+ backendSelection ?> + filterEditor ?>
diff --git a/doc/about.md b/doc/01-About.md similarity index 100% rename from doc/about.md rename to doc/01-About.md diff --git a/doc/installation.md b/doc/02-Installation.md similarity index 94% rename from doc/installation.md rename to doc/02-Installation.md index 04ec7b842..9afe087e8 100644 --- a/doc/installation.md +++ b/doc/02-Installation.md @@ -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. ## Installing Requirements @@ -179,7 +179,7 @@ git clone git://git.icinga.org/icingaweb2.git ### 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. #### 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 #### 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 = "*" #### 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 # Upgrading Icinga Web 2 -## Upgrading to Icinga Web 2 Beta 2 +## 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. +## 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 what’s 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`. + +## Upgrading to Icinga Web 2 2.1.x -## 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. - -## 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`. ## 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 **<config-dir>/preferences/<username>/config.ini**. The content of the file remains unchanged. -## Upgrading to Icinga Web 2 2.1.x +## 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: -## 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. + +## 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. + +## 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 what’s going on. A similar change +affects environments that opted for not storing preferences, your new backend is `none`. diff --git a/doc/03-Configuration.md b/doc/03-Configuration.md new file mode 100644 index 000000000..4df9ad2e6 --- /dev/null +++ b/doc/03-Configuration.md @@ -0,0 +1,15 @@ +# 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 diff --git a/doc/resources.md b/doc/04-Resources.md similarity index 100% rename from doc/resources.md rename to doc/04-Resources.md diff --git a/doc/authentication.md b/doc/05-Authentication.md similarity index 59% rename from doc/authentication.md rename to doc/05-Authentication.md index 21f5a8b9f..c30c593ff 100644 --- a/doc/authentication.md +++ b/doc/05-Authentication.md @@ -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. -### External Authentication +## 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. -### Active Directory or LDAP Authentication +### 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 **<Directory> 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. + +## 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. -#### LDAP +### 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. -#### Active Directory +### 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 ``` -### Database Authentication +## 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 ``` -#### Database Setup +### 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'); -```` +``` diff --git a/doc/security.md b/doc/06-Security.md similarity index 88% rename from doc/security.md rename to doc/06-Security.md index 3b8731a7d..65c908342 100644 --- a/doc/security.md +++ b/doc/06-Security.md @@ -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 -Command Pipe -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).
@@ -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 **. @@ -157,18 +156,18 @@ 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/ | Allow access to module +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> ### 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. ## 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 diff --git a/doc/preferences.md b/doc/07-Preferences.md similarity index 89% rename from doc/preferences.md rename to doc/07-Preferences.md index ca26b66b0..e89a507bf 100644 --- a/doc/preferences.md +++ b/doc/07-Preferences.md @@ -27,13 +27,13 @@ type = ini ### 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). diff --git a/doc/vagrant.md b/doc/99-Vagrant.md similarity index 100% rename from doc/vagrant.md rename to doc/99-Vagrant.md diff --git a/doc/configuration.md b/doc/configuration.md deleted file mode 100644 index 90946be83..000000000 --- a/doc/configuration.md +++ /dev/null @@ -1,15 +0,0 @@ -# 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 diff --git a/doc/external_authentication.md b/doc/external_authentication.md deleted file mode 100644 index 05225fb82..000000000 --- a/doc/external_authentication.md +++ /dev/null @@ -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. - -```` - - AuthType digest - AuthName "Icinga Web 2" - AuthDigestProvider file - AuthUserFile /etc/icingaweb2/.icingawebdigest - Require valid-user - -```` - -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. \ No newline at end of file diff --git a/icingaweb2.spec b/icingaweb2.spec index f284df23b..91862e0ee 100644 --- a/icingaweb2.spec +++ b/icingaweb2.spec @@ -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 diff --git a/library/Icinga/Application/Hook/Ticket/TicketPattern.php b/library/Icinga/Application/Hook/Ticket/TicketPattern.php new file mode 100644 index 000000000..8116ca6be --- /dev/null +++ b/library/Icinga/Application/Hook/Ticket/TicketPattern.php @@ -0,0 +1,152 @@ +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); + } +} diff --git a/library/Icinga/Application/Hook/TicketHook.php b/library/Icinga/Application/Hook/TicketHook.php index 72bb308ee..0cf106175 100644 --- a/library/Icinga/Application/Hook/TicketHook.php +++ b/library/Icinga/Application/Hook/TicketHook.php @@ -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,22 +181,28 @@ 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; } - try { - $text = preg_replace_callback( - $pattern, - array($this, 'createLink'), - $text - ); - } catch (ErrorException $e) { - $this->fail('Can\'t create ticket links: Pattern is invalid: %s', IcingaException::describe($e)); - return $text; - } catch (Exception $e) { - $this->fail('Can\'t create ticket links: %s', IcingaException::describe($e)); - return $text; + + if (is_array($pattern)) { + $text = $this->applyTicketPatterns($text, $pattern); + } else { + try { + $text = preg_replace_callback( + $pattern, + array($this, 'createLink'), + $text + ); + } catch (ErrorException $e) { + $this->fail('Can\'t create ticket links: Pattern is invalid: %s', IcingaException::describe($e)); + return $text; + } catch (Exception $e) { + $this->fail('Can\'t create ticket links: %s', IcingaException::describe($e)); + return $text; + } } return $text; diff --git a/library/Icinga/Authentication/AdmissionLoader.php b/library/Icinga/Authentication/AdmissionLoader.php index 391622fb1..0a80be127 100644 --- a/library/Icinga/Authentication/AdmissionLoader.php +++ b/library/Icinga/Authentication/AdmissionLoader.php @@ -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 + * @param User $user */ - 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); } } diff --git a/library/Icinga/Authentication/Auth.php b/library/Icinga/Authentication/Auth.php index 9fb43922c..814f1366e 100644 --- a/library/Icinga/Authentication/Auth.php +++ b/library/Icinga/Authentication/Auth.php @@ -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(); } } diff --git a/library/Icinga/Authentication/Role.php b/library/Icinga/Authentication/Role.php new file mode 100644 index 000000000..f00d063e2 --- /dev/null +++ b/library/Icinga/Authentication/Role.php @@ -0,0 +1,109 @@ +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; + } +} diff --git a/library/Icinga/Authentication/User/ExternalBackend.php b/library/Icinga/Authentication/User/ExternalBackend.php index db3212499..3baf1c8e0 100644 --- a/library/Icinga/Authentication/User/ExternalBackend.php +++ b/library/Icinga/Authentication/User/ExternalBackend.php @@ -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) { diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php index f643d4849..81afb2c3d 100644 --- a/library/Icinga/Data/Db/DbConnection.php +++ b/library/Icinga/Data/Db/DbConnection.php @@ -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; diff --git a/library/Icinga/Data/Tree/TreeNodeIterator.php b/library/Icinga/Data/Tree/TreeNodeIterator.php index dcba0f1e9..839b1a1d0 100644 --- a/library/Icinga/Data/Tree/TreeNodeIterator.php +++ b/library/Icinga/Data/Tree/TreeNodeIterator.php @@ -92,6 +92,6 @@ class TreeNodeIterator implements RecursiveIterator */ public function isEmpty() { - return empty($this->children); + return ! $this->children->count(); } } diff --git a/library/Icinga/Protocol/Ldap/LdapConnection.php b/library/Icinga/Protocol/Ldap/LdapConnection.php index 0729addfc..e741e9678 100644 --- a/library/Icinga/Protocol/Ldap/LdapConnection.php +++ b/library/Icinga/Protocol/Ldap/LdapConnection.php @@ -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, diff --git a/library/Icinga/Protocol/Ldap/LdapQuery.php b/library/Icinga/Protocol/Ldap/LdapQuery.php index 1184765ba..7f31d32a0 100644 --- a/library/Icinga/Protocol/Ldap/LdapQuery.php +++ b/library/Icinga/Protocol/Ldap/LdapQuery.php @@ -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; + } + } diff --git a/library/Icinga/Protocol/Ldap/LdapUtils.php b/library/Icinga/Protocol/Ldap/LdapUtils.php index 68031fa57..3d86cf100 100644 --- a/library/Icinga/Protocol/Ldap/LdapUtils.php +++ b/library/Icinga/Protocol/Ldap/LdapUtils.php @@ -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 ); } diff --git a/library/Icinga/User.php b/library/Icinga/User.php index cbd8c4743..738df109e 100644 --- a/library/Icinga/User.php +++ b/library/Icinga/User.php @@ -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; } /** diff --git a/library/Icinga/Util/DirectoryIterator.php b/library/Icinga/Util/DirectoryIterator.php index 3addbd2bb..60368733e 100644 --- a/library/Icinga/Util/DirectoryIterator.php +++ b/library/Icinga/Util/DirectoryIterator.php @@ -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)) { - $skip = true; + 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); - } - } } diff --git a/library/Icinga/Web/Form/Decorator/Autosubmit.php b/library/Icinga/Web/Form/Decorator/Autosubmit.php index e4392bbd9..f3ce02d53 100644 --- a/library/Icinga/Web/Form/Decorator/Autosubmit.php +++ b/library/Icinga/Web/Form/Decorator/Autosubmit.php @@ -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 = '' . $warning . '' . $content; diff --git a/library/Icinga/Web/LessCompiler.php b/library/Icinga/Web/LessCompiler.php index acf616d18..7ec34a143 100644 --- a/library/Icinga/Web/LessCompiler.php +++ b/library/Icinga/Web/LessCompiler.php @@ -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; } diff --git a/library/Icinga/Web/Widget/Dashboard.php b/library/Icinga/Web/Widget/Dashboard.php index 415a08857..ab47afb3f 100644 --- a/library/Icinga/Web/Widget/Dashboard.php +++ b/library/Icinga/Web/Widget/Dashboard.php @@ -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( diff --git a/library/Icinga/Web/Widget/Dashboard/Pane.php b/library/Icinga/Web/Widget/Dashboard/Pane.php index 587ea3a90..f323a613b 100644 --- a/library/Icinga/Web/Widget/Dashboard/Pane.php +++ b/library/Icinga/Web/Widget/Dashboard/Pane.php @@ -40,7 +40,7 @@ class Pane extends UserWidget * * @var bool */ - private $disabled; + private $disabled = false; /** * Create a new pane diff --git a/library/Icinga/Web/Widget/Tabs.php b/library/Icinga/Web/Widget/Tabs.php index b8c058604..2f8094197 100644 --- a/library/Icinga/Web/Widget/Tabs.php +++ b/library/Icinga/Web/Widget/Tabs.php @@ -377,8 +377,8 @@ EOT; array( $tabs, $drop, - $close, - $refresh + $refresh, + $close ), $this->baseTpl ); diff --git a/library/vendor/Parsedown/Parsedown.php b/library/vendor/Parsedown/Parsedown.php index 71a033e2e..c8c92a392 100644 --- a/library/vendor/Parsedown/Parsedown.php +++ b/library/vendor/Parsedown/Parsedown.php @@ -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; diff --git a/library/vendor/Parsedown/SOURCE b/library/vendor/Parsedown/SOURCE index cbbe887be..38fc9f5bf 100644 --- a/library/vendor/Parsedown/SOURCE +++ b/library/vendor/Parsedown/SOURCE @@ -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 diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php index b6b2d6375..fafd94656 100644 --- a/modules/doc/application/controllers/ModuleController.php +++ b/modules/doc/application/controllers/ModuleController.php @@ -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 * diff --git a/modules/doc/doc/1-module-documentation.md b/modules/doc/doc/1-module-documentation.md index edf20aac2..2334324a9 100644 --- a/modules/doc/doc/1-module-documentation.md +++ b/modules/doc/doc/1-module-documentation.md @@ -1,6 +1,6 @@ # 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. ## 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) diff --git a/modules/doc/library/Doc/DocController.php b/modules/doc/library/Doc/DocController.php index acd085d04..47c96b1a9 100644 --- a/modules/doc/library/Doc/DocController.php +++ b/modules/doc/library/Doc/DocController.php @@ -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); diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php index 6d52665c7..ac98584b6 100644 --- a/modules/doc/library/Doc/DocParser.php +++ b/modules/doc/library/Doc/DocParser.php @@ -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) diff --git a/modules/doc/library/Doc/DocSection.php b/modules/doc/library/Doc/DocSection.php index 3138a70be..ce5297e1d 100644 --- a/modules/doc/library/Doc/DocSection.php +++ b/modules/doc/library/Doc/DocSection.php @@ -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 * diff --git a/modules/doc/library/Doc/Renderer/DocRenderer.php b/modules/doc/library/Doc/Renderer/DocRenderer.php index feb420e91..3662fd574 100644 --- a/modules/doc/library/Doc/Renderer/DocRenderer.php +++ b/modules/doc/library/Doc/Renderer/DocRenderer.php @@ -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 * diff --git a/modules/doc/library/Doc/Renderer/DocSectionRenderer.php b/modules/doc/library/Doc/Renderer/DocSectionRenderer.php index a39d24a77..113f707c5 100644 --- a/modules/doc/library/Doc/Renderer/DocSectionRenderer.php +++ b/modules/doc/library/Doc/Renderer/DocSectionRenderer.php @@ -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( - '#
.+
#ms', + '#
.+?
#ms', array($this, 'markupNotes'), $html ); diff --git a/modules/doc/public/css/module.less b/modules/doc/public/css/module.less index 26dd18fd0..35cfc3d3a 100644 --- a/modules/doc/public/css/module.less +++ b/modules/doc/public/css/module.less @@ -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; diff --git a/modules/doc/run.php b/modules/doc/run.php index 9d49b89d4..df9dd0930 100644 --- a/modules/doc/run.php +++ b/modules/doc/run.php @@ -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); diff --git a/modules/monitoring/application/clicommands/ListCommand.php b/modules/monitoring/application/clicommands/ListCommand.php index 26f50f592..7e875e37b 100644 --- a/modules/monitoring/application/clicommands/ListCommand.php +++ b/modules/monitoring/application/clicommands/ListCommand.php @@ -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()); diff --git a/modules/monitoring/application/controllers/HostsController.php b/modules/monitoring/application/controllers/HostsController.php index 12017657c..17969df8d 100644 --- a/modules/monitoring/application/controllers/HostsController.php +++ b/modules/monitoring/application/controllers/HostsController.php @@ -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( diff --git a/modules/monitoring/application/controllers/ListController.php b/modules/monitoring/application/controllers/ListController.php index 35ce34797..8e493ed44 100644 --- a/modules/monitoring/application/controllers/ListController.php +++ b/modules/monitoring/application/controllers/ListController.php @@ -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_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() diff --git a/modules/monitoring/application/controllers/ServicesController.php b/modules/monitoring/application/controllers/ServicesController.php index 53312ae48..17e82124b 100644 --- a/modules/monitoring/application/controllers/ServicesController.php +++ b/modules/monitoring/application/controllers/ServicesController.php @@ -41,6 +41,7 @@ class ServicesController extends Controller 'host_name', 'host_problem', 'host_state', + 'instance_name', 'service_acknowledged', 'service_active_checks_enabled', 'service_description', diff --git a/modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php b/modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php index ed50466fe..dbd786d5e 100644 --- a/modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php +++ b/modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php @@ -58,24 +58,27 @@ class ToggleInstanceFeaturesCommandForm extends CommandForm */ public function createElements(array $formData = array()) { - if ((bool) $this->status->notifications_enabled) { - if ($this->hasPermission('monitoring/command/feature/instance')) { + $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( + '%3$s', + $this->translate('Disable notifications for a specific time on a program-wide basis'), + $this->getView()->href('monitoring/health/disable-notifications'), + $this->translate('Disable temporarily') + ); + } else { + $notificationDescription = null; + } + } elseif ($this->status->disable_notif_expire_time) { $notificationDescription = sprintf( - '%3$s', - $this->translate('Disable notifications for a specific time on a program-wide basis'), - $this->getView()->href('monitoring/health/disable-notifications'), - $this->translate('Disable temporarily') + $this->translate('Notifications will be re-enabled in %s'), + $this->getView()->timeUntil($this->status->disable_notif_expire_time) ); - } else { - $notificationDescription = null; } - } elseif ($this->status->disable_notif_expire_time) { - $notificationDescription = sprintf( - $this->translate('Notifications will be re-enabled in %s'), - $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, diff --git a/modules/monitoring/application/views/helpers/IconImage.php b/modules/monitoring/application/views/helpers/IconImage.php index 3fee8e3a7..8ae72d83e 100644 --- a/modules/monitoring/application/views/helpers/IconImage.php +++ b/modules/monitoring/application/views/helpers/IconImage.php @@ -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( diff --git a/modules/monitoring/application/views/helpers/PluginOutput.php b/modules/monitoring/application/views/helpers/PluginOutput.php index f5c73ee21..ba3013a3a 100644 --- a/modules/monitoring/application/views/helpers/PluginOutput.php +++ b/modules/monitoring/application/views/helpers/PluginOutput.php @@ -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​$3', $output); + // Add zero width space before '(' and '[' if not surrounded by whitespaces + $output = preg_replace('/([^\s])([([])([^\s])/', '$1​$2$3', $output); if (! $raw) { if ($isHtml) { diff --git a/modules/monitoring/application/views/scripts/list/comments.phtml b/modules/monitoring/application/views/scripts/list/comments.phtml index 0801a7e4d..4941922a9 100644 --- a/modules/monitoring/application/views/scripts/list/comments.phtml +++ b/modules/monitoring/application/views/scripts/list/comments.phtml @@ -1,11 +1,11 @@ compact): ?>
- + tabs ?> render('list/components/selectioninfo.phtml') ?> -
- sortBox ?> + paginator ?> +
limiter ?> - paginator ?> + sortBox ?>
filterEditor ?>
diff --git a/modules/monitoring/application/views/scripts/list/contactgroups.phtml b/modules/monitoring/application/views/scripts/list/contactgroups.phtml index eea6302b1..c0656471a 100644 --- a/modules/monitoring/application/views/scripts/list/contactgroups.phtml +++ b/modules/monitoring/application/views/scripts/list/contactgroups.phtml @@ -2,52 +2,49 @@ if (! $this->compact): ?>
- -
- sortBox ?> + tabs ?> + paginator ?> +
limiter ?> - paginator ?> + sortBox ?>
filterEditor ?>
-translate('No contactgroups found matching the filter') . '
'; - return; -} -?> -
+hasResult()): ?> +

translate('No contact groups found matching the filter') ?>

+ + +
- - - - - + + + + + - - $groupInfo): ?> - - + + + + - diff --git a/modules/monitoring/application/views/scripts/list/contacts.phtml b/modules/monitoring/application/views/scripts/list/contacts.phtml index aca29d88e..f712ada8a 100644 --- a/modules/monitoring/application/views/scripts/list/contacts.phtml +++ b/modules/monitoring/application/views/scripts/list/contacts.phtml @@ -1,79 +1,83 @@ compact): ?>
- -
- sortBox ?> + tabs ?> + paginator ?> +
limiter ?> - paginator ?> + sortBox ?>
filterEditor ?>
-hasResult()): ?> -
translate('Contact Group ') ?>translate('Alias') ?>
translate('Contact Group ') ?>translate('Alias') ?>
- -
+ contact_count ?> + + qlink( + $contactGroup->contactgroup_name, + 'monitoring/list/contacts', + array('contactgroup_name' => $contactGroup->contactgroup_name), + array('title' => sprintf( + $this->translate('Show detailed information about %s'), + $contactGroup->contactgroup_name + )) + ) ?> + - qlink( - $groupName, - 'monitoring/list/contacts', - array('contactgroup_name' => $groupName), - array('title' => sprintf( - $this->translate('Show detailed information about %s'), - $groupName - )) - ) ?> - - - - + contactgroup_name !== $contactGroup->contactgroup_alias): ?> + contactgroup_alias ?> +
- +hasResult()): ?> +

translate('No contacts found matching the filter') ?>

+ + +
+ - - - peekAhead($this->compact) as $contact): ?> - - + peekAhead($this->compact) as $contact): ?> + + + ) + ) + ) ?> + + - - contact_notify_service_timeperiod): ?> - - + contact_notify_service_timeperiod): ?> + + - contact_notify_host_timeperiod): ?> - - - - - + contact_notify_host_timeperiod): ?> + + + + +
translate('Name') ?> translate('Email') ?> translate('Pager') ?>
- qlink( - $contact->contact_name, - 'monitoring/show/contact', - array('contact_name' => $contact->contact_name), - array('title' => sprintf( + +
+ qlink( + $contact->contact_name, + 'monitoring/show/contact', + array('contact_name' => $contact->contact_name), + array( + 'title' => sprintf( $this->translate('Show detailed information about %s'), $contact->contact_alias - ), 'class' => 'rowaction') - ); ?> - + translate('Email') ?>: + + escape($contact->contact_email) ?> + + - translate('Email') ?>: - - escape($contact->contact_email); ?> - + contact_pager): ?> + escape($contact->contact_pager) ?> + - contact_pager): ?> - escape($contact->contact_pager) ?> - - - escape($contact->contact_notify_service_timeperiod) ?> - + escape($contact->contact_notify_service_timeperiod) ?> + - escape($contact->contact_notify_host_timeperiod) ?> -
+ escape($contact->contact_notify_host_timeperiod) ?> +
- hasMore()): ?> +hasMore()): ?> +
diff --git a/modules/monitoring/application/views/scripts/list/downtimes.phtml b/modules/monitoring/application/views/scripts/list/downtimes.phtml index 1033fdfe8..cefddfa53 100644 --- a/modules/monitoring/application/views/scripts/list/downtimes.phtml +++ b/modules/monitoring/application/views/scripts/list/downtimes.phtml @@ -3,51 +3,56 @@ use Icinga\Module\Monitoring\Object\Host; use Icinga\Module\Monitoring\Object\Service; if (! $this->compact): ?> -
- +
+ tabs ?> render('list/components/selectioninfo.phtml') ?> -
- sortBox ?> + paginator ?> +
limiter ?> - paginator ?> + sortBox ?>
filterEditor ?>
- " - data-icinga-multiselect-data="downtime_id"> +hasResult()): ?> +

translate('No downtimes found matching the filter.') ?>

+ + +
" + data-icinga-multiselect-data="downtime_id"> peekAhead($this->compact) as $downtime): - if (isset($downtime->service_description)) { - $this->isService = true; - $this->stateName = Service::getStateText($downtime->service_state); - } else { - $this->isService = false; - $this->stateName = Host::getStateText($downtime->host_state); - } - $this->downtime = $downtime; - ?> - - render('partials/downtime/downtime-header.phtml'); ?> - + if (isset($downtime->service_description)) { + $this->isService = true; + $this->stateName = Service::getStateText($downtime->service_state); + } else { + $this->isService = false; + $this->stateName = Host::getStateText($downtime->host_state); + } + // Set downtime for partials + $this->downtime = $downtime; + ?> + + render('partials/downtime/downtime-header.phtml') ?> + -
-hasResult()): ?> - translate('No downtimes found matching the filter, maybe the downtime already expired.'); ?> -hasMore()): ?> - qlink( - $this->translate('Show More'), - $this->url()->without(array('view', 'limit')), - null, - array( - 'data-base-target' => '_next', - 'class' => 'pull-right action-link' - ) - ); ?> + +hasMore()): ?> +
diff --git a/modules/monitoring/application/views/scripts/list/eventgrid.phtml b/modules/monitoring/application/views/scripts/list/eventgrid.phtml index 37024b841..aafa9b6b1 100644 --- a/modules/monitoring/application/views/scripts/list/eventgrid.phtml +++ b/modules/monitoring/application/views/scripts/list/eventgrid.phtml @@ -4,12 +4,8 @@ use Icinga\Web\Widget\Chart\HistoryColorGrid; if (! $this->compact): ?>
- tabs; ?> - sortBox; ?> - limiter; ?> - paginator; ?> - filterEditor; ?> - + tabs ?> + form ?>
diff --git a/modules/monitoring/application/views/scripts/list/eventhistory.phtml b/modules/monitoring/application/views/scripts/list/eventhistory.phtml index 32bb3d128..2ee788d60 100644 --- a/modules/monitoring/application/views/scripts/list/eventhistory.phtml +++ b/modules/monitoring/application/views/scripts/list/eventhistory.phtml @@ -3,11 +3,11 @@ if (! $this->compact): ?>
tabs ?> -
- sortBox ?> +
limiter ?> - filterEditor ?> + sortBox ?>
+ filterEditor ?>
partial( diff --git a/modules/monitoring/application/views/scripts/list/hostgroups.phtml b/modules/monitoring/application/views/scripts/list/hostgroups.phtml index c39a80d33..2fbd9d4c5 100644 --- a/modules/monitoring/application/views/scripts/list/hostgroups.phtml +++ b/modules/monitoring/application/views/scripts/list/hostgroups.phtml @@ -1,278 +1,280 @@ compact): ?>
- -
- sortBox ?> + tabs ?> + paginator ?> +
limiter ?> - paginator ?> + sortBox ?>
filterEditor ?>
-hasResult()): ?> +hasResult()): ?>

translate('No host groups found matching the filter.') ?>

- - - - - - - - - - - -peekAhead($this->compact) as $hostgroup): ?> - - - - - - + + + +
translate('Host Group') ?>translate('Host States') ?>translate('Service States') ?>
- hosts_total ?> - - qlink( - $hostgroup->hostgroup_alias, - 'monitoring/list/hosts', - array('hostgroup_name' => $hostgroup->hostgroup_name), - array('title' => sprintf( - $this->translate('List all hosts in the group "%s"'), - $hostgroup->hostgroup_alias - )) - ) ?> - - setUrl('monitoring/list/hosts') - ->setBaseFilter($this->filterEditor->getFilter()) - ->add( - StateBadges::STATE_UP, - $hostgroup->hosts_up, - array( - 'host_state' => 0, - '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) - ) - ->add( - StateBadges::STATE_DOWN, - $hostgroup->hosts_down_unhandled, - array( - 'host_state' => 1, - 'host_acknowledged' => 0, - 'host_in_downtime' => 0, - '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) - ) - ->add( - StateBadges::STATE_DOWN_HANDLED, - $hostgroup->hosts_down_handled, - array( - 'host_state' => 1, - 'host_handled' => 1, - '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) - ) - ->add( - StateBadges::STATE_UNREACHABLE, - $hostgroup->hosts_unreachable_unhandled, - array( - 'host_state' => 2, - 'host_acknowledged' => 0, - 'host_in_downtime' => 0, - '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) - ) - ->add( - StateBadges::STATE_UNREACHABLE_HANDLED, - $hostgroup->hosts_unreachable_handled, - array( - 'host_state' => 2, - 'host_handled' => 1, - '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) - ) - ->add( - StateBadges::STATE_PENDING, - $hostgroup->hosts_pending, - array( - 'host_state' => 99, - '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) - ); - echo $stateBadges->render(); - ?> - - qlink( - $hostgroup->services_total, - 'monitoring/list/services', - array('hostgroup_name' => $hostgroup->hostgroup_name), - array('title' => sprintf( - $this->translate('List all services of all hosts in host group "%s"'), - $hostgroup->hostgroup_alias - ), 'class' => 'badge') - ) ?> - - setUrl('monitoring/list/services') - ->add( - StateBadges::STATE_OK, - $hostgroup->services_ok, - array( - 'service_state' => 0, - '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) - ) - ->add( - StateBadges::STATE_CRITICAL, - $hostgroup->services_critical_unhandled, - array( - 'service_state' => 2, - 'service_acknowledged' => 0, - 'service_in_downtime' => 0, - 'host_problem' => 0, - '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) - ) - ->add( - StateBadges::STATE_CRITICAL_HANDLED, - $hostgroup->services_critical_handled, - array( - 'service_state' => 2, - 'service_handled' => 1, - '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) - ) - ->add( - StateBadges::STATE_UNKNOWN, - $hostgroup->services_unknown_unhandled, - array( - 'service_state' => 3, - 'service_acknowledged' => 0, - 'service_in_downtime' => 0, - 'host_problem' => 0, - '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) - ) - ->add( - StateBadges::STATE_UNKNOWN_HANDLED, - $hostgroup->services_unknown_handled, - array( - 'service_state' => 3, - 'service_handled' => 1, - '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) + + + + + + + + + + + + peekAhead($this->compact) as $hostGroup): ?> + + + + + + - - - -
translate('Host Group') ?>translate('Host States') ?>translate('Service States') ?>
+ hosts_total ?> + + qlink( + $hostGroup->hostgroup_alias, + 'monitoring/list/hosts', + array('hostgroup_name' => $hostGroup->hostgroup_name), + array('title' => sprintf( + $this->translate('List all hosts in the group "%s"'), + $hostGroup->hostgroup_alias + )) + ) ?> + + setUrl('monitoring/list/hosts') + ->setBaseFilter($this->filterEditor->getFilter()) + ->add( + StateBadges::STATE_UP, + $hostGroup->hosts_up, + array( + 'host_state' => 0, + '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) + ) + ->add( + StateBadges::STATE_DOWN, + $hostGroup->hosts_down_unhandled, + array( + 'host_state' => 1, + 'host_acknowledged' => 0, + 'host_in_downtime' => 0, + '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) + ) + ->add( + StateBadges::STATE_DOWN_HANDLED, + $hostGroup->hosts_down_handled, + array( + 'host_state' => 1, + 'host_handled' => 1, + '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) + ) + ->add( + StateBadges::STATE_UNREACHABLE, + $hostGroup->hosts_unreachable_unhandled, + array( + 'host_state' => 2, + 'host_acknowledged' => 0, + 'host_in_downtime' => 0, + '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) + ) + ->add( + StateBadges::STATE_UNREACHABLE_HANDLED, + $hostGroup->hosts_unreachable_handled, + array( + 'host_state' => 2, + 'host_handled' => 1, + '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) + ) + ->add( + StateBadges::STATE_PENDING, + $hostGroup->hosts_pending, + array( + 'host_state' => 99, + '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) + ); + echo $stateBadges->render(); + ?> + + qlink( + $hostGroup->services_total, + 'monitoring/list/services', + array('hostgroup_name' => $hostGroup->hostgroup_name), + array('title' => sprintf( + $this->translate('List all services of all hosts in host group "%s"'), + $hostGroup->hostgroup_alias + ), 'class' => 'badge') + ) ?> + + setUrl('monitoring/list/services') + ->add( + StateBadges::STATE_OK, + $hostGroup->services_ok, + array( + 'service_state' => 0, + '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) + ) + ->add( + StateBadges::STATE_CRITICAL, + $hostGroup->services_critical_unhandled, + array( + 'service_state' => 2, + 'service_acknowledged' => 0, + 'service_in_downtime' => 0, + 'host_problem' => 0, + '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) + ) + ->add( + StateBadges::STATE_CRITICAL_HANDLED, + $hostGroup->services_critical_handled, + array( + 'service_state' => 2, + 'service_handled' => 1, + '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) + ) + ->add( + StateBadges::STATE_UNKNOWN, + $hostGroup->services_unknown_unhandled, + array( + 'service_state' => 3, + 'service_acknowledged' => 0, + 'service_in_downtime' => 0, + 'host_problem' => 0, + '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) + ) + ->add( + StateBadges::STATE_UNKNOWN_HANDLED, + $hostGroup->services_unknown_handled, + array( + 'service_state' => 3, + 'service_handled' => 1, + '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) - ) - ->add( - StateBadges::STATE_WARNING, - $hostgroup->services_warning_unhandled, - array( - 'service_state' => 1, - 'service_acknowledged' => 0, - 'service_in_downtime' => 0, - 'host_problem' => 0, - '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) - ) - ->add( - StateBadges::STATE_WARNING_HANDLED, - $hostgroup->services_warning_handled, - array( - 'service_state' => 1, - 'service_handled' => 1, - '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) - ) - ->add( - StateBadges::STATE_PENDING, - $hostgroup->services_pending, - array( - 'service_state' => 99, - '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) - ); - echo $stateBadges->render(); - ?> -
-hasMore()): ?> -qlink( - $this->translate('Show More'), - $this->url()->without(array('view', 'limit')), - null, - array( - 'data-base-target' => '_next', - 'class' => 'pull-right action-link' - ) -) ?> + ) + ->add( + StateBadges::STATE_WARNING, + $hostGroup->services_warning_unhandled, + array( + 'service_state' => 1, + 'service_acknowledged' => 0, + 'service_in_downtime' => 0, + 'host_problem' => 0, + '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) + ) + ->add( + StateBadges::STATE_WARNING_HANDLED, + $hostGroup->services_warning_handled, + array( + 'service_state' => 1, + 'service_handled' => 1, + '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) + ) + ->add( + StateBadges::STATE_PENDING, + $hostGroup->services_pending, + array( + 'service_state' => 99, + '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) + ); + echo $stateBadges->render(); + ?> +
+hasMore()): ?> +
diff --git a/modules/monitoring/application/views/scripts/list/hosts.phtml b/modules/monitoring/application/views/scripts/list/hosts.phtml index f5ff2569a..6de773435 100644 --- a/modules/monitoring/application/views/scripts/list/hosts.phtml +++ b/modules/monitoring/application/views/scripts/list/hosts.phtml @@ -4,14 +4,12 @@ use Icinga\Module\Monitoring\Object\Host; if (! $this->compact): ?>
tabs ?> -
- render('list/components/hostssummary.phtml') ?> - render('list/components/selectioninfo.phtml') ?> -
-
- sortBox ?> + render('list/components/hostssummary.phtml') ?> + render('list/components/selectioninfo.phtml') ?> + paginator ?> +
limiter ?> - paginator ?> + sortBox ?>
filterEditor ?>
@@ -19,7 +17,7 @@ if (! $this->compact): ?>
hasResult()): ?>

translate('No hosts found matching the filter.') ?>

-
+
compact): ?>
hasMore()): ?> -
+ diff --git a/modules/monitoring/application/views/scripts/list/notifications.phtml b/modules/monitoring/application/views/scripts/list/notifications.phtml index 6428055cc..8316665d1 100644 --- a/modules/monitoring/application/views/scripts/list/notifications.phtml +++ b/modules/monitoring/application/views/scripts/list/notifications.phtml @@ -4,11 +4,11 @@ use Icinga\Module\Monitoring\Object\Service; if (! $this->compact): ?>
- -
- sortBox ?> + tabs ?> + paginator ?> +
limiter ?> - paginator ?> + sortBox ?>
filterEditor ?>
@@ -77,7 +77,7 @@ if (! $this->compact): ?> hasMore()): ?> -
+