summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:46:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:46:43 +0000
commit3e02d5aff85babc3ffbfcf52313f2108e313aa23 (patch)
treeb01f3923360c20a6a504aff42d45670c58af3ec5
parentInitial commit. (diff)
downloadicingaweb2-3e02d5aff85babc3ffbfcf52313f2108e313aa23.tar.xz
icingaweb2-3e02d5aff85babc3ffbfcf52313f2108e313aa23.zip
Adding upstream version 2.12.1.upstream/2.12.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.mailmap43
-rw-r--r--AUTHORS145
-rw-r--r--CHANGELOG.md1583
-rw-r--r--LICENSE339
-rw-r--r--README.md56
-rw-r--r--SECURITY.md9
-rw-r--r--VERSION1
-rw-r--r--application/VERSION1
-rw-r--r--application/clicommands/AutocompleteCommand.php120
-rw-r--r--application/clicommands/HelpCommand.php43
-rw-r--r--application/clicommands/ModuleCommand.php228
-rw-r--r--application/clicommands/VersionCommand.php55
-rw-r--r--application/clicommands/WebCommand.php101
-rw-r--r--application/controllers/AboutController.php27
-rw-r--r--application/controllers/AccountController.php83
-rw-r--r--application/controllers/AnnouncementsController.php123
-rw-r--r--application/controllers/ApplicationStateController.php95
-rw-r--r--application/controllers/AuthenticationController.php127
-rw-r--r--application/controllers/ConfigController.php518
-rw-r--r--application/controllers/DashboardController.php346
-rw-r--r--application/controllers/ErrorController.php176
-rw-r--r--application/controllers/GroupController.php418
-rw-r--r--application/controllers/HealthController.php65
-rw-r--r--application/controllers/IframeController.php20
-rw-r--r--application/controllers/IndexController.php36
-rw-r--r--application/controllers/LayoutController.php28
-rw-r--r--application/controllers/ListController.php59
-rw-r--r--application/controllers/ManageUserDevicesController.php84
-rw-r--r--application/controllers/MigrationsController.php249
-rw-r--r--application/controllers/MyDevicesController.php74
-rw-r--r--application/controllers/NavigationController.php447
-rw-r--r--application/controllers/RoleController.php392
-rw-r--r--application/controllers/SearchController.php28
-rw-r--r--application/controllers/StaticController.php78
-rw-r--r--application/controllers/UserController.php374
-rw-r--r--application/controllers/UsergroupbackendController.php133
-rw-r--r--application/fonts/fontello-ifont/LICENSE.txt57
-rw-r--r--application/fonts/fontello-ifont/README.txt75
-rw-r--r--application/fonts/fontello-ifont/config.json874
-rw-r--r--application/fonts/fontello-ifont/css/animation.css85
-rw-r--r--application/fonts/fontello-ifont/css/ifont-codes.css145
-rw-r--r--application/fonts/fontello-ifont/css/ifont-embedded.css198
-rw-r--r--application/fonts/fontello-ifont/css/ifont-ie7-codes.css145
-rw-r--r--application/fonts/fontello-ifont/css/ifont-ie7.css156
-rw-r--r--application/fonts/fontello-ifont/css/ifont.css201
-rw-r--r--application/fonts/fontello-ifont/demo.html519
-rw-r--r--application/fonts/fontello-ifont/font/ifont.eotbin0 -> 46504 bytes
-rw-r--r--application/fonts/fontello-ifont/font/ifont.svg298
-rw-r--r--application/fonts/fontello-ifont/font/ifont.ttfbin0 -> 46348 bytes
-rw-r--r--application/fonts/fontello-ifont/font/ifont.woffbin0 -> 27688 bytes
-rw-r--r--application/fonts/fontello-ifont/font/ifont.woff2bin0 -> 22984 bytes
-rw-r--r--application/fonts/icingaweb.md9
-rw-r--r--application/forms/Account/ChangePasswordForm.php123
-rw-r--r--application/forms/AcknowledgeApplicationStateMessageForm.php75
-rw-r--r--application/forms/ActionForm.php78
-rw-r--r--application/forms/Announcement/AcknowledgeAnnouncementForm.php92
-rw-r--r--application/forms/Announcement/AnnouncementForm.php135
-rw-r--r--application/forms/Authentication/LoginForm.php214
-rw-r--r--application/forms/AutoRefreshForm.php83
-rw-r--r--application/forms/Config/General/ApplicationConfigForm.php105
-rw-r--r--application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php46
-rw-r--r--application/forms/Config/General/LoggingConfigForm.php142
-rw-r--r--application/forms/Config/General/ThemingConfigForm.php78
-rw-r--r--application/forms/Config/GeneralConfigForm.php40
-rw-r--r--application/forms/Config/Resource/DbResourceForm.php239
-rw-r--r--application/forms/Config/Resource/FileResourceForm.php67
-rw-r--r--application/forms/Config/Resource/LdapResourceForm.php129
-rw-r--r--application/forms/Config/Resource/SshResourceForm.php148
-rw-r--r--application/forms/Config/ResourceConfigForm.php442
-rw-r--r--application/forms/Config/User/CreateMembershipForm.php192
-rw-r--r--application/forms/Config/User/UserForm.php210
-rw-r--r--application/forms/Config/UserBackend/DbBackendForm.php82
-rw-r--r--application/forms/Config/UserBackend/ExternalBackendForm.php83
-rw-r--r--application/forms/Config/UserBackend/LdapBackendForm.php414
-rw-r--r--application/forms/Config/UserBackendConfigForm.php482
-rw-r--r--application/forms/Config/UserBackendReorderForm.php86
-rw-r--r--application/forms/Config/UserGroup/AddMemberForm.php183
-rw-r--r--application/forms/Config/UserGroup/DbUserGroupBackendForm.php79
-rw-r--r--application/forms/Config/UserGroup/LdapUserGroupBackendForm.php370
-rw-r--r--application/forms/Config/UserGroup/UserGroupBackendForm.php314
-rw-r--r--application/forms/Config/UserGroup/UserGroupForm.php158
-rw-r--r--application/forms/ConfigForm.php192
-rw-r--r--application/forms/ConfirmRemovalForm.php38
-rw-r--r--application/forms/Control/LimiterControlForm.php134
-rw-r--r--application/forms/Dashboard/DashletForm.php171
-rw-r--r--application/forms/LdapDiscoveryForm.php34
-rw-r--r--application/forms/MigrationForm.php143
-rw-r--r--application/forms/Navigation/DashletForm.php35
-rw-r--r--application/forms/Navigation/MenuItemForm.php31
-rw-r--r--application/forms/Navigation/NavigationConfigForm.php853
-rw-r--r--application/forms/Navigation/NavigationItemForm.php114
-rw-r--r--application/forms/PreferenceForm.php485
-rw-r--r--application/forms/RepositoryForm.php453
-rw-r--r--application/forms/Security/RoleForm.php632
-rw-r--r--application/layouts/scripts/body.phtml98
-rw-r--r--application/layouts/scripts/external-logout.phtml34
-rw-r--r--application/layouts/scripts/guest-error.phtml10
-rw-r--r--application/layouts/scripts/inline.phtml2
-rw-r--r--application/layouts/scripts/layout.phtml83
-rw-r--r--application/layouts/scripts/parts/navigation.phtml35
-rw-r--r--application/layouts/scripts/pdf.phtml44
-rw-r--r--application/views/helpers/CreateTicketLinks.php23
-rw-r--r--application/views/helpers/FormDate.php46
-rw-r--r--application/views/helpers/FormDateTime.php63
-rw-r--r--application/views/helpers/FormNumber.php77
-rw-r--r--application/views/helpers/FormTime.php46
-rw-r--r--application/views/helpers/ProtectId.php13
-rw-r--r--application/views/helpers/Util.php68
-rw-r--r--application/views/scripts/about/index.phtml199
-rw-r--r--application/views/scripts/account/index.phtml11
-rw-r--r--application/views/scripts/announcements/index.phtml71
-rw-r--r--application/views/scripts/authentication/login.phtml74
-rw-r--r--application/views/scripts/authentication/logout.phtml64
-rw-r--r--application/views/scripts/config/devtools.phtml6
-rw-r--r--application/views/scripts/config/general.phtml6
-rw-r--r--application/views/scripts/config/module-configuration-error.phtml28
-rw-r--r--application/views/scripts/config/module.phtml136
-rw-r--r--application/views/scripts/config/modules.phtml42
-rw-r--r--application/views/scripts/config/resource.phtml73
-rw-r--r--application/views/scripts/config/resource/create.phtml6
-rw-r--r--application/views/scripts/config/resource/modify.phtml6
-rw-r--r--application/views/scripts/config/resource/remove.phtml6
-rw-r--r--application/views/scripts/config/userbackend/reorder.phtml75
-rw-r--r--application/views/scripts/dashboard/error.phtml13
-rw-r--r--application/views/scripts/dashboard/index.phtml26
-rw-r--r--application/views/scripts/dashboard/new-dashlet.phtml6
-rw-r--r--application/views/scripts/dashboard/remove-dashlet.phtml6
-rw-r--r--application/views/scripts/dashboard/remove-pane.phtml6
-rw-r--r--application/views/scripts/dashboard/rename-pane.phtml6
-rw-r--r--application/views/scripts/dashboard/settings.phtml91
-rw-r--r--application/views/scripts/dashboard/update-dashlet.phtml6
-rw-r--r--application/views/scripts/error/error.phtml106
-rw-r--r--application/views/scripts/filter/index.phtml11
-rw-r--r--application/views/scripts/form/reorder-authbackend.phtml83
-rw-r--r--application/views/scripts/group/form.phtml6
-rw-r--r--application/views/scripts/group/list.phtml96
-rw-r--r--application/views/scripts/group/show.phtml108
-rw-r--r--application/views/scripts/iframe/index.phtml8
-rw-r--r--application/views/scripts/index/welcome.phtml2
-rw-r--r--application/views/scripts/inline.phtml2
-rw-r--r--application/views/scripts/joystickPagination.phtml162
-rw-r--r--application/views/scripts/layout/announcements.phtml1
-rw-r--r--application/views/scripts/layout/menu.phtml20
-rw-r--r--application/views/scripts/list/applicationlog.phtml29
-rw-r--r--application/views/scripts/mixedPagination.phtml79
-rw-r--r--application/views/scripts/navigation/dashboard.phtml27
-rw-r--r--application/views/scripts/navigation/index.phtml78
-rw-r--r--application/views/scripts/navigation/shared.phtml68
-rw-r--r--application/views/scripts/pivottablePagination.phtml48
-rw-r--r--application/views/scripts/role/list.phtml65
-rw-r--r--application/views/scripts/search/hint.phtml8
-rw-r--r--application/views/scripts/search/index.phtml7
-rw-r--r--application/views/scripts/showConfiguration.phtml27
-rw-r--r--application/views/scripts/simple-form.phtml6
-rw-r--r--application/views/scripts/user/form.phtml6
-rw-r--r--application/views/scripts/user/list.phtml90
-rw-r--r--application/views/scripts/user/show.phtml138
-rwxr-xr-xbin/icingacli7
-rw-r--r--doc/01-About.md93
-rw-r--r--doc/02-Installation.md537
-rw-r--r--doc/02-Installation.md.d/01-Debian.md3
-rw-r--r--doc/02-Installation.md.d/02-Ubuntu.md3
-rw-r--r--doc/02-Installation.md.d/03-CentOS.md3
-rw-r--r--doc/02-Installation.md.d/04-RHEL.md3
-rw-r--r--doc/02-Installation.md.d/05-SLES.md3
-rw-r--r--doc/02-Installation.md.d/06-Amazon-Linux.md3
-rw-r--r--doc/02-Installation.md.d/07-From-Source.md3
-rw-r--r--doc/03-Configuration.md88
-rw-r--r--doc/04-Resources.md136
-rw-r--r--doc/05-Authentication.md293
-rw-r--r--doc/06-Security.md242
-rw-r--r--doc/07-Preferences.md21
-rw-r--r--doc/08-Modules.md69
-rw-r--r--doc/15-Auditing.md14
-rw-r--r--doc/20-Advanced-Topics.md440
-rw-r--r--doc/60-Hooks.md49
-rw-r--r--doc/70-Troubleshooting.md17
-rw-r--r--doc/80-Upgrading.md435
-rw-r--r--doc/90-SELinux.md76
-rw-r--r--doc/accessibility/ifont-mute.html21
-rw-r--r--doc/accessibility/ifont.html18
-rw-r--r--doc/accessibility/link-labels.html15
-rw-r--r--doc/accessibility/required-form-elements.html19
-rw-r--r--doc/accessibility/skip-content.html179
-rw-r--r--doc/accessibility/svg.html19
-rw-r--r--doc/accessibility/text-cue-for-required-form-control-labels.html36
-rw-r--r--doc/phpdoc.xml26
-rw-r--r--doc/res/GraphExample#1.pngbin0 -> 25851 bytes
-rw-r--r--doc/res/GraphExample#2.pngbin0 -> 23514 bytes
-rw-r--r--doc/res/GraphExample#3.pngbin0 -> 19380 bytes
-rw-r--r--doc/res/GraphExample#4.pngbin0 -> 35522 bytes
-rw-r--r--doc/res/GraphExample#5.pngbin0 -> 31663 bytes
-rw-r--r--doc/res/GraphExample#6.pngbin0 -> 59658 bytes
-rw-r--r--doc/res/GraphExample#7.1.pngbin0 -> 7438 bytes
-rw-r--r--doc/res/GraphExample#7.pngbin0 -> 34631 bytes
-rw-r--r--doc/res/GraphExample#8.pngbin0 -> 24815 bytes
-rw-r--r--doc/res/GraphExample#9.pngbin0 -> 37848 bytes
-rw-r--r--doc/res/gitlab-job-artifacts.pngbin0 -> 9523 bytes
-rw-r--r--doc/res/gitlab-rpm-package-pipeline-jobs.pngbin0 -> 52427 bytes
-rw-r--r--doc/res/monitoring-module-preview.pngbin0 -> 305002 bytes
-rw-r--r--etc/bash_completion.d/icingacli10
-rw-r--r--icingaweb2.ruleset.xml52
-rw-r--r--library/Icinga/Application/ApplicationBootstrap.php747
-rw-r--r--library/Icinga/Application/Benchmark.php300
-rw-r--r--library/Icinga/Application/ClassLoader.php306
-rw-r--r--library/Icinga/Application/Cli.php211
-rw-r--r--library/Icinga/Application/Config.php498
-rw-r--r--library/Icinga/Application/EmbeddedWeb.php115
-rw-r--r--library/Icinga/Application/Hook.php328
-rw-r--r--library/Icinga/Application/Hook/ApplicationStateHook.php90
-rw-r--r--library/Icinga/Application/Hook/AuditHook.php123
-rw-r--r--library/Icinga/Application/Hook/AuthenticationHook.php75
-rw-r--r--library/Icinga/Application/Hook/Common/DbMigrationStep.php129
-rw-r--r--library/Icinga/Application/Hook/ConfigFormEventsHook.php137
-rw-r--r--library/Icinga/Application/Hook/DbMigrationHook.php421
-rw-r--r--library/Icinga/Application/Hook/GrapherHook.php111
-rw-r--r--library/Icinga/Application/Hook/HealthHook.php222
-rw-r--r--library/Icinga/Application/Hook/PdfexportHook.php25
-rw-r--r--library/Icinga/Application/Hook/ThemeLoaderHook.php22
-rw-r--r--library/Icinga/Application/Hook/Ticket/TicketPattern.php140
-rw-r--r--library/Icinga/Application/Hook/TicketHook.php210
-rw-r--r--library/Icinga/Application/Hook/WebBaseHook.php54
-rw-r--r--library/Icinga/Application/Icinga.php49
-rw-r--r--library/Icinga/Application/LegacyWeb.php33
-rw-r--r--library/Icinga/Application/Libraries.php91
-rw-r--r--library/Icinga/Application/Libraries/Library.php259
-rw-r--r--library/Icinga/Application/Logger.php349
-rw-r--r--library/Icinga/Application/Logger/LogWriter.php30
-rw-r--r--library/Icinga/Application/Logger/Writer/FileWriter.php80
-rw-r--r--library/Icinga/Application/Logger/Writer/PhpWriter.php39
-rw-r--r--library/Icinga/Application/Logger/Writer/StderrWriter.php62
-rw-r--r--library/Icinga/Application/Logger/Writer/StdoutWriter.php13
-rw-r--r--library/Icinga/Application/Logger/Writer/SyslogWriter.php90
-rw-r--r--library/Icinga/Application/MigrationManager.php417
-rw-r--r--library/Icinga/Application/Modules/DashboardContainer.php58
-rw-r--r--library/Icinga/Application/Modules/Manager.php698
-rw-r--r--library/Icinga/Application/Modules/MenuItemContainer.php55
-rw-r--r--library/Icinga/Application/Modules/Module.php1451
-rw-r--r--library/Icinga/Application/Modules/NavigationItemContainer.php117
-rw-r--r--library/Icinga/Application/Platform.php435
-rw-r--r--library/Icinga/Application/ProvidedHook/DbMigration.php83
-rw-r--r--library/Icinga/Application/StaticWeb.php21
-rw-r--r--library/Icinga/Application/Test.php140
-rw-r--r--library/Icinga/Application/Version.php65
-rw-r--r--library/Icinga/Application/Web.php509
-rw-r--r--library/Icinga/Application/functions.php110
-rw-r--r--library/Icinga/Application/webrouter.php106
-rw-r--r--library/Icinga/Authentication/AdmissionLoader.php249
-rw-r--r--library/Icinga/Authentication/Auth.php453
-rw-r--r--library/Icinga/Authentication/AuthChain.php269
-rw-r--r--library/Icinga/Authentication/Authenticatable.php21
-rw-r--r--library/Icinga/Authentication/Role.php334
-rw-r--r--library/Icinga/Authentication/RolesConfig.php43
-rw-r--r--library/Icinga/Authentication/User/DbUserBackend.php256
-rw-r--r--library/Icinga/Authentication/User/DomainAwareInterface.php17
-rw-r--r--library/Icinga/Authentication/User/ExternalBackend.php124
-rw-r--r--library/Icinga/Authentication/User/LdapUserBackend.php479
-rw-r--r--library/Icinga/Authentication/User/UserBackend.php259
-rw-r--r--library/Icinga/Authentication/User/UserBackendInterface.php39
-rw-r--r--library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php325
-rw-r--r--library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php945
-rw-r--r--library/Icinga/Authentication/UserGroup/UserGroupBackend.php189
-rw-r--r--library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php56
-rw-r--r--library/Icinga/Chart/Axis.php485
-rw-r--r--library/Icinga/Chart/Chart.php162
-rw-r--r--library/Icinga/Chart/Donut.php465
-rw-r--r--library/Icinga/Chart/Format.php21
-rw-r--r--library/Icinga/Chart/Graph/BarGraph.php163
-rw-r--r--library/Icinga/Chart/Graph/LineGraph.php202
-rw-r--r--library/Icinga/Chart/Graph/StackedGraph.php88
-rw-r--r--library/Icinga/Chart/Graph/Tooltip.php143
-rw-r--r--library/Icinga/Chart/GridChart.php446
-rw-r--r--library/Icinga/Chart/Inline/Inline.php96
-rw-r--r--library/Icinga/Chart/Inline/PieChart.php41
-rw-r--r--library/Icinga/Chart/Legend.php102
-rw-r--r--library/Icinga/Chart/Palette.php65
-rw-r--r--library/Icinga/Chart/PieChart.php306
-rw-r--r--library/Icinga/Chart/Primitive/Animatable.php43
-rw-r--r--library/Icinga/Chart/Primitive/Animation.php87
-rw-r--r--library/Icinga/Chart/Primitive/Canvas.php140
-rw-r--r--library/Icinga/Chart/Primitive/Circle.php84
-rw-r--r--library/Icinga/Chart/Primitive/Drawable.php22
-rw-r--r--library/Icinga/Chart/Primitive/Line.php103
-rw-r--r--library/Icinga/Chart/Primitive/Path.php187
-rw-r--r--library/Icinga/Chart/Primitive/PieSlice.php307
-rw-r--r--library/Icinga/Chart/Primitive/RawElement.php43
-rw-r--r--library/Icinga/Chart/Primitive/Rect.php119
-rw-r--r--library/Icinga/Chart/Primitive/Styleable.php161
-rw-r--r--library/Icinga/Chart/Primitive/Text.php184
-rw-r--r--library/Icinga/Chart/Render/LayoutBox.php200
-rw-r--r--library/Icinga/Chart/Render/RenderContext.php225
-rw-r--r--library/Icinga/Chart/Render/Rotator.php80
-rw-r--r--library/Icinga/Chart/SVGRenderer.php331
-rw-r--r--library/Icinga/Chart/Unit/AxisUnit.php56
-rw-r--r--library/Icinga/Chart/Unit/CalendarUnit.php167
-rw-r--r--library/Icinga/Chart/Unit/LinearUnit.php227
-rw-r--r--library/Icinga/Chart/Unit/LogarithmicUnit.php263
-rw-r--r--library/Icinga/Chart/Unit/StaticAxis.php130
-rw-r--r--library/Icinga/Cli/AnsiScreen.php122
-rw-r--r--library/Icinga/Cli/Command.php216
-rw-r--r--library/Icinga/Cli/Documentation.php167
-rw-r--r--library/Icinga/Cli/Documentation/CommentParser.php85
-rw-r--r--library/Icinga/Cli/Loader.php501
-rw-r--r--library/Icinga/Cli/Params.php320
-rw-r--r--library/Icinga/Cli/Screen.php106
-rw-r--r--library/Icinga/Common/Database.php56
-rw-r--r--library/Icinga/Common/PdfExport.php105
-rw-r--r--library/Icinga/Crypt/AesCrypt.php337
-rw-r--r--library/Icinga/Data/ConfigObject.php289
-rw-r--r--library/Icinga/Data/ConnectionInterface.php8
-rw-r--r--library/Icinga/Data/DataArray/ArrayDatasource.php292
-rw-r--r--library/Icinga/Data/Db/DbConnection.php655
-rw-r--r--library/Icinga/Data/Db/DbQuery.php565
-rw-r--r--library/Icinga/Data/Extensible.php22
-rw-r--r--library/Icinga/Data/Fetchable.php47
-rw-r--r--library/Icinga/Data/Filter/Filter.php255
-rw-r--r--library/Icinga/Data/Filter/FilterAnd.php42
-rw-r--r--library/Icinga/Data/Filter/FilterChain.php286
-rw-r--r--library/Icinga/Data/Filter/FilterEqual.php16
-rw-r--r--library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php16
-rw-r--r--library/Icinga/Data/Filter/FilterEqualOrLessThan.php26
-rw-r--r--library/Icinga/Data/Filter/FilterException.php15
-rw-r--r--library/Icinga/Data/Filter/FilterExpression.php224
-rw-r--r--library/Icinga/Data/Filter/FilterGreaterThan.php16
-rw-r--r--library/Icinga/Data/Filter/FilterLessThan.php26
-rw-r--r--library/Icinga/Data/Filter/FilterMatch.php8
-rw-r--r--library/Icinga/Data/Filter/FilterMatchCaseInsensitive.php13
-rw-r--r--library/Icinga/Data/Filter/FilterMatchNot.php12
-rw-r--r--library/Icinga/Data/Filter/FilterMatchNotCaseInsensitive.php13
-rw-r--r--library/Icinga/Data/Filter/FilterNot.php58
-rw-r--r--library/Icinga/Data/Filter/FilterNotEqual.php12
-rw-r--r--library/Icinga/Data/Filter/FilterOr.php39
-rw-r--r--library/Icinga/Data/Filter/FilterParseException.php10
-rw-r--r--library/Icinga/Data/Filter/FilterQueryString.php320
-rw-r--r--library/Icinga/Data/FilterColumns.php21
-rw-r--r--library/Icinga/Data/Filterable.php27
-rw-r--r--library/Icinga/Data/Identifiable.php17
-rw-r--r--library/Icinga/Data/Inspectable.php20
-rw-r--r--library/Icinga/Data/Inspection.php129
-rw-r--r--library/Icinga/Data/Limitable.php48
-rw-r--r--library/Icinga/Data/Paginatable.php10
-rw-r--r--library/Icinga/Data/PivotTable.php396
-rw-r--r--library/Icinga/Data/QueryInterface.php8
-rw-r--r--library/Icinga/Data/Queryable.php20
-rw-r--r--library/Icinga/Data/Reducible.php23
-rw-r--r--library/Icinga/Data/ResourceFactory.php138
-rw-r--r--library/Icinga/Data/Selectable.php17
-rw-r--r--library/Icinga/Data/SimpleQuery.php650
-rw-r--r--library/Icinga/Data/SortRules.php14
-rw-r--r--library/Icinga/Data/Sortable.php49
-rw-r--r--library/Icinga/Data/Tree/SimpleTree.php90
-rw-r--r--library/Icinga/Data/Tree/TreeNode.php109
-rw-r--r--library/Icinga/Data/Tree/TreeNodeIterator.php75
-rw-r--r--library/Icinga/Data/Updatable.php24
-rw-r--r--library/Icinga/Date/DateFormatter.php265
-rw-r--r--library/Icinga/Exception/AlreadyExistsException.php11
-rw-r--r--library/Icinga/Exception/AuthenticationException.php11
-rw-r--r--library/Icinga/Exception/ConfigurationError.php12
-rw-r--r--library/Icinga/Exception/Http/BaseHttpException.php73
-rw-r--r--library/Icinga/Exception/Http/HttpBadRequestException.php12
-rw-r--r--library/Icinga/Exception/Http/HttpException.php25
-rw-r--r--library/Icinga/Exception/Http/HttpExceptionInterface.php21
-rw-r--r--library/Icinga/Exception/Http/HttpMethodNotAllowedException.php36
-rw-r--r--library/Icinga/Exception/Http/HttpNotFoundException.php12
-rw-r--r--library/Icinga/Exception/IcingaException.php114
-rw-r--r--library/Icinga/Exception/InvalidPropertyException.php11
-rw-r--r--library/Icinga/Exception/Json/JsonDecodeException.php11
-rw-r--r--library/Icinga/Exception/Json/JsonEncodeException.php11
-rw-r--r--library/Icinga/Exception/Json/JsonException.php13
-rw-r--r--library/Icinga/Exception/MissingParameterException.php40
-rw-r--r--library/Icinga/Exception/NotFoundError.php8
-rw-r--r--library/Icinga/Exception/NotImplementedError.php12
-rw-r--r--library/Icinga/Exception/NotReadableError.php8
-rw-r--r--library/Icinga/Exception/NotWritableError.php8
-rw-r--r--library/Icinga/Exception/ProgrammingError.php12
-rw-r--r--library/Icinga/Exception/QueryException.php11
-rw-r--r--library/Icinga/Exception/StatementException.php8
-rw-r--r--library/Icinga/Exception/SystemPermissionException.php11
-rw-r--r--library/Icinga/File/Csv.php47
-rw-r--r--library/Icinga/File/Ini/Dom/Comment.php37
-rw-r--r--library/Icinga/File/Ini/Dom/Directive.php166
-rw-r--r--library/Icinga/File/Ini/Dom/Document.php132
-rw-r--r--library/Icinga/File/Ini/Dom/Section.php190
-rw-r--r--library/Icinga/File/Ini/IniParser.php310
-rw-r--r--library/Icinga/File/Ini/IniWriter.php205
-rw-r--r--library/Icinga/File/Pdf.php81
-rw-r--r--library/Icinga/File/Storage/LocalFileStorage.php164
-rw-r--r--library/Icinga/File/Storage/StorageInterface.php94
-rw-r--r--library/Icinga/File/Storage/TemporaryLocalFileStorage.php59
-rw-r--r--library/Icinga/Legacy/DashboardConfig.php137
-rw-r--r--library/Icinga/Less/Call.php77
-rw-r--r--library/Icinga/Less/ColorProp.php109
-rw-r--r--library/Icinga/Less/ColorPropOrVariable.php71
-rw-r--r--library/Icinga/Less/DeferredColorProp.php136
-rw-r--r--library/Icinga/Less/LightMode.php128
-rw-r--r--library/Icinga/Less/LightModeCall.php38
-rw-r--r--library/Icinga/Less/LightModeDefinition.php75
-rw-r--r--library/Icinga/Less/LightModeTrait.php30
-rw-r--r--library/Icinga/Less/LightModeVisitor.php26
-rw-r--r--library/Icinga/Less/Visitor.php233
-rw-r--r--library/Icinga/Model/Schema.php49
-rw-r--r--library/Icinga/Protocol/Dns.php89
-rw-r--r--library/Icinga/Protocol/File/Exception/FileReaderException.php12
-rw-r--r--library/Icinga/Protocol/File/FileIterator.php81
-rw-r--r--library/Icinga/Protocol/File/FileQuery.php86
-rw-r--r--library/Icinga/Protocol/File/FileReader.php208
-rw-r--r--library/Icinga/Protocol/File/LogFileIterator.php149
-rw-r--r--library/Icinga/Protocol/Ldap/Discovery.php143
-rw-r--r--library/Icinga/Protocol/Ldap/LdapCapabilities.php440
-rw-r--r--library/Icinga/Protocol/Ldap/LdapConnection.php1584
-rw-r--r--library/Icinga/Protocol/Ldap/LdapException.php14
-rw-r--r--library/Icinga/Protocol/Ldap/LdapQuery.php361
-rw-r--r--library/Icinga/Protocol/Ldap/LdapUtils.php148
-rw-r--r--library/Icinga/Protocol/Ldap/Node.php69
-rw-r--r--library/Icinga/Protocol/Ldap/Root.php242
-rw-r--r--library/Icinga/Protocol/Nrpe/Connection.php111
-rw-r--r--library/Icinga/Protocol/Nrpe/Packet.php69
-rw-r--r--library/Icinga/Repository/DbRepository.php1078
-rw-r--r--library/Icinga/Repository/IniRepository.php418
-rw-r--r--library/Icinga/Repository/LdapRepository.php71
-rw-r--r--library/Icinga/Repository/Repository.php1261
-rw-r--r--library/Icinga/Repository/RepositoryQuery.php797
-rw-r--r--library/Icinga/Security/SecurityException.php13
-rw-r--r--library/Icinga/Test/BaseTestCase.php313
-rw-r--r--library/Icinga/Test/ClassLoader.php113
-rw-r--r--library/Icinga/Test/DbTest.php47
-rw-r--r--library/Icinga/User.php649
-rw-r--r--library/Icinga/User/Preferences.php169
-rw-r--r--library/Icinga/User/Preferences/PreferencesStore.php344
-rw-r--r--library/Icinga/Util/ASN1.php102
-rw-r--r--library/Icinga/Util/Color.php121
-rw-r--r--library/Icinga/Util/ConfigAwareFactory.php18
-rw-r--r--library/Icinga/Util/Csp.php107
-rw-r--r--library/Icinga/Util/Dimension.php123
-rw-r--r--library/Icinga/Util/DirectoryIterator.php214
-rw-r--r--library/Icinga/Util/EnumeratingFilterIterator.php30
-rw-r--r--library/Icinga/Util/Environment.php42
-rw-r--r--library/Icinga/Util/File.php195
-rw-r--r--library/Icinga/Util/Format.php197
-rw-r--r--library/Icinga/Util/GlobFilter.php182
-rw-r--r--library/Icinga/Util/Json.php151
-rw-r--r--library/Icinga/Util/LessParser.php15
-rw-r--r--library/Icinga/Util/StringHelper.php184
-rw-r--r--library/Icinga/Util/TimezoneDetect.php107
-rw-r--r--library/Icinga/Web/Announcement.php158
-rw-r--r--library/Icinga/Web/Announcement/AnnouncementCookie.php138
-rw-r--r--library/Icinga/Web/Announcement/AnnouncementIniRepository.php152
-rw-r--r--library/Icinga/Web/ApplicationStateCookie.php74
-rw-r--r--library/Icinga/Web/Controller.php264
-rw-r--r--library/Icinga/Web/Controller/ActionController.php617
-rw-r--r--library/Icinga/Web/Controller/AuthBackendController.php151
-rw-r--r--library/Icinga/Web/Controller/BasePreferenceController.php39
-rw-r--r--library/Icinga/Web/Controller/ControllerTabCollector.php97
-rw-r--r--library/Icinga/Web/Controller/Dispatcher.php93
-rw-r--r--library/Icinga/Web/Controller/ModuleActionController.php80
-rw-r--r--library/Icinga/Web/Controller/StaticController.php87
-rw-r--r--library/Icinga/Web/Cookie.php299
-rw-r--r--library/Icinga/Web/CookieSet.php58
-rw-r--r--library/Icinga/Web/Dom/DomNodeIterator.php84
-rw-r--r--library/Icinga/Web/FileCache.php293
-rw-r--r--library/Icinga/Web/Form.php1666
-rw-r--r--library/Icinga/Web/Form/Decorator/Autosubmit.php133
-rw-r--r--library/Icinga/Web/Form/Decorator/ConditionalHidden.php35
-rw-r--r--library/Icinga/Web/Form/Decorator/ElementDoubler.php63
-rw-r--r--library/Icinga/Web/Form/Decorator/FormDescriptions.php76
-rw-r--r--library/Icinga/Web/Form/Decorator/FormHints.php142
-rw-r--r--library/Icinga/Web/Form/Decorator/FormNotifications.php125
-rw-r--r--library/Icinga/Web/Form/Decorator/Help.php113
-rw-r--r--library/Icinga/Web/Form/Decorator/Spinner.php48
-rw-r--r--library/Icinga/Web/Form/Element/Button.php81
-rw-r--r--library/Icinga/Web/Form/Element/Checkbox.php9
-rw-r--r--library/Icinga/Web/Form/Element/CsrfCounterMeasure.php99
-rw-r--r--library/Icinga/Web/Form/Element/Date.php19
-rw-r--r--library/Icinga/Web/Form/Element/DateTimePicker.php80
-rw-r--r--library/Icinga/Web/Form/Element/Note.php55
-rw-r--r--library/Icinga/Web/Form/Element/Number.php144
-rw-r--r--library/Icinga/Web/Form/Element/Textarea.php20
-rw-r--r--library/Icinga/Web/Form/Element/Time.php19
-rw-r--r--library/Icinga/Web/Form/ErrorLabeller.php71
-rw-r--r--library/Icinga/Web/Form/FormElement.php61
-rw-r--r--library/Icinga/Web/Form/InvalidCSRFTokenException.php11
-rw-r--r--library/Icinga/Web/Form/Validator/DateFormatValidator.php61
-rw-r--r--library/Icinga/Web/Form/Validator/DateTimeValidator.php77
-rw-r--r--library/Icinga/Web/Form/Validator/InArray.php28
-rw-r--r--library/Icinga/Web/Form/Validator/InternalUrlValidator.php41
-rw-r--r--library/Icinga/Web/Form/Validator/ReadablePathValidator.php53
-rw-r--r--library/Icinga/Web/Form/Validator/TimeFormatValidator.php58
-rw-r--r--library/Icinga/Web/Form/Validator/UrlValidator.php40
-rw-r--r--library/Icinga/Web/Form/Validator/WritablePathValidator.php72
-rw-r--r--library/Icinga/Web/Helper/CookieHelper.php81
-rw-r--r--library/Icinga/Web/Helper/HtmlPurifier.php95
-rw-r--r--library/Icinga/Web/Helper/Markdown.php34
-rw-r--r--library/Icinga/Web/Helper/Markdown/LinkTransformer.php73
-rw-r--r--library/Icinga/Web/Hook.php16
-rw-r--r--library/Icinga/Web/JavaScript.php269
-rw-r--r--library/Icinga/Web/LessCompiler.php255
-rw-r--r--library/Icinga/Web/Menu.php152
-rw-r--r--library/Icinga/Web/Navigation/ConfigMenu.php327
-rw-r--r--library/Icinga/Web/Navigation/DashboardPane.php84
-rw-r--r--library/Icinga/Web/Navigation/DropdownItem.php20
-rw-r--r--library/Icinga/Web/Navigation/Navigation.php572
-rw-r--r--library/Icinga/Web/Navigation/NavigationItem.php948
-rw-r--r--library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php139
-rw-r--r--library/Icinga/Web/Navigation/Renderer/HealthNavigationRenderer.php44
-rw-r--r--library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php235
-rw-r--r--library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php356
-rw-r--r--library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php142
-rw-r--r--library/Icinga/Web/Navigation/Renderer/RecursiveNavigationRenderer.php186
-rw-r--r--library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php72
-rw-r--r--library/Icinga/Web/Notification.php220
-rw-r--r--library/Icinga/Web/Paginator/Adapter/QueryAdapter.php84
-rw-r--r--library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php78
-rw-r--r--library/Icinga/Web/RememberMe.php363
-rw-r--r--library/Icinga/Web/RememberMeUserDevicesList.php144
-rw-r--r--library/Icinga/Web/RememberMeUserList.php106
-rw-r--r--library/Icinga/Web/Request.php142
-rw-r--r--library/Icinga/Web/Response.php460
-rw-r--r--library/Icinga/Web/Response/JsonResponse.php241
-rw-r--r--library/Icinga/Web/Session.php54
-rw-r--r--library/Icinga/Web/Session/Php72Session.php37
-rw-r--r--library/Icinga/Web/Session/PhpSession.php256
-rw-r--r--library/Icinga/Web/Session/Session.php126
-rw-r--r--library/Icinga/Web/Session/SessionNamespace.php201
-rw-r--r--library/Icinga/Web/StyleSheet.php342
-rw-r--r--library/Icinga/Web/Url.php806
-rw-r--r--library/Icinga/Web/UrlParams.php433
-rw-r--r--library/Icinga/Web/UserAgent.php86
-rw-r--r--library/Icinga/Web/View.php254
-rw-r--r--library/Icinga/Web/View/AppHealth.php89
-rw-r--r--library/Icinga/Web/View/Helper/IcingaCheckbox.php30
-rw-r--r--library/Icinga/Web/View/PrivilegeAudit.php622
-rw-r--r--library/Icinga/Web/View/helpers/format.php72
-rw-r--r--library/Icinga/Web/View/helpers/generic.php15
-rw-r--r--library/Icinga/Web/View/helpers/string.php36
-rw-r--r--library/Icinga/Web/View/helpers/url.php158
-rw-r--r--library/Icinga/Web/Widget.php49
-rw-r--r--library/Icinga/Web/Widget/AbstractWidget.php121
-rw-r--r--library/Icinga/Web/Widget/Announcements.php55
-rw-r--r--library/Icinga/Web/Widget/ApplicationStateMessages.php74
-rw-r--r--library/Icinga/Web/Widget/Chart/HistoryColorGrid.php400
-rw-r--r--library/Icinga/Web/Widget/Chart/InlinePie.php257
-rw-r--r--library/Icinga/Web/Widget/Dashboard.php475
-rw-r--r--library/Icinga/Web/Widget/Dashboard/Dashlet.php315
-rw-r--r--library/Icinga/Web/Widget/Dashboard/Pane.php335
-rw-r--r--library/Icinga/Web/Widget/Dashboard/UserWidget.php36
-rw-r--r--library/Icinga/Web/Widget/FilterEditor.php811
-rw-r--r--library/Icinga/Web/Widget/ItemList/MigrationFileListItem.php92
-rw-r--r--library/Icinga/Web/Widget/ItemList/MigrationList.php133
-rw-r--r--library/Icinga/Web/Widget/ItemList/MigrationListItem.php151
-rw-r--r--library/Icinga/Web/Widget/Limiter.php54
-rw-r--r--library/Icinga/Web/Widget/Paginator.php167
-rw-r--r--library/Icinga/Web/Widget/SearchDashboard.php111
-rw-r--r--library/Icinga/Web/Widget/SingleValueSearchControl.php200
-rw-r--r--library/Icinga/Web/Widget/SortBox.php260
-rw-r--r--library/Icinga/Web/Widget/Tab.php323
-rw-r--r--library/Icinga/Web/Widget/Tabextension/DashboardAction.php35
-rw-r--r--library/Icinga/Web/Widget/Tabextension/DashboardSettings.php39
-rw-r--r--library/Icinga/Web/Widget/Tabextension/MenuAction.php35
-rw-r--r--library/Icinga/Web/Widget/Tabextension/OutputFormat.php114
-rw-r--r--library/Icinga/Web/Widget/Tabextension/Tabextension.php25
-rw-r--r--library/Icinga/Web/Widget/Tabs.php453
-rw-r--r--library/Icinga/Web/Widget/Widget.php24
-rw-r--r--library/Icinga/Web/Window.php125
-rw-r--r--library/Icinga/Web/Wizard.php720
-rw-r--r--modules/doc/application/controllers/IcingawebController.php62
-rw-r--r--modules/doc/application/controllers/IndexController.php27
-rw-r--r--modules/doc/application/controllers/ModuleController.php206
-rw-r--r--modules/doc/application/controllers/SearchController.php97
-rw-r--r--modules/doc/application/controllers/StyleController.php41
-rw-r--r--modules/doc/application/views/scripts/chapter.phtml6
-rw-r--r--modules/doc/application/views/scripts/index/index.phtml19
-rw-r--r--modules/doc/application/views/scripts/module/index.phtml19
-rw-r--r--modules/doc/application/views/scripts/pdf.phtml5
-rw-r--r--modules/doc/application/views/scripts/search/index.phtml8
-rw-r--r--modules/doc/application/views/scripts/style/font.phtml14
-rw-r--r--modules/doc/application/views/scripts/style/guide.phtml112
-rw-r--r--modules/doc/application/views/scripts/toc.phtml6
-rw-r--r--modules/doc/configuration.php24
-rw-r--r--modules/doc/doc/01-About.md6
-rw-r--r--modules/doc/doc/02-Installation.md15
-rw-r--r--modules/doc/doc/03-Module-Documentation.md87
-rw-r--r--modules/doc/doc/img/markdown.pngbin0 -> 2180 bytes
-rw-r--r--modules/doc/library/Doc/DocController.php116
-rw-r--r--modules/doc/library/Doc/DocParser.php235
-rw-r--r--modules/doc/library/Doc/DocSection.php159
-rw-r--r--modules/doc/library/Doc/DocSectionFilterIterator.php73
-rw-r--r--modules/doc/library/Doc/Exception/ChapterNotFoundException.php11
-rw-r--r--modules/doc/library/Doc/Exception/DocException.php13
-rw-r--r--modules/doc/library/Doc/Renderer/DocRenderer.php208
-rw-r--r--modules/doc/library/Doc/Renderer/DocSearchRenderer.php131
-rw-r--r--modules/doc/library/Doc/Renderer/DocSectionRenderer.php345
-rw-r--r--modules/doc/library/Doc/Renderer/DocTocRenderer.php117
-rw-r--r--modules/doc/library/Doc/Search/DocSearch.php95
-rw-r--r--modules/doc/library/Doc/Search/DocSearchIterator.php114
-rw-r--r--modules/doc/library/Doc/Search/DocSearchMatch.php215
-rw-r--r--modules/doc/module.info4
-rw-r--r--modules/doc/public/css/module.less120
-rw-r--r--modules/doc/public/js/module.js30
-rw-r--r--modules/doc/run.php64
-rw-r--r--modules/migrate/application/clicommands/ConfigCommand.php119
-rw-r--r--modules/migrate/application/clicommands/NavigationCommand.php20
-rw-r--r--modules/migrate/application/clicommands/PreferencesCommand.php131
-rw-r--r--modules/migrate/library/Migrate/Config/UserDomainMigration.php378
-rw-r--r--modules/migrate/module.info5
-rw-r--r--modules/monitoring/application/clicommands/ListCommand.php400
-rw-r--r--modules/monitoring/application/clicommands/NrpeCommand.php58
-rw-r--r--modules/monitoring/application/controllers/ActionsController.php135
-rw-r--r--modules/monitoring/application/controllers/CommentController.php91
-rw-r--r--modules/monitoring/application/controllers/CommentsController.php108
-rw-r--r--modules/monitoring/application/controllers/ConfigController.php298
-rw-r--r--modules/monitoring/application/controllers/DowntimeController.php108
-rw-r--r--modules/monitoring/application/controllers/DowntimesController.php108
-rw-r--r--modules/monitoring/application/controllers/EventController.php551
-rw-r--r--modules/monitoring/application/controllers/HealthController.php197
-rw-r--r--modules/monitoring/application/controllers/HostController.php185
-rw-r--r--modules/monitoring/application/controllers/HostsController.php260
-rw-r--r--modules/monitoring/application/controllers/ListController.php808
-rw-r--r--modules/monitoring/application/controllers/ServiceController.php147
-rw-r--r--modules/monitoring/application/controllers/ServicesController.php262
-rw-r--r--modules/monitoring/application/controllers/ShowController.php101
-rw-r--r--modules/monitoring/application/controllers/TacticalController.php128
-rw-r--r--modules/monitoring/application/controllers/TimelineController.php325
-rw-r--r--modules/monitoring/application/forms/Command/CommandForm.php92
-rw-r--r--modules/monitoring/application/forms/Command/Instance/DisableNotificationsExpireCommandForm.php64
-rw-r--r--modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php279
-rw-r--r--modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php172
-rw-r--r--modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php148
-rw-r--r--modules/monitoring/application/forms/Command/Object/CheckNowCommandForm.php87
-rw-r--r--modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php109
-rw-r--r--modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php89
-rw-r--r--modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php129
-rw-r--r--modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php89
-rw-r--r--modules/monitoring/application/forms/Command/Object/ObjectsCommandForm.php47
-rw-r--r--modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php140
-rw-r--r--modules/monitoring/application/forms/Command/Object/RemoveAcknowledgementCommandForm.php122
-rw-r--r--modules/monitoring/application/forms/Command/Object/ScheduleHostCheckCommandForm.php67
-rw-r--r--modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php178
-rw-r--r--modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php112
-rw-r--r--modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php263
-rw-r--r--modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php110
-rw-r--r--modules/monitoring/application/forms/Command/Object/ToggleObjectFeaturesCommandForm.php187
-rw-r--r--modules/monitoring/application/forms/Config/BackendConfigForm.php367
-rw-r--r--modules/monitoring/application/forms/Config/SecurityConfigForm.php75
-rw-r--r--modules/monitoring/application/forms/Config/Transport/ApiTransportForm.php75
-rw-r--r--modules/monitoring/application/forms/Config/Transport/LocalTransportForm.php37
-rw-r--r--modules/monitoring/application/forms/Config/Transport/RemoteTransportForm.php185
-rw-r--r--modules/monitoring/application/forms/Config/TransportConfigForm.php392
-rw-r--r--modules/monitoring/application/forms/Config/TransportReorderForm.php87
-rw-r--r--modules/monitoring/application/forms/EventOverviewForm.php157
-rw-r--r--modules/monitoring/application/forms/Navigation/ActionForm.php79
-rw-r--r--modules/monitoring/application/forms/Navigation/HostActionForm.php8
-rw-r--r--modules/monitoring/application/forms/Navigation/ServiceActionForm.php8
-rw-r--r--modules/monitoring/application/forms/Setup/BackendPage.php51
-rw-r--r--modules/monitoring/application/forms/Setup/IdoResourcePage.php188
-rw-r--r--modules/monitoring/application/forms/Setup/SecurityPage.php27
-rw-r--r--modules/monitoring/application/forms/Setup/TransportPage.php55
-rw-r--r--modules/monitoring/application/forms/Setup/WelcomePage.php63
-rw-r--r--modules/monitoring/application/forms/StatehistoryForm.php141
-rw-r--r--modules/monitoring/application/views/helpers/CheckPerformance.php50
-rw-r--r--modules/monitoring/application/views/helpers/ContactFlags.php46
-rw-r--r--modules/monitoring/application/views/helpers/Customvar.php67
-rw-r--r--modules/monitoring/application/views/helpers/EscapeComment.php34
-rw-r--r--modules/monitoring/application/views/helpers/HostFlags.php38
-rw-r--r--modules/monitoring/application/views/helpers/IconImage.php69
-rw-r--r--modules/monitoring/application/views/helpers/Link.php72
-rw-r--r--modules/monitoring/application/views/helpers/MonitoringFlags.php40
-rw-r--r--modules/monitoring/application/views/helpers/Perfdata.php120
-rw-r--r--modules/monitoring/application/views/helpers/PluginOutput.php199
-rw-r--r--modules/monitoring/application/views/helpers/RuntimeVariables.php50
-rw-r--r--modules/monitoring/application/views/helpers/ServiceFlags.php38
-rw-r--r--modules/monitoring/application/views/scripts/comment/remove.phtml11
-rw-r--r--modules/monitoring/application/views/scripts/comment/show.phtml86
-rw-r--r--modules/monitoring/application/views/scripts/comments/delete-all.phtml12
-rw-r--r--modules/monitoring/application/views/scripts/comments/show.phtml19
-rw-r--r--modules/monitoring/application/views/scripts/config/form.phtml6
-rw-r--r--modules/monitoring/application/views/scripts/config/index.phtml78
-rw-r--r--modules/monitoring/application/views/scripts/config/security.phtml6
-rw-r--r--modules/monitoring/application/views/scripts/downtime/remove.phtml13
-rw-r--r--modules/monitoring/application/views/scripts/downtime/show.phtml173
-rw-r--r--modules/monitoring/application/views/scripts/downtimes/delete-all.phtml12
-rw-r--r--modules/monitoring/application/views/scripts/downtimes/show.phtml19
-rw-r--r--modules/monitoring/application/views/scripts/event/show.phtml34
-rw-r--r--modules/monitoring/application/views/scripts/form/reorder-command-transports.phtml93
-rw-r--r--modules/monitoring/application/views/scripts/health/disable-notifications.phtml20
-rw-r--r--modules/monitoring/application/views/scripts/health/info.phtml87
-rw-r--r--modules/monitoring/application/views/scripts/health/not-running.phtml8
-rw-r--r--modules/monitoring/application/views/scripts/health/stats.phtml150
-rw-r--r--modules/monitoring/application/views/scripts/host/services.phtml23
-rw-r--r--modules/monitoring/application/views/scripts/host/show.phtml14
-rw-r--r--modules/monitoring/application/views/scripts/hosts/show.phtml206
-rw-r--r--modules/monitoring/application/views/scripts/list/comments.phtml61
-rw-r--r--modules/monitoring/application/views/scripts/list/components/hostssummary.phtml92
-rw-r--r--modules/monitoring/application/views/scripts/list/components/selectioninfo.phtml15
-rw-r--r--modules/monitoring/application/views/scripts/list/components/servicesummary.phtml118
-rw-r--r--modules/monitoring/application/views/scripts/list/contactgroups.phtml53
-rw-r--r--modules/monitoring/application/views/scripts/list/contacts.phtml83
-rw-r--r--modules/monitoring/application/views/scripts/list/downtimes.phtml64
-rw-r--r--modules/monitoring/application/views/scripts/list/eventgrid.phtml123
-rw-r--r--modules/monitoring/application/views/scripts/list/eventhistory.phtml22
-rw-r--r--modules/monitoring/application/views/scripts/list/hostgroup-grid.phtml173
-rw-r--r--modules/monitoring/application/views/scripts/list/hostgroups.phtml296
-rw-r--r--modules/monitoring/application/views/scripts/list/hosts.phtml106
-rw-r--r--modules/monitoring/application/views/scripts/list/notifications.phtml124
-rw-r--r--modules/monitoring/application/views/scripts/list/servicegrid-flipped.phtml144
-rw-r--r--modules/monitoring/application/views/scripts/list/servicegrid.phtml144
-rw-r--r--modules/monitoring/application/views/scripts/list/servicegroup-grid.phtml217
-rw-r--r--modules/monitoring/application/views/scripts/list/servicegroups.phtml184
-rw-r--r--modules/monitoring/application/views/scripts/list/services.phtml161
-rw-r--r--modules/monitoring/application/views/scripts/object/detail-history.phtml13
-rw-r--r--modules/monitoring/application/views/scripts/object/detail-tabhook.phtml21
-rw-r--r--modules/monitoring/application/views/scripts/partials/command/object-command-form.phtml18
-rw-r--r--modules/monitoring/application/views/scripts/partials/command/objects-command-form.phtml15
-rw-r--r--modules/monitoring/application/views/scripts/partials/comment/comment-description.phtml24
-rw-r--r--modules/monitoring/application/views/scripts/partials/comment/comment-detail.phtml82
-rw-r--r--modules/monitoring/application/views/scripts/partials/comment/comment-header.phtml10
-rw-r--r--modules/monitoring/application/views/scripts/partials/comment/comments-header.phtml32
-rw-r--r--modules/monitoring/application/views/scripts/partials/downtime/downtime-header.phtml101
-rw-r--r--modules/monitoring/application/views/scripts/partials/downtime/downtimes-header.phtml40
-rw-r--r--modules/monitoring/application/views/scripts/partials/event-history.phtml267
-rw-r--r--modules/monitoring/application/views/scripts/partials/host/objects-header.phtml41
-rw-r--r--modules/monitoring/application/views/scripts/partials/object/detail-content.phtml53
-rw-r--r--modules/monitoring/application/views/scripts/partials/object/host-header.phtml51
-rw-r--r--modules/monitoring/application/views/scripts/partials/object/quick-actions.phtml144
-rw-r--r--modules/monitoring/application/views/scripts/partials/object/service-header.phtml72
-rw-r--r--modules/monitoring/application/views/scripts/partials/service/objects-header.phtml45
-rw-r--r--modules/monitoring/application/views/scripts/partials/show-more.phtml15
-rw-r--r--modules/monitoring/application/views/scripts/service/show.phtml8
-rw-r--r--modules/monitoring/application/views/scripts/services/show.phtml208
-rw-r--r--modules/monitoring/application/views/scripts/show/components/acknowledgement.phtml94
-rw-r--r--modules/monitoring/application/views/scripts/show/components/actions.phtml43
-rw-r--r--modules/monitoring/application/views/scripts/show/components/checksource.phtml6
-rw-r--r--modules/monitoring/application/views/scripts/show/components/checkstatistics.phtml85
-rw-r--r--modules/monitoring/application/views/scripts/show/components/checktimeperiod.phtml21
-rw-r--r--modules/monitoring/application/views/scripts/show/components/command.phtml52
-rw-r--r--modules/monitoring/application/views/scripts/show/components/comments.phtml86
-rw-r--r--modules/monitoring/application/views/scripts/show/components/contacts.phtml38
-rw-r--r--modules/monitoring/application/views/scripts/show/components/downtime.phtml109
-rw-r--r--modules/monitoring/application/views/scripts/show/components/extensions.phtml4
-rw-r--r--modules/monitoring/application/views/scripts/show/components/flags.phtml4
-rw-r--r--modules/monitoring/application/views/scripts/show/components/flapping.phtml14
-rw-r--r--modules/monitoring/application/views/scripts/show/components/grapher.phtml6
-rw-r--r--modules/monitoring/application/views/scripts/show/components/hostgroups.phtml19
-rw-r--r--modules/monitoring/application/views/scripts/show/components/notes.phtml48
-rw-r--r--modules/monitoring/application/views/scripts/show/components/notifications.phtml68
-rw-r--r--modules/monitoring/application/views/scripts/show/components/output.phtml5
-rw-r--r--modules/monitoring/application/views/scripts/show/components/perfdata.phtml4
-rw-r--r--modules/monitoring/application/views/scripts/show/components/reachable.phtml15
-rw-r--r--modules/monitoring/application/views/scripts/show/components/servicegroups.phtml20
-rw-r--r--modules/monitoring/application/views/scripts/show/components/status.phtml0
-rw-r--r--modules/monitoring/application/views/scripts/show/contact.phtml67
-rw-r--r--modules/monitoring/application/views/scripts/tactical/components/hostservicechecks.phtml131
-rw-r--r--modules/monitoring/application/views/scripts/tactical/components/monitoringfeatures.phtml287
-rw-r--r--modules/monitoring/application/views/scripts/tactical/components/ok_hosts.phtml81
-rw-r--r--modules/monitoring/application/views/scripts/tactical/components/parts/servicestatesummarybyhoststate.phtml394
-rw-r--r--modules/monitoring/application/views/scripts/tactical/components/problem_hosts.phtml74
-rw-r--r--modules/monitoring/application/views/scripts/tactical/index.phtml145
-rw-r--r--modules/monitoring/application/views/scripts/timeline/index.phtml196
-rw-r--r--modules/monitoring/configuration.php432
-rw-r--r--modules/monitoring/doc/01-About.md10
-rw-r--r--modules/monitoring/doc/02-Installation.md15
-rw-r--r--modules/monitoring/doc/03-Configuration.md69
-rw-r--r--modules/monitoring/doc/04-Backends.md30
-rw-r--r--modules/monitoring/doc/05-Command-Transports.md185
-rw-r--r--modules/monitoring/doc/06-Security.md66
-rw-r--r--modules/monitoring/doc/10-Restrict-Custom-Variables.md77
-rw-r--r--modules/monitoring/doc/11-Add-Columns-List-Views.md32
-rw-r--r--modules/monitoring/doc/20-Hooks.md161
-rw-r--r--modules/monitoring/doc/img/hooks-detailviewextension-01.pngbin0 -> 10714 bytes
-rw-r--r--modules/monitoring/doc/img/list_hosts_add_columns.pngbin0 -> 187915 bytes
-rw-r--r--modules/monitoring/doc/img/list_services_add_columns.pngbin0 -> 209925 bytes
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/IdoBackend.php10
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/AllcontactsQuery.php75
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/CommandQuery.php61
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentQuery.php158
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php179
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenteventQuery.php39
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php179
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactQuery.php139
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php214
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/CustomvarQuery.php116
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php163
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php179
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeeventQuery.php42
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php179
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyhostgroupQuery.php38
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyservicegroupQuery.php51
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridQuery.php56
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridhostsQuery.php16
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridservicesQuery.php15
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/EventhistoryQuery.php134
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingendhistoryQuery.php49
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingeventQuery.php36
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php179
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/GroupsummaryQuery.php131
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php202
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentdeletionhistoryQuery.php44
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php197
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php247
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php208
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeendhistoryQuery.php42
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php206
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingendhistoryQuery.php31
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php200
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php295
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupsummaryQuery.php142
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php284
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php222
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php338
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatussummaryQuery.php91
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php1599
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/InstanceQuery.php26
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationQuery.php144
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationeventQuery.php52
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationhistoryQuery.php142
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ProgramstatusQuery.php68
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimesummaryQuery.php80
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimevariablesQuery.php18
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php218
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentdeletionhistoryQuery.php44
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php195
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php235
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php222
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeendhistoryQuery.php42
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php205
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingendhistoryQuery.php31
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php197
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php303
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupsummaryQuery.php113
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php287
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php220
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php524
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatussummaryQuery.php104
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/StatechangeeventQuery.php41
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php179
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/StatussummaryQuery.php243
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledhostproblemsQuery.php48
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledserviceproblemsQuery.php48
-rw-r--r--modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php349
-rw-r--r--modules/monitoring/library/Monitoring/BackendStep.php208
-rw-r--r--modules/monitoring/library/Monitoring/Cli/CliUtils.php122
-rw-r--r--modules/monitoring/library/Monitoring/Command/IcingaApiCommand.php126
-rw-r--r--modules/monitoring/library/Monitoring/Command/IcingaCommand.php21
-rw-r--r--modules/monitoring/library/Monitoring/Command/Instance/DisableNotificationsExpireCommand.php42
-rw-r--r--modules/monitoring/library/Monitoring/Command/Instance/ToggleInstanceFeatureCommand.php122
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/AcknowledgeProblemCommand.php144
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/AddCommentCommand.php80
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/ApiScheduleHostDowntimeCommand.php40
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/CommandAuthor.php38
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/DeleteCommentCommand.php110
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/DeleteDowntimeCommand.php110
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/ObjectCommand.php61
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/ProcessCheckResultCommand.php176
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/PropagateHostDowntimeCommand.php48
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/RemoveAcknowledgementCommand.php21
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/ScheduleHostCheckCommand.php48
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/ScheduleHostDowntimeCommand.php75
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/ScheduleServiceCheckCommand.php92
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/ScheduleServiceDowntimeCommand.php190
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/SendCustomNotificationCommand.php82
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/ToggleObjectFeatureCommand.php113
-rw-r--r--modules/monitoring/library/Monitoring/Command/Object/WithCommentCommand.php42
-rw-r--r--modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php322
-rw-r--r--modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php478
-rw-r--r--modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandRendererInterface.php11
-rw-r--r--modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php291
-rw-r--r--modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php170
-rw-r--r--modules/monitoring/library/Monitoring/Command/Transport/CommandTransportInterface.php22
-rw-r--r--modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php168
-rw-r--r--modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php465
-rw-r--r--modules/monitoring/library/Monitoring/Controller.php159
-rw-r--r--modules/monitoring/library/Monitoring/Data/ColumnFilterIterator.php30
-rw-r--r--modules/monitoring/library/Monitoring/Data/CustomvarProtectionIterator.php25
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Command.php24
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Comment.php82
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Commentevent.php30
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Contact.php73
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Contactgroup.php57
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Customvar.php47
-rw-r--r--modules/monitoring/library/Monitoring/DataView/DataView.php608
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Downtime.php96
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Downtimeevent.php33
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Eventgrid.php60
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Eventgridhosts.php7
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Eventgridservices.php7
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Eventhistory.php60
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Flappingevent.php27
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Hostcomment.php45
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Hostcontact.php17
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Hostdowntime.php50
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Hostgroup.php34
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Hostgroupsummary.php81
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Hoststatus.php129
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Hoststatussummary.php40
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Instance.php33
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Notification.php59
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Notificationevent.php29
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Programstatus.php44
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Runtimesummary.php38
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Runtimevariables.php34
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Servicecomment.php48
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Servicecontact.php8
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Servicedowntime.php50
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Servicegroup.php31
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Servicegroupsummary.php75
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Servicestatus.php180
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Servicestatussummary.php45
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Statechangeevent.php32
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Statussummary.php111
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Unhandledhostproblems.php28
-rw-r--r--modules/monitoring/library/Monitoring/DataView/Unhandledserviceproblems.php28
-rw-r--r--modules/monitoring/library/Monitoring/Exception/CommandTransportException.php13
-rw-r--r--modules/monitoring/library/Monitoring/Exception/CurlException.php13
-rw-r--r--modules/monitoring/library/Monitoring/Exception/UnsupportedBackendException.php11
-rw-r--r--modules/monitoring/library/Monitoring/Hook/CustomVarRendererHook.php98
-rw-r--r--modules/monitoring/library/Monitoring/Hook/DataviewExtensionHook.php20
-rw-r--r--modules/monitoring/library/Monitoring/Hook/DetailviewExtensionHook.php126
-rw-r--r--modules/monitoring/library/Monitoring/Hook/EventDetailsExtensionHook.php79
-rw-r--r--modules/monitoring/library/Monitoring/Hook/HostActionsHook.php52
-rw-r--r--modules/monitoring/library/Monitoring/Hook/IdoQueryExtensionHook.php15
-rw-r--r--modules/monitoring/library/Monitoring/Hook/ObjectActionsHook.php47
-rw-r--r--modules/monitoring/library/Monitoring/Hook/ObjectDetailsTabHook.php60
-rw-r--r--modules/monitoring/library/Monitoring/Hook/PluginOutputHook.php46
-rw-r--r--modules/monitoring/library/Monitoring/Hook/ServiceActionsHook.php52
-rw-r--r--modules/monitoring/library/Monitoring/Hook/TimelineProviderHook.php37
-rw-r--r--modules/monitoring/library/Monitoring/MonitoringWizard.php160
-rw-r--r--modules/monitoring/library/Monitoring/Object/Acknowledgement.php215
-rw-r--r--modules/monitoring/library/Monitoring/Object/Host.php205
-rw-r--r--modules/monitoring/library/Monitoring/Object/HostList.php133
-rw-r--r--modules/monitoring/library/Monitoring/Object/Macro.php83
-rw-r--r--modules/monitoring/library/Monitoring/Object/MonitoredObject.php930
-rw-r--r--modules/monitoring/library/Monitoring/Object/ObjectList.php295
-rw-r--r--modules/monitoring/library/Monitoring/Object/Service.php220
-rw-r--r--modules/monitoring/library/Monitoring/Object/ServiceList.php184
-rw-r--r--modules/monitoring/library/Monitoring/Plugin/Perfdata.php550
-rw-r--r--modules/monitoring/library/Monitoring/Plugin/PerfdataSet.php144
-rw-r--r--modules/monitoring/library/Monitoring/Plugin/ThresholdRange.php179
-rw-r--r--modules/monitoring/library/Monitoring/ProvidedHook/ApplicationState.php32
-rw-r--r--modules/monitoring/library/Monitoring/ProvidedHook/Health.php102
-rw-r--r--modules/monitoring/library/Monitoring/ProvidedHook/X509/Sni.php37
-rw-r--r--modules/monitoring/library/Monitoring/SecurityStep.php84
-rw-r--r--modules/monitoring/library/Monitoring/Timeline/TimeEntry.php233
-rw-r--r--modules/monitoring/library/Monitoring/Timeline/TimeLine.php491
-rw-r--r--modules/monitoring/library/Monitoring/Timeline/TimeRange.php258
-rw-r--r--modules/monitoring/library/Monitoring/TransportStep.php143
-rw-r--r--modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php339
-rw-r--r--modules/monitoring/library/Monitoring/Web/Helper/PluginOutputHookRenderer.php105
-rw-r--r--modules/monitoring/library/Monitoring/Web/Hook/HostActionsHook.php15
-rw-r--r--modules/monitoring/library/Monitoring/Web/Hook/ServiceActionsHook.php15
-rw-r--r--modules/monitoring/library/Monitoring/Web/Hook/TimelineProviderHook.php15
-rw-r--r--modules/monitoring/library/Monitoring/Web/Navigation/Action.php123
-rw-r--r--modules/monitoring/library/Monitoring/Web/Navigation/HostAction.php11
-rw-r--r--modules/monitoring/library/Monitoring/Web/Navigation/HostNote.php11
-rw-r--r--modules/monitoring/library/Monitoring/Web/Navigation/Renderer/MonitoringBadgeNavigationItemRenderer.php171
-rw-r--r--modules/monitoring/library/Monitoring/Web/Navigation/ServiceAction.php11
-rw-r--r--modules/monitoring/library/Monitoring/Web/Navigation/ServiceNote.php11
-rw-r--r--modules/monitoring/library/Monitoring/Web/Rest/RestRequest.php297
-rw-r--r--modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php272
-rw-r--r--modules/monitoring/library/Monitoring/Web/Widget/SelectBox.php120
-rw-r--r--modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php341
-rw-r--r--modules/monitoring/module.info5
-rw-r--r--modules/monitoring/public/css/event-grid.less9
-rw-r--r--modules/monitoring/public/css/module.less1922
-rw-r--r--modules/monitoring/public/css/service-grid.less75
-rw-r--r--modules/monitoring/public/css/tables.less282
-rw-r--r--modules/monitoring/public/js/module.js84
-rw-r--r--modules/monitoring/run.php8
-rw-r--r--modules/setup/application/clicommands/ConfigCommand.php188
-rw-r--r--modules/setup/application/clicommands/TokenCommand.php89
-rw-r--r--modules/setup/application/controllers/IndexController.php91
-rw-r--r--modules/setup/application/forms/AdminAccountPage.php431
-rw-r--r--modules/setup/application/forms/AuthBackendPage.php274
-rw-r--r--modules/setup/application/forms/AuthenticationPage.php69
-rw-r--r--modules/setup/application/forms/DatabaseCreationPage.php209
-rw-r--r--modules/setup/application/forms/DbResourcePage.php183
-rw-r--r--modules/setup/application/forms/GeneralConfigPage.php41
-rw-r--r--modules/setup/application/forms/LdapDiscoveryConfirmPage.php133
-rw-r--r--modules/setup/application/forms/LdapDiscoveryPage.php115
-rw-r--r--modules/setup/application/forms/LdapResourcePage.php152
-rw-r--r--modules/setup/application/forms/ModulePage.php108
-rw-r--r--modules/setup/application/forms/RequirementsPage.php68
-rw-r--r--modules/setup/application/forms/SummaryPage.php84
-rw-r--r--modules/setup/application/forms/UserGroupBackendPage.php147
-rw-r--r--modules/setup/application/forms/WelcomePage.php45
-rw-r--r--modules/setup/application/views/scripts/form/setup-modules.phtml33
-rw-r--r--modules/setup/application/views/scripts/form/setup-requirements.phtml48
-rw-r--r--modules/setup/application/views/scripts/form/setup-summary.phtml40
-rw-r--r--modules/setup/application/views/scripts/form/setup-welcome.phtml120
-rw-r--r--modules/setup/application/views/scripts/index/index.phtml224
-rw-r--r--modules/setup/application/views/scripts/index/parts/finish.phtml34
-rw-r--r--modules/setup/application/views/scripts/index/parts/wizard.phtml1
-rw-r--r--modules/setup/library/Setup/Exception/SetupException.php22
-rw-r--r--modules/setup/library/Setup/Requirement.php343
-rw-r--r--modules/setup/library/Setup/Requirement/ClassRequirement.php48
-rw-r--r--modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php42
-rw-r--r--modules/setup/library/Setup/Requirement/OSRequirement.php27
-rw-r--r--modules/setup/library/Setup/Requirement/PhpConfigRequirement.php22
-rw-r--r--modules/setup/library/Setup/Requirement/PhpModuleRequirement.php42
-rw-r--r--modules/setup/library/Setup/Requirement/PhpVersionRequirement.php28
-rw-r--r--modules/setup/library/Setup/Requirement/SetRequirement.php34
-rw-r--r--modules/setup/library/Setup/Requirement/WebLibraryRequirement.php24
-rw-r--r--modules/setup/library/Setup/Requirement/WebModuleRequirement.php31
-rw-r--r--modules/setup/library/Setup/RequirementSet.php335
-rw-r--r--modules/setup/library/Setup/RequirementsRenderer.php67
-rw-r--r--modules/setup/library/Setup/Setup.php99
-rw-r--r--modules/setup/library/Setup/SetupWizard.php24
-rw-r--r--modules/setup/library/Setup/Step.php31
-rw-r--r--modules/setup/library/Setup/Steps/AuthenticationStep.php238
-rw-r--r--modules/setup/library/Setup/Steps/DatabaseStep.php266
-rw-r--r--modules/setup/library/Setup/Steps/GeneralConfigStep.php133
-rw-r--r--modules/setup/library/Setup/Steps/ResourceStep.php201
-rw-r--r--modules/setup/library/Setup/Steps/UserGroupStep.php213
-rw-r--r--modules/setup/library/Setup/Utils/DbTool.php950
-rw-r--r--modules/setup/library/Setup/Utils/EnableModuleStep.php77
-rw-r--r--modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php73
-rw-r--r--modules/setup/library/Setup/WebWizard.php768
-rw-r--r--modules/setup/library/Setup/Webserver.php233
-rw-r--r--modules/setup/library/Setup/Webserver/Apache.php142
-rw-r--r--modules/setup/library/Setup/Webserver/Nginx.php36
-rw-r--r--modules/setup/module.info6
-rw-r--r--modules/translation/application/clicommands/CompileCommand.php40
-rw-r--r--modules/translation/application/clicommands/RefreshCommand.php40
-rw-r--r--modules/translation/application/clicommands/TestCommand.php140
-rw-r--r--modules/translation/doc/01-About.md6
-rw-r--r--modules/translation/doc/02-Installation.md15
-rw-r--r--modules/translation/doc/03-Translation.md204
-rw-r--r--modules/translation/doc/img/poedit_001.pngbin0 -> 24252 bytes
-rw-r--r--modules/translation/doc/img/poedit_002.pngbin0 -> 40936 bytes
-rw-r--r--modules/translation/doc/img/poedit_003.pngbin0 -> 21482 bytes
-rw-r--r--modules/translation/doc/img/poedit_004.pngbin0 -> 40052 bytes
-rw-r--r--modules/translation/doc/img/poedit_005.pngbin0 -> 23752 bytes
-rw-r--r--modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php229
-rw-r--r--modules/translation/library/Translation/Cli/TranslationCommand.php73
-rw-r--r--modules/translation/library/Translation/Util/GettextTranslationHelper.php441
-rw-r--r--modules/translation/module.info7
-rw-r--r--phpstan-baseline.neon26246
-rw-r--r--phpstan.neon67
-rw-r--r--public/css/icinga/about.less113
-rw-r--r--public/css/icinga/animation.less366
-rw-r--r--public/css/icinga/audit.less363
-rw-r--r--public/css/icinga/badges.less22
-rw-r--r--public/css/icinga/base.less344
-rw-r--r--public/css/icinga/compat.less35
-rw-r--r--public/css/icinga/configmenu.less303
-rw-r--r--public/css/icinga/controls.less281
-rw-r--r--public/css/icinga/dev.less10
-rw-r--r--public/css/icinga/forms.less596
-rw-r--r--public/css/icinga/grid.less47
-rw-r--r--public/css/icinga/health.less69
-rw-r--r--public/css/icinga/layout-structure.less167
-rw-r--r--public/css/icinga/layout.less379
-rw-r--r--public/css/icinga/login-orbs.less104
-rw-r--r--public/css/icinga/login.less183
-rw-r--r--public/css/icinga/main.less459
-rw-r--r--public/css/icinga/menu.less554
-rw-r--r--public/css/icinga/mixins.less201
-rw-r--r--public/css/icinga/modal.less113
-rw-r--r--public/css/icinga/nav.less54
-rw-r--r--public/css/icinga/pending-migration.less173
-rw-r--r--public/css/icinga/php-diff.less17
-rw-r--r--public/css/icinga/print.less39
-rw-r--r--public/css/icinga/responsive.less167
-rw-r--r--public/css/icinga/setup.less480
-rw-r--r--public/css/icinga/spinner.less42
-rw-r--r--public/css/icinga/tabs.less109
-rw-r--r--public/css/icinga/widgets.less666
-rw-r--r--public/css/modes/light.less4
-rw-r--r--public/css/modes/none.less4
-rw-r--r--public/css/modes/system.less4
-rw-r--r--public/css/pdf/pdfprint.less103
-rw-r--r--public/css/themes/Winter.less30
-rw-r--r--public/css/themes/colorblind.less29
-rw-r--r--public/css/themes/high-contrast.less250
-rw-r--r--public/css/vendor/normalize.css427
-rw-r--r--public/error_norewrite.html1
-rw-r--r--public/error_unavailable.html7
-rw-r--r--public/font/ifont.eotbin0 -> 45480 bytes
-rw-r--r--public/font/ifont.svg286
-rw-r--r--public/font/ifont.ttfbin0 -> 45324 bytes
-rw-r--r--public/font/ifont.woffbin0 -> 27320 bytes
-rw-r--r--public/font/ifont.woff2bin0 -> 22700 bytes
-rw-r--r--public/img/favicon.pngbin0 -> 806 bytes
-rw-r--r--public/img/icinga-loader-light.gifbin0 -> 94929 bytes
-rw-r--r--public/img/icinga-loader.gifbin0 -> 32942 bytes
-rw-r--r--public/img/icinga-logo-big-dark.pngbin0 -> 3491 bytes
-rw-r--r--public/img/icinga-logo-big-dark.svg14
-rw-r--r--public/img/icinga-logo-big.pngbin0 -> 3586 bytes
-rw-r--r--public/img/icinga-logo-big.svg1
-rw-r--r--public/img/icinga-logo-compact-inverted.svg9
-rw-r--r--public/img/icinga-logo-compact.svg30
-rw-r--r--public/img/icinga-logo-dark.svg30
-rw-r--r--public/img/icinga-logo-inverted.svg1
-rw-r--r--public/img/icinga-logo.pngbin0 -> 1132 bytes
-rw-r--r--public/img/icinga-logo.svg32
-rw-r--r--public/img/icingaweb2-background-orbs.jpgbin0 -> 1467100 bytes
-rw-r--r--public/img/icingaweb2-background.jpgbin0 -> 305235 bytes
-rw-r--r--public/img/icons/acknowledgement.pngbin0 -> 501 bytes
-rw-r--r--public/img/icons/acknowledgement_petrol.pngbin0 -> 492 bytes
-rw-r--r--public/img/icons/active_checks_disabled.pngbin0 -> 511 bytes
-rw-r--r--public/img/icons/active_checks_disabled_petrol.pngbin0 -> 513 bytes
-rw-r--r--public/img/icons/active_passive_checks_disabled.pngbin0 -> 537 bytes
-rw-r--r--public/img/icons/active_passive_checks_disabled_petrol.pngbin0 -> 546 bytes
-rw-r--r--public/img/icons/comment.pngbin0 -> 491 bytes
-rw-r--r--public/img/icons/comment_petrol.pngbin0 -> 502 bytes
-rw-r--r--public/img/icons/configuration.pngbin0 -> 647 bytes
-rw-r--r--public/img/icons/configuration_petrol.pngbin0 -> 645 bytes
-rw-r--r--public/img/icons/create.pngbin0 -> 475 bytes
-rw-r--r--public/img/icons/create_petrol.pngbin0 -> 482 bytes
-rw-r--r--public/img/icons/csv.pngbin0 -> 509 bytes
-rw-r--r--public/img/icons/csv_petrol.pngbin0 -> 511 bytes
-rw-r--r--public/img/icons/dashboard.pngbin0 -> 415 bytes
-rw-r--r--public/img/icons/dashboard_petrol.pngbin0 -> 420 bytes
-rw-r--r--public/img/icons/disabled.pngbin0 -> 535 bytes
-rw-r--r--public/img/icons/disabled_petrol.pngbin0 -> 525 bytes
-rw-r--r--public/img/icons/down.pngbin0 -> 492 bytes
-rw-r--r--public/img/icons/down_petrol.pngbin0 -> 505 bytes
-rw-r--r--public/img/icons/downtime_end.pngbin0 -> 550 bytes
-rw-r--r--public/img/icons/downtime_end_petrol.pngbin0 -> 561 bytes
-rw-r--r--public/img/icons/downtime_start.pngbin0 -> 524 bytes
-rw-r--r--public/img/icons/downtime_start__petrol.pngbin0 -> 529 bytes
-rw-r--r--public/img/icons/edit.pngbin0 -> 486 bytes
-rw-r--r--public/img/icons/edit_petrol.pngbin0 -> 493 bytes
-rw-r--r--public/img/icons/error.pngbin0 -> 532 bytes
-rw-r--r--public/img/icons/error_petrol.pngbin0 -> 535 bytes
-rw-r--r--public/img/icons/error_white.pngbin0 -> 429 bytes
-rw-r--r--public/img/icons/expand.pngbin0 -> 2993 bytes
-rw-r--r--public/img/icons/expand_petrol.pngbin0 -> 2992 bytes
-rw-r--r--public/img/icons/flapping.pngbin0 -> 621 bytes
-rw-r--r--public/img/icons/flapping_petrol.pngbin0 -> 636 bytes
-rw-r--r--public/img/icons/history.pngbin0 -> 516 bytes
-rw-r--r--public/img/icons/history_petrol.pngbin0 -> 520 bytes
-rw-r--r--public/img/icons/host.pngbin0 -> 500 bytes
-rw-r--r--public/img/icons/host_petrol.pngbin0 -> 489 bytes
-rw-r--r--public/img/icons/hostgroup.pngbin0 -> 541 bytes
-rw-r--r--public/img/icons/hostgroup_petrol.pngbin0 -> 554 bytes
-rw-r--r--public/img/icons/in_downtime.pngbin0 -> 490 bytes
-rw-r--r--public/img/icons/in_downtime_petrol.pngbin0 -> 497 bytes
-rw-r--r--public/img/icons/json.pngbin0 -> 517 bytes
-rw-r--r--public/img/icons/json_petrol.pngbin0 -> 525 bytes
-rw-r--r--public/img/icons/logout.pngbin0 -> 462 bytes
-rw-r--r--public/img/icons/logout_petrol.pngbin0 -> 470 bytes
-rw-r--r--public/img/icons/next.pngbin0 -> 509 bytes
-rw-r--r--public/img/icons/next_petrol.pngbin0 -> 513 bytes
-rw-r--r--public/img/icons/notification.pngbin0 -> 544 bytes
-rw-r--r--public/img/icons/notification_disabled.pngbin0 -> 487 bytes
-rw-r--r--public/img/icons/notification_disabled_petrol.pngbin0 -> 494 bytes
-rw-r--r--public/img/icons/notification_petrol.pngbin0 -> 548 bytes
-rw-r--r--public/img/icons/pdf.pngbin0 -> 507 bytes
-rw-r--r--public/img/icons/pdf_petrol.pngbin0 -> 511 bytes
-rw-r--r--public/img/icons/prev.pngbin0 -> 514 bytes
-rw-r--r--public/img/icons/prev_petrol.pngbin0 -> 519 bytes
-rw-r--r--public/img/icons/refresh.pngbin0 -> 523 bytes
-rw-r--r--public/img/icons/refresh_petrol.pngbin0 -> 517 bytes
-rw-r--r--public/img/icons/remove.pngbin0 -> 661 bytes
-rw-r--r--public/img/icons/remove_petrol.pngbin0 -> 661 bytes
-rw-r--r--public/img/icons/reschedule.pngbin0 -> 400 bytes
-rw-r--r--public/img/icons/reschedule_petrol.pngbin0 -> 403 bytes
-rw-r--r--public/img/icons/save.pngbin0 -> 506 bytes
-rw-r--r--public/img/icons/save_petrol.pngbin0 -> 517 bytes
-rw-r--r--public/img/icons/search.pngbin0 -> 491 bytes
-rw-r--r--public/img/icons/search_icinga_blue.pngbin0 -> 432 bytes
-rw-r--r--public/img/icons/search_petrol.pngbin0 -> 493 bytes
-rw-r--r--public/img/icons/search_white.pngbin0 -> 1352 bytes
-rw-r--r--public/img/icons/service.pngbin0 -> 496 bytes
-rw-r--r--public/img/icons/service_petrol.pngbin0 -> 505 bytes
-rw-r--r--public/img/icons/servicegroup.pngbin0 -> 598 bytes
-rw-r--r--public/img/icons/servicegroup_petrol.pngbin0 -> 605 bytes
-rw-r--r--public/img/icons/softstate.pngbin0 -> 511 bytes
-rw-r--r--public/img/icons/submit.pngbin0 -> 418 bytes
-rw-r--r--public/img/icons/submit_petrol.pngbin0 -> 426 bytes
-rw-r--r--public/img/icons/success.pngbin0 -> 509 bytes
-rw-r--r--public/img/icons/success_petrol.pngbin0 -> 505 bytes
-rw-r--r--public/img/icons/tux.pngbin0 -> 495 bytes
-rw-r--r--public/img/icons/uebersicht.pngbin0 -> 20920 bytes
-rw-r--r--public/img/icons/unhandled.pngbin0 -> 553 bytes
-rw-r--r--public/img/icons/unhandled_petrol.pngbin0 -> 555 bytes
-rw-r--r--public/img/icons/up.pngbin0 -> 497 bytes
-rw-r--r--public/img/icons/up_petrol.pngbin0 -> 499 bytes
-rw-r--r--public/img/icons/user.pngbin0 -> 487 bytes
-rw-r--r--public/img/icons/user_petrol.pngbin0 -> 508 bytes
-rw-r--r--public/img/icons/win.pngbin0 -> 738 bytes
-rw-r--r--public/img/orb-analytics.pngbin0 -> 232972 bytes
-rw-r--r--public/img/orb-automation.pngbin0 -> 320334 bytes
-rw-r--r--public/img/orb-cloud.pngbin0 -> 294936 bytes
-rw-r--r--public/img/orb-icinga.pngbin0 -> 259264 bytes
-rw-r--r--public/img/orb-infrastructure.pngbin0 -> 293092 bytes
-rw-r--r--public/img/orb-metrics.pngbin0 -> 289015 bytes
-rw-r--r--public/img/orb-notifications.pngbin0 -> 308893 bytes
-rw-r--r--public/img/select-icon-2x.pngbin0 -> 305 bytes
-rw-r--r--public/img/select-icon.pngbin0 -> 176 bytes
-rw-r--r--public/img/select-icon.svg1
-rw-r--r--public/img/textarea-corner-2x.pngbin0 -> 238 bytes
-rw-r--r--public/img/textarea-corner.pngbin0 -> 181 bytes
-rw-r--r--public/img/theme-mode-thumbnail-dark.svg59
-rw-r--r--public/img/theme-mode-thumbnail-light.svg54
-rw-r--r--public/img/theme-mode-thumbnail-system.svg91
-rw-r--r--public/img/touch-icon.pngbin0 -> 4777 bytes
-rw-r--r--public/img/tree/tree-minus.gifbin0 -> 149 bytes
-rw-r--r--public/img/tree/tree-plus.gifbin0 -> 151 bytes
-rw-r--r--public/img/website-icon.svg4
-rw-r--r--public/img/winter/logo_icinga_big_winter.pngbin0 -> 9474 bytes
-rw-r--r--public/img/winter/snow1.pngbin0 -> 2765 bytes
-rw-r--r--public/img/winter/snow2.pngbin0 -> 4867 bytes
-rw-r--r--public/img/winter/snow3.pngbin0 -> 3117 bytes
-rw-r--r--public/index.php4
-rw-r--r--public/js/bootstrap.js28
-rw-r--r--public/js/define.js118
-rw-r--r--public/js/helpers.js91
-rw-r--r--public/js/icinga.js279
-rw-r--r--public/js/icinga/behavior/actiontable.js498
-rw-r--r--public/js/icinga/behavior/application-state.js40
-rw-r--r--public/js/icinga/behavior/autofocus.js28
-rw-r--r--public/js/icinga/behavior/collapsible.js470
-rw-r--r--public/js/icinga/behavior/copy-to-clipboard.js41
-rw-r--r--public/js/icinga/behavior/datetime-picker.js222
-rw-r--r--public/js/icinga/behavior/detach.js73
-rw-r--r--public/js/icinga/behavior/dropdown.js66
-rw-r--r--public/js/icinga/behavior/filtereditor.js77
-rw-r--r--public/js/icinga/behavior/flyover.js85
-rw-r--r--public/js/icinga/behavior/form.js96
-rw-r--r--public/js/icinga/behavior/input-enrichment.js148
-rw-r--r--public/js/icinga/behavior/modal.js254
-rw-r--r--public/js/icinga/behavior/navigation.js464
-rw-r--r--public/js/icinga/behavior/selectable.js49
-rw-r--r--public/js/icinga/eventlistener.js78
-rw-r--r--public/js/icinga/events.js425
-rw-r--r--public/js/icinga/history.js338
-rw-r--r--public/js/icinga/loader.js1367
-rw-r--r--public/js/icinga/logger.js129
-rw-r--r--public/js/icinga/module.js134
-rw-r--r--public/js/icinga/storage.js549
-rw-r--r--public/js/icinga/timer.js176
-rw-r--r--public/js/icinga/timezone.js105
-rw-r--r--public/js/icinga/ui.js645
-rw-r--r--public/js/icinga/utils.js582
-rw-r--r--public/js/logout.js16
-rw-r--r--schema/mysql-upgrades/2.0.0beta3-2.0.0rc1.sql26
-rw-r--r--schema/mysql-upgrades/2.11.0.sql33
-rw-r--r--schema/mysql-upgrades/2.12.0.sql11
-rw-r--r--schema/mysql-upgrades/2.5.0.sql5
-rw-r--r--schema/mysql-upgrades/2.9.0.sql11
-rw-r--r--schema/mysql-upgrades/2.9.1.sql2
-rw-r--r--schema/mysql.schema.sql68
-rw-r--r--schema/pgsql-upgrades/2.0.0beta3-2.0.0rc1.sql60
-rw-r--r--schema/pgsql-upgrades/2.11.0.sql10
-rw-r--r--schema/pgsql-upgrades/2.12.0.sql13
-rw-r--r--schema/pgsql-upgrades/2.5.0.sql5
-rw-r--r--schema/pgsql-upgrades/2.9.0.sql16
-rw-r--r--schema/pgsql-upgrades/2.9.1.sql2
-rw-r--r--schema/pgsql.schema.sql135
1249 files changed, 185451 insertions, 0 deletions
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 0000000..3f55aa6
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,43 @@
+Alexander A. Klimov <alexander.klimov@icinga.com> <Alexander.Klimov@netways.de>
+Alexander A. Klimov <alexander.klimov@icinga.com> <alexander.klimov@netways.de>
+Alexander A. Klimov <alexander.klimov@icinga.com> <Al2Klimov@users.noreply.github.com>
+Alexander A. Klimov <alexander.klimov@icinga.com> <grandmaster@al2klimov.de>
+Bernd Erk <bernd.erk@icinga.com> <bernd.erk@netways.de>
+Bernd Erk <bernd.erk@icinga.com><berk@nb-berk.int.netways.de>
+Bernd Erk <bernd.erk@icinga.com><berk@nb-berk.local>
+Bernd Erk <bernd.erk@icinga.com><bernd.erk@icinga.com>
+Bernd Erk <bernd.erk@icinga.com><bernd.erk@icinga.org>
+Blerim Sheqa <blerim.sheqa@icinga.com> <blerim.sheqa@netways.de>
+Carlos Cesario <carloscesario@gmail.com> <ccesario@tecnomega.com.br>
+Christopher Rüll <christopher.ruell@netways.de> <Christopher.Ruell@netways.de>
+Eric Lippmann <eric.lippmann@icinga.com> <eric.lippmann@netways.de>
+Eric Lippmann <eric.lippmann@icinga.com> <lippserd@googlemail.com>
+Florian Strohmaier <florian.strohmaier@icinga.com> <florian.strohmaier@netways.de>
+Florian Strohmaier <florian.strohmaier@icinga.com> <hello@florianstrohmaier.com>
+Florian Strohmaier <florian.strohmaier@icinga.com> <florian.strohmaier@me.com>
+Gunnar Beutner <gunnar.beutner@netways.de> <gunnar@beutner.name>
+Jannis Moßhammer <jannis.mosshammer@netways.de>
+Johannes Meyer <johannes.meyer@icinga.com> <johannes.meyer@netways.de>
+Jennifer Mourek <jennifer.mourek@icinga.com> <jennifer.mourek@netways.de>
+Marius Hein <marius.hein@netways.de> <mhein@itsocks.de>
+Markus Frosch <markus.frosch@icinga.com> <lazyfrosch@icinga.org>
+Markus Frosch <markus.frosch@icinga.com> <markus.frosch@netways.de>
+Markus Frosch <markus.frosch@icinga.com> <markus@lazyfrosch.de>
+Matthias Jentsch <matthias.jentsch@netways.de> <mjentsch@localhost.int.netways.de>
+Max Kozlov <m.v.kozlov@gmail.com> <M.V.Kozlov@gmail.com>
+Michael Friedrich <michael.friedrich@icinga.com> <Michael.Friedrich@netways.de>
+Michael Friedrich <michael.friedrich@icinga.com> <michael.friedrich@gmail.com>
+Michael Friedrich <michael.friedrich@icinga.com> <michael.friedrich@netways.de>
+Nicolai Buchwitz <nicolai.buchwitz@enda.eu> <nbuchwitz@users.noreply.github.com>
+Noah Hilverling <noah.hilverling@icinga.com> <noah.hilverling@netways.de>
+Noah Hilverling <noah.hilverling@icinga.com> <noah@hilverling.com>
+Philipp Dorschner <philipp.dorschner@netways.de> <pdorschner@netways.de>
+Sylph Lin <sylph.lin@gmail.com>
+Thomas Gelf <thomas.gelf@icinga.com> <root@squeeze-devel1.osmc.lab>
+Thomas Gelf <thomas.gelf@icinga.com> <tgelf@tgelf-web2dep.(none)>
+Thomas Gelf <thomas.gelf@icinga.com> <thomas.gelf@netways.de>
+Thomas Gelf <thomas.gelf@icinga.com> <thomas@gelf.net>
+Yonas Habteab <yonas.habteab@icinga.com> <yonas.habteab@netways.de>
+Ravi Kumar Kempapura Srinivasa <ravi.srinivasa@icinga.com> <33730024+raviks789@users.noreply.github.com>
+Sukhwinder Dhillon <sukhwinder.dhillon@icinga.com> <54990055+sukhwinder33445@users.noreply.github.com>
+Sukhwinder Dhillon <sukhwinder.dhillon@icinga.com> <sukhwinder33445@gmail.com>
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..a56dd0a
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,145 @@
+Aaron Collins <acollins@chegg.com>
+Alexander A. Klimov <alexander.klimov@icinga.com>
+Alexander Aleksandrovič Klimov <alexander.klimov@icinga.com>
+Alexander Fuhr <alexander.fuhr@netways.de>
+Alexander Wirt <formorer@debian.org>
+Andreas Olsson <andreas@arrakis.se>
+ayoubabid <ayoubabid@users.noreply.github.com>
+Bas Couwenberg <sebastic@xs4all.nl>
+baufrecht <baufrecht@users.noreply.github.com>
+Bence Nagy <bence@underyx.me>
+Benedikt Heine <bebe@bebehei.de>
+Bernd Arnold <wopfel@gmail.com>
+Bernd Erk <bernd.erk@icinga.com>
+Bernhard Friedreich <bernhard.friedreich@brz.gv.at>
+Blerim Sheqa <blerim.sheqa@icinga.com>
+Boden Garman <boden.garman@spintel.net.au>
+bradynathan <bradynathan@gmail.com>
+Carlos Cesario <carloscesario@gmail.com>
+Carsten <carsten.koebke@gmx.de>
+Carsten Koebke <carsten.koebke@koebbes.de>
+chisatohasimoto <hasimoto@designet.co.jp>
+Chivvv <60713338+Chivvv@users.noreply.github.com>
+Chris Reeves <chris.reeves@york.ac.uk>
+Christopher Rüll <christopher.ruell@netways.de>
+Christoph Niemann <kordolan@googlemail.com>
+Christoph Wiechert <wio@psitrax.de>
+Constantin Matheis <constantin.matheis@gmail.com>
+Cornelius Wachinger <cornelius@dercorn.com>
+cstegm <cstegm@users.noreply.github.com>
+Damiano Chini <damiano.chini@wuerth-phoenix.com>
+Daniel <d.lorych@gmail.com>
+Daniel Shirley <aditaa@ig2ad.com>
+Davide Bizzarri <davide.bizzarri@wuerth-phoenix.com>
+Davide Demuru <davide.demuru@buongiorno.com>
+Dirk Goetz <dirk.goetz@netways.de>
+Dokon <david.okon@hotmail.com>
+Elias Probst <mail@eliasprobst.eu>
+Emil Vikström <emil@pixelstore.se>
+Eric Jaw <naisanza@gmail.com>
+Eric Lippmann <eric.lippmann@icinga.com>
+Feu Mourek <feu.mourek@icinga.com>
+Florian Strohmaier <florian.strohmaier@icinga.com>
+Francesco Colista <fcolista@alpinelinux.org>
+Francesco Mazzi <fmazzi@comune.genova.it>
+Gianluca Piccolo <gianluca.piccolo@wuerth-phoenix.com>
+Goran Rakic <grakic@devbase.net>
+Gunnar Beutner <gunnar.beutner@netways.de>
+h0rmiga <github@hormiga.ru>
+hailthemelody@rm-laptop04 <hailthemelody@rm-laptop04>
+Hector Sanjuan <hector.sanjuan@nugg.ad>
+Heike Jurzik <huhn@lion-3.fritz.box>
+Ian Shearin <ishearin@womply.com>
+ignasr <ignas.linux@gmail.com>
+Janne Heß <janne@hess.ooo>
+Jannis Moßhammer <jannis.mosshammer@netways.de>
+Jennifer Mourek <jennifer.mourek@icinga.com>
+Jiri Pejchal <jiri.pejchal@gmail.com>
+Joe Doherty <git@pjuu.com>
+Johannes Meyer <johannes.meyer@icinga.com>
+Joonas Kylmälä <joonas.kylmala@kirjastot.fi>
+Jorge Vallecillo <jorgevallecilloc@gmail.com>
+Jo Rhett <jo@chegg.com>
+Ken Jungclaus <lum33n@web.de>
+Kevin Köllmann <mail@kevinkoellmann.de>
+Klaus Jrgensen <klaus@blackwoodseven.com>
+Lee Clemens <java@leeclemens.net>
+Loei Petrus Marogi <loeipetrus.marogi@netways.de>
+log1-c <24474580+log1-c@users.noreply.github.com>
+Louis Sautier <sautier.louis@gmail.com>
+mapa82 <maik.paetzold@akra.de>
+Marc DeTrano <marc@gridshield.net>
+Marcel Weinberg <marcel.weinberg@secucloud.com>
+Marcus Cobden <marcus@marcuscobden.co.uk>
+Marian Rainer-Harbach <marian@rainer-harbach.at>
+Mario Rimann <mario@rimann.org>
+Marius Hein <marius.hein@netways.de>
+Markus Frosch <markus.frosch@icinga.com>
+Markus Opolka <opolkams@iis.fraunhofer.de>
+Massimiliano Torromeo <massimiliano.torromeo@gmail.com>
+Matthias Jentsch <matthias.jentsch@netways.de>
+Matthias <pub@matthias-henning.de>
+Mattia Codato <mattia.codato@wuerth-phoenix.com>
+Max Kozlov <m.v.kozlov@gmail.com>
+Max Stephan <xam.stephan@web.de>
+mbaschnitzi <mbaschnitzi@users.noreply.github.com>
+mdetrano <marc@gridshield.net>
+Michael Friedrich <michael.friedrich@icinga.com>
+Michael T. DeGuzis <mdeguzis@users.noreply.github.com>
+Mike Pennisi <mike@mikepennisi.com>
+Mikesch-mp <Mikesch-mp@koebbes.de>
+Mikko Peltokangas <mikko@peltokangas.org>
+moreamazingnick <github@nicolas-schneider.at>
+mrdsam <69315803+mrdsam@users.noreply.github.com>
+mrzo2s45 <dominik.lueffe@komm.one>
+Munzir Taha <munzirtaha@gmail.com>
+Nicolai Buchwitz <nicolai.buchwitz@enda.eu>
+Niko Martini <niko.martini@netways.de>
+nmartini <niko.martini@netways.de>
+Noah Hilverling <noah.hilverling@icinga.com>
+Oliver Rahner <oliver@rahner.me>
+p4k8 <pkuznetsunit@gmail.com>
+Paolo Schiro <paolo.schiro@kpnqwest.it>
+papillon326 <udagawa@www2178ue.sakura.ne.jp>
+Patrick Dolinic <pdolinic@netways.de>
+Paul Richards <paul@minimoo.org>
+Pavlos Daoglou <pdaoglou@gmail.com>
+Peter Eckel <pe-git@hindenburgring.com>
+Philipp Dorschner <philipp.dorschner@netways.de>
+Pieter Lexis <pieter.lexis@powerdns.com>
+PunkoIvan <punkoivan@gmail.com>
+Ramy Talal <ramy@thinkquality.nl>
+Raphael Bicker <raphael@bicker.ch>
+Ravi Kumar Kempapura Srinivasa <ravi.srinivasa@icinga.com>
+rbelinsky <rbelinsky@dalet.com>
+realitygaps <github@gapsinreality.com>
+Rene Moser <rene.moser@swisstxt.ch>
+Rick Henry <rjh@rick-h.xyz>
+rkcpi <thieme.sandra@gmail.com>
+Roland Hopferwieser <rhopfer@ica.jku.at>
+Rudy Gevaert <rudy.gevaert@ugent.be>
+Rune Darrud <theflyingcorpse@gmail.com>
+Russell Kubik <russkubik@3d-p.com>
+Sander Ferdinand <sa.ferdinand@gmail.com>
+sant-swedge <simon.wedge@sant.ox.ac.uk>
+Simone Orsi <simahawk@users.noreply.github.com>
+ss23 <stephen@zxsecurity.co.nz>
+Sukhwinder Dhillon <sukhwinder.dhillon@icinga.com>
+Susanne Vestner-Ludwig <susanne.vestner-ludwig@inserteffect.com>
+Sylph Lin <sylph.lin@gmail.com>
+tfylling <torbfylling@gmail.com>
+Thomas Gelf <thomas.gelf@icinga.com>
+Tim Helfensdörfer <tim@visualappeal.de>
+Timm Ortloff <timm.ortloff@icinga.com>
+Tobias Tiederle <ttiederle@fimltd.org>
+Tobias von der Krone <tobias.vonderkrone@profitbricks.com>
+Tomas Barton <barton.tomas@gmail.com>
+Tom Ford <exptom@users.noreply.github.com>
+Ulf Lange <mopp@gmx.net>
+Uwe Ebel <kobmaki@aol.com>
+ValeDaRold <36924916+ValeDaRold@users.noreply.github.com>
+Valentina Da Rold <Valentina.DaRold@wuerth-phoenix.com>
+Vladislav Ponomarev <vponomarev@team.mobile.de>
+xert <xert@users.noreply.github.com>
+Yonas Habteab <yonas.habteab@icinga.com>
+Yuri Konotopov <ykonotopov@gmail.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..7a80c4c
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,1583 @@
+# Icinga Web 2 Changelog
+
+Please make sure to always read our [Upgrading](doc/80-Upgrading.md) documentation before switching to a new version.
+
+## What's New
+
+### What's New in Version 2.12.1
+
+You can find all issues related to this release on our Roadmap.
+
+#### PHP 8.3 Support
+
+This time we're a little ahead for once. PHP 8.3 is due in a week, and we are compatible with it now!
+There's not much else to say about it, so let's continue with the fixes.
+
+* Support for PHP 8.3 [#5136](https://github.com/Icinga/icingaweb2/issues/5136)
+
+#### Fixes
+
+You may have noticed a dashboard endlessly loading in the morning after you got to work again.
+The web server may also have stopped that with a complaint about a too long URL. This is now
+fixed and the dashboard should appear as usual. Then there was an issue with our support for
+PostgreSQL. We learned it the hard way to avoid such already in the past again and again.
+Though, this one slipped through our thorough testing and prevented some from successfully
+migrating the database schema. It's fixed now. Another fixed issue, is that the UI looks
+somewhat skewed if you have CSP enabled and logged out and in again.
+
+* Login Redirect Loop [#5133](https://github.com/Icinga/icingaweb2/issues/5133)
+* UI database migration not fully compatible with PostgreSQL [#5129](https://github.com/Icinga/icingaweb2/issues/5129)
+* Missing styles when logging out and in while CSP is enabled [#5126](https://github.com/Icinga/icingaweb2/issues/5126)
+
+### What's New in Version 2.12.0
+
+You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/79?closed=1).
+
+#### PHP 8.2 Support
+
+This release finally adds support for the latest version of PHP, 8.2. This means that installations on Debian Bookworm,
+Ubuntu 23.10 and Fedora 38+ can now install Icinga Web without worrying about PHP related incompatibilities. Some of our
+other modules still require an update, which they will receive in the coming weeks. Next week Icinga DB Web will follow.
+Icinga Certificate Monitoring, Icinga Business Process Modeling and Icinga Reporting the weeks after.
+
+* Support for PHP 8.2 [#4918](https://github.com/Icinga/icingaweb2/issues/4918)
+
+#### Simplified Database Migrations
+
+Anyone who already performed an upgrade of Icinga Web or some Icinga Web module in the past has done it: A database
+schema upgrade. This usually involved the following steps:
+
+* Knowing that a database might need an upgrade
+* Figuring out if that's true, by checking the upgrade documentation
+* Alternatively relying on the users to find out about it as they're running into database errors
+* Locating the upgrade file
+* Connecting to the machine the database is running on
+* Transferring the upgrade file over
+* Importing the upgrade file into the correct database
+
+With Icinga Web v2.12 and later, upgrade the application and, yes, still check the upgrade documentation. That's still
+mandatory! But if you notice there, that just a database upgrade is necessary you can simply log in and check the
+*Migrations* section in the *System* menu. With a single additional click you can perform the database upgrade directly
+in the UI then. This view also offers to migrate module databases. The earlier mentioned updates of Icinga Certificate
+Monitoring and Icinga Reporting will pop up there once they arrive.
+
+* Provide a way to easily perform database migrations [#5043](https://github.com/Icinga/icingaweb2/issues/5043)
+
+#### Content-Security-Policy Conformance
+
+Err, what? That's an HTTP header to prevent cross site scripting attacks. (XSS) Still confused? It's a technique
+to stop bad individuals. A very effective technique even. You don't need to do anything, other than visiting the
+general configuration of Icinga Web and enabling the respective setting. The only downer here, is that support
+for it isn't as widespread yet as you might hope. Icinga Web itself of course has it, but not all modules. But don't
+worry, you might have guessed it already, those are the same modules which will receive updates in the coming weeks.
+
+* Support for Content-Security-Policy [#4528](https://github.com/Icinga/icingaweb2/issues/4528)
+
+#### Other Notable Changes
+
+There are not only such big changes as previously mentioned part of this release.
+
+Some module developers may be happy to hear that there is now more control for the server over the UI possible.
+And with a new Javascript event it is now possible to react upon a column's content being moved to another column.
+Now built-in into the framework is also an easy way to mark content in the UI as being copiable with a single click
+by the user.
+
+* Allow to initiate a refresh with `__REFRESH__` [#5108](https://github.com/Icinga/icingaweb2/pull/5108)
+* Don't refresh twice upon `__CLOSE__` [#5106](https://github.com/Icinga/icingaweb2/pull/5106)
+* Add event `column-moved` [#5049](https://github.com/Icinga/icingaweb2/pull/5049)
+* Add copy-to-clipboard behavior [#5041](https://github.com/Icinga/icingaweb2/pull/5041)
+
+Then there are some fixes related to other integrations. It is now possible to set up resources for Oracle databases,
+without a `host` setting, which facilitate dynamic host name resolution. A part of the `monitoring` module's integration
+into the Icinga Certificate Monitoring prevents a crash of its collector daemon in case the connection to the IDO was
+interrupted. And exported content, with data that has double quotes, to CSV is now correctly escaped.
+
+* Access Oracle Database via tnsnames.ora / LDAP Naming Services [#5062](https://github.com/Icinga/icingaweb2/issues/5062)
+* Reduce risk of crashing the x509 collector daemon [#5115](https://github.com/Icinga/icingaweb2/pull/5115)
+* CSV export does not escape double quotes [#4910](https://github.com/Icinga/icingaweb2/issues/4910)
+
+### What's New in Version 2.11.4
+
+You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/78?closed=1).
+
+#### Notable Fixes
+
+* Add/Edit dashlet not possible [#4970](https://github.com/Icinga/icingaweb2/issues/4970)
+* Custom library path + custom library, without slash in its name, results in exception [#4971](https://github.com/Icinga/icingaweb2/issues/4971)
+* Reflected XSS vulnerability in User Backends config page [#4979](https://github.com/Icinga/icingaweb2/issues/4979)
+
+### What's New in Version 2.11.3
+
+**Notice**: This is a security release. It is recommended to upgrade immediately.
+
+You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/77?closed=1).
+
+#### Minor to Medium Vulnerabilities
+
+In late November we received multiple security vulnerability reports. They are listed below in order of severity
+where you can also find further notes:
+
+* Open Redirects for logged in users [#4945](https://github.com/Icinga/icingaweb2/issues/4945)
+ This one is quite old, though got worse and easier to exploit since v2.9. It is for this reason that
+ this fix has been backported all the way down to v2.9.8. It can be used to exploit incautious users,
+ no matter their browser and its security settings. They need to click a specifically crafted link
+ (in the easiest form) and log in to Icinga Web by filling in their access credentials. If they're
+ already logged in, (due to an existing session or SSO) the browser prevents the exploit from happening.
+ We encourage you to update to the latest release as soon as possible to mitigate any potential harm.
+
+* SSH Resource Configuration form XSS Bug [#4947](https://github.com/Icinga/icingaweb2/issues/4947)
+ Dashlets allow the user to run Javascript code [#4959](https://github.com/Icinga/icingaweb2/issues/4959)
+ These two are very similar. Both revolve around Javascript getting injected by logged in users
+ interacting with forms. The SSH resource configuration requires configuration access though and, since
+ custom dashlets are only shown to the user who created them, the dashlet configuration cannot affect
+ other users. Note that both interactions cannot be initiated externally by CSRF, the forms are protected
+ against this. Because of this we assess the severity of these two very low.
+
+* Role member suggestion endpoint is reachable for unauthorized users [#4961](https://github.com/Icinga/icingaweb2/issues/4961)
+ This is more a case of missing authorization checks than a full fledged security flaw. But nevertheless,
+ it allows any logged-in user, by use of a manually crafted request, to retrieve the names of all available
+ users and usergroups.
+
+#### The More Usual Dose of Fixes
+
+* Browser print dialog result broken [#4957](https://github.com/Icinga/icingaweb2/issues/4957)
+ If you tried to export a view using the browser's builtin print dialog, (e.g. Ctrl+P) you may have
+ noticed a degradation of fanciness since the update to v2.10. This looks nicer than ever now.
+
+* Shared navigation items are not accessible [#4953](https://github.com/Icinga/icingaweb2/issues/4953)
+ Since v2.11.0 the shared navigation overview hasn't been accessible using the configuration menu.
+ It is now accessible again.
+
+* While using dropdown filter menu it gets closed automatically due to autorefresh [#4942](https://github.com/Icinga/icingaweb2/issues/4942)
+ Are you annoyed by the filter editor repeatedly closing the column selection while you're looking for
+ something? We have you covered with a fix for this and the column selection should stay open as long
+ as you don't click anywhere else.
+
+### What's New in Version 2.11.2
+
+You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/76?closed=1).
+
+It brings performance improvements and general fixes. Most notable of which are that having e.g. notifications
+disabled globally is now visible in the menu again and that the event history is grouped by days again.
+
+### What's New in Version 2.11.1
+
+You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/75?closed=1).
+
+This update's main focus is to solve the issue that all history views didn't work correctly or showed invalid
+time and dates. ([#4853](https://github.com/Icinga/icingaweb2/issues/4853))
+
+### What's New in Version 2.11.0
+
+You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/70?closed=1).
+
+#### Enhancements, Some
+
+Many of you were waiting for it: PHP 8.1 Support. This means that Icinga Web should be available soon on e.g.
+Ubuntu 22.04. You'll also notice that we changed the sidebar, as the user menu went to the very bottom of it.
+With it moved the less frequently used menu entries (system and configuration) to a section that pops up by
+hovering over the :gear: icon. We did that in order to prepare an area where we can add further functionality
+in the future. Oh, and announcements are now visible in fullscreen mode. :upside_down_face:
+
+* Support for PHP 8.1 [#4609](https://github.com/Icinga/icingaweb2/issues/4609)
+* Redesign User Menu [#4651](https://github.com/Icinga/icingaweb2/issues/4651)
+* &showFullscreen suppresses announcements [#4596](https://github.com/Icinga/icingaweb2/issues/4596)
+
+#### Fixes, More
+
+There are also bug fixes of course. The first mentioned here is one we fixed *accidentally*, as by adding support for
+PHP 8.1 we avoided a common PHP quirk responsible for it. If you have a host or service with an asterisk in the name,
+it will show up correctly in the detail view now. There was also a remaining issue with the theme mode selection in the
+user preferences which is fixed now.
+
+* Navigation item filter `*` not working [#4772](https://github.com/Icinga/icingaweb2/issues/4772)
+* Objects with a `*` in the name are not found [#4682](https://github.com/Icinga/icingaweb2/issues/4682)
+* Theme mode switch disabled on theme with mode support [#4744](https://github.com/Icinga/icingaweb2/issues/4744)
+
+#### When developers become cleaning maniacs
+
+Usually I write a short note at the start of release notes to make you read the upgrading documentation. This time
+however, a more prominent hint is required. We've removed so much (legacy) stuff, anyone tasked with upgrading is
+obliged to read [the upgrading documentation](https://icinga.com/docs/icinga-web-2/latest/doc/80-Upgrading/#upgrading-to-icinga-web-211x).
+The changes mentioned below only provide a glimpse at it.
+
+* User preferences in INI files not supported anymore [#4765](https://github.com/Icinga/icingaweb2/pull/4765)
+* mysql: use of utf8 vs utfmb4 [#4680](https://github.com/Icinga/icingaweb2/issues/4680)
+* Remove Vagrant file and its assets [#4762](https://github.com/Icinga/icingaweb2/pull/4762)
+
+### What's New in Version 2.10.1
+
+It's a rather small update this time without any critical bugs. :tada: So let's get straight to the fixes:
+
+* Clicking anywhere on a list item in the dashboard now opens the primary link again, instead of nothing [#4710](https://github.com/Icinga/icingaweb2/issues/4710)
+* The `Check Now` and `Remove Acknowledgement` quick actions in an object's detail header are now working again [#4711](https://github.com/Icinga/icingaweb2/issues/4711)
+* Clicking on the big number in the tactical overview if there are `UNKNOWN` services, shows `UNKNOWN` services now [#4714](https://github.com/Icinga/icingaweb2/issues/4714)
+* The contrast of text in the sidebar, while in light mode, has been increased [#4720](https://github.com/Icinga/icingaweb2/issues/4720)
+* A theme without mode support, which is set globally, now also prevents users from configuring the mode [#4723](https://github.com/Icinga/icingaweb2/issues/4723)
+
+### What's New in Version 2.10.0
+
+You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/63?closed=1).
+
+Please make sure to also check the respective [upgrading section](https://icinga.com/docs/icinga-web-2/latest/doc/80-Upgrading/#upgrading-to-icinga-web-2-210x)
+in the documentation.
+
+#### The Appearance of Dark and Light
+
+We have already spoken a lot about the [theme mode support](https://icinga.com/blog/2021/06/16/introducing-dark-and-light-theme-modes/)
+that we were working on [for some time](https://icinga.com/blog/2022/02/10/icinga-web-not-just-black-and-white/) now.
+It was planned for v2.9.0, but in respect of many modules and themes out there we gave it the deserved attention.
+Below is a glimpse of what this looks like.
+
+[![Icinga Web 2 Theme Mode Preview](https://icinga.com/wp-content/uploads/2022/03/theme-mode-demo-small.jpg "Icinga Web 2 Theme Mode Preview")](https://icinga.com/wp-content/uploads/2022/03/theme-mode-demo.jpg)
+
+#### Custom Variables Shown Unaltered – Or not
+
+Icinga Web 2 had some bad habits when displaying custom variables in the UI. We've driven out the last one regarding
+names now. Uppercase characters are now shown as such. What Icinga Web 2 stopped doing though, can now be accomplished
+by modules. A new hook that enables modules to influence the rendering of custom variables has been introduced.
+
+* CustomVarNames should not be converted to lowercase [#4639](https://github.com/Icinga/icingaweb2/issues/4639)
+* Display the Director Caption of a Custom Variable [#3479](https://github.com/Icinga/icingaweb2/issues/3479)
+
+#### Surprising Beauty in Exported Places
+
+Anyone who already attempted to export a list of services to PDF has seen the degradation of details in recent years.
+Be it images, icons, colors or the general layout. We simply reached a technical limit with the builtin PDF export.
+That is why we made [Icinga PDF Export](https://github.com/Icinga/icingaweb2-module-pdfexport). Icinga Web 2 has now
+a much enhanced compatibility with it. Exporting a list of services while Icinga PDF Export is set up, will now lead
+to a much better looking result.
+
+* Enhance PDF export [#4685](https://github.com/Icinga/icingaweb2/pull/4685)
+* Image not found when creating PDF view of objects [#4674](https://github.com/Icinga/icingaweb2/issues/4674)
+
+### What's New in Version 2.9.6
+
+**Notice**: This is a security release. It is recommended to upgrade immediately.
+
+#### Security Fixes
+
+This release includes three security related fixes. The first is a path traversal issue that affects installations
+of v2.9.0 and above. Another one allows admins to run arbitrary PHP code just by accessing the UI. The last one may
+disclose unwanted details to restricted users. Please check the advisories on GitHub for more details.
+
+* Path traversal in static library file requests for unauthenticated users [GHSA-5p3f-rh28-8frw](https://github.com/Icinga/icingaweb2/security/advisories/GHSA-5p3f-rh28-8frw)
+* SSH resources allow arbitrary code execution for authenticated users [GHSA-v9mv-h52f-7g63](https://github.com/Icinga/icingaweb2/security/advisories/GHSA-v9mv-h52f-7g63)
+* Unwanted disclosure of hosts and related data, linked to decommissioned services [GHSA-qcmg-vr56-x9wf](https://github.com/Icinga/icingaweb2/security/advisories/GHSA-qcmg-vr56-x9wf)
+
+### What's New in Version 2.9.5
+
+This is a hotfix release which fixes the following issues:
+
+* Some detail views of Icinga Director and other modules are broken with Web 2.9.4 [#4598](https://github.com/Icinga/icingaweb2/issues/4598)
+* Error on skipping LDAP Discovery [#4603](https://github.com/Icinga/icingaweb2/issues/4603)
+
+### What's New in Version 2.9.4
+
+You can also find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/68?closed=1).
+
+#### Broken Preference Configuration
+
+The preferences configuration broke with the release of v2.9 in some cases. Previously it was possible to access this
+and the general configuration without any configuration at all on disk. This is now possible again. The preferences of
+some users, which have a theme of a disabled module enabled, also showed an error. This doesn't happen anymore now.
+
+* Config/Preferences not accessible without config.ini [#4504](https://github.com/Icinga/icingaweb2/issues/4504)
+* "My Account" broken after Upgrade from 2.8.2 to 2.9.3 [#4512](https://github.com/Icinga/icingaweb2/issues/4512)
+
+#### Notable Fixes in the UI
+
+For a long time now, comments in lists had the bad habit to spread erratically if their content was large. They're
+limited to two lines now in lists and are still shown in full glory in their respective detail area. While talking
+of lines... Plugin output with subsequent empty lines erroneously showed only one of them. This is now fixed.
+
+* Proposal for new Feature make comments collapsible [#4515](https://github.com/Icinga/icingaweb2/issues/4515)
+* new line character is being removed in the plugin output [#4522](https://github.com/Icinga/icingaweb2/issues/4522)
+
+#### Less Notable But No Less Important Fixes
+
+We are actually very committed to provide a good experience for restricted users. So I'm happy to tell you that a nasty
+bug is fixed that resulted in the focus being lost randomly. Third party integrations are also important to us, hence
+I'm happy that this release fixes an issue where module specific JavaScript didn't load properly. Are you happy now?
+
+* `announcements` request clears focus [#4543](https://github.com/Icinga/icingaweb2/issues/4543)
+* js: Fix regression for loading dependent modules for sub-containers [#4533](https://github.com/Icinga/icingaweb2/issues/4533)
+
+### What's New in Version 2.9.3
+
+You can also find the issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/66?closed=1).
+
+#### Staying remembered on RHEL/CentOS 7 now possible
+
+RHEL/CentOS 7 still relies on OpenSSL v1.0.2 by default. A change in v2.9.1 resulted in an error in combination with
+this when ticking `Stay Logged In` during authentication. Staying logged in now works fine also on this platform.
+
+* Stay Logged In - Unknown cipher algorithm [#4493](https://github.com/Icinga/icingaweb2/issues/4493)
+
+#### Missing icons with SLES/OpenSUSE 15
+
+If you're running Icinga Web 2 Version 2.9.x on a SLES/OpenSUSE 15.x, you may have noticed some missing icons in the UI.
+This is due to a missing PHP extension `fileinfo`. By upgrading to this release using packages, this dependency will now
+be installed automatically.
+
+* Missing fileinfo php extension on SLES/OpenSUSE 15+ [#4503](https://github.com/Icinga/icingaweb2/issues/4503)
+
+#### Child downtimes for services are now removed automatically
+
+With Icinga v2.13, Icinga Web 2 will now make sure that service downtimes that were created automatically are also
+removed automatically. This will only work for downtimes you create with the `All Services` option after upgrading
+to this release. It will not work for downtimes created with earlier versions of Icinga Web 2.
+
+* If appropriate, set the API parameter all_services for schedule-downtime [#4501](https://github.com/Icinga/icingaweb2/pull/4501)
+
+### What's New in Version 2.9.2
+
+This is a hotfix release. v2.9.1 included a change that wasn't compatible with PostgreSQL again. This has been fixed
+in this release. (#4490)
+
+### What's New in Version 2.9.1
+
+You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/64?closed=1).
+
+Please make sure to also check the respective [upgrading section](https://icinga.com/docs/icinga-web-2/latest/doc/80-Upgrading/#upgrading-to-icinga-web-2-291)
+in the documentation.
+
+This release is accompanied by the minor releases v2.7.6 and v2.8.4 which include the fix for the flattened custom variables.
+
+#### Pancakes everywhere
+
+One of the security fixes included in v2.7.5, v2.8.3 and v2.9.0 went rampant and let you see similarities between custom
+variables and pancakes. These are gone now. Also, the login allowed some users to bake pancakes on their CPUs. However,
+we'd still recommend not to. What we do recommend, is to use graphical details to ease recognition. A pancake 🥞 in
+performance data labels for example.
+
+* Nested custom variables are flattened [#4439](https://github.com/Icinga/icingaweb2/issues/4439)
+* Disable login orb animation and all orbs for themes [#4468](https://github.com/Icinga/icingaweb2/pull/4468)
+* SVG chart library doesn't process input as UTF-8 [#4462](https://github.com/Icinga/icingaweb2/issues/4462)
+
+#### Staying remembered too difficult
+
+We all have sometimes difficulties remembering people we rarely meet. Especially obvious is this on those that slip
+through because they don't do the same things we do. With v2.9.0 this has happened for PostgreSQL, PHP v5.6-v7.0 and
+setup wizard users. Now they get their deserved attention, and Icinga Web 2 will remember them just like all others.
+
+* RememberMe not working with only PostgreSQL [#4441](https://github.com/Icinga/icingaweb2/issues/4441)
+* RememberMe compatibility with php version 5.6+ [#4472](https://github.com/Icinga/icingaweb2/pull/4472)
+* RememberMe fails after running the wizard for grants [#4434](https://github.com/Icinga/icingaweb2/issues/4434)
+
+#### Being picky pays off
+
+A custom datetime picker was introduced with v2.9.0. It had it's issues, but we didn't anticipate that much headwind.
+After careful reconsideration, we chose to only show the custom datetime picker for Firefox and IE users. Other browsers
+have their own capable enough native implementation which, in Chrome's case, may even be superior. If it is now used,
+it also closes automatically and doesn't swallow unrelated key presses.
+
+* Datetimepicker not usable by keyboard [#4442](https://github.com/Icinga/icingaweb2/issues/4442)
+* Close the datepicker automatically [#4461](https://github.com/Icinga/icingaweb2/issues/4461)
+* Paragraphs in Acknowledge/Downtime not possible [#4443](https://github.com/Icinga/icingaweb2/issues/4443)
+
+### What's New in Version 2.9.0
+
+You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/59?closed=1).
+
+Please make sure to also check the respective [upgrading section](https://icinga.com/docs/icinga-web-2/latest/doc/80-Upgrading/#upgrading-to-icinga-web-2-29x)
+in the documentation.
+
+This release is accompanied by the minor releases v2.7.5 and v2.8.3 which include the security fixes mentioned below.
+
+#### Icinga DB
+
+We continue our endeavour soon. Icinga Web 2 is still a crucial part of it and this update is again required
+for Icinga DB. If you like to participate again, don't forget to update Icinga Web 2 as well.
+
+#### Security Fixes
+
+This release includes two security related fixes. Both were published as part of a security advisory on Github.
+They allow the circumvention of custom variable protection rules and blacklists as well as a path traversal if
+the `doc` module is enabled. Please check the respective advisory for details.
+
+* Custom variable protection and blacklists can be circumvented [GHSA-2xv9-886q-p7xx](https://github.com/Icinga/icingaweb2/security/advisories/GHSA-2xv9-886q-p7xx)
+* Possible path traversal by use of the `doc` module [GHSA-cmgc-h4cx-3v43](https://github.com/Icinga/icingaweb2/security/advisories/GHSA-cmgc-h4cx-3v43)
+
+#### RBAC, The Elephant In Icinga Web 2
+
+Role Based Access Control, for the non-initiated. I'll make it short: Permission refusals, Role inheritance,
+Privilege Audit. Icinga DB will also solve the long-standing issue [#2455](https://github.com/Icinga/icingaweb2/issues/2455)
+and also allows [#3349](https://github.com/Icinga/icingaweb2/issues/3349) and [#3550](https://github.com/Icinga/icingaweb2/issues/3550).
+I've also written a blog post about this very topic: https://icinga.com/blog/2021/04/07/web-access-control-redefined/
+
+* Authorization enhancements [#4306](https://github.com/Icinga/icingaweb2/pull/4306)
+* Audit View [#4336](https://github.com/Icinga/icingaweb2/pull/4336)
+* Highlight modules with permissions set inside a role [#4241](https://github.com/Icinga/icingaweb2/issues/4241)
+
+#### Support for PHP 8
+
+PHP 8 is released and with Icinga Web 2.9 it will now (hopefully) work flawlessly. We also took the chance
+to prepare to drop the support of some legacy PHP versions. We now require PHP 7.3 at a minimum and all
+versions below that will not be supported anymore with the release of v2.11.
+
+* Support PHP 8 [#4289](https://github.com/Icinga/icingaweb2/pull/4289)
+* Raise minimum required PHP version to 7.3 [#4397](https://github.com/Icinga/icingaweb2/pull/4397)
+
+#### Stay, Be Remembered
+
+Have you ever been disappointed that Icinga Web 2 always forgets you after closing your browser? This is in
+your hands now! Just tick the new checkbox on the login screen and Icinga Web 2 doesn't forget your presence
+anymore. Unless of course the administrator or you on a different device clears your session.
+
+* Implement a "remember me" feature [#2495](https://github.com/Icinga/icingaweb2/issues/2495)
+
+#### It Does Matter, When
+
+Browsers are bad when it's about date and time inputs. (I'm looking at you Mozilla!) Now we've given our hopes
+up and use a specifically invented solution to show you a date and time picker throughout every browser. With
+Icinga v2.13 onwards you will also be able to use this when defining an expiry date for comments! Though, you
+might not necessarily use it that often once you've configured new custom defaults for downtime endings.
+
+* Add datetime picker widget [#4354](https://github.com/Icinga/icingaweb2/pull/4354)
+* Expire Option for Comments [#3447](https://github.com/Icinga/icingaweb2/issues/3447)
+* Custom defaults for downtime end, comment and duration [#4364](https://github.com/Icinga/icingaweb2/issues/4364)
+
+### What's New in Version 2.8.2
+
+**Notice**: This is a security release. It is recommended to immediately upgrade to this release.
+
+You can find all issues related to this release on the respective [milestone](https://github.com/Icinga/icingaweb2/milestone/62?closed=1).
+
+#### Path Traversal Vulnerability
+
+The vulnerability in question allows an attacker to access arbitrary files which are readable by the process running
+Icinga Web 2. Technical details can be found at the corresponding [CVE-2020-24368](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-24368)
+and in the issue below.
+
+* Possible path traversal when serving static image files [#4226](https://github.com/Icinga/icingaweb2/issues/4226)
+
+#### Broken Negated Filters with PostgreSQL
+
+We've also included a small non-security related fix. Searching for e.g. `servicegroup!=support` leads to an error
+instead of the desired result when using a PostgreSQL database.
+
+* Single negated membership filter fails with PostgreSQL [#4196](https://github.com/Icinga/icingaweb2/issues/4196)
+
+### What's New in Version 2.8.1
+
+You can find all issues related to this release on the respective [milestone](https://github.com/Icinga/icingaweb2/milestone/61?closed=1).
+
+#### Case Sensitivity Problems
+
+A fix in v2.8.0 led to users being not able to login if they got their username's case wrong. A hostgroup name's case
+has also been incorrectly taken into account despite using a `CI` labelled column in the servicegrid and other lists.
+
+* Login usernames now case sensitive in 2.8 [#4184](https://github.com/Icinga/icingaweb2/issues/4184)
+* Case insensitive hostgroup filter in service grid not working [#4178](https://github.com/Icinga/icingaweb2/issues/4178)
+
+#### Issues With Numbers
+
+An attempt to avoid misrepresenting environments in the tactical overview had an opposite effect by showing negative
+numbers. Filtering for timestamps in the event history also showed no results because our filters couldn't cope with
+plain numbers anymore.
+
+* Tactical overview showing "-1 pending" hosts [#4174](https://github.com/Icinga/icingaweb2/issues/4174)
+* Timestamp filters not working correctly in history views [#4182](https://github.com/Icinga/icingaweb2/issues/4182)
+
+### What's New in Version 2.8.0
+
+You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/60?closed=1).
+
+#### Icinga DB
+
+It's happening. Yes. Our latest achievement is now available for those who are willing to participate in this enormous
+endeavour. Icinga Web 2 is also a crucial part of it and accompanies the first release of Icinga DB. If you like
+to participate, don't forget to update Icinga Web 2 as well.
+
+#### Support for PHP 7.4 and MySQL 8
+
+We also made sure that you won't be disappointed by Icinga Web 2 if you're running PHP 7.4 or trying to access a MySQL
+database with version 8+. These should pose no issues anymore now. But if you still somehow managed to get issues
+please let us now and we'll fix it asap.
+
+* Exceptions with MySQL 8 [#3740](https://github.com/Icinga/icingaweb2/issues/3740)
+* Support for PHP 7.4 [#4009](https://github.com/Icinga/icingaweb2/issues/4009)
+
+#### Find What You Search For
+
+It's been previously not possible to properly filter for range values. This was especially true for custom variables
+where, if you searched for e.g. `_host_interfaces>=20`, you wouldn't find the correct results. If you often copy some
+values in our search fields you may also been a victim of extraneous spaces which are now automatically trimmed.
+
+* Filter: more/less than doesn't seem to working [#3974](https://github.com/Icinga/icingaweb2/issues/3974)
+* Search object followed by a space finds no results [#4002](https://github.com/Icinga/icingaweb2/issues/4002)
+
+#### Don't Leave Your Little Sheep Unattended
+
+It's time again to further restrict your users. It's now possible to completely block any access to contacts and
+contactgroups for specific roles. These won't ever see again who's notified and who's not. Also, if you are using
+single accounts for a group of people you can now disable password changes for those.
+
+* Prohibit access to contacts and contactgroups [#3973](https://github.com/Icinga/icingaweb2/issues/3973)
+* Allow to forbid password changes on specific user accounts [#3286](https://github.com/Icinga/icingaweb2/issues/3286)
+
+#### In and Out, Access Control Done Right
+
+While we have no burgers (but cookies!) you are nevertheless welcome to visit Icinga Web 2. And now you can also
+successfully leave while being externally authenticated and unsuccessfully enter while being unable to not add
+extraneous spaces to your username.
+
+* External logout not working from the navigation dashboard [#3995](https://github.com/Icinga/icingaweb2/issues/3995)
+* Username with extraneous spaces are not invalid [#4030](https://github.com/Icinga/icingaweb2/pull/4030)
+
+### Changes in Packaging and Dependencies
+
+Valid for distributions:
+
+* RHEL / CentOS 7
+ * Upgrade to PHP 7.3 via RedHat SCL
+ * See [Upgrading to Icinga Web 2 2.8.x](doc/80-Upgrading.md#upgrading-to-icinga-web-2-28x)
+ for manual steps that are required
+
+#### Discontinued Package Updates
+
+Icinga Web 2 v2.8+ is not supported on these platforms:
+
+* RHEL / CentOS 6
+* Debian 8 Jessie
+* Ubuntu 16.04 LTS (Xenial Xerus)
+
+Please consider an upgrade of your central Icinga system to a newer distribution release.
+
+[icinga.com](https://icinga.com/subscription/support-details/) provides an overview about
+currently supported distributions.
+
+### What's New in Version 2.7.3
+
+This is a hotfix release and fixes the following issue:
+
+* Servicegroups for roles with filtered objects not available [#3983](https://github.com/Icinga/icingaweb2/issues/3983)
+
+### What's New in Version 2.7.2
+
+You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/57?closed=1).
+
+#### Less Smoky Database Servers
+
+The release of v2.7.1 introduced a change which revealed an inefficient part of our database queries. We made some
+general optimizations on our queries and changed the way we utilize them in some views. The result are faster
+response times by less work for the database server.
+
+* Consuming more CPU resources since upgraded to 2.7.1 [#3928](https://github.com/Icinga/icingaweb2/issues/3928)
+
+#### Anarchism Infested Dashboards
+
+Recent history already showed signs of anarchism. (Pun intended) A similar mindset now infested default dashboards
+which appeared in a different way than before v2.7.0. We taught their dashlets a lesson and order has been reestablished
+as previously.
+
+* Recently Recovered Services in dashboard "Current Incidents" seems out of order [#3931](https://github.com/Icinga/icingaweb2/issues/3931)
+
+#### Solitary Downtimes
+
+We improved the host and service distinction with v2.7.0. The downtimes list however got confused by this and didn't
+knew anymore how to combine multiple downtimes. If you now instruct the list to select multiple downtimes this works
+again as we removed the confusing parts.
+
+* Selection of multiple downtimes fails [#3920](https://github.com/Icinga/icingaweb2/issues/3920)
+
+### What's New in Version 2.7.1
+
+You can find all issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/56?closed=1).
+
+#### Sneaky Solution for Sneaky Links
+
+Usually we try to include only bugs in minor-releases. Sorry, bug-fixes, of course. But thanks to
+[@winem_](https://twitter.com/winem_/status/1156531270521896960) we have also a little enhancement this time:
+Links in comments, notes, etc. are now [highlighted](https://github.com/Icinga/icingaweb2/pull/3893) as such.
+
+* Highlight links in the notes of an object [#3888](https://github.com/Icinga/icingaweb2/issues/3888)
+
+#### Nobody's Perfect, Not Even Developers
+
+We knew it. We saw it coming. And forgot about it. Some views, especially histories, showed an anarchic behavior
+since v2.7.0. The change responsible for this has been undone and history's order is reestablished now.
+
+* Default sort rules no longer work in 2.7.0 [#3891](https://github.com/Icinga/icingaweb2/issues/3891)
+
+#### Restrictions Gone ~~Wild~~ Cagey
+
+A [fix](https://github.com/Icinga/icingaweb2/pull/3868) unfortunately caused restrictions using wildcards to show no
+results anymore. This is now solved and such restrictions are as permissive as ever.
+
+* Wildcard filters in chains broken [#3886](https://github.com/Icinga/icingaweb2/issues/3886)
+
+### What's New in Version 2.7.0
+
+You can find issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/52?closed=1).
+
+#### Icinga's Amazingness Spreads Further
+
+All the Japanese and Ukrainian monitoring enthusiasts can now appreciate our web-frontend in their native tongue. Being
+so late to the party is also of their advantage, though. Because they can adjust their dashboard without worrying it gets
+broke with the next update. (All other admins with non-english users, please have a look at our
+[upgrading documentation](doc/80-Upgrading.md#upgrading-to-icinga-web-2-27x-))
+
+* Add Japanese language support [#3776](https://github.com/Icinga/icingaweb2/pull/3776)
+* Add Ukrainian language support [#3828](https://github.com/Icinga/icingaweb2/pull/3828)
+* Don't translate pane and dashlet names in configs [#3837](https://github.com/Icinga/icingaweb2/pull/3837)
+
+#### Modules - Bonus Functionality Unleashed
+
+With this release module developers got additional ways to customize Icinga Web 2. Whether you ever wanted to hook into
+a configuration form's handling, to perform your very own Ajax requests or enhance our multi-select views with fancy
+graphs. All is possible now.
+
+* Allow to hook into a configuration form's handling [#3862](https://github.com/Icinga/icingaweb2/pull/3862)
+* Allow to fully customize click and submit handling [#3794](https://github.com/Icinga/icingaweb2/issues/3767)
+* Integrate DetailviewExtension into multi-select views [#3304](https://github.com/Icinga/icingaweb2/pull/3304)
+
+#### UI - Your Daily Routine and Incident Management, Enhanced
+
+Users with color deficiencies now have a built-in theme to ease navigating within Icinga Web 2. Also, our forms got
+a long overdue re-design and now look less boring. Though, the best of all features is that clicking while holding
+the Ctrl-key now actually opens a new browser tab! Lost comments? No more. Defining an expiry date again? No more!
+
+* Add colorblind theme [#3743](https://github.com/Icinga/icingaweb2/pull/3743)
+* Improve the look of forms [#3416](https://github.com/Icinga/icingaweb2/issues/3416)
+* Make ctrl-click open new tab [#3723](https://github.com/Icinga/icingaweb2/pull/3723)
+
+#### Stay Focused - More Room for More Important Stuff
+
+Some of you know that some checks tend to produce walls of text or measure (too) many interfaces. Now, plugin output
+and performance data will collapse if they exceed a certain height. If necessary they can of course be expanded and
+keep that way across browser restarts. The same is also true for the sidebar. (Though, this one stays *collapsed*)
+
+* Persistent Collapsible Containers [#3638](https://github.com/Icinga/icingaweb2/pull/3638)
+* Collapsible plugin output [#3870](https://github.com/Icinga/icingaweb2/pull/3870)
+* Collapsed sidebar should stay collapsed [#3682](https://github.com/Icinga/icingaweb2/issues/3628)
+
+#### Markdown - Tables, Lists and Emphasized Text The Easy Way
+
+Since we now have the possibility to collapse large content dynamically, we allow you to add entire wiki pages to hosts
+and services. Though, if you prefer to use a real wiki to maintain those (what we'd strongly suggest) it's now easier
+than ever before to link to it. Copy url, paste url, submit comment, Done.
+
+* Make notes, comments and announcements markdown aware [#3814](https://github.com/Icinga/icingaweb2/pull/3814)
+* Transform any URL in a Comment to a clickable Link [#3441](https://github.com/Icinga/icingaweb2/issues/3441)
+* Support relative links in plugin output [#2916](https://github.com/Icinga/icingaweb2/issues/2916)
+
+#### Things You Have Missed Previously
+
+The tactical overview, our fancy pie charts, is now the very first result when you search something in the sidebar.
+If you'll see two entirely green circles there, relax. Also overdue or unreachable checks are now appropriately marked
+in list views and the service grid now allows you to switch between everything or problems only.
+
+* Add tactical overview to global search [#3845](https://github.com/Icinga/icingaweb2/pull/3845)
+* Servicegrid: Add toggle to show problems only [#3871](https://github.com/Icinga/icingaweb2/pull/3871)
+* Make overdue/unreachable checks better visible [#3860](https://github.com/Icinga/icingaweb2/pull/3860)
+
+#### Authorization - Knowing and Controlling What's Going On
+
+Roles can now be even more tailored to users since the introduction of a new placeholder. This placeholder allows to
+use a user's name in restrictions. Things like `_service_responsible_person=$user:local_name$` are now possible. The
+audit log now receives failed login-attempts, that's been made possible since hooks can now run for anonymous users.
+
+* Allow roles to filter for the currently logged in user [#3493](https://github.com/Icinga/icingaweb2/issues/3493)
+* Add possibility to disable permission checks for hooks [#3849](https://github.com/Icinga/icingaweb2/pull/3849)
+* Send failed login-attempts to the audit log [#3856](https://github.com/Icinga/icingaweb2/pull/3856)
+
+See also the [audit module](https://github.com/Icinga/icingaweb2-module-audit/releases) which got an update and is
+required for [#3856](https://github.com/Icinga/icingaweb2/pull/3856) to work.
+
+### What's New in Version 2.6.3
+
+You can find issues related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/54?closed=1).
+
+#### PHP 7.3
+
+Now supported. :tada:
+
+#### LDAP - Community contributions, that's the spirit
+
+With the help of our users we've finally fixed the issue that defining multiple hostnames and enabling STARTTLS has
+never properly worked. Also, they've identified that defining multiple hostnames caused a customized port not being
+utilized and fixed it themselves.
+
+There has also a rare case been fixed that caused no group members being found in case object classes had a different
+casing than what we expected. (Good news for all the non-OpenLdap and non-MSActiveDirectory users)
+
+* LDAP connection fails with multiple servers using STARTTLS [#3639](https://github.com/Icinga/icingaweb2/issues/3639)
+* LDAPS authentication ignores custom port setting [#3713](https://github.com/Icinga/icingaweb2/issues/3713)
+* LDAP group members not found [#3650](https://github.com/Icinga/icingaweb2/issues/3650)
+
+#### We take care about your data even better now
+
+With this are newlines and HTML entities (such as `&nbsp;`) in plugin output and custom variables meant.
+Sorry if I've teased some data security folks now. :innocent:
+
+* Newlines in plugin output disappear [#3662](https://github.com/Icinga/icingaweb2/issues/3662)
+* Windows path separators are converted to newlines in custom variables [#3636](https://github.com/Icinga/icingaweb2/issues/3636)
+* HTML entities in plugin output are not resolved if no other HTML is there [#3707](https://github.com/Icinga/icingaweb2/issues/3707)
+
+#### You've wondered how you got into a famous blue police box?
+
+Don't worry, not only you and the european union are sometimes unsure what's the correct time.
+
+* Set client timezone on DB connection [#3525](https://github.com/Icinga/icingaweb2/issues/3525)
+* Ensure a valid default timezone is set in any case [#3747](https://github.com/Icinga/icingaweb2/pull/3747)
+* Fix that the event detail view is not showing times in correct timezone [#3660](https://github.com/Icinga/icingaweb2/pull/3660)
+
+#### UI - The portal to your monitoring environment, improved
+
+The collapsible sidebar introduced with v2.5 has been plagued by some issues since then. They're now fixed. Also,
+the UI should now flicker less and properly preserve the scroll position when interacting with action links. (This
+also allows the business process module to behave more stable when using drag and drop in large configurations.)
+
+* Collapsible Sidebar Issues [#3187](https://github.com/Icinga/icingaweb2/issues/3187)
+* Fix title when closing right column [#3654](https://github.com/Icinga/icingaweb2/issues/3654)
+* Preserve scroll position upon form submits [#3661](https://github.com/Icinga/icingaweb2/pull/3661)
+
+#### Corrected things we've broke recently
+
+That's due to preemptive changes to protect you from bad individuals. Unfortunately this meant that some unforeseen
+side-effects appeared after the release of v2.6.2. These are now fixed.
+
+* Multiline values in ini files broken [#3705](https://github.com/Icinga/icingaweb2/issues/3705)
+* PHP ini parser doesn't strip trailing whitespace [#3733](https://github.com/Icinga/icingaweb2/issues/3733)
+* Escaped characters in INI values are not unescaped [#3648](https://github.com/Icinga/icingaweb2/issues/3648)
+
+Though, if you've faced issue [#3705](https://github.com/Icinga/icingaweb2/issues/3705) you still need to take manual
+action (if not already done) as the provided fix does only prevent further occurrences of the resulting error. The
+required changes involve the transformation of all real newlines in Icinga Web 2's INI files to literal `\n` or `\r\n`
+sequences. (Files likely having such are the `roles.ini` and `announcements.ini`)
+
+### What's New in Version 2.6.2
+
+You can find issues and features related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/53?closed=1).
+
+This bugfix release addresses the following topics:
+
+* Database connections to MySQL 8 no longer fail
+* LDAP connections now have a timeout configuration which defaults to 5 seconds
+* User groups are now correctly loaded for externally authenticated users
+* Filters are respected for all links in the host and service group overviews
+* Fixed permission problems where host and service actions provided by modules were missing
+* Fixed an SQL error in the contact list view when filtering for host groups
+* Fixed time zone (DST) detection
+* Fixed the contact details view if restrictions are active
+* Doc parser and documentation fixes
+
+### What's New in Version 2.6.1
+
+You can find issues and features related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/51?closed=1).
+
+The command audit now logs a command's payload as JSON which fixes a
+[bug](https://github.com/Icinga/icingaweb2/issues/3535) that has been introduced in version 2.6.0.
+
+### What's New in Version 2.6.0
+
+You can find issues and features related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/48?closed=1).
+
+#### Enabling you to do stuff you couldn't before
+
+* Support for PHP 7.2 added
+* Support for SQLite resources added
+* Login and Command (monitoring) auditing added with the help of a dedicated [module](https://github.com/Icinga/icingaweb2-module-audit)
+* Pluginoutput rendering is now hookable by modules which allows to render custom icons, emojis and .. cute kitties :octocat:
+
+#### Avoiding that you miss something
+
+* It's now possible to toggle between list- and grid-mode for the host- and servicegroup overviews
+* The servicegrid now supports to flip its axes which allows it to be put into a [landscape mode](https://github.com/Icinga/icingaweb2/pull/3449#issue-185415579)
+* Contacts only associated with services are visible now when restricted based on host filters
+* Negated and combined membership filters now work as expected ([#2934](https://github.com/Icinga/icingaweb2/issues/2934))
+* A more prominent error message in case the monitoring backend goes down
+* The filter editor doesn't get cleared anymore upon hitting Enter
+
+#### Making your life a bit easier
+
+* The tactical overview is now filterable and can be safely put into [the dashboard](https://github.com/Icinga/icingaweb2/pull/3446#issue-185379142)
+* It is now possible to register new announcements over the [REST Api](https://github.com/Icinga/icingaweb2/issues/2749#issuecomment-279667189)
+* Filtering for custom variables now works in UTF8 environments
+
+#### Ensuring you understand everything
+
+* The monitoring health is now beautiful to look at and properly behaves in [narrow environments](https://github.com/Icinga/icingaweb2/pull/3515#issue-200075373)
+* Updated German localization
+* Updated Italian localization
+
+#### Freeing you from unrealiable things
+
+* Removed support for PHP < 5.6
+* Removed support for persistent database connections
+
+### What's New in Version 2.5.3
+
+You can find issues and features related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/50?closed=1).
+
+#### Fixes
+
+A fix for an issue introduced with v2.5.2 that prevented service-only contacts from appearing in the UI resulted in long
+database response times and has been reverted.
+
+### What's New in Version 2.5.2
+
+You can find issues and features related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/49?closed=1).
+
+#### UI Changes
+
+The sidebar's search behaviour has been changed so that it does only react to user-input after the user stopped typing.
+Also, the cursor does not jump to the end of form-inputs anymore in case of an auto-refresh. We've also fixed an issue
+that caused [custom icons](https://github.com/Icinga/icingaweb2/issues/3181#issuecomment-378875462) to be inverted when
+placed in the sidebar. Last but not least, the header now expands its width beyond the 3840px mark and single dashlets
+do not show a horizontal scrollbar anymore.
+
+#### PHP7 MSSQL Compatibility
+
+Support for Microsoft's `sqlsrv` extension has been added. Also, it's now possible to setup MSSQL resources in the
+front-end using the `dblib` extension.
+
+#### Proper Error Responses
+
+An issue introduced with v2.5.1 has been resolved where some errors (especially HTTP 404 Not Found) were masked
+by another subsequent error.
+
+#### Broken LDAP Group Memberships
+
+An issue introduced with v2.5.1 has been resolved where users with a domain in their name were not associated with any
+LDAP groups.
+
+#### Monitoring Module
+
+Issuing a check using the "Check Now" action now properly causes a check being made by Icinga 2 even if outside the
+timeperiod. (Note: This issue was only present if using the Icinga 2 Api as command transport.)
+
+#### Login/Logout Expandability
+
+It's now possible for modules to provide hooks for the user authorization. This for example allows to transparently
+authenticate users in third-party applications such as [Grafana](https://github.com/Icinga/icingaweb2/pull/3401#issue-178030542).
+
+### What's New in Version 2.5.1
+
+You can find issues and features related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/47?closed=1).
+
+Besides many other bug fixes, Icinga Web 2 v2.5.1 fixes an issue where it was no longer possible to filter by host
+custom variables in service related views. Also, this release introduces detail views for the event history and
+improved upgrading docs. Furthermore, this version censors sensitive information (e.g. LDAP passwords) in exception
+stack traces.
+
+### What's New in Version 2.5.0
+
+You can find issues and features related to this release on our [Roadmap](https://github.com/Icinga/icingaweb2/milestone/45?closed=1).
+
+#### Raised PHP Version Dependency
+
+Icinga Web 2 now requires at least PHP 5.6.
+
+#### UI Changes
+
+The style of the login screen and menu have been changed. Also, the menu of Icinga Web 2 is now collapsible.
+Browser tabs will not auto-refresh if they are inactive. Users are now allowed to change the default pagination limit
+via their preferences.
+
+#### Domain-aware Authentication for Active Directory and LDAP Backends
+
+If there are multiple AD/LDAP authentication backends with distinct domains, you are now able to make Icinga Web 2
+aware of the domains. This can be done by configuring each AD/LDAP backend's domain. You can also use the GUI for this
+purpose. Please read our [documentation](doc/05-Authentication.md#domain-aware-auth) for more information about this
+feature.
+
+#### Changes in Packaging and Dependencies
+
+Valid for distributions:
+
+* RHEL / CentOS 6 + 7
+ * Upgrading to PHP 7.0 / 7.1 via RedHat SCL (new dependency)
+ * See [Upgrading to FPM](doc/02-Installation.md#upgrading-to-fpm) for manual steps that are required
+* SUSE SLE 12
+ * Upgrading PHP to >= 5.6.0 via the alternative packages.
+ You might have to confirm the replacement of PHP < 5.6 - but that should work with any other PHP app as well
+ * Make sure to enable the new Apache module `a2enmod php7` and restart `apache2`
+
+#### Discontinued Package Updates
+
+For the following distributions Icinga Web 2 won't be updated past 2.4.x anymore:
+
+* Debian 7 wheezy
+* Ubuntu 14.04 LTS (trusty)
+* SUSE SLE 11 (all service packs)
+
+Please think about replacing your central Icinga system to a newer distribution release.
+
+Also see [packages.icinga.com](https://packages.icinga.com) for the currently supported distributions.
+
+### What's New in Version 2.4.2
+
+#### Bugfixes
+
+* Bug 2965: Transport config: Default port not changing upon auto-submit
+* Bug 2926: Wrong order when sorting by host_severity
+* Bug 2923: Number fields should be valid when empty
+* Bug 2919: Fix cached loading of module config
+* Bug 2911: Acknowledgements are not working without an expiry time
+* Bug 2878: process-check-result Button is visible even when user isn't allowed to use it
+* Bug 2850: Link to acknowledgements is wrong in the timeline
+* Bug 2841: Wrong menu height when switching back from mobile layout
+* Bug 2806: Wrong service state count in hostgroup overview
+* Bug 2805: Response from the Icinga 2 API w/ an empty result set leads to exception
+* Bug 2801: Wrong help text for the director in the icingacli
+* Bug 2784: Module and gravatar images are not served with their proper MIME type
+* Bug 2776: Defaults not respected when acknowledging problems
+* Bug 2767: Monitoring module: Config field protected vars not updated after zeroing config.ini
+* Bug 2728: Gracefully handle invalid Icinga 2 API response types
+* Bug 2718: Hide check attempt for hard states in history views
+* Bug 2716: Web 2 doesn't detect the browser time zone if the time zone offset is negative
+* Bug 2714: icingacli module disable fails on consecutive calls
+* Bug 2695: Macros cannot be used for a navigation item's url-port
+* Bug 2684: [dev.icinga.com #14027] Translation module should not write absolute path to .po files
+* Bug 2683: [dev.icinga.com #14025] Translation module should remove temp files
+* Bug 2661: [dev.icinga.com #13651] Don't offer the Icinga 2 API as transport if PHP cURL is missing
+* Bug 2660: [dev.icinga.com #13649] Make the Icinga 2 API the default command transport
+* Bug 2656: [dev.icinga.com #13627] Wrong count of handled critical service in the hover text
+* Bug 2645: [dev.icinga.com #13539] Improve error handling and validation of multiple LDAP URIs
+* Bug 2598: [dev.icinga.com #12977] Adding an empty user backend fails
+* Bug 2545: [dev.icinga.com #12640] MSSQL ressource not working
+* Bug 2523: [dev.icinga.com #12410] Click on Host in Service Grid can cause "Invalid Filter" error
+* Bug 2519: [dev.icinga.com #12330] Filter editor may show wrong values after searching
+* Bug 2509: [dev.icinga.com #12295] group_name_attribute should be "sAMAccountName" by default
+
+### What's New in Version 2.4.1
+
+Our public repositories and issue tracker have been migrated to GitHub.
+
+#### Bugfixes
+
+* Bug 2651: [dev.icinga.com #13607] Displayed times messed up in Icinga Web 2.4.0 w/ PostgreSQL
+* Bug 2654: [dev.icinga.com #13615] Setup wizard: Not possible to setup Icinga Web 2 with an external database
+* Bug 2663: [dev.icinga.com #13691] Hook::all() is broken on CLI
+* Bug 2669: [dev.icinga.com #13735] Setup wizard: Progress bar isn't shown correctly, if setup is at finish step
+* Bug 2681: [dev.icinga.com #13957] Support failover API command transport configuration
+* Bug 2686: Granular module permissions do not work for hooks
+* Bug 2687: Update URLs to icinga.com, remove wiki & update to GitHub
+
+### What's New in Version 2.4.0
+
+#### Feature
+
+* Feature 12598 (Authentication & Authorization): Support nested AD groups for Roles and not just login
+* Feature 11809 (Authentication & Authorization): Test and document multiple LDAP-URIs separated by space in LDAP ressources
+* Feature 10616 (Authentication & Authorization): Users w/o administrative permissions should be allowed to change their password
+* Feature 13381 (CLI): Allow to configure the default listen address for the CLI command web serve
+* Feature 11820 (Configuration): Check whether chosen locale is available
+* Feature 11214 (Configuration): Logger: Allow to configure the Syslog Facility
+* Feature 13117 (Framework): Add charset UTF-8 to default content type
+* Feature 12634 (Framework): Possibitlity to fold and unfold filter by click
+* Feature 11198 (Framework): Announce banner
+* Feature 11115 (Framework): Add SSL support to MySQL database resources
+* Feature 8270 (Installation): Add SELinux policy for Icinga Web 2
+* Feature 13187 (Monitoring): Command toolbar in the host and service detail views
+* Feature 12873 (Monitoring): Change default for sticky option of acknowledgements from true to false
+* Feature 12820 (Monitoring): Export detail views to JSON
+* Feature 12766 (Monitoring): Show flapping events in the host and service history views
+* Feature 12764 (Monitoring): Display downtime end even if it hasn't been started yet
+* Feature 12125 (Monitoring): Allow th in plugin output
+* Feature 11952 (Monitoring): Allow changing default of 'sticky' in acknowledgement and other command options
+* Feature 11398 (Monitoring): Send commands over Icinga 2's API
+* Feature 11835 (UI): Add clear button to search field
+* Feature 11792 (UI): Show hint if notifications are disabled globally
+* Feature 11664 (UI): Show git HEAD for modules if available
+* Feature 13461 (Vendor Libraries): Use Icinga's fork of Zend Framework 1 icingaweb2-vendor-zf1
+
+#### Bugfixes
+
+* Bug 12396 (Authentication & Authorization): Hooks don't respect module permissions
+* Bug 12164 (Authentication & Authorization): REDIRECT_REMOTE_USER not evaluated during external auth
+* Bug 12108 (Authentication & Authorization): assertPermission allows everything for unauthenticated requests
+* Bug 13357 (Configuration): Persistent database resources cannot be made non-persistent
+* Bug 12848 (Configuration): Empty "Protected Custom Variables" falls back to defaults
+* Bug 12655 (Configuration): Permission application/log is not configurable
+* Bug 12170 (Configuration): Adding a DB resource via webinterface requires one to enter a password
+* Bug 10401 (Configuration): LdapUserGroupBackendForm: user_* settings not purged
+* Bug 9804 (Configuration): Renaming the resource used for the config backend does not update the global configuration
+* Bug 11920 (Dashboard): Add to dashboard: wrong url makes whole dashboard unusable
+* Bug 13387 (Documentation): Can't display documentation of disabled modules
+* Bug 12923 (Framework): Navigation Item name must be of type string or NavigationItem
+* Bug 12852 (Framework): Hosts without any services are hidden from roles with monitoring/filter/objects set
+* Bug 12760 (Framework): Do not log exceptions other than those resulting in a HTTP 500 status-code
+* Bug 12583 (Framework): Unhandled exceptions while handling REST requests will silently drop the http response code
+* Bug 12580 (Framework): REST requests cannot be anonymous
+* Bug 12557 (Framework): Module description cannot be on a single line
+* Bug 12299 (Framework): FilterExpression renders a&!b as a=1&b!=1
+* Bug 12161 (Framework): Icinga Web 2 doesn't set Content-Type
+* Bug 12065 (Framework): IniRepository: update/delete not possible with iterator
+* Bug 11743 (Framework): INI writer must not persist section keys with a null value
+* Bug 11185 (Framework): SummaryNavigationItemRenderer should show worst state
+* Bug 10361 (Framework): Handle E_RECOVERABLE_ERROR
+* Bug 13459 (Installation): Setup: Can't view monitoring config summary with Icinga 2 API as command transport
+* Bug 13467 (JavaScript): renderLayout has side-effects
+* Bug 13115 (JavaScript): actiontable should not clear active row in case there is no newer one
+* Bug 12541 (JavaScript): Menu not reloaded in case no search is available
+* Bug 12328 (JavaScript): Separate vendor JavaScript libraries w/ semicolons and newlines on import
+* Bug 10704 (JavaScript): JS: Always use the jQuery find method w/ node context when selecting elements
+* Bug 10703 (JavaScript): JS: Don't use var self = this, but var _this = this
+* Bug 11431 (Modules): Modules can't require permission on menu items
+* Bug 10870 (Modules): Refuse erroneous module folder names when enabling the module
+* Bug 13243 (Monitoring): Inconsistent host and service flags
+* Bug 12889 (Monitoring): Timeline broken
+* Bug 12810 (Monitoring): Scheduling a downtime for all services of a host does not work w/ the Icinga 2 API as command transport
+* Bug 12313 (Monitoring): Multi-line strings within host.notes are being displayed as single line
+* Bug 12223 (Monitoring): State not highlighted in plugin output if it contains HTML
+* Bug 12019 (Monitoring): Contact view shows service filters with 'Downtime' even if not set
+* Bug 11915 (Monitoring): Performance data: negative values not handled
+* Bug 11859 (Monitoring): Can't separate between SOFT and HARD states in the history views
+* Bug 11766 (Monitoring): Performance data: Fit label column to show as much text as possible
+* Bug 11744 (Monitoring): Empty user groups are not displayed
+* Bug 10774 (Monitoring): Scheduling downtimes for child hosts doesn't work w/ Icinga 2.x (waiting for Icinga 2)
+* Bug 10537 (Monitoring): Filtering with not-equal on custom variable doesn't show hosts without this cv
+* Bug 7755 (Monitoring): Remove autosubmit in eventgrid
+* Bug 12133 (Navigation): Username and password not being passed in navigation item URLs
+* Bug 12776 (Print & Export): dompdf fails when border-style is set to auto
+* Bug 12723 (Print & Export): Allowed memory size exhausted when exporting the history view to CSV
+* Bug 12660 (QA): Choosing the Icinga theme floods the log with error messages
+* Bug 12774 (UI): Lot's of <span style="visibility:hidden; display:none;"></span> in Output
+* Bug 12134 (UI): Copy and paste: Plugin output contains unicode zero-width space characters
+* Bug 10691 (UI): Closing the detail area does not update the rows selected counter
+* Bug 13095 (Vagrant VM): TicketSalt constant missing
+* Bug 12717 (Vagrant VM): PluginContribDir constant removed during vagrant provisioning
+
+### What's New in Version 2.3.4/2.3.3
+
+#### Bugfixes
+
+* Bug 11267: Links in plugin output don't behave as expected
+* Bug 11348: Host aliases are not shown in detail area
+* Bug 11728: First non whitespace character after comma stripped from plugin output
+* Bug 11729: Sort by severity depends on state type
+* Bug 11737: Zero width space characters destroy state highlighting in plugin output
+* Bug 11796: Zero width space characters may destroy links in plugin output
+* Bug 11831: module.info parsing fails in case it contains newlines that are not part of the module's description
+* Bug 11850: "Add to menu" tab unnecessarily appears in command forms
+* Bug 11871: Colors used in the timeline are not accessible
+* Bug 11883: Delete action on comments and downtimes in list views not accessible because they lack context
+* Bug 11885: Database: Asterisk filters ignored when combined w/ other filters
+* Bug 11910: Web 2 lacks mobile meta tags
+* Fix remote code execution via remote command transport
+
+### What's New in Version 2.3.2
+
+#### Feature
+
+* Feature 11629: Simplified event-history date and time representation
+
+#### Bugfixes
+
+* Fix a privilege escalation issue in the monitoring module for authenticated users
+* Bug 10486: Menu rendering fails when no monitoring backend was configured
+* Bug 10847: Warn about illogical dates
+* Bug 10848: Can't change items per page if filter is in modify state
+* Bug 11392: Can't configure monitoring backend via the web interface when no monitoring backend was configured
+
+### What's New in Version 2.3.1
+
+#### Bugfixes
+
+* Bug 11598: Invalid SQL queries for PostgreSQL
+
+### What's New in Version 2.3.0
+
+#### Features
+
+* Feature 10887: lib: Provide User::getRoles()
+* Feature 10965: Roles: Restrict visibility of custom variables
+* Feature 11404: Add is_reachable filter column to host and service data views
+* Feature 11485: lib/LDAP: Support scopes base and one
+* Feature 11495: Support data URIs in href
+* Feature 11529: Don't offer command disable notifications /w expire time if backend is Icinga 2
+
+#### Bugfixes
+
+* Bug 9386: Improve order of documentation chapters
+* Bug 10820: Style problems with long plugin output lines
+* Bug 11078: Can't remove default dashboards
+* Bug 11099: Mobile menu icon is mispositioned
+* Bug 11128: Menu stops refreshing when there is text in the search field
+* Bug 11145: Pagination compontents should not float around
+* Bug 11171: Icinga Web 2 tries to load an ifont which results in 404
+* Bug 11245: icingacli monitoring list --problems throws an exception
+* Bug 11264: Cannot execute queries while other unbuffered queries are active
+* Bug 11277: external auth with PHP internal webserver still buggy
+* Bug 11279: Restrict access to Applicationlog
+* Bug 11299: Icon images no longer prepend img/icons
+* Bug 11391: External auth reads REMOTE_USER from process environment instead of request
+* Bug 11414: Doc module does not render images with relative path
+* Bug 11465: Stylesheet remains unchanged when module CSS/LESS files have been changed
+* Bug 11489: lib/LDAP: ordering does explicitly set fields
+* Bug 11490: lib/LDAP: LdapUtils::explodeDN replace deprecated use of eval in preg_replace
+* Bug 11516: Accessibility: Focus in Tactical Overview barely visible
+* Bug 11558: Missing ) in the documentation
+* Bug 11568: Docs: Global permissions table is broken
+
+### What's New in Version 2.2.0
+
+#### Features
+
+* Feature 8487: Number headings in the documentation module
+* Feature 8963: Feature commands in the multi select views
+* Feature 10654: Render links in acknowledgements, comments and downtimes
+* Feature 11062: Allow style classes in plugin output
+* Feature 11238: Puppet/Vagrant: Install mod_ssl and forward port 443
+
+#### Bugfixes
+
+* Bug 7350: Tabs are missing if JS is disabled
+* Bug 9800: Debian packaging: Ship translation module w/ the icingaweb2 package and install its config.ini
+* Bug 10173: Failed commands give no useful error any more
+* Bug 10251: Icinga Web 2 fails to run with PHP7
+* Bug 10277: Special characters are incorrectly escaped for tooltips in the service grid
+* Bug 10289: Doc module: Headers are cut off when clicking on TOC links
+* Bug 10309: Move auth backend configuration to app config
+* Bug 10310: Monitoring details: information/action ordering
+* Bug 10362: Debian packaging: Separate package for CLI missing
+* Bug 10366: Text plugin output treated as HTML in too many occasions
+* Bug 10369: Accessibility: Focus not visible and lost after refresh
+* Bug 10397: Users with no permissions can check multiple services
+* Bug 10442: Edit user control should be more prominent
+* Bug 10469: "Remove Acknowledgement" text missing in multi-select views
+* Bug 10506: HTTP basic auth request is sent when using Kerberos authentication with Apache2 and mod_php
+* Bug 10625: Return local date and time when lost connection to the web server
+* Bug 10640: Respect protected_variables in nested custom variables too
+* Bug 10778: Filters in the host group and service group overview not applied to state links
+* Bug 10786: Whitespace characters are ignored in the plugin output in list views
+* Bug 10805: Setup Wizard: Obsolete PHP sockets requirement
+* Bug 10856: Benchmark is not rendered on many pages
+* Bug 10871: Get rid of padding in controls
+* Bug 10878: Dashboards different depending on username casing
+* Bug 10881: Move iframe from modules to framework
+* Bug 10917: Event grid tiles: The filter column "from" is not allowed here
+* Bug 10918: Error on logout when using external authentication
+* Bug 10921: icingacli monitoring list --format=csv throws error
+* Bug 11000: Change license header to only reflect a file's year of creation/initial commit
+* Bug 11008: Wobbling spinners
+* Bug 11021: Global default theme is not applied while not authenticated
+* Bug 11032: Fix icon_image size and provide a CSS class for theming
+* Bug 11039: Misleading tooltip in Tactical Overview
+* Bug 11051: Preferences and navigation items stored in INI files rely on case sensitive usernames
+* Bug 11073: Active row is flickering on refresh
+* Bug 11091: Custom navigation items: URL is not escaped/encoded
+* Bug 11100: Comments are always persistent
+* Bug 11114: Validate that a proper root DN is set for LDAP resources
+* Bug 11117: Vendor: Update dompdf to version 0.6.2
+* Bug 11119: icingacli shows ugly exception when unable to access the config directory
+* Bug 11120: icingacli: command and action shortcuts have been broken
+* Bug 11126: Invalid cookie value in cookie icingaweb2-tzo
+* Bug 11142: LDAP User Groups backend group_filter
+* Bug 11143: Layout: Tabs should be left-aligned
+* Bug 11151: Having basic authentication on the webserver but not in Icinga Web 2 causes Web 2 to require basic auth
+* Bug 11168: Debian packaging: Don't patch HTMLPurifier loading and install HTMLPurifier*.php files from the library/vendor root
+* Bug 11187: Session cookie: Path too broad and unset secure flag on HTTPS
+* Bug 11197: Menu items without url should ignore the target configuration
+* Bug 11260: Scheduling downtimes through the API not working
+
+### What's New in Version 2.1.1
+
+#### Features
+
+* Feature 10488: Use _ENV variables with built-in PHP webserver
+* Feature 10705: Theming
+* Feature 10898: Winter theme
+
+#### Bugfixes
+
+* Bug 9685: Deprecate Module::registerHook() in favor of Hook::provideHook()
+* Bug 9957: Sort hosts and services by last state change
+* Bug 10123: CSS loading may fail w/ mkdir(): File exists in FileCache.php
+* Bug 10126: setup config directory --config should use mkdir -p instead of mkdir()
+* Bug 10166: library/vendor/HTMLPurifier tree is incorrectly unpacked
+* Bug 10170: Link to service downtimes from multiple selected services includes host downtimes aswell
+* Bug 10338: Debian: Failed to open stream HTMLPurifier/HTMLPurifier.php
+* Bug 10603: Line breaks are not respected in acknowledgements, comments and downtimes
+* Bug 10658: SUSE packages have the wrong dependencies
+* Bug 10659: LDAP group members are shown with their DN and membership registration does not work
+* Bug 10670: State not highlighted in plugin output
+* Bug 10671: Auto-focus the username field on the login page
+* Bug 10683: lib/CLI command web serve: rename variable basedir to something meaningful
+* Bug 10702: Host- and Service-Actions configured in Web 2 do not resolve any macros
+* Bug 10749: XHR application-state requests pollute the URL if not authenticated
+* Bug 10771: Login shows "Anmelden........" upon login with the german locale
+* Bug 10781: LoggingConfigForm.php complains about whitespace but checks with /^[^\W]+$/
+* Bug 10790: "Problems - Service Grid" does not work with host names that contain only digits
+* Bug 10884: Tabs MUST throw an exception when activating an inexistant tab
+* Bug 10886: "impacted" container is no longer fading out
+* Bug 10892: Wrong mask for FileCache's temp directory
+
+### What's New in Version 2.1.0
+
+#### Features
+
+* Feature 10613: Extend and simplify Hook api
+
+#### Bugfixes
+
+* Bug 8713: Invalid filter "host_name=*(test)*", unexpected ) at pos 17
+* Bug 8999: Navigation and search bar is not available using a small width
+* Bug 10229: Dashboard requests do not refresh the session
+* Bug 10268: Unhandled services in the hosts overview list don't stand out
+* Bug 10287: Redirect after login no longer working
+* Bug 10288: The order for the limit links is incorrect
+* Bug 10292: Hovered links in hover menu are unreadable
+* Bug 10293: Hover menu is missing it's arrow for menu entries providing badges
+* Bug 10295: Reset static line-height on body
+* Bug 10296: Scrolling to the bottom of the page does not load more events
+* Bug 10299: Badges are overridden by menu text
+* Bug 10301: Format helpers like timeSince are polluted with text-small
+* Bug 10303: Zooming in, or having another layout destroys the hover menu
+* Bug 10304: Cannot access a host's customvars for service actions
+* Bug 10305: Hover menu arrow color no longer fits background color
+* Bug 10316: Not all Servicegroups / Hostgroups are shown
+* Bug 10317: Event history style broken
+* Bug 10319: Recursive sharing navigation items doesn't work.
+* Bug 10321: Module iframe doesn't show website with parameters as a single column
+* Bug 10328: ZendFramework packages missing for SLES12
+* Bug 10359: Charset option not passed thru PDO adapter
+* Bug 10364: PostgreSQL queries apply LOWER() on selected columns
+* Bug 10367: Broken user- and group-management
+* Bug 10389: Host overview: vsprintf(): Too few arguments
+* Bug 10402: LdapUserGroupBackend: user_base_dn not used from UserBackend
+* Bug 10419: Swapped icon image order in service header
+* Bug 10490: Unhandled service counter in the hosts overview shows incorrect values
+* Bug 10533: Form notifications of type information are green
+* Bug 10567: Member user name used for basedn when querying usergroup members
+* Bug 10597: Empty PDO charset option is invalid
+* Bug 10614: Class loader: hardcode module and Zend prefixes
+* Bug 10623: Acknowledging multiple selected objects erroneous
+
+### What's New in Version 2.0.0
+
+#### Changes
+
+
+Upgrading to Icinga Web 2 2.0.0
+
+Icinga Web 2 installations from package on RHEL/CentOS 7 now depend on php-ZendFramework which is available through the EPEL repository. Before, Zend was installed as Icinga Web 2 vendor library through the package icingaweb2-vendor-zend. After upgrading, please make sure to remove the package icingaweb2-vendor-zend.
+
+Icinga Web 2 version 2.0.0 requires permissions for accessing modules. Those permissions are automatically generated for each installed module in the format module/<moduleName>. Administrators have to grant the module permissions to users and/or user groups in the roles configuration for permitting access to specific modules. In addition, restrictions provided by modules are now configurable for each installed module too. Before, a module had to be enabled before having the possibility to configure restrictions.
+
+The instances.ini configuration file provided by the monitoring module has been renamed to commandtransports.ini. The content and location of the file remains unchanged.
+
+The location of a user's preferences has been changed from config-dir/preferences/username.ini to config-dir/preferences/username/config.ini. The content of the file remains unchanged.
+
+#### Features
+
+* Feature 5600: User specific menu entries
+* Feature 5647: GUI for permission and restriction assignment
+* Feature 5786: Namespace all web controllers
+* Feature 6144: Provide additional dashboard panes per default
+* Feature 6677: Allow to extend the content of a dashlet on the right
+* Feature 7180: Show active cluster hostname in the monitoring health view
+* Feature 7367: GUI for adding action and notes URLs
+* Feature 7570: Document installation
+* Feature 7773: Interpret links in custom variables
+* Feature 8336: IDO: Double check that we always add the is_active = 1 condition in our queries
+* Feature 8369: Show an indicator when automatic form submission is ongoing
+* Feature 8378: Indicate when check results are being late
+* Feature 8407: Document example commands for installing from source
+* Feature 8642: Show acknowledgement expire time (if any) in the host and service detail view
+* Feature 8645: Generic iFrame module
+* Feature 8758: Add support for file uploads
+* Feature 8848: Show activity indicator for dashlets
+* Feature 8884: Move the menu entry for notifications beneath history
+* Feature 8981: Combo backend for command transports (fallback mechanism)
+* Feature 8985: Visually separate enabled and disabled modules in the modules view
+* Feature 9029: Provide a complete list of available filter columns plus custom variables (where appropriate) in the filter editor
+* Feature 9030: Service grid: Add limit control
+* Feature 9247: Show Icinga Web 2's version in the frontend
+* Feature 9364: Apply sort rules for ldap queries on the server's side
+* Feature 9381: List installed modules, versions and state in the about page
+* Feature 9453: Vagrant: Upgrade to CentOS 7
+* Feature 9460: IDO resource configuration: Ensure that the user is running PostgreSQL 9.1+
+* Feature 9524: Improve setup wizard
+* Feature 9525: Configuration enhancements
+* Feature 9591: IP Address Search
+* Feature 9604: Add Inspection API for Connections
+* Feature 9605: LDAP Connection add Test Function
+* Feature 9630: Inspectable: Add inspectable API to LDAP connections
+* Feature 9641: Add Inspection API for DB Connections
+* Feature 9644: Permit access to modules
+* Feature 9645: Support for address6
+* Feature 9651: Automatically use the correct instance configuration based on a host's or service's instance
+* Feature 9660: Basic access authentication
+* Feature 9661: Query for limit+1 for "Show more results" candidates
+* Feature 9683: Allow to create MSSQL and Oracle DB resources
+* Feature 9702: Allow module developers to define additional static files
+* Feature 9761: Store active menu item as HTML5 history state information
+* Feature 9772: Allow to list groups from a LDAP backend
+* Feature 9826: Allow to select text in the host and service detail area header via double click
+* Feature 9830: Monitoring: Support the wildcard restriction for "administrative" roles
+* Feature 9888: Display a host's and service's check timeperiod as well as notification timeperiod in the detail view
+* Feature 9908: Use better icons for resources, backends and module state
+* Feature 9942: Add a warning to the navigition if the last IDO update is older than 5 minutes
+* Feature 9943: Offer instance_name as query column
+* Feature 9945: Show instance_name in a host's and service's detail view
+* Feature 10033: Provide "Counter"-View
+
+#### Bugfixes
+
+* Bug 6644: Default sort order is not applied
+* Bug 7383: This webpage has a redirect loop without cookies
+* Bug 7486: Instance Configuration: Instance must NOT be a GET parameter when creating an instance
+* Bug 7488: Instance Configuration: Instance parameter must be mandatory for updating and removing instances
+* Bug 7489: Instance Configuration: Custom validation errors must be shown in the form not as notification
+* Bug 7490: Instance Configuration: HTTP response code flaws
+* Bug 7818: Incorrect language & timezone detection w/ Safari
+* Bug 7930: Hide external commands which are not supported by Icinga 2
+* Bug 8312: Don't show last and next check information and schedule check controls for passive only checks
+* Bug 8620: Searching in the downtimes list view throws an exception
+* Bug 8623: Selected row lost after auto-refresh in every overview except for hosts and services
+* Bug 8703: Do not show computer accounts for Active Directory
+* Bug 8768: Range multiselection not working in IE11
+* Bug 8845: Missing downtime end information in host and service detail views
+* Bug 8954: Document and rename Ldap\Connection to Ldap\LdapConnection
+* Bug 8955: Document and rename Ldap\Query to Ldap\LdapQuery
+* Bug 8969: Tooltips hidden after auto refresh
+* Bug 8975: Error messages disappear after auto refresh #2
+* Bug 8983: Remove yellow boxes from forms and wherever else used
+* Bug 9024: Form autosubmits cause autorefreshs to not run anymore
+* Bug 9036: Plugin output HTML tags are always escaped
+* Bug 9042: Browser address bar gets not updated when closing the detail area while a request for the url that has just been closed is pending
+* Bug 9054: Multiselection not visible until a subsequent auto-refresh has been completed
+* Bug 9168: Can't use Icinga Web 2 w/ IDO version 1.7
+* Bug 9179: LDAP discovery relies on anonymous access and does not respect encryption
+* Bug 9266: Downtimes show "Starts in" for objects with non-problem state
+* Bug 9306: Installation Wizard complains about "required and must not be empty"-fields when the user changes the database type first
+* Bug 9314: RPM packages do not require Zend PDO packages which results in missing 'php-pdo' exception
+* Bug 9330: Uncaught TypeError: Cannot read property 'id' of undefined when deleting comments or downtimes via their respective overview
+* Bug 9333: Sorting the service grid by service description fails w/ PostgreSQL
+* Bug 9346: Potential active rows not deselected when navigating by browser history
+* Bug 9347: Service names with round bracket fail w/ innvalid filter exception when selecting multiple services
+* Bug 9348: LDAP filter input errors w/ "The filter must not be wrapped in parantheses"
+* Bug 9349: Duplicate headers from Controller::postDispatch()
+* Bug 9360: service matrix does not show all intersections
+* Bug 9374: Non-existent modules can be disabled
+* Bug 9375: Fatal error in icingacli (icingacli-2.0.0-3.beta3.el7.centos.noarch)
+* Bug 9376: INI writer must not persist section keys with a null value
+* Bug 9398: Rename menu "authentication" to "security"
+* Bug 9402: A command form's view script cannot be found if benchmark is enabled
+* Bug 9418: DB resources: Do not allow to configure table prefixes
+* Bug 9421: Sort controls misbehavior
+* Bug 9449: The use statement with non-compound name ... has no effect w/ PHP 5.6.9+
+* Bug 9454: Ghost host- and servicegroups
+* Bug 9472: Fetch object statistics only if they're actually displayed
+* Bug 9473: Inconsistent counters for service problems
+* Bug 9477: Command forms have no tabs
+* Bug 9483: Icinga\Web\Widget\Paginator should not require a full query interface
+* Bug 9484: Document that the web server has to be restarted after adding the web server user to the icingaweb2 system group
+* Bug 9494: Refresh button loads invalid links for views with complex filters
+* Bug 9497: Eventhistory: Quick search not working
+* Bug 9498: Service overview: Cannot quick search for hosts
+* Bug 9499: Hostgroup overview: Cannot quick search for hosts
+* Bug 9500: Servicegroup overview: Cannot quick search for services
+* Bug 9502: Comment overview: Cannot quick search
+* Bug 9503: Comment overview shows duplicate entries when filtering for services
+* Bug 9504: Contactgroup overview: Cannot quick search
+* Bug 9505: Contact overview: Cannot quick search
+* Bug 9506: Notification overview: Cannot quick search
+* Bug 9509: Setup: Authentication backend validation broken
+* Bug 9511: Setup: Cannot select an existing user as admin account when I've configured an authentication backend of type msldap
+* Bug 9516: Improve request processing for all monitoring config forms
+* Bug 9517: Behave nicely in case no monitoring instance resources are configured
+* Bug 9519: Monitoring backend configuration does not validate IDO resources
+* Bug 9529: RPM: Apache config ist not defined as configuration file
+* Bug 9530: Creating a dashlet with "()" in dashboard title affects all dashboards
+* Bug 9538: Use display_name for host and service names in the service grid
+* Bug 9553: User- and Group-Management broken on PHP > 5.3
+* Bug 9572: Cannot remove a user group from a MariaDB backend
+* Bug 9573: Selecting multiple services not working while being restricted
+* Bug 9574: Multiviews do not only display the chosen objects but everything, if a restriction is active
+* Bug 9582: icon_image does not allow to use an icon from our ifont
+* Bug 9597: Clicking on the row of a service notification will show the host
+* Bug 9607: Ignoring LDAP connection certificate errors does not have any effect
+* Bug 9608: LDAP connection must fail when the configured encryption is not possible
+* Bug 9611: generictts integration fails if regular expression is empty
+* Bug 9615: Hardcoded PHP and gettext tools path
+* Bug 9616: Security config form shows no tabs
+* Bug 9626: Tactical overview does not auto-refresh
+* Bug 9633: Icinga\Cli\Command is unable to detect exact action names
+* Bug 9646: If a CLI command fails, crucial exception information missing w/o --trace
+* Bug 9668: Browser history issues
+* Bug 9672: Invalid host passive check result state: unreachable
+* Bug 9674: Don't show comment(s) of acknowledgement(s) in the comment list of a host or service but next to whether the host or service problem is acknowledged
+* Bug 9687: @import rules not working in a module's module.less
+* Bug 9688: Icinga Web 2 ignores Cache-Control:no-cache
+* Bug 9692: Can't filter for custom variables
+* Bug 9694: Lib: Weird interface for creating problem menu entries
+* Bug 9695: IDO: Empty programstatus table not indicated as problem in the menu
+* Bug 9696: Logged exceptions for custom menu item renderers are missing crucial exception information
+* Bug 9719: Monitoring backend validation cannot be skipped
+* Bug 9739: DbUserBackend inspection unsuccessful for backends with just a single user
+* Bug 9751: Bad performance for quick searches
+* Bug 9765: instances.ini: transport is undocumented
+* Bug 9787: It's not possible to use Unix socket to connect to PostgreSQL
+* Bug 9790: Do not suggest to enable modules if it's not possible
+* Bug 9815: Multiview detail: controls have wrong link target
+* Bug 9817: Documentation: Required parameter 'chapter' missing
+* Bug 9819: JS Behaviors: Selection not updated when using multi detail controls
+* Bug 9828: Wrong count for queries having a group by clause
+* Bug 9837: Documentation: Don't suggest to install icingacli on Debian
+* Bug 9844: url anchors not working if a column hash (#!) is also part of the url
+* Bug 9869: A module's rendered event is not called upon initialization
+* Bug 9892: Module styles not visible for anonymous users
+* Bug 9901: Use the DN to fetch group memberships from LDAP
+* Bug 9932: Url to extend the timeline is pushed to history
+* Bug 9954: PostgreSQL queries use LOWER(...) for non-collated columns which have a collated counterpart
+* Bug 9955: PostgreSQL queries ordered by collated columns don't use LOWER
+* Bug 9956: Unnecessary GROUP BY clauses
+* Bug 9959: Authentication documentation suggests outdated backend identifier "ad"
+* Bug 9963: Service history is disordered and shows service and host history
+* Bug 9965: format=json does not respect the filter objects
+* Bug 9971: Seleting multiple objects at once doesn't work anymore
+* Bug 9995: "Show More" links broken in the Alert Summary
+* Bug 9998: Can't use custom variables as restriction filter
+* Bug 10009: Prettify page layout when accessing a non-existent route while not being authenticated
+* Bug 10016: config/* does not permit access to the application and authentication configuration
+* Bug 10025: Filter, submitting form via keyboard doesn't work on chrome
+* Bug 10031: Navigation by history is broken
+* Bug 10046: Menu is somehow confusing top/sub-level entries
+* Bug 10082: Adding an entry to a menu section influences it's position
+* Bug 10150: IniParser should unescape escaped sections automatically
+* Bug 10151: Do not validate section names in forms
+* Bug 10155: Multiselection disapperears when issuing commands
+* Bug 10160: Notifications/Alert Summary: Grouping errors w/ PostgreSQL
+* Bug 10163: Search for hostname does not work in snapshot release
+* Bug 10169: Multiselect URLs broken where base url != /icingaweb2
+* Bug 10172: Customvar filters are mostly broken, completely for Icinga 1.x
+* Bug 10218: Notes URL isn't showing properly
+* Bug 10236: notes_url and action_url target is always icinga.domain.de
+* Bug 10246: Use a separate configuration file for each type of navigation item
+* Bug 10263: Forms with target=_next remain unusable after first submission
+
+### What's New in Version 2.0.0-rc1
+
+#### Changes
+
+* Improve layout and look and feel in many ways
+* Apply host, service and custom variable restrictions to all monitoring objects
+* Add fullscreen mode (?showFullscreen)
+* User and group management
+* Comment and Downtime Detail View
+* Show icon_image in host/service views
+* Show Icinga program version in monitoring health
+
+#### Features
+
+* Feature 4139: Notify monitoring backend availability problems
+* Feature 4498: Allow to add columns to monitoring views via URL
+* Feature 6392: Resolve Icinga 2 runtime macros in action and notes URLs
+* Feature 6729: Fullscreen mode
+* Feature 7343: Fetch user groups from LDAP
+* Feature 7595: Remote connection resource configuration
+* Feature 7614: Right-align icons
+* Feature 7651: Add module information (module.info) to all core modules
+* Feature 8054: Host Groups should list number of hosts (as well as services)
+* Feature 8235: Show host and service notes in the host and service detail view
+* Feature 8247: Move notifications to the bottom of the page
+* Feature 8281: Improve layout of comments and downtimes in the host and service detail views
+* Feature 8310: Improve layout of performance data and check statistics in the host and service detail views
+* Feature 8565: Improve look and feel of the monitoring multi-select views
+* Feature 8613: IDO queries related to concrete objects should not depend on collations
+* Feature 8665: Show icon_image in the host and service detail views
+* Feature 8781: Automatically deselect rows when closing the detail area
+* Feature 8826: User and group management
+* Feature 8849: Show only three (or four) significant digits (e.g. in check execution time)
+* Feature 8877: Allow module developers to implement new/custom authentication methods
+* Feature 8886: Require mandatory parameters in controller actions and CLI commands
+* Feature 8902: Downtime detail view
+* Feature 8903: Comment detail view
+* Feature 9009: Apply host and service restrictions to related views as well
+* Feature 9203: Wizard: Validate that a resource is actually an IDO instance
+* Feature 9207: Show icinga program version in Monitoring Health
+* Feature 9223: Show the active ido endpoint in the monitoring health view
+* Feature 9284: Create a ServiceActionsHook
+* Feature 9300: Support icon_image_alt
+* Feature 9361: Refine UI for RC1
+* Feature 9377: Permission and restriction documentation
+* Feature 9379: Provide an about.md
+
+#### Bugfixes
+
+* Bug 6281: ShowController's hostAction() and serviceAction() do not respond with 400 for invalid/missing parameters and with 404 if the host or service wasn't found
+* Bug 6778: Duration and history time formatting isn't correct
+* Bug 6952: Unauthenticated users are provided helpful error messages
+* Bug 7151: Play nice with form-button-double-clickers
+* Bug 7165: Invalid host address leads to exception w/ PostgreSQL
+* Bug 7447: Commands sent over SSH are missing the -i option when using a ssh user aside from the webserver's user
+* Bug 7491: Switching from MySQL to PostgreSQL and vice versa doesn't change the port in the resource configuration
+* Bug 7642: Monitoring menu renderers should be moved to the monitoring module
+* Bug 7658: MenuItemRenderer is not so easy to extend
+* Bug 7876: Not all views can be added to the dashboard w/o breaking the layout
+* Bug 7931: Can't acknowledge multiple selected services which are in downtime
+* Bug 7997: Service-Detail-View tabs are changing their context when clicking the Host-Tab
+* Bug 7998: Navigating to the Services-Tab in the Service-Detail-View displays only the selected service
+* Bug 8006: Beautify command transport error exceptions
+* Bug 8205: List views should not show more than the five worst pies
+* Bug 8241: Take display_name into account when searching for host and service names
+* Bug 8334: Perfdata details partially hidden depending on the resolution
+* Bug 8339: Lib: SimpleQuery::paginate() must not fetch page and limit from request but use them from parameters
+* Bug 8343: Status summary does not respect restrictions
+* Bug 8363: Updating dashlets corrupts their URLs
+* Bug 8453: The filter column "_dev" is not allowed here
+* Bug 8472: Missing support for command line arguments in the format --arg=<value>
+* Bug 8474: Improve layout of dictionaries in the host and service detail views
+* Bug 8624: Delete multiple downtimes and comments at once
+* Bug 8696: Can't search for Icinga 2 custom variables
+* Bug 8705: Show all shell commands required to get ready in the setup wizard
+* Bug 8706: INI files should end with a newline character and should not contain superfluous newlines
+* Bug 8707: Wizard: setup seems to fail with just one DB user
+* Bug 8711: JS is logging "ugly" side exceptions
+* Bug 8731: Apply host restrictions to service views
+* Bug 8744: Performance data metrics with value 0 are not displayed
+* Bug 8747: Icinga 2 boolean variables not shown in the host and service detail views
+* Bug 8777: Server error: Service not found exception when service name begins or ends with whitespaces
+* Bug 8815: Only the first external command is sent over SSH when submitting commands for multiple selected hosts or services
+* Bug 8847: Missing indication that nothing was found in the docs when searching
+* Bug 8860: Host group view calculates states from service states; but states should be calculated from host states instead
+* Bug 8927: Tactical overview does not respect restrictions
+* Bug 8928: Host and service groups views do not respect restrictions
+* Bug 8929: Setup wizard does not validate whether the PostgreSQL user for creating the database owns the CREATE ROLE system privilege
+* Bug 8930: Error message about refused connection to the PostgreSQL database server displayed twice in the setup wizard
+* Bug 8934: Status text for ok/up becomes white when hovered
+* Bug 8941: Long plugin output makes the whole container horizontally scrollable instead of just the row containing the long plugin output
+* Bug 8950: Improve English for "The last one occured %s ago"
+* Bug 8953: LDAP encryption settings have no effect
+* Bug 8956: Can't login when creating the database connection for the preferences store fails
+* Bug 8957: Fall back on syslog if the logger's type directive is misconfigured
+* Bug 8958: Switching LDAP encryption to LDAPS doesn't change the port in the resource configuration
+* Bug 8960: Remove exclamation mark from the notification "Authentication order updated!"
+* Bug 8966: Show custom variables visually separated in the host and service detail views
+* Bug 8967: Remove right petrol border from plugin output in the host and service detail views
+* Bug 8972: Can't view Icinga Web 2's log file
+* Bug 8994: Uncaught exception on empty session.save_path()
+* Bug 9000: Only the first line of a stack trace is shown in the applications log view
+* Bug 9007: Misspelled host and service names in commands are not accepted by icinga
+* Bug 9008: Notification overview does not respect restrictions
+* Bug 9022: Browser title does not change in case of an error
+* Bug 9023: Toggling feature...
+* Bug 9025: A tooltip of the service grid's x-axe makes it difficult to click the title of the currently hovered column
+* Bug 9026: Add To Dashboard ... on the dashboard
+* Bug 9046: Detail View: Downtimes description misses space between duration and comment text
+* Bug 9056: Filter for host/servicegroup search doesn't work anymore
+* Bug 9057: contact_notify_host_timeperiod
+* Bug 9059: Can't initiate an ascending sort by host or service severity
+* Bug 9198: monitoring/command/feature/object does not grant the correct permissions
+* Bug 9202: The config\* permission does not permit to navigate to the configuration
+* Bug 9211: Empty filters are being rendered to SQL which leads to syntax errors
+* Bug 9214: Detect multitple icinga_instances entries and warn the user
+* Bug 9220: Centralize submission and apply handling of sort rules
+* Bug 9224: Allow anonymous LDAP binding
+* Bug 9281: Problem with Icingaweb 2 after PHP Upgrade 5.6.8 -> 5.6.9
+* Bug 9317: Web 2's ListController inherits from the monitoring module's base controller
+* Bug 9319: Downtimes overview does not respect restrictions
+* Bug 9350: Menu disappears in user group management view
+* Bug 9351: Timeline links are broken
+* Bug 9352: User list should be sorted
+* Bug 9353: Searching for users fails, at least with LDAP backend
+* Bug 9355: msldap seems not to be a first-class citizen
+* Bug 9378: Rpm calls usermod w/ invalid option on openSUSE
+* Bug 9384: Timeline+Role problem
+* Bug 9392: Command links seem to be broken
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ecbc059
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5f89e53
--- /dev/null
+++ b/README.md
@@ -0,0 +1,56 @@
+# Icinga Web 2
+
+[![PHP Support](https://img.shields.io/badge/php-%3E%3D%207.2-777BB4?logo=PHP)](https://php.net/)
+![Build Status](https://github.com/icinga/icingaweb2/workflows/PHP%20Tests/badge.svg?branch=main)
+[![Github Tag](https://img.shields.io/github/tag/Icinga/icingaweb2.svg)](https://github.com/Icinga/icingaweb2)
+
+![Icinga Logo](https://icinga.com/wp-content/uploads/2014/06/icinga_logo.png)
+
+1. [About](#about)
+2. [License](#license)
+3. [Installation](#installation)
+4. [Documentation](#documentation)
+5. [Support](#support)
+6. [Contributing](#contributing)
+
+## About
+
+**Icinga Web 2** is the next generation open source monitoring web interface, framework
+and command-line interface developed by the [Icinga Project](https://icinga.com/), supporting Icinga 2,
+Icinga Core and any other monitoring backend compatible with the IDO database.
+
+![Icinga Web 2 Monitoring Module with Graphite](doc/res/monitoring-module-preview.png "Icinga Web 2 Monitoring Module with Graphite")
+
+## License
+
+Icinga Web 2 and the Icinga Web 2 documentation are licensed under the terms of the GNU
+General Public License Version 2, you will find a copy of this license in the
+COPYING file included in the source package.
+
+## Installation
+
+For installing Icinga Web 2 please check the [installation chapter](https://icinga.com/docs/icingaweb2/latest/doc/02-Installation/)
+in the documentation.
+
+## Documentation
+
+The documentation is located in the [doc/](doc/) directory and also available
+on [icinga.com/docs](https://icinga.com/docs/icingaweb2/latest/).
+
+## Support
+
+Check the [project website](https://icinga.com) for status updates. Join the
+[community channels](https://icinga.com/community/) for questions
+or ask an Icinga partner for [professional support](https://icinga.com/support/).
+
+## Contributing
+
+There are many ways to contribute to Icinga -- whether it be sending patches,
+testing, reporting bugs, or reviewing and updating the documentation. Every
+contribution is appreciated!
+
+Please continue reading in the [contributing chapter](CONTRIBUTING.md).
+
+### Security Issues
+
+For reporting security issues please visit [this page](https://icinga.com/contact/security/).
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..31283ae
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,9 @@
+# Security Policy
+
+## Supported Versions
+
+The latest two minors. If it's a critical threat and the latest minor is just a few weeks old, the third latest minor may also get an update.
+
+## Reporting a Vulnerability
+
+Please head [here](https://icinga.com/company/contact/security-issues/).
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..ba6ff6b
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+v2.12.1
diff --git a/application/VERSION b/application/VERSION
new file mode 100644
index 0000000..d963627
--- /dev/null
+++ b/application/VERSION
@@ -0,0 +1 @@
+cd2daeb2cb8537c633d343a29eb76c54cd2ebbf2 2023-11-15 12:50:13 +0100
diff --git a/application/clicommands/AutocompleteCommand.php b/application/clicommands/AutocompleteCommand.php
new file mode 100644
index 0000000..34e4005
--- /dev/null
+++ b/application/clicommands/AutocompleteCommand.php
@@ -0,0 +1,120 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Clicommands;
+
+use Icinga\Cli\Command;
+use Icinga\Cli\Loader;
+
+/**
+ * Autocomplete for modules, commands and actions
+ *
+ * The autocomplete command shows help for a given command, module and also for a
+ * given module's command or a specific command's action.
+ *
+ * Usage: icingacli autocomplete [<module>] [<command> [<action>]]
+ */
+class AutocompleteCommand extends Command
+{
+ protected $defaultActionName = 'complete';
+
+ protected function suggest($suggestions)
+ {
+ if ($suggestions) {
+ $key = array_search('autocomplete', $suggestions);
+ if ($key !== false) {
+ unset($suggestions[$key]);
+ }
+ echo implode("\n", $suggestions)
+ //. serialize($GLOBALS['argv'])
+ . "\n";
+ }
+ }
+
+ /**
+ * Show help for modules, commands and actions [default]
+ *
+ * The help command shows help for a given command, module and also for a
+ * given module's command or a specific command's action.
+ *
+ * Usage: icingacli autocomplete [<module>] [<command> [<action>]]
+ */
+ public function completeAction()
+ {
+ $module = null;
+ $command = null;
+ $action = null;
+
+ $loader = new Loader($this->app);
+ $params = $this->params;
+ $bare_params = $GLOBALS['argv'];
+ $cword = (int) $params->shift('autoindex');
+
+ $search_word = $bare_params[$cword];
+ if ($search_word === '--') {
+ // TODO: Unfinished, completion missing
+ return $this->suggest(array('--verbose', '--help', '--debug'));
+ }
+
+ $search = $params->shift();
+ if (!$search) {
+ return $this->suggest(
+ array_merge($loader->listCommands(), $loader->listModules())
+ );
+ }
+ $found = $loader->resolveName($search);
+ if ($found) {
+ // Do not return suggestions if we are already on the next word:
+ if ($bare_params[$cword] === $search) {
+ return $this->suggest(array($found));
+ }
+ } else {
+ return $this->suggest($loader->getLastSuggestions());
+ }
+
+ $obj = null;
+ if ($loader->hasCommand($found)) {
+ $command = $found;
+ $obj = $loader->getCommandInstance($command);
+ } elseif ($loader->hasModule($found)) {
+ $module = $found;
+ $search = $params->shift();
+ if (! $search) {
+ return $this->suggest(
+ $loader->listModuleCommands($module)
+ );
+ }
+ $command = $loader->resolveModuleCommandName($found, $search);
+ if ($command) {
+ // Do not return suggestions if we are already on the next word:
+ if ($bare_params[$cword] === $search) {
+ return $this->suggest(array($command));
+ }
+ $obj = $loader->getModuleCommandInstance(
+ $module,
+ $command
+ );
+ } else {
+ return $this->suggest($loader->getLastSuggestions());
+ }
+ }
+
+ if ($obj !== null) {
+ $search = $params->shift();
+ if (! $search) {
+ return $this->suggest($obj->listActions());
+ }
+ $action = $loader->resolveObjectActionName(
+ $obj,
+ $search
+ );
+ if ($action) {
+ if ($bare_params[$cword] === $search) {
+ return $this->suggest(array($action));
+ }
+ } else {
+ return $this->suggest($loader->getLastSuggestions());
+ }
+ }
+ }
+}
diff --git a/application/clicommands/HelpCommand.php b/application/clicommands/HelpCommand.php
new file mode 100644
index 0000000..a863eb4
--- /dev/null
+++ b/application/clicommands/HelpCommand.php
@@ -0,0 +1,43 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Clicommands;
+
+use Icinga\Cli\Command;
+use Icinga\Cli\Loader;
+use Icinga\Cli\Documentation;
+
+/**
+ * Help for modules, commands and actions
+ *
+ * The help command shows help for a given command, module and also for a
+ * given module's command or a specific command's action.
+ *
+ * Usage: icingacli help [<module>] [<command> [<action>]]
+ */
+class HelpCommand extends Command
+{
+ protected $defaultActionName = 'show';
+
+ /**
+ * Show help for modules, commands and actions [default]
+ *
+ * The help command shows help for a given command, module and also for a
+ * given module's command or a specific command's action.
+ *
+ * Usage: icingacli help [<module>] [<command> [<action>]]
+ */
+ public function showAction()
+ {
+ $module = null;
+ $command = null;
+ $action = null;
+ $loader = new Loader($this->app);
+ $loader->parseParams();
+ echo $this->docs()->usage(
+ $loader->getModuleName(),
+ $loader->getCommandName(),
+ $loader->getActionName()
+ );
+ }
+}
diff --git a/application/clicommands/ModuleCommand.php b/application/clicommands/ModuleCommand.php
new file mode 100644
index 0000000..fc42167
--- /dev/null
+++ b/application/clicommands/ModuleCommand.php
@@ -0,0 +1,228 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Clicommands;
+
+use Icinga\Application\Logger;
+use Icinga\Application\Modules\Manager;
+use Icinga\Cli\Command;
+
+/**
+ * List and handle modules
+ *
+ * The module command allows you to handle your IcingaWeb modules
+ *
+ * Usage: icingacli module [<action>] [<modulename>]
+ */
+class ModuleCommand extends Command
+{
+ /**
+ * @var Manager
+ */
+ protected $modules;
+
+ public function init()
+ {
+ $this->modules = $this->app->getModuleManager();
+ }
+
+ /**
+ * List all enabled modules
+ *
+ * If you are interested in all installed modules pass 'installed' (or
+ * even --installed) as a command parameter. If you enable --verbose even
+ * more details will be shown
+ *
+ * Usage: icingacli module list [installed] [--verbose]
+ */
+ public function listAction()
+ {
+ if ($type = $this->params->shift()) {
+ if (! in_array($type, array('enabled', 'installed'))) {
+ return $this->showUsage();
+ }
+ } else {
+ $type = 'enabled';
+ $this->params->shift('enabled');
+ if ($this->params->shift('installed')) {
+ $type = 'installed';
+ }
+ }
+
+ if ($this->hasRemainingParams()) {
+ return $this->showUsage();
+ }
+
+ if ($type === 'enabled') {
+ $modules = $this->modules->listEnabledModules();
+ } else {
+ $modules = $this->modules->listInstalledModules();
+ }
+ if (empty($modules)) {
+ echo "There are no $type modules\n";
+ return;
+ }
+ if ($this->isVerbose) {
+ printf("%-14s %-9s %-9s DIRECTORY\n", 'MODULE', 'VERSION', 'STATE');
+ } else {
+ printf("%-14s %-9s %-9s %s\n", 'MODULE', 'VERSION', 'STATE', 'DESCRIPTION');
+ }
+ foreach ($modules as $module) {
+ $mod = $this->modules->loadModule($module)->getModule($module);
+ if ($this->isVerbose) {
+ $dir = ' ' . $this->modules->getModuleDir($module);
+ } else {
+ $dir = $mod->getTitle();
+ }
+ printf(
+ "%-14s %-9s %-9s %s\n",
+ $module,
+ $mod->getVersion(),
+ ($type === 'enabled' || $this->modules->hasEnabled($module))
+ ? $this->modules->hasInstalled($module) ? 'enabled' : 'dangling'
+ : 'disabled',
+ $dir
+ );
+ }
+ echo "\n";
+ }
+
+ /**
+ * Enable a given module
+ *
+ * Usage: icingacli module enable <module-name>
+ */
+ public function enableAction()
+ {
+ if (! $module = $this->params->shift()) {
+ $module = $this->params->shift('module');
+ }
+
+ if (! $module || $this->hasRemainingParams()) {
+ return $this->showUsage();
+ }
+
+ $this->modules->enableModule($module, true);
+ }
+
+ /**
+ * Disable a given module
+ *
+ * Usage: icingacli module disable <module-name>
+ */
+ public function disableAction()
+ {
+ if (! $module = $this->params->shift()) {
+ $module = $this->params->shift('module');
+ }
+ if (! $module || $this->hasRemainingParams()) {
+ return $this->showUsage();
+ }
+
+ if ($this->modules->hasEnabled($module)) {
+ $this->modules->disableModule($module);
+ } else {
+ Logger::info('Module "%s" is already disabled', $module);
+ }
+ }
+
+ /**
+ * Show all restrictions provided by your modules
+ *
+ * Asks each enabled module for all available restriction names and
+ * descriptions and shows a quick overview
+ *
+ * Usage: icingacli module restrictions
+ */
+ public function restrictionsAction()
+ {
+ printf("%-14s %-16s %s\n", 'MODULE', 'RESTRICTION', 'DESCRIPTION');
+ foreach ($this->modules->listEnabledModules() as $moduleName) {
+ $module = $this->modules->loadModule($moduleName)->getModule($moduleName);
+ foreach ($module->getProvidedRestrictions() as $restriction) {
+ printf(
+ "%-14s %-16s %s\n",
+ $moduleName,
+ $restriction->name,
+ $restriction->description
+ );
+ }
+ }
+ }
+
+ /**
+ * Show all permissions provided by your modules
+ *
+ * Asks each enabled module for it's available permission names and
+ * descriptions and shows a quick overview
+ *
+ * Usage: icingacli module permissions
+ */
+ public function permissionsAction()
+ {
+ printf("%-14s %-24s %s\n", 'MODULE', 'PERMISSION', 'DESCRIPTION');
+ foreach ($this->modules->listEnabledModules() as $moduleName) {
+ $module = $this->modules->loadModule($moduleName)->getModule($moduleName);
+ foreach ($module->getProvidedPermissions() as $restriction) {
+ printf(
+ "%-14s %-24s %s\n",
+ $moduleName,
+ $restriction->name,
+ $restriction->description
+ );
+ }
+ }
+ }
+
+ /**
+ * Search for a given module
+ *
+ * Does a lookup against your configured IcingaWeb app stores and tries to
+ * find modules matching your search string
+ *
+ * Usage: icingacli module search <search-string>
+ */
+ public function searchAction()
+ {
+ $this->fail("Not implemented yet");
+ }
+
+ /**
+ * Install a given module
+ *
+ * Downloads a given module or installes a module from a given archive
+ *
+ * Usage: icingacli module install <module-name>
+ * icingacli module install </path/to/archive.tar.gz>
+ */
+ public function installAction()
+ {
+ $this->fail("Not implemented yet");
+ }
+
+ /**
+ * Remove a given module
+ *
+ * Removes the given module from your disk. Module configuration will be
+ * preserved
+ *
+ * Usage: icingacli module remove <module-name>
+ */
+ public function removeAction()
+ {
+ $this->fail("Not implemented yet");
+ }
+
+ /**
+ * Purge a given module
+ *
+ * Removes the given module from your disk. Also wipes configuration files
+ * and other data stored and/or generated by this module
+ *
+ * Usage: icingacli module remove <module-name>
+ */
+ public function purgeAction()
+ {
+ $this->fail("Not implemented yet");
+ }
+}
diff --git a/application/clicommands/VersionCommand.php b/application/clicommands/VersionCommand.php
new file mode 100644
index 0000000..9bdd443
--- /dev/null
+++ b/application/clicommands/VersionCommand.php
@@ -0,0 +1,55 @@
+<?php
+/* Icinga Web 2 | (c) 2019 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Clicommands;
+
+use Icinga\Application\Version;
+use Icinga\Application\Icinga;
+use Icinga\Cli\Loader;
+use Icinga\Cli\Command;
+
+/**
+ * Shows version of Icinga Web 2, loaded modules and PHP
+ *
+ * The version command shows version numbers for Icinga Web 2, loaded modules and PHP.
+ *
+ * Usage: icingacli --version
+ */
+class VersionCommand extends Command
+{
+ protected $defaultActionName = 'show';
+
+ /**
+ * Shows version of Icinga Web 2, loaded modules and PHP
+ *
+ * The version command shows version numbers for Icinga Web 2, loaded modules and PHP.
+ *
+ * Usage: icingacli --version
+ */
+ public function showAction()
+ {
+ $getVersion = Version::get();
+ printf("%-12s %-9s \n", 'Icinga Web 2', $getVersion['appVersion']);
+
+ if (isset($getVersion['gitCommitID'])) {
+ printf("%-12s %-9s \n", 'Git Commit', $getVersion['gitCommitID']);
+ }
+
+ printf("%-12s %-9s \n", 'PHP Version', PHP_VERSION);
+
+ $modules = Icinga::app()->getModuleManager()->loadEnabledModules()->getLoadedModules();
+
+ $maxLength = 0;
+ foreach ($modules as $module) {
+ $length = strlen($module->getName());
+ if ($length > $maxLength) {
+ $maxLength = $length;
+ }
+ }
+
+ printf("%-{$maxLength}s %-9s \n", 'MODULE', 'VERSION');
+ foreach ($modules as $module) {
+ printf("%-{$maxLength}s %-9s \n", $module->getName(), $module->getVersion());
+ }
+ }
+}
diff --git a/application/clicommands/WebCommand.php b/application/clicommands/WebCommand.php
new file mode 100644
index 0000000..67d50a3
--- /dev/null
+++ b/application/clicommands/WebCommand.php
@@ -0,0 +1,101 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Clicommands;
+
+use Icinga\Application\Icinga;
+use Icinga\Cli\Command;
+use Icinga\Exception\IcingaException;
+
+class WebCommand extends Command
+{
+ /**
+ * Serve Icinga Web 2 with PHP's built-in web server
+ *
+ * USAGE
+ *
+ * icingacli web serve [options] [<document-root>]
+ *
+ * OPTIONS
+ *
+ * --daemonize Run in background
+ * --port=<port> The port to listen on
+ * --listen=<host:port> The address to listen on
+ * <document-root> The document root directory of Icinga Web 2 (e.g. ./public)
+ *
+ * EXAMPLES
+ *
+ * icingacli web serve --port=8080
+ * icingacli web serve --listen=127.0.0.1:8080 ./public
+ */
+ public function serveAction()
+ {
+ $fork = $this->params->get('daemonize');
+ $listen = $this->params->get('listen');
+ $port = $this->params->get('port');
+ $documentRoot = $this->params->shift();
+ if ($listen === null) {
+ $socket = $port === null ? $this->params->shift() : '0.0.0.0:' . $port;
+ } else {
+ $socket = $listen;
+ }
+
+ if ($socket === null) {
+ $socket = $this->Config()->get('standalone', 'listen', '0.0.0.0:80');
+ }
+ if ($documentRoot === null) {
+ $documentRoot = Icinga::app()->getBaseDir('public');
+ if (! file_exists($documentRoot) || ! is_dir($documentRoot)) {
+ throw new IcingaException('Document root directory is required');
+ }
+ }
+ $documentRoot = realpath($documentRoot);
+
+ if ($fork) {
+ $this->forkAndExit();
+ }
+ echo "Serving Icinga Web 2 from directory $documentRoot and listening on $socket\n";
+
+ // TODO: Store webserver log, switch uid, log index.php includes, pid file
+ pcntl_exec(
+ readlink('/proc/self/exe'),
+ ['-S', $socket, '-t', $documentRoot, Icinga::app()->getLibraryDir('/Icinga/Application/webrouter.php')]
+ );
+ }
+
+ public function stopAction()
+ {
+ // TODO: No, that's NOT what we want
+ $prog = readlink('/proc/self/exe');
+ `killall $prog`;
+ }
+
+ protected function forkAndExit()
+ {
+ $pid = pcntl_fork();
+ if ($pid == -1) {
+ throw new IcingaException('Could not fork');
+ } elseif ($pid) {
+ echo $this->screen->colorize('[OK]')
+ . " Icinga Web server forked successfully\n";
+ fclose(STDIN);
+ fclose(STDOUT);
+ fclose(STDERR);
+ exit;
+ // pcntl_wait($status);
+ } else {
+ // child
+
+ // Replace console with /dev/null by first freeing the (lowest possible) FDs 0, 1 and 2
+ // and then opening /dev/null once for every one of them (open(2) chooses the lowest free FD).
+
+ fclose(STDIN);
+ fclose(STDOUT);
+ fclose(STDERR);
+
+ fopen('/dev/null', 'rb');
+ fopen('/dev/null', 'wb');
+ fopen('/dev/null', 'wb');
+ }
+ }
+}
diff --git a/application/controllers/AboutController.php b/application/controllers/AboutController.php
new file mode 100644
index 0000000..59e3c20
--- /dev/null
+++ b/application/controllers/AboutController.php
@@ -0,0 +1,27 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Version;
+use Icinga\Web\Controller;
+
+class AboutController extends Controller
+{
+ public function indexAction()
+ {
+ $this->view->version = Version::get();
+ $this->view->libraries = Icinga::app()->getLibraries();
+ $this->view->modules = Icinga::app()->getModuleManager()->getLoadedModules();
+ $this->view->title = $this->translate('About');
+ $this->view->tabs = $this->getTabs()->add(
+ 'about',
+ array(
+ 'label' => $this->translate('About'),
+ 'title' => $this->translate('About Icinga Web 2'),
+ 'url' => 'about'
+ )
+ )->activate('about');
+ }
+}
diff --git a/application/controllers/AccountController.php b/application/controllers/AccountController.php
new file mode 100644
index 0000000..f172cfe
--- /dev/null
+++ b/application/controllers/AccountController.php
@@ -0,0 +1,83 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Config;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Forms\Account\ChangePasswordForm;
+use Icinga\Forms\PreferenceForm;
+use Icinga\User\Preferences\PreferencesStore;
+use Icinga\Web\Controller;
+
+/**
+ * My Account
+ */
+class AccountController extends Controller
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->getTabs()
+ ->add('account', array(
+ 'title' => $this->translate('Update your account'),
+ 'label' => $this->translate('My Account'),
+ 'url' => 'account'
+ ))
+ ->add('navigation', array(
+ 'title' => $this->translate('List and configure your own navigation items'),
+ 'label' => $this->translate('Navigation'),
+ 'url' => 'navigation'
+ ))
+ ->add(
+ 'devices',
+ array(
+ 'title' => $this->translate('List of devices you are logged in'),
+ 'label' => $this->translate('My Devices'),
+ 'url' => 'my-devices'
+ )
+ );
+ }
+
+ /**
+ * My account
+ */
+ public function indexAction()
+ {
+ $config = Config::app()->getSection('global');
+ $user = $this->Auth()->getUser();
+ if ($user->getAdditional('backend_type') === 'db') {
+ if ($user->can('user/password-change')) {
+ try {
+ $userBackend = UserBackend::create($user->getAdditional('backend_name'));
+ } catch (ConfigurationError $e) {
+ $userBackend = null;
+ }
+ if ($userBackend !== null) {
+ $changePasswordForm = new ChangePasswordForm();
+ $changePasswordForm
+ ->setBackend($userBackend)
+ ->handleRequest();
+ $this->view->changePasswordForm = $changePasswordForm;
+ }
+ }
+ }
+
+ $form = new PreferenceForm();
+ $form->setPreferences($user->getPreferences());
+ if (isset($config->config_resource)) {
+ $form->setStore(PreferencesStore::create(new ConfigObject(array(
+ 'resource' => $config->config_resource
+ )), $user));
+ }
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('My Account');
+ $this->getTabs()->activate('account');
+ }
+}
diff --git a/application/controllers/AnnouncementsController.php b/application/controllers/AnnouncementsController.php
new file mode 100644
index 0000000..ee7fd4c
--- /dev/null
+++ b/application/controllers/AnnouncementsController.php
@@ -0,0 +1,123 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Announcement\AcknowledgeAnnouncementForm;
+use Icinga\Forms\Announcement\AnnouncementForm;
+use Icinga\Web\Announcement\AnnouncementIniRepository;
+use Icinga\Web\Controller;
+use Icinga\Web\Url;
+
+class AnnouncementsController extends Controller
+{
+ public function init()
+ {
+ $this->view->title = $this->translate('Announcements');
+
+ parent::init();
+ }
+
+ /**
+ * List all announcements
+ */
+ public function indexAction()
+ {
+ $this->getTabs()->add(
+ 'announcements',
+ array(
+ 'active' => true,
+ 'label' => $this->translate('Announcements'),
+ 'title' => $this->translate('List All Announcements'),
+ 'url' => Url::fromPath('announcements')
+ )
+ );
+
+ $announcements = (new AnnouncementIniRepository())
+ ->select([
+ 'id',
+ 'author',
+ 'message',
+ 'start',
+ 'end'
+ ]);
+
+ $sortAndFilterColumns = [
+ 'author' => $this->translate('Author'),
+ 'message' => $this->translate('Message'),
+ 'start' => $this->translate('Start'),
+ 'end' => $this->translate('End')
+ ];
+
+ $this->setupSortControl($sortAndFilterColumns, $announcements, ['start' => 'desc']);
+ $this->setupFilterControl($announcements, $sortAndFilterColumns, ['message']);
+
+ $this->view->announcements = $announcements->fetchAll();
+ }
+
+ /**
+ * Create an announcement
+ */
+ public function newAction()
+ {
+ $this->assertPermission('application/announcements');
+
+ $form = $this->prepareForm()->add();
+ $form->handleRequest();
+ $this->renderForm($form, $this->translate('New Announcement'));
+ }
+
+ /**
+ * Update an announcement
+ */
+ public function updateAction()
+ {
+ $this->assertPermission('application/announcements');
+
+ $form = $this->prepareForm()->edit($this->params->getRequired('id'));
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound($this->translate('Announcement not found'));
+ }
+ $this->renderForm($form, $this->translate('Update Announcement'));
+ }
+
+ /**
+ * Remove an announcement
+ */
+ public function removeAction()
+ {
+ $this->assertPermission('application/announcements');
+
+ $form = $this->prepareForm()->remove($this->params->getRequired('id'));
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound($this->translate('Announcement not found'));
+ }
+ $this->renderForm($form, $this->translate('Remove Announcement'));
+ }
+
+ public function acknowledgeAction()
+ {
+ $this->assertHttpMethod('POST');
+ $this->getResponse()->setHeader('X-Icinga-Container', 'ignore', true);
+ $form = new AcknowledgeAnnouncementForm();
+ $form->handleRequest();
+ }
+
+ /**
+ * Assert permission admin and return a prepared RepositoryForm
+ *
+ * @return AnnouncementForm
+ */
+ protected function prepareForm()
+ {
+ $form = new AnnouncementForm();
+ return $form
+ ->setRepository(new AnnouncementIniRepository())
+ ->setRedirectUrl(Url::fromPath('announcements'));
+ }
+}
diff --git a/application/controllers/ApplicationStateController.php b/application/controllers/ApplicationStateController.php
new file mode 100644
index 0000000..b828ca2
--- /dev/null
+++ b/application/controllers/ApplicationStateController.php
@@ -0,0 +1,95 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Forms\AcknowledgeApplicationStateMessageForm;
+use Icinga\Web\Announcement\AnnouncementCookie;
+use Icinga\Web\Announcement\AnnouncementIniRepository;
+use Icinga\Web\Controller;
+use Icinga\Web\RememberMe;
+use Icinga\Web\Session;
+use Icinga\Web\Widget;
+
+/**
+ * @TODO(el): https://dev.icinga.com/issues/10646
+ */
+class ApplicationStateController extends Controller
+{
+ protected $requiresAuthentication = false;
+
+ protected $autorefreshInterval = 60;
+
+ public function init()
+ {
+ $this->_helper->layout->disableLayout();
+ $this->_helper->viewRenderer->setNoRender(true);
+ }
+
+ public function indexAction()
+ {
+ if ($this->Auth()->isAuthenticated()) {
+ if (isset($_COOKIE['icingaweb2-session'])) {
+ $last = (int) $_COOKIE['icingaweb2-session'];
+ } else {
+ $last = 0;
+ }
+ $now = time();
+ if ($last + 600 < $now) {
+ Session::getSession()->write();
+ $params = session_get_cookie_params();
+ setcookie(
+ 'icingaweb2-session',
+ $now,
+ 0,
+ $params['path'],
+ $params['domain'],
+ $params['secure'],
+ $params['httponly']
+ );
+ $_COOKIE['icingaweb2-session'] = $now;
+ }
+ $announcementCookie = new AnnouncementCookie();
+ $announcementRepo = new AnnouncementIniRepository();
+ if ($announcementCookie->getEtag() !== $announcementRepo->getEtag()) {
+ $announcementCookie
+ ->setEtag($announcementRepo->getEtag())
+ ->setNextActive($announcementRepo->findNextActive());
+ $this->getResponse()->setCookie($announcementCookie);
+ $this->getResponse()->setHeader('X-Icinga-Announcements', 'refresh', true);
+ } else {
+ $nextActive = $announcementCookie->getNextActive();
+ if ($nextActive && $nextActive <= $now) {
+ $announcementCookie->setNextActive($announcementRepo->findNextActive());
+ $this->getResponse()->setCookie($announcementCookie);
+ $this->getResponse()->setHeader('X-Icinga-Announcements', 'refresh', true);
+ }
+ }
+ }
+
+ RememberMe::removeExpired();
+ }
+
+ public function summaryAction()
+ {
+ if ($this->Auth()->isAuthenticated()) {
+ $this->getResponse()->setBody((string) Widget::create('ApplicationStateMessages'));
+ }
+ }
+
+ public function acknowledgeMessageAction()
+ {
+ if (! $this->Auth()->isAuthenticated()) {
+ $this->getResponse()
+ ->setHttpResponseCode(401)
+ ->sendHeaders();
+ exit;
+ }
+
+ $this->assertHttpMethod('POST');
+
+ $this->getResponse()->setHeader('X-Icinga-Container', 'ignore', true);
+
+ (new AcknowledgeApplicationStateMessageForm())->handleRequest();
+ }
+}
diff --git a/application/controllers/AuthenticationController.php b/application/controllers/AuthenticationController.php
new file mode 100644
index 0000000..752f845
--- /dev/null
+++ b/application/controllers/AuthenticationController.php
@@ -0,0 +1,127 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Hook\AuthenticationHook;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Common\Database;
+use Icinga\Exception\AuthenticationException;
+use Icinga\Forms\Authentication\LoginForm;
+use Icinga\Web\Controller;
+use Icinga\Web\Helper\CookieHelper;
+use Icinga\Web\RememberMe;
+use Icinga\Web\Url;
+use RuntimeException;
+
+/**
+ * Application wide controller for authentication
+ */
+class AuthenticationController extends Controller
+{
+ use Database;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $requiresAuthentication = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $innerLayout = 'inline';
+
+ /**
+ * Log into the application
+ */
+ public function loginAction()
+ {
+ $icinga = Icinga::app();
+ if (($requiresSetup = $icinga->requiresSetup()) && $icinga->setupTokenExists()) {
+ $this->redirectNow(Url::fromPath('setup'));
+ }
+ $form = new LoginForm();
+
+ if (RememberMe::hasCookie() && $this->hasDb()) {
+ $authenticated = false;
+ try {
+ $rememberMeOld = RememberMe::fromCookie();
+ $authenticated = $rememberMeOld->authenticate();
+ if ($authenticated) {
+ $rememberMe = $rememberMeOld->renew();
+ $this->getResponse()->setCookie($rememberMe->getCookie());
+ $rememberMe->persist($rememberMeOld->getAesCrypt()->getIV());
+ }
+ } catch (RuntimeException $e) {
+ Logger::error("Can't authenticate user via remember me cookie: %s", $e->getMessage());
+ } catch (AuthenticationException $e) {
+ Logger::error($e);
+ }
+
+ if (! $authenticated) {
+ $this->getResponse()->setCookie(RememberMe::forget());
+ }
+ }
+
+ if ($this->Auth()->isAuthenticated()) {
+ // Call provided AuthenticationHook(s) when login action is called
+ // but icinga web user is already authenticated
+ AuthenticationHook::triggerLogin($this->Auth()->getUser());
+
+ $redirect = $this->params->get('redirect');
+ if ($redirect) {
+ $redirectUrl = Url::fromPath($redirect, [], $this->getRequest());
+ if ($redirectUrl->isExternal()) {
+ $this->httpBadRequest('nope');
+ }
+ } else {
+ $redirectUrl = $form->getRedirectUrl();
+ }
+
+ $this->redirectNow($redirectUrl);
+ }
+ if (! $requiresSetup) {
+ $cookies = new CookieHelper($this->getRequest());
+ if (! $cookies->isSupported()) {
+ $this
+ ->getResponse()
+ ->setBody("Cookies must be enabled to run this application.\n")
+ ->setHttpResponseCode(403)
+ ->sendResponse();
+ exit;
+ }
+ $form->handleRequest();
+ }
+ $this->view->form = $form;
+ $this->view->defaultTitle = $this->translate('Icinga Web 2 Login');
+ $this->view->requiresSetup = $requiresSetup;
+ }
+
+ /**
+ * Log out the current user
+ */
+ public function logoutAction()
+ {
+ $auth = $this->Auth();
+ if (! $auth->isAuthenticated()) {
+ $this->redirectToLogin();
+ }
+ // Get info whether the user is externally authenticated before removing authorization which destroys the
+ // session and the user object
+ $isExternalUser = $auth->getUser()->isExternalUser();
+ // Call provided AuthenticationHook(s) when logout action is called
+ AuthenticationHook::triggerLogout($auth->getUser());
+ $auth->removeAuthorization();
+ if ($isExternalUser) {
+ $this->view->layout()->setLayout('external-logout');
+ $this->getResponse()->setHttpResponseCode(401);
+ } else {
+ if (RememberMe::hasCookie() && $this->hasDb()) {
+ $this->getResponse()->setCookie(RememberMe::forget());
+ }
+
+ $this->redirectToLogin();
+ }
+ }
+}
diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php
new file mode 100644
index 0000000..671e1a7
--- /dev/null
+++ b/application/controllers/ConfigController.php
@@ -0,0 +1,518 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Version;
+use InvalidArgumentException;
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\ActionForm;
+use Icinga\Forms\Config\GeneralConfigForm;
+use Icinga\Forms\Config\ResourceConfigForm;
+use Icinga\Forms\Config\UserBackendConfigForm;
+use Icinga\Forms\Config\UserBackendReorderForm;
+use Icinga\Forms\ConfirmRemovalForm;
+use Icinga\Security\SecurityException;
+use Icinga\Web\Controller;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+use Icinga\Web\Widget;
+
+/**
+ * Application and module configuration
+ */
+class ConfigController extends Controller
+{
+ /**
+ * Create and return the tabs to display when showing application configuration
+ */
+ public function createApplicationTabs()
+ {
+ $tabs = $this->getTabs();
+ if ($this->hasPermission('config/general')) {
+ $tabs->add('general', array(
+ 'title' => $this->translate('Adjust the general configuration of Icinga Web 2'),
+ 'label' => $this->translate('General'),
+ 'url' => 'config/general',
+ 'baseTarget' => '_main'
+ ));
+ }
+ if ($this->hasPermission('config/resources')) {
+ $tabs->add('resource', array(
+ 'title' => $this->translate('Configure which resources are being utilized by Icinga Web 2'),
+ 'label' => $this->translate('Resources'),
+ 'url' => 'config/resource',
+ 'baseTarget' => '_main'
+ ));
+ }
+ if ($this->hasPermission('config/access-control/users')
+ || $this->hasPermission('config/access-control/groups')
+ ) {
+ $tabs->add('authentication', array(
+ 'title' => $this->translate('Configure the user and group backends'),
+ 'label' => $this->translate('Access Control Backends'),
+ 'url' => 'config/userbackend',
+ 'baseTarget' => '_main'
+ ));
+ }
+
+ return $tabs;
+ }
+
+ public function devtoolsAction()
+ {
+ $this->view->tabs = null;
+ }
+
+ /**
+ * Redirect to the general configuration
+ */
+ public function indexAction()
+ {
+ if ($this->hasPermission('config/general')) {
+ $this->redirectNow('config/general');
+ } elseif ($this->hasPermission('config/resources')) {
+ $this->redirectNow('config/resource');
+ } elseif ($this->hasPermission('config/access-control/*')) {
+ $this->redirectNow('config/userbackend');
+ } else {
+ throw new SecurityException('No permission to configure Icinga Web 2');
+ }
+ }
+
+ /**
+ * General configuration
+ *
+ * @throws SecurityException If the user lacks the permission for configuring the general configuration
+ */
+ public function generalAction()
+ {
+ $this->assertPermission('config/general');
+ $form = new GeneralConfigForm();
+ $form->setIniConfig(Config::app());
+ $form->setOnSuccess(function (GeneralConfigForm $form) {
+ $config = Config::app();
+ $useStrictCsp = (bool) $config->get('security', 'use_strict_csp', false);
+ if ($form->onSuccess() === false) {
+ return false;
+ }
+
+ $appConfigForm = $form->getSubForm('form_config_general_application');
+ if ($appConfigForm && (bool) $appConfigForm->getValue('security_use_strict_csp') !== $useStrictCsp) {
+ $this->getResponse()->setReloadWindow(true);
+ }
+ })->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('General');
+ $this->createApplicationTabs()->activate('general');
+ }
+
+ /**
+ * Display the list of all modules
+ */
+ public function modulesAction()
+ {
+ $this->assertPermission('config/modules');
+ // Overwrite tabs created in init
+ // @TODO(el): This seems not natural to me. Module configuration should have its own controller.
+ $this->view->tabs = Widget::create('tabs')
+ ->add('modules', array(
+ 'label' => $this->translate('Modules'),
+ 'title' => $this->translate('List intalled modules'),
+ 'url' => 'config/modules'
+ ))
+ ->activate('modules');
+ $this->view->modules = Icinga::app()->getModuleManager()->select()
+ ->from('modules')
+ ->order('enabled', 'desc')
+ ->order('installed', 'asc')
+ ->order('name');
+ $this->setupLimitControl();
+ $this->setupPaginationControl($this->view->modules);
+ $this->view->title = $this->translate('Modules');
+ }
+
+ public function moduleAction()
+ {
+ $this->assertPermission('config/modules');
+ $app = Icinga::app();
+ $manager = $app->getModuleManager();
+ $name = $this->getParam('name');
+ if ($manager->hasInstalled($name) || $manager->hasEnabled($name)) {
+ $this->view->moduleData = $manager->select()->from('modules')->where('name', $name)->fetchRow();
+ if ($manager->hasLoaded($name)) {
+ $module = $manager->getModule($name);
+ } else {
+ $module = new Module($app, $name, $manager->getModuleDir($name));
+ }
+
+ $toggleForm = new ActionForm();
+ $toggleForm->setDefaults(['identifier' => $name]);
+ if (! $this->view->moduleData->enabled) {
+ $toggleForm->setAction(Url::fromPath('config/moduleenable'));
+ $toggleForm->setDescription(sprintf($this->translate('Enable the %s module'), $name));
+ } elseif ($this->view->moduleData->loaded) {
+ $toggleForm->setAction(Url::fromPath('config/moduledisable'));
+ $toggleForm->setDescription(sprintf($this->translate('Disable the %s module'), $name));
+ } else {
+ $toggleForm = null;
+ }
+
+ $this->view->module = $module;
+ $this->view->libraries = $app->getLibraries();
+ $this->view->moduleManager = $manager;
+ $this->view->toggleForm = $toggleForm;
+ $this->view->title = $module->getName();
+ $this->view->tabs = $module->getConfigTabs()->activate('info');
+ $this->view->moduleGitCommitId = Version::getGitHead($module->getBaseDir());
+ } else {
+ $this->view->module = false;
+ $this->view->tabs = null;
+ }
+ }
+
+ /**
+ * Enable a specific module provided by the 'name' param
+ */
+ public function moduleenableAction()
+ {
+ $this->assertPermission('config/modules');
+
+ $form = new ActionForm();
+ $form->setOnSuccess(function (ActionForm $form) {
+ $moduleName = $form->getValue('identifier');
+ $module = Icinga::app()->getModuleManager()
+ ->enableModule($moduleName)
+ ->getModule($moduleName);
+ Notification::success(sprintf($this->translate('Module "%s" enabled'), $moduleName));
+ $form->onSuccess();
+
+ if ($module->hasJs()) {
+ $this->getResponse()
+ ->setReloadWindow(true)
+ ->sendResponse();
+ } else {
+ if ($module->hasCss()) {
+ $this->reloadCss();
+ }
+
+ $this->rerenderLayout()->redirectNow('config/modules');
+ }
+ });
+
+ try {
+ $form->handleRequest();
+ } catch (Exception $e) {
+ $this->view->exceptionMessage = $e->getMessage();
+ $this->view->moduleName = $form->getValue('identifier');
+ $this->view->action = 'enable';
+ $this->render('module-configuration-error');
+ }
+ }
+
+ /**
+ * Disable a module specific module provided by the 'name' param
+ */
+ public function moduledisableAction()
+ {
+ $this->assertPermission('config/modules');
+
+ $form = new ActionForm();
+ $form->setOnSuccess(function (ActionForm $form) {
+ $mm = Icinga::app()->getModuleManager();
+ $moduleName = $form->getValue('identifier');
+ $module = $mm->getModule($moduleName);
+ $mm->disableModule($moduleName);
+ Notification::success(sprintf($this->translate('Module "%s" disabled'), $moduleName));
+ $form->onSuccess();
+
+ if ($module->hasJs()) {
+ $this->getResponse()
+ ->setReloadWindow(true)
+ ->sendResponse();
+ } else {
+ if ($module->hasCss()) {
+ $this->reloadCss();
+ }
+
+ $this->rerenderLayout()->redirectNow('config/modules');
+ }
+ });
+
+ try {
+ $form->handleRequest();
+ } catch (Exception $e) {
+ $this->view->exceptionMessage = $e->getMessage();
+ $this->view->moduleName = $form->getValue('identifier');
+ $this->view->action = 'disable';
+ $this->render('module-configuration-error');
+ }
+ }
+
+ /**
+ * Action for listing user and group backends
+ */
+ public function userbackendAction()
+ {
+ if ($this->hasPermission('config/access-control/users')) {
+ $form = new UserBackendReorderForm();
+ $form->setIniConfig(Config::app('authentication'));
+ $form->handleRequest();
+ $this->view->form = $form;
+ }
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $this->view->backendNames = Config::app('groups');
+ }
+
+ $this->createApplicationTabs()->activate('authentication');
+ $this->view->title = $this->translate('Authentication');
+ $this->render('userbackend/reorder');
+ }
+
+ /**
+ * Create a new user backend
+ */
+ public function createuserbackendAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $form = new UserBackendConfigForm();
+ $form
+ ->setRedirectUrl('config/userbackend')
+ ->addDescription($this->translate(
+ 'Create a new backend for authenticating your users. This backend'
+ . ' will be added at the end of your authentication order.'
+ ))
+ ->setIniConfig(Config::app('authentication'));
+
+ try {
+ $form->setResourceConfig(ResourceFactory::getResourceConfigs());
+ } catch (ConfigurationError $e) {
+ if ($this->hasPermission('config/resources')) {
+ Notification::error($e->getMessage());
+ $this->redirectNow('config/createresource');
+ }
+
+ throw $e; // No permission for resource configuration, show the error
+ }
+
+ $form->setOnSuccess(function (UserBackendConfigForm $form) {
+ try {
+ $form->add($form::transformEmptyValuesToNull($form->getValues()));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(t('User backend successfully created'));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('New User Backend'));
+ }
+
+ /**
+ * Edit a user backend
+ */
+ public function edituserbackendAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $backendName = $this->params->getRequired('backend');
+
+ $form = new UserBackendConfigForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->setIniConfig(Config::app('authentication'));
+ $form->setOnSuccess(function (UserBackendConfigForm $form) use ($backendName) {
+ try {
+ $form->edit($backendName, $form::transformEmptyValuesToNull($form->getValues()));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(sprintf(t('User backend "%s" successfully updated'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+
+ try {
+ $form->load($backendName);
+ $form->setResourceConfig(ResourceFactory::getResourceConfigs());
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User backend "%s" not found'), $backendName));
+ }
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('Update User Backend'));
+ }
+
+ /**
+ * Display a confirmation form to remove the backend identified by the 'backend' parameter
+ */
+ public function removeuserbackendAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $backendName = $this->params->getRequired('backend');
+
+ $backendForm = new UserBackendConfigForm();
+ $backendForm->setIniConfig(Config::app('authentication'));
+ $form = new ConfirmRemovalForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($backendName, $backendForm) {
+ try {
+ $backendForm->delete($backendName);
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($backendForm->save()) {
+ Notification::success(sprintf(t('User backend "%s" successfully removed'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('Remove User Backend'));
+ }
+
+ /**
+ * Display all available resources and a link to create a new one and to remove existing ones
+ */
+ public function resourceAction()
+ {
+ $this->assertPermission('config/resources');
+ $this->view->resources = Config::app('resources', true)->getConfigObject()
+ ->setKeyColumn('name')
+ ->select()
+ ->order('name');
+ $this->view->title = $this->translate('Resources');
+ $this->createApplicationTabs()->activate('resource');
+ }
+
+ /**
+ * Display a form to create a new resource
+ */
+ public function createresourceAction()
+ {
+ $this->assertPermission('config/resources');
+ $this->getTabs()->add('resources/new', array(
+ 'label' => $this->translate('New Resource'),
+ 'url' => Url::fromRequest()
+ ))->activate('resources/new');
+ $form = new ResourceConfigForm();
+ $form->addDescription($this->translate('Resources are entities that provide data to Icinga Web 2.'));
+ $form->setIniConfig(Config::app('resources'));
+ $form->setRedirectUrl('config/resource');
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('Resources');
+ $this->render('resource/create');
+ }
+
+ /**
+ * Display a form to edit a existing resource
+ */
+ public function editresourceAction()
+ {
+ $this->assertPermission('config/resources');
+ $this->getTabs()->add('resources/update', array(
+ 'label' => $this->translate('Update Resource'),
+ 'url' => Url::fromRequest()
+ ))->activate('resources/update');
+ $form = new ResourceConfigForm();
+ $form->setIniConfig(Config::app('resources'));
+ $form->setRedirectUrl('config/resource');
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('Resources');
+ $this->render('resource/modify');
+ }
+
+ /**
+ * Display a confirmation form to remove a resource
+ */
+ public function removeresourceAction()
+ {
+ $this->assertPermission('config/resources');
+ $this->getTabs()->add('resources/remove', array(
+ 'label' => $this->translate('Remove Resource'),
+ 'url' => Url::fromRequest()
+ ))->activate('resources/remove');
+ $form = new ConfirmRemovalForm(array(
+ 'onSuccess' => function ($form) {
+ $configForm = new ResourceConfigForm();
+ $configForm->setIniConfig(Config::app('resources'));
+ $resource = $form->getRequest()->getQuery('resource');
+
+ try {
+ $configForm->remove($resource);
+ } catch (InvalidArgumentException $e) {
+ Notification::error($e->getMessage());
+ return false;
+ }
+
+ if ($configForm->save()) {
+ Notification::success(sprintf(t('Resource "%s" has been successfully removed'), $resource));
+ } else {
+ return false;
+ }
+ }
+ ));
+ $form->setRedirectUrl('config/resource');
+ $form->handleRequest();
+
+ // Check if selected resource is currently used for authentication
+ $resource = $this->getRequest()->getQuery('resource');
+ $authConfig = Config::app('authentication');
+ foreach ($authConfig as $backendName => $config) {
+ if ($config->get('resource') === $resource) {
+ $form->warning(sprintf(
+ $this->translate(
+ 'The resource "%s" is currently utilized for authentication by user backend "%s".'
+ . ' Removing the resource can result in noone being able to log in any longer.'
+ ),
+ $resource,
+ $backendName
+ ));
+ }
+ }
+
+ // Check if selected resource is currently used as user preferences backend
+ if (Config::app()->get('global', 'config_resource') === $resource) {
+ $form->warning(sprintf(
+ $this->translate(
+ 'The resource "%s" is currently utilized to store user preferences. Removing the'
+ . ' resource causes all current user preferences not being available any longer.'
+ ),
+ $resource
+ ));
+ }
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('Resources');
+ $this->render('resource/remove');
+ }
+}
diff --git a/application/controllers/DashboardController.php b/application/controllers/DashboardController.php
new file mode 100644
index 0000000..ff2580c
--- /dev/null
+++ b/application/controllers/DashboardController.php
@@ -0,0 +1,346 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Zend_Controller_Action_Exception;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\Http\HttpNotFoundException;
+use Icinga\Forms\ConfirmRemovalForm;
+use Icinga\Forms\Dashboard\DashletForm;
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Dashboard;
+use Icinga\Web\Widget\Tabextension\DashboardSettings;
+
+/**
+ * Handle creation, removal and displaying of dashboards, panes and dashlets
+ *
+ * @see Icinga\Web\Widget\Dashboard for more information about dashboards
+ */
+class DashboardController extends ActionController
+{
+ /**
+ * @var Dashboard;
+ */
+ private $dashboard;
+
+ public function init()
+ {
+ $this->dashboard = new Dashboard();
+ $this->dashboard->setUser($this->Auth()->getUser());
+ $this->dashboard->load();
+ }
+
+ public function newDashletAction()
+ {
+ $form = new DashletForm();
+ $this->getTabs()->add('new-dashlet', array(
+ 'active' => true,
+ 'label' => $this->translate('New Dashlet'),
+ 'url' => Url::fromRequest()
+ ));
+ $dashboard = $this->dashboard;
+ $form->setDashboard($dashboard);
+ if ($this->_request->getParam('url')) {
+ $params = $this->_request->getParams();
+ $params['url'] = rawurldecode($this->_request->getParam('url'));
+ $form->populate($params);
+ }
+ $action = $this;
+ $form->setOnSuccess(function (Form $form) use ($dashboard, $action) {
+ try {
+ $pane = $dashboard->getPane($form->getValue('pane'));
+ } catch (ProgrammingError $e) {
+ $pane = new Dashboard\Pane($form->getValue('pane'));
+ $pane->setUserWidget();
+ $dashboard->addPane($pane);
+ }
+ $dashlet = new Dashboard\Dashlet($form->getValue('dashlet'), $form->getValue('url'), $pane);
+ $dashlet->setUserWidget();
+ $pane->addDashlet($dashlet);
+ $dashboardConfig = $dashboard->getConfig();
+ try {
+ $dashboardConfig->saveIni();
+ } catch (Exception $e) {
+ $action->view->error = $e;
+ $action->view->config = $dashboardConfig;
+ $action->render('error');
+ return false;
+ }
+ Notification::success(t('Dashlet created'));
+ return true;
+ });
+ $form->setTitle($this->translate('Add Dashlet To Dashboard'));
+ $form->setRedirectUrl('dashboard');
+ $form->handleRequest();
+ $this->view->form = $form;
+ }
+
+ public function updateDashletAction()
+ {
+ $this->getTabs()->add('update-dashlet', array(
+ 'active' => true,
+ 'label' => $this->translate('Update Dashlet'),
+ 'url' => Url::fromRequest()
+ ));
+ $dashboard = $this->dashboard;
+ $form = new DashletForm();
+ $form->setDashboard($dashboard);
+ $form->setSubmitLabel($this->translate('Update Dashlet'));
+ if (! $this->_request->getParam('pane')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "pane"',
+ 400
+ );
+ }
+ if (! $this->_request->getParam('dashlet')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "dashlet"',
+ 400
+ );
+ }
+ $action = $this;
+ $form->setOnSuccess(function (Form $form) use ($dashboard, $action) {
+ try {
+ $pane = $dashboard->getPane($form->getValue('org_pane'));
+ $pane->setTitle($form->getValue('pane'));
+ } catch (ProgrammingError $e) {
+ $pane = new Dashboard\Pane($form->getValue('pane'));
+ $pane->setUserWidget();
+ $dashboard->addPane($pane);
+ }
+ try {
+ $dashlet = $pane->getDashlet($form->getValue('org_dashlet'));
+ $dashlet->setTitle($form->getValue('dashlet'));
+ $dashlet->setUrl($form->getValue('url'));
+ } catch (ProgrammingError $e) {
+ $dashlet = new Dashboard\Dashlet($form->getValue('dashlet'), $form->getValue('url'), $pane);
+ $pane->addDashlet($dashlet);
+ }
+ $dashlet->setUserWidget();
+ $dashboardConfig = $dashboard->getConfig();
+ try {
+ $dashboardConfig->saveIni();
+ } catch (Exception $e) {
+ $action->view->error = $e;
+ $action->view->config = $dashboardConfig;
+ $action->render('error');
+ return false;
+ }
+ Notification::success(t('Dashlet updated'));
+ return true;
+ });
+ $form->setTitle($this->translate('Edit Dashlet'));
+ $form->setRedirectUrl('dashboard/settings');
+ $form->handleRequest();
+ $pane = $dashboard->getPane($this->getParam('pane'));
+ $dashlet = $pane->getDashlet($this->getParam('dashlet'));
+ $form->load($dashlet);
+
+ $this->view->form = $form;
+ }
+
+ public function removeDashletAction()
+ {
+ $form = new ConfirmRemovalForm();
+ $this->getTabs()->add('remove-dashlet', array(
+ 'active' => true,
+ 'label' => $this->translate('Remove Dashlet'),
+ 'url' => Url::fromRequest()
+ ));
+ $dashboard = $this->dashboard;
+ if (! $this->_request->getParam('pane')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "pane"',
+ 400
+ );
+ }
+ if (! $this->_request->getParam('dashlet')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "dashlet"',
+ 400
+ );
+ }
+ $pane = $this->_request->getParam('pane');
+ $dashlet = $this->_request->getParam('dashlet');
+ $action = $this;
+ $form->setOnSuccess(function (Form $form) use ($dashboard, $dashlet, $pane, $action) {
+ $pane = $dashboard->getPane($pane);
+ $pane->removeDashlet($dashlet);
+ $dashboardConfig = $dashboard->getConfig();
+ try {
+ $dashboardConfig->saveIni();
+ Notification::success(t('Dashlet has been removed from') . ' ' . $pane->getTitle());
+ } catch (Exception $e) {
+ $action->view->error = $e;
+ $action->view->config = $dashboardConfig;
+ $action->render('error');
+ return false;
+ }
+ return true;
+ });
+ $form->setTitle($this->translate('Remove Dashlet From Dashboard'));
+ $form->setRedirectUrl('dashboard/settings');
+ $form->handleRequest();
+ $this->view->pane = $pane;
+ $this->view->dashlet = $dashlet;
+ $this->view->form = $form;
+ }
+
+ public function renamePaneAction()
+ {
+ $paneName = $this->params->getRequired('pane');
+ if (! $this->dashboard->hasPane($paneName)) {
+ throw new HttpNotFoundException('Pane not found');
+ }
+
+ $form = new Form();
+ $form->setRedirectUrl('dashboard/settings');
+ $form->setSubmitLabel($this->translate('Update Pane'));
+ $form->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Name')
+ )
+ );
+ $form->addElement(
+ 'text',
+ 'title',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Title')
+ )
+ );
+ $form->setDefaults(array(
+ 'name' => $paneName,
+ 'title' => $this->dashboard->getPane($paneName)->getTitle()
+ ));
+ $form->setOnSuccess(function ($form) use ($paneName) {
+ $newName = $form->getValue('name');
+ $newTitle = $form->getValue('title');
+
+ $pane = $this->dashboard->getPane($paneName);
+ $pane->setName($newName);
+ $pane->setTitle($newTitle);
+ $this->dashboard->getConfig()->saveIni();
+
+ Notification::success(
+ sprintf($this->translate('Pane "%s" successfully renamed to "%s"'), $paneName, $newName)
+ );
+ });
+
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->getTabs()->add(
+ 'update-pane',
+ array(
+ 'title' => $this->translate('Update Pane'),
+ 'url' => $this->getRequest()->getUrl()
+ )
+ )->activate('update-pane');
+ }
+
+ public function removePaneAction()
+ {
+ $form = new ConfirmRemovalForm();
+ $this->createTabs();
+ $dashboard = $this->dashboard;
+ if (! $this->_request->getParam('pane')) {
+ throw new Zend_Controller_Action_Exception(
+ 'Missing parameter "pane"',
+ 400
+ );
+ }
+ $pane = $this->_request->getParam('pane');
+ $action = $this;
+ $form->setOnSuccess(function (Form $form) use ($dashboard, $pane, $action) {
+ $pane = $dashboard->getPane($pane);
+ $dashboard->removePane($pane->getName());
+ $dashboardConfig = $dashboard->getConfig();
+ try {
+ $dashboardConfig->saveIni();
+ Notification::success(t('Dashboard has been removed') . ': ' . $pane->getTitle());
+ } catch (Exception $e) {
+ $action->view->error = $e;
+ $action->view->config = $dashboardConfig;
+ $action->render('error');
+ return false;
+ }
+ return true;
+ });
+ $form->setTitle($this->translate('Remove Dashboard'));
+ $form->setRedirectUrl('dashboard/settings');
+ $form->handleRequest();
+ $this->view->pane = $pane;
+ $this->view->form = $form;
+ }
+
+ /**
+ * Display the dashboard with the pane set in the 'pane' request parameter
+ *
+ * If no pane is submitted or the submitted one doesn't exist, the default pane is
+ * displayed (normally the first one)
+ */
+ public function indexAction()
+ {
+ $this->createTabs();
+ if (! $this->dashboard->hasPanes()) {
+ $this->view->title = 'Dashboard';
+ } else {
+ $panes = array_filter(
+ $this->dashboard->getPanes(),
+ function ($pane) {
+ return ! $pane->getDisabled();
+ }
+ );
+ if (empty($panes)) {
+ $this->view->title = 'Dashboard';
+ $this->getTabs()->add('dashboard', array(
+ 'active' => true,
+ 'title' => $this->translate('Dashboard'),
+ 'url' => Url::fromRequest()
+ ));
+ } else {
+ if ($this->_getParam('pane')) {
+ $pane = $this->_getParam('pane');
+ $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;
+ }
+ }
+ }
+ }
+
+ /**
+ * Setting dialog
+ */
+ public function settingsAction()
+ {
+ $this->createTabs();
+ $this->view->dashboard = $this->dashboard;
+ }
+
+ /**
+ * Create tab aggregation
+ */
+ private function createTabs()
+ {
+ $this->view->tabs = $this->dashboard->getTabs()->extend(new DashboardSettings());
+ }
+}
diff --git a/application/controllers/ErrorController.php b/application/controllers/ErrorController.php
new file mode 100644
index 0000000..476b71f
--- /dev/null
+++ b/application/controllers/ErrorController.php
@@ -0,0 +1,176 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Hook\DbMigrationHook;
+use Icinga\Application\MigrationManager;
+use Icinga\Exception\IcingaException;
+use Zend_Controller_Plugin_ErrorHandler;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Exception\Http\HttpExceptionInterface;
+use Icinga\Exception\MissingParameterException;
+use Icinga\Security\SecurityException;
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Url;
+
+/**
+ * Application wide controller for displaying exceptions
+ */
+class ErrorController extends ActionController
+{
+ /**
+ * Regular expression to match exceptions resulting from missing functions/classes
+ */
+ const MISSING_DEP_ERROR =
+ "/Uncaught Error:.*(?:undefined function (\S+)|Class ['\"]([^']+)['\"] not found).* in ([^:]+)/";
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $requiresAuthentication = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->rerenderLayout = $this->params->has('renderLayout');
+ }
+
+ /**
+ * Display exception
+ */
+ public function errorAction()
+ {
+ $error = $this->_getParam('error_handler');
+ $exception = $error->exception;
+ /** @var \Exception $exception */
+
+ if (! ($isAuthenticated = $this->Auth()->isAuthenticated())) {
+ $this->innerLayout = 'guest-error';
+ }
+
+ $modules = Icinga::app()->getModuleManager();
+ $sourcePath = ltrim($this->_request->get('PATH_INFO'), '/');
+ $pathParts = preg_split('~/~', $sourcePath);
+ $moduleName = array_shift($pathParts);
+
+ $module = null;
+ switch ($error->type) {
+ case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE:
+ case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER:
+ case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION:
+ $this->getResponse()->setHttpResponseCode(404);
+ $this->view->messages = array($this->translate('Page not found.'));
+ if ($isAuthenticated) {
+ if ($modules->hasInstalled($moduleName) && ! $modules->hasEnabled($moduleName)) {
+ $this->view->messages[0] .= ' ' . sprintf(
+ $this->translate('Enabling the "%s" module might help!'),
+ $moduleName
+ );
+ }
+ }
+
+ break;
+ default:
+ switch (true) {
+ case $exception instanceof HttpExceptionInterface:
+ $this->getResponse()->setHttpResponseCode($exception->getStatusCode());
+ foreach ($exception->getHeaders() as $name => $value) {
+ $this->getResponse()->setHeader($name, $value, true);
+ }
+ break;
+ case $exception instanceof MissingParameterException:
+ $this->getResponse()->setHttpResponseCode(400);
+ $this->getResponse()->setHeader(
+ 'X-Status-Reason',
+ 'Missing parameter ' . $exception->getParameter()
+ );
+ break;
+ case $exception instanceof SecurityException:
+ $this->getResponse()->setHttpResponseCode(403);
+ break;
+ default:
+ $mm = MigrationManager::instance();
+ $action = $this->getRequest()->getActionName();
+ $controller = $this->getRequest()->getControllerName();
+ if ($action !== 'hint' && $controller !== 'migrations' && $mm->hasMigrations($moduleName)) {
+ // The view renderer from IPL web doesn't render the HTML content set in the respective
+ // controller if the error_handler request param is set, as it doesn't support error
+ // rendering. Since this error handler isn't caused by the migrations controller, we can
+ // safely unset this.
+ $this->setParam('error_handler', null);
+ $this->forward('hint', 'migrations', 'default', [
+ DbMigrationHook::MIGRATION_PARAM => $moduleName
+ ]);
+
+ return;
+ }
+
+ $this->getResponse()->setHttpResponseCode(500);
+ $module = $modules->hasLoaded($moduleName) ? $modules->getModule($moduleName) : null;
+ Logger::error("%s\n%s", $exception, IcingaException::getConfidentialTraceAsString($exception));
+ break;
+ }
+
+ // Try to narrow down why the request has failed
+ if (preg_match(self::MISSING_DEP_ERROR, $exception->getMessage(), $match)) {
+ $sourcePath = $match[3];
+ foreach ($modules->listLoadedModules() as $name) {
+ $candidate = $modules->getModule($name);
+ $modulePath = $candidate->getBaseDir();
+ if (substr($sourcePath, 0, strlen($modulePath)) === $modulePath) {
+ $module = $candidate;
+ break;
+ }
+ }
+
+ if (preg_match('/^(?:Icinga\\\Module\\\(\w+)|(\w+)\\\(\w+))/', $match[1] ?: $match[2], $natch)) {
+ $this->view->requiredModule = isset($natch[1]) ? strtolower($natch[1]) : null;
+ $this->view->requiredVendor = isset($natch[2]) ? $natch[2] : null;
+ $this->view->requiredProject = isset($natch[3]) ? $natch[3] : null;
+ }
+ }
+
+ $this->view->messages = array();
+
+ if ($this->getInvokeArg('displayExceptions')) {
+ $this->view->stackTraces = array();
+
+ do {
+ $this->view->messages[] = $exception->getMessage();
+ $this->view->stackTraces[] = IcingaException::getConfidentialTraceAsString($exception);
+ $exception = $exception->getPrevious();
+ } while ($exception !== null);
+ } else {
+ do {
+ $this->view->messages[] = $exception->getMessage();
+ $exception = $exception->getPrevious();
+ } while ($exception !== null);
+ }
+
+ break;
+ }
+
+ if ($this->getRequest()->isApiRequest()) {
+ $this->getResponse()->json()
+ ->setErrorMessage($this->view->messages[0])
+ ->sendResponse();
+ }
+
+ $this->view->module = $module;
+ $this->view->request = $error->request;
+ if (! $isAuthenticated) {
+ $this->view->hideControls = true;
+ } else {
+ $this->view->hideControls = false;
+ $this->getTabs()->add('error', array(
+ 'active' => true,
+ 'label' => $this->translate('Error'),
+ 'url' => Url::fromRequest()
+ ));
+ }
+ }
+}
diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php
new file mode 100644
index 0000000..d18397c
--- /dev/null
+++ b/application/controllers/GroupController.php
@@ -0,0 +1,418 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Authentication\User\DomainAwareInterface;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Reducible;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Config\UserGroup\AddMemberForm;
+use Icinga\Forms\Config\UserGroup\UserGroupForm;
+use Icinga\User;
+use Icinga\Web\Controller\AuthBackendController;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+use Icinga\Web\Widget;
+
+class GroupController extends AuthBackendController
+{
+ public function init()
+ {
+ $this->view->title = $this->translate('User Groups');
+
+ parent::init();
+ }
+
+ /**
+ * List all user groups of a single backend
+ */
+ public function listAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $this->createListTabs()->activate('group/list');
+ $backendNames = array_map(
+ function ($b) {
+ return $b->getName();
+ },
+ $this->loadUserGroupBackends('Icinga\Data\Selectable')
+ );
+ if (empty($backendNames)) {
+ return;
+ }
+
+ $this->view->backendSelection = new Form();
+ $this->view->backendSelection->setAttrib('class', 'backend-selection icinga-controls');
+ $this->view->backendSelection->setUidDisabled();
+ $this->view->backendSelection->setMethod('GET');
+ $this->view->backendSelection->setTokenDisabled();
+ $this->view->backendSelection->addElement(
+ 'select',
+ 'backend',
+ array(
+ 'autosubmit' => true,
+ 'label' => $this->translate('User Group Backend'),
+ 'multiOptions' => array_combine($backendNames, $backendNames),
+ 'value' => $this->params->get('backend')
+ )
+ );
+
+ $backend = $this->getUserGroupBackend($this->params->get('backend'));
+ if ($backend === null) {
+ $this->view->backend = null;
+ return;
+ }
+
+ $query = $backend->select(array('group_name'));
+
+ $this->view->groups = $query;
+ $this->view->backend = $backend;
+
+ $this->setupPaginationControl($query);
+ $this->setupFilterControl($query);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'group_name' => $this->translate('User Group'),
+ 'created_at' => $this->translate('Created at'),
+ 'last_modified' => $this->translate('Last modified')
+ ),
+ $query
+ );
+ }
+
+ /**
+ * Show a group
+ */
+ public function showAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'));
+
+ $group = $backend->select(array(
+ 'group_name',
+ 'created_at',
+ 'last_modified'
+ ))->where('group_name', $groupName)->fetchRow();
+ if ($group === false) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $members = $backend
+ ->select()
+ ->from('group_membership', array('user_name'))
+ ->where('group_name', $groupName);
+
+ $this->setupFilterControl($members, null, array('user'), array('group'));
+ $this->setupPaginationControl($members);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'user_name' => $this->translate('Username'),
+ 'created_at' => $this->translate('Created at'),
+ 'last_modified' => $this->translate('Last modified')
+ ),
+ $members
+ );
+
+ $this->view->group = $group;
+ $this->view->backend = $backend;
+ $this->view->members = $members;
+ $this->createShowTabs($backend->getName(), $groupName)->activate('group/show');
+
+ if ($this->hasPermission('config/access-control/groups') && $backend instanceof Reducible) {
+ $removeForm = new Form();
+ $removeForm->setUidDisabled();
+ $removeForm->setAttrib('class', 'inline');
+ $removeForm->setAction(
+ Url::fromPath('group/removemember', array('backend' => $backend->getName(), 'group' => $groupName))
+ );
+ $removeForm->addElement('hidden', 'user_name', array(
+ 'isArray' => true,
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('hidden', 'redirect', array(
+ 'value' => Url::fromPath('group/show', array(
+ 'backend' => $backend->getName(),
+ 'group' => $groupName
+ )),
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('button', 'btn_submit', array(
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-button spinner',
+ 'value' => 'btn_submit',
+ 'decorators' => array('ViewHelper'),
+ 'label' => $this->view->icon('trash'),
+ 'title' => $this->translate('Remove this member')
+ ));
+ $this->view->removeForm = $removeForm;
+ }
+ }
+
+ /**
+ * Add a group
+ */
+ public function addAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
+ $form = new UserGroupForm();
+ $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+ $form->add()->handleRequest();
+
+ $this->renderForm($form, $this->translate('New User Group'));
+ }
+
+ /**
+ * Edit a group
+ */
+ public function editAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable');
+
+ $form = new UserGroupForm();
+ $form->setRedirectUrl(
+ Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName))
+ );
+ $form->setRepository($backend);
+
+ try {
+ $form->edit($groupName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $this->renderForm($form, $this->translate('Update User Group'));
+ }
+
+ /**
+ * Remove a group
+ */
+ public function removeAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
+
+ $form = new UserGroupForm();
+ $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+
+ try {
+ $form->remove($groupName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $this->renderForm($form, $this->translate('Remove User Group'));
+ }
+
+ /**
+ * Add a group member
+ */
+ public function addmemberAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
+
+ $form = new AddMemberForm();
+ $form->setDataSource($this->fetchUsers())
+ ->setBackend($backend)
+ ->setGroupName($groupName)
+ ->setRedirectUrl(
+ Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName))
+ )
+ ->setUidDisabled();
+
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $this->renderForm($form, $this->translate('New User Group Member'));
+ }
+
+ /**
+ * Remove a group member
+ */
+ public function removememberAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $this->assertHttpMethod('POST');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
+
+ $form = new Form(array(
+ 'onSuccess' => function ($form) use ($groupName, $backend) {
+ foreach ($form->getValue('user_name') as $userName) {
+ try {
+ $backend->delete(
+ 'group_membership',
+ Filter::matchAll(
+ Filter::where('group_name', $groupName),
+ Filter::where('user_name', $userName)
+ )
+ );
+ Notification::success(sprintf(
+ t('User "%s" has been removed from group "%s"'),
+ $userName,
+ $groupName
+ ));
+ } catch (NotFoundError $e) {
+ throw $e;
+ } catch (Exception $e) {
+ Notification::error($e->getMessage());
+ }
+ }
+
+ $redirect = $form->getValue('redirect');
+ if (! empty($redirect)) {
+ $form->setRedirectUrl(htmlspecialchars_decode($redirect));
+ }
+
+ return true;
+ }
+ ));
+ $form->setUidDisabled();
+ $form->setSubmitLabel('btn_submit'); // Required to ensure that isSubmitted() is called
+ $form->addElement('hidden', 'user_name', array('required' => true, 'isArray' => true));
+ $form->addElement('hidden', 'redirect');
+
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+ }
+
+ /**
+ * Fetch and return all users from all user backends
+ *
+ * @return ArrayDatasource
+ */
+ protected function fetchUsers()
+ {
+ $users = array();
+ foreach ($this->loadUserBackends('Icinga\Data\Selectable') as $backend) {
+ try {
+ if ($backend instanceof DomainAwareInterface) {
+ $domain = $backend->getDomain();
+ } else {
+ $domain = null;
+ }
+ foreach ($backend->select(array('user_name')) as $user) {
+ $userObj = new User($user->user_name);
+ if ($domain !== null) {
+ if ($userObj->hasDomain() && $userObj->getDomain() !== $domain) {
+ // Users listed in a user backend which is configured to be responsible for a domain should
+ // not have a domain in their username. Ultimately, if the username has a domain, it must
+ // not differ from the backend's domain. We could log here - but hey, who cares :)
+ continue;
+ } else {
+ $userObj->setDomain($domain);
+ }
+ }
+
+ $user->user_name = $userObj->getUsername();
+
+ $users[] = $user;
+ }
+ } catch (Exception $e) {
+ Logger::error($e);
+ Notification::warning(sprintf(
+ $this->translate('Failed to fetch any users from backend %s. Please check your log'),
+ $backend->getName()
+ ));
+ }
+ }
+
+ return new ArrayDatasource($users);
+ }
+
+ /**
+ * Create the tabs to display when showing a group
+ *
+ * @param string $backendName
+ * @param string $groupName
+ */
+ protected function createShowTabs($backendName, $groupName)
+ {
+ $tabs = $this->getTabs();
+ $tabs->add(
+ 'group/show',
+ array(
+ 'title' => sprintf($this->translate('Show group %s'), $groupName),
+ 'label' => $this->translate('Group'),
+ 'url' => Url::fromPath('group/show', array('backend' => $backendName, 'group' => $groupName))
+ )
+ );
+
+ return $tabs;
+ }
+
+ /**
+ * Create the tabs to display when listing groups
+ */
+ protected function createListTabs()
+ {
+ $tabs = $this->getTabs();
+
+ if ($this->hasPermission('config/access-control/roles')) {
+ $tabs->add(
+ 'role/list',
+ array(
+ 'baseTarget' => '_main',
+ 'label' => $this->translate('Roles'),
+ 'title' => $this->translate(
+ 'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
+ ),
+ 'url' => 'role/list'
+ )
+ );
+
+ $tabs->add(
+ 'role/audit',
+ [
+ 'title' => $this->translate('Audit a user\'s or group\'s privileges'),
+ 'label' => $this->translate('Audit'),
+ 'url' => 'role/audit',
+ 'baseTarget' => '_main',
+ ]
+ );
+ }
+
+ if ($this->hasPermission('config/access-control/users')) {
+ $tabs->add(
+ 'user/list',
+ array(
+ 'title' => $this->translate('List users of authentication backends'),
+ 'label' => $this->translate('Users'),
+ 'url' => 'user/list'
+ )
+ );
+ }
+
+ $tabs->add(
+ 'group/list',
+ array(
+ 'title' => $this->translate('List groups of user group backends'),
+ 'label' => $this->translate('User Groups'),
+ 'url' => 'group/list'
+ )
+ );
+
+ return $tabs;
+ }
+}
diff --git a/application/controllers/HealthController.php b/application/controllers/HealthController.php
new file mode 100644
index 0000000..f176e69
--- /dev/null
+++ b/application/controllers/HealthController.php
@@ -0,0 +1,65 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Hook\HealthHook;
+use Icinga\Web\View\AppHealth;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use ipl\Html\Html;
+use ipl\Html\HtmlString;
+use ipl\Web\Compat\CompatController;
+
+class HealthController extends CompatController
+{
+ public function indexAction()
+ {
+ $query = HealthHook::collectHealthData()
+ ->select();
+
+ $this->setupSortControl(
+ [
+ 'module' => $this->translate('Module'),
+ 'name' => $this->translate('Name'),
+ 'state' => $this->translate('State')
+ ],
+ $query,
+ ['state' => 'desc']
+ );
+ $this->setupLimitControl();
+ $this->setupPaginationControl($query);
+ $this->setupFilterControl($query, [
+ 'module' => $this->translate('Module'),
+ 'name' => $this->translate('Name'),
+ 'state' => $this->translate('State'),
+ 'message' => $this->translate('Message')
+ ], ['name'], ['format']);
+
+ $this->getTabs()->extend(new OutputFormat(['csv']));
+ $this->handleFormatRequest($query);
+
+ $this->addControl(HtmlString::create((string) $this->view->paginator));
+ $this->addControl(Html::tag('div', ['class' => 'sort-controls-container'], [
+ HtmlString::create((string) $this->view->limiter),
+ HtmlString::create((string) $this->view->sortBox)
+ ]));
+ $this->addControl(HtmlString::create((string) $this->view->filterEditor));
+
+ $this->addTitleTab(t('Health'));
+ $this->setAutorefreshInterval(10);
+ $this->addContent(new AppHealth($query));
+ }
+
+ protected function handleFormatRequest($query)
+ {
+ $formatJson = $this->params->get('format') === 'json';
+ if (! $formatJson && ! $this->getRequest()->isApiRequest()) {
+ return;
+ }
+
+ $this->getResponse()
+ ->json()
+ ->setSuccessData($query->fetchAll())
+ ->sendResponse();
+ }
+}
diff --git a/application/controllers/IframeController.php b/application/controllers/IframeController.php
new file mode 100644
index 0000000..8aebba4
--- /dev/null
+++ b/application/controllers/IframeController.php
@@ -0,0 +1,20 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Web\Controller;
+
+/**
+ * Display external or internal links within an iframe
+ */
+class IframeController extends Controller
+{
+ /**
+ * Display iframe w/ the given URL
+ */
+ public function indexAction()
+ {
+ $this->view->url = $this->params->getRequired('url');
+ }
+}
diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php
new file mode 100644
index 0000000..539c16b
--- /dev/null
+++ b/application/controllers/IndexController.php
@@ -0,0 +1,36 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Url;
+
+/**
+ * Application wide index controller
+ */
+class IndexController extends ActionController
+{
+ /**
+ * Use a default redirection rule to welcome page
+ */
+ public function preDispatch()
+ {
+ if ($this->getRequest()->getActionName() !== 'welcome') {
+ $landingPage = getenv('ICINGAWEB_LANDING_PAGE');
+ if (! $landingPage) {
+ $landingPage = 'dashboard';
+ }
+
+ // @TODO(el): Avoid landing page redirects: https://dev.icinga.com/issues/9656
+ $this->redirectNow(Url::fromRequest()->setPath($landingPage));
+ }
+ }
+
+ /**
+ * Application's start page
+ */
+ public function welcomeAction()
+ {
+ }
+}
diff --git a/application/controllers/LayoutController.php b/application/controllers/LayoutController.php
new file mode 100644
index 0000000..237681c
--- /dev/null
+++ b/application/controllers/LayoutController.php
@@ -0,0 +1,28 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Menu;
+
+/**
+ * Create complex layout parts
+ */
+class LayoutController extends ActionController
+{
+ /**
+ * Render the menu
+ */
+ public function menuAction()
+ {
+ $this->setAutorefreshInterval(15);
+ $this->_helper->layout()->disableLayout();
+ $this->view->menuRenderer = (new Menu())->getRenderer();
+ }
+
+ public function announcementsAction()
+ {
+ $this->_helper->layout()->disableLayout();
+ }
+}
diff --git a/application/controllers/ListController.php b/application/controllers/ListController.php
new file mode 100644
index 0000000..2fbc5a9
--- /dev/null
+++ b/application/controllers/ListController.php
@@ -0,0 +1,59 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Data\ConfigObject;
+use Icinga\Protocol\File\FileReader;
+use Icinga\Web\Controller;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+
+/**
+ * Application wide controller for various listing actions
+ */
+class ListController extends Controller
+{
+ /**
+ * Add title tab
+ *
+ * @param string $action
+ */
+ protected function addTitleTab($action)
+ {
+ $this->getTabs()->add($action, array(
+ 'label' => ucfirst($action),
+ 'url' => Url::fromPath('list/' . str_replace(' ', '', $action))
+ ))->extend(new OutputFormat())->extend(new DashboardAction())->extend(new MenuAction())->activate($action);
+ }
+
+ /**
+ * Display the application log
+ */
+ public function applicationlogAction()
+ {
+ $this->assertPermission('application/log');
+
+ if (! Logger::writesToFile()) {
+ $this->httpNotFound('Page not found');
+ }
+
+ $this->addTitleTab('application log');
+
+ $resource = new FileReader(new ConfigObject(array(
+ 'filename' => Config::app()->get('logging', 'file'),
+ 'fields' => '/(?<!.)(?<datetime>[0-9]{4}(?:-[0-9]{2}){2}' // date
+ . 'T[0-9]{2}(?::[0-9]{2}){2}(?:[\+\-][0-9]{2}:[0-9]{2})?)' // time
+ . ' - (?<loglevel>[A-Za-z]+) - (?<message>.*)(?!.)/msS' // loglevel, message
+ )));
+ $this->view->logData = $resource->select()->order('DESC');
+
+ $this->setupLimitControl();
+ $this->setupPaginationControl($this->view->logData);
+ $this->view->title = $this->translate('Application Log');
+ }
+}
diff --git a/application/controllers/ManageUserDevicesController.php b/application/controllers/ManageUserDevicesController.php
new file mode 100644
index 0000000..db054d1
--- /dev/null
+++ b/application/controllers/ManageUserDevicesController.php
@@ -0,0 +1,84 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Common\Database;
+use Icinga\Web\Notification;
+use Icinga\Web\RememberMe;
+use Icinga\Web\RememberMeUserList;
+use Icinga\Web\RememberMeUserDevicesList;
+use ipl\Web\Compat\CompatController;
+use ipl\Web\Url;
+
+/**
+ * ManageUserDevicesController
+ *
+ * you need 'application/sessions' permission to use this controller
+ */
+class ManageUserDevicesController extends CompatController
+{
+ use Database;
+
+ public function init()
+ {
+ $this->assertPermission('application/sessions');
+ }
+
+ public function indexAction()
+ {
+ $this->getTabs()
+ ->add(
+ 'manage-user-devices',
+ array(
+ 'title' => $this->translate('List of users who stay logged in'),
+ 'label' => $this->translate('Users'),
+ 'url' => 'manage-user-devices',
+ 'data-base-target' => '_self'
+ )
+ )->activate('manage-user-devices');
+
+ $usersList = (new RememberMeUserList())
+ ->setUsers(RememberMe::getAllUser())
+ ->setUrl('manage-user-devices/devices');
+
+ $this->addContent($usersList);
+
+ if (! $this->hasDb()) {
+ Notification::warning(
+ $this->translate("Users can't stay logged in without a database configuration backend")
+ );
+ }
+ }
+
+ public function devicesAction()
+ {
+ $this->getTabs()
+ ->add(
+ 'manage-devices',
+ array(
+ 'title' => $this->translate('List of devices'),
+ 'label' => $this->translate('Devices'),
+ 'url' => 'manage-user-devices/devices'
+ )
+ )->activate('manage-devices');
+
+ $name = $this->params->getRequired('name');
+ $data = (new RememberMeUserDevicesList())
+ ->setDevicesList(RememberMe::getAllByUsername($name))
+ ->setUsername($name)
+ ->setUrl('manage-user-devices/delete');
+
+ $this->addContent($data);
+ }
+
+ public function deleteAction()
+ {
+ (new RememberMe())->remove($this->params->getRequired('fingerprint'));
+
+ $this->redirectNow(
+ Url::fromPath('manage-user-devices/devices')
+ ->addParams(['name' => $this->params->getRequired('name')])
+ );
+ }
+}
diff --git a/application/controllers/MigrationsController.php b/application/controllers/MigrationsController.php
new file mode 100644
index 0000000..5229f06
--- /dev/null
+++ b/application/controllers/MigrationsController.php
@@ -0,0 +1,249 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Hook\DbMigrationHook;
+use Icinga\Application\Icinga;
+use Icinga\Application\MigrationManager;
+use Icinga\Common\Database;
+use Icinga\Exception\MissingParameterException;
+use Icinga\Forms\MigrationForm;
+use Icinga\Web\Notification;
+use Icinga\Web\Widget\ItemList\MigrationList;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use ipl\Html\Attributes;
+use ipl\Html\FormElement\SubmitButtonElement;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\Web\Compat\CompatController;
+use ipl\Web\Widget\ActionLink;
+
+class MigrationsController extends CompatController
+{
+ use Database;
+
+ public function init()
+ {
+ Icinga::app()->getModuleManager()->loadModule('setup');
+ }
+
+ public function indexAction(): void
+ {
+ $mm = MigrationManager::instance();
+
+ $this->getTabs()->extend(new OutputFormat(['csv']));
+ $this->addTitleTab($this->translate('Migrations'));
+
+ $canApply = $this->hasPermission('application/migrations');
+ if (! $canApply) {
+ $this->addControl(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'migration-state-banner']),
+ new HtmlElement(
+ 'span',
+ null,
+ Text::create(
+ $this->translate('You do not have the required permission to apply pending migrations.')
+ )
+ )
+ )
+ );
+ }
+
+ $migrateListForm = new MigrationForm();
+ $migrateListForm->setAttribute('id', $this->getRequest()->protectId('migration-form'));
+ $migrateListForm->setRenderDatabaseUserChange(! $mm->validateDatabasePrivileges());
+
+ if ($canApply && $mm->hasPendingMigrations()) {
+ $migrateAllButton = new SubmitButtonElement(sprintf('migrate-%s', DbMigrationHook::ALL_MIGRATIONS), [
+ 'form' => $migrateListForm->getAttribute('id')->getValue(),
+ 'label' => $this->translate('Migrate All'),
+ 'title' => $this->translate('Migrate all pending migrations')
+ ]);
+
+ // Is the first button, so will be cloned and that the visible
+ // button is outside the form doesn't matter for Web's JS
+ $migrateListForm->registerElement($migrateAllButton);
+
+ // Make sure it looks familiar, even if not inside a form
+ $migrateAllButton->setWrapper(new HtmlElement('div', Attributes::create(['class' => 'icinga-controls'])));
+
+ $this->controls->getAttributes()->add('class', 'default-layout');
+ $this->addControl($migrateAllButton);
+ }
+
+ $this->handleFormatRequest($mm->toArray());
+
+ $frameworkList = new MigrationList($mm->yieldMigrations(), $migrateListForm);
+ $frameworkListControl = new HtmlElement('div', Attributes::create(['class' => 'migration-list-control']));
+ $frameworkListControl->addHtml(new HtmlElement('h2', null, Text::create($this->translate('System'))));
+ $frameworkListControl->addHtml($frameworkList);
+
+ $moduleList = new MigrationList($mm->yieldMigrations(true), $migrateListForm);
+ $moduleListControl = new HtmlElement('div', Attributes::create(['class' => 'migration-list-control']));
+ $moduleListControl->addHtml(new HtmlElement('h2', null, Text::create($this->translate('Modules'))));
+ $moduleListControl->addHtml($moduleList);
+
+ $migrateListForm->addHtml($frameworkListControl, $moduleListControl);
+ if ($canApply && $mm->hasPendingMigrations()) {
+ $frameworkList->ensureAssembled();
+ $moduleList->ensureAssembled();
+
+ $this->handleMigrateRequest($migrateListForm);
+ }
+
+ $migrations = new HtmlElement('div', Attributes::create(['class' => 'migrations']));
+ $migrations->addHtml($migrateListForm);
+
+ $this->addContent($migrations);
+ }
+
+ public function hintAction(): void
+ {
+ // The forwarded request doesn't modify the original server query string, but adds the migration param to the
+ // request param instead. So, there is no way to access the migration param other than via the request instance.
+ /** @var ?string $module */
+ $module = $this->getRequest()->getParam(DbMigrationHook::MIGRATION_PARAM);
+ if ($module === null) {
+ throw new MissingParameterException(
+ $this->translate('Required parameter \'%s\' missing'),
+ DbMigrationHook::MIGRATION_PARAM
+ );
+ }
+
+ $mm = MigrationManager::instance();
+ if (! $mm->hasMigrations($module)) {
+ $this->httpNotFound(sprintf('There are no pending migrations matching the given name: %s', $module));
+ }
+
+ $migration = $mm->getMigration($module);
+ $this->addTitleTab($this->translate('Error'));
+ $this->addContent(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'pending-migrations-hint']),
+ new HtmlElement('h2', null, Text::create($this->translate('Error!'))),
+ new HtmlElement(
+ 'p',
+ null,
+ Text::create(sprintf($this->translate('%s has pending migrations.'), $migration->getName()))
+ ),
+ new HtmlElement('p', null, Text::create($this->translate('Please apply the migrations first.'))),
+ new ActionLink($this->translate('View pending Migrations'), 'migrations')
+ )
+ );
+ }
+
+ public function migrationAction(): void
+ {
+ /** @var string $name */
+ $name = $this->params->getRequired(DbMigrationHook::MIGRATION_PARAM);
+
+ $this->addTitleTab($this->translate('Migration'));
+ $this->getTabs()->disableLegacyExtensions();
+ $this->controls->getAttributes()->add('class', 'default-layout');
+
+ $mm = MigrationManager::instance();
+ if (! $mm->hasMigrations($name)) {
+ $migrations = [];
+ } else {
+ $hook = $mm->getMigration($name);
+ $migrations = array_reverse($hook->getMigrations());
+ if (! $this->hasPermission('application/migrations')) {
+ $this->addControl(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'migration-state-banner']),
+ new HtmlElement(
+ 'span',
+ null,
+ Text::create(
+ $this->translate('You do not have the required permission to apply pending migrations.')
+ )
+ )
+ )
+ );
+ } else {
+ $this->addControl(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'migration-controls']),
+ new HtmlElement('span', null, Text::create($hook->getName()))
+ )
+ );
+ }
+ }
+
+ $migrationWidget = new HtmlElement('div', Attributes::create(['class' => 'migrations']));
+ $migrationWidget->addHtml((new MigrationList($migrations))->setMinimal(false));
+ $this->addContent($migrationWidget);
+ }
+
+ public function handleMigrateRequest(MigrationForm $form): void
+ {
+ $this->assertPermission('application/migrations');
+
+ $form->on(MigrationForm::ON_SUCCESS, function (MigrationForm $form) {
+ $mm = MigrationManager::instance();
+
+ /** @var array<string, string> $elevatedPrivileges */
+ $elevatedPrivileges = $form->getValue('database_setup');
+ if ($elevatedPrivileges !== null && $elevatedPrivileges['grant_privileges'] === 'y') {
+ $mm->fixIcingaWebMysqlGrants($this->getDb(), $elevatedPrivileges);
+ }
+
+ $pressedButton = $form->getPressedSubmitElement();
+ if ($pressedButton) {
+ $name = substr($pressedButton->getName(), 8);
+ switch ($name) {
+ case DbMigrationHook::ALL_MIGRATIONS:
+ if ($mm->applyAll($elevatedPrivileges)) {
+ Notification::success($this->translate('Applied all migrations successfully'));
+ } else {
+ Notification::error(
+ $this->translate(
+ 'Applied migrations successfully. Though, one or more migration hooks'
+ . ' failed to run. See logs for details'
+ )
+ );
+ }
+ break;
+ default:
+ $migration = $mm->getMigration($name);
+ if ($mm->apply($migration, $elevatedPrivileges)) {
+ Notification::success($this->translate('Applied pending migrations successfully'));
+ } else {
+ Notification::error(
+ $this->translate('Failed to apply pending migration(s). See logs for details')
+ );
+ }
+ }
+ }
+
+ $this->sendExtraUpdates(['#col2' => '__CLOSE__']);
+
+ $this->redirectNow('migrations');
+ })->handleRequest($this->getServerRequest());
+ }
+
+ /**
+ * Handle exports
+ *
+ * @param array<string, mixed> $data
+ */
+ protected function handleFormatRequest(array $data): void
+ {
+ $formatJson = $this->params->get('format') === 'json';
+ if (! $formatJson && ! $this->getRequest()->isApiRequest()) {
+ return;
+ }
+
+ $this->getResponse()
+ ->json()
+ ->setSuccessData($data)
+ ->sendResponse();
+ }
+}
diff --git a/application/controllers/MyDevicesController.php b/application/controllers/MyDevicesController.php
new file mode 100644
index 0000000..e0fb98a
--- /dev/null
+++ b/application/controllers/MyDevicesController.php
@@ -0,0 +1,74 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Common\Database;
+use Icinga\Web\Notification;
+use Icinga\Web\RememberMe;
+use Icinga\Web\RememberMeUserDevicesList;
+use ipl\Web\Compat\CompatController;
+
+/**
+ * MyDevicesController
+ *
+ * this controller shows you all the devices you are logged in
+ */
+class MyDevicesController extends CompatController
+{
+ use Database;
+
+ public function init()
+ {
+ $this->getTabs()
+ ->add(
+ 'account',
+ array(
+ 'title' => $this->translate('Update your account'),
+ 'label' => $this->translate('My Account'),
+ 'url' => 'account'
+ )
+ )
+ ->add(
+ 'navigation',
+ array(
+ 'title' => $this->translate('List and configure your own navigation items'),
+ 'label' => $this->translate('Navigation'),
+ 'url' => 'navigation'
+ )
+ )
+ ->add(
+ 'devices',
+ array(
+ 'title' => $this->translate('List of devices you are logged in'),
+ 'label' => $this->translate('My Devices'),
+ 'url' => 'my-devices'
+ )
+ )->activate('devices');
+ }
+
+ public function indexAction()
+ {
+ $name = $this->auth->getUser()->getUsername();
+
+ $data = (new RememberMeUserDevicesList())
+ ->setDevicesList(RememberMe::getAllByUsername($name))
+ ->setUsername($name)
+ ->setUrl('my-devices/delete');
+
+ $this->addContent($data);
+
+ if (! $this->hasDb()) {
+ Notification::warning(
+ $this->translate("Users can't stay logged in without a database configuration backend")
+ );
+ }
+ }
+
+ public function deleteAction()
+ {
+ (new RememberMe())->remove($this->params->getRequired('fingerprint'));
+
+ $this->redirectNow('my-devices');
+ }
+}
diff --git a/application/controllers/NavigationController.php b/application/controllers/NavigationController.php
new file mode 100644
index 0000000..b0babc3
--- /dev/null
+++ b/application/controllers/NavigationController.php
@@ -0,0 +1,447 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Exception\NotFoundError;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Data\Filter\FilterMatchCaseInsensitive;
+use Icinga\Forms\ConfirmRemovalForm;
+use Icinga\Forms\Navigation\NavigationConfigForm;
+use Icinga\Web\Controller;
+use Icinga\Web\Form;
+use Icinga\Web\Menu;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+
+/**
+ * Navigation configuration
+ */
+class NavigationController extends Controller
+{
+ /**
+ * The global navigation item type configuration
+ *
+ * @var array
+ */
+ protected $itemTypeConfig;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ parent::init();
+ $this->itemTypeConfig = Navigation::getItemTypeConfiguration();
+ }
+
+ /**
+ * Return the label for the given navigation item type
+ *
+ * @param string $type
+ *
+ * @return string $type if no label can be found
+ */
+ protected function getItemLabel($type)
+ {
+ return isset($this->itemTypeConfig[$type]['label']) ? $this->itemTypeConfig[$type]['label'] : $type;
+ }
+
+ /**
+ * Return a list of available navigation item types
+ *
+ * @return array
+ */
+ protected function listItemTypes()
+ {
+ $types = array();
+ foreach ($this->itemTypeConfig as $type => $options) {
+ $types[$type] = isset($options['label']) ? $options['label'] : $type;
+ }
+
+ return $types;
+ }
+
+ /**
+ * Return all shared navigation item configurations
+ *
+ * @param string $owner A username if only items shared by a specific user are desired
+ *
+ * @return array
+ */
+ protected function fetchSharedNavigationItemConfigs($owner = null)
+ {
+ $configs = array();
+ foreach ($this->itemTypeConfig as $type => $_) {
+ $config = Config::navigation($type);
+ $config->getConfigObject()->setKeyColumn('name');
+ $query = $config->select();
+ if ($owner !== null) {
+ $query->applyFilter(new FilterMatchCaseInsensitive('owner', '=', $owner));
+ }
+
+ foreach ($query as $itemConfig) {
+ $configs[] = $itemConfig;
+ }
+ }
+
+ return $configs;
+ }
+
+ /**
+ * Return all user navigation item configurations
+ *
+ * @param string $username
+ *
+ * @return array
+ */
+ protected function fetchUserNavigationItemConfigs($username)
+ {
+ $configs = array();
+ foreach ($this->itemTypeConfig as $type => $_) {
+ $config = Config::navigation($type, $username);
+ $config->getConfigObject()->setKeyColumn('name');
+ foreach ($config->select() as $itemConfig) {
+ $configs[] = $itemConfig;
+ }
+ }
+
+ return $configs;
+ }
+
+ /**
+ * Show the current user a list of his/her navigation items
+ */
+ public function indexAction()
+ {
+ $user = $this->Auth()->getUser();
+ $ds = new ArrayDatasource(array_merge(
+ $this->fetchSharedNavigationItemConfigs($user->getUsername()),
+ $this->fetchUserNavigationItemConfigs($user->getUsername())
+ ));
+ $query = $ds->select();
+
+ $this->view->types = $this->listItemTypes();
+ $this->view->items = $query;
+
+ $this->view->title = $this->translate('Navigation');
+ $this->getTabs()
+ ->add(
+ 'account',
+ array(
+ 'title' => $this->translate('Update your account'),
+ 'label' => $this->translate('My Account'),
+ 'url' => 'account'
+ )
+ )
+ ->add(
+ 'navigation',
+ array(
+ 'active' => true,
+ 'title' => $this->translate('List and configure your own navigation items'),
+ 'label' => $this->translate('Navigation'),
+ 'url' => 'navigation'
+ )
+ )
+ ->add(
+ 'devices',
+ array(
+ 'title' => $this->translate('List of devices you are logged in'),
+ 'label' => $this->translate('My Devices'),
+ 'url' => 'my-devices'
+ )
+ );
+ $this->setupSortControl(
+ array(
+ 'type' => $this->translate('Type'),
+ 'owner' => $this->translate('Shared'),
+ 'name' => $this->translate('Navigation')
+ ),
+ $query
+ );
+ }
+
+ /**
+ * List all shared navigation items
+ */
+ public function sharedAction()
+ {
+ $this->assertPermission('config/navigation');
+ $ds = new ArrayDatasource($this->fetchSharedNavigationItemConfigs());
+ $query = $ds->select();
+
+ $removeForm = new Form();
+ $removeForm->setUidDisabled();
+ $removeForm->setAttrib('class', 'inline');
+ $removeForm->addElement('hidden', 'name', array(
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('hidden', 'redirect', array(
+ 'value' => Url::fromPath('navigation/shared'),
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('button', 'btn_submit', array(
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-button spinner',
+ 'value' => 'btn_submit',
+ 'decorators' => array('ViewHelper'),
+ 'label' => $this->view->icon('trash'),
+ 'title' => $this->translate('Unshare this navigation item')
+ ));
+
+ $this->view->removeForm = $removeForm;
+ $this->view->types = $this->listItemTypes();
+ $this->view->items = $query;
+
+ $this->view->title = $this->translate('Shared Navigation');
+ $this->getTabs()->add(
+ 'navigation/shared',
+ array(
+ 'title' => $this->translate('List and configure shared navigation items'),
+ 'label' => $this->translate('Shared Navigation'),
+ 'url' => 'navigation/shared'
+ )
+ )->activate('navigation/shared');
+ $this->setupSortControl(
+ array(
+ 'type' => $this->translate('Type'),
+ 'owner' => $this->translate('Owner'),
+ 'name' => $this->translate('Shared Navigation')
+ ),
+ $query
+ );
+ }
+
+ /**
+ * Add a navigation item
+ */
+ public function addAction()
+ {
+ $form = new NavigationConfigForm();
+ $form->setRedirectUrl('navigation');
+ $form->setUser($this->Auth()->getUser());
+ $form->setItemTypes($this->listItemTypes());
+ $form->addDescription($this->translate('Create a new navigation item, such as a menu entry or dashlet.'));
+
+ // TODO: Fetch all "safe" parameters from the url and populate them
+ $form->setDefaultUrl(rawurldecode($this->params->get('url', '')));
+
+ $form->setOnSuccess(function (NavigationConfigForm $form) {
+ $data = $form::transformEmptyValuesToNull($form->getValues());
+
+ try {
+ $form->add($data);
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ if ($data['type'] === 'menu-item') {
+ $form->getResponse()->setRerenderLayout();
+ }
+
+ Notification::success(t('Navigation item successfully created'));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Navigation');
+ $this->renderForm($form, $this->translate('New Navigation Item'));
+ }
+
+ /**
+ * Edit a navigation item
+ */
+ public function editAction()
+ {
+ $itemName = $this->params->getRequired('name');
+ $itemType = $this->params->getRequired('type');
+ $referrer = $this->params->get('referrer', 'index');
+
+ $user = $this->Auth()->getUser();
+ if ($user->can('config/navigation')) {
+ $itemOwner = $this->params->get('owner', $user->getUsername());
+ } else {
+ $itemOwner = $user->getUsername();
+ }
+
+ $form = new NavigationConfigForm();
+ $form->setUser($user);
+ $form->setShareConfig(Config::navigation($itemType));
+ $form->setUserConfig(Config::navigation($itemType, $itemOwner));
+ $form->setRedirectUrl($referrer === 'shared' ? 'navigation/shared' : 'navigation');
+ $form->setOnSuccess(function (NavigationConfigForm $form) use ($itemName) {
+ $data = $form::transformEmptyValuesToNull($form->getValues());
+
+ try {
+ $form->edit($itemName, $data);
+ } catch (NotFoundError $e) {
+ throw $e;
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ if (isset($data['type']) && $data['type'] === 'menu-item') {
+ $form->getResponse()->setRerenderLayout();
+ }
+
+ Notification::success(sprintf(t('Navigation item "%s" successfully updated'), $itemName));
+ return true;
+ }
+
+ return false;
+ });
+
+ try {
+ $form->load($itemName);
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Navigation item "%s" not found'), $itemName));
+ }
+
+ $this->view->title = $this->translate('Navigation');
+ $this->renderForm($form, $this->translate('Update Navigation Item'));
+ }
+
+ /**
+ * Remove a navigation item
+ */
+ public function removeAction()
+ {
+ $itemName = $this->params->getRequired('name');
+ $itemType = $this->params->getRequired('type');
+ $user = $this->Auth()->getUser();
+
+ $navigationConfigForm = new NavigationConfigForm();
+ $navigationConfigForm->setUser($user);
+ $navigationConfigForm->setShareConfig(Config::navigation($itemType));
+ $navigationConfigForm->setUserConfig(Config::navigation($itemType, $user->getUsername()));
+
+ $form = new ConfirmRemovalForm();
+ $form->setRedirectUrl('navigation');
+ $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($itemName, $navigationConfigForm) {
+ try {
+ $itemConfig = $navigationConfigForm->delete($itemName);
+ } catch (NotFoundError $e) {
+ Notification::success(sprintf(t('Navigation Item "%s" not found. No action required'), $itemName));
+ return true;
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($navigationConfigForm->save()) {
+ if ($itemConfig->type === 'menu-item') {
+ $form->getResponse()->setRerenderLayout();
+ }
+
+ Notification::success(sprintf(t('Navigation Item "%s" successfully removed'), $itemName));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Navigation');
+ $this->renderForm($form, $this->translate('Remove Navigation Item'));
+ }
+
+ /**
+ * Unshare a navigation item
+ */
+ public function unshareAction()
+ {
+ $this->assertPermission('config/navigation');
+ $this->assertHttpMethod('POST');
+
+ // TODO: I'd like these being form fields
+ $itemType = $this->params->getRequired('type');
+ $itemOwner = $this->params->getRequired('owner');
+
+ $navigationConfigForm = new NavigationConfigForm();
+ $navigationConfigForm->setUser($this->Auth()->getUser());
+ $navigationConfigForm->setShareConfig(Config::navigation($itemType));
+ $navigationConfigForm->setUserConfig(Config::navigation($itemType, $itemOwner));
+
+ $form = new Form(array(
+ 'onSuccess' => function ($form) use ($navigationConfigForm) {
+ $itemName = $form->getValue('name');
+
+ try {
+ $newConfig = $navigationConfigForm->unshare($itemName);
+ if ($navigationConfigForm->save()) {
+ if ($newConfig->getSection($itemName)->type === 'menu-item') {
+ $form->getResponse()->setRerenderLayout();
+ }
+
+ Notification::success(sprintf(
+ t('Navigation item "%s" has been unshared'),
+ $form->getValue('name')
+ ));
+ } else {
+ // TODO: It failed obviously to write one of the configs, so we're leaving the user in
+ // a inconsistent state. Luckily, it's nothing lost but possibly duplicated...
+ Notification::error(sprintf(
+ t('Failed to unshare navigation item "%s"'),
+ $form->getValue('name')
+ ));
+ }
+ } catch (NotFoundError $e) {
+ throw $e;
+ } catch (Exception $e) {
+ Notification::error($e->getMessage());
+ }
+
+ $redirect = $form->getValue('redirect');
+ if (! empty($redirect)) {
+ $form->setRedirectUrl(htmlspecialchars_decode($redirect));
+ }
+
+ return true;
+ }
+ ));
+ $form->setUidDisabled();
+ $form->setSubmitLabel('btn_submit'); // Required to ensure that isSubmitted() is called
+ $form->addElement('hidden', 'name', array('required' => true));
+ $form->addElement('hidden', 'redirect');
+
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Navigation item "%s" not found'), $form->getValue('name')));
+ }
+ }
+
+ public function dashboardAction()
+ {
+ $name = $this->params->getRequired('name');
+
+ $this->getTabs()->add('dashboard', array(
+ 'active' => true,
+ 'label' => ucwords($name),
+ 'url' => Url::fromRequest()
+ ));
+
+ $menu = new Menu();
+
+ $navigation = $menu->findItem($name);
+
+ if ($navigation === null) {
+ $this->httpNotFound($this->translate('Navigation not found'));
+ }
+
+ $this->view->navigation = $navigation;
+ $this->view->title = $navigation->getLabel();
+ }
+}
diff --git a/application/controllers/RoleController.php b/application/controllers/RoleController.php
new file mode 100644
index 0000000..4223d33
--- /dev/null
+++ b/application/controllers/RoleController.php
@@ -0,0 +1,392 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use GuzzleHttp\Psr7\ServerRequest;
+use Icinga\Authentication\AdmissionLoader;
+use Icinga\Authentication\Auth;
+use Icinga\Authentication\RolesConfig;
+use Icinga\Authentication\User\DomainAwareInterface;
+use Icinga\Data\Selectable;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Security\RoleForm;
+use Icinga\Repository\Repository;
+use Icinga\Security\SecurityException;
+use Icinga\User;
+use Icinga\Web\Controller\AuthBackendController;
+use Icinga\Web\View\PrivilegeAudit;
+use Icinga\Web\Widget\SingleValueSearchControl;
+use ipl\Html\Html;
+use ipl\Html\HtmlString;
+use ipl\Web\Url;
+use ipl\Web\Widget\Link;
+
+/**
+ * Manage user permissions and restrictions based on roles
+ *
+ * @TODO(el): Rename to RolesController: https://dev.icinga.com/issues/10015
+ */
+class RoleController extends AuthBackendController
+{
+ public function init()
+ {
+ $this->assertPermission('config/access-control/roles');
+ $this->view->title = $this->translate('Roles');
+
+ parent::init();
+ }
+
+ public function indexAction()
+ {
+ if ($this->hasPermission('config/access-control/roles')) {
+ $this->redirectNow('role/list');
+ } elseif ($this->hasPermission('config/access-control/users')) {
+ $this->redirectNow('user/list');
+ } elseif ($this->hasPermission('config/access-control/groups')) {
+ $this->redirectNow('group/list');
+ } else {
+ throw new SecurityException('No permission to configure Icinga Web 2');
+ }
+ }
+
+ /**
+ * List roles
+ *
+ * @TODO(el): Rename to indexAction()
+ */
+ public function listAction()
+ {
+ $this->createListTabs()->activate('role/list');
+ $this->view->roles = (new RolesConfig())
+ ->select();
+
+ $sortAndFilterColumns = [
+ 'name' => $this->translate('Name'),
+ 'users' => $this->translate('Users'),
+ 'groups' => $this->translate('Groups'),
+ 'permissions' => $this->translate('Permissions')
+ ];
+
+ $this->setupFilterControl($this->view->roles, $sortAndFilterColumns, ['name']);
+ $this->setupLimitControl();
+ $this->setupPaginationControl($this->view->roles);
+ $this->setupSortControl($sortAndFilterColumns, $this->view->roles, ['name']);
+ }
+
+ /**
+ * Create a new role
+ *
+ * @TODO(el): Rename to newAction()
+ */
+ public function addAction()
+ {
+ $role = new RoleForm();
+ $role->setRedirectUrl('__CLOSE__');
+ $role->setRepository(new RolesConfig());
+ $role->setSubmitLabel($this->translate('Create Role'));
+ $role->add()->handleRequest();
+
+ $this->renderForm($role, $this->translate('New Role'));
+ }
+
+ /**
+ * Update a role
+ *
+ * @TODO(el): Rename to updateAction()
+ */
+ public function editAction()
+ {
+ $name = $this->params->getRequired('role');
+ $role = new RoleForm();
+ $role->setRedirectUrl('__CLOSE__');
+ $role->setRepository(new RolesConfig());
+ $role->setSubmitLabel($this->translate('Update Role'));
+ $role->edit($name);
+
+ try {
+ $role->handleRequest();
+ } catch (NotFoundError $e) {
+ $this->httpNotFound($this->translate('Role not found'));
+ }
+
+ $this->renderForm($role, $this->translate('Update Role'));
+ }
+
+ /**
+ * Remove a role
+ */
+ public function removeAction()
+ {
+ $name = $this->params->getRequired('role');
+ $role = new RoleForm();
+ $role->setRedirectUrl('__CLOSE__');
+ $role->setRepository(new RolesConfig());
+ $role->setSubmitLabel($this->translate('Remove Role'));
+ $role->remove($name);
+
+ try {
+ $role->handleRequest();
+ } catch (NotFoundError $e) {
+ $this->httpNotFound($this->translate('Role not found'));
+ }
+
+ $this->renderForm($role, $this->translate('Remove Role'));
+ }
+
+ public function auditAction()
+ {
+ $this->createListTabs()->activate('role/audit');
+ $this->view->title = t('Audit');
+
+ $roleName = $this->params->get('role');
+ $type = $this->params->has('group') ? 'group' : 'user';
+ $name = $this->params->get($type);
+
+ $backend = null;
+ if ($type === 'user') {
+ if ($name) {
+ $backend = $this->params->getRequired('backend');
+ } else {
+ $backends = $this->loadUserBackends();
+ if (! empty($backends)) {
+ $backend = array_shift($backends)->getName();
+ }
+ }
+ }
+
+ $form = new SingleValueSearchControl();
+ $form->setMetaDataNames('type', 'backend');
+ $form->populate(['q' => $name, 'q-type' => $type, 'q-backend' => $backend]);
+ $form->setInputLabel(t('Enter user or group name'));
+ $form->setSubmitLabel(t('Inspect'));
+ $form->setSuggestionUrl(Url::fromPath(
+ 'role/suggest-role-member',
+ ['_disableLayout' => true, 'showCompact' => true]
+ ));
+
+ $form->on(SingleValueSearchControl::ON_SUCCESS, function ($form) {
+ $type = $form->getValue('q-type') ?: 'user';
+ $params = [$type => $form->getValue('q')];
+
+ if ($type === 'user') {
+ $params['backend'] = $form->getValue('q-backend');
+ }
+
+ $this->redirectNow(Url::fromPath('role/audit', $params));
+ })->handleRequest(ServerRequest::fromGlobals());
+
+ $this->addControl($form);
+
+ if (! $name) {
+ $this->addContent(Html::wantHtml(t('No user or group selected.')));
+ return;
+ }
+
+ if ($type === 'user') {
+ $header = Html::tag('h2', sprintf(t('Privilege Audit for User "%s"'), $name));
+
+ $user = new User($name);
+ $user->setAdditional('backend_name', $backend);
+ Auth::getInstance()->setupUser($user);
+ } else {
+ $header = Html::tag('h2', sprintf(t('Privilege Audit for Group "%s"'), $name));
+
+ $user = new User((string) time());
+ $user->setGroups([$name]);
+ (new AdmissionLoader())->applyRoles($user);
+ }
+
+ $chosenRole = null;
+ $assignedRoles = array_filter($user->getRoles(), function ($role) use ($user, &$chosenRole, $roleName) {
+ if (! in_array($role->getName(), $user->getAdditional('assigned_roles'), true)) {
+ return false;
+ }
+
+ if ($role->getName() === $roleName) {
+ $chosenRole = $role;
+ }
+
+ return true;
+ });
+
+ $this->addControl(Html::tag(
+ 'ul',
+ ['class' => 'privilege-audit-role-control'],
+ [
+ Html::tag('li', $roleName ? null : ['class' => 'active'], new Link(
+ t('All roles'),
+ Url::fromRequest()->without('role'),
+ ['class' => 'button-link', 'title' => t('Show privileges of all roles')]
+ )),
+ array_map(function ($role) use ($roleName) {
+ return Html::tag(
+ 'li',
+ $role->getName() === $roleName ? ['class' => 'active'] : null,
+ new Link(
+ $role->getName(),
+ Url::fromRequest()->setParam('role', $role->getName()),
+ [
+ 'class' => 'button-link',
+ 'title' => sprintf(t('Only show privileges of role %s'), $role->getName())
+ ]
+ )
+ );
+ }, $assignedRoles)
+ ]
+ ));
+
+ $this->addControl($header);
+ $this->addContent(
+ (new PrivilegeAudit($chosenRole !== null ? [$chosenRole] : $assignedRoles))
+ ->addAttributes(['id' => 'role-audit'])
+ );
+ }
+
+ public function suggestRoleMemberAction()
+ {
+ $this->assertHttpMethod('POST');
+ $requestData = $this->getRequest()->getPost();
+ $limit = $this->params->get('limit', 50);
+
+ $searchTerm = $requestData['term']['label'];
+ $userBackends = $this->loadUserBackends(Selectable::class);
+
+ $suggestions = [];
+ while ($limit > 0 && ! empty($userBackends)) {
+ /** @var Repository $backend */
+ $backend = array_shift($userBackends);
+ $query = $backend->select()
+ ->from('user', ['user_name'])
+ ->where('user_name', $searchTerm)
+ ->limit($limit);
+
+ try {
+ $names = $query->fetchColumn();
+ } catch (Exception $e) {
+ continue;
+ }
+
+ $domain = '';
+ if ($backend instanceof DomainAwareInterface) {
+ $domain = '@' . $backend->getDomain();
+ }
+
+ $users = [];
+ foreach ($names as $name) {
+ $users[] = [$name . $domain, [
+ 'type' => 'user',
+ 'backend' => $backend->getName()
+ ]];
+ }
+
+ if (! empty($users)) {
+ $suggestions[] = [
+ [
+ t('Users'),
+ HtmlString::create('&nbsp;'),
+ Html::tag('span', ['class' => 'badge'], $backend->getName())
+ ],
+ $users
+ ];
+ }
+
+ $limit -= count($names);
+ }
+
+ $groupBackends = $this->loadUserGroupBackends(Selectable::class);
+
+ while ($limit > 0 && ! empty($groupBackends)) {
+ /** @var Repository $backend */
+ $backend = array_shift($groupBackends);
+ $query = $backend->select()
+ ->from('group', ['group_name'])
+ ->where('group_name', $searchTerm)
+ ->limit($limit);
+
+ try {
+ $names = $query->fetchColumn();
+ } catch (Exception $e) {
+ continue;
+ }
+
+ $groups = [];
+ foreach ($names as $name) {
+ $groups[] = [$name, ['type' => 'group']];
+ }
+
+ if (! empty($groups)) {
+ $suggestions[] = [
+ [
+ t('Groups'),
+ HtmlString::create('&nbsp;'),
+ Html::tag('span', ['class' => 'badge'], $backend->getName())
+ ],
+ $groups
+ ];
+ }
+
+ $limit -= count($names);
+ }
+
+ if (empty($suggestions)) {
+ $suggestions[] = [t('Your search does not match any user or group'), []];
+ }
+
+ $this->document->add(SingleValueSearchControl::createSuggestions($suggestions));
+ }
+
+ /**
+ * Create the tabs to display when listing roles
+ */
+ protected function createListTabs()
+ {
+ $tabs = $this->getTabs();
+ $tabs->add(
+ 'role/list',
+ array(
+ 'baseTarget' => '_main',
+ 'label' => $this->translate('Roles'),
+ 'title' => $this->translate(
+ 'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
+ ),
+ 'url' => 'role/list'
+ )
+ );
+
+ $tabs->add(
+ 'role/audit',
+ [
+ 'title' => $this->translate('Audit a user\'s or group\'s privileges'),
+ 'label' => $this->translate('Audit'),
+ 'url' => 'role/audit',
+ 'baseTarget' => '_main'
+ ]
+ );
+
+ if ($this->hasPermission('config/access-control/users')) {
+ $tabs->add(
+ 'user/list',
+ array(
+ 'title' => $this->translate('List users of authentication backends'),
+ 'label' => $this->translate('Users'),
+ 'url' => 'user/list'
+ )
+ );
+ }
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $tabs->add(
+ 'group/list',
+ array(
+ 'title' => $this->translate('List groups of user group backends'),
+ 'label' => $this->translate('User Groups'),
+ 'url' => 'group/list'
+ )
+ );
+ }
+
+ return $tabs;
+ }
+}
diff --git a/application/controllers/SearchController.php b/application/controllers/SearchController.php
new file mode 100644
index 0000000..92aeabe
--- /dev/null
+++ b/application/controllers/SearchController.php
@@ -0,0 +1,28 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Widget;
+use Icinga\Web\Widget\SearchDashboard;
+
+/**
+ * Search controller
+ */
+class SearchController extends ActionController
+{
+ public function indexAction()
+ {
+ $searchDashboard = new SearchDashboard();
+ $searchDashboard->setUser($this->Auth()->getUser());
+ $this->view->dashboard = $searchDashboard->search($this->params->get('q'));
+
+ // NOTE: This renders the dashboard twice. Remove this once we can catch exceptions thrown in view scripts.
+ $this->view->dashboard->render();
+ }
+
+ public function hintAction()
+ {
+ }
+}
diff --git a/application/controllers/StaticController.php b/application/controllers/StaticController.php
new file mode 100644
index 0000000..44a807a
--- /dev/null
+++ b/application/controllers/StaticController.php
@@ -0,0 +1,78 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Controller;
+use Icinga\Web\FileCache;
+
+/**
+ * Deliver static content to clients
+ */
+class StaticController extends Controller
+{
+ /**
+ * Static routes don't require authentication
+ *
+ * @var bool
+ */
+ protected $requiresAuthentication = false;
+
+ /**
+ * Disable layout rendering as this controller doesn't provide any html layouts
+ */
+ public function init()
+ {
+ $this->_helper->viewRenderer->setNoRender(true);
+ $this->_helper->layout()->disableLayout();
+ }
+
+ /**
+ * Return an image from a module's public folder
+ */
+ public function imgAction()
+ {
+ $imgRoot = Icinga::app()
+ ->getModuleManager()
+ ->getModule($this->getParam('module_name'))
+ ->getBaseDir() . '/public/img/';
+
+ $file = $this->getParam('file');
+ $filePath = realpath($imgRoot . $file);
+
+ if ($filePath === false || substr($filePath, 0, strlen($imgRoot)) !== $imgRoot) {
+ $this->httpNotFound('%s does not exist', $file);
+ }
+
+ if (preg_match('/\.([a-z]+)$/i', $file, $m)) {
+ $extension = $m[1];
+ if ($extension === 'svg') {
+ $extension = 'svg+xml';
+ }
+ } else {
+ $extension = 'fixme';
+ }
+
+ $s = stat($filePath);
+ $eTag = sprintf('%x-%x-%x', $s['ino'], $s['size'], (float) str_pad((string) $s['mtime'], 16, '0'));
+
+ $this->getResponse()->setHeader(
+ 'Cache-Control',
+ 'public, max-age=1814400, stale-while-revalidate=604800',
+ true
+ );
+
+ if ($this->getRequest()->getServer('HTTP_IF_NONE_MATCH') === $eTag) {
+ $this->getResponse()
+ ->setHttpResponseCode(304);
+ } else {
+ $this->getResponse()
+ ->setHeader('ETag', $eTag)
+ ->setHeader('Content-Type', 'image/' . $extension, true)
+ ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $s['mtime']) . ' GMT');
+
+ readfile($filePath);
+ }
+ }
+}
diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php
new file mode 100644
index 0000000..dac80d3
--- /dev/null
+++ b/application/controllers/UserController.php
@@ -0,0 +1,374 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Authentication\AdmissionLoader;
+use Icinga\Authentication\User\DomainAwareInterface;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Config\User\CreateMembershipForm;
+use Icinga\Forms\Config\User\UserForm;
+use Icinga\User;
+use Icinga\Web\Controller\AuthBackendController;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+use Icinga\Web\Widget;
+
+class UserController extends AuthBackendController
+{
+ public function init()
+ {
+ $this->view->title = $this->translate('Users');
+
+ parent::init();
+ }
+
+ /**
+ * List all users of a single backend
+ */
+ public function listAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $this->createListTabs()->activate('user/list');
+ $backendNames = array_map(
+ function ($b) {
+ return $b->getName();
+ },
+ $this->loadUserBackends('Icinga\Data\Selectable')
+ );
+ if (empty($backendNames)) {
+ return;
+ }
+
+ $this->view->backendSelection = new Form();
+ $this->view->backendSelection->setAttrib('class', 'backend-selection icinga-controls');
+ $this->view->backendSelection->setUidDisabled();
+ $this->view->backendSelection->setMethod('GET');
+ $this->view->backendSelection->setTokenDisabled();
+ $this->view->backendSelection->addElement(
+ 'select',
+ 'backend',
+ array(
+ 'autosubmit' => true,
+ 'label' => $this->translate('User Backend'),
+ 'multiOptions' => array_combine($backendNames, $backendNames),
+ 'value' => $this->params->get('backend')
+ )
+ );
+
+ $backend = $this->getUserBackend($this->params->get('backend'));
+ if ($backend === null) {
+ $this->view->backend = null;
+ return;
+ }
+
+ $query = $backend->select(array('user_name'));
+
+ $this->view->users = $query;
+ $this->view->backend = $backend;
+
+ $this->setupPaginationControl($query);
+ $this->setupFilterControl($query);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'user_name' => $this->translate('Username'),
+ 'is_active' => $this->translate('Active'),
+ 'created_at' => $this->translate('Created at'),
+ 'last_modified' => $this->translate('Last modified')
+ ),
+ $query
+ );
+ }
+
+ /**
+ * Show a user
+ */
+ public function showAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'));
+
+ $user = $backend->select(array(
+ 'user_name',
+ 'is_active',
+ 'created_at',
+ 'last_modified'
+ ))->where('user_name', $userName)->fetchRow();
+ if ($user === false) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $userObj = new User($userName);
+ if ($backend instanceof DomainAwareInterface) {
+ $userObj->setDomain($backend->getDomain());
+ }
+
+ $memberships = $this->loadMemberships($userObj)->select();
+
+ $this->setupFilterControl(
+ $memberships,
+ array('group_name' => t('User Group')),
+ array('group'),
+ array('user')
+ );
+ $this->setupPaginationControl($memberships);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'group_name' => $this->translate('Group')
+ ),
+ $memberships
+ );
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $extensibleBackends = $this->loadUserGroupBackends('Icinga\Data\Extensible');
+ $this->view->showCreateMembershipLink = ! empty($extensibleBackends);
+ } else {
+ $this->view->showCreateMembershipLink = false;
+ }
+
+ $this->view->user = $user;
+ $this->view->backend = $backend;
+ $this->view->memberships = $memberships;
+ $this->createShowTabs($backend->getName(), $userName)->activate('user/show');
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $removeForm = new Form();
+ $removeForm->setUidDisabled();
+ $removeForm->setAttrib('class', 'inline');
+ $removeForm->addElement('hidden', 'user_name', array(
+ 'isArray' => true,
+ 'value' => $userName,
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('hidden', 'redirect', array(
+ 'value' => Url::fromPath('user/show', array(
+ 'backend' => $backend->getName(),
+ 'user' => $userName
+ )),
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('button', 'btn_submit', array(
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-button spinner',
+ 'value' => 'btn_submit',
+ 'decorators' => array('ViewHelper'),
+ 'label' => $this->view->icon('trash'),
+ 'title' => $this->translate('Cancel this membership')
+ ));
+ $this->view->removeForm = $removeForm;
+ }
+
+ $admissionLoader = new AdmissionLoader();
+ $admissionLoader->applyRoles($userObj);
+ $this->view->userObj = $userObj;
+ $this->view->allowedToEditRoles = $this->hasPermission('config/access-control/groups');
+ }
+
+ /**
+ * Add a user
+ */
+ public function addAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
+ $form = new UserForm();
+ $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+ $form->add()->handleRequest();
+
+ $this->renderForm($form, $this->translate('New User'));
+ }
+
+ /**
+ * Edit a user
+ */
+ public function editAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable');
+
+ $form = new UserForm();
+ $form->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName)));
+ $form->setRepository($backend);
+
+ try {
+ $form->edit($userName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $this->renderForm($form, $this->translate('Update User'));
+ }
+
+ /**
+ * Remove a user
+ */
+ public function removeAction()
+ {
+ $this->assertPermission('config/access-control/users');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
+
+ $form = new UserForm();
+ $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+
+ try {
+ $form->remove($userName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $this->renderForm($form, $this->translate('Remove User'));
+ }
+
+ /**
+ * Create a membership for a user
+ */
+ public function createmembershipAction()
+ {
+ $this->assertPermission('config/access-control/groups');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'));
+
+ if ($backend->select()->where('user_name', $userName)->count() === 0) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $backends = $this->loadUserGroupBackends('Icinga\Data\Extensible');
+ if (empty($backends)) {
+ throw new ConfigurationError($this->translate(
+ 'You\'ll need to configure at least one user group backend first that allows to create new memberships'
+ ));
+ }
+
+ $form = new CreateMembershipForm();
+ $form->setBackends($backends)
+ ->setUsername($userName)
+ ->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName)))
+ ->handleRequest();
+
+ $this->renderForm($form, $this->translate('Create New Membership'));
+ }
+
+ /**
+ * Fetch and return the given user's groups from all user group backends
+ *
+ * @param User $user
+ *
+ * @return ArrayDatasource
+ */
+ protected function loadMemberships(User $user)
+ {
+ $groups = $alreadySeen = array();
+ foreach ($this->loadUserGroupBackends() as $backend) {
+ try {
+ foreach ($backend->getMemberships($user) as $groupName) {
+ if (array_key_exists($groupName, $alreadySeen)) {
+ continue; // Ignore duplicate memberships
+ }
+
+ $alreadySeen[$groupName] = null;
+ $groups[] = (object) array(
+ 'group_name' => $groupName,
+ 'group' => $groupName,
+ 'backend' => $backend
+ );
+ }
+ } catch (Exception $e) {
+ Logger::error($e);
+ Notification::warning(sprintf(
+ $this->translate('Failed to fetch memberships from backend %s. Please check your log'),
+ $backend->getName()
+ ));
+ }
+ }
+
+ return new ArrayDatasource($groups);
+ }
+
+ /**
+ * Create the tabs to display when showing a user
+ *
+ * @param string $backendName
+ * @param string $userName
+ */
+ protected function createShowTabs($backendName, $userName)
+ {
+ $tabs = $this->getTabs();
+ $tabs->add(
+ 'user/show',
+ array(
+ 'title' => sprintf($this->translate('Show user %s'), $userName),
+ 'label' => $this->translate('User'),
+ 'url' => Url::fromPath('user/show', array('backend' => $backendName, 'user' => $userName))
+ )
+ );
+
+ return $tabs;
+ }
+
+ /**
+ * Create the tabs to display when listing users
+ */
+ protected function createListTabs()
+ {
+ $tabs = $this->getTabs();
+
+ if ($this->hasPermission('config/access-control/roles')) {
+ $tabs->add(
+ 'role/list',
+ array(
+ 'baseTarget' => '_main',
+ 'label' => $this->translate('Roles'),
+ 'title' => $this->translate(
+ 'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
+ ),
+ 'url' => 'role/list'
+ )
+ );
+
+ $tabs->add(
+ 'role/audit',
+ [
+ 'title' => $this->translate('Audit a user\'s or group\'s privileges'),
+ 'label' => $this->translate('Audit'),
+ 'url' => 'role/audit',
+ 'baseTarget' => '_main'
+ ]
+ );
+ }
+
+ $tabs->add(
+ 'user/list',
+ array(
+ 'title' => $this->translate('List users of authentication backends'),
+ 'label' => $this->translate('Users'),
+ 'url' => 'user/list'
+ )
+ );
+
+ if ($this->hasPermission('config/access-control/groups')) {
+ $tabs->add(
+ 'group/list',
+ array(
+ 'title' => $this->translate('List groups of user group backends'),
+ 'label' => $this->translate('User Groups'),
+ 'url' => 'group/list'
+ )
+ );
+ }
+
+ return $tabs;
+ }
+}
diff --git a/application/controllers/UsergroupbackendController.php b/application/controllers/UsergroupbackendController.php
new file mode 100644
index 0000000..a96ab75
--- /dev/null
+++ b/application/controllers/UsergroupbackendController.php
@@ -0,0 +1,133 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Controllers;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\Config\UserGroup\UserGroupBackendForm;
+use Icinga\Forms\ConfirmRemovalForm;
+use Icinga\Web\Controller;
+use Icinga\Web\Notification;
+
+/**
+ * Controller to configure user group backends
+ */
+class UsergroupbackendController extends Controller
+{
+ /**
+ * Initialize this controller
+ */
+ public function init()
+ {
+ $this->assertPermission('config/access-control/users');
+ }
+
+ /**
+ * Redirect to this controller's list action
+ */
+ public function indexAction()
+ {
+ $this->redirectNow('config/userbackend');
+ }
+
+ /**
+ * Create a new user group backend
+ */
+ public function createAction()
+ {
+ $form = new UserGroupBackendForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->addDescription($this->translate('Create a new backend to associate users and groups with.'));
+ $form->setIniConfig(Config::app('groups'));
+ $form->setOnSuccess(function (UserGroupBackendForm $form) {
+ try {
+ $form->add($form::transformEmptyValuesToNull($form->getValues()));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(t('User group backend successfully created'));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('New User Group Backend'));
+ }
+
+ /**
+ * Edit an user group backend
+ */
+ public function editAction()
+ {
+ $backendName = $this->params->getRequired('backend');
+
+ $form = new UserGroupBackendForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->setIniConfig(Config::app('groups'));
+ $form->setOnSuccess(function (UserGroupBackendForm $form) use ($backendName) {
+ try {
+ $form->edit($backendName, $form::transformEmptyValuesToNull($form->getValues()));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(sprintf(t('User group backend "%s" successfully updated'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+
+ try {
+ $form->load($backendName);
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $backendName));
+ }
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('Update User Group Backend'));
+ }
+
+ /**
+ * Remove a user group backend
+ */
+ public function removeAction()
+ {
+ $backendName = $this->params->getRequired('backend');
+
+ $backendForm = new UserGroupBackendForm();
+ $backendForm->setIniConfig(Config::app('groups'));
+ $form = new ConfirmRemovalForm();
+ $form->setRedirectUrl('config/userbackend');
+ $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($backendName, $backendForm) {
+ try {
+ $backendForm->delete($backendName);
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($backendForm->save()) {
+ Notification::success(sprintf(t('User group backend "%s" successfully removed'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->title = $this->translate('Authentication');
+ $this->renderForm($form, $this->translate('Remove User Group Backend'));
+ }
+}
diff --git a/application/fonts/fontello-ifont/LICENSE.txt b/application/fonts/fontello-ifont/LICENSE.txt
new file mode 100644
index 0000000..b4cfe3f
--- /dev/null
+++ b/application/fonts/fontello-ifont/LICENSE.txt
@@ -0,0 +1,57 @@
+Font license info
+
+
+## Font Awesome
+
+ Copyright (C) 2016 by Dave Gandy
+
+ Author: Dave Gandy
+ License: SIL ()
+ Homepage: http://fortawesome.github.com/Font-Awesome/
+
+
+## Iconic
+
+ Copyright (C) 2012 by P.J. Onori
+
+ Author: P.J. Onori
+ License: SIL (http://scripts.sil.org/OFL)
+ Homepage: http://somerandomdude.com/work/iconic/
+
+
+## Entypo
+
+ Copyright (C) 2012 by Daniel Bruce
+
+ Author: Daniel Bruce
+ License: SIL (http://scripts.sil.org/OFL)
+ Homepage: http://www.entypo.com
+
+
+## Fontelico
+
+ Copyright (C) 2012 by Fontello project
+
+ Author: Crowdsourced, for Fontello project
+ License: SIL (http://scripts.sil.org/OFL)
+ Homepage: http://fontello.com
+
+
+## Typicons
+
+ (c) Stephen Hutchings 2012
+
+ Author: Stephen Hutchings
+ License: SIL (http://scripts.sil.org/OFL)
+ Homepage: http://typicons.com/
+
+
+## MFG Labs
+
+ Copyright (C) 2012 by Daniel Bruce
+
+ Author: MFG Labs
+ License: SIL (http://scripts.sil.org/OFL)
+ Homepage: http://www.mfglabs.com/
+
+
diff --git a/application/fonts/fontello-ifont/README.txt b/application/fonts/fontello-ifont/README.txt
new file mode 100644
index 0000000..beaab33
--- /dev/null
+++ b/application/fonts/fontello-ifont/README.txt
@@ -0,0 +1,75 @@
+This webfont is generated by http://fontello.com open source project.
+
+
+================================================================================
+Please, note, that you should obey original font licenses, used to make this
+webfont pack. Details available in LICENSE.txt file.
+
+- Usually, it's enough to publish content of LICENSE.txt file somewhere on your
+ site in "About" section.
+
+- If your project is open-source, usually, it will be ok to make LICENSE.txt
+ file publicly available in your repository.
+
+- Fonts, used in Fontello, don't require a clickable link on your site.
+ But any kind of additional authors crediting is welcome.
+================================================================================
+
+
+Comments on archive content
+---------------------------
+
+- /font/* - fonts in different formats
+
+- /css/* - different kinds of css, for all situations. Should be ok with
+ twitter bootstrap. Also, you can skip <i> style and assign icon classes
+ directly to text elements, if you don't mind about IE7.
+
+- demo.html - demo file, to show your webfont content
+
+- LICENSE.txt - license info about source fonts, used to build your one.
+
+- config.json - keeps your settings. You can import it back into fontello
+ anytime, to continue your work
+
+
+Why so many CSS files ?
+-----------------------
+
+Because we like to fit all your needs :)
+
+- basic file, <your_font_name>.css - is usually enough, it contains @font-face
+ and character code definitions
+
+- *-ie7.css - if you need IE7 support, but still don't wish to put char codes
+ directly into html
+
+- *-codes.css and *-ie7-codes.css - if you like to use your own @font-face
+ rules, but still wish to benefit from css generation. That can be very
+ convenient for automated asset build systems. When you need to update font -
+ no need to manually edit files, just override old version with archive
+ content. See fontello source code for examples.
+
+- *-embedded.css - basic css file, but with embedded WOFF font, to avoid
+ CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain.
+ We strongly recommend to resolve this issue by `Access-Control-Allow-Origin`
+ server headers. But if you ok with dirty hack - this file is for you. Note,
+ that data url moved to separate @font-face to avoid problems with <IE9, when
+ string is too long.
+
+- animate.css - use it to get ideas about spinner rotation animation.
+
+
+Attention for server setup
+--------------------------
+
+You MUST setup server to reply with proper `mime-types` for font files -
+otherwise some browsers will fail to show fonts.
+
+Usually, `apache` already has necessary settings, but `nginx` and other
+webservers should be tuned. Here is list of mime types for our file extensions:
+
+- `application/vnd.ms-fontobject` - eot
+- `application/x-font-woff` - woff
+- `application/x-font-ttf` - ttf
+- `image/svg+xml` - svg
diff --git a/application/fonts/fontello-ifont/config.json b/application/fonts/fontello-ifont/config.json
new file mode 100644
index 0000000..a982335
--- /dev/null
+++ b/application/fonts/fontello-ifont/config.json
@@ -0,0 +1,874 @@
+{
+ "name": "ifont",
+ "css_prefix_text": "icon-",
+ "css_use_suffix": false,
+ "hinting": true,
+ "units_per_em": 1000,
+ "ascent": 850,
+ "glyphs": [
+ {
+ "uid": "9bc2902722abb366a213a052ade360bc",
+ "css": "spin6",
+ "code": 59508,
+ "src": "fontelico"
+ },
+ {
+ "uid": "9dd9e835aebe1060ba7190ad2b2ed951",
+ "css": "search",
+ "code": 59484,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "8b80d36d4ef43889db10bc1f0dc9a862",
+ "css": "user",
+ "code": 59393,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "31972e4e9d080eaa796290349ae6c1fd",
+ "css": "users",
+ "code": 59394,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "b1887b423d2fd15c345e090320c91ca0",
+ "css": "dashboard",
+ "code": 59392,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "ce3cf091d6ebd004dd0b52d24074e6e3",
+ "css": "help",
+ "code": 59483,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3d4ea8a78dc34efe891f3a0f3d961274",
+ "css": "info",
+ "code": 59482,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "d7271d490b71df4311e32cdacae8b331",
+ "css": "home",
+ "code": 59481,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "0d6ab6194c0eddda2b8c9cedf2ab248e",
+ "css": "attach",
+ "code": 59498,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "c1f1975c885aa9f3dad7810c53b82074",
+ "css": "lock",
+ "code": 59480,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "657ab647f6248a6b57a5b893beaf35a9",
+ "css": "lock-open",
+ "code": 59479,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "05376be04a27d5a46e855a233d6e8508",
+ "css": "lock-open-alt",
+ "code": 59478,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "c5fd349cbd3d23e4ade333789c29c729",
+ "css": "eye",
+ "code": 59475,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "7fd683b2c518ceb9e5fa6757f2276faa",
+ "css": "eye-off",
+ "code": 59491,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3db5347bd219f3bce6025780f5d9ef45",
+ "css": "tag",
+ "code": 59476,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "a3f89e106175a5c5c4e9738870b12e55",
+ "css": "tags",
+ "code": 59477,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "acf41aa4018e58d49525665469e35665",
+ "css": "thumbs-up",
+ "code": 59495,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "7533e68038fc6d520ede7a7ffa0a2f64",
+ "css": "thumbs-down",
+ "code": 59496,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9a76bc135eac17d2c8b8ad4a5774fc87",
+ "css": "download",
+ "code": 59400,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "eeec3208c90b7b48e804919d0d2d4a41",
+ "css": "upload",
+ "code": 59401,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "c6be5a58ee4e63a5ec399c2b0d15cf2c",
+ "css": "reply",
+ "code": 59473,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "1b5597a3bacaeca6600e88ae36d02e0a",
+ "css": "reply-all",
+ "code": 59474,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3d39c828009c04ddb6764c0b04cd2439",
+ "css": "forward",
+ "code": 59472,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "41087bc74d4b20b55059c60a33bf4008",
+ "css": "edit",
+ "code": 59471,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "7277ded7695b2a307a5f9d50097bb64c",
+ "css": "print",
+ "code": 59470,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "ecb97add13804c190456025e43ec003b",
+ "css": "keyboard",
+ "code": 59499,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "85528017f1e6053b2253785c31047f44",
+ "css": "comment",
+ "code": 59464,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "dcedf50ab1ede3283d7a6c70e2fe32f3",
+ "css": "chat",
+ "code": 59465,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9c1376672bb4f1ed616fdd78a23667e9",
+ "css": "comment-empty",
+ "code": 59463,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "31951fbb9820ed0690f675b3d495c8da",
+ "css": "chat-empty",
+ "code": 59466,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "cd21cbfb28ad4d903cede582157f65dc",
+ "css": "bell",
+ "code": 59467,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "671f29fa10dda08074a4c6a341bb4f39",
+ "css": "bell-alt",
+ "code": 59468,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "563683020e0bf9f22f3f055a69b5c57a",
+ "css": "bell-off",
+ "code": 59488,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "8a074400a056c59d389f2d0517281bd5",
+ "css": "bell-off-empty",
+ "code": 59489,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "00391fac5d419345ffcccd95b6f76263",
+ "css": "attention-alt",
+ "code": 59469,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "f48ae54adfb27d8ada53d0fd9e34ee10",
+ "css": "trash",
+ "code": 59462,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "5408be43f7c42bccee419c6be53fdef5",
+ "css": "doc-text",
+ "code": 59461,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9daa1fdf0838118518a7e22715e83abc",
+ "css": "file-pdf",
+ "code": 59458,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "310ffd629da85142bc8669f010556f2d",
+ "css": "file-word",
+ "code": 59459,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "f761c3bbe16ba2d332914ecb28e7a042",
+ "css": "file-excel",
+ "code": 59460,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9f7e588c66cfd6891f6f507cf6f6596b",
+ "css": "phone",
+ "code": 59457,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "559647a6f430b3aeadbecd67194451dd",
+ "css": "menu",
+ "code": 59500,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "e99461abfef3923546da8d745372c995",
+ "css": "service",
+ "code": 59456,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "98687378abd1faf8f6af97c254eb6cd6",
+ "css": "services",
+ "code": 59455,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "5bb103cd29de77e0e06a52638527b575",
+ "css": "wrench",
+ "code": 59453,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "21b42d3c3e6be44c3cc3d73042faa216",
+ "css": "sliders",
+ "code": 59454,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "531bc468eecbb8867d822f1c11f1e039",
+ "css": "calendar",
+ "code": 59452,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "ead4c82d04d7758db0f076584893a8c1",
+ "css": "calendar-empty",
+ "code": 59451,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3a00327e61b997b58518bd43ed83c3df",
+ "css": "endtime",
+ "code": 59449,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "0d20938846444af8deb1920dc85a29fb",
+ "css": "starttime",
+ "code": 59450,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "19c50c52858a81de58f9db488aba77bc",
+ "css": "mic",
+ "code": 59448,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "43c629249e2cca7e73cd4ef410c9551f",
+ "css": "mute",
+ "code": 59447,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "e44601720c64e6bb6a2d5cba6b0c588c",
+ "css": "volume-off",
+ "code": 59446,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "fee6e00f36e8ca8ef3e4a62caa213bf6",
+ "css": "volume-down",
+ "code": 59445,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "76857a03fbaa6857fe063b6c25aa98ed",
+ "css": "volume-up",
+ "code": 59444,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "598a5f2bcf3521d1615de8e1881ccd17",
+ "css": "clock",
+ "code": 59443,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "5278ef7773e948d56c4d442c8c8c98cf",
+ "css": "lightbulb",
+ "code": 59442,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "98d9c83c1ee7c2c25af784b518c522c5",
+ "css": "block",
+ "code": 59440,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "e594fc6e5870b4ab7e49f52571d52577",
+ "css": "resize-full",
+ "code": 59434,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "b013f6403e5ab0326614e68d1850fd6b",
+ "css": "resize-full-alt",
+ "code": 59433,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3c24ee33c9487bbf18796ca6dffa1905",
+ "css": "resize-small",
+ "code": 59435,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "d3b3f17bc3eb7cd809a07bbd4d178bee",
+ "css": "resize-vertical",
+ "code": 59438,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3c73d058e4589b65a8d959c0fc8f153d",
+ "css": "resize-horizontal",
+ "code": 59437,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "6605ee6441bf499ffa3c63d3c7409471",
+ "css": "move",
+ "code": 59436,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "0b2b66e526028a6972d51a6f10281b4b",
+ "css": "zoom-in",
+ "code": 59439,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "d25d10efa900f529ad1d275657cfd30e",
+ "css": "zoom-out",
+ "code": 59441,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "2d6150442079cbda7df64522dc24f482",
+ "css": "down-dir",
+ "code": 59421,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "80cd1022bd9ea151d554bec1fa05f2de",
+ "css": "up-dir",
+ "code": 59422,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9dc654095085167524602c9acc0c5570",
+ "css": "left-dir",
+ "code": 59423,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "fb1c799ffe5bf8fb7f8bcb647c8fe9e6",
+ "css": "right-dir",
+ "code": 59424,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "ccddff8e8670dcd130e3cb55fdfc2fd0",
+ "css": "down-open",
+ "code": 59425,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "d870630ff8f81e6de3958ecaeac532f2",
+ "css": "left-open",
+ "code": 59428,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "399ef63b1e23ab1b761dfbb5591fa4da",
+ "css": "right-open",
+ "code": 59426,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "fe6697b391355dec12f3d86d6d490397",
+ "css": "up-open",
+ "code": 59427,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "745f12abe1472d14f8f658de7e5aba66",
+ "css": "angle-double-left",
+ "code": 59514,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "fdfbd1fcbd4cb229716a810801a5f207",
+ "css": "angle-double-right",
+ "code": 59515,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "1c4068ed75209e21af36017df8871802",
+ "css": "down-big",
+ "code": 59432,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "555ef8c86832e686fef85f7af2eb7cde",
+ "css": "left-big",
+ "code": 59431,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "ad6b3fbb5324abe71a9c0b6609cbb9f1",
+ "css": "right-big",
+ "code": 59430,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "95376bf082bfec6ce06ea1cda7bd7ead",
+ "css": "up-big",
+ "code": 59429,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "d407a4707f719b042ed2ad28d2619d7e",
+ "css": "barchart",
+ "code": 59420,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "cd4bfdae4dc89b175ff49330ce29611a",
+ "css": "wifi",
+ "code": 59501,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "500fc1f109021e4b1de4deda2f7ed399",
+ "css": "host",
+ "code": 59494,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "197375a3cea8cb90b02d06e4ddf1433d",
+ "css": "globe",
+ "code": 59417,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "2c413e78faf1d6631fd7b094d14c2253",
+ "css": "cloud",
+ "code": 59418,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3212f42c65d41ed91cb435d0490e29ed",
+ "css": "flash",
+ "code": 59419,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "567e3e257f2cc8fba2c12bf691c9f2d8",
+ "css": "moon",
+ "code": 59502,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "8772331a9fec983cdb5d72902a6f9e0e",
+ "css": "scissors",
+ "code": 59416,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "b429436ec5a518c78479d44ef18dbd60",
+ "css": "paste",
+ "code": 59415,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "8b9e6a8dd8f67f7c003ed8e7e5ee0857",
+ "css": "off",
+ "code": 59413,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9755f76110ae4d12ac5f9466c9152031",
+ "css": "book",
+ "code": 59414,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "130380e481a7defc690dfb24123a1f0c",
+ "css": "circle",
+ "code": 59516,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "266d5d9adf15a61800477a5acf9a4462",
+ "css": "chart-bar",
+ "code": 59505,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "7d1ca956f4181a023de4b9efbed92524",
+ "css": "chart-area",
+ "code": 59504,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "554ee96588a6c9ee632624cd051fe6fc",
+ "css": "chart-pie",
+ "code": 59503,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "ea2d9a8c51ca42b38ef0d2a07f16d9a7",
+ "css": "chart-line",
+ "code": 59487,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3e674995cacc2b09692c096ea7eb6165",
+ "css": "megaphone",
+ "code": 59409,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "7432077e6a2d6aa19984ca821bb6bbda",
+ "css": "bug",
+ "code": 59410,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "9396b2d8849e0213a0f11c5fd7fcc522",
+ "css": "tasks",
+ "code": 59411,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "4109c474ff99cad28fd5a2c38af2ec6f",
+ "css": "filter",
+ "code": 59412,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "0f444c61b0d2c9966016d7ddb12f5837",
+ "css": "beaker",
+ "code": 59506,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "ff70f7b3228702e0d590e60ed3b90bea",
+ "css": "magic",
+ "code": 59507,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "3ed68ae14e9cde775121954242a412b2",
+ "css": "sort-name-up",
+ "code": 59407,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "6586267200a42008a9fc0a1bf7ac06c7",
+ "css": "sort-name-down",
+ "code": 59408,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "0bda4bc779d4c32623dec2e43bd67ee8",
+ "css": "gauge",
+ "code": 59405,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "6fe95ffc3c807e62647d4f814a96e0d7",
+ "css": "sitemap",
+ "code": 59406,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "cda0cdcfd38f5f1d9255e722dad42012",
+ "css": "spinner",
+ "code": 59497,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "af95ef0ddda80a78828c62d386506433",
+ "css": "cubes",
+ "code": 59403,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "347c38a8b96a509270fdcabc951e7571",
+ "css": "database",
+ "code": 59404,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "a14be0c7e0689076e2bdde97f8e309f9",
+ "css": "plug",
+ "code": 59490,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "4743b088aa95d6f3b6b990e770d3b647",
+ "css": "facebook-squared",
+ "code": 59519,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "e7cb72a17f3b21e3576f35c3f0a7639b",
+ "css": "git",
+ "code": 59402,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "f0cf7db1b03cb65adc450aa3bdaf8c4d",
+ "css": "gplus-squared",
+ "code": 59520,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "627abcdb627cb1789e009c08e2678ef9",
+ "css": "twitter",
+ "code": 59518,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "7e4164950ffa4990961958b2d6318658",
+ "css": "info-circled",
+ "code": 59517,
+ "src": "entypo"
+ },
+ {
+ "uid": "465bb89b6f204234e5787c326b4ae54c",
+ "css": "rewind",
+ "code": 59486,
+ "src": "entypo"
+ },
+ {
+ "uid": "bb46b15cb78cc4cc05d3d715d522ac4d",
+ "css": "cw",
+ "code": 59493,
+ "src": "entypo"
+ },
+ {
+ "uid": "3bd18d47a12b8709e9f4fe9ead4f7518",
+ "css": "reschedule",
+ "code": 59524,
+ "src": "entypo"
+ },
+ {
+ "uid": "p57wgnf4glngbchbucdi029iptu8oxb8",
+ "css": "pin",
+ "code": 59513,
+ "src": "typicons"
+ },
+ {
+ "uid": "c16a63e911bc47b46dc2a7129d2f0c46",
+ "css": "down-small",
+ "code": 59509,
+ "src": "typicons"
+ },
+ {
+ "uid": "58b78b6ca784d5c3db5beefcd9e18061",
+ "css": "left-small",
+ "code": 59510,
+ "src": "typicons"
+ },
+ {
+ "uid": "877a233d7fdca8a1d82615b96ed0d7a2",
+ "css": "right-small",
+ "code": 59511,
+ "src": "typicons"
+ },
+ {
+ "uid": "62bc6fe2a82e4864e2b94d4c0985ee0c",
+ "css": "up-small",
+ "code": 59512,
+ "src": "typicons"
+ },
+ {
+ "uid": "11e664deed5b2587456a4f9c01d720b6",
+ "css": "cancel",
+ "code": 59396,
+ "src": "iconic"
+ },
+ {
+ "uid": "dbd39eb5a1d67beb54cfcb535e840e0f",
+ "css": "plus",
+ "code": 59397,
+ "src": "iconic"
+ },
+ {
+ "uid": "9559f17a471856ef50ed266e726cfa25",
+ "css": "minus",
+ "code": 59398,
+ "src": "iconic"
+ },
+ {
+ "uid": "13ea1e82d38c7ed614d9ee85e9c42053",
+ "css": "folder-empty",
+ "code": 59399,
+ "src": "iconic"
+ },
+ {
+ "uid": "8f28d948aa6379b1a69d2a090e7531d4",
+ "css": "warning-empty",
+ "code": 59525,
+ "src": "typicons"
+ },
+ {
+ "uid": "d4816c0845aa43767213d45574b3b145",
+ "css": "history",
+ "code": 61914,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "b035c28eba2b35c6ffe92aee8b0df507",
+ "css": "attention-circled",
+ "code": 59521,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "73ffeb70554099177620847206c12457",
+ "css": "binoculars",
+ "code": 61925,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "a73c5deb486c8d66249811642e5d719a",
+ "css": "arrows-cw",
+ "code": 59492,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "dd6c6b221a1088ff8a9b9cd32d0b3dd5",
+ "css": "check",
+ "code": 59523,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "b90d80c250a9bbdd6cd3fe00e6351710",
+ "css": "ok",
+ "code": 59395,
+ "src": "iconic"
+ },
+ {
+ "uid": "37c5ab63f10d7ad0b84d0978dcd0c7a8",
+ "css": "flapping",
+ "code": 59485,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "0f6a2573a7b6df911ed199bb63717e27",
+ "css": "github-circled",
+ "code": 61595,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "5eb43711f62fb4dcbef10d0224c28065",
+ "css": "th-thumb-empty",
+ "code": 61451,
+ "src": "mfglabs"
+ },
+ {
+ "uid": "1b58555745e7378f7634ee7c63eada46",
+ "css": "th-list",
+ "code": 61449,
+ "src": "mfglabs"
+ },
+ {
+ "uid": "63b3012c8cbe3654ba5bea598235aa3a",
+ "css": "angle-double-up",
+ "code": 61698,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "dfec4ffa849d8594c2e4b86f6320b8a6",
+ "css": "angle-double-down",
+ "code": 61699,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "f3f90c8c89795da30f7444634476ea4f",
+ "css": "angle-left",
+ "code": 61700,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "7bf14281af5633a597f85b061ef1cfb9",
+ "css": "angle-right",
+ "code": 61701,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "5de9370846a26947e03f63142a3f1c07",
+ "css": "angle-up",
+ "code": 61702,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "e4dde1992f787163e2e2b534b8c8067d",
+ "css": "angle-down",
+ "code": 61703,
+ "src": "fontawesome"
+ }
+ ]
+} \ No newline at end of file
diff --git a/application/fonts/fontello-ifont/css/animation.css b/application/fonts/fontello-ifont/css/animation.css
new file mode 100644
index 0000000..ac5a956
--- /dev/null
+++ b/application/fonts/fontello-ifont/css/animation.css
@@ -0,0 +1,85 @@
+/*
+ Animation example, for spinners
+*/
+.animate-spin {
+ -moz-animation: spin 2s infinite linear;
+ -o-animation: spin 2s infinite linear;
+ -webkit-animation: spin 2s infinite linear;
+ animation: spin 2s infinite linear;
+ display: inline-block;
+}
+@-moz-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@-webkit-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@-o-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@-ms-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
diff --git a/application/fonts/fontello-ifont/css/ifont-codes.css b/application/fonts/fontello-ifont/css/ifont-codes.css
new file mode 100644
index 0000000..74856f3
--- /dev/null
+++ b/application/fonts/fontello-ifont/css/ifont-codes.css
@@ -0,0 +1,145 @@
+
+.icon-dashboard:before { content: '\e800'; } /* '' */
+.icon-user:before { content: '\e801'; } /* '' */
+.icon-users:before { content: '\e802'; } /* '' */
+.icon-ok:before { content: '\e803'; } /* '' */
+.icon-cancel:before { content: '\e804'; } /* '' */
+.icon-plus:before { content: '\e805'; } /* '' */
+.icon-minus:before { content: '\e806'; } /* '' */
+.icon-folder-empty:before { content: '\e807'; } /* '' */
+.icon-download:before { content: '\e808'; } /* '' */
+.icon-upload:before { content: '\e809'; } /* '' */
+.icon-git:before { content: '\e80a'; } /* '' */
+.icon-cubes:before { content: '\e80b'; } /* '' */
+.icon-database:before { content: '\e80c'; } /* '' */
+.icon-gauge:before { content: '\e80d'; } /* '' */
+.icon-sitemap:before { content: '\e80e'; } /* '' */
+.icon-sort-name-up:before { content: '\e80f'; } /* '' */
+.icon-sort-name-down:before { content: '\e810'; } /* '' */
+.icon-megaphone:before { content: '\e811'; } /* '' */
+.icon-bug:before { content: '\e812'; } /* '' */
+.icon-tasks:before { content: '\e813'; } /* '' */
+.icon-filter:before { content: '\e814'; } /* '' */
+.icon-off:before { content: '\e815'; } /* '' */
+.icon-book:before { content: '\e816'; } /* '' */
+.icon-paste:before { content: '\e817'; } /* '' */
+.icon-scissors:before { content: '\e818'; } /* '' */
+.icon-globe:before { content: '\e819'; } /* '' */
+.icon-cloud:before { content: '\e81a'; } /* '' */
+.icon-flash:before { content: '\e81b'; } /* '' */
+.icon-barchart:before { content: '\e81c'; } /* '' */
+.icon-down-dir:before { content: '\e81d'; } /* '' */
+.icon-up-dir:before { content: '\e81e'; } /* '' */
+.icon-left-dir:before { content: '\e81f'; } /* '' */
+.icon-right-dir:before { content: '\e820'; } /* '' */
+.icon-down-open:before { content: '\e821'; } /* '' */
+.icon-right-open:before { content: '\e822'; } /* '' */
+.icon-up-open:before { content: '\e823'; } /* '' */
+.icon-left-open:before { content: '\e824'; } /* '' */
+.icon-up-big:before { content: '\e825'; } /* '' */
+.icon-right-big:before { content: '\e826'; } /* '' */
+.icon-left-big:before { content: '\e827'; } /* '' */
+.icon-down-big:before { content: '\e828'; } /* '' */
+.icon-resize-full-alt:before { content: '\e829'; } /* '' */
+.icon-resize-full:before { content: '\e82a'; } /* '' */
+.icon-resize-small:before { content: '\e82b'; } /* '' */
+.icon-move:before { content: '\e82c'; } /* '' */
+.icon-resize-horizontal:before { content: '\e82d'; } /* '' */
+.icon-resize-vertical:before { content: '\e82e'; } /* '' */
+.icon-zoom-in:before { content: '\e82f'; } /* '' */
+.icon-block:before { content: '\e830'; } /* '' */
+.icon-zoom-out:before { content: '\e831'; } /* '' */
+.icon-lightbulb:before { content: '\e832'; } /* '' */
+.icon-clock:before { content: '\e833'; } /* '' */
+.icon-volume-up:before { content: '\e834'; } /* '' */
+.icon-volume-down:before { content: '\e835'; } /* '' */
+.icon-volume-off:before { content: '\e836'; } /* '' */
+.icon-mute:before { content: '\e837'; } /* '' */
+.icon-mic:before { content: '\e838'; } /* '' */
+.icon-endtime:before { content: '\e839'; } /* '' */
+.icon-starttime:before { content: '\e83a'; } /* '' */
+.icon-calendar-empty:before { content: '\e83b'; } /* '' */
+.icon-calendar:before { content: '\e83c'; } /* '' */
+.icon-wrench:before { content: '\e83d'; } /* '' */
+.icon-sliders:before { content: '\e83e'; } /* '' */
+.icon-services:before { content: '\e83f'; } /* '' */
+.icon-service:before { content: '\e840'; } /* '' */
+.icon-phone:before { content: '\e841'; } /* '' */
+.icon-file-pdf:before { content: '\e842'; } /* '' */
+.icon-file-word:before { content: '\e843'; } /* '' */
+.icon-file-excel:before { content: '\e844'; } /* '' */
+.icon-doc-text:before { content: '\e845'; } /* '' */
+.icon-trash:before { content: '\e846'; } /* '' */
+.icon-comment-empty:before { content: '\e847'; } /* '' */
+.icon-comment:before { content: '\e848'; } /* '' */
+.icon-chat:before { content: '\e849'; } /* '' */
+.icon-chat-empty:before { content: '\e84a'; } /* '' */
+.icon-bell:before { content: '\e84b'; } /* '' */
+.icon-bell-alt:before { content: '\e84c'; } /* '' */
+.icon-attention-alt:before { content: '\e84d'; } /* '' */
+.icon-print:before { content: '\e84e'; } /* '' */
+.icon-edit:before { content: '\e84f'; } /* '' */
+.icon-forward:before { content: '\e850'; } /* '' */
+.icon-reply:before { content: '\e851'; } /* '' */
+.icon-reply-all:before { content: '\e852'; } /* '' */
+.icon-eye:before { content: '\e853'; } /* '' */
+.icon-tag:before { content: '\e854'; } /* '' */
+.icon-tags:before { content: '\e855'; } /* '' */
+.icon-lock-open-alt:before { content: '\e856'; } /* '' */
+.icon-lock-open:before { content: '\e857'; } /* '' */
+.icon-lock:before { content: '\e858'; } /* '' */
+.icon-home:before { content: '\e859'; } /* '' */
+.icon-info:before { content: '\e85a'; } /* '' */
+.icon-help:before { content: '\e85b'; } /* '' */
+.icon-search:before { content: '\e85c'; } /* '' */
+.icon-flapping:before { content: '\e85d'; } /* '' */
+.icon-rewind:before { content: '\e85e'; } /* '' */
+.icon-chart-line:before { content: '\e85f'; } /* '' */
+.icon-bell-off:before { content: '\e860'; } /* '' */
+.icon-bell-off-empty:before { content: '\e861'; } /* '' */
+.icon-plug:before { content: '\e862'; } /* '' */
+.icon-eye-off:before { content: '\e863'; } /* '' */
+.icon-arrows-cw:before { content: '\e864'; } /* '' */
+.icon-cw:before { content: '\e865'; } /* '' */
+.icon-host:before { content: '\e866'; } /* '' */
+.icon-thumbs-up:before { content: '\e867'; } /* '' */
+.icon-thumbs-down:before { content: '\e868'; } /* '' */
+.icon-spinner:before { content: '\e869'; } /* '' */
+.icon-attach:before { content: '\e86a'; } /* '' */
+.icon-keyboard:before { content: '\e86b'; } /* '' */
+.icon-menu:before { content: '\e86c'; } /* '' */
+.icon-wifi:before { content: '\e86d'; } /* '' */
+.icon-moon:before { content: '\e86e'; } /* '' */
+.icon-chart-pie:before { content: '\e86f'; } /* '' */
+.icon-chart-area:before { content: '\e870'; } /* '' */
+.icon-chart-bar:before { content: '\e871'; } /* '' */
+.icon-beaker:before { content: '\e872'; } /* '' */
+.icon-magic:before { content: '\e873'; } /* '' */
+.icon-spin6:before { content: '\e874'; } /* '' */
+.icon-down-small:before { content: '\e875'; } /* '' */
+.icon-left-small:before { content: '\e876'; } /* '' */
+.icon-right-small:before { content: '\e877'; } /* '' */
+.icon-up-small:before { content: '\e878'; } /* '' */
+.icon-pin:before { content: '\e879'; } /* '' */
+.icon-angle-double-left:before { content: '\e87a'; } /* '' */
+.icon-angle-double-right:before { content: '\e87b'; } /* '' */
+.icon-circle:before { content: '\e87c'; } /* '' */
+.icon-info-circled:before { content: '\e87d'; } /* '' */
+.icon-twitter:before { content: '\e87e'; } /* '' */
+.icon-facebook-squared:before { content: '\e87f'; } /* '' */
+.icon-gplus-squared:before { content: '\e880'; } /* '' */
+.icon-attention-circled:before { content: '\e881'; } /* '' */
+.icon-check:before { content: '\e883'; } /* '' */
+.icon-reschedule:before { content: '\e884'; } /* '' */
+.icon-warning-empty:before { content: '\e885'; } /* '' */
+.icon-th-list:before { content: '\f009'; } /* '' */
+.icon-th-thumb-empty:before { content: '\f00b'; } /* '' */
+.icon-github-circled:before { content: '\f09b'; } /* '' */
+.icon-angle-double-up:before { content: '\f102'; } /* '' */
+.icon-angle-double-down:before { content: '\f103'; } /* '' */
+.icon-angle-left:before { content: '\f104'; } /* '' */
+.icon-angle-right:before { content: '\f105'; } /* '' */
+.icon-angle-up:before { content: '\f106'; } /* '' */
+.icon-angle-down:before { content: '\f107'; } /* '' */
+.icon-history:before { content: '\f1da'; } /* '' */
+.icon-binoculars:before { content: '\f1e5'; } /* '' */ \ No newline at end of file
diff --git a/application/fonts/fontello-ifont/css/ifont-embedded.css b/application/fonts/fontello-ifont/css/ifont-embedded.css
new file mode 100644
index 0000000..beb6f9a
--- /dev/null
+++ b/application/fonts/fontello-ifont/css/ifont-embedded.css
@@ -0,0 +1,198 @@
+@font-face {
+ font-family: 'ifont';
+ src: url('../font/ifont.eot?21447335');
+ src: url('../font/ifont.eot?21447335#iefix') format('embedded-opentype'),
+ url('../font/ifont.svg?21447335#ifont') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'ifont';
+ src: url('data:application/octet-stream;base64,d09GRgABAAAAAGwoAA8AAAAAtQwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABWAAAADsAAABUIIslek9TLzIAAAGUAAAAQwAAAFY+IVLUY21hcAAAAdgAAAMtAAAJesmSl21jdnQgAAAFCAAAABMAAAAgBtf/AmZwZ20AAAUcAAAFkAAAC3CKkZBZZ2FzcAAACqwAAAAIAAAACAAAABBnbHlmAAAKtAAAWTwAAJDm6DgeXmhlYWQAAGPwAAAAMwAAADYZdM73aGhlYQAAZCQAAAAgAAAAJAf3BOVobXR4AABkRAAAAOIAAAJE6Mr/lmxvY2EAAGUoAAABJAAAASS9sOQFbWF4cAAAZkwAAAAgAAAAIAIIDb5uYW1lAABmbAAAAXcAAAKpxRR69HBvc3QAAGfkAAADyAAABlAML0mAcHJlcAAAa6wAAAB6AAAAhuVBK7x4nGNgZGBg4GIwYLBjYHJx8wlh4MtJLMljkGJgYYAAkDwymzEnMz2RgQPGA8qxgGkOIGaDiAIAJjsFSAB4nGNgZI5nnMDAysDAVMW0h4GBoQdCMz5gMGRkAooysDIzYAUBaa4pDA4vGD4+ZQ76n8UQxRzMMB0ozAiSAwDvugx8AHic5dbHdltlGIXhV7ZjYxxaCBA6mN5777333nvvPXRCEnrxPFNGXA3jzLPWHkpXEN7js5nAJSCtx5L+gc9ZWt+3t4ANwLzO1ALM/cnEd0z+8HSyfj7PvuvnC5Otft7EgZ7MZVt2TpenK9Nds6XZ7tmevXshZHvP5v45+9djwqbJ5smWPlcnq+tnc/7HBe9kkSX2YdnrrbCR/difA7zaQV7zYDZzCIdyGFs4nCM4kqM4mmM4luM4nlVO4ERO4mRO4VRO43TO8L7P4mzO4VzO43wu4EIu4mIu4VIu43Ku4Equ4mqu4Vqu43pu4EZu4mZu4VZu43bu4E7u4m7u4V7u434e4EEe4mEe4VEe43Ge4Eme4mme4Vme43le4EVe4mVe4VVe43Xe4E3e4m3e4V3e430+4EM+4mM+YSuf8hmf8wVf8hVf8w3b+Jbt7GAn3/E9P/AjP/Ezv/Arv/E7a35Bi//5Hv9/j43Dn8W/+mltmLbRMKUpJ4nUMNWpYbJTw8SnnDhSzh4pp5CU80hq2ISUM0pquLuUc0vKCSblLJNyqkk536ScdFLOPCmnn5R7QMqNIOVukHJLSLkvpNwcUu4QKbeJlHtFyg0j5a6RcutIuX+k3ERS7iQpt5OUe0rKjSXl7pJyi0m5z6TcbFLuOKkhvVLuPSkTgJRZQMpUIGU+kDIpSJkZpEwPUuYIKROFlNlCypQhZd6QMnlImUGkTCNS5hIpE4qUWUXK1CJlfpEyyUiZaaRMN1LmHCkTj5TZR8oUJGUekjIZSZmRpExLUuYmKROUlFlKylQlZb6SMmlJmbmkTF9S5jApE5mU2UzKlCZlXpMyuUmZ4aRMc1LmOikTnpRZT8rUJ2X+k7IJSNkJpGwHUvYEKRuDlN1ByhYhZZ+QsllI2TGkbBtS9g4pG4iUXUTKViJlP5GyqUjZWaRsL1L2GCkbjZTdRsqWI2XfkbL5SNmBpGxDUvYiKRuSlF1JytYkZX+Sskn9DTJieN0xYnjdObJnmS6PbFymKyO7l+mukS3MbG5kHzObH9nMzBZGdjSzDSPbmtniyN5mtjSywZntHtnlzPaMWPsbqOC/vgAAAHicY2BAAxIQyBz8PxOEARJwA90AeJytVml300YUHXlJnIQsJQstamHExGmwRiZswYAJQbJjIF2crZWgixQ76b7xid/gX/Nk2nPoN35a7xsvJJC053Cak6N3583VzNtlElqS2AvrkZSbL8XU1iaN7DwJ6YZNy1F8KDt7IWWKyd8FURCtltq3HYdERCJQta6wRBD7HlmaZHzoUUbLtqRXTcotPekuW+NBvVXffho6yrE7oaRmM3RoPbIlVRhVokimPVLSpmWo+itJK7y/wsxXzVDCiE4iabwZxtBI3htntMpoNbbjKIpsstwoUiSa4UEUeZTVEufkigkMygfNkPLKpxHlw/yIrNijnFawS7bT/L4vead3OT+xX29RtuRAH8iO7ODsdCVfhFtbYdy0k+0oVBF213dCbNnsVP9mj/KaRgO3KzK90IxgqXyFECs/ocz+IVktnE/5kkejWrKRE0HrZU7sSz6B1uOIKXHNGFnQ3dEJEdT9kjMM9pg+Hvzx3imWCxMCeBzLekclnAgTKWFzNEnaMHJgJWWLKqn1rpg45XVaxFvCfu3a0ZfOaONQd2I8Ww8dWzlRyfFoUqeZTJ3aSc2jKQ2ilHQmeMyvAyg/oklebWM1iZVH0zhmxoREIgIt3EtTQSw7saQpBM2jGb25G6a5di1apMkD9dyj9/TmVri501PaDvSzRn9Wp2I62AvT6WnkL/Fp2uUiRen66Rl+TOJB1gIykS02w5SDB2/9DtLL15YchdcG2O7t8yuofdZE8KQB+xvQHk/VKQlMhZhViFZAYq1rWZbJ1awWqcjUd0OaVr6s0wSKchwXx76Mcf1fMzOWmBK+34nTsyMuPXPtSwjTHHybdT2a16nFcgFxZnlOp1mW7+s0x/IDneZZntfpCEtbp6MsP9RpgeVHOh1jeUELmnTfwZCLMOQCDpAwhKUDQ1hegiEsFQxhuQhDWBZhCMslGMLyYxjCchmGsLysZdXUU0nj2plYBmxCYGKOHrnMReVqKrlUQrtoVGpDnhJulVQUz6p/ZaBePPKGObAWSJfIml8xzpWPRuX41hUtbxo7V8Cx6m8fjvY58VLWi4U/Bf/V1lQlvWLNw5Or8BuGnmwnqjapeHRNl89VPbr+X1RUWAv0G0iFWCjKsmxwZyKEjzqdhmqglUPMbMw8tOt1y5qfw/03MUIWUP34NxQaC9yDTllJWe3grNXX27LcO4NyOBMsSTE38/pW+CIjs9J+kVnKno98HnAFjEpl2GoDrRW82ScxD5neJM8EcVtRNkja2M4EiQ0c84B5850EJmHqqg3kTuGGDfgFYW7BeSdconqjLIfuRezzKKT8W6fiRPaoaIzAs9kbYa/vQspvcQwkNPmlfgxUFaGpGDUV0DRSbqgGX8bZum1Cxg70Iyp2w7Ks4sPHFveVkm0ZhHykiNWjo5/WXqJOqtx+ZhSX752+BcEgNTF/e990cZDKu1rJMkdtA1O3GpVT15pD41WH6uZR9b3j7BM5a5puuiceel/TqtvBxVwssPZtDtJSJhfU9WGFDaLLxaVQ6mU0Se+4BxgWGNDvUIqN/6v62HyeK1WF0XEk307Ut9HnYAz8D9h/R/UD0Pdj6HINLs/3mhOfbvThbJmuohfrp+g3MGutuVm6BtzQdAPiIUetjrjKDXynBnF6pLkc6SHgY90V4gHAJoDF4BPdtYzmUwCj+Yw5PsDnzGHQZA6DLeYw2GbOGsAOcxjsMofBHnMYfMGcdYAvmcMgZA6DiDkMnjAnAHjKHAZfMYfB18xh8A1z7gN8yxwGMXMYJMxhsK/p1jDMLV7QXaC2QVWgA1NPWNzD4lBTZcj+jheG/b1BzP7BIKb+qOn2kPoTLwz1Z4OY+otBTP1V050h9TdeGOrvBjH1D4OY+ky/GMtlBr+MfJcKB5RdbD7n74n3D9vFQLkAAQAB//8AD3icxL0LeBvHdTA6Z/a9izcWC5AEQeJBgAQpiAJBQCIpCqIokaIomaIomZRlmn5IlkU9HEexXUdyXMvXv52kUuLm4TiOYyV2kqZO61fz+pukt3XS1k1TN72VkzatGydNFefGTVsnt9FvwfecWYCi/O7//9+9Eri7szM7O3PmzHnNObPMYOyVX0tnJYv5WRtbydaxS9gV7Ah7DzvFLqlOBr1cC3i4Kmnqgt/gkk/nHCS+YCkcGIMZOgObNWXOgE2efN+dJ259943vPLy47+r5y3bt2Lalv/6vL6S0dndEbFVLp7K5/lI52ld0wpjO1dMVTMOr8indDW56ENz065VfC29envIr9fdRPqVFfqLdeTyagNce9y1L1O5/oxyRcNrfoFQ9Ay5c3nfQwWTkoNMOCVAX6RRdpBK1v1yWwzOLVJ4OtW//VwuBvnSfMY5j+xh/WvKwCEuwjmqKKaAckQBkOMJkLh/BEvwIY2xfOBoMRouq0tzdYavpZCrbXxqWok6xUkxIkq2mClBOAH9606paZtUmM5YfXrH56Yn8SDaunzr25C3y7Y/csXFodnaod2bXUCeMj2eHZ3bBH83eeuujt/FjjKmvvPLKAXmlNM2CrMgG2SjbRThWPXiJj0scxphl6IalHwqAzg2dL/qBS4hrixooiFUKHPKYHFSGIGOqjL9FH0hezJDYQhAMwzPOZNkrb56/fPfcrpnpqS0Tm8c2jKwbHljTFLGbKulwMkDYBwJXKqW+YgIqxYoascHta640rOBNPgzroIw9lqOYkcqWyoghtlqAYe4ohEjZXLm/lHP6isMQLeaWimzZPbBlRRU2yvmRZLZD4iemN9Rio1MgewLt2YGkmimMT21q6groqdXZ9oAPzn9r5sYZ/MHdAjxP3gIbhgtb1uxeIXV0JEc75Y1j9fx5abSn55tOM3gjgW21y0a2bRtJrB5ZXco6sXgzdwLNJneypdUjcX5qkB6Yqf3r3K38li/erN7xN90FGJXWbwtEvLEY1LMRluyVX0mn+LeYwtQnZAYruhWoRCEKC7XHH3gUPvBJE7Y++HvwwQeZKPus1Mb/mVlYVgMs26HltFwlV4lWoprUdv8LP7v/hRfu/9kL979w9BM/+9knXnhBHBGbxLOflE5Jrfhsb3UFkyVENWBwvapwiTFpms4S24UkRGIT+IDFrCD905Smbogkg+lgsj8Z7AtKp2qPPVd7DC55Dr7zXO1RmHoOLqk9RvVjLafgO/h4otqCLwQ2Te/dRcQIqEaJSUFJiXRX+pORkHT8X597Tty80K4gzonR6nrGdUQyrsxjtiwxeR4LqZqkzhuAfdam8aTBLMPEZCjk8YQioYgd9gQ9wWAo3Bcwqb3JYH3KBJNKBNvdD8FkEJ6Bb4wUzl9dGIGv1x7nn6jh+fzV/Avnr145MrJS8l7/3PVHzl8teoMvFjT4NM4PHduVQxqMLRsCTTcYxxk6ho3QJV2TDjFVkiVVPqQATmKGsFzAZ7nM9zBd9+ib163NdDipUMeaWMgkclsqgA+cYagsXRDBTLoEsi+J83sdJIuO5PhBTa2EbAWpYp20apG+In/aTtg81hz7Lbs9xJ14bFO78/JfCIoG0pbkruQkSE77F83QOTNhngsaZvSU4zvlc+BU7Bq/eJDb/sbF+58QxOsJp32yHX/QGQ2cs6xzgWjknN8Gx3euDofHEA4FAYcuVmWbqhv6QVPrcGCGahzRcR6rR5gmaUdE52eWA0PmswSPybVD6b50qngBElkfT+Dkbpwj9ZkvGE0CkBmIXkvIHpZ4EB6QHhTbwHkTQLzoKadPpcreFxEQRuyU7T+FvTkVDQcFTEKtPoeH2kNys6dxcfcTRJ/xAG2dnW0JmHbq/e/BRxCKhNoqwuGA9MeIDypy5UE2xhbZUXauGpubDgdlyRxfxf3S5h6u+aWxACiwacujvqnZ6qXMEwzonr1M1bmu8kNMMv2m5D/ENL/h14xDjBlgMDiEs5+bCl/wWlz3gSnp5pX4GoP5jQUWCARnWTCIJBW5xKxLV1uqs1Rz0BM49L+76rlq+p03HDm8eN21V1915RXzl+2evXRyYsPI2qHBgTWVbCaVTiZDSgzHL52y+4pSKZtLRfAiHFE1hwbKh8OVUKJ2OlXg/aVKf5YOBcjhsBajxXC63B8s5frUSNDuiKhYSFoLfTiwuZTW39cvcvoiqSwNel+RaD3yAAcm98wuzCRT7Rs3jnzEjhnTGx2nOVss5B3+F9nR4cy+bLqzBAe2lQvlXx7j/JgE051r0sV4QAZLkzyRsvwe6Qq9XV/Zm6r9XU+1B3pG8srqd8IPUz2wfQvA9Zoai266xqvY0aDjM22nt+3zvvahrj1Jyeld65e884Wt+6G5trJpFcz0h8PF2u+suvqwE0sM9GSeQuHrQD5hRzce4N/ezNPtRegd6YUiYxrhjPwNxBkJr0maC7MozqD+anFtL071SDgUDPh9Xss0dE1VkO2yGCPey7lnHOcP87LNlXK2oy0hKXa3UuknUq9B/ZxrpBHoLrGonxDWOIfWgTjQZElAK+DMgL1nzmSef/6H0no8//CHz8fw9Ktf/UraaYfP+TK+Gkh48p4L2xCz/z0WPudNeM/ZsX+3Y2sXH92zbuHKK2ufql9s2PPhPeuuO3CgduNP7ZRxXNffA4DH40bK/mkko+9/zsnoi6q6aKSiz+3XM6xORw5IlyEsfKyV9bC1JGus8UgoxOaT2PGWEFdkacwC1gVyMwobm5imItlnh3QSapExLph4VLisLFigqp5xAziXZpkkeaXNhRVtiYAfWLl/xdrC2mxHoqetJ+r4WwOtusZ84PMg/IieRGwfpAg+CQhfnKy4Sd7ITIC2lFsaBvjCHT8ZueHPfvz0YWnkJ//HG10ffepG7iaOPgUP9668IjuSxd8VK3trM5jKUSqHqTNuFs8Pd2KKn8C7Y24mnQTePCad5X8iaG4aodXPhtkUu6Z65QrgahnlMC3DiFOP6ZjWVK7hrJeQRy4agLxSYosy8lyUzhZJUuCWuqAAmCYgXcbzLDPBnNw6uRHn9JrVpb7eld35XDZsh0MenNdIj1NqgpfDePYh6pSjlXKBUMuHRJ/oNCJkIwuWyixd2BpR8RwJqxzltzP5gR7eOZw9hH9yodz7szVSoDkurU6WdatndsJrNMOu/FAn71ldqN1bL/Or+vn3LzHtl//dNi+ZuuMP/+wP75j6q0ReFAWnfvGLv/U1q57A30o9ye7JWKE7We1oFPnN+sUl9fM/fvBYMPjue/Z9+a5t2+76cl0eekzoWDvZtuoW5rE8R7xgmdYRv6EpkgKcAHxIRyWKHUayybk5gyeT71KBm3xiZvqSrZMTCMLBgdVIt5LBDvrrC9RVqoZKE8UsZNtOdJka9Ab38JDsT1/QlKKC60WSLuN3pLN1paW/9qP+ZarRq5N5nO0X3YDW1h7AO/ctcbnFxUT0DRJYdFFIBJROwGI+sZioz91P4tw1WQoxcRukqv4ool+ph6t6FsCQxrY8GkZ2txEnLSLkXhlhpHJzMQymYZhXI7y9Huadt0MBn+zxg6V7rHlEblXT1fkgSBIyIMOAOUJPL2xu2fKojZVtep3KVNNY/K/WVh1zK+KH/hdrmpurdq5bl06v27Zu29ZJVGnGxzZtHN0wsr6aHk4Prx0MBptsO51JBCKk2vSh9NafRpKsgV0X5nBcg+7gRoI4pCSaholQp0nGSSNWSDjPIkLkC2O6L5fW+ugc7gvzj13VqaKejzSv9nfq44pPfULXvYumvqib+IPv1r76HU1RddW8fQOs/Y6sq7pive9dp/PNdzXnH8zddtlt/PAtzQa3TFM9v0nVHleUJ4yIxHTT1M8zc+L2IuQsBdG+elux9j3ZREmbj8Avh4ampoaG4B21U0s0vIEHRbYVClXPpuGo5JPK4A3yOhJMML8v5PPvtSGEIvnVzPJylD0I+MzLUWwKMq8V9F6BFSKF1+aZzyfNBgSwwx5Tl1UVdhlkwriACVuW1egLBRf/l6qsTrq1hQ7976iOcKKvL53u29q3dXKziw39pXQxXVzVu7Kwogc1qAs40XEBJ5Q3xImLx11q4EhKzS1hCSFJX/9boASXlw2+5aIHoKriIohH1T/e5yKEob3nNfjwwcHBqanBQbihdpKfONEHOVNR6FWEHd/34+UGBA5/5ZVXHhE0tMBKbGt1ohdkpQhM7kPxf6Xfkrgkj0WAjzJFVkgzQJVTArJVHUIR57DQEWeIFs+SJjrZg2wo1R4OB1QlitzaQfJHJDCbQz0pityF4yGbK8jlCtkMhLpU7EA6KQWxoCOdbs3nW3viP/1Be1a2LdnT3OwE9yw0yc2mT9b10Qxyqw7w/AdAax7Gf/DMd+HnyBngDKb+OGSWhu1wJp5ygu1xX8w7nhopVBOljsWO0rM98fPf5/bnoh+NCtvNn6CuWmDrUf7fW71qbYFLKGIzLRP3qsgppDETdK/Hq3sELqEGBIeIdhxGSc5jeD3zKHZLmiHNW4A8XpvBk8ZmVayATW7auGGkOrxmdbk/YmdzwWDUCfuIjJCVw9aiCA3EFs0npVETR8kar1F4zpbWEUKRLQgRIkfGjzZIVspFhwgJmd+SZA+RUAk83nHUtBPmEV3JpgaaxlpX5xOmvs8KeB39He3HSSX0Hr/ScuLWlfDsghWPyfqVeLf2y9rH93/oAAygCrkw8i4r7liHNTkW8sFLNY8vZuv6UU84Yb1n3W5Ux+ChK82EbV55Jb3oyoccKE3s31/XnVx7pisHt7Bu1KCuqO7pAcUqgSnHQTJslFfkMcxVZAtVf5mZhmwuEMA4Agxhr2tcX8DnEVowj5Wqwr6gzmL16uRApbgy35nNdgibRcBDkw31/hz+daB2AcuU7GiybuWspyv1dHpJCS86fBD21e47w4fOPwV7n30WEo7v5QWhE0oPitMbpqrjZ8aeHTt/9qS4c9LvgA1CAXfVUAcCmBAqucPglV+jDBJDOc9msWrEJyHKoPSPc0EYAFfnuOJ0g6NBkKQqIhb9QSQY2EDJbwRrP3c0zW+eMms/D4Vj/Oko/4KDN2tXRU3JOmX5wA/hYA8T8/Q8vmcO6XYnztNUtQ01c5yEqBqSaM32oAztkTb3FVf0NDfZshJBpPNxxLEKAqfsRJ26TseLw3gLZyAMC3UiSOa2SlAg2NzoDQ8+8qnDE9Lu7bGhQEiPlYcKkweO7p/Kw1A5amaGott31z6OwiDQpJu/9IGjo6NHH7h03+PDWDY6FOy6ecPAgckCPjMwemM+NNCrh9Y+CeO1+2hSwz48kq1IwOt3EI+2syvZO9hMdftqVLWnkOr4vR6Jq9IYsm6ukTKMIjKXFslUKYNAJ02VtQUyN0kz2F82S6anycMH910zf/munVvGUZIb6Nep744moahLyEC2BuxwP5LfYUC5t3GuuBel7DCsExbKYR6NCLypP6pGHUyVK+FyzlFUBzGONLEsYhypaEtpKqlJv+147jOrHefPd0+hLohsVwY55I2hpiNrpseRQEbZ02s2SzKq+aahqKql2HYgosHvdBW8H7M7S7UWv0fxbeBSIKF8VobI+a+iWAY7dB/3cNWofUHz8QFJ12AHkg8LL+iOxMfaumpapheGejpSfq+m+JCvNUeGIqpjGV572FZi+H6vbyQi24ZHs4KmbYaIq4BS7aypI1kI+nMhH896w3pQM8GjZsTxifq1B0x9SWY4K+xoMZz5FbalOh7CaqQi9rQFFI6DpiHKo6ZHtiNF5vNMwaQCOFqSPktmtPH6RFe96ub+dH84mgknAwbOcSUZdO1CyA4EeFFodnBCl8N9OSXYmPN0CLoTHRkF6cPwBXjppyjfnjlparWbRFvhbs089dnP0vrD+Z8ietf+WojP/AoUiz36jwPOJbAvET17OnOhf7UPfvbF2t2i2Fx7hL8orr6k6T/2l5n2yjnE1ReEDjeB2LaPvZPdxu5k767efNON1WHZ0G8+dHD/tSMrNMU48Rt5Cdidt92SCymmfHsEebJCMquOcihisiVZnOyMTDdUnSiiwgxlgdW1YqKUFlFKLGTNMMsindiSJvddc8Xls7vyzZ2due7msKCIyFN9UODlqOIIU0GuA8mKo/kEK8llK4TcfcUo4mYum6NbqPxmy+JeAqKO1gYdrjlBUxCZs+mUhrjs9BWlYRApH1AqHO1HrpTT6BXpNoCcEtHaOIJdqeD7NB5rmN2egJaAEhjXwlrtPcOypHO5vHJiarK3T5PLhYnthayij44iuypsnyiUZclpWrl1aqKwWuK6Ngy/iY+N4+Pnr2uOryhV8hE89a3ualoRb3a6VvfhKZKvfLQc0nv8IBsAN5fhvrHajjlFRkbth+fHav94Nagw78RFa1L/aHXqGVn+XkaKN3dPta7sWbkxj6eQrnR2KVpwZWJbd3LI6VnZuq2npUXq+J4iZ4ycZ2tsIp60Y+OJZO1d8YmonaQD3JTUUABp9nF88e0hWJ/5RbVfkRCj/Y+nf7yVc2Eqr9Nki3nga/D/8Ju2PGpMza4fYl9j/519mX2SfYTdhUOO9IqdIkECr/6O/Q3OkTkkfiOohPWxdtaEgriGQsYDcC98BN4P74Nb4F2wF64Gif2Q/RPzYA047WErdOLzqNfAS/D38NfwF/BH8HVYDX14D+g+G0NJ28T3b6i//S5EKwXf/TVG6wHK/wdt0FCk+gjRa/zb1PL/HyDm5sRIVPuJhUjCskIrrYtM1SVVX2Q6SDogY4HDBhAjmcETk2aRbklMmnTBWB2QQVK4Iu1lXFO4toh1KG4diluHcqEORXHrUHZh35WJlv/JN8/NrW8S/Px7cAa+Al+CS2EX+1P2TfYH7En2GPs99hvsRoQRymIIDcA/E19HVrEECDsXMjIy8RSHoZ/oQJS4Hf7UbL+tlbJqf0FG+aNEYqedBzulprQyTnqkA30FjoQCb7eBoyJtcFkcyat4gWQil9Xor5jFWZumSnMO8VWUGvqcUq4oCqhRKowvyGG1WGsuS+kEFJHq4KtUR0NS5OTSeI10qhTNqZpgodFKFB/WHI1IFZIjLcHtiqPhY/hgLqs6fVRPGzaoorZJqEGoVF8/lnIq5VyB9/chPVMTvA/bXUzIbRItv5DIU0kRnYsg2Sv3Yy14oN5ny9FiGbuL3bLVSLqMdK6M9zUkfRLJRZTOUbtQnSthP5wy1oQNdioJjtApVxwkpMMoxuX6SWIX9tlcEUuksDXD0OfQseKUUbCIVMppaiMBuNiPAJHKlSyKgOUsvhB/fsCeRRBetIzkR6kwS3AvqxEU1QpQEfRaRbjaqgNfOPqtG2741tk/O6ze8ocQ5jrqYLIUjISRkaFIIeGQybKpqDLopJxJMv5TQeW6gcQSS4LuASUuS6j/6fgyjgqMjDIKypAa8nHFK0m2LyzrKj7MFYND2EASq6impMuI/JJqYG2KISuShNwdfJrllwMS1irroNMJK0bZRg4pkseDr+eephZJVZSwIlmy18IXqbIuG/L2oqxwVZEgZmIbFJnaia9EgcnUtJCsGchCZO7DNEfKy7lfl7BqSQHZNAFrUDwoEeqSoTmqquh6QLaxHqxc8kkymIoeNDn+Q4EEUxwFYo7Q0ElO1ix8D9dtSccHqN8KqbCo4soxyZCwAZKX+wgcMuao2AaEE8puuqJ5ZExw7L1oiEfmIRLmFJ+BEp2OoFJV5P4e87p3ToEHvPh8hMgGAVrx4JzHf0AtN3GEOEleHuqcbPmBGyZI1tGnXnjqqDjU/gF0rA0LS4qFxbAKlE80AVfgqkdREa4y0ODiCa+5TmAF7DmOtYYyoanJiqp4CDWwax4DgaJgF6Qg+ZfQfcnAYZVU8MkmVqlgt0xZ0zQwFF3TEUgSwRLRwZQkH2UrssZRrPJziYiZj2wBKv7HRqy4RKZRl1W/iW1QZISHbXFQmzlEEeMkxZakAMJY1hVdBivmVTzYa9mj+2QfmJatIflEkONYhCRTllEi5pIpAMwDeojwF9thojRCQ4nwDih+osXcwk5jUo75DJ9iAGIleTpgn5A4cz/iCKbxpytRlLQRkD5umgrekC1DIdTAMcA+yzghEAQqYPfwQRp3PNS8kZ3UZxVlC5oHCGpuSireQuj6VE5lCJ+oHiWuBw2f4eFyALXZV1555ddyr0RacVs13hQyJbEEhFVccAVJdxUlJUp+ICTQklqWJUW16ETpELG1BMgrj+0+f/raD8FUFT534+zJVK48OBMdn/+ruWNwz/6JWxIB/cbPXTWVnhnMp4NHkU28UnvlAPwnvred5au5Jmxv3CS8HkPuw8lOIx1paKJetjlWLqNmGBPaqJpGUlcJ14XG8DAI45WtSSQpwoumWvuuFtANkx9+niumZkrXc5/+JcvHF/5BwUFwLO/5W3wgBXT4k9Wg6V74S930IURrtTInoaNul7/g+7SzOr0ZNGMNKFoXTpAmBO76VcmgQSscKCgbmnGEaYp2hCmSQi0nFU94QpGBaVZHaiSTJ9TWLdn6vzQtQFde5Xn0mnTQTfcHXz8dflX6lDC8icOZ5ZfaQc0Uh2dft0ChcWFWn6ArOvy32tfpEkbw+J3zZ+max/AIzstPU0Iq45G5Phd8CH7OfCxatWm4lpkP+m1hPmjYGRuGDfOcFbfOWagJvORz4M99Fi35W3HHIZx4BesbhD/H+oJV31JNkSLV1CFMJgVoLMAP2v6ax29zj1hWN3+XbBwJixbMLVZvG1zLjyH2vLZt0bBoW9B2m0ZVOrDXbjfE4/EYn615fI5Vbypbqm89P45tM79EddggLBXUPRqDYcBGwXpT9AUfM2HeV++n20h3zedfpC/yLmazpqrjFW0S7imiUXY0RLPMIM2o3qio4XbWln63dhV2s3aVZV2OZ+iETivu2WPBqdrVlgWfsBLmHsuqfQ9vW3usuPuuP+JHpVF814o/MMSrtjyaRNnQoszrMe0hY7SxBJO5J+MhgopRH6+K0WgEvp8fwFfHrcvxXZ2179Ub8YAJ19WuNM3LMQe6qEVUgArW4YV93eT2VQF3YRkJCi0s74vaoq+okKVySy+sv0v63T3Yndr38D3uGx+g+h+wFvcgBnbVnjVNyse3m+6r6n29t95XeFt9jUZFX7kjBjBXfz2Bmp+sPQtdbq8I3viihHm5yT+PLXpWXJrwSdF9AYaltbwT/F9YgrVXWwMaKgnYWZSKj3B8I0NCBvuikX6XgtUnRND1KekPahf1XjoR6XIG7Yc728YS+YfsITsficCiPYSHCJTiDiYfqt3UmoFsHO5+KBLJRwYpK1L7sCPacZfUz3+O7chUkwx50hEXAIhkfIYhX58lwj7pdGTrbdGgMQOS9QXDZG5p1KV+G6sdiOQd59P0zkwr3P1pB5sw4GCTYCBh1z5kYwOHIp/OJ7C58LDb3tqH7Xpb5rAtraItgqiTv9erTWDZqJMWbREm0Ohrx8Mv/JHmMq21m+r9pbfCQWwFAQPuRmDM14HVXc8giHQ5A85DnQ3fs7+STiBNF+MT87t2TmST5PjYIDL90WUwaUzANlocITegSLAimoPjI2BS+7B4Q+Qh7Hlr/mF70OmKwO0EERyvgyIp2paHnrhoeJdTx5Pd2I6NrFhdOdzfl0L2XUq2t4SCAQ7k0TSGMhGOmlj+dWdKdqDSMrCG3DBBi1bKTisIsk/GwZxGQnZjCbcNKjmXJ9Y5SNTxQ5TEf8EetIgw4ub4t7/1ralgjx04FzXM4HZKGGaUHI56glOYsnya5ji+c1HToPT2oGlgroOMV/NZPIMlQuHYOZ8TjdDD24ORKBaOhUPiWcl4bZYhET3mS32PsV62qlrobGtCkS3kQUQAZPqu6+mrOf++ATvaXFHIpQ7xgJS6ui8wUVt3QIDcJEgtrOMxLUIIR+CHla9MmbZfuFLFramvKj61qsCTSOSnvqLStfLV7SLfQb6ufmW7mRA+V0SssYC6ToW/X0pU8eF6iXpfviJNY8uj2Jfe6op8pgVHza9ytyvk2ijmHmHYUlfsyJqBmOgKYnr2VS0Wrsf1LtKA0vKSs9TFL2AHtn9FCchVVf3KFPIUBDI8fOHSTpjbqU/Y5gFMc2P7VxWlijewBHIjuE5TviqufCJ36iuYq7jzwrWNjrKp6lYLDJSWDO2QF6UiU9MXVSBqwcTCJEmmix7QFURKHeaZaXrGXd+X9dW1Q4NrSn2FnnA0HOyIJMO2u3AkOONaSNM60AUpYBD605HlfLdVuF81ppvof4S8EcRsO2udtBL2s+PIjE8hzT2FHR5/1qYLStczkFA3MrA48d23U255zQ1+dZYHUR7trHYoqJrUqff1DHUGeZqh6rCLoZA+EcZ+hm0xIxt9TL6qR8mL23/+qQtN4UMXt/KiljVaUx8beC+OjWiPTD7PCoNDSLA4UodFpig4AJx7+eZIkoDutkfNpYPkzLwM3GShrjtiOXC34xsbI6wW0ET0p1dyw20A/yTi0uu0FX9MIn9t6bTkFf5AG9nl1d1tMdRNwiFAPudFDYNtAA2KYGgoEqMWhcoare+ghqEdYiZTZZO8gMjHDMgtU2OGrhkLTNe9+ua1Q/19djhip7N2Om0R8ggv/bqrfuWCV8qSawrpHqUCqLYzDCBgT658fFhO8CitVqKcqR13zeTHhcH8uKmT5RwT4/c8cw/+IJEfsr9xzbun7tlf5UMHTz508uAQbPxGBD7kPkQ2efeh4yTyHjebrr2Hf+Tpe9X306JQ5Bsbhw984FMnDw/II/s+svXd13wjwgSMHpXmpAAzWAjhhHoNrUELZiORIRMWGHmt0pozOaUyPhltymYE1ynlgLzIcWyz6ZQPNLuDnMEk6hD/QW/tssndw9dPFc9/Fx7esmfH+6eA/0CseF2/iY8cfeDR+2+swsLuidqeYnHqhuvg4eLUyenLLpt98AbMvvH+J++7eVidOPgZ1+7bGEc/UuJVbLC62tBxRJCESiyBii2tYSHhUvkC4SAqM7RopciSsuC6Z0bTOFLZtKa0uKO0bNXyLUek9nUxFjDyNkdBgP+tAU8+3I/yYcnDAizHLmO7qjsUhC5s3byxXCyQjjxGixXA5ENMVkBWKIpAxd8iU9XDTEdirUsLrxaSZi9dXx1eOzjQHM3YIUOwIFpvLOH4ZIeBJhn2l6d8XLPbuBPtK4v1i4qt4R1SR+t/atrHszl6kJRm8Vfgwj9NSnD4uMfj40OtmofrRrzcM5sdnpycHM5CNhgc196jj6mOmh1b05Rql5q93iY902QVir1Gcwa0Jp+vmafamwaKU/v3799W5kFC06a4GTBD+dbO0UIsVhjtXNMTCu/cvn2n2qz0rLl0XUt+pMXfZvv9kdaA19scb4rz9mgcqw60Rvx+u80fr/Y0r7u0sjCc4Z0D1yytE1yBfNtmSVZg1epaMnEZ4PK6xkKuymj5U6CMTCgjK4o8gwijzJLvxWTEiTjpC0hzQc1tTGjyRpco3KLSMAYjfOElU3/cnXxaRjfPTr/3M++f4jN3ff7OXe9u2BNf+uqtPHWONFws8RQW/vaO987wqZOfOokl37vjXXUD1dGn6muBfy2d5j9jYezJFnaQTVTHciZSsQrS1hnQFGlsP/CNjKvIt1VQjzA3LEYwdEbCtILCvKKhlq9p+6anxjZ1royGnKxOtDcouPpSKBIZmJFLCC+JCg52Qa4ID2REm6W7FcKZkrBRC3vKsswcsRLMpSUwsQ5GlVIBIQx89R6xvH/PV4m39eYUAwm3xwkEZD3ms2Xbbyi53lMLWzUNhZtIu9VVKHRZ7RHbr2vb5j9w8ka8j483NUVGt/GJTZEmOSShbKBpN57kJ88/SAzhq47vDJb5qmnX/s/8hF+yA56Ax5Nob9dDegArlfwT+Z33tEsOMqyAkdi9f3fCCCAHc6TkB3bd9r0iZthej8/Xd+/n7+3zeyVV8tpYn9R3xpWf8HCCPybGAamkGIO6RP4akLN9AsZCcnrbMH7bUPovdLhuA/iCaHdrtdmEJTViScjDhpJe+3oNfU2TXvvqejzKY5Il7WAW24A4erJqb2wLexE/VRtFr1IxTabfMXeNqJsclDWVHWK0fk1rM0i9rmZkwlUk4QKE7OdyxFWPhupvz2tLa0KSeL3ic9XwxPi64Up51cqermwmGgkFdMXuDms0cYeB1BAQ7lGpnIaMqyxkckfcIebsA3JnIdNcfza3TMMlAS9HOlW06Hx0dYxWP298ev9EW6K/PHTth1BQd+Ch3zjpd3goWPs4LdQGTm1aexIl34Kq8sdQCi+of1d7+MDmNXtgw5qB4THhNzSWgJVPj03sB6scv2d//d6xL3onHDLFOBNa9qSqFrAS/pg4b6x9BlonDsBI3YfsMX4C4Z1jfSjVWHHkD2HypeJ1IMeRmEkKSTOkuV4tYhwZXE4CmIJQbV2WrUhs8eL8uaoRaWlN93UI8awOnQSQs0+Blmn6C1BpQA4ZpwvFXNAmTzOEUgL4iYduOSV6VPu4OJ36jc8IMH3o2x/heD61/yP7gDyk/nx5j71fPFYHw7Uf4h+4Tlz/ee1HE/v5wXHqOt/H6nrMr1FHfwolgSppAGTfh7EkNn8TkzSKeTqEtBBlAXLVIY65yIicEx/VmMo1dQEJoqzMYF8pekORJ3s68rmwk7Z1JV7XpEkDaEwDUmqisGTJrJASOwxRQp9gKYuqXSlL61n1mw7/sFX7axSf69EwKIvCSuup+Q2Po2hA6y6g6493kBP4k6rJeePOhnmSrWvPoHB72p1Zp2mW4Y0/qN28Yd7ULUP1ajqUM3AXPi1zXa7fmV+CySL/E8SHKhuprrPJmI4yEafFn0M4xDhXFikuTJIZCbgIHxSVMLHcqOGkmzryHTnidx22I3zX0xcstZGouEXLiVrQRtGhiJLAMmC5sal1YMFe3ugX9lw3lzpewh48qSKr4iqBKe6cI8euc0hkEEoFHlJ8hgzzG+CuZX0uZWs344O0FtDz+jC6ECtxWsSNhFGOHWO/WfVEFK7xVi/5NY25i8xdloG3JEnbq4Ar3l4hFqpmTXiVz0xLNe+W5Zp06K0Kz1VDm0bXDw8O9HWUs9lsxvWmiyaDSXKyQbxRkfY4FJ7V8ap0ZSnqbploQeplqW4BQZwak0Ivv/gsCpYkV+ABHjH1NF3j4dme+MsvksPXeKkj01E6U86kEUHGWvOX8KHF01To4kPP+X4qzf+iNV/uKJU6yu6RXMVQur4AwwuxNy2sDeHZxypsUKwsvJPdW/3w+g7eGtq8IiOFW/lYso23ho3WmRYIN0W9kqGHjStijkfSI0ENyYyuzNsBVZL9loSiOY5vM4RCidl2SCQ84z6TQinjsywe98Y333D9oQPX7rt64fLLdu3YOrlxdN2wCKFaXSn3l1at7ELsS7a3JVrjLc1NsSgKquFQsP4vkCKAo1ZOf7llZ3jVWSKvJ8Ji8nrCWQzLylfqedF63tsanNOnv/n4499sHOHjTzxx5vHH4ZHTp8888cRTDQ8vOn5c3Dpz+nQIR05oCm86fqdPn848/vjjmdPnnzp9jg6Zx6H3tKjttPCkymLe6dOLy269yeASj75RehBlYh2l4gKbqwayIEuduZBEKz0UF7Pl0SzOkGZGVoTlYdWuMVi4gcfeIHPf3Jfi6RWxEAk8jeDEjiUTeQI6UMOAHAUaV4qOsJdGokK/d87UQxJhX+1EZMAZjETgmDMDH/O23H7J/nvu2d++qckwPn2Q57ck/eZSGOKvaidse6096MCxysw/OB1b5uGeZz7AEc1C2vzxId60wkaqYAi5hHiFtITJtDq2AanDJ6v3d4OlDoIXNa3mYETSAqBamjofBZNZHtNaaPLbkuID2avI8zHwMC/3eBfCwhV7xjFCEuhEEdh8uB5TxaXJ0dFqdVVvsj0ej8VsW0GaNDo2OrZpY3VDdcPI+jXl3nWr1nVl21cmV8bb4ojHsZYYYrIdtRGXvaYcVsIi4LeFAn4j6f6OYsNyKsjrOiC/drxPZ6noCi6NbQbgVWnyiQ9juaeegrsbzrO+s7XE8ePSXG3vcfKxDbj+tmJNq+5p64P1x4/XEtWxsfH6I3T7zNgYJMbHzz81NsZPNB4jN93a843nyFt3zC3m+i7fIK+UNiOuhRDqt7Avsn9j91c/9uL3uew7cDlX9Gf+8HoUPb7+yIPvmp7cmG41gD3xQBWlxIEVyBw+fBu3JG3sX/6e+zbeDtYokltDMXTkZMi/uaodYj4mGz55gRk6M5CT06qUghoc8bUZ5LNMk4AcVy1LMDhrllmSNfnPP/rTb33+c+9776GD11y1e65U7M6HbRtJiJ/EHIrgsp1iWUHsRYUGryQR1IUkWnODBTVCXERjshKQm4hYx8wJSZE0HdKksxRI6A5VuYIKEt0kb5kyKk2CYqAoGUW5mkiXqFH4VZKRW2jewl8Za0PO61aZFXbxili6xirJgFSheSMqoPYElz+bzYln3+aj8FI9PP8LPat7gLyM6+fv6fKlqifcJsue0YBSjTiqJnv3q6Y3HB2RveqUrGR0r7ZT0XVll2a55UxVrYZjKHxRQcCS6xWvsl2OBXWvulNVYd8O1YzzUVAirZbH0nokGJVaTW3HDs1slUoBkPN6MBiPynwDjxt4u146r4vS8psWhnl3W4Aftfbwzjakf3wOD4P5/F9ei23xhaMtHapHDo7IRUsdavHq2CBPUZa3+BVF7/HEHC/o2n5lqaQSwJKmPtQsSlqrqKSsJiLelrCH67W/2mboft86H+edLR0AVgk6Oce0Xze2GUbASzkJo4RI2BmFHGV5Awbvwzz3qRxleUo59yEDH9KXHrIAcksP6cvXH3R2gG2sjuydm1wvM3nQRIG91NkSkCVUq0SgiAoiUsQlxxI0bPl8357LdmzfPN6dT7WHQxqtH4uoQ0TvjouxOve6aO2isIvXboDsW+C1W5mL2Hxw+uZpvuvoLogjhE0r3Kkq/imvpm1tajY0OXBM9wRaopeoAXWTIyt6p+nX96G8Zyr7dV+0wy2rb401G7oUPIbszR+PXqL4tXFblg23sNkY+5spP5CItBRVnxqZAmXIq0/GA6Z2reEZUtRqArUwT9Efb/GDRxNlm5rbV6DOaE8tK2oNKsqGeL1ocwBpPnN9TaQQjsE2lq62F0HY1zl/lbfJSDXZnqWNGsjfxFXuV3LXCEKwo00JXMtJHwKJgBNNcAqd1WxK9GNaALFSHOZR1LUIno4U8qjWmlKfPxFsHix3j93X3RI2dV1G4tbaFvf1+nXZtAOmrfGA3p5NkGOSL394EkKKpWpmItHu0QIxvpCT+AP+Xl+8PY5AjtgtPfeOd5fj0UCo3ecvldZYqodLuTY74eexgG61JxKmoluSA1sP5z2oN0B7JgV6gJfDJvIzxESSESkuIsyaUS6cY/PsILuJ3cqOVt+Beod88ACKzEev3LVZVqUjWW6qt5a4Ze7BSaaOMV0jB6lDZImTVC4tIKswLZUcsZmmWxqZa8mSCZ5xDygKKSbkSNPScuzdt/zG1NbVlWJvV64l3ZIOZ+3+spesKwmINHzW6+7qURTLyNsv7Lq5J4MgXH6ijman62RSUFruOm8jPSQ0Rp3WXX/E0aOofMRtH4TVaFmMG20kk5PwTO6W5PuUTTt9xYrUF3AS0fMhdy+Jl5yyf+K7ivb76rfOkjP8Dngk1Jpsnw4aOoCUaZnIvHthZ1HXfNh1pVzQFRzGrO3VdTXo8WomBT1pbbV/b13Z2vmsodFwohanbZrcAarRHfe3BWHBuz7fonN+a6AUO3+reKt0DE9OIPvdoPr72suP8qH2yE9f/gxs8QXlFm8AeMQJeZP3yMg2fdHVzQq2ZOW6yU2FXXFH9zRL4IzmLqv9m3eLU4R/ixZlmzvkSefRa7+V8Ct6x9re4TYVq2noVxfGfQd7omrReE8OU/AICo8hFB7XBHweyZRUydzrtQyKrla1RR00RdGuJjdkk8Z8aYz9NMZz9TGuDi49qx66+GFJQyX+TZ+eq2YymZaWzI7MjuntY5vqGkNfZy7bsYQuwbeJLuEIxRAiTvT3kW0Tr1EziPSRj24a9QPUEKS0JFI0t/v63wIJBnN7ZsdJbSQ/xPG5y3KfRbwhVzfOt6/bLu6isry6+DYG9aXB2inwWz4DxVJbrv0bvGNw8JeWz4PiuBxSoLf2jFeXPR6f9cvB1xmvv3DHa3qQa1ZjvEaQHkqW5Nkb8HklS8PfYjhoIokRTtQGqIqiioHzXAT60EUDN1qvxDr0hrVIqjuCb1zNXDXfGMHtU5ds2zq5vkrKnzuSF42j/TbHsRIhjQ6xs0/Q2DZa1EVVr+KOJjkAp0vuCGI6qr3FMH7uE/kBSVZp4EDmQ92fzi4ezN7ftYbr5MwqqYP5T2UPHHrrQTwxOLhbNlFOlXHUpN2Dg/c+MDg4J5u0Vq1qc5Su75tyYez6UGPZwW6oHqHx68ngfKuu5roxNc5NSyYHpQYt1QxVW/CCwXTL0Bd8YDFTscw3oKqjo6VSS8vojtHpyYnShtLI0EBvoTO7BGb/250u7jpaa30Tqn7lwjVJwn2vTr8FnE9RVBEZK2o36agyN66W3zX1tzFXflRfXjH1uQuGk1MXLhv70zwmeLvrNzmIcL6Wdoy4dCNX9a5kk+suycdomxGVqcAOyZyL+OZFVEEM3Wcs+L1c95ioeOjqPNMsS5thmkYKh2ZN7r3myvnL5nZMXzK5eWz9Ojtju46UtN8BvMoRMvoW6XAwGbQTgJAeBuhDwSytaopYlaqbKHLBdGN9ithaH4kT5FGSeN3en6zDJqOZ4zWPrnN4hut67e5zLbLymCrDz0y9XOqo9XaUoJ/KPZIzepzHo3kj9wUclS/VviFGZL0Ykde/rl3Dg+f/1WObps33rkfepuzAN57/18LoSIGHRSMuj8QhYV9uCjn3gNi7I8rWskq1VMIGsaxFXt1jtPpJUVu0TkceHQuuBwewwYFMqrUlFGBRiKq0V4lYgsZuIwdHQTUqlqYLQJEa6RQxbVc3zpazIkhjmK8jobY0DO20anfDt47C1OZev7d558ZYezaFaX7zH8Ntd/7krlz+8G+3ZCQdhXSZSx7Za2t2QPPPXgN3/gQCP7mTH9t2x+TwDV3x/r5CZigiKdvuuPeObbXnr3xoQb4yixQZxR1dlvyKz9Hj8XC+eM8MZi08JPwlrhP9zrJ4talFrM2gLju6tDSTSaW5G1va6IP6el3gZJ+Xzi611fRJr27q1B1/eM1dP7kT5hutAo/5mmZtvmNhNbaLxuNR0a4OtoWtqw5tjnMG60BiXUilKGBbgk1FilF32ykx0je40LXJR0DikyPV4aF0Ki1f3HiyJ2dL5f51vKwhZa648C+6K4hC6m2ThDKSrQfT0SoNonOulMP+hQ48udhb3LQz0sJVD0UVSBKgtO6TN26DA0+eefLAmalRxaO3GLKCihE3tbi9a1Ox96PXtwd3PzSycRK84zPw8NY7Jo1VUYV2a0CyiOKGBAklFgp0Ht2Awzh5xw823JjzOWbSkhSVIGSCrERXGRNScWXpzonufO5md+1ewCeCNGOB1u73eHQJWL+BGDotHBLStJ/VRpBHNeDCJ0kgsKQAEmpFvmj9AXVxtnPH9ksScWcwilIQi0BEvwihG+gMJYRYMcFJXXsTgMJbgdJW4e6dH9kFQ/3lcNjflOjOY2rXR3Ye+OJ+fvDxg28MZF5+KwBP8WMD16xZsSvRXrDMkKoPXHP9vtXjk3fcf/s2eBO4nz/65jD/bzfWbZgHpKekHcyDfHGEvafqIcDCWGcLwr2+e1i3BkQwlL2MNqO5mpxYFHJikeVGXCznF2z8PReV5otvWnyu6mlLhTqdYDocEpG0JbGGmyJrcbEjmQ26vhWRgEKEmTwvyAuof1iuRx8Mgxt3ELUTEpw3kr0QGug0au/nZz7cXJo+OF1q5p/Jt55DhfZcaz5e6M2E+B3XKe097cqB28FJ9fYu6L1Jw+gagN/5HHTFh1anUquH4rVnP9eaRzV4MN8aK87M37V15p6AaaF+mYpYZuCemW13LkyX6jTWhV2J5nSzV+xFKfajA1VSwV3loI2QEBYqwYJ2jMLuNzY5w76n02RFaHlVz/8rvYU/fZs9fJt9+jX01vl3T7WrKRiQuHBjXubJ2IhpIOPIZH/W9V1qbOHa2HI1LIm1LZTts/UtVS/YP0OBuoHTD473CZ9z33Fh8jxOZlM+XNshzKfwBdqdAPUkWpuVTot9G2i/0rVsrrqLItSjoJLziSQbsmQc0kEVu02Rm5ksmfK8hVKbwilGGRFa5bSup2litwZtlmlMmxwa6Otd0R3u6wqHk8HG+lI//RfdSMJFmyOmlzZnqO/7Ee0LuoKaiEWmhYsztB0FvDTntNc+zk80dgT0qMfbnfNnnYRwJ6t2Z9ojcFO07N/pj0F6xbB26syZdmeudpO7H6IcCJ7VTJijvY/mPOrZnipAr9Me8+0MlJ1Tw4yJ2JRfSz9GeNAegGvYMBsHrRqpDg/iUIHG5BJteji2HmSawLRE18s0SZY0+RC55QDbi4CUEWwLKGgzVQH1CkaRUkQ/idconOJHLVraq5dHzfmtH4j+T72oumr5IzJtFPhWz8zNzVURL0Y3rB1aVejKJlqcMG2+YRtkK6rkcCQiFJioUnxnY0eNflfWjgqXyFw2nayvuSaL5bUiXtKRoqiLgpZzfW1t+EV1Z7UfIobxlBHCv8z8hlovrb3CM+mEIWktuun1CGmunIFnOkpKRo9VTtfuPs2P9J3uC/QEdga+tn7n+rYy3NOoovb1A24FI/Pgk8NqHEWFukRYzmzSsAYdTj1Yu/tBKJROl/z+nYGepf3BVrIe8jVZ7hXNmbBG5lv7ljmsi5VjH4qxUdvRkIDkXCezSjnZWEiWzlq1H1lx53hl167h1Smbog6Dqqyr0lhiN5w8TgvHrRb83G/WnrdwkqqhRG919+q2rIysx2MqPlv65OVHtvzwlFgpft66sIdZgXWzrmqW1sCIXohVLaDtXS+KIYuG10ZpTatDeMgRiaPtPfpLBUU4Riz5/NNSYHsUmzsGpqxoQYoKs1Orh3ftqhy32w3sA7UzHuPH4eTuxA8v/6QcCsimB7m8lG1bvbvamwip2EYLxfWEiQfbf+qHW+r237NSP7PZAOuudmZJ10uKCRMAaeOrojVWV3ryrfEMyVwddcd5t3mogGmqHVVsH6f9BSgCrUymyYsKRbEULQvsFTukuC0O9vyDZUkLoea4bimWhHJHbEfmxssiuioAjs0NRCM/uW7+VznaP6Xe+GA4mvtHK279R/vMpk0BB3m5/7k9nT3a+5eXMczob/sPLrg8/dcoSx1nQZZCTXdTdYNFcjaKwZJwsUdJVvipcMGYiN0vkrOnLJw9FUUQd/LYAwUFqYwdiTnCYy+bIz/GAiA6kWjZTi4XWkqN0MqO8FsrK9h7uT1D1uxyD8g4eqd33pa655l7Urft3PJPID9f+1LA2nRNwAmM9loB+L61rfaftb+r/ec2y9oGOmRB32bBwB3rBzYIf5UNA+vvuOGuu2Azlr1moxUIWL2jgW+Hw795332/iQr1bffxB261XZv+16StwqbvRplQNMeYG2aCmcKbk5gVZ5Orm0XAUZiWpBK83IjycJeYSMXDGXKuvlkg32nXXnQG7do7Iol8609bxyNwyuaziTzPVjNqb+37iUjtxQjejIy3nm3NAybfEanLVF+TM/X2lCgyIEuiELjhDSLW5HoK7cCW4anRtPamLmwdOQi9aeuidJPYKpRLlJV+G41+FhvqtNrNAVAj1Mjx1uMiI/JWvWkVNwNBfG6QshKNPtKc/7Xwl0qzcrUPhW4UwdkhTrEPCnO3B0CKLTbG9CioWra3tcTscMBnqCwNaU1shClU8oYlRGw9iHy0rqTTSmE0wrcKg4TYCaXdCZKzk+3P+BxaZIOHE9Ha18XWwTDitMMTAmdIwCAp54l6G1Fu8KDOhWMgi3UGTn5vFJuJcqmETURMJ9ZCxhwFJsMd/elIZslJy5VmcDAEd7+otUkhjUmnHR82Z/rmaehf3lK4idbOg/WF3MEZPrX24MWtPUjezHU/I04xSRrKW/lqTqHdl0S0hdibjGIOZurGJhkm0+GOMm1MhjiCDaBlVeXVIKz7UDwBrQJIz1wMPbIRPXxQNOj0Rc3Z96EP7TuYqPs93cO/yQJsFStUuwvZjljE7/PqIHkIfBSZ1OBB5OO2L9+ZTiWDtlJ3d9TSCEFCz6ABTkXoUxTZR/atsuv5t+TzEXVsKeT4zpwixyNYj/+PpRSNa0rt1tqtmldJy1yBD4Z6w3eJwNRbVOis/ZLWzs84ZGrUa/8JmS7a4nmk9nUs2qX6FNjm97/jIFlMfrRPDjT8uGCfiPkqkIzeBoraHELNWiepUHi5kymVvNvF1wtIbEHxFk8X5Nt0Nun0CSeuZft3Vfr6lxzaCDdob8xlErCQePfagdqPBA6k0+JEPU37nWWeANeMiQw6nnFPjq/2DYe/a1FcLroicB1P1qB+TLs+L/4Bq0c3+lHoyjKK65f3ig7g1FOQ9yquE/ZyOb2lmnMLEmq9acm5qpGzuxNZMRFeI9bbPklsxLV013VeTrlrdLQ0RwHYCQ53+/Q7DZ/PuNPwPBaIZZsj0QQmdM9EPhkvpTIxu1MzNe0ynctzn1+xe7zwYSwI4hnwwMZEKdUeMr29XhN1BaO5MB0OtBdTEPAVDXmTGtA/lBrY5fptunEMOksSP2/DXvgQJ4jRsYaRi3xlF9woGYpbED418KZRCicacQk9A68bl3D7W0aBuHzgT1DWqLIQq7B3sE9XY4sAFnlWdFFE/ipUyXflcEAUHMhmHMgCISMT7oUy/hbJ7HO9CaABGUoXDJBxFiLvXmCW5bFQ1S4zC6wjb/mUJMkzeJKlWZ3imXB8o4cPHbjustnpqZHq2iHaercvUo7RtrugarmClLuAxBfit1Iqjq2SK1cScqURmBW8kEvCnEbSkI8ve3yYi13KlKUn4Giq29YTzQXHjad5H/+O8oTiN3sTieactxDrbM54k92eQKK5J3bK9IkNITH7VEtPe7Mn1BSIZUJd0fJI1n26JZ+xA8HmuCeTKZSrne4DfFNpb1cg2+zhgm6c/6aOVXjURZz0YJqOk7VL1zQl8rbNMVd5XIH/u14gkEq1pNZ2FEcjK5tjDoinQ+2ZpvTa4eZqb0/KI7kP1PnfI9Ij/J9YlG2oVsM6Cli0VykfU6A+yVSKm0FN5jURJcAidijo91mGKpOltc4MAyQ4VsimFEDep/Vpub5h4PbnX3zx8ycPb5q56tYnnzz3JD/55S8f47/g//SL2iO/mJ0YehLYk7937Nvf/swLdd8G+SakdQryki3V8ZjFZSUaCuD4B4WVkXjckfoumI3oRFUIItMqCSI0oeirC60tEdvnpX2A6MMLTrcSoU8u0JxfCcIFTcsJUk77chGhk2/6H3ePSWdtc7T2SdWn3n52EcYUPIO0wdC9/4HTfFza/fKL/Mv/7jENKqPe/tODsEmlEqO6Y9btUCgbkb97lK1j49WNg9kMl2XFQt1TmKyFgIElFcG7aScNN/qNWi7WYryw2esp9aGU4fdEvdFQRjhnoMBUt6XkosWSRqIF2VhQxdNctiT2rlxuYtGEiYW2HJT2pu/IJ87N3DgNJw2jvTc0wGdl9X/8TAnIK2V1v0XmFhtuPyC351MgbzzQ+gLmwE1/er2wtAxcGQLX9CLdqgbkl29SuNKPF7uD4ddaYMzx0tOYv+R3K2DhQVjk2CXsA9WWyWyHT1alfoR/ErAIjpo8VolxlXT8ABIP1BVVWQWhR3MZVW+V0QdRyLZP5g5N2J1Iqle85K++4uLi/NCblp+rejaOhkOdmXQ4pDeMVf11Y5WBJF8TAVBLRrpcJYl/Tjj6NqDMRwVkO43a38JPo/sm+nYVm4F/tg73HfCZPam16SdXvhXcl8xcEILPrRoBsuM9e84diPHrPlr5U/6x/rc5Co24SxET28pWVPM4faC5KeT1GIqsC1eVhixJS4Du4l+0qVnEfJArZpTMxCgKVSjkktx4yO5AZ+lvIpHndsxd9vL+p/c+vbH5srmZ5yJDndx57sRzzgDfGRlwnpuZu6x5I+ZeW9s9t+O5SH6AMiN1uw/yE/4YM1gzG2aj7FJ2aXWm1IKosEPF6Tu9gQOfGunK6UCb8LhuTKy+ASxrfCaHDJKu8IuIJoRLYvsym5zcEi40ZexWjbg+eW+Qnue8Rsmr2BG1ofRSFBs0nJeixPfJ5UYT0ThhUlVoz8608HfKic2goNzduxpQHZyHA8tUwacHXxiMK6Y2ajRN3Vm0rF0vf7RYbFNMyWdlLDAis5s/IZ+znNzMD4513fRnG9dfnu6/qt267pL0gbWkJH4Arl2uIr5Lhutqu68rGjnV1PKZm7cG86ET95plQ1VtFZTa+W23tUCsaT4czqxYODBh3nHdNdV1mavK4YZ/2GVIQzNsjKJbN3Rw1ViJQmMUaCsm5J9jzNBV3VAPUfQgVxU3LloV23xKpFocoh3cFF1dqO8BsxQOsGljtqOj3JHttzOmCI2O+ERQhKZekBlVP7hfAai7gTU+TIV4lCUX67o7WaWfIC48xBxpS+bHH9t+79D4hdDurZ3XVCZuz6kx2YMsy2cH3LvT12/Bm1HFc0TzQOYnH9t+Hz0UI/P8R746vGbcjSN34tbWTB4mhs3VXg98uX5nq5tW5XpJd448hTzwP10eGKB9joK0wxBtZiLR/kQuDwTiga+KBARmh/xe09BUWVrGA6PE9QpAfqN9xUqU9NxAGe793IvyLV/+8gdkZIBP3rp/26bDJz/3i1/w/zz7me9859gjT9aeHJqc/QXM/KJuK/61vFLIyahPsSLt2tMU5MKE7QqC8/RhHyR6QjRy92M1hVfGDJ5Uap+qTPZ0J9uB9a7oLvasymXa88l8azwSRuUV5RsL21pxTYoi3JWWi1H1ERt+4k2xASj01VWFSr9SLOGI2X1nyXGcXyGsfedPeVR+hdjk+rRm8lfmSh0vn8iUAWmhMWcYd5U64G4qXrsJiz+/bGtQ8Jx/qSzUtnI5FGrEie0W+5quY0doL9qJkdWSpGB36wFdDNkrN1EORsKl6Jw+Y6TJEjmGIblX9yAFM2aZgaL35sMHr927Z/fO6Usmh9euKnR2ZNKRbgt5aGVJVCZf/lI5h6clcY88Ht1oczlK4Rf9pRzqA/Q9rCztIrcUAFtX7HPZC2vwhOQkINJSKxZAfD7lBi7xEz3xp8qQqvh8dtSb4I7P45j4M0MhzfHFPXF+4+YNizxgm62mEYqNtSgRSC5sGy/v37jeIzbg/pi9pj3u9TgxJ75yorPlqoFdS98dgqnWPLTf0F4pgX9kIubNSs22r8nS/RrUPs5VVZflrsv9ASPfGYrnfCkDipFiV8juzno8q7u37W6ORvOtsDeR927OJ3yj47FIZtv6vtUzjbW0OWH3qbIb6Bt579gf9MtiyxuSypkYDDBMoO1TOTPEYOiKpFMAlSorKi2SCe7r1TZTzEa+I9ORSDatbaFo+PCyYUiLZcdySXx4jEBKkUR+7o4ELUjQRn/I1KJFMVqCbGOmE3FXjlHEp0XMC8FIdQeHVlgKWV4ailhI90CraQd4edXY5hsDXp83EjL1UMhwLPwfML0Jb9T2+coZKMFT8Z6xmcGrWnKThbgTdlSf2ZJcY3+MhuS0Z/34gYF12xaSEFFa+GxjPGrfCZfMlC8bD+Y7zYB/ZaFLEVu0cRwO07TNRMgbzHpjEyP+cjl5fbKnZc0uZE0jWzORULm34I13b/bmE7C3NR+NxWB2q9S92uPJ5iOhLlb3rTwpPVX/flbz0ldMxthsdWcL6okGuJ/Rkj3gkYFIt+bRcXJ4mUf2ehZUALFnkQh/xyHcw0yTXFEsj7l508aR9fTpkt6VYTscs+nLJbTjRUfj829K4+t4tFxfv1ApGrJ+4X5FzgcX31HcwpiVJY/rgR6w6x8iqR2tf6KE3485ZLY7/9PSRAl//Lfq1r/aJ4Z3D+MPOieuHYeJfRPjrt/2B1vzogQcSIga1FTtz+q3uLeDqijVvtnIgmtXUB3DD5ZEFRONfSielAKSzspsbXUAKQkyPvWQQFqZ9reWNFKxNSQ6Gm0RcLFkkY1mO8KRSIrCAekrgsJAPgy0y5Mw+KdUMmESkpZygs3ZYgcQsdW4CIgGRwrsrm5ZXdu27tijVZhWUgGl9oWeo9OQh8fShUwB0rX/7gsHHN8/qxmf+s/p6opVaRhdvXX9bAUerz56C6yr/Z4SSCmwo3vmXd21SzK96QIW8Dkp/49VvP/PqVWFKtZRrsds/Vru5ceELpIWXkxjbAe7gi2ym9gJdop9kn2aPcHeX717HARjSbAWJdEy3xyN+C1FcWyficq2Ii80Bb2GLMUCHp12M+RXhjUOIZWzOOJTG8RbW+PTeIq3zrLWeOvkgw8+/vuf/+yDn37w0586/cn777v3w/ecev/dd5647fgtNx19x+HF/XuvumLP3K7pqa0TYxQYt7qv/q/Y7m4jh1QApQiazMuuc8uukcimk3RNXyx4gzLRt3E/enGdb1Wmn9LkbQXLYr+Ic54x+g38jRvHDfyNuyk+ZNQShgHPG7WPG2UDf/WMMzql9LF6yj096xapPe+eTzVOWOMwXoy9fIV0trPt5SuIAEmnE/lviafudI/uo99/za17X3PtHiEibtV/T4lCRm2Hu+hb84g94l9K5BuxYb+WdvOn6n5wlWqpE2RFGB1Re1CYwI+G0fHVBrhsP/7vW7K+LftCgLRsr41oPZ1enp6zAy//q/sZgKCISHrD1DLLowOBcbHkLo7gGyPj5Bg9UNdH8UB7P5isnQ2wafa+6l1jYOkrQVYjwDWJvMnGgl5uoWBsqYcCBtdQqdT4IT/JIDRHDtFOv7rHJCmEFhvF1wkl+aKu+2jZbXJwMJm0LGCD04PTk1s2bqiuSw4kB0p9K7pzHVa71d7cFA75fapCX48KoSTmfqWT9Em1kiLR0S+WD1CwcCguB2/mhHoqpd3bFCjQuM8bmyxUypl2Wdzmg9YUbOoZh+n3Q358fJPjmDNKz+2339GtzJxU1cnbd65c2LS6nRsz6sQz3/vuZhXvald/r3bmGk01ZkDdD+3QA6lrlZlpKxTj8YA1/dF4PO7zzZgouffyUpeqmTP3KgOrIZbKxPCuMjHNt00qePejyuws37NLoaL7b7hhP5UUsek1lPHKCP8Q6yWbvL58hbVh5ORcgJHWyIFPdnZmepJimZUMm6TKkeNTgpNmQRIAfVWBRC+x7botvnpajiaA/1Gibd8j+2Dg+pMwMH/n+NQHHu7/v45d+uD147x6+L6ZJjvcW0TITI+sjQZ1+d3KNb+3d3FP8us3jd+5Z1gaPfSOu+hrCjvvv2GTBIVg99HqjvfOoOgQ1B13zxnqB/FiiwWYI9aNEDcCxIHJY0aWZKB1SS6TaYeTiE6fRkI9VfbIm4N2MJL+fyu7+tg4juu+b/bzvnZv727vjtRpdbwvkscTRfG+RPFDJ1IUKYqyFdGRKEWhaVmOvkKRtimJta04QopAgB3bbYrENVqniY0gLuI0gQO0aeDETWwFSJomAVLlDwlB4KJF2rqF0SBp6uqOfW92j1+i24bHu92Z272Z2Z2dee/Ne78ftchDYdQcG6QYbiM9s83s5VD6Pf0vHrt0Y0Fa+tbvnYW/GB482SDcD3ig8ePBYXDwaaemX+yHJXj1kRvCenuZTyjVdvq8HlHk+JDNcB/JMY5Ja41jmuoYxuRVwxhWIQyOEezEM3dm34O9rsGrH0eGYdjb+Lbg6u9OeRQLHOA8ox+pPSjIXnneAK/onfeDqInzOmigzfs4JJ1HZaivC/MBUCRJOYIbRToqoHQ0EQrpuoayqiA4zKO6qZtBQwtoAT+2Q1IlFZ8RCn0MksOgmTb526kv7mUp9dCz8NAz7PtYb/aDZyk103gbnw+7/iYbqF/lTcCBbQpevVm/yj7RXIf4mvioSDOjLRRrPfaWmEZoumME+ALcQod3bNXiSX6+DCZD4XQx47AIRx29CMoOgE4b4SB0gWVAm1hOW+J923L99av9uW0Qz3DkB7A5hEHmhc/Cp14ay/fuh/Gd8M2fO+Lpz3PvAvzqV9zv+A03zjoiJFB7Lwgl1Eq39PZ0F/IduUwqabfGo1gZkxZOSgkGowe/uu1/x4aMc3TQaruaXQFIdMErsjEDdsAe2AbOFmLrt+z5lwdehoq33uu1vfNeL/s7vq3rlYppVqvmT+fmUm1zc22sExMmZja+TN/gm+kvDbx0xqAz8QSbzsTth4J0VrD6B/ys1FzjOiaqmAnd7jc8rurd5SfFfxVHhT7BqPkLIIxSQ8IEUJrlbq78VeUhZxWbFDiC5o7Sy/VjpPAqtVxx8ohxRRGPjd5/4nsnZvZ9OJtubf2gFPZvGe7WdNUz2toSjY1MnDt1Y6TcB8nOqcM/fOjSlUsnZ3ekGdtVDXpG021Ktn3fh598/OrHH5Riqql2D20JeCZmZz40MzIRCY0eGnz+8JHJB2qDqRR0hsP7DywcOX7si6OrOKtMY5/HZ6SlFg35JNH14eWLjpYVX7G1uXRDnACGwzPCfxnGr39t2AVjyc4nl4w8nDAKlFOwjSvwFF/EvnTFcMv4JDuFPQaf/oDjNcDRZrDPLlB/BuE+CVaYoUAIGj6vIgkRiMiOwYJmY5cUiUwxWB021SzAXi3008l8Eq7QvlOxy4LQxLml8i1a+ZUcWZK7/uIjs7DR/zediZjc0YdaSyYjjtZJAO6E1QnjRj6IP95lG0uNS1GbpSLw1JLBpprX4opdsOGyUVh7bR2+H5UJo6suL5YV4ZfWbZXJPT5MXl4VJPypvLGUzNtL1Bj2ed4+LNBKskSMCsSSmvrC6+K4KKPOlai1qDLH2lTWOmJ3hFw4XyxFDlO/LPbyeD4C8KeeyQbtruArjbfe+VLcr6f+9OETg55K7MeW3vLk9l3ZmB185R3ofyXWluzb/rF40PpJrOLZc/ziCq5QiX0Ar2sXld4Z5qU7IJ+8/8S2Oiif2U1gRrHjb5oLMwoFMjbukG1VDkoJRbl1S7kpJ5RmJu4kZPn2bfmISjsg4+GYi/tB6fYtRd00t1nf1Gp923wb65uIJ+5Cal0FaI1tmguTcuO/sSynnrw4LPumjHXl9bx1S+Z1x8bAbpWO5clbt51zGndkbdNc7LdNLDD/enxmB015DXKXeHwNPhfMrEHhEpfvLD8nviAWUB4MC53Cqdr9BBefTsVjkqwyop2RFZWvaSqydEEDARV1lJZnXRJuYmEl1wgvrPhGWBGvB4RcJmlvaYl0Wp1B3RP2hmVJwNmUTHZQQfkJpU4hllIshbTNcklEcYhcufGSkXGVghrx1sONb0FQafym8fuNf1fA/x/5bpZK5rq/bn+0FOqJ6958a499eVewFPX7M61injV+6xzphScg9Jme1kSykNxd/0qpFM/k/mx2d3drKvX8WeeZ+E/xOvsu6gOHahM4LyWZLCsteAOiIcJ3J9KlMQEbvnGNblO/20w0mjZTHdw1i6A3dLGdu6STRd7ZDGLX6IYSGd5d3LBKVVEr4vVsOX906UsPfPZqOP6xU/0nQ2EjHt9zJFfIFlr2vbEgn5+4tzxYsfpL7GKlPXbguU+errEPsEOwvyIqgVPDzGIt98zmp07LVuTgWejzJ2sZRWj6AR7ntvOJ2liIOtaYCi42HAV1XeQEI5JAFgJalEbJTjyqkK14AgSbQ5BYYdMgAUZThAykvc4oyyOm0maxmnYYhXMcUCRtFc2mayf7K3KtPDsffLI1ty+99+BJWFho/IzyRN/s3vovh2cADl2b2oaC4qPy9NT91xqvze5lccxu2lCPc+z7dqFKMlkIaMlUI3c5DScETZjz0vKJoKAi48PB2cfmUICTxADpcCij0arKDK2zyBRNI/v93KnIPy34Zf9kpdjd1Z7LZtIp0wyl0+mMye1EUGrHFtBdy3EbR5UjPkZjvdy1hLxorKJVDLv+Pa4DaxTF4u8/xoLYA6v33FMtpXYP3Hf8CfbxYycfeWOQ//3xzEj9n6nhLDYyA8cNayJgJ6Jw+OLhtr7qE19/jC2ODA0ODg3CWX5V3Kswu4Lxx7mAUILqJqb5DpvJUtTSiaqBRm9ahpXIWEnsE7S+AXyGoidQpPmxkE+3tcZDQVUR/OB3jPqrxC5hF6iHcGI42lO4xM3BqmKFHeiedRwhf+NViVLUr15QUddTZO1xTVa8Xs8ZzbuWdKI+fp4M6efp0O/BCVWUZVFtfFHRtBUcJ8Jo2S4ME8NuPw69Wb5IIWDXBJUWPokygmNCMkm+0IR2WoF0Wo9THYlZ7eW+cpEjW23mXJzezLuYYqXDsIoNuw2agO6hjX7FOOptcCzG8U7yXWv0BTL6u7o+qGf0T8M5TAwF4ODX1nsUy7DqUfwpTU4rxJPW2K3r7/LjA3RiAH+B65RvihX2W86Dna2ltiZiplciQDgY8wCMRtdO1Q4i/B4ws9VcCOfoGC3rgaxGSYZs57Kj+teLjXemF6XLp5+WTo9PS/AFiFBy9jl2CpNHH3l6sXE7ePQxOLx0OviR4Pj0e/AeJhtfXToVOUvJk4vczLD8D8ufkWwxh32wBTWMTmGydiBFJBlj7gI1DiSMXFso+gdHEhxkUPnFLkgjI7kbgjipB0DoaCdMIysUaNFbNJl6o4fH/+BMyUnxcHZvp1mS30Cc5sOl3kgqzMVJvjzN2M5k42+T+WH28N5CqvHDth3n9+CtPHO1/uzjF2pQHmq/fv0PlbEDSvdwgb1erS9XDx+uMqieu1BfPvfNb1xgcKG+OHodntCN114z9GQ3623GR/5GfIXd4O3LCUWhJhwSBmp9PpC9wI1CHuJX4mYh1fFFU7kvmsKtI5MH9o/sGSgVnVcvhSyUnUfHebTM903gE0btLvHeab5vIm8n7fz7fvxL3mY7aL/+E/rcfH+IyK7zeWDrt/UvbJ4vaMvLy0+Jfy7u5LayNB+Dx4SDwr30vHqBGcQ7o87ooPh4NM0sSgMBoGlkxs+lgCN+LgVgt0Ux4F6XE3n/yFB/pVgyccwtl8ucEdtqsu+5dsgWcOyQqjvGUlS6SbhWG44Lu/ZKa8NxdhRsKwl2tPF21F6TsJLHb+If/GBdHlCKjuMH/Az/hikNSQszbJtV8VjSHN6mvEbsJkz+/Z1//L+OaK5xvMnH7SnhhHC/cFo4hyr4o8KScL52JmNvsfDiPBRAFeI8XsgxfGwlUtYp5npOcAC9BYpVIt8AfDqIJUwGggeQ5YuaowNpa3WgRx95eGH/vl3VnT2Frq0JYQqmPO44T7ZKhVay24ckWp1qJzKoXErtxmEiplgRW+QDR4q0zB1ASEBOFLnNKsUqhSwo7eRdPgRihci4orFK7q6PakmJVKq448wVL18z9K3J3l2pVlawWoIDllW6WNbsmtESKcRTu3raLMXfmkvpvqTf59eYJkr+uKKoqY6cPwBB/drLi9/5IyYrDLwRyad6Izi/eBNSwBPIgimZuXA4CSEWounm0tO/6BJ17+VKi6jbhfGekZ7iHjmqBwxDCbUqe4o9IzvGuxNBFsnJSjwWIsJMryKKiq0HrFaNib0J5tXFrl88jVJw/XkK9pMUQ/QpfkvSVT0iBbx+jyL7FUmFAPiIrUdY0WWm2YNcN+iqdWwFJqvAo3hlJjtY+RL5HTQJNeKxCAclz7pY6+t5Jcqb5rJprtrcvuWoNTimNlWUuzMvqxuUB1IofqS6mo+bSYrCuvpfatY/i4KCwQMRcOgGaZ5cBWAV4zUSi1M/4CDm6wkYnLic8qa5WH9UTIISqWFUZ9RWeJ03y4QFdaO6hpX+EWWicsYzXYVJadohYIbrZrFaRNzAH9PS5La5m7AC1UVH7VqnMrKUKqPSp2/QBN1yJpvl+NaXk3A5dP7/at77aHSu7j8NP3XsKupdfCwRh+Hpd+glr6vyStnNey+v2nCm4Z+csoy7eG4cQ8PvckfhL51m8Uu3UuqKXZLGwJ3CfuFE7VgeRHkrSvC1LJO0/iJTaNQjKjaNJD2UqHCcA4VEvDlyg9ZkvnqpAIGZbogZSefaO8uxdCzj4U47OqMlA15d4tEr9g6JqB+i8hhdw8HBkcFZdjOQa05SfHT2KwOSPKmYUuflkd1nD+dZ9+RH5093HDLDLgnH+MALR469uDjc+JN16NcOafGRA4ouHZJBKe8mfuPzk90d2UPB7Q4Fx3jf0L7FFyfec+Es3yKSJnnFny0oRIQsaqB9wpna6WInU7U2kNlWK+DHCVUckyjIWvGLPpQFCRcYpwjUx2k2wGlBuOABVW2CdZJlLCAcME1BqJZ7d27v6mjPpLah9m1GzEg4hCUZ1QA3PkcUdSVMIYxTKbdGNzPojfJx1kq7F0he2SOeAteF/ikpAJ97hvVHdZ7E/38LSI2htzgEJ1xxtuxzjfvwm8a3HeC9rfBLf2MJrjf8rhc+7MX3l/2vXnW8h+nzfwBpRuuSeJxjYGRgYADi8itp8vH8Nl8ZuJlfAEUYbt08uhpG/3/8P5NlP3MwkMvBwAQSBQCTxw8bAHicY2BkYGAO+p/FwMDK+v/x/ycs+xmAIihgIgCZggbkeJxtUTsOwjAMTetWYs1JKiQ2Nm6RA3AFDsDAGRizVuIkXIOlIwMLCGr8HKcNqMOT7frzXl5pcI4E9dW5pmOmKPVaEBcgc20rMaRcMfVsF988Pynwh6JEneMXatyvHjwq3xaQeu8ckDVMMRpP+MsL7uqCnvAGcDPjZuKVG0fre+SYWXhPwJvtpreZEr545x9UM7iX+nYXGrIWROyoxrwn+prDjHJfe12haUh6sl6dD/CSx2l//hf3eiVcJ0B8kZx22XPz6MzvX1+Tb7SRezXfqJe8T/zQr37av8p11vUF6jqIwwAAAAAAAADuATIB9gIMAioCWgJ2AsIDRgPKBOQFagYABrIHSAhMCVQJzApoCvQLKAuMDGYM4g3yEfYSMhJ+ExQTPBNiE4oTrBPiFCIUWBSYFNwVIhVoFawWKhaQFvQXehfCGAoYnBjuGVgaBhpsGzAbjBu+HHYc9B1+HgAeoh+QIAogxiJqIywjtCSwJYgmZicaJ94oWiimKTYp8iqQKvorRCvMLMItFC1qLdwuVC6cLw4vYC+yL/owYjDGMVIxoDKOMtgzNjO8NH40yDV6NhA2WjbSN5g4YjkGOXw6njsEO8Q8KDxwPKg9Cj1WPdg+Pj5wPrA+7D8eP1w/tEAMQC5AqEEYQXRB+EJiQu5DOkOqRDRE1EXCRiJGgka2RupHIEdWR+ZIcwABAAAAkQH4AA8AAAAAAAIARABUAHMAAACwC3AAAAAAeJx1ks1OwkAUhc8IaIToQhM3bu5GIzEptYkbVhoiLFyYsGDjqkJpS0qHTAcTXsB38AF8LZ/F02FUXNhmpt8592fuJAVwgk8obJ9bri0rtKi2vIcD9D036N97bpKHnlvo4NHzPtXEcxvXePbcwSne2EE1D6kWePes0FYNz3s4VkeeG/TPPTfJF55bOFM9z/v0Hzy3MVFPnju4VB8DvdqYPM2sXA26EoVRKC8b0bTyMi4kXttMm0ruZK5LmxSFDqZ6mddinKTrIjaO3TZJTJXrUm6C0OlRUiYmtsms7li9ppG1c5kbvZSh7yUroxfJ1AaZtat+r7d7BgbQWGEDgxwpMlgIruh2+Y0QuiV4YYYwc5uVo0SMgk6MNSsyF6mo77jmVCXdhBkFOcCU+5JV35ExYykrC9abHf+XJsyoO+ZOC27YJdyJjxgvXU7sTpr9zFjhlb0jupbZ9TTGnS78Qf7OJbx3HVvQmdIP3O0t3T56fP+5xxdzvHj7AHicbVSFluM2FM2d2DHMJLOzZWauy90yMzOjLMu2GtnyCiaTKTPD9osr2U63c05zTqT73tHTg3vlydZk+KWT//+dwhamCBBihggxEqTYxg7mWGAXx7CH4zgDZ+IsnI1zcC7Ow/m4ABfiIlyMS3ApLsPluAJX4ipcjWtwLa7D9bgBNyLDTbgZt+BW3IbbcQfuxAnchbtxD+7FfbgfD+BBPISH8QgexWN4HE/gSTyFp/EMnsVzeB4v4EW8hJfxCl7Fa3gdb+BNvIW38Q7exXt4Hx/gQ3yEj0GQg6IAQ4kKNTg+wRICDVpIdDgJBQ0Di32scIA1DvEpPsPn+AJf4it8jW/wLb7D9/gBP+In/Ixf8Ct+w+/4A3/iL5zC35OkILrOJVFFYDVToV/0llzOKGkpE0EnrA4b3lq9U0pRMJWxpjPruJCrVkhSzGznt2nFTUhtznRcEENyollYEVuxSHPDGtLtaKlM1pKGZbZbnDb8PUnDKtLVsmXT3FahIXqpZyUXhqmpLMsgl3IZdkQbFmvKtQvWYSVkzkIqpC3CUrge4pwoWhNl+tKygitXmt9iwUrjQaJ4VQ+oPyI71qaDz8PIHfd70p/3yF+Q82qMc2i4yoP+Agd2FdP8kGWlFSIjwmz/x94ZsW6IEEEj99ne6Kml4oeyNURs4veZMpwSER1K2WS8DXMh6TLuLWlNInwJuRW5b5kuk30pbD/K7RH5gtIR+5k11rBpw2nE2sLwhiXauNl4tHBpnJNsmNyYs5ViLa0jLbijWcdOCPucMh2NIOwZih0vLOuKMunBSqoi7RE7cHJxc6GZYQcmNMpxMqeyaVhrhkzRaAWOJpP6ZfAHORMi9ouf4JwY4w5x2Xor7BR3EazgJiqlWjmdhop1Yp30qzsipmzNpoZUgfvruZ9OT56PTv61Ao+CWjYs4G0pg5qJbqaZl0zs5NN1vK1miq14W6S9ijLBXbN9UW6ciw0YK3avoopcXu9KiFJypTO62qIrl0KbxNS2ybVnZ0SenUi7JC1TM9cgcWmXbD28OzcTG6x4yZ1GZJsM6TvOxkKIYmR0OonPckaW7p02pOI09FeeSHsx9ipLe4H2cHtQbY9jJ+QeTN35PdJWwgvG5m7zAcePePq4GeWKCrbjh5UNuIjMijtu1LGSUObfZKZPWlddMa/8V2Jj7Z0mcAwMac3oMnVSd6Cwgs0dj62b+KgLU7tpa7Nwez+vwb1wn5Ta5ptLdo8UabujbfTyHzy+o+0B9q3EA7ZduolwXNQun1TrNOetpFYQpSeTfwDl8f2IeJxj8N7BcCIoYiMjY1/kBsadHAwcDMkFGxlYnTYxMDJogRibuZgYOSAsPgYwi81pF9MBoDQnkM3utIvBAcJmZnDZqMLYERixwaEjYiNzistGNRBvF0cDAyOLQ0dySARISSQQbOZhYuTR2sH4v3UDS+9GJgYXAAx2I/QAAA==') format('woff'),
+ url('data:application/octet-stream;base64,AAEAAAAPAIAAAwBwR1NVQiCLJXoAAAD8AAAAVE9TLzI+IVLUAAABUAAAAFZjbWFwyZKXbQAAAagAAAl6Y3Z0IAbX/wIAAKj0AAAAIGZwZ22KkZBZAACpFAAAC3BnYXNwAAAAEAAAqOwAAAAIZ2x5Zug4Hl4AAAskAACQ5mhlYWQZdM73AACcDAAAADZoaGVhB/cE5QAAnEQAAAAkaG10eOjK/5YAAJxoAAACRGxvY2G9sOQFAACerAAAASRtYXhwAggNvgAAn9AAAAAgbmFtZcUUevQAAJ/wAAACqXBvc3QML0mAAACinAAABlBwcmVw5UErvAAAtIQAAACGAAEAAAAKADAAPgACREZMVAAObGF0bgAaAAQAAAAAAAAAAQAAAAQAAAAAAAAAAQAAAAFsaWdhAAgAAAABAAAAAQAEAAQAAAABAAgAAQAGAAAAAQAAAAEDXwGQAAUAAAJ6ArwAAACMAnoCvAAAAeAAMQECAAACAAUDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBmRWQAQOgA8eUDUv9qAFoDUwCXAAAAAQAAAAAAAAAAAAUAAAADAAAALAAAAAQAAAKqAAEAAAAAAaQAAwABAAAALAADAAoAAAKqAAQBeAAAABIAEAADAALogeiF8AnwC/Cb8Qfx2vHl//8AAOgA6IPwCfAL8JvxAvHa8eX//wAAAAAAAAAAAAAAAAAAAAAAAQASARQBGAEYARgBGAEiASIAAAABAAIAAwAEAAUABgAHAAgACQAKAAsADAANAA4ADwAQABEAEgATABQAFQAWABcAGAAZABoAGwAcAB0AHgAfACAAIQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAAxADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0ATgBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXQBeAF8AYABhAGIAYwBkAGUAZgBnAGgAaQBqAGsAbABtAG4AbwBwAHEAcgBzAHQAdQB2AHcAeAB5AHoAewB8AH0AfgB/AIAAgQCCAIMAhACFAIYAhwCIAIkAigCLAIwAjQCOAI8AkAAAAQYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAG0AAAAAAAAACQAADoAAAA6AAAAAABAADoAQAA6AEAAAACAADoAgAA6AIAAAADAADoAwAA6AMAAAAEAADoBAAA6AQAAAAFAADoBQAA6AUAAAAGAADoBgAA6AYAAAAHAADoBwAA6AcAAAAIAADoCAAA6AgAAAAJAADoCQAA6AkAAAAKAADoCgAA6AoAAAALAADoCwAA6AsAAAAMAADoDAAA6AwAAAANAADoDQAA6A0AAAAOAADoDgAA6A4AAAAPAADoDwAA6A8AAAAQAADoEAAA6BAAAAARAADoEQAA6BEAAAASAADoEgAA6BIAAAATAADoEwAA6BMAAAAUAADoFAAA6BQAAAAVAADoFQAA6BUAAAAWAADoFgAA6BYAAAAXAADoFwAA6BcAAAAYAADoGAAA6BgAAAAZAADoGQAA6BkAAAAaAADoGgAA6BoAAAAbAADoGwAA6BsAAAAcAADoHAAA6BwAAAAdAADoHQAA6B0AAAAeAADoHgAA6B4AAAAfAADoHwAA6B8AAAAgAADoIAAA6CAAAAAhAADoIQAA6CEAAAAiAADoIgAA6CIAAAAjAADoIwAA6CMAAAAkAADoJAAA6CQAAAAlAADoJQAA6CUAAAAmAADoJgAA6CYAAAAnAADoJwAA6CcAAAAoAADoKAAA6CgAAAApAADoKQAA6CkAAAAqAADoKgAA6CoAAAArAADoKwAA6CsAAAAsAADoLAAA6CwAAAAtAADoLQAA6C0AAAAuAADoLgAA6C4AAAAvAADoLwAA6C8AAAAwAADoMAAA6DAAAAAxAADoMQAA6DEAAAAyAADoMgAA6DIAAAAzAADoMwAA6DMAAAA0AADoNAAA6DQAAAA1AADoNQAA6DUAAAA2AADoNgAA6DYAAAA3AADoNwAA6DcAAAA4AADoOAAA6DgAAAA5AADoOQAA6DkAAAA6AADoOgAA6DoAAAA7AADoOwAA6DsAAAA8AADoPAAA6DwAAAA9AADoPQAA6D0AAAA+AADoPgAA6D4AAAA/AADoPwAA6D8AAABAAADoQAAA6EAAAABBAADoQQAA6EEAAABCAADoQgAA6EIAAABDAADoQwAA6EMAAABEAADoRAAA6EQAAABFAADoRQAA6EUAAABGAADoRgAA6EYAAABHAADoRwAA6EcAAABIAADoSAAA6EgAAABJAADoSQAA6EkAAABKAADoSgAA6EoAAABLAADoSwAA6EsAAABMAADoTAAA6EwAAABNAADoTQAA6E0AAABOAADoTgAA6E4AAABPAADoTwAA6E8AAABQAADoUAAA6FAAAABRAADoUQAA6FEAAABSAADoUgAA6FIAAABTAADoUwAA6FMAAABUAADoVAAA6FQAAABVAADoVQAA6FUAAABWAADoVgAA6FYAAABXAADoVwAA6FcAAABYAADoWAAA6FgAAABZAADoWQAA6FkAAABaAADoWgAA6FoAAABbAADoWwAA6FsAAABcAADoXAAA6FwAAABdAADoXQAA6F0AAABeAADoXgAA6F4AAABfAADoXwAA6F8AAABgAADoYAAA6GAAAABhAADoYQAA6GEAAABiAADoYgAA6GIAAABjAADoYwAA6GMAAABkAADoZAAA6GQAAABlAADoZQAA6GUAAABmAADoZgAA6GYAAABnAADoZwAA6GcAAABoAADoaAAA6GgAAABpAADoaQAA6GkAAABqAADoagAA6GoAAABrAADoawAA6GsAAABsAADobAAA6GwAAABtAADobQAA6G0AAABuAADobgAA6G4AAABvAADobwAA6G8AAABwAADocAAA6HAAAABxAADocQAA6HEAAAByAADocgAA6HIAAABzAADocwAA6HMAAAB0AADodAAA6HQAAAB1AADodQAA6HUAAAB2AADodgAA6HYAAAB3AADodwAA6HcAAAB4AADoeAAA6HgAAAB5AADoeQAA6HkAAAB6AADoegAA6HoAAAB7AADoewAA6HsAAAB8AADofAAA6HwAAAB9AADofQAA6H0AAAB+AADofgAA6H4AAAB/AADofwAA6H8AAACAAADogAAA6IAAAACBAADogQAA6IEAAACCAADogwAA6IMAAACDAADohAAA6IQAAACEAADohQAA6IUAAACFAADwCQAA8AkAAACGAADwCwAA8AsAAACHAADwmwAA8JsAAACIAADxAgAA8QIAAACJAADxAwAA8QMAAACKAADxBAAA8QQAAACLAADxBQAA8QUAAACMAADxBgAA8QYAAACNAADxBwAA8QcAAACOAADx2gAA8doAAACPAADx5QAA8eUAAACQAAAACQAA//kD6AMLAA8AHwAvAD8ATwBfAG8AfwCPAE9ATBENAgcQDAIGAwcGYA8JAgMOCAICAQMCYAsFAgEAAAFUCwUCAQEAWAoEAgABAEyOi4aDfnt2c25rZmNeW1ZTTks1NTU1NTU1NTMSBR0rJRUUBgcjIiYnNTQ2FzMyFhMVFAYnIyImJzU0NjczMhYBFRQGByMiJic1NDYXMzIWARUUBisBIiYnNTQ2OwEyFgEVFAYnIyImJzU0NjczMhYBFRQGByMiJj0BNDYXMzIWARUUBisBIiYnNTQ2OwEyFgEVFAYnIyImPQE0NjczMhYTFRQGKwEiJj0BNDY7ATIWAR4gFrIXHgEgFrIXHgEgFrIXHgEgFrIXHgFmIBayFx4BIBayFx7+nCAWshceASAWshceAWYgFrIXHgEgFrIXHgFmIBayFiAgFrIXHv6cIBayFx4BIBayFx4BZiAWshYgIBayFx4BIBayFiAgFrIXHppsFh4BIBVsFiABHgEGaxYgAR4XaxceASD+zWwWHgEgFWwWIAEeAiRrFiAgFmsWICD+zGsWIAEeF2sXHgEg/s1sFh4BIBVsFiABHgIkaxYgIBZrFiAg/sxrFiABHhdrFx4BIAEIaxYgIBZrFiAgAAACAAD/sQLKAwwAFQAeACVAIgAFAQVvAwEBBAFvAAQCBG8AAgACbwAAAGYTFxERFzIGBRorJRQGIyEiJjU0PgMXFjI3Mh4DAxQGIi4BNh4BAspGMf4kMUYKGCo+LUnKSipCJhwIj3y0egSCrIRFPFhYPDBUVjwoAUhIJj5UVgHAWH5+sIACfAAABv///2oELwNSABEAMgA7AEQAVgBfAG9AbE8OAgMCAUcACwkICQsIbRABCAIJCAJrDwECAwkCA2sHAQUAAQAFAW0MCgIBBgABBmsABgQABgRrDgEDDQEABQMAYBEBCQkMSAAEBA0ESV5dWllWVFJQS0pJR0NCPz46ORkVFBk3IxMhEBIFHSsBBgcjIiY3NDMyHgE3MjcGFRQBFAYjISImJzQ+BTMyHgI+AT8BNjcyHgQXARQGIiY0NjIWARQGLgE+AhYFFAYnIyYnNjU0JxYzMj4BFzInFAYiJjQ2MhYBS1o6Sy1AAUUEKkIhJiUDAoNSQ/4YRFABBAwQICY6IQYkLkhQRhkpEAgiOCYgEA4B/cZUdlRUdlQBiX6wgAJ8tHoBQz4uSzlaLQMlJSFEKARFR1R2VFR2VAFeA0QsLMUWGgENFRBO/ltCTk5CHjhCODQmFhgcGgIWEBoKAhYmNDhCHAKPO1RUdlRU/u9ZfgJ6tngGhNMrLgFEA0FOEBUNGBgBjztUVHZUVAABAAD/9gOPAsYABQAGswQAAS0rBQE3FwEXAWD+sp6wAZCfCgFNoK4BkaAAAAEAAP/XAx8C5QALAAazBwEBLSslBycHJzcnNxc3FwcDH5zq65zq6pzr6pzqdJ3r653q6p3r653qAAAAAAEAAP+fA48DHQALADBALQAEAwRvAAEAAXAGBQIDAAADUgYFAgMDAFYCAQADAEoAAAALAAsREREREQcFGSsBFSERIxEhNSERMxEDj/6x3/6xAU/fAc7f/rABUN8BT/6xAAEAAAAAA48BzgADAB5AGwAAAQEAUgAAAAFWAgEBAAFKAAAAAwADEQMFFSs3NSEVEgN979/fAAAAAwAA/58DjwMdAAsAEQAVAERAQQACCAEFAAIFXgAAAAQDAAReAAMABgcDBl4JAQcBAQdSCQEHBwFYAAEHAUwSEgwMEhUSFRQTDBEMERESEzMQCgUZKwEhERQGIyEiJjURIQUVITUhNQERIREB0AG/Qi79Yy5CAb7+sgKd/kIBvv1jAq39Yy9CQi8DDXDfcG/9YwFP/rEAAAAEAAD/+QOhA1IACAARACcAPwBEQEE8AQcICQACAgACRwkBBwgDCAcDbQAGAwQDBgRtBQEDAQEAAgMAYAAEAAIEAlwACAgMCEk/PSQlFiISJTkYEgoFHSslNC4BDgEWPgE3NC4BDgEWPgE3FRQGByEiJic1NDYzIRcWMj8BITIWAxYPAQYiLwEmNzY7ATU0NjczMhYHFTMyAsoUHhQCGBoYjRQgEgIWHBhGIBb8yxceASAWAQNLIVYhTAEDFiC2ChL6Ch4K+hEJChePFg6PDhYBjxhkDxQCGBoYAhQPDxQCGBoYAhSMsxYeASAVsxYgTCAgTCABKBcQ+gsL+hAXFfoPFAEWDvoAAAAEAAD/sQOhAy4ACAARACkAQABGQEM1AQcGCQACAgACRwAJBglvCAEGBwZvAAcDB28ABAACBFQFAQMBAQACAwBgAAQEAlgAAgQCTD08IzMjIjIlORgSCgUdKyU0Jg4CHgE2NzQmDgIeATY3FRQGIyEiJic1NDYXMx4BOwEyNjczMhYDBisBFRQGByMiJic1IyImPwE2Mh8BFgLKFB4UAhgaGI0UIBICFhwYRiAW/MsXHgEgFu4MNiOPIjYN7hYgtgkYjxQPjw8UAY8XExH6Ch4K+hIdDhYCEiASBBoMDhYCEiASBBqJsxYgIBazFiABHygoHx4BUhb6DxQBFg76LBH6Cgr6EQAAAAAGAAD/agPCA1IABgAPADsARwBrAHQA+kAYWVITEQQDCkgxAg8DSSwCBw8DRxABBQFGS7AOUFhAVwAMERAIDGUABggCCAYCbQADCg8KAw9tAAcPCQ8HCW0AAAkBCQABbQAFAAIKBQJgDQsCCA4BCgMICmEADwAJAA8JYAAQEBFYABERDEgAAQEEWAAEBA0ESRtAWAAMERARDBBtAAYIAggGAm0AAwoPCgMPbQAHDwkPBwltAAAJAQkAAW0ABQACCgUCYA0LAggOAQoDCAphAA8ACQAPCWAAEBARWAAREQxIAAEBBFgABAQNBElZQCNzcm9ua2lnY2JhX15bWlhXTEpDQj08Ozo5NyYkIiMhIRIFGCslNCMiFDMyAzQmJyIVFDMyExUGBxYVFAYHDgEVFB4FFxQjIi4CNTQ3NSY1NDc1LgEnNDYXMhcyEyM2NRE0JzMGFREUJRUGIyIuAz0BMzUjIiciBzUzNTQnMwYVMxUiJisBFRQzMgEUBi4CPgEWAUxcWGBUISIgRUVClhQYCVJFFhYaJjIuKhYCyyZEPiRmJiMoNAFqTjYuNvV8AgJ8AwFSKDkjMhwQBAELBwMMFTYEfwNfCCAILzAi/tosQCwBLEIqBThzAeEiLAFRSwEBcAcGGBdGZA0FFBcRFg4KFBYwH6oOIDwpXCEDFjA9DwMNXi5NaAEa/i8ZMQFUNRMTMv6pMWNuFhgeOiwkxAIBA2oqHhQXRWoCzEkCIyAyATBCMAEyAAAHAAD/agS/A1IAAwAHAAsADwATABcAQAA1QDI9MCEXFhUTEhEQDw4NCwoJCAcGBQMCAQAYAAIBRwACAgxIAQEAAA0ASTc2JiUfHgMFFCsFNzUHJzcnBwE3NQcnNycHJzc1Byc3JwcBFRQGDwEGIi8BBg8BBiIvAS4BJzU0Nj8BNTQ2PwE2Mh8BHgEdARceAQFl1tYk4uLhA0HW1iTh4eIY1tYk9vb2A1UUE/oOJA7+AQP6DiQN+hMUARgU8hgT+g0eDfoUGPIUGD1rsFw/YGFh/qJrsFw/YGFhQ1yVXD9pamr+dukUIgl9CAh/AQF9CAh9CSIU6RUkCGjfFiQIawYGawkiF99oCCQAAAAABAAA/2oDWwNSAA4AHQAsAD0Ab0BsOQwDAwcGKiECAQAbEgIFBANHCwEAKQEEGgECA0YABwYABgcAbQgBAAABBAABYAoBBAAFAgQFYAsBBgYMSAkBAgIDWAADAw0DSS4tHx4QDwEANjUtPS49JiUeLB8sFxYPHRAdCAcADgEODAUUKwEyNjcVFA4BIi4BJzUeARMyNjcVFA4BIi4BJzUeATcyNjcVFA4CLgEnNR4BEzIeAQcVFA4BIi4BJzU0PgEBrYTmQnLI5MpuA0LmhYTmQnLI5MpuA0LmhYTmQnLI5MpuA0LmhXTEdgJyyOTKbgN0xAGlMC9fJkImJkImXy8w/lQwL18nQiYmQidfLzDWMC9fJkImAio+KF8vMAKDJkInRydCJiZCJ0cnQiYABwAA/7ED6ALDAAgAEQAjACwANQA+AFAAZEBhLQECBjYJAgMHJAACAQADRwgBAgYHBgIHbQAHAwYHA2sJAQMABgMAawQBAAEGAAFrAAsABgILBmAFAQEKCgFUBQEBAQpYAAoBCkxNTEVCPTw5ODQzMC8rKicmExQTEgwFGCs3NCYiBh4CNhM0JiIOAR4BNhc3Ni4BBg8BDgEHBh4BNjc2JiU0JiIOAR4BNgE0JiIOAR4BNhc0JiIOAR4BNhcUBwYjISInJjU0PgIyHgLWKjosAig+Jm0oPiYELjYw6zkDEBocAzghNggLLFhKDQkaAVYqPCgCLDgu/pgoPiYELjYw9ig+JgQuNjCvTwoU/PIUCk9QhLzIvIRQzx4qKjwoAiwBFh4qKjwoAizw1Q4aBgwQ1QMsIStMGC4rIUAlHioqPCgCLAGBHioqPCgCLE8eKio8KAIs3pF8ERF7kma4iE5OiLgAAAABAAD/sQPoAwsAVQBOQEsADAsMbw0BCwoLbw8JBwUDBQECAAIBAG0IBAIAAG4OAQoCAgpUDgEKCgJWBgECCgJKVFJPTUxKRUI9Ozo4NTM1IRElNSERJTMQBR0rJRUUBisBIiY9ATQ2FzM1IRUzMhYXFRQGKwEiJic1NDYXMzUhFTMyFhcVFAYrASImJzU0NhczNTQ2FyE1IyImJzU0NjsBMhYXFRQGByMVITIWBxUzMhYD6CAWshYgIBY1/uM1Fx4BIBayFx4BIBY1/uM1Fx4BIBayFx4BIBY1Kh4BHTUXHgEgFrIXHgEgFjUBHR0sATUXHpqzFiAgFrMWIAFrax4XsxYgIBazFiABa2seF7MWICAWsxYgAWsdLAFrIBWzFiAgFrMWHgFrKh5rHgAEAAD/agOfA1IACgAiAD4ATgEiQA8XAQADNCwCBggmAQEJA0dLsBNQWEBFAAcGAgYHZQQBAgoGAgprEwEKCQkKYwAAAA0MAA1eFBIQDgQMDwELCAwLXgAIAAYHCAZeEQEDAwxIAAkJAVkFAQEBDQFJG0uwFFBYQEYABwYCBgdlBAECCgYCCmsTAQoJBgoJawAAAA0MAA1eFBIQDgQMDwELCAwLXgAIAAYHCAZeEQEDAwxIAAkJAVkFAQEBDQFJG0BHAAcGAgYHAm0EAQIKBgIKaxMBCgkGCglrAAAADQwADV4UEhAOBAwPAQsIDAteAAgABgcIBl4RAQMDDEgACQkBWQUBAQENAUlZWUAoPz8jIz9OP05NTEtKSUhHRkVEQ0JBQCM+Iz49OxERGRQUIyQeEBUFHSsBMy8BJjUjDwEGBwEUDwEGIi8BJjY7ARE0NjsBMhYVETMyFgUVITUTNj8BNSMGKwEVIzUhFQMGDwEVNzY7ATUTFSM1MycjBzMVIzUzEzMTApliKAYCAgECAgP+2gayBQ4GswgIDWsKCGsICmsICgHS/rrOBwUGCAYKgkMBPc4ECAYIBQuLdaEqGogaKqAngFuAAm56GgkCCwoKBv1GBgeyBQWzCRUDAAgKCgj9AApKgjIBJwsFBQECQIAy/tgECgcBAQJCAfU8PFBQPDwBcf6PAAAABAAA/2oDnwNSAAoAIgAyAE0BLkAMRj4XAw4DNgENEQJHS7ATUFhASgAPDhIOD2UUARIRERJjAAsNAg0LAm0EAQIADQIAawARAA0LEQ1fAAAABwYAB14ADg4DWBABAwMMSBMMCggEBgYBVgkFAgEBDQFJG0uwFFBYQEsADw4SDg9lFAESEQ4SEWsACw0CDQsCbQQBAgANAgBrABEADQsRDV8AAAAHBgAHXgAODgNYEAEDAwxIEwwKCAQGBgFWCQUCAQENAUkbQEwADw4SDg8SbRQBEhEOEhFrAAsNAg0LAm0EAQIADQIAawARAA0LEQ1fAAAABwYAB14ADg4DWBABAwMMSBMMCggEBgYBVgkFAgEBDQFJWVlAKDMzIyMzTTNNTElFRENCQUA1NCMyIzIxMC8uLSwREREUFCMkHhAVBR0rJTMvASY1Iw8BBgcFFA8BBiIvASY2OwERNDY7ATIWFREzMhYFFSM1MycjBzMVIzUzEzMTAxUhNRM2PwE1IgYnBisBFSM1IRUDDwEVNzM1ApliKAYCAgECAgP+2gayBQ4GswgIDWsKCGsICmsICgIEoSoaiBoqoCeAW4AL/rrOBwUGAQQDBgqCQwE9zgwGCJszehoJAgsKCQd/BgeyBQWzCRUDAAgKCgj9AAqROztQUDs7AXL+jgKDgzMBJwoFBQICAQJAgDL+2Q8FAgJDAAAAAv///6wD6AMLAC4ANABNQEowAQQFMgEABDMBAwEvDwsDAgMERxUBAkQABQQFbwAEAARvAAMBAgEDAm0AAgJuAAABAQBUAAAAAVgAAQABTCwrKiciIBMTEAYFFysBMhYUBgcVFAYHJicOARYXDgEeAhcOASYnLgQ2NyMiJjc1NDYzITIlMhYXAxEGBxUWA6EdKiodLBzp3CAmBBQLBAwaGhYRXGAZBBoKDgQICEQkNgE0JQEM8wEBHSoBSNzQ0gHtKjwoAdYdKgHCEgo0PhQTJBwiFhEgHA4YDUgiQi5AHjQlayU01ywc/dkCFKgXlxcAAgAA/8MDjwMuAEEARwBlQGI9LgIDCQABAAckHA0GBAIAA0cKAQgNDA0IDG0EAQIAAQACAW0FAQEBbgANAAwJDQxeAAkAAwcJA14LAQcAAAdUCwEHBwBYBgEABwBMRkVDQkA+OTg2NRUUJicRERcWEw4FHSsBFAYnIxQHFxYUBiIvAQcOAyMRIxEiLgIvAQcGIyImND8BJjUjIi4BNjczNScmNDYyHwEhNzYyFgYPARUzMhYBITQ2MhYDjxYOfSV0ChQeCm8IBSYiOhlHHTgqHgoIZgsQDRYIcSB9DxQCGA19YQsWHAthAddgCxwYBAhhfQ8U/vX+m2iUagE6DhYBYEJ1CxwWC24HBBgSDgH0/gwOGBQICHQMEx4Lfz9aFB4UAaRhCh4UCmFhChQeCmGkFgE0SmhoAAAABgAA//kD6AMLAAMABwALABsAKwA7AF9AXCwBBQs0AQoEHAEDCRQBBgAERwALAAUECwVeAAQACgkECmAACQADAgkDXgACAAgHAghgAAcAAQAHAV4AAAYGAFIAAAAGWAAGAAZMOjcyLyooJiYlEREREREQDAUdKyUhNSEnITUhJTM1IwEVFAYHISImJzU0NhchMhYTFRQGJyEiJic1NDY3ITIWExUUBiMhIiYnNTQ2MyEyFgI7AWb+mtYCPP3EAWXX1wEeFg78YA8UARYOA6APFAEWDvxgDxQBFg4DoA8UARYO/GAPFAEWDgOgDxRASNZH10f96I4PFAEWDo4PFgEUAQ6PDhYBFA+PDxQBFgEQjw4WFg6PDhYWAAH/+f+xAxgCwwAUABhAFQ4DAgABAUcAAQABbwAAAGY4JwIFFisBFgcBERQHBiMiLwEmNREBJjYzITIDDwkR/u0WBwcPCo8K/u0SExgCyhcCrRYR/u3+YhcKAwuPCw4BDwETESwAAAAAAv/9/7EDWQNSACgANAAiQB8AAgMBAwIBbQABAAABAFwAAwMMA0kzMi0sGhkUBAUVKwEUDgIiLgI3NDY3NhYXFgYHDgEVFB4CMj4CNzQmJy4BPgEXHgEBERQGIiY3ETQ2MhYDWURyoKyibkoDWlEYPBASCBg2PC5ManRoUCoBPDYXCiQ8F1Fa/psqOiwBKjwoAV5XnnRERHSeV2ayPhIIGBc8ESl4QzpqTC4uTGo6RHYqEjowCBI9tAFI/podKiodAWYdKioAAAAD//n/sQOpAwsAUQBhAHEAVEBROAEFAVABBAUPDQwDAgYDRwAGBwIHBgJtAAIDBwIDawABAAUEAQVeAAQABwYEB2AAAwAAA1QAAwMAWAAAAwBMbmxmZF5dVlVLSEVCPTo1CAUVKwEWBwMOAQchIiYnJj8BNjc0JjU2PwE+ATc2JjY/AT4BNzYmNzY/AT4BNzQmPgE/Aj4BPwE+AhcVNjMhMhYHAw4BByEiBhcWMyEyNjcTNicWBQYWFyEyNj8BNiYnISIGDwEGFhchMjY/ATYmByEiBgcDkxYMmgpAJf39K1APDg0BAQIEAQQSDRgFAgQEBwoMFgMBBAICCg0KGgMEAggGCgkFBgYLBRQUEBUHAakpLg2ZFCg0/hsPDAUOQwIDEB4FpwQBFf26AgYIAVMIDgIMAgYJ/q0HDgI6AwgHAVMHDgMLAwgH/q0HDgMCRx8p/gckMAE8LCUiDw0HBQ4EBgYaFTwVBhYLCQ0UPhQFGAQHCg0OQhUEFAkMBwsRChQKEggKAgQBBUAo/gZCJgERDycSDgImDRMIEQcKAQwGJAcKAQwGswcKAQwGJAcMAQoIAAAABAAA/2oD6ANSAAgAGAAbADcAS0BIEgoCBAMyAQIEGwEFAgNHAAcBAAEHAG0ABAACBQQCXgAFAAEHBQFgAAMDCFgACAgMSAAAAAZYAAYGDQZJNSM1ExckEyEQCQUdKwUhESMiJic1Izc1NCYnISIGFxUUFjchMjYTMycFERQGByEiJic1ISImJxE0NjchMhYHFRYfAR4BAa0B9OkWHgHWjgoH/ncHDAEKCAGJBwqPp6cBHiAW/ekXHgH+0RceASAWAl8WIAEMCOQQFk8BZh4X6KEkBwoBDAYkBwwBCv6Rp+7+iRceASAWWSAVAu4XHgEgFrcHCOQPNgAH//r/sQPqAsMACABKAFgAZgBzAIAAhgB7QHh3dkA+BAkIeG1saGdCLQcFCYN5KgMBAIaAeicSBQoEghUCCwoFRwAHBggGBwhtAAILAwsCA20ABgAICQYIYAAJAAUACQVgAAAAAQQAAWAABAAKCwQKYAALAgMLVAALCwNYAAMLA0xmZF9dWFYqGigoJysaExAMBR0rATIWDgEuAjYXBRYGDwEGIiclBwYjFgcOAQcGIyInJjc+ATc2MzIXNj8BJyYnBiMiJy4BJyY2NzYzMhceARcWBx8BJTYyHwEeAQcFNiYnJiMiBwYWFxYzMgM+AScmIyIHDgEXFjMyExc1ND8BJwcGDwEGIx8BAScFFQcfAhYfAQU3JQcGBwIYDhYCEiASBBqzARsQBRBIBxMH/n8+BAMIAgQ2L0pQTDAzBwQ2LkpRLiYFCERECAUmLlFKLjYEAxYZL01QSi44AwIIBz4BgQcTB0gQBRD9aRocLTQ3KhUaHC0zOCkZLRwaFik4My0cGhUqN5c2EggsDwEECQEBeDYBmkf+U1kFBAYEAg8B4kf+3mMBBgFeFhwWAhIgEiLeCygIJAQE2CQDHBorUB0vLC9FKlAdLxIIBSgpBQcRLx5OKyE8FiwvHU4sGxsDJdgFBCQJJwxNGEocIRQYSB4h/nUcShcUIRxKFxQBdyEHFAsEGg4CBAkBghIBQSTwQDUFAwcFAQ+yI+RNAgIAAAAAA//9/7EDWQMLAAwBvQH3AndLsAlQWEE8AL0AuwC4AJ8AlgCIAAYAAwAAAI8AAQACAAMA2gDTAG0AWQBRAEIAPgAzACAAGQAKAAcAAgGeAZgBlgGMAYsBegF1AWUBYwEDAOEA4AAMAAYABwFTAU0BKAADAAgABgH0AdsB0QHLAcABvgE4ATMACAABAAgABgBHG0uwClBYQUMAuwC4AJ8AiAAEAAUAAAC9AAEAAwAFAI8AAQACAAMA2gDTAG0AWQBRAEIAPgAzACAAGQAKAAcAAgGeAZgBlgGMAYsBegF1AWUBYwEDAOEA4AAMAAYABwFTAU0BKAADAAgABgH0AdsB0QHLAcABvgE4ATMACAABAAgABwBHAJYAAQAFAAEARhtBPAC9ALsAuACfAJYAiAAGAAMAAACPAAEAAgADANoA0wBtAFkAUQBCAD4AMwAgABkACgAHAAIBngGYAZYBjAGLAXoBdQFlAWMBAwDhAOAADAAGAAcBUwFNASgAAwAIAAYB9AHbAdEBywHAAb4BOAEzAAgAAQAIAAYAR1lZS7AJUFhANQACAwcDAgdtAAcGAwcGawAGCAMGCGsACAEDCAFrAAEBbgkBAAMDAFQJAQAAA1gFBAIDAANMG0uwClBYQDoEAQMFAgUDZQACBwUCB2sABwYFBwZrAAYIBQYIawAIAQUIAWsAAQFuCQEABQUAVAkBAAAFVgAFAAVKG0A1AAIDBwMCB20ABwYDBwZrAAYIAwYIawAIAQMIAWsAAQFuCQEAAwMAVAkBAAADWAUEAgMAA0xZWUEZAAEAAAHYAdYBuQG3AVcBVgDHAMUAtQC0ALEArgB5AHYABwAGAAAADAABAAwACgAFABQrATIeARQOASIuAj4BAQ4BBzI+ATU+ATc2FyY2PwE2PwEGJjUUBzQmBjUuBC8BJjQvAQcGFCoBFCIGIgc2JyYjNiYnMy4CJy4BBwYUHwEWBh4BBwYPAQYWFxYUBiIPAQYmJyYnJgcmJyYHMiYHPgEjNj8BNicWPwE2NzYyFjMWNCcyJyYnJgcGFyIPAQYvASYnIgc2JiM2JyYiDwEGHgEyFxYHIgYiBhYHLgEnFicjIgYiJyY3NBcnBgcyNj8BNhc3FyYHBgcWBycuASciBwYHHgIUNxYHMhcWFxYHJyYGFjMiDwEGHwEGFjcGHwMeAhcGFgciBjUeAhQWNzYnLgI1MzIfAQYeAjMeAQcyHgQfAxYyPwE2FhcWNyIfAR4BFR4BFzY1BhYzNjUGLwEmNCY2FzI2LgInBiYnFAYVIzY0PwE2LwEmByIHDgMmJy4BND8BNic2PwE2OwEyNDYmIxY2FxY3JyY3FjceAh8BFjY3FhceAT4BJjUnNS4BNjc0Nj8BNicyNycmIjc2Jz4BMxY2Jz4BNxY2Jj4BFTc2IxY3Nic2JiczMjU2JyYDNjcmIi8BNiYvASYvASYPASIPARUmJyIuAQ4BDwEmNiYGDwEGNgYVDgEVLgE3HgEXFgcGBwYXFAYWAa10xnJyxujIbgZ6vAETAggDAQIEAxEVEwoBDAIIBgMBBwYEBAoFBgQBCAECAQMDBAQEBAYBBgIICQUEBgIEAwEIDAEFHAQDAgIBCAEOAQIHCQMEBAEEAgMBBwoCBAUNAwMUDhMECAYBAgECBQkCARMJBgQCBQYKAwgEBwUCAwYJBAYBBQkEBQMDAgUEAQ4HCw8EEAMDAQgECAEIAwEIBAMCAgMEAgQSBQMMDAEDAwIMGRsDBgUFEwUDCwQNCwEEAgYECAQJBFEyBAUCBgUDARgKAQIHBQQDBAQEAQIBAQECCgcHEgQHCQQDCAQCDgEBAgIOAgQCAg8IAwQDAgMFAQQKCgEECAQFDAcCAwgDCQcWBgYFCAgQBBQKAQIEAgYDDgMEAQoFCBEKAgICAgEFAgQBCgIDDAMCCAECCAMBAwIHCwQBAgIIFAMICgECAQQCAwUCAQMCAQMBBBgDCQMBAQEDDQIOBAIDAQQDBQIGCAQCAgEIBAQHCAUHDAQEAgICBgEFBAMCAwUMBAISAQQCAgUOCQICCggFCQIGBgcFCQwKaXNQAQwBDQEEAxUBAwUCAwICAQUMCAMGBgYGAQEECAQKAQcGAgoCBAEMAQECAgQLDwECCQoBAwt0xOrEdHTE6sR0/t0BCAIGBgEECAMFCwEMAQMCAgwBCgcCAwQCBAECBgwFBgMDAgQBAQMDBAIEAQMDAgIIBAIGBAEDBAEEBAYHAwgHCgcEBQYFDAMBAgQCAQMMCQ4DBAUHCAUDEQIDDggFDAMBAwkJBgQDBgEOBAoEAQIFAgIGCgQHBwcBCQUIBwgDAgcDAgQCBgIEBQoDAw4CBQICBQQHAgEKCA8CAwMHAwIOAwIDBAYEBgQEAQEtTwQBCAQDBAYPCgIGBAUEBQ4JFAsCAQYaAgEXBQQGAwUUAwMQBQIBBAgFCAQBCxgNBQwCAgQEDAgOBA4BCgsUBwgBBQMNAgECARIDCgQECQUGAgMKAwIDBQwCEAgSAwMEBAYCBAoHDgEFAgQBBAICEAUPBQIFAwILAggEBAICBBgOCQ4FCQEEBgECAwIBBAMGBwYFAg8KAQQBAgMBAgMIBRcEAggIAwUOAgoKBQECAwQLCQUCAgICBgIKBgoEBAQDAQQKBAYBBwIBBwYFBAIDAQUEAv4NFVUCAgUEBgIPAQECAQIBAQMCCgMGAgIFBgcDDgYCAQUEAggBAggCAgICBRwIEQkOCQwCBBAHAAH////5BDADCwAbAB9AHBkSCgMAAgFHAAECAW8AAgACbwAAAGYjKTIDBRcrJRQGByEiJjc0NjcmNTQ2MzIWFzYzMhYVFAceAQQvfFr9oWeUAVBAAah2WI4iJzY7VBdIXs9ZfAGSaEp6HhAIdqhiUCNUOyojEXQAAAH//v9qAfgDCwAgACpAJxkBAwIcCgIBAwJHAAIDAm8AAwEDbwABAAFvAAAADQBJGDY2FAQFGCsBFgcBBiMnLgE3EwcGIyInJjcTPgE7ATIWFRQHAzc2MzIB7goG/tIHEAgJCgJu4gIFCgcKA3ACDgi3Cw4CYN0FAgsCFgsN/XoOAQMQCAHDOAEHCA0BzQgKDgoEBv7+NgIABQAA/7ED6AMLAA8AHwAvAD8ATwBVQFJJAQcJOQEFBykBAwUZAQEDQTEhEQkBBgABBUcACQcJbwAHBQdvAAUDBW8AAwEAA1QAAQAAAVQAAQEAWAgGBAIEAAEATE1LJiYmJiYmJiYjCgUdKzcVFAYrASImPQE0NjsBMhY3FRQGKwEiJj0BNDY7ATIWNxEUBisBIiY1ETQ2OwEyFjcRFAYrASImNRE0NjsBMhYTERQGKwEiJjURNDY7ATIWjwoIawgKCghrCArWCghrCAoKCGsICtYKB2wHCgoHbAcK1woIawgKCghrCArWCghrCAoKCGsICi5rCAoKCGsICgpAswgKCgizCAoKh/6+CAoKCAFCCAoKzv3oCAoKCAIYCAoKARb8yggKCggDNggKCgAAAQAAAAACPAHtAA4AF0AUAAEAAQFHAAEAAW8AAABmNRQCBRYrARQPAQYiLwEmNDYzITIWAjsK+gscC/oLFg4B9A4WAckOC/oLC/oLHBYWAAAB//8AAAI7AckADgARQA4AAQABbwAAAGYVMgIFFislFAYnISIuAT8BNjIfARYCOxQP/gwPFAIM+goeCvoKqw4WARQeC/oKCvoLAAAAAQAAAAABZwJ8AA0AF0AUAAEAAQFHAAEAAW8AAABmFxMCBRYrAREUBiIvASY0PwE2MhYBZRQgCfoKCvoLHBgCWP4MDhYL+gscC/oLFgAAAAABAAAAAAFBAn0ADgAKtwAAAGYUAQUVKwEUDwEGIiY1ETQ+AR8BFgFBCvoLHBYWHAv6CgFeDgv6CxYOAfQPFAIM+goAAAEAAP/nA7YCKQAUABlAFg0BAAEBRwIBAQABbwAAAGYUFxIDBRcrCQEGIicBJjQ/ATYyFwkBNjIfARYUA6v+YgoeCv5iCwtdCh4KASgBKAscDFwLAY/+YwsLAZ0LHgpcCwv+2AEoCwtcCxwAAAEAAP/AAnQDRAAUAC21CQEAAQFHS7AhUFhACwAAAQBwAAEBDAFJG0AJAAEAAW8AAABmWbQcEgIFFisJAQYiLwEmNDcJASY0PwE2MhcBFhQCav5iCxwLXQsLASj+2AsLXQoeCgGeCgFp/mEKCl0LHAsBKQEoCxwLXQsL/mILHAAAAQAAAAADtgJGABQAGUAWBQEAAgFHAAIAAm8BAQAAZhcUEgMFFyslBwYiJwkBBiIvASY0NwE2MhcBFhQDq1wLHgr+2P7YCxwLXQsLAZ4LHAsBngtrXAoKASn+1woKXAseCgGeCgr+YgscAAAAAQAA/8ACmANEABQALbUBAQABAUdLsCFQWEALAAABAHAAAQEMAUkbQAkAAQABbwAAAGZZtBcXAgUWKwkCFhQPAQYiJwEmNDcBNjIfARYUAo7+1wEpCgpdCxwL/mILCwGeCh4KXQoCqv7Y/tcKHgpdCgoBnwoeCgGeCwtdCh4AAAABAAD/sQODAucAHgAgQB0QBwIAAwFHAAMAA28CAQABAG8AAQFmFxU1FAQFGCsBFA8BBiIvAREUBgcjIiY1EQcGIi8BJjQ3ATYyFwEWA4MVKRY7FKUoH0ceKqQUPBQqFRUBaxQ8FQFrFQE0HBYqFRWk/ncdJAEmHAGJpBUVKhU7FQFrFRX+lRYAAQAA/4gDNQLtAB4AJEAhAAMCA28AAAEAcAACAQECVAACAgFYAAECAUwWJSYUBAUYKwEUBwEGIi8BJjQ/ASEiJj0BNDYXIScmND8BNjIXARYDNRT+lRY6FSoWFqP+dx0kJB0BiaMWFioVOhYBaxQBOh4U/pQUFCoVPBWjKh5HHioBpRQ8FCoVFf6VFAABAAD/iANZAu0AHQAkQCEAAgMCbwABAAFwAAMAAANUAAMDAFgAAAMATCYXFiMEBRgrARUUBiMhFxYUDwEGIicBJjQ3ATYyHwEWFA8BITIWA1kkHf53pBUVKhU7Ff6UFBQBbBU6FioVFaQBiR0kAV5HHiqkFDwUKxQUAWwVOhYBaxUVKRY6FqQoAAAAAAEAAP/PA4MDCwAeACBAHRgPAgABAUcAAgECbwMBAQABbwAAAGYVNRcUBAUYKwEUBwEGIicBJjQ/ATYyHwERNDY3MzIWFRE3NjIfARYDgxX+lRY6Ff6VFRUpFjoVpCoeRx0qpRQ7FikVAYIeFP6UFRUBbBQ7FikVFaQBiR0qASwc/nekFRUpFgABAAD/sQNaAwsARQAyQC8+NTMiBAIDNCEgGxIREAIBCQACAkcEAQMCA28FAQIAAm8BAQAAZiY6Nxs6OQYFGisBBxc3NhYdARQGKwEiJyY/AScHFxYHBisBIiYnNTQ2HwE3JwcGIyInJj0BNDY7ATIXFg8BFzcnJjc2OwEyFgcVFAcGIyInAszGxlARLBQQ+hcJChFRxsZQEQkKF/oPFAEsEVDGxlALDgcHFhYO+hcKCRFQxsZREQoJF/oPFgEWBwcOCwIkxsZQEhMY+g4WFxURUcbGUREVFxYO+hgTElDGxlALAwkY+g4WFxURUcbGUREVFxYO+hgJAwsAAAACAAD/sQNaAwsAGAAwADFALigfGQMCBBIMAwMAAQJHAAQCBG8AAgMCbwADAQNvAAEAAW8AAABmOhQXGjcFBRkrARQPARcWFAYHIyImJzU0PgEfATc2Mh8BFgEVFA4BLwEHBiIvASY0PwEnJjQ2NzMyFgGlBblQChQP+g8UARYcC1C6BQ4GQAUBtBQgCVC5Bg4GQAUFulEKFA/6DxYBBQcGuVEKHhQBFg76DxQCDFC5BgY/BgHb+g8UAgxQuQYGQAUOBrlRCh4UARYAAAACAAD/uQNSAwMAFwAwADBALSokGwMCAw8GAgABAkcABAMEbwADAgNvAAIBAm8AAQABbwAAAGYUFTk6GAUFGSsBFRQGJi8BBwYiLwEmND8BJyY0NjsBMhYBFA8BFxYUBisBIiY3NTQ2Fh8BNzYyHwEWAa0WHAtRuQUQBEAGBrlQCxYO+g4WAaUGuVALFg76DhYBFB4KUbkGDgY/BgE6+g4WAglRugUFQAYOBrlQCxwWFgFpBwW6UAscFhYO+g4WAglQuQUFQAUAAAEAAP9qA+gDUgBEAFBATQsBCQoHCgkHbQ0BBwgKBwhrBgEAAQIBAAJtBAECAwECA2sMAQgFAQEACAFeAAoKDEgAAwMNA0lBQD08Ozk0My4sExcTESUVIRMUDgUdKwEUDwEGIiY9ASMVMzIWFA8BBiIvASY0NjsBNSMVFAYiLwEmND8BNjIWHQEzNSMiJjQ/ATYyHwEWFAYrARUzNTQ2Mh8BFgPoC44LHhTXSA4WC48KHgqPCxYOSNcUHgqPCwuPCh4U10gOFguPCxwLjwsWDkjXFB4LjgsBXg4LjwsWDkjXFB4KjwsLjwoeFNdIDhYLjwscC48LFg5I1xQeC44LC44LHhTXSA4WC48KAAABAAAAAAPoAhEAIAAoQCUFAQMEA28CAQABAHAABAEBBFIABAQBVgABBAFKExMXExMUBgUaKwEUDwEGIiY9ASEVFAYiLwEmND8BNjIWHQEhNTQ2Mh8BFgPoC44LHhT9xBQeCo8LC48KHhQCPBQeC44LAV4OC48LFg5ISA4WC48LHAuPCxYOSEgOFguPCgAAAAABAAD/agGKA1IAIAAoQCUEAQAFAQUAAW0DAQECBQECawAFBQxIAAICDQJJFSElFSETBgUaKwEUBicjETMyHgEPAQYiLwEmNDY7AREjIiY2PwE2Mh8BFgGJFg5HRw8UAgyPCh4KjwoUD0hIDhYCCY8LHAuPCwKfDhYB/cQUHgqPCwuPCh4UAjwUHguOCwuOCwAD////agOhAw0AIwAsAEUAXUBaHxgCAwQTEgEDAAMNBgIBAEMBBwEyAQkHBUcABAYDBgQDbQABAAcAAQdtAAoABgQKBmAFAQMCAQABAwBgAAcACQgHCWAACAgNCEk9PDUzFBMVFCMmFCMjCwUdKwEVFAYnIxUUBicjIiY3NSMiJic1NDY7ATU0NjsBMhYXFTMyFhc0LgEGFBY+AQEUBiIvAQYjIi4CPgQeAhcUBxcWAjsKB30MBiQHDAF9BwoBDAZ9CggkBwoBfQcKSJLQkpLQkgEeKjwUv2R7UJJoQAI8bI6kjmw8AUW/FQGUJAcMAX0HDAEKCH0KCCQHCn0ICgoIfQoZZ5IClsqYBoz+mh0qFb9FPmqQoo5uOgRCZpZNe2S/FQAAA////7ADWQMQAAkAEgAjACpAJwsDAgMAAQFHAAMAAQADAWAAAAICAFQAAAACWAACAAJMFxkmJAQFGCsBNCcBFjMyPgIFASYjIg4BBxQlFA4CLgM+BB4CAtww/ltMWj5wUDL90gGlS1xTjFABAtxEcqCsonBGAkJ0nrCcdkABYFpK/lwyMlByaQGlMlCOUltbWKByRgJCdpy0mng+BkpspgAAAAAD////agOhAw0ADwAYADEAO0A4CQgBAwABLwEDAB4BBQMDRwAGAAIBBgJgAAEAAAMBAGAAAwAFBAMFYAAEBA0ESRcjFBMVJiMHBRsrARUUBichIiYnNTQ2MyEyFhc0LgEGFBY+AQEUBiIvAQYjIi4CPgQeAhcUBxcWAjsKB/6+BwoBDAYBQgcKSJLQkpLQkgEeKjwUv2R7UJJoQAI8bI6kjmw8AUW/FQGUJAcMAQoIJAcKChlnkgKWypgGjP6aHSoVv0U+apCijm46BEJmlk17ZL8VAAMAAP+wAj4DDAAQACcAWwBWQFMFAAIAAU1JRTYyLgYFBAJHAAABBAEABG0ABAUBBAVrBwEFBgEFBmsABgZuAAgAAwIIA2AAAgEBAlQAAgIBWAABAgFMWFdBQD49OzoaFyQUEgkFGSsBFAYiJjc0JiMiJj4BMzIeARc0LgIiDgIHFB8CFhczNjc+ATc2NxQHDgIHFhUUBxYVFAcWFRQGIw4CJiciJjc0NyY1NDcmNTQ3LgInJjU0PgMeAgGbDAwOAjwdBwwCCAkcNixYJj5MTEw+JgEmERFIB38IRwYWBiZHORkiIAMaDQ0ZCCQZCy4yMAkaJAEHGQ4OGgIiIBk6MlBoaGhONgIRCAoKCBkcChAKEiodKEQuGBguRCg5LBITVVFRVQYaBSw5Vz8bKkIbDx8UDw8VHRANDRocGRwCIBccGg0NEB0VDw8UHw8cQCwaP1c3YD4kAig6ZAAAAAP//f+xA18DCwAUACEALgBAQD0OAQECCQECAAECRwACAwEDAgFtAAYAAwIGA2AAAQAABAEAYAAEBQUEVAAEBAVYAAUEBUwVFhUWIyYjBwUbKwEVFAYrASImPQE0NjsBNTQ2OwEyFhc0LgEOAx4CPgE3FA4BIi4CPgEyHgEB9AoIsggKCgh9CgckCAroUoqmjFACVIiqhlZ7csboyG4Gerz0un4CIvoHCgoHJAgKxAgKCsxTilQCUI6ijlACVIpTdcR0dMTqxHR0xAAAAAQAAP/RA6EC6wATAC4ASwBsAEpARycKAgMENwEFAFQBBwUDR2gBAkUAAgYCbwAGAQZvAAEEAW8ABAMEbwADAANvAAAFAG8ABQcFbwAHB2ZSUEdGKC8XEhYmCAUaKwERFAYmLwEjIiYnNTQ2NzM3NjIWExQGBwYjIiY3ND4DLgQ3NDYXMhceARcUBgcGIyImNzQ3Njc+ATQmJyYnJjU0NjMyFx4BFxQGBwYjIiYnND8BNjc+AS4BJyYnLgEnJjU0NjcyFx4BAa0WHAu6kg8UARYOkroKHhTXMCcFCQ4WAQwWEBAECBgOFAQUDwkFJzCPYE0HBw8WARUgCykuLikLIBUUDwgHTl6QjnYHBw8UARYZGRVETgJKRhUZBBIDFhYOBwd2jgKO/aAOFgIJuhYO1g8UAboKFP7BKkoPAxQQDBAMDB4gIAgSCBAPFgEDD0oqVZIgAxYOFgsQCR5aaFoeCRALFg4WAyGQVoDYMgMWDhQNDA4OM5iqmDMPDQMGAw0UDxQBAzPWAAAAAgAAAAACgwKxABMALgAqQCcnCgIDBAFHAAIBAm8AAQQBbwAEAwRvAAMAA28AAABmLxcSFiYFBRkrAREUBiYvASMiJic1NDY3Mzc2MhYTFAYHBiMiJjc0PgMuBDc0NhcyFx4BAa0WHAu6kg8UARYOkroKHhTXMCcFCQ4WAQwWEBAECBgOFAQUDwkFJzACjv2gDhYCCboWDtYPFAG6ChT+wSpKDwMUEAwQDAweICAIEggQDxYBAw9KAAEAAAAAAa0CsQATAB1AGgoBAAEBRwACAQJvAAEAAW8AAABmEhYmAwUXKwERFAYmLwEjIiYnNTQ2NzM3NjIWAa0WHAu6kg8UARYOkroKHhQCjv2gDhYCCboWDtYPFAG6ChQAAAADAAD/sQMLA1MACwBDAEsAjkAURR8TDQEFAAYUAQEANDIjAwIBA0dLsAlQWEArAAYHAAcGAG0AAAEHAAFrAAECAgFjAAUCAwIFA20EAQIAAwIDXQAHBwwHSRtALAAGBwAHBgBtAAABBwABawABAgcBAmsABQIDAgUDbQQBAgADAgNdAAcHDAdJWUATSkg/Pjc2MS8sKSYkFxUSEAgFFCsTByY9ATQ+ARYdARQBBxUUBgciJwcWMzI2JzU0PgEWBxUUBgcVMzIWDgEjISImPgE7ATUmJwcGIi8BJjQ3ATYyHwEWFCcBETQ2FzIWlzgYFhwWAnbKaEofHjU2PGeUARYcFgGkeY4PFgISEf6bDhYCEhCPRj2OBRAELgYGArEFDgYuBtr+pWpJOVwBQzk6PkcPFAIYDUceAS/KR0poAQs2HJJoRw8UAhgNR3y2DUoWHBYWHBZKByaOBgYuBRAEArEGBi4FEEX+pgEdSmoBQgAAAAL///+xAoMDUwAnADMAXUALHAEEBRMEAgADAkdLsAlQWEAcAAQFAwUEA20AAwAAA2MCAQAAAQABXQAFBQwFSRtAHQAEBQMFBANtAAMABQMAawIBAAABAAFdAAUFDAVJWUAJFRsdIzMlBgUaKwEVFAYHFTMyHgEGIyEiLgE2OwE1LgE3NTQ+ARYHFRQWPgEnNTQ+ARYnERQOASYnETQ2HgECg6R6jw8UAhgN/psPFAIYDY95pgEWHBYBlMyWAhYcFo9olmYBaJRqAclHfLYNShYcFhYcFkoNtnxHDxQCGA1HZ5QCkGlHDxQCGMn+40poAmxIAR1KagJmAAAAAAIAAP/5A1kCxAAYAEAAUEBNDAEBAgFHIQEAAUYAAwcGBwMGbQACBgEGAgFtAAEFBgEFawAABQQFAARtAAcABgIHBmAABQAEBVQABQUEWAAEBQRMLCUqJxMWIxQIBRwrARQHAQYiJj0BIyImJzU0NjczNTQ2FhcBFjcRFAYrASImNycmPwE+ARczMjYnETQmByMiNCY2LwEmPwE+ARczMhYClQv+0QseFPoPFAEWDvoUHgsBLwvEXkOyBwwBAQEBAgEICLIlNgE0JrQGCgICAQEBAgEICLJDXgFeDgv+0AoUD6EWDtYPFAGhDhYCCf7QCrX+eENeCggLCQYNBwgBNiQBiCU2AQQCCAQLCQYNBwgBXgAAAAIAAP/5A2sCwwAnAEAAQkA/FAECAQFHAAYCBQIGBW0ABQMCBQNrAAQDAAMEAG0AAQACBgECYAADBAADVAADAwBYAAADAEwWIxklKiUnBwUbKyUUFg8BDgEHIyImNRE0NjsBMhYVFxYPAQ4BJyMiBgcRFBYXMzIeAgEUBwEGIiY9ASMiJj0BNDY3MzU0NhYXARYBZQIBAgEICLJDXl5DsggKAQEBAgEICLIlNAE2JLQGAgYCAgYL/tELHBb6DhYWDvoWHAsBLwsuAhIFDgkEAV5DAYhDXgoICwkGDQcIATQm/nglNAEEAggBLA4L/tAKFA+hFg7WDxQBoQ4WAgn+0AoAAAAABAAA/2oDoQNSAAMAEwAjAEcAgUAMFQUCBwIdDQIDBwJHS7AKUFhAKQsJAgcCAwMHZQUBAwABAAMBXwQBAgIIWAoBCAgMSAAAAAZYAAYGDQZJG0AqCwkCBwIDAgcDbQUBAwABAAMBXwQBAgIIWAoBCAgMSAAAAAZYAAYGDQZJWUASRkRBPjs6MyU2JiYmJBEQDAUdKxchESE3NTQmKwEiBh0BFBY7ATI2JTU0JisBIgYdARQWOwEyNjcRFAYjISImNRE0NjsBNTQ2OwEyFh0BMzU0NjsBMhYHFTMyFkcDEvzu1woIJAgKCggkCAoBrAoIIwgKCggjCArXLBz87h0qKh1INCUkJTTWNiQjJTYBRx0qTwI8a6EICgoIoQgKCgihCAoKCKEICgos/TUdKiodAssdKjYlNDQlNjYlNDQlNioAAAAADwAA/2oDoQNSAAMABwALAA8AEwAXABsAHwAjADMANwA7AD8ATwBzAJhAlUElAh0SSS0kAxMdAkchHwIdEwkdVBsBExkXDQMJCBMJXxgWDAMIFREHAwUECAVeFBAGAwQPCwMDAQAEAV4aARISHlggAR4eDEgOCgIDAAAcWAAcHA0cSXJwbWpnZmNgXVtWU01MRUQ/Pj08Ozo5ODc2NTQxLyknIyIhIB8eHRwbGhkYFxYVFBMSEREREREREREQIgUdKxczNSMXMzUjJzM1IxczNSMnMzUjATM1IyczNSMBMzUjJzM1IwM1NCYnIyIGBxUUFjczMjYBMzUjJzM1IxczNSM3NTQmJyMiBhcVFBY3MzI2NxEUBiMhIiY1ETQ2OwE1NDY7ATIWHQEzNTQ2OwEyFgcVMzIWR6GhxbKyxaGhxbKyxaGhAZuzs9aysgGsoaHWs7PEDAYkBwoBDAYkBwoBm6Gh1rOz1qGhEgoIIwcMAQoIIwgK1ywc/O4dKiodSDQlJCU01jYkIyU2AUcdKk+hoaEksrKyJKH9xKH6of3EoSSyATChBwoBDAahBwwBCv4msiShoaFroQcKAQwGoQcMAQos/TUdKiodAssdKjYlNDQlNjYlNDQlNioAAAADAAD/dgOgAwsACAAUAC4AWUAQJgEEAygnEgMCBAABAQADR0uwJlBYQBoAAwQDbwAEAgRvAAIAAm8AAAEAbwABAQ0BSRtAGAADBANvAAQCBG8AAgACbwAAAQBvAAEBZlm3HCMtGBIFBRkrNzQmDgIeATYlAQYiLwEmNDcBHgElFAcOASciJjQ2NzIWFxYUDwEVFzY/ATYyFtYUHhQCGBoYAWb+gxU6FjsVFQF8FlQBmQ0bgk9okpJoIEYZCQmjbAIqSyEPCh0OFgISIBIEGvb+gxQUPRQ7FgF8N1TdFiVLXgGS0JACFBAGEgdefTwCGS0UCgAACQAA/7EDWQLEAAMAEwAXABsAHwAvAD8AQwBHAJ9AnCsBCwY7AQ0EAkcaERUDBxABBgsHBl4XAQoACwwKC2AZDxQDBQ4BBA0FBF4YAQwADQIMDWATAQIBAwJUFgkSAwEIAQADAQBeEwECAgNYAAMCA0xEREBAMTAhIBwcGBgUFAUEAABER0RHRkVAQ0BDQkE5NjA/MT8pJiAvIS8cHxwfHh0YGxgbGhkUFxQXFhUNCgQTBRMAAwADERsFFSs3FSM1JTIWHQEUBisBIiY9ATQ2PwEVITUTFSM1ARUhNQMyFgcVFAYHIyImJzU0NhcBMhYHFRQGByMiJic1NDYXBRUjNRMVITXExAGJDhYWDo8OFhYO6P4efX0DWf5lfQ8WARQQjg8UARYOAfQOFgEUD48PFAEWDgFBfX3+HkBHR0gWDo8OFhYOjw8UAdZHRwEeSEj9xEdHAoMUEI4PFAEWDo4PFgH+4hQPjw8UARYOjw4WAUdHRwEeSEgAAAYAAP9yBC8DSQAIABIAGwB6ALYA8QCcQJnu2QIEDmpdAgUI0LxwAwAFvqygdVJMRSMdCQEAs55AAwIBOi0CBgKVgAILAwdH59sCDkWCAQtECgEICQUJCAVtAAYCBwIGB20ADgAECQ4EYAAJCAAJVAAFDQEAAQUAYAACBgECVAwBAQAHAwEHYAADCwsDVAADAwtYAAsDC0zl48fGqqiLim1sZGJaWTQyKyoTFBQUExIPBRorATQmIgYUFjI2BTQmDgEXFBYyNgM0JiIGHgEyNgcVFAYPAQYHFhcWFAcOASIvAQYHBgcGKwEiJjUnJicHBiInJjU0Nz4BNyYvAS4BPQE0Nj8BNjcmJyY0Nz4BMzIfATY3Njc2OwEyFh8BFhc3NjIXFhUUDwEGBxYfAR4BARUUBwYHFhUUBwYjIi8BBiInDgEHIicmNTQ3JicmPQE0NzY3JjU0PwE2MzIWFzcXNj8BMhcWFRQHFhcWERUUBwYHFhUUBwYjIiYnBiInDgEiJyY1NDcmJyY9ATQ3NjcmNTQ/ATYzMhYXNxc2PwEyFxYVFAcWFxYB9FR2VFR2VAGtLDgsASo6LAEsOCwBKjos2AgEVwYMEx8EBAxEEAVAFRYGBwQNaAYKDRMXQgQNBlAEBSQIDQdVBQgIBVYHCxMfBAQMRAoGBkATGAYHAw1oBgoBDRMXQQUNBVEEGBEIDQZVBgYBZlMGChwCRAEFFR0LDAsHLAMBRAMdCgdTUwcKHQM0EAEEKggRERwXBAJDAhwJB1NTBgocAkQBBSoICwwLBywERAMdCgdTUwcKHQM0EAEEKggRERwXBAJDAhwJB1MBXjtUVHZUVOMdLAIoHx0qKgJZHSoqOyoqzWcGCgEOExcbJQYMBBFCBDILBjwbDQgGVQYMMgQESw8FBQgsDBgWDQEIB2gFCgEOExcbJQYMBRBCBDIKCDwaDQgGVQYLMQQESw8EBh4VDRsTDAII/s9OCQgPDj8OAgIoGyUBAQs0ASgCAg4/Dg8ICU4JCRANPw4CAh4JNAwBASgXAScCAg4/DRAJAjNOCQkPDj8OAgInNAwBAQw0JwICDj8ODwkJTgkIEA0/DgICHgk0CwEBJxcBJwICDj8NEAgAAAIAAP+xA1oDCwAIAGoARUBCZVlMQQQABDsKAgEANCgbEAQDAQNHAAUEBW8GAQQABG8AAAEAbwABAwFvAAMCA28AAgJmXFtTUUlIKyoiIBMSBwUWKwE0JiIOARYyNiUVFAYPAQYHFhcWFAcOASciLwEGBwYHBisBIiY1JyYnBwYiJyYnJjQ3PgE3Ji8BLgEnNTQ2PwE2NyYnJjQ3PgEzMh8BNjc2NzY7ATIWHwEWFzc2MhcWFxYUBw4BBxYfAR4BAjtSeFICVnRWARwIB2gKCxMoBgUPUA0HB00ZGgkHBBB8CAwQGxdPBhAGRhYEBQgoCg8IZgcIAQoFaAgOFyUGBQ9QDQcITRgaCQgDEXwHDAEPHBdPBQ8HSBQEBAkoCg8IZgcKAV47VFR2VFR4fAcMARAeFRsyBg4GFVABBTwNCEwcEAoHZwkMPAUGQB4FDgYMMg8cGw8BDAd8BwwBEBkaIC0HDAcUUAU8DQhMHBAKB2cJCzsFBUMcBQ4GDDIPHBoQAQwAAAAB////+QMSAwsATgAjQCAyAQIBAAEAAgJHAAECAW8AAgACbwAAAGZCQCEgJgMFFSslFAYHBgcGIyImLwImJy4BJyYvAS4BLwEmNzQ3Njc+ATMyFxYfAR4BFx4CFRQOAgcUHwEeATUeARcyFh8BFjcyPgIXMh4BHwEWFxYDEgwGCzk0Mw8eERo7NitHmisbEwoICAQHAwEdHxwOMA8IBAoUEAoUBwIQCCAmHgEDBAEOKm5MARIFCwYHCh4eIAwHEBgCYCcDAp4PMA4cIBwEBQgVFBssmEgrNhwXEBIgDg80NDkLBgwCAycfFB4PAhgQCAsgHh4KBQgLAxYBTW4qDAIFAwEgJCIBCBACNhMKBAAAAAgAAP9qA1kDUgATABoAIwBZAF4AbAB3AH4AdEBxFAECBGxqAgMCdGFWSQQGA28mAgoGfjQCCwpcAQgHBkcACAcFBwgFbQkBAgADBgIDYAAGAAoLBgpgAAsABwgLB2AABAQBWAABAQxIDAEFBQBYAAAADQBJGxt8e3p5UE04NzIwKScbIxsjEyYUNTYNBRkrAR4BFREUBgchIiYnETQ2NyEyFhcHFTMmLwEmExEjIiYnNSERARYXNjMyFxYHFCMHBiMiJicGBwYjIi8CJjc+ATc2FxYVNjc2Ny4BNzY7ATIXFgcGBxUGBxYBNjcOARMGFzY3NDc2NyImNTQnAzY3Ii8BJicGBwYFJiMWMzI3AzMQFh4X/RIXHgEgFgH0FjYPStIFB68GxugXHgH+UwGsEh0hIFIRCQgBAQMkG0oke2BVMggHDgMGAgU2LggFAR0fJhQNCAgGEQwNBwoFAQEBBx/+8h0vHSjXCQcBAwQBAgEBB0ZMUwEGCSscDx8RAWANQSobCAICfhA0GP1+Fx4BIBYDfBceARYQJtIRBq8H/LACPCAV6fymAUsOEQQbDRABAhUWEg0hkgQHAgYOFzgaBQgBAS8/TEYuVhwWCAwaAwEWRCdb/vENSxYyAfEXMgQUAhYDAgIBDAj+jR4PBQglPTA+HwYNEAEAAAQAAP9qA1kDUgATABoAIwBTALNACxQBAgRMPgIHBgJHS7ASUFhAORAODAMKAwYDCmUNCwkDBgcDBgdrCAEHBQUHYwACAAMKAgNgAAQEAVgAAQEMSA8BBQUAWQAAAA0ASRtAOxAODAMKAwYDCgZtDQsJAwYHAwYHawgBBwUDBwVrAAIAAwoCA2AABAQBWAABAQxIDwEFBQBZAAAADQBJWUAkJCQbGyRTJFNSUUdGOjk4NzY1NDMoJyYlGyMbIxMmFDU2EQUZKwEeARURFAYHISImJxE0NjchMhYXBxUzJi8BJhMRIyImJzUhERMVMxMzEzY3NjUzFx4BFxMzEzM1IxUzBwYPASM1NCY0JicDIwMHBg8BIycmLwEzNQMzEBYeF/0SFx4BIBYB9BY2D0rSBQevBsboFx4B/lM7J1xYSAQBAgIBAQICSFlbJ6cyNwMBAQMCAgJRP1ECAQECAgIBAjgyAn4QNBj9fhceASAWA3wXHgEWECbSEQavB/ywAjwgFen8pgH0O/6PAQ8LDgkFDgEUBP7xAXE7O/ULDgwEAgQEEgUBMP7QDQgEDAwOC/U7AAQAAP9qA1kDUgATABoAIwBTAMtACxQBAgRSOwIHCwJHS7ASUFhAQg8BDAMLAwxlEA4NAwsHAwsHaxMRCggEBwYDBwZrCQEGBQUGYwACAAMMAgNgAAQEAVgAAQEMSBIBBQUAWQAAAA0ASRtARA8BDAMLAwwLbRAODQMLBwMLB2sTEQoIBAcGAwcGawkBBgUDBgVrAAIAAwwCA2AABAQBWAABAQxIEgEFBQBZAAAADQBJWUAqJCQbGyRTJFNRUE9OTUxBQD8+PTw6OTg3NjUoJyYlGyMbIxMmFDU2FAUZKwEeARURFAYHISImJxE0NjchMhYXBxUzJi8BJhMRIyImJzUhETcVMzUjNz4CBzMUHwEeAR8BIxUzNSMnNzM1IxUzBw4BDwEjNCcmLwEzNSMVMxcHAzMQFh4X/RIXHgEgFgH0FjYPStIFB68GxugXHgH+U6idKjoDBAYBAQMCAQQCPCujJmtsJpwpOQIIAQEBAwMGOyqiJmptAn4QNBj9fhceASAWA3wXHgEWECbSEQavB/ywAjwgFen8poM7O1oECgYBAgQEAgQDWjs7mJ47O1kECgMBAgMGB1k7O5ieAAYAAP9qA1kDUgATABoAIwAzAEMAUwByQG8UAQIELCQCBwZAOAIICVBIAgoLBEcAAgADBgIDYAAGAAcJBgdgDQEJAAgLCQhgDgELAAoFCwpgAAQEAVgAAQEMSAwBBQUAWAAAAA0ASURENDQbG0RTRFJMSjRDNEI8OjAuKCYbIxsjEyYUNTYPBRkrAR4BFREUBgchIiYnETQ2NyEyFhcHFTMmLwEmExEjIiYnNSEREzQ2MyEyFh0BFAYjISImNQUyFh0BFAYjISImPQE0NjMFMhYdARQGIyEiJj0BNDYzAzMQFh4X/RIXHgEgFgH0FjYPStIFB68GxugXHgH+U48KCAGJCAoKCP53CAoBmwgKCgj+dwgKCggBiQgKCgj+dwgKCggCfhA0GP1+Fx4BIBYDfBceARYQJtIRBq8H/LACPCAV6fymAeMHCgoHJAgKCghZCggkCAoKCCQICo8KCCQICgoIJAgKAAAAAAYAAP+xAxIDCwAPAB8ALwA7AEMAZwBkQGFXRQIGCCkhGREJAQYAAQJHBQMCAQYABgEAbQQCAgAHBgAHawAOAAkIDglgDw0CCAwKAgYBCAZeAAcLCwdUAAcHC1gACwcLTGVkYV5bWVNST0xJR0E/FCQUJiYmJiYjEAUdKwERFAYrASImNRE0NjsBMhYXERQGKwEiJjURNDY7ATIWFxEUBisBIiY1ETQ2OwEyFhMRIREUHgEzITI+AQEzJyYnIwYHBRUUBisBERQGIyEiJicRIyImPQE0NjsBNz4BNzMyFh8BMzIWAR4KCCQICgoIJAgKjwoIJAgKCggkCAqOCgckCAoKCCQHCkj+DAgIAgHQAggI/on6GwQFsQYEAesKCDY0Jf4wJTQBNQgKCgisJwksFrIXKgknrQgKAbf+vwgKCggBQQgKCgj+vwgKCggBQQgKCgj+vwgKCggBQQgKCv5kAhH97wwUCgoUAmVBBQEBBVMkCAr97y5EQi4CEwoIJAgKXRUcAR4UXQoAAgAA/2oD6ALDABcAPQA3QDQ0CAIBACYLAgMCAkcABAUBAAEEAGAAAQACAwECYAADAw0DSQEAOzokIh0bEhAAFwEXBgUUKwEiDgEHFBYfAQcGBzY/ARcWMzI+Ai4BARQOASMiJwYHBgcjIiYnNSY2Jj8BNj8BPgI/AS4BJzQ+ASAeAQH0csZ0AVBJMA8NGlVFGCAmInLGdAJ4wgGAhuaIJypukxskAwgOAgIEAgMMBA0UBxQQBw9YZAGG5gEQ5oYCfE6ETD5yKRw1My4kPBUDBU6EmIRO/uJhpGAEYSYIBAwJAQIIBAMPBQ4WCBwcEyoyklRhpGBgpAABAAD/aQPoAsMAJgAcQBkbAQABAUcNAQBEAAEAAW8AAABmJCIjAgUVKwEUDgEjIicGBwYHBiYnNSY2Jj8BNj8BPgI/AS4BJzQ+AjMyHgED6IbmiCcqbpMbJAoOAwIEAgMMBA0UBxQQBw9YZAFQhLxkiOaGAV5hpGAEYSYIBAEMCgECCAQDDwUOFggcHBMqMpJUSYRgOGCkAAIAAP+wA+gCwwAlAEsAP0A8SRwCAAE/AQMAKQECAwNHCgEDAUYyAQJEAAEAAW8AAAMAbwADAgIDVAADAwJYAAIDAkxCQD48IyIjBAUVKwEUDgEjIicGBwYHIyImNSY0NjU/AjYHNz4CNy4BJzQ+ATIeARcUBgceAR8BFh8DFAcOAScmJyYnBiMiJxYzMjY3PgEnNCceAQMSarRrMDJGVRUbAgYMAQIBBAMDARwFDg4ERU4BarTWtGrWUEQFDAgbCQQFBAMBAgoHHBRWRjIwl3AgEVqkQkVMAQ1IVAGlTYRMCTEXBQQKBwEEBAEDBgMDAR4FGBIQKHRDToRMTITcQ3YnDhYKIQsDBQYKAQIICgEEBRcxCUoDMi80hkorKid4AAMAAP+wA+gCwwAVADsAYABWQFNcDAgDAQA1CQIDAVIBBQMDRyMBBQFGRQEERAcBAgYBAAECAGAAAQADBQEDYAAFBAQFVAAFBQRYAAQFBEwXFgEAVVNRTx4cFjsXOxAOABUBFQgFFCsBIg4BBxQWHwEHNj8BFxYzMj4BNC4BJzIeAg4BJyInBgcGByMiJjUmNDY1PwI2Bzc+AjcuASc0PgEBHgEfARYfAxQHDgEnJicmJwYjIicWMzI2Nz4BJzQnHgEUBgGJVZZWATw1NhMTDxkeKypVllZWllVqtmgCbLJsMDJGVRUbAgYMAQIBBAMDARwFDg4ERU4BarQCNgUMCBsJBAUEAwECCgccFFZGMjCXcCARWqRCRUwBDUhUUAJ8OmQ5LVYeIC4LChIGCDpkcGY4SEyEnIJOAQkxFwUECgcBBAQBAwYDAwEeBRgSECh0Q06ETP10DhYKIQsDBQYKAQIICgEEBRcxCUoDMi80hkorKid4h3YAAAADAAD/agPEA1MADAAaAEIAf0AMAAECAAFHKBsCAwFGS7AOUFhAKwcBBQEAAQVlAAACAQBjAAMAAQUDAWAABAQIWAAICAxIAAICBlgABgYNBkkbQCwHAQUBAAEFZQAAAgEAAmsAAwABBQMBYAAEBAhYAAgIDEgAAgIGWAAGBg0GSVlADB8iEigWESMTEgkFHSsFNCMiJjc0IhUUFjcyJSEmETQuAiIOAhUQBRQGKwEUBiImNSMiJjU+BDc0NjcmNTQ+ARYVFAceARcUHgMB/QkhMAESOigJ/owC1pUaNFJsUjQaAqYqHfpUdlT6HSocLjAkEgKEaQUgLCAFaoIBFiIwMGAIMCEJCSk6AamoASkcPDgiIjg8HP7XqB0qO1RUOyodGDJUXohNVJIQCgsXHgIiFQsKEJJUToZgUjQAAgAA/2oDxANTAAwANAA/QDwaDQIBBgABAgACRwABBgMGAQNtBQEDAAYDAGsAAAIGAAJrAAYGDEgAAgIEWAAEBA0ESR8iEiMjExIHBRsrBTQjIiY3NCIVFBY3MiUUBisBFAYiJjUjIiY1PgQ3NDY3JjU0PgEWFRQHHgEXFB4DAf0JITABEjooCQHHKh36VHZU+h0qHC4wJBIChGkFICwgBWqCARYiMDBgCDAhCQkpOgGpHSo7VFQ7Kh0YMlReiE1UkhAKCxceAiIVCwoQklROhmBSNAACAAD/+QEwAwsADwAfACxAKRkREAMCAwFHAAMCA28AAgECbwABAAABVAABAQBYAAABAEw1JiYkBAUYKyUVFAYHIyImPQE0NhczMhYTAw4BJyMiJicDJjY7ATIWAR4WDo8OFhYOjw8UEhABFg6PDhYBDwEWDbMOFpp9DxQBFg59DhYBFAI+/lMOFgEUDwGtDhYWAAAABP///7EDoQMLAAMADAAVAD0AWUBWDQEBAhcBBgECRwADBAkEAwltCAEGAQABBgBtAAoABAMKBF4LAQkABQIJBWAAAgABBgIBXgAABwcAUgAAAAdYAAcAB0w8OjMwLSsTMykTEyERERAMBR0rFyE1ITUhNSMiJj0BIQE0LgEOARY+ATcVFAYHIxUUBiMhIiYnNSMiJjc1NDYXMxE0NjMhMhYfAR4BBxUzMhbWAfT+DAH0WRYg/psCgxQgEgIWHBhGDAZ9IBb96BYeAX0HDAFAKyQgFQF3FzYPVQ8YASMtPgeP1tYgFln+dw8UAhgaGAQQEegHCgFZFiAgFlkMBugsQAEBMBYgGA5VEDYWjz4AAAAFAAD/+QPkAwsABgAPADkAPgBIAQdAFUA+OxADAgEHAAQ0AQEAAkdBAQQBRkuwClBYQDAABwMEAwcEbQAABAEBAGUAAwAEAAMEYAgBAQAGBQEGXwAFAgIFVAAFBQJYAAIFAkwbS7ALUFhAKQAABAEBAGUHAQMABAADBGAIAQEABgUBBl8ABQICBVQABQUCWAACBQJMG0uwF1BYQDAABwMEAwcEbQAABAEBAGUAAwAEAAMEYAgBAQAGBQEGXwAFAgIFVAAFBQJYAAIFAkwbQDEABwMEAwcEbQAABAEEAAFtAAMABAADBGAIAQEABgUBBl8ABQICBVQABQUCWAACBQJMWVlZQBYAAERDPTwxLikmHhsWEwAGAAYUCQUVKyU3JwcVMxUBJg8BBhY/ATYTFRQGIyEiJjURNDY3ITIXHgEPAQYnJiMhIgYHERQWFyEyNj0BND8BNhYDFwEjNQEHJzc2Mh8BFhQB8EBVQDUBFQkJxAkSCcQJJF5D/jBDXl5DAdAjHgkDBxsICg0M/jAlNAE2JAHQJTQFJAgYN6H+iaECbzOhMxAsEFUQvUFVQR82AZIJCcQJEgnECf6+akNeXkMB0EJeAQ4EEwYcCAQDNCX+MCU0ATYkRgcFJAgIAY+g/omgAS40oTQPD1UQLAABAAD/sQPoAy8ALAAdQBoAAwEDbwABAAFvAAACAG8AAgJmKh0zFAQFGCsBFAcBBiImPQEjIg4FFRQXFBYHFAYiJy4CJyY1NDc2ITM1NDYWFwEWA+gL/uMLHBZ9N1ZWPjgiFAMEAQoRBgQIBgNHHloBjn0WHAsBHQsB7Q8K/uILFg6PBhIeMEBaOB8mBBIGCAwKBQ4UA59db0vhjw4WAgn+4gsAAAEAAP+xA+gDLgArAClAJiYBBAMBRwADBANvAAQBBG8AAQIBbwACAAJvAAAAZiMXEz0XBQUZKyUUBw4CBwYiJjU0Njc2NTQuBSsBFRQGIicBJjQ3ATYyFgcVMyAXFgPoRwEKBAUHEQoCAQMUIjg+VlY3fRQgCf7jCwsBHQscGAJ9AY5aHuFdnwQSEAQKDAgFFAMmHzhaQDAeEgaPDhYLAR4KHgoBHgoUD4/hSwACAAD/sQPoAzUAFAA6ACtAKCYAAgADIQEBAAJHEAEDRQADAANvAgEAAQBvAAEBZjg3LCodHCQEBRUrJRUUBwYjIicBJjQ3ATYWHQEHBhQXBRQOAg8BBiMiJyY3NicuAScVFAcGIyInASY0NwE2FxYdARYXFgFlFgcHDwr+4wsLAR0RLN0LCwNgEhocCAsFCwMCDgEYUyR2WxUIBg8K/uILCwEeEBcV5mle9icXCgMLAR4KHgoBHhETFyfeCxwL8yBURkYQFgoBBA/fXCgsB4wXCgMLAR4KHgoBHhEJCheTD2xgAAADAAD/+QPoAn0AEQAiADMARkBDCwICBAINAQADAkcABAIDAgQDbQADAAIDAGsAAAECAAFrAAYAAgQGAmAAAQUFAVQAAQEFWAAFAQVMFxYkFBUYFgcFGysBJicWFRQGLgE1NDcGBx4BIDYBNCYHIgYVFBYyNjU0NjMyNgUUBwYEICQnJjQ3NiwBBBcWA6FVgCKS0JIigFVL4AEE4v63EAtGZBAWEEQwCxAB2QtO/vj+2v74TgsLTgEIASYBCE4LATqEQTpDZ5QCkGlDOkGEcoiIAUkLEAFkRQsQEAswRBDMExOBmpqBEyYUgJoCnn4UAAACAAD/vQNNAwsACAAdACRAIQABAQABRwABAAFwAAIAAAJUAAICAFgAAAIATDgaEgMFFysTNCYOAR4CNgEUBwEGIicBLgE9ATQ2NzMyFhcBFvoqOiwCKD4mAlUU/u4WOxT+cRUeKh3pHUgVAY8UAlgeKgImQCQGMP7ZHhX+7hUVAY8VSB3oHSoBHhX+cRUAAAADAAD/vQQkAwsACAAdADQAMEAtJgACAQABRwAEAgRvAwEBAAFwBQECAAACVAUBAgIAWAAAAgBMIBkpOBoSBgUaKxM0Jg4BHgI2ARQHAQYiJwEuAT0BNDY3MzIWFwEWFxQHAQYjIiYnATY0JwEuASMzMhYXARb6KjosAig+JgJVFP7uFjsU/nEVHiod6R1IFQGPFNcV/u4WHRQaEAEGFRX+cRVIHX0dSBUBjxUCWB4qAiZAJAYw/tkeFf7uFRUBjxVIHegdKgEeFf5xFR0eFf7uFRARAQYVOxUBjxUeHhX+cRUAAAABAAD/+QKDA1MAIwA2QDMABAUABQQAbQIGAgABBQABawABAW4ABQUDWAADAwwFSQEAIB8bGBQTEA4JBgAjASMHBRQrATIWFxEUBgchIiYnETQ2FzM1NDYeAQcUBisBIiY1NCYiBhcVAk0XHgEgFv3pFx4BIBYRlMyWAhQPJA4WVHZUAQGlHhf+vhYeASAVAUIWIAGzZ5QCkGkOFhYOO1RUO7MAAAEAAP/5A6EDDAAlADBALQQBAgEAAQIAbQAAAwEAA2sAAwNuAAUBAQVUAAUFAVgAAQUBTBMlNSMVJAYFGisBFRQGByMiJj0BNCYOAQcVMzIWFxEUBgchIiYnETQ2FyE1ND4BFgOhFg4kDhZSeFIBNRceASAW/ekXHgEgFgF3ktCQAhGPDxQBFg6PO1QCUD1sHhf+vhYeASAVAUIWIAFsZ5IClgAAAgAA//kCgwMLAAcAHwAqQCcFAwIAAQIBAAJtAAICbgAEAQEEVAAEBAFYAAEEAUwjEyU2ExAGBRorEyE1NCYOARcFERQGByEiJicRNDYXMzU0NjIWBxUzMhazAR1UdlQBAdAgFv3pFx4BIBYRlMyWAhIXHgGlbDtUAlA9of6+Fh4BIBUBQhYgAWxmlJRmbB4AAgAA//kDkgLFABAAMQAuQCsuJiUYFQ8ODQgBAwwBAAECRwQBAwEDbwABAAFvAgEAAGYqKCMiIREUBQUXKwERFAYHIzUjFSMiJicRCQEWNwcGByMiJwkBBiYvASY2NwE2Mh8BNTQ2OwEyFh0BFxYUAxIWDtaP1g8UAQFBAUEBfCIFBwIHBf5+/n4HDQUjBAIFAZESMBOICghrCAp6BgEo/vUPFAHW1hYOAQ8BCP74ASQpBQEDAUL+vgQCBSkGDgUBTg8PcWwICgoI42YEEAAAAAIAAP/5AWYDCwAeAC4AP0A8HwEFBhoSAgIDCAACAAEDRwAGAAUDBgVgAAMAAgEDAmAEAQEAAAFUBAEBAQBYAAABAEw1JiMmIRYzBwUbKyUVFAYHISImJzU0NjczNSMiJic1NDY3MzIWFxEzMhYDFRQGByMiJj0BNDY7ATIWAWUUEP7jDxQBFg4jIw8UARYO1g8UASMPFkgWDo8OFhYOjw8UZEcPFAEWDkcPFAHWFg5HDxQBFg7+vxYCdWsPFAEWDmsOFhYAAAAAAgAA//kCOQLDAA8AOwBrtQABAAEBR0uwD1BYQCYABAMCAwRlAAIBAwIBawAFAAMEBQNgAAEAAAFUAAEBAFgAAAEATBtAJwAEAwIDBAJtAAIBAwIBawAFAAMEBQNgAAEAAAFUAAEBAFgAAAEATFlACScUKx4mJAYFGislFRQGByMiJj0BNDYXMzIWExQOAwcOARUUBgcjIiY9ATQ2Nz4BNCYnIgcGBwYjIi8BLgE3NjMyHgIBiQ4IhgkODgmGCQyxEBgmGhUXHg4JhggMSiohHDQiJBgUKAcKBwdbCAIEWaotWkgulYYJDAEOCIYJDgEMAUUeNCIgEgoNMA0KEAEWCRouUhMQIDIiARAOMgkERgYQCJQiOlYAAAL///9qA6EDDQAIACEAK0AoHwEBAA4BAwECRwAEAAABBABgAAEAAwIBA2AAAgINAkkXIxQTEgUFGSsBNC4BBhQWPgEBFAYiLwEGIyIuAj4EHgIXFAcXFgKDktCSktCSAR4sOhS/ZHtQkmhAAjxsjqSObDwBRb8VAYJnkgKWypgGjP6aHSoVv0U+apCijm46BEJmlk17ZL8VAAAAAAMAAP/DA+gDQAASADcAcQCjQBhrAQELDQEAASkCAgUGMQEEBVYnAgMEBUdLsBpQWEAuAAYABQAGBW0ABQQABQRrAAIDAnAKAQEHAQAGAQBgCQEECAEDAgQDYAALCwwLSRtANgALAQtvAAYABQAGBW0ABQQABQRrAAIDAnAKAQEHAQAGAQBgCQEEAwMEVAkBBAQDWAgBAwQDTFlAF25tamlbWFJQQkA9PDQzMC8zFTYYDAUYKwEGBycuAycjIiY9ATQ2OwEyARQPAQYiJj0BIyIGLwEuBSc2Nx4ENzM1NDYyHwEWERQPAQYiJj0BIyIOAgcGBw4CDwEOAicjIiY9ATQ2OwEyPgI3Nj8BPgU3MzU0NjIfARYBdCIrFAgeGi4WfQgKCgh9iwLOBbMFDwowHh4aJw0uGCgaJA0hKwwQHhosGI8KDgeyBQWzBQ8KjxssIBoMEhkQGCQSKRc2QiZ9CAoKCH0bKiQUEBEaHAwkJC42QCiPCg4HsgUCRjRlKRAmGgwCCghrCAr9xQgFswUMBmsCAgMBCgoWFiYUNGQZHioUFAJrCAoFsgUB7AgFswUMBmsQIiIbIj0lMkQVLxoYFgEKCGsIChIgJBkjPT4aQDAsIgwDawgKBbIFAAAAAQAA/6wDrALgABcAQ0BAEwgCAgQHAQECAkcFAQQDAgMEAm0GAQAAAwQAA2AAAgEBAlQAAgIBWAABAgFMAQAVFBIRDw4LCQYEABcBFwcFFCsBMhYQBiMiJzcWMzI2ECYiBgczByczPgECFKru7qqObkZUYn60tPq0Ao64uHwC8ALg8P6s8FhKPLQBALSufMzMpuoAAAIAAP+xBHcDCwAFAB8AS0BIGAsCBAUXEhADAwQRAQIDA0cAAQUBbwAFBAVvAAQDBG8AAwIDbwYBAgAAAlIGAQICAFYAAAIASgAAHRsVFA4NAAUABRERBwUWKwUVIREzEQEVFAYvAQEGIi8BBycBNjIfAQEnJjY7ATIWBHf7iUcD6BQKRP6fBg4GguhrAUcFDgaCAQNDCQgN8wcKB0gDWvzuArjyDAoJRP6fBgaC6WwBRgYGggEDRAgWCgAAAwAA/2oEbwNTAAsAFwA/AEhARTsmJAIEBAULAQMAAkcABAUABQQAbQAAAwUAA2sAAwIFAwJrAAUFDEgGAQICAVgAAQENAUkNDDQzFBMQDwwXDRcSJAcFFisBFhcUBisBFAYiJicXMjQHIiY1NCIVFBYBFhQHAQYmLwEmND8BJjU+BDc0NjcmNTQ+ARYHFAceARc3NhYXA2UjhCoe+lR2UgGOCQkgMBI6AlgEBvvrBRAELwQGaAscLjAkFAGCagQgKiIBBEVqHeoFEAQBd8dwHSo7VFQ6YRIBMCEJCSk6A34GEAT8dwUCBTUGEARaERMYMlReiE1UkhAKCxceAiIVCwoKSDTKBQIFAAAAAAQAAP9qBG8DUwAMABcAJwBPAJBAG0wmJQ4EBgM1AQEGIQEABAABAgAERzcYAgYBRkuwEFBYQCwAAQYEBgEEbQAABAIEAGUABgAEAAYEYAADAwdYAAcHDEgAAgIFWAAFBQ0FSRtALQABBgQGAQRtAAAEAgQAAm0ABgAEAAYEYAADAwdYAAcHDEgAAgIFWAAFBQ0FSVlADEVEExIoJCMTEggFGysFNCMiJjU0IhUUFjcyCQEuAQciDgIHFAUUBisBFAYiJic3ISYnNxYTFxYUBwEGJi8BJjQ/ASY1PgQ3NDY3JjU0PgEWBxQHHgEXNzYWAkQJIDASOigJ/tUB6RdmSjNWMhoBAqcqHvpUdlIBUwGmXCI9I7QvBAb76wUQBC8EBmgLHC4wJBQBgmoEICoiAQRFah3qBRBgCDAhCQkpOgEBEgGoMUIBIjg8HNf6HSo7VFQ6SGmXN8cCmTUGEAT8dwUCBTUGEARaERMYMlReiE1UkhAKCxceAiIVCwoKSDTKBQIAAAABAAD/agPoA1IAHQAtQCoRAQIBGhkSDQwJBQQIAAICRwACAQABAgBtAAEBDEgAAAANAEkXGRoDBRcrARYUDwEXBw4BJwcjNTcmNj8BFzc2Mh4BDwEXNzYyA9MVFd9TWVv8aMplykUaW1lU3xU8KAIW34PfFjoCVRU6Ft9UWVsaRcplymf+WllT3xUqOhbfg98VAAAABQAA/8MD6AKxAAkAGgA+AEQAVwBXQFQ0GwIABFMGAgIAUkMCAQJQQiknCAEGBgEERwAFBAVvAAIAAQACAW0AAQYAAQZrAAYDAAYDawADA24ABAAABFQABAQAWAAABABMTEsTLhkkFB0HBRorJTcuATc0NwYHFgE0JgciBhUUFjI2NTQ2MzI2NxQVBgIPAQYjIicmNTQ3LgEnJjQ3PgEzMhc3NjMyFh8BFgcWExQGBxMWFxQHBgcOASM3PgE3Jic3HgEXFgE2KzA4ASKAVV4BahALRmQQFhBEMAsQyjvqOxwFCgdECRlQhjILC1b8lzIyHwUKAw4LJAsBCRVYSZ0E+gsWJ1TcfCl3yEVBXSM1YiALaU8jaj1DOkGEkAFnCxABZEULEBALMEQQdQQBaf5aaTIJJwYKByokeE0RKhKDmAo2CQYGFAYBBf79ToAbARgZXhMTJC1gakoKhGlkQD8kYjYTAAACAAD/sQNbAwsAJABHAF1AWkMlAgYJLwEFBhcBAwIIAQEDBEcACQgGCAkGbQcBBQYCBgUCbQQBAgMGAgNrAAEDAAMBAG0ACAAGBQgGYAADAQADVAADAwBYAAADAExGRSYlJTYlJjUUJAoFHSsBFBUOASMiJicHBiImPQE0NjsBMhYGDwEeATcyNjc2NzY7ATIWExUUBisBIiY2PwEmIyIGBwYHBisBIiY3NT4BMzIWFzc2MhYDSyTkmVGYPEgLHBYWDvoOFgIJTShkN0qCJwYYBAxrCAoOFBD6DhYCCU1ScEuCJwYXBQxvBwwBJOaZUZo8SAscGAEFAwGWuj45SAsWDvoOFhYcC00kKgFKPgo4DQwBuPoOFhYcC01NSj4KOA0MBgSWuj45SAsWAAABAAD/xAOsAvgAFwBDQEAQBQIEAREBBQQCRwIBAQMEAwEEbQYBAAADAQADYAAEBQUEVAAEBAVYAAUEBUwBABQSDw0KCQcGBAMAFwEXBwUUKwEyFhczByczLgEiBhQWMzI3FwYjIiYQNgGYqO4Eeri4kAS0+rS0fmhORm6OqPDwAvjops7OfKy0/rQ8TFjwAVTwAAAABP////kELwLDAA8AHwAqADIAVUBSGRECAgMBRwABAAMCAQNeAAIIAQAEAgBgCQEEAAcGBAdgCgEGBQUGVAoBBgYFWAAFBgVMLCshIAEAMC0rMiwxJyQgKiEqHRwVEwkGAA8BDgsFFCs3IiY1ETQ2MyEyFhcRFAYjAREUFjchMjY1ETQmJyEiBgEzFRQGByEiJjc1BTI0KwEiFDPoJTQ0JQJfJTQBNiT9jwwGAl8ICgoI/aEHCgL/WTQl/IMkNgECRAkJWQkJiDQlAYklNDQl/nclNAHi/ncHDAEKCAGJBwoBDP30NhYeASAVNjYSEgAAAwAA/7EDWgNSAAgAPwBvAFRAUUpCOAMDBQFHAAUCAwIFA20ACgAAAgoAYAAIAAIFCAJeAAMABwQDB2AABAAGBAZcAAEBCVgACQkMAUlubGdlXFpVUk9MPj0xLiglJCMVKwsFFis3NC4BBhQWPgEBNCYnIzQ2JzQmJw4CBwYHDgIPAQYPAQYnIxEzMh4EFxY7ATI1NCc+ATQnNjU0Jic+ATcUBxYVFAcWFRQHFAYrASImJyYrASImNRE0NjsBNjc2Nz4CNzYzMh4BFRQHMzIWjxYcFhYcFgKDLBzENgEiNw4OFBcNHgIWDgwWCgwWCgoSEgcWDhwMHAJ2SUNrAhAUCh0KCRIYRxsFFQEhYE5INmhFQQyhHSoqHZkUOSAcDQwWGBYcL0ooG2I6VmQPFAIYGhgCFAFQHSoBIHIgNzQBD0JKGA0mAxoUDhkLCA8HAf6bAgYGCAQEKV0PEAkqKBIcJw4iCQEyFTIpEhQrJgwMOCtOWhoXFyodAWUeKg1JKh4OREgYFSROQTM4VAAAAwAA/2oDWQMLAAgAQAByAE9ATHFoEQ8EAAIBRwAAAgMCAANtAAoAAQkKAWAACQACAAkCXgADAAgFAwhgAAUABgQFBmAABAQHWAAHBw0HSWZjYF0qJSQlHiEZPRsLBR0rEzQuAQYUFj4BATQmIz4BJzQnNjQmJzY1NCYrASIPAQ4BDwIGJyMRMzIWHwEeAh8BFhceAhcyNic0JiczMjY3FAYnIxYVFA4BIyInLgMnJicmJyMiJjURNDY7ATI3PgE3MzIWHQEWFRQHFhUUBxaPFhwWFhwWAoMYEggMAR0KFBACNjFHSXYQDQ4NFRIKCBISCRYLFgsWEAoNHg0XFA4ONiQBNAHEHCxHVDtiGydMLhwWExYGDgobITkUmR0qKh2hDEFIajo/TmAhARUFGwJYDxQCGBoYAhT+zhM0CiIOJhwRKigKEA8vLikFBAYEBgQCAf6bCgoUCh4SDREmDRhKQg82NiFwISwbOVYBNzRCTSQVEjYwLg0cK0kNKh4BZR0qFxgYAVhNAys4DAwmKhUSKQAAAAAIAAD/jgPEA1IACAARABoAIwAsADUAPgBHAFhAVRsBAwEJAQIAAkcJAQQMAQwEAW0ACAAHDAgHYAANAAwEDQxgBgEBBQEAAgEAYAADAAIDAlwACgoLWAALCwwKSUZFQkE9PDk4MC8TFBMYFBMUExIOBR0rJRQGIiY0NjIWBRQGIi4BNh4BARQOAS4BNh4BARQGIiY+AR4BARQGIiY0NjIWARQOASY+AR4BARQGIiY0NjIWBRQOAS4BNjIWASYqOyoqOiwBFCg+JgQuNjD+dCo8KAIsOC4CnCo7KgImQCT96TRKNDRKNAKNKjosAig+Jv6dPlo+Plo+AShKZ0gBSmZKSB0qKjsqKpEdKio6LAIoAWoeKAIsOC4GIv7IHSoqOiwCKAINJTQ0SjQ0/sUeKAIsOC4GIgFnLT4+Wj4+oDRIAUpmSkoAAAAAAQAA/7QDEAMIADYAPUA6AAIFBgUCBm0ABgQFBgRrAAEAAwcBA2AABwAFAgcFYAAEAAAEVAAEBABYAAAEAEwmFyYlExUVIggFHCslFAYjIicBJjQ+ARcBFhQGIicBJiIGFhcBFjMyNjc0JwEmIyIGFB8BFhQGIi8BJjU0NjMyFwEWAxBaQEs4/k4/fLBAAVIFIhAF/q0sdFIBKgGxIy4kLgEj/rsOExAWDuUGJA4G5SNALTEjAUQ4TUFYNwGyQLB6AT/+rgUQIgUBUytUdSv+TyQwIy4jAUQOFiIP5AYQIgXlIjEuQCP+uzYAAAAPAAD/+QQwAnwACwAXACMALwA7AEcAUwBfAGsAdwCDAI8AnwCjALMAjECJSAECAwFHAB4AGwUeG14aFxUPCwUFFhQOCgQEAwUEYBkRDQkEAxgQDAgEAgEDAmETBwIBEgYCABwBAGAfARwdHRxSHwEcHB1YAB0cHUygoLKvqqego6CjoqGfnJqYlZKPjImGg4B9end0cW5raGViX1xZVlJQTUpHREE+OzgzMzMzMzMzMzIgBR0rNxUUKwEiPQE0OwEyNxUUKwEiPQE0OwEyJxUUKwEiPQE0OwEyARUUIyEiPQE0MyEyJRUUKwEiPQE0OwEyJxUUKwEiPQE0OwEyFxUUKwEiPQE0OwEyJxUUKwEiPQE0OwEyFxUUKwEiPQE0OwEyFxUUKwEiPQE0OwEyARUUKwEiPQE0OwEyFxUUKwEiPQE0OwEyFxUUKwEiPQE0OwE1NDsBMhMRIREBERQGIyEiJjURNDYzITIW1gk1CQk1CUgJfQkJfQlICTUJCTUJAjwJ/h4JCQHiCf6bCTYJCTYJSAk1CQk1CdYINgkJNghHCTUJCTUJ1gk1CQk1CdcJNgkJNgn+4gk2CQk2CY8JNgkJNgmPCX0JCT4JNglH/F8D6Cgf/F8dKiodA6EeKsY1CQk1CYY1CQk1CYY2CQk2Cf7ZNQkJNQmGNQkJNQmGNgkJNgmYNQkJNQmGNgkJNgmYNQkJNQmYNQkJNQkBFTYJCTYJCTYJCTYJCcQJCTUJhgn+UwH0/gwB9P4MHSoqHQH0HioqAAAAAwAA//kDWgLEAA8AHwAvADdANCgBBAUIAAIAAQJHAAUABAMFBGAAAwACAQMCYAABAAABVAABAQBYAAABAEwmNSY1JjMGBRorJRUUBgchIiYnNTQ2NyEyFgMVFAYnISImJzU0NhchMhYDFRQGIyEiJic1NDYXITIWA1kUEPzvDxQBFg4DEQ8WARQQ/O8PFAEWDgMRDxYBFBD87w8UARYOAxEPFmRHDxQBFg5HDxQBFgEQSA4WARQPSA4WARQBDkcOFhYORw8WARQAAAAABAAAAAAEXwMLAAoAIAA6AFIAi0CIRwELCC8BBAYVAQIHAwEAAQRHEQ0CCwgGCAsGbRAJAgcEAgQHAm0PBQIDAgECAwFtAAwACggMCmAACAAGBAgGYAAEAAIDBAJgAAEAAAFUAAEBAFgOAQABAEw7OyEhCwsBADtSO1JMS0VDQD8hOiE6NDMtKyclCyALIBoZExIPDgYFAAoBChIFFCshIiYnND4BFgcUBjciLgEiBg8BIiY1NDc+AhYXFhUUBjciJy4BByIOAyMiJjU0Nz4BHgEXFhUUBjciJy4CBgcGIyImJzQ3NiQgBBcWFRQGAjsLUAFGLEgBUowBKkhIRhYWClQFLIKChCsFVI4GBkyCVS9gRjggAglUBkrQ2NJJBlSOBgdj2P7WZAcGCVQBBmgBIAEsASJnBVRSCxIYAhwQC1KXHBwcDg5UCgcGKzACNCkGBwpUmAU6OAEYIiQYVAoHBUpSAk5MBQcKVJcFWFgCXFYFVAoHBmhycmgGBwpUAAAAAv/+/7EDNgMLABIAMAAuQCsIAQQDAUcAAwQDbwAEAAABBABgAAECAgFUAAEBAlgAAgECTCgoJCwhBQUZKyUGIyIuATc0Nw4BBxQeAjcyNjcOASMiLgI3ND4CNzYWBw4BBxQeATcyNzYXHgECwB4fZqxmATpwjgE6XoZIUJClNdR8V6BwSAJAbppUGRQTMDIBUoxSQj0XEQgEewVkrmVrXCG+d0iGXD4DRG1xiER0nldVnHJGAwEuESt0QFOKVAEdChEIFgAAAAAD//7/sQPEA1IACwAQABYANkAzAAECAxABAgACAkcAAQQDBAEDbQADAgQDAmsAAgAEAgBrAAAAbgAEBAwESREUERUjBQUZKwkBDgEHIi4CPgEzEyEUBgcTIREyHgEBrQEwO55XdcZwBHi+eWgBr0I9XP5TdcR0AWH+0D1CAXTE6sR0/lNYnjsBeAGtcsYAAAACAAD/sQR3AwsABQALADRAMQsKCQMDAQFHAAEDAW8AAwIDbwQBAgAAAlIEAQICAFYAAAIASgAACAcABQAFEREFBRYrBRUhETMRARMhERMBBHf7iUcDWo78YPoBQQdIA1r87gI7/gwBQgFB/r8AAAAABQAA/7EEdwMLAAMABwANABEAFQBmQGMABQoFbw8BCgMKbwwBAwgDbw4BCAEIbwsBAQABbwkHAgMABgBvDQEGBAQGUg0BBgYEVgAEBgRKEhIODggIBAQAABIVEhUUEw4RDhEQDwgNCA0MCwoJBAcEBwYFAAMAAxEQBRUrAREjEQERIxEBFSERMxEBESMRJREjEQFljwFljgLK+4lHAsuPAWWPAV7+4gEeAR79xAI8/X1IA1r87gH0/lMBrdb9fQKDAAAAAAIAAP+xA3MDCwAXAB4AM0AwHhsXCAQEAQFHAAQBAAEEAG0AAABuAAIBAQJUAAICAVgFAwIBAgFMEhMjMyQyBgUaKyUWBgchIiY3ATUjIiY+ATMhMh4BBisBFQ8BIQM1IxUDVB8mO/19OyYfARgkDhYCEhABHg8UAhgNJJqXAY2jRyoyRgFIMQG73hYcFhYcFt4m8AEB8/MABgAA/8ADoQNSAAMAFAAcACQALAA0AENAGzIwLiwqKCYkIiAeGhgWAwIBEQABAUc0HAIBRUuwH1BYQAsAAAEAcAABAQwBSRtACQABAAFvAAAAZlm0FxgCBRYrATcnByUUBwEGIi8BJjQ3ATYyHwEWJRcPAS8BPwEfAQ8BLwE/AQEXDwEvAT8BARcPAS8BPwECmKQ8pAE2Cv0yCh4KbwoKAs4KHgpvCv0ONjYRETc3EdRtbSIhbW0hAik3NxERNjYR/qw2NhERNjYRAg6jPKNnDwr9MgoKbwoeCgLOCgpvClsQETc3ERA3kSIhbW0hIm3+iBEQNzcQETcBLhARNzcREDcAAAAB//D/fwPrA0UAOQAPQAwsAQBFAAAAZhMBBRUrJQYHBiYnJicmJyY3Nj8BNjc2HgIHBgcGBwYXFhcWFxY2Nz4BJzQnJicuAQc1NhcWFxYXFhcWBgcGA1dFX1rHWl5EXSUjGhpVBBMMG0IuCA4HCUUaGRYXQ0ppYsZDNTkBIClTUM1ldXd1XGAvIwICODcQCUUjIQYlJ0Rdf3t9gGMEFwcRBy4+Gw0JSmBeW15DShQSRU09mFBSTGFAPSIiASkTE0ZJcFJZV6ZFFgAAAAABAAAAAAIIAqEAFQAZQBYSCwQDAEQAAQABbwIBAABmFRUYAwUXKwEWFA8BJyY0NjIfARE0NjIWFRE3NjIB+Q8P9fUPHiwPeB4qIHgPKgFaDywP9fUPLB4PdwGLFR4eFf51dw8AAQAAAAAChgJiABQANEAxDQEBAAFHAAMAA28AAgECcAQBAAEBAFQEAQAAAVgAAQABTAEAEA8LCgYEABQBFAUFFCsBMhYUBichFxYUBiIvATc2MhYUDwECUxUeHhX+dXcPHiwP9fUPLB4PdwGTICogAXcPLB4P9fUPHiwPdgAAAAAB//8AAAKGAmIAFQAqQCcEAQIDAUcAAAMAbwABAgFwAAMCAgNUAAMDAlgAAgMCTCMkFBEEBRgrATYyHwEHBiImND8BISIuATY3IScmNAFIDyoQ9fUPKx4PeP51Fh4CIhQBi3gPAlMPD/X1Dx4sD3ceLB4Bdg8sAAABAAAAAAIIAqEAFAAYQBUOBwIARQIBAAEAbwABAWYVFRQDBRcrARcWFAYiLwERFAYuATURBwYiJjQ3AQT1Dx4qD3ggKh54DyweDwKh9Q8sHg94/nUVIAIcFwGLeA8eLA8AAAAAAQAA/70DSAMFABoAHEAZBwUCAAEBRwYBAEQAAQABbwAAAGYoEgIFFislFAYiLwEFEycmNzYzMjc2Nz4BHwEWBgcGBwYCPR4rEKn+xeyoGAwOIp1xWj0JNhfQFQ4Zfy04JRceEKnsATupFyEgOS1+GBAV0Rc2CT9ZbgAAAAIAAAAAAjQCUQAVACsAHEAZKRMCAAEBRwMBAQABbwIBAABmFx0XFAQFGCslFA8BBiInASY0NwE2Mh8BFhQPARcWFxQPAQYiJwEmNDcBNjIfARYUDwEXFgFeBhwFDgb+/AYGAQQFEAQcBgbb2wbWBRwGDgb+/AYGAQQGDgYcBQXc3AVSBwYcBQUBBQUOBgEEBgYcBRAE3NsGBwcGHAUFAQUFDgYBBAYGHAUQBNzbBgAAAgAAAAACIgJRABUAKwAcQBkhCwIAAQFHAwEBAAFvAgEAAGYcGBwUBAUYKwEUBwEGIi8BJjQ/AScmND8BNjIXARYXFAcBBiIvASY0PwEnJjQ/ATYyFwEWAUwF/vsFDgYcBgbb2wYGHAUQBAEFBdYF/vwGDgYcBQXb2wUFHAYOBgEEBQE6BwX++wUFHAYOBtvcBQ4GHAYG/vwFCAcF/vsFBRwGDgbb3AUOBhwGBv78BQAB//3/sQNfAwsADAARQA4AAQABbwAAAGYVEwIFFisBFA4BIi4CPgEyHgEDWXLG6MhuBnq89Lp+AV51xHR0xOrEdHTEAAP//P+QA5oDLAAIABMAKQBiQF8MAQMCIyIYFwQFBwJHAAcGBQYHBW0ABQQGBQRrCAEACQECAwACYAADAAYHAwZgCgEEAQEEVAoBBAQBWAABBAFMFRQKCQEAJiQgHhsZFCkVKRAOCRMKEwUEAAgBCAsFFCsBNgASAAQAAgAXIgYVBhYzMjY1NAMyNjcnBiMiPwE2IyIGBxc2MzIPAQYBxr4BEAb+9v6E/u4GAQzyKi4CIiAmLrQebDQSMBgOCioaMB52OBA0FgwMJBoDKgL++P6E/u4GAQoBfAESljAaHCAsIDr9rjQ0GCQmoGA6LhoiIphoAAABAAD/9wOIAsMALwBNQEouLCogAgUFBhkBBAUWEgIDBAsBAQIERwAGBQZvAAUEBW8ABAMEbwADAgNvAAIBAm8AAQAAAVQAAQEAWAAAAQBMJBYWIxEiKAcFGysBBgcVFA4DJyInFjMyNy4BJxYzMjcuAT0BFhcuATQ3HgEXJjU0NjcyFzY3Bgc2A4glNSpWeKhhl30TGH5iO1wSEw8YGD9SJiwlLBlEwHAFakpPNT02FTs0Am42JxdJkIZkQAJRAk0BRjYDBg1iQgIVAhlOYCpTZAUVFEtoATkMIEAkBgAAAAEAAP+xA1kDCwAkAEpARxIBBAUBRwcBAgMBAwIBbQgBAQFuCQEAAAUEAAVgAAQDAwRUAAQEA1YGAQMEA0oBAB4cGxoZGBUTEQ8MCwoJCAYAJAEjCgUUKwEyFhURFAYHIxEzNyM1NDY/ATUmIyIGFxUjFTMRISImNRE0NjcCuENeXkNobxB/GiZEI0FLXAFwcP7XQ15eQwMLYEH96EJeAQFNgVMfHgEBcwVYU1+B/rNgQQIYQl4BAAADAAD/sQNZAwsAGwAnADcAZkBjEgEDBBEBCAMCRwAIAwADCABtCgEGAAEABgFtAAsBAgELAm0ADQAEAw0EYAADCQcCAAYDAF4AAQACBQECYAAFDAwFVAAFBQxYAAwFDEw2My4rJyYlJCMiERESIyMjJBESDgUdKwE0JyMVMw4DJyImNDYzMhc3JiMiDgEWFzI2NzM1IzUjFSMVMxUzExEUBgchIiY1ETQ2NyEyFgIABMp6AhAaMB43Tk43NCI6PFRZfAKAV1xywD09PT09PZleQ/3pQ15eQwIXQ14BWQ8VSg0eHBYBUG5QITk3fLR6AnRDPj09Pj0BaP3oQl4BYEECGEJeAWAAAAAD//3/sQNZAwsADAAcAC4AREBBKB4CBQQWFQ4DAwICRwYBAAAEBQAEYAAFAAIDBQJgAAMBAQNUAAMDAVgAAQMBTAEALCojIRoYEhAHBgAMAQwHBRQrATIeARQOASIuAj4BEzU0JisBIgYHFRQWFzMyNicTNCcmKwEiBwYVExQWOwEyNgGtdMZycsboyG4GerzBCgdrCAoBDAdrBwoBCgYFCHsIBQYKCglnCAoDC3TE6sR0dMTqxHT9SGoICgoIaggKAQzHAVoHAwUFAwf+pgYICAAAAAIAAP/5A6ADCwAtAEIATkBLOwEEBiUBBQQCRwAHAQIBBwJtAAYCBAIGBG0ABAUCBAVrAAUDAgUDawABAAIGAQJgAAMAAANUAAMDAFgAAAMATBQXFSc1OTUzCAUcKwEVFAYjISImNRE0NjchMhceAQ8BBiMnJiMhIgYHERQWFyEyNj0BND8BNjMyFxYTAQYiLwEmND8BNjIfAQE2Mh8BFhQDEl5D/jBDXl5DAdAjHgkDBxsGBwUNDP4wJTQBNiQB0CU0BSQGBwMEC4H+OQ0kDvAODj0OJA6TAWkNJA4+DQFLsUNeXkMB0EJeAQ4EEwYcBQEDNCX+MCU0ATYkjQgFIwYCBAEF/joODvANJA4+DQ2TAWkNDT0OJAAC//7/xAM2AvgADgAdACVAIh0cFxEKBAEHAAEBRwkBAUUWAQBEAAEAAW8AAABmHBICBRYrPwERJTcmEjc2NxcGBw4BAQUHFgIHBgcnNjc+AScHunT+7Fh0BHZkjARkSFgEAaIBFFh0BHZgkAJiSFgEVnKMdP7cEFZ6AVB4ZBBmEEhY+gH6EFZ6/rB4YhRoEEhY+lx0AAAAAAT/4/+WBB4DJgAMABkAHgApAExASSIBBAYBRwAGAAQABgRtCAECBwEABgIAYAAEAAUBBAVgAAEDAwFUAAEBA1gAAwEDTA4NAQAoJx4dHBsVEg0ZDhkIBQAMAQwJBRQrASIHAQYWMyEyNicBJicyFwEWBiMhIiY3ATYTNDIUIhMUDwEnJjU0PgEWAgIxIP7MICpCAnFBLCL+zSEvaj8BND9nff2Pe2tAATU+J4iIkgZHSQYuQiwCvTf9/zdQUDcCATdpa/3/abu5awIBa/10RYgBfA4Ps7MPDiAuAjIAAAAABgAA//YDqQLGAAwAGQAmADMAQABNADxAOQsBBQoBBAMFBGAJAQMIAQIBAwJgBwEBAAABVAcBAQEAWAYBAAEATExJRkM/PDQzNDM0MzQzMgwFHSs1FBY7ATI2NCYrASIGERQWOwEyNjQmKwEiBhEUFjsBMjY0JisBIgYTFBYzITI2NCYjISIGERQWMyEyNjQmIyEiBhEUFjMhMjY0JiMhIgYqHiAeKioeIB4qKh4gHioqHiAeKioeIB4qKh4gHirqKh4CLx4qKh790R4qKh4CLx4qKh790R4qKh4CLx4qKh790R4qPh4qKjwqKgECHioqPCoqAQIeKio8Kir9oh4qKjwqKgECHioqPCoqAQIeKio8KioACP///4sDqgMxAA8AHwAjACcANwBHAEsATwBOQEsKAQIPAQcGAgdeDgEGCwEDAAYDYAgBAA0BBQQABV4MAQQBAQRSDAEEBAFYCQEBBAFMT05NTEtKSUhGQz47NjM0EREREjU1NTMQBR0rFRE0NjchMhYHERQGIyEiJhkBNDYzITIWBxEUBgchIiYTMzUjETM1IwERNDY3ITIWBxEUBiMhIiYTETQ2MyEyFhURFAYHISImEzM1IxEzNSMeFgEeFSABHhb+4hYeHhYBHhUgAR4W/uIVIFnW1tbWAcseFgEeFSABHhb+4hUgAR4WAR4WHh4W/uIVIFnX19fXQgEeFh4BIBX+4hUeHgI3AR4VHh4V/uIWHgEg/hfWAUzV/OUBHhYeASAV/uIVHh4CNwEeFR4eFf7iFh4BIP4X1gFM1QAAAAAIAAD/xANZAwsAUwBaAF8AZABpAG4AcwB4AGpAZyQeGxUEBAFlDQIDAmoBBwZHAQUHBEcABAECAQQCbQACAwECA2sAAwYBAwZrAAYHAQYHawAHBQEHBWsABQVuCAEAAQEAVAgBAAABWAABAAFMAQBzcnFwRkQ4NzEwLCsdHABTAVMJBRQrATIeARUUBgcGJj0BNCc+BCc0JzYnJgYPASYiBy4CBwYXBhUUHgMXBgcOASImJy4BLwEiBh4BHwEeAR8BHgI2MzcVFBcUBicuATU0PgEDNicmBwYWFzYmBhYXNiYGFhc2JgYWFzYmBhY3NAYUNjcmBhY2Aa10xnKkgQ8OHSAyOCIaAiwVGRA8FRU0bjUIHkAPGRQsGCI4MCEVBgwaJiIOCyAMCwwIAggDBAwYBgYHIigmDA0BEA6BpHTClAIFBgIBChQECwcKFAYKCgocBA0JDSUBEQQRJhMTIAESAhIDC3TEdYzgKwMOCnY2GQMOHixIMEMwMz8FFg4NDw8GEhoGPzMwQy9ILhwQAhQmBQYYFxIWAwEECgYDAwYeDg0VGggCAzIcAgoOAyvgjHXEdP2YBAMBAgQGDwMLBgwVBA4HDhQEDQoMCQYFDAYEBwENAQsHAw4GAAAAAAIAAAAAAlgCYwAVACsAK0AoHQECBQcBAwICRwAFAgVvAAIDAm8EAQMAA28BAQAAZhcUGBcUFAYFGislFA8BBiIvAQcGIi8BJjQ3ATYyFwEWNRQPAQYiLwEHBiIvASY0NwE2MhcBFgJYBhwFDgbc2wUQBBwGBgEEBQ4GAQQGBhwFDgbc2wUQBBwGBgEEBQ4GAQQGdgcGHAUF29sFBRwGDgYBBAUF/vwGzwcGHAUF3NwFBRwGDgYBBAYG/vwGAAAAAAIAAAAAAlgCdQAVACsAK0AoJQEDAQ8BAAMCRwUBBAEEbwIBAQMBbwADAANvAAAAZhQXGBQXFAYFGisBFAcBBiInASY0PwE2Mh8BNzYyHwEWNRQHAQYiJwEmND8BNjIfATc2Mh8BFgJYBv78BRAE/vwGBhwFDgbb3AUQBBwGBv78BRAE/vwGBhwFDgbb3AUQBBwGAXAHBv78BgYBBAYOBhwFBdzcBQUcBs8HBv78BQUBBAYOBhwGBtvbBgYcBgAAAAEAAAAAAV4CUQAVABdAFAMBAAEBRwABAAFvAAAAZhcZAgUWKwEUDwEXFhQPAQYiJwEmNDcBNjIfARYBXgbb2wYGHAUOBv78BgYBBAUQBBwGAiIHBdzbBg4GHAUFAQUFDgYBBAYGHAUAAQAAAAABTAJRABUAF0AUCwEAAQFHAAEAAW8AAABmHBQCBRYrARQHAQYiLwEmND8BJyY0PwE2MhcBFgFMBf77BQ4GHAYG29sGBhwFEAQBBQUBOgcF/vsFBRwGDgbb3AUOBhwGBv78BQABAAAAAAJYAdQAFQAZQBYHAQACAUcAAgACbwEBAABmFxQUAwUXKyUUDwEGIi8BBwYiLwEmNDcBNjIXARYCWAYcBQ4G3NsFEAQcBgYBBAUOBgEEBr0HBRwGBtvbBgYcBQ4GAQQGBv78BQAAAAABAAAAAAJYAeYAFQAZQBYPAQABAUcCAQEAAW8AAABmFBcUAwUXKwEUBwEGIicBJjQ/ATYyHwE3NjIfARYCWAb+/AUQBP78BgYcBQ4G29wFEAQcBgG3BwX++wUFAQUFDgYcBgbb2wYGHAUAAAACAAD/sQNZAwsAMQBGAFpAVyoBAwUdAQgDQCUCBAg7MwIGBwRHAAgDBAMIBG0ABAcDBAdrAAEGAgYBAm0ABQADCAUDYAAHAAYBBwZgAAIAAAJUAAICAFgAAAIATCMmJyk1FyMXJAkFHSsBFA4CIyImJyY0PwE2FhceATMyPgMuAiIGBxcWBisBIiYnNTQ2HwE+ATMyHgIlFRQGKwEiJj0BNDY7ATU0NjsBMhYDWURyoFZgrjwEBUwGEQQpdkM6aFAqAi5MbG9kKE0RExf6DxQBLBFIPJpSV550Qv6cCgiyCAoKCH0KByQICgFeV550RFJJBg4ETQUBBjU6LkxqdGpMLiglTRAtFg76GBMSSDk+RHSeSvoICgoIIwgKxQgKCgAFAAD/agPoA1IAEAAUACUALwA5AGdAZDMpAgcIIQEFAh0VDQwEAAUDRwQBBQFGBgwDCwQBBwIHAQJtAAIFBwIFawAFAAcFAGsJAQcHCFgKAQgIDEgEAQAADQBJEREAADc1MjEtKygnJCIfHhsZERQRFBMSABAADzcNBRUrAREUBgcRFAYHISImJxETNjMhESMRAREUBgchIiYnESImJxEzMhclFSM1NDY7ATIWBRUjNTQ2OwEyFgGJFg4UEP7jDxQBiwQNAZ+OAjsWDv7jDxQBDxQB7Q0E/j7FCgihCAoBd8UKCKEICgKf/lQPFAH+vw8UARYOAR0B6Az+eAGI/gz+4w8UARYOAUEWDgGsDK19fQgKCgh9fQgKCgAAAAEAAAABAAB31GYfXw889QALA+gAAAAA2tnFqwAAAADa2cWr/+P/aQS/A1MAAAAIAAIAAAAAAAAAAQAAA1L/agAABQX/4//kBL8AAQAAAAAAAAAAAAAAAAAAAJED6AAAA+gAAALKAAAEL///A6AAAAMxAAADoAAAA6AAAAOgAAADoAAAA6AAAAPoAAAFBQAAA1kAAAPoAAAD6AAAA6AAAAOgAAAD6P//A6AAAAPoAAADEf/5A1n//QOg//kD6AAAA+j/+gNZ//0EL///AfT//gPoAAACOwAAAjv//wFlAAABZQAAA+gAAALKAAAD6AAAAsoAAAOgAAADWQAAA1kAAAOgAAADWQAAA1kAAANZAAAD6AAAA+gAAAGsAAADoP//A1n//wOg//8COwAAA1n//QOgAAACggAAAawAAAMRAAACgv//A1kAAAOgAAADoAAAA6AAAAOgAAADWQAABC8AAANZAAADEf//A1kAAANZAAADWQAAA1kAAAMRAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAABZQAAA6D//wPoAAAD6AAAA+gAAAPoAAAD6AAAA1kAAAQvAAACggAAA6AAAAKCAAADoAAAAWUAAAI7AAADoP//A+gAAAOsAAAEdgAABHYAAAR2AAAD6AAAA+gAAANZAAADrAAABC///wNZAAADWQAAA+gAAAMRAAAELwAAA1kAAAR2AAADWf/+A+j//gR2AAAEdgAAA6AAAAOgAAAD6P/wAggAAAKGAAAChv//AggAAANCAAACOwAAAjsAAANZ//0DmP/8A6AAAANZAAADWQAAA1n//QOgAAADNP/+BAL/4wOpAAADqf//A1kAAAKCAAACggAAAWUAAAFlAAACggAAAoIAAANZAAAD6AAAAAAAAADuATIB9gIMAioCWgJ2AsIDRgPKBOQFagYABrIHSAhMCVQJzApoCvQLKAuMDGYM4g3yEfYSMhJ+ExQTPBNiE4oTrBPiFCIUWBSYFNwVIhVoFawWKhaQFvQXehfCGAoYnBjuGVgaBhpsGzAbjBu+HHYc9B1+HgAeoh+QIAogxiJqIywjtCSwJYgmZicaJ94oWiimKTYp8iqQKvorRCvMLMItFC1qLdwuVC6cLw4vYC+yL/owYjDGMVIxoDKOMtgzNjO8NH40yDV6NhA2WjbSN5g4YjkGOXw6njsEO8Q8KDxwPKg9Cj1WPdg+Pj5wPrA+7D8eP1w/tEAMQC5AqEEYQXRB+EJiQu5DOkOqRDRE1EXCRiJGgka2RupHIEdWR+ZIcwABAAAAkQH4AA8AAAAAAAIARABUAHMAAACwC3AAAAAAAAAAEgDeAAEAAAAAAAAANQAAAAEAAAAAAAEABQA1AAEAAAAAAAIABwA6AAEAAAAAAAMABQBBAAEAAAAAAAQABQBGAAEAAAAAAAUACwBLAAEAAAAAAAYABQBWAAEAAAAAAAoAKwBbAAEAAAAAAAsAEwCGAAMAAQQJAAAAagCZAAMAAQQJAAEACgEDAAMAAQQJAAIADgENAAMAAQQJAAMACgEbAAMAAQQJAAQACgElAAMAAQQJAAUAFgEvAAMAAQQJAAYACgFFAAMAAQQJAAoAVgFPAAMAAQQJAAsAJgGlQ29weXJpZ2h0IChDKSAyMDIwIGJ5IG9yaWdpbmFsIGF1dGhvcnMgQCBmb250ZWxsby5jb21pZm9udFJlZ3VsYXJpZm9udGlmb250VmVyc2lvbiAxLjBpZm9udEdlbmVyYXRlZCBieSBzdmcydHRmIGZyb20gRm9udGVsbG8gcHJvamVjdC5odHRwOi8vZm9udGVsbG8uY29tAEMAbwBwAHkAcgBpAGcAaAB0ACAAKABDACkAIAAyADAAMgAwACAAYgB5ACAAbwByAGkAZwBpAG4AYQBsACAAYQB1AHQAaABvAHIAcwAgAEAAIABmAG8AbgB0AGUAbABsAG8ALgBjAG8AbQBpAGYAbwBuAHQAUgBlAGcAdQBsAGEAcgBpAGYAbwBuAHQAaQBmAG8AbgB0AFYAZQByAHMAaQBvAG4AIAAxAC4AMABpAGYAbwBuAHQARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABzAHYAZwAyAHQAdABmACAAZgByAG8AbQAgAEYAbwBuAHQAZQBsAGwAbwAgAHAAcgBvAGoAZQBjAHQALgBoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAAAAAAIAAAAAAAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkQECAQMBBAEFAQYBBwEIAQkBCgELAQwBDQEOAQ8BEAERARIBEwEUARUBFgEXARgBGQEaARsBHAEdAR4BHwEgASEBIgEjASQBJQEmAScBKAEpASoBKwEsAS0BLgEvATABMQEyATMBNAE1ATYBNwE4ATkBOgE7ATwBPQE+AT8BQAFBAUIBQwFEAUUBRgFHAUgBSQFKAUsBTAFNAU4BTwFQAVEBUgFTAVQBVQFWAVcBWAFZAVoBWwFcAV0BXgFfAWABYQFiAWMBZAFlAWYBZwFoAWkBagFrAWwBbQFuAW8BcAFxAXIBcwF0AXUBdgF3AXgBeQF6AXsBfAF9AX4BfwGAAYEBggGDAYQBhQGGAYcBiAGJAYoBiwGMAY0BjgGPAZABkQGSAAlkYXNoYm9hcmQEdXNlcgV1c2VycwJvawZjYW5jZWwEcGx1cwVtaW51cwxmb2xkZXItZW1wdHkIZG93bmxvYWQGdXBsb2FkA2dpdAVjdWJlcwhkYXRhYmFzZQVnYXVnZQdzaXRlbWFwDHNvcnQtbmFtZS11cA5zb3J0LW5hbWUtZG93bgltZWdhcGhvbmUDYnVnBXRhc2tzBmZpbHRlcgNvZmYEYm9vawVwYXN0ZQhzY2lzc29ycwVnbG9iZQVjbG91ZAVmbGFzaAhiYXJjaGFydAhkb3duLWRpcgZ1cC1kaXIIbGVmdC1kaXIJcmlnaHQtZGlyCWRvd24tb3BlbgpyaWdodC1vcGVuB3VwLW9wZW4JbGVmdC1vcGVuBnVwLWJpZwlyaWdodC1iaWcIbGVmdC1iaWcIZG93bi1iaWcPcmVzaXplLWZ1bGwtYWx0C3Jlc2l6ZS1mdWxsDHJlc2l6ZS1zbWFsbARtb3ZlEXJlc2l6ZS1ob3Jpem9udGFsD3Jlc2l6ZS12ZXJ0aWNhbAd6b29tLWluBWJsb2NrCHpvb20tb3V0CWxpZ2h0YnVsYgVjbG9jawl2b2x1bWUtdXALdm9sdW1lLWRvd24Kdm9sdW1lLW9mZgRtdXRlA21pYwdlbmR0aW1lCXN0YXJ0dGltZQ5jYWxlbmRhci1lbXB0eQhjYWxlbmRhcgZ3cmVuY2gHc2xpZGVycwhzZXJ2aWNlcwdzZXJ2aWNlBXBob25lCGZpbGUtcGRmCWZpbGUtd29yZApmaWxlLWV4Y2VsCGRvYy10ZXh0BXRyYXNoDWNvbW1lbnQtZW1wdHkHY29tbWVudARjaGF0CmNoYXQtZW1wdHkEYmVsbAhiZWxsLWFsdA1hdHRlbnRpb24tYWx0BXByaW50BGVkaXQHZm9yd2FyZAVyZXBseQlyZXBseS1hbGwDZXllA3RhZwR0YWdzDWxvY2stb3Blbi1hbHQJbG9jay1vcGVuBGxvY2sEaG9tZQRpbmZvBGhlbHAGc2VhcmNoCGZsYXBwaW5nBnJld2luZApjaGFydC1saW5lCGJlbGwtb2ZmDmJlbGwtb2ZmLWVtcHR5BHBsdWcHZXllLW9mZglhcnJvd3MtY3cCY3cEaG9zdAl0aHVtYnMtdXALdGh1bWJzLWRvd24Hc3Bpbm5lcgZhdHRhY2gIa2V5Ym9hcmQEbWVudQR3aWZpBG1vb24JY2hhcnQtcGllCmNoYXJ0LWFyZWEJY2hhcnQtYmFyBmJlYWtlcgVtYWdpYwVzcGluNgpkb3duLXNtYWxsCmxlZnQtc21hbGwLcmlnaHQtc21hbGwIdXAtc21hbGwDcGluEWFuZ2xlLWRvdWJsZS1sZWZ0EmFuZ2xlLWRvdWJsZS1yaWdodAZjaXJjbGUMaW5mby1jaXJjbGVkB3R3aXR0ZXIQZmFjZWJvb2stc3F1YXJlZA1ncGx1cy1zcXVhcmVkEWF0dGVudGlvbi1jaXJjbGVkBWNoZWNrCnJlc2NoZWR1bGUNd2FybmluZy1lbXB0eQd0aC1saXN0DnRoLXRodW1iLWVtcHR5DmdpdGh1Yi1jaXJjbGVkD2FuZ2xlLWRvdWJsZS11cBFhbmdsZS1kb3VibGUtZG93bgphbmdsZS1sZWZ0C2FuZ2xlLXJpZ2h0CGFuZ2xlLXVwCmFuZ2xlLWRvd24HaGlzdG9yeQpiaW5vY3VsYXJzAAAAAQAB//8ADwAAAAAAAAAAAAAAAAAAAAAAGAAYABgAGANT/2kDU/9psAAsILAAVVhFWSAgS7gADlFLsAZTWliwNBuwKFlgZiCKVViwAiVhuQgACABjYyNiGyEhsABZsABDI0SyAAEAQ2BCLbABLLAgYGYtsAIsIGQgsMBQsAQmWrIoAQpDRWNFUltYISMhG4pYILBQUFghsEBZGyCwOFBYIbA4WVkgsQEKQ0VjRWFksChQWCGxAQpDRWNFILAwUFghsDBZGyCwwFBYIGYgiophILAKUFhgGyCwIFBYIbAKYBsgsDZQWCGwNmAbYFlZWRuwAStZWSOwAFBYZVlZLbADLCBFILAEJWFkILAFQ1BYsAUjQrAGI0IbISFZsAFgLbAELCMhIyEgZLEFYkIgsAYjQrEBCkNFY7EBCkOwAWBFY7ADKiEgsAZDIIogirABK7EwBSWwBCZRWGBQG2FSWVgjWSEgsEBTWLABKxshsEBZI7AAUFhlWS2wBSywB0MrsgACAENgQi2wBiywByNCIyCwACNCYbACYmawAWOwAWCwBSotsAcsICBFILALQ2O4BABiILAAUFiwQGBZZrABY2BEsAFgLbAILLIHCwBDRUIqIbIAAQBDYEItsAkssABDI0SyAAEAQ2BCLbAKLCAgRSCwASsjsABDsAQlYCBFiiNhIGQgsCBQWCGwABuwMFBYsCAbsEBZWSOwAFBYZVmwAyUjYUREsAFgLbALLCAgRSCwASsjsABDsAQlYCBFiiNhIGSwJFBYsAAbsEBZI7AAUFhlWbADJSNhRESwAWAtsAwsILAAI0KyCwoDRVghGyMhWSohLbANLLECAkWwZGFELbAOLLABYCAgsAxDSrAAUFggsAwjQlmwDUNKsABSWCCwDSNCWS2wDywgsBBiZrABYyC4BABjiiNhsA5DYCCKYCCwDiNCIy2wECxLVFixBGREWSSwDWUjeC2wESxLUVhLU1ixBGREWRshWSSwE2UjeC2wEiyxAA9DVVixDw9DsAFhQrAPK1mwAEOwAiVCsQwCJUKxDQIlQrABFiMgsAMlUFixAQBDYLAEJUKKiiCKI2GwDiohI7ABYSCKI2GwDiohG7EBAENgsAIlQrACJWGwDiohWbAMQ0ewDUNHYLACYiCwAFBYsEBgWWawAWMgsAtDY7gEAGIgsABQWLBAYFlmsAFjYLEAABMjRLABQ7AAPrIBAQFDYEItsBMsALEAAkVUWLAPI0IgRbALI0KwCiOwAWBCIGCwAWG1EBABAA4AQkKKYLESBiuwcisbIlktsBQssQATKy2wFSyxARMrLbAWLLECEystsBcssQMTKy2wGCyxBBMrLbAZLLEFEystsBossQYTKy2wGyyxBxMrLbAcLLEIEystsB0ssQkTKy2wHiwAsA0rsQACRVRYsA8jQiBFsAsjQrAKI7ABYEIgYLABYbUQEAEADgBCQopgsRIGK7ByKxsiWS2wHyyxAB4rLbAgLLEBHistsCEssQIeKy2wIiyxAx4rLbAjLLEEHistsCQssQUeKy2wJSyxBh4rLbAmLLEHHistsCcssQgeKy2wKCyxCR4rLbApLCA8sAFgLbAqLCBgsBBgIEMjsAFgQ7ACJWGwAWCwKSohLbArLLAqK7AqKi2wLCwgIEcgILALQ2O4BABiILAAUFiwQGBZZrABY2AjYTgjIIpVWCBHICCwC0NjuAQAYiCwAFBYsEBgWWawAWNgI2E4GyFZLbAtLACxAAJFVFiwARawLCqwARUwGyJZLbAuLACwDSuxAAJFVFiwARawLCqwARUwGyJZLbAvLCA1sAFgLbAwLACwAUVjuAQAYiCwAFBYsEBgWWawAWOwASuwC0NjuAQAYiCwAFBYsEBgWWawAWOwASuwABa0AAAAAABEPiM4sS8BFSotsDEsIDwgRyCwC0NjuAQAYiCwAFBYsEBgWWawAWNgsABDYTgtsDIsLhc8LbAzLCA8IEcgsAtDY7gEAGIgsABQWLBAYFlmsAFjYLAAQ2GwAUNjOC2wNCyxAgAWJSAuIEewACNCsAIlSYqKRyNHI2EgWGIbIVmwASNCsjMBARUUKi2wNSywABawBCWwBCVHI0cjYbAJQytlii4jICA8ijgtsDYssAAWsAQlsAQlIC5HI0cjYSCwBCNCsAlDKyCwYFBYILBAUVizAiADIBuzAiYDGllCQiMgsAhDIIojRyNHI2EjRmCwBEOwAmIgsABQWLBAYFlmsAFjYCCwASsgiophILACQ2BkI7ADQ2FkUFiwAkNhG7ADQ2BZsAMlsAJiILAAUFiwQGBZZrABY2EjICCwBCYjRmE4GyOwCENGsAIlsAhDRyNHI2FgILAEQ7ACYiCwAFBYsEBgWWawAWNgIyCwASsjsARDYLABK7AFJWGwBSWwAmIgsABQWLBAYFlmsAFjsAQmYSCwBCVgZCOwAyVgZFBYIRsjIVkjICCwBCYjRmE4WS2wNyywABYgICCwBSYgLkcjRyNhIzw4LbA4LLAAFiCwCCNCICAgRiNHsAErI2E4LbA5LLAAFrADJbACJUcjRyNhsABUWC4gPCMhG7ACJbACJUcjRyNhILAFJbAEJUcjRyNhsAYlsAUlSbACJWG5CAAIAGNjIyBYYhshWWO4BABiILAAUFiwQGBZZrABY2AjLiMgIDyKOCMhWS2wOiywABYgsAhDIC5HI0cjYSBgsCBgZrACYiCwAFBYsEBgWWawAWMjICA8ijgtsDssIyAuRrACJUZSWCA8WS6xKwEUKy2wPCwjIC5GsAIlRlBYIDxZLrErARQrLbA9LCMgLkawAiVGUlggPFkjIC5GsAIlRlBYIDxZLrErARQrLbA+LLA1KyMgLkawAiVGUlggPFkusSsBFCstsD8ssDYriiAgPLAEI0KKOCMgLkawAiVGUlggPFkusSsBFCuwBEMusCsrLbBALLAAFrAEJbAEJiAuRyNHI2GwCUMrIyA8IC4jOLErARQrLbBBLLEIBCVCsAAWsAQlsAQlIC5HI0cjYSCwBCNCsAlDKyCwYFBYILBAUVizAiADIBuzAiYDGllCQiMgR7AEQ7ACYiCwAFBYsEBgWWawAWNgILABKyCKimEgsAJDYGQjsANDYWRQWLACQ2EbsANDYFmwAyWwAmIgsABQWLBAYFlmsAFjYbACJUZhOCMgPCM4GyEgIEYjR7ABKyNhOCFZsSsBFCstsEIssDUrLrErARQrLbBDLLA2KyEjICA8sAQjQiM4sSsBFCuwBEMusCsrLbBELLAAFSBHsAAjQrIAAQEVFBMusDEqLbBFLLAAFSBHsAAjQrIAAQEVFBMusDEqLbBGLLEAARQTsDIqLbBHLLA0Ki2wSCywABZFIyAuIEaKI2E4sSsBFCstsEkssAgjQrBIKy2wSiyyAABBKy2wSyyyAAFBKy2wTCyyAQBBKy2wTSyyAQFBKy2wTiyyAABCKy2wTyyyAAFCKy2wUCyyAQBCKy2wUSyyAQFCKy2wUiyyAAA+Ky2wUyyyAAE+Ky2wVCyyAQA+Ky2wVSyyAQE+Ky2wViyyAABAKy2wVyyyAAFAKy2wWCyyAQBAKy2wWSyyAQFAKy2wWiyyAABDKy2wWyyyAAFDKy2wXCyyAQBDKy2wXSyyAQFDKy2wXiyyAAA/Ky2wXyyyAAE/Ky2wYCyyAQA/Ky2wYSyyAQE/Ky2wYiywNysusSsBFCstsGMssDcrsDsrLbBkLLA3K7A8Ky2wZSywABawNyuwPSstsGYssDgrLrErARQrLbBnLLA4K7A7Ky2waCywOCuwPCstsGkssDgrsD0rLbBqLLA5Ky6xKwEUKy2wayywOSuwOystsGwssDkrsDwrLbBtLLA5K7A9Ky2wbiywOisusSsBFCstsG8ssDorsDsrLbBwLLA6K7A8Ky2wcSywOiuwPSstsHIsswkEAgNFWCEbIyFZQiuwCGWwAyRQeLABFTAtAEu4AMhSWLEBAY5ZsAG5CAAIAGNwsQAFQrIAAQAqsQAFQrMKAgEIKrEABUKzDgABCCqxAAZCugLAAAEACSqxAAdCugBAAAEACSqxAwBEsSQBiFFYsECIWLEDZESxJgGIUVi6CIAAAQRAiGNUWLEDAERZWVlZswwCAQwquAH/hbAEjbECAEQAAA==') format('truetype');
+}
+/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
+/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
+/*
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ @font-face {
+ font-family: 'ifont';
+ src: url('../font/ifont.svg?21447335#ifont') format('svg');
+ }
+}
+*/
+
+ [class^="icon-"]:before, [class*=" icon-"]:before {
+ font-family: "ifont";
+ font-style: normal;
+ font-weight: normal;
+ speak: none;
+
+ display: inline-block;
+ text-decoration: inherit;
+ width: 1em;
+ margin-right: .2em;
+ text-align: center;
+ /* opacity: .8; */
+
+ /* For safety - reset parent styles, that can break glyph codes*/
+ font-variant: normal;
+ text-transform: none;
+
+ /* fix buttons height, for twitter bootstrap */
+ line-height: 1em;
+
+ /* Animation center compensation - margins should be symmetric */
+ /* remove if not needed */
+ margin-left: .2em;
+
+ /* you can be more comfortable with increased icons size */
+ /* font-size: 120%; */
+
+ /* Uncomment for 3D effect */
+ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
+}
+.icon-dashboard:before { content: '\e800'; } /* '' */
+.icon-user:before { content: '\e801'; } /* '' */
+.icon-users:before { content: '\e802'; } /* '' */
+.icon-ok:before { content: '\e803'; } /* '' */
+.icon-cancel:before { content: '\e804'; } /* '' */
+.icon-plus:before { content: '\e805'; } /* '' */
+.icon-minus:before { content: '\e806'; } /* '' */
+.icon-folder-empty:before { content: '\e807'; } /* '' */
+.icon-download:before { content: '\e808'; } /* '' */
+.icon-upload:before { content: '\e809'; } /* '' */
+.icon-git:before { content: '\e80a'; } /* '' */
+.icon-cubes:before { content: '\e80b'; } /* '' */
+.icon-database:before { content: '\e80c'; } /* '' */
+.icon-gauge:before { content: '\e80d'; } /* '' */
+.icon-sitemap:before { content: '\e80e'; } /* '' */
+.icon-sort-name-up:before { content: '\e80f'; } /* '' */
+.icon-sort-name-down:before { content: '\e810'; } /* '' */
+.icon-megaphone:before { content: '\e811'; } /* '' */
+.icon-bug:before { content: '\e812'; } /* '' */
+.icon-tasks:before { content: '\e813'; } /* '' */
+.icon-filter:before { content: '\e814'; } /* '' */
+.icon-off:before { content: '\e815'; } /* '' */
+.icon-book:before { content: '\e816'; } /* '' */
+.icon-paste:before { content: '\e817'; } /* '' */
+.icon-scissors:before { content: '\e818'; } /* '' */
+.icon-globe:before { content: '\e819'; } /* '' */
+.icon-cloud:before { content: '\e81a'; } /* '' */
+.icon-flash:before { content: '\e81b'; } /* '' */
+.icon-barchart:before { content: '\e81c'; } /* '' */
+.icon-down-dir:before { content: '\e81d'; } /* '' */
+.icon-up-dir:before { content: '\e81e'; } /* '' */
+.icon-left-dir:before { content: '\e81f'; } /* '' */
+.icon-right-dir:before { content: '\e820'; } /* '' */
+.icon-down-open:before { content: '\e821'; } /* '' */
+.icon-right-open:before { content: '\e822'; } /* '' */
+.icon-up-open:before { content: '\e823'; } /* '' */
+.icon-left-open:before { content: '\e824'; } /* '' */
+.icon-up-big:before { content: '\e825'; } /* '' */
+.icon-right-big:before { content: '\e826'; } /* '' */
+.icon-left-big:before { content: '\e827'; } /* '' */
+.icon-down-big:before { content: '\e828'; } /* '' */
+.icon-resize-full-alt:before { content: '\e829'; } /* '' */
+.icon-resize-full:before { content: '\e82a'; } /* '' */
+.icon-resize-small:before { content: '\e82b'; } /* '' */
+.icon-move:before { content: '\e82c'; } /* '' */
+.icon-resize-horizontal:before { content: '\e82d'; } /* '' */
+.icon-resize-vertical:before { content: '\e82e'; } /* '' */
+.icon-zoom-in:before { content: '\e82f'; } /* '' */
+.icon-block:before { content: '\e830'; } /* '' */
+.icon-zoom-out:before { content: '\e831'; } /* '' */
+.icon-lightbulb:before { content: '\e832'; } /* '' */
+.icon-clock:before { content: '\e833'; } /* '' */
+.icon-volume-up:before { content: '\e834'; } /* '' */
+.icon-volume-down:before { content: '\e835'; } /* '' */
+.icon-volume-off:before { content: '\e836'; } /* '' */
+.icon-mute:before { content: '\e837'; } /* '' */
+.icon-mic:before { content: '\e838'; } /* '' */
+.icon-endtime:before { content: '\e839'; } /* '' */
+.icon-starttime:before { content: '\e83a'; } /* '' */
+.icon-calendar-empty:before { content: '\e83b'; } /* '' */
+.icon-calendar:before { content: '\e83c'; } /* '' */
+.icon-wrench:before { content: '\e83d'; } /* '' */
+.icon-sliders:before { content: '\e83e'; } /* '' */
+.icon-services:before { content: '\e83f'; } /* '' */
+.icon-service:before { content: '\e840'; } /* '' */
+.icon-phone:before { content: '\e841'; } /* '' */
+.icon-file-pdf:before { content: '\e842'; } /* '' */
+.icon-file-word:before { content: '\e843'; } /* '' */
+.icon-file-excel:before { content: '\e844'; } /* '' */
+.icon-doc-text:before { content: '\e845'; } /* '' */
+.icon-trash:before { content: '\e846'; } /* '' */
+.icon-comment-empty:before { content: '\e847'; } /* '' */
+.icon-comment:before { content: '\e848'; } /* '' */
+.icon-chat:before { content: '\e849'; } /* '' */
+.icon-chat-empty:before { content: '\e84a'; } /* '' */
+.icon-bell:before { content: '\e84b'; } /* '' */
+.icon-bell-alt:before { content: '\e84c'; } /* '' */
+.icon-attention-alt:before { content: '\e84d'; } /* '' */
+.icon-print:before { content: '\e84e'; } /* '' */
+.icon-edit:before { content: '\e84f'; } /* '' */
+.icon-forward:before { content: '\e850'; } /* '' */
+.icon-reply:before { content: '\e851'; } /* '' */
+.icon-reply-all:before { content: '\e852'; } /* '' */
+.icon-eye:before { content: '\e853'; } /* '' */
+.icon-tag:before { content: '\e854'; } /* '' */
+.icon-tags:before { content: '\e855'; } /* '' */
+.icon-lock-open-alt:before { content: '\e856'; } /* '' */
+.icon-lock-open:before { content: '\e857'; } /* '' */
+.icon-lock:before { content: '\e858'; } /* '' */
+.icon-home:before { content: '\e859'; } /* '' */
+.icon-info:before { content: '\e85a'; } /* '' */
+.icon-help:before { content: '\e85b'; } /* '' */
+.icon-search:before { content: '\e85c'; } /* '' */
+.icon-flapping:before { content: '\e85d'; } /* '' */
+.icon-rewind:before { content: '\e85e'; } /* '' */
+.icon-chart-line:before { content: '\e85f'; } /* '' */
+.icon-bell-off:before { content: '\e860'; } /* '' */
+.icon-bell-off-empty:before { content: '\e861'; } /* '' */
+.icon-plug:before { content: '\e862'; } /* '' */
+.icon-eye-off:before { content: '\e863'; } /* '' */
+.icon-arrows-cw:before { content: '\e864'; } /* '' */
+.icon-cw:before { content: '\e865'; } /* '' */
+.icon-host:before { content: '\e866'; } /* '' */
+.icon-thumbs-up:before { content: '\e867'; } /* '' */
+.icon-thumbs-down:before { content: '\e868'; } /* '' */
+.icon-spinner:before { content: '\e869'; } /* '' */
+.icon-attach:before { content: '\e86a'; } /* '' */
+.icon-keyboard:before { content: '\e86b'; } /* '' */
+.icon-menu:before { content: '\e86c'; } /* '' */
+.icon-wifi:before { content: '\e86d'; } /* '' */
+.icon-moon:before { content: '\e86e'; } /* '' */
+.icon-chart-pie:before { content: '\e86f'; } /* '' */
+.icon-chart-area:before { content: '\e870'; } /* '' */
+.icon-chart-bar:before { content: '\e871'; } /* '' */
+.icon-beaker:before { content: '\e872'; } /* '' */
+.icon-magic:before { content: '\e873'; } /* '' */
+.icon-spin6:before { content: '\e874'; } /* '' */
+.icon-down-small:before { content: '\e875'; } /* '' */
+.icon-left-small:before { content: '\e876'; } /* '' */
+.icon-right-small:before { content: '\e877'; } /* '' */
+.icon-up-small:before { content: '\e878'; } /* '' */
+.icon-pin:before { content: '\e879'; } /* '' */
+.icon-angle-double-left:before { content: '\e87a'; } /* '' */
+.icon-angle-double-right:before { content: '\e87b'; } /* '' */
+.icon-circle:before { content: '\e87c'; } /* '' */
+.icon-info-circled:before { content: '\e87d'; } /* '' */
+.icon-twitter:before { content: '\e87e'; } /* '' */
+.icon-facebook-squared:before { content: '\e87f'; } /* '' */
+.icon-gplus-squared:before { content: '\e880'; } /* '' */
+.icon-attention-circled:before { content: '\e881'; } /* '' */
+.icon-check:before { content: '\e883'; } /* '' */
+.icon-reschedule:before { content: '\e884'; } /* '' */
+.icon-warning-empty:before { content: '\e885'; } /* '' */
+.icon-th-list:before { content: '\f009'; } /* '' */
+.icon-th-thumb-empty:before { content: '\f00b'; } /* '' */
+.icon-github-circled:before { content: '\f09b'; } /* '' */
+.icon-angle-double-up:before { content: '\f102'; } /* '' */
+.icon-angle-double-down:before { content: '\f103'; } /* '' */
+.icon-angle-left:before { content: '\f104'; } /* '' */
+.icon-angle-right:before { content: '\f105'; } /* '' */
+.icon-angle-up:before { content: '\f106'; } /* '' */
+.icon-angle-down:before { content: '\f107'; } /* '' */
+.icon-history:before { content: '\f1da'; } /* '' */
+.icon-binoculars:before { content: '\f1e5'; } /* '' */ \ No newline at end of file
diff --git a/application/fonts/fontello-ifont/css/ifont-ie7-codes.css b/application/fonts/fontello-ifont/css/ifont-ie7-codes.css
new file mode 100644
index 0000000..df7974b
--- /dev/null
+++ b/application/fonts/fontello-ifont/css/ifont-ie7-codes.css
@@ -0,0 +1,145 @@
+
+.icon-dashboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
+.icon-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
+.icon-users { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
+.icon-ok { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
+.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
+.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
+.icon-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
+.icon-folder-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
+.icon-download { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
+.icon-upload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
+.icon-git { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
+.icon-cubes { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80b;&nbsp;'); }
+.icon-database { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80c;&nbsp;'); }
+.icon-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80d;&nbsp;'); }
+.icon-sitemap { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80e;&nbsp;'); }
+.icon-sort-name-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80f;&nbsp;'); }
+.icon-sort-name-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
+.icon-megaphone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe811;&nbsp;'); }
+.icon-bug { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe812;&nbsp;'); }
+.icon-tasks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); }
+.icon-filter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe814;&nbsp;'); }
+.icon-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe815;&nbsp;'); }
+.icon-book { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); }
+.icon-paste { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe817;&nbsp;'); }
+.icon-scissors { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); }
+.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe819;&nbsp;'); }
+.icon-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81a;&nbsp;'); }
+.icon-flash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81b;&nbsp;'); }
+.icon-barchart { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81c;&nbsp;'); }
+.icon-down-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81d;&nbsp;'); }
+.icon-up-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81e;&nbsp;'); }
+.icon-left-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81f;&nbsp;'); }
+.icon-right-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe820;&nbsp;'); }
+.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe821;&nbsp;'); }
+.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe822;&nbsp;'); }
+.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe823;&nbsp;'); }
+.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe824;&nbsp;'); }
+.icon-up-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe825;&nbsp;'); }
+.icon-right-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe826;&nbsp;'); }
+.icon-left-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe827;&nbsp;'); }
+.icon-down-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe828;&nbsp;'); }
+.icon-resize-full-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe829;&nbsp;'); }
+.icon-resize-full { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82a;&nbsp;'); }
+.icon-resize-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82b;&nbsp;'); }
+.icon-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82c;&nbsp;'); }
+.icon-resize-horizontal { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82d;&nbsp;'); }
+.icon-resize-vertical { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82e;&nbsp;'); }
+.icon-zoom-in { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82f;&nbsp;'); }
+.icon-block { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe830;&nbsp;'); }
+.icon-zoom-out { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe831;&nbsp;'); }
+.icon-lightbulb { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
+.icon-clock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe833;&nbsp;'); }
+.icon-volume-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
+.icon-volume-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe835;&nbsp;'); }
+.icon-volume-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe836;&nbsp;'); }
+.icon-mute { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe837;&nbsp;'); }
+.icon-mic { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe838;&nbsp;'); }
+.icon-endtime { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe839;&nbsp;'); }
+.icon-starttime { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83a;&nbsp;'); }
+.icon-calendar-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83b;&nbsp;'); }
+.icon-calendar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83c;&nbsp;'); }
+.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83d;&nbsp;'); }
+.icon-sliders { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83e;&nbsp;'); }
+.icon-services { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83f;&nbsp;'); }
+.icon-service { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe840;&nbsp;'); }
+.icon-phone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe841;&nbsp;'); }
+.icon-file-pdf { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe842;&nbsp;'); }
+.icon-file-word { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe843;&nbsp;'); }
+.icon-file-excel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe844;&nbsp;'); }
+.icon-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe845;&nbsp;'); }
+.icon-trash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe846;&nbsp;'); }
+.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe847;&nbsp;'); }
+.icon-comment { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe848;&nbsp;'); }
+.icon-chat { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe849;&nbsp;'); }
+.icon-chat-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84a;&nbsp;'); }
+.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84b;&nbsp;'); }
+.icon-bell-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84c;&nbsp;'); }
+.icon-attention-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84d;&nbsp;'); }
+.icon-print { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84e;&nbsp;'); }
+.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84f;&nbsp;'); }
+.icon-forward { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe850;&nbsp;'); }
+.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe851;&nbsp;'); }
+.icon-reply-all { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe852;&nbsp;'); }
+.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe853;&nbsp;'); }
+.icon-tag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe854;&nbsp;'); }
+.icon-tags { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe855;&nbsp;'); }
+.icon-lock-open-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe856;&nbsp;'); }
+.icon-lock-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe857;&nbsp;'); }
+.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe858;&nbsp;'); }
+.icon-home { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe859;&nbsp;'); }
+.icon-info { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85a;&nbsp;'); }
+.icon-help { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85b;&nbsp;'); }
+.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85c;&nbsp;'); }
+.icon-flapping { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85d;&nbsp;'); }
+.icon-rewind { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85e;&nbsp;'); }
+.icon-chart-line { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85f;&nbsp;'); }
+.icon-bell-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe860;&nbsp;'); }
+.icon-bell-off-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe861;&nbsp;'); }
+.icon-plug { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe862;&nbsp;'); }
+.icon-eye-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe863;&nbsp;'); }
+.icon-arrows-cw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe864;&nbsp;'); }
+.icon-cw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe865;&nbsp;'); }
+.icon-host { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe866;&nbsp;'); }
+.icon-thumbs-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe867;&nbsp;'); }
+.icon-thumbs-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe868;&nbsp;'); }
+.icon-spinner { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe869;&nbsp;'); }
+.icon-attach { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86a;&nbsp;'); }
+.icon-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86b;&nbsp;'); }
+.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86c;&nbsp;'); }
+.icon-wifi { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86d;&nbsp;'); }
+.icon-moon { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86e;&nbsp;'); }
+.icon-chart-pie { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86f;&nbsp;'); }
+.icon-chart-area { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe870;&nbsp;'); }
+.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe871;&nbsp;'); }
+.icon-beaker { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe872;&nbsp;'); }
+.icon-magic { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe873;&nbsp;'); }
+.icon-spin6 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe874;&nbsp;'); }
+.icon-down-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe875;&nbsp;'); }
+.icon-left-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe876;&nbsp;'); }
+.icon-right-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe877;&nbsp;'); }
+.icon-up-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe878;&nbsp;'); }
+.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe879;&nbsp;'); }
+.icon-angle-double-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87a;&nbsp;'); }
+.icon-angle-double-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87b;&nbsp;'); }
+.icon-circle { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87c;&nbsp;'); }
+.icon-info-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87d;&nbsp;'); }
+.icon-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87e;&nbsp;'); }
+.icon-facebook-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87f;&nbsp;'); }
+.icon-gplus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe880;&nbsp;'); }
+.icon-attention-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe881;&nbsp;'); }
+.icon-check { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe883;&nbsp;'); }
+.icon-reschedule { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe884;&nbsp;'); }
+.icon-warning-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe885;&nbsp;'); }
+.icon-th-list { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf009;&nbsp;'); }
+.icon-th-thumb-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf00b;&nbsp;'); }
+.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09b;&nbsp;'); }
+.icon-angle-double-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf102;&nbsp;'); }
+.icon-angle-double-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf103;&nbsp;'); }
+.icon-angle-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf104;&nbsp;'); }
+.icon-angle-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf105;&nbsp;'); }
+.icon-angle-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf106;&nbsp;'); }
+.icon-angle-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf107;&nbsp;'); }
+.icon-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1da;&nbsp;'); }
+.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); } \ No newline at end of file
diff --git a/application/fonts/fontello-ifont/css/ifont-ie7.css b/application/fonts/fontello-ifont/css/ifont-ie7.css
new file mode 100644
index 0000000..084c292
--- /dev/null
+++ b/application/fonts/fontello-ifont/css/ifont-ie7.css
@@ -0,0 +1,156 @@
+[class^="icon-"], [class*=" icon-"] {
+ font-family: 'ifont';
+ font-style: normal;
+ font-weight: normal;
+
+ /* fix buttons height */
+ line-height: 1em;
+
+ /* you can be more comfortable with increased icons size */
+ /* font-size: 120%; */
+}
+
+.icon-dashboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
+.icon-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
+.icon-users { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
+.icon-ok { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
+.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
+.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
+.icon-minus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
+.icon-folder-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
+.icon-download { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
+.icon-upload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
+.icon-git { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
+.icon-cubes { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80b;&nbsp;'); }
+.icon-database { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80c;&nbsp;'); }
+.icon-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80d;&nbsp;'); }
+.icon-sitemap { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80e;&nbsp;'); }
+.icon-sort-name-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80f;&nbsp;'); }
+.icon-sort-name-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
+.icon-megaphone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe811;&nbsp;'); }
+.icon-bug { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe812;&nbsp;'); }
+.icon-tasks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); }
+.icon-filter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe814;&nbsp;'); }
+.icon-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe815;&nbsp;'); }
+.icon-book { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); }
+.icon-paste { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe817;&nbsp;'); }
+.icon-scissors { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); }
+.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe819;&nbsp;'); }
+.icon-cloud { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81a;&nbsp;'); }
+.icon-flash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81b;&nbsp;'); }
+.icon-barchart { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81c;&nbsp;'); }
+.icon-down-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81d;&nbsp;'); }
+.icon-up-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81e;&nbsp;'); }
+.icon-left-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81f;&nbsp;'); }
+.icon-right-dir { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe820;&nbsp;'); }
+.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe821;&nbsp;'); }
+.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe822;&nbsp;'); }
+.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe823;&nbsp;'); }
+.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe824;&nbsp;'); }
+.icon-up-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe825;&nbsp;'); }
+.icon-right-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe826;&nbsp;'); }
+.icon-left-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe827;&nbsp;'); }
+.icon-down-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe828;&nbsp;'); }
+.icon-resize-full-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe829;&nbsp;'); }
+.icon-resize-full { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82a;&nbsp;'); }
+.icon-resize-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82b;&nbsp;'); }
+.icon-move { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82c;&nbsp;'); }
+.icon-resize-horizontal { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82d;&nbsp;'); }
+.icon-resize-vertical { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82e;&nbsp;'); }
+.icon-zoom-in { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe82f;&nbsp;'); }
+.icon-block { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe830;&nbsp;'); }
+.icon-zoom-out { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe831;&nbsp;'); }
+.icon-lightbulb { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
+.icon-clock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe833;&nbsp;'); }
+.icon-volume-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
+.icon-volume-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe835;&nbsp;'); }
+.icon-volume-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe836;&nbsp;'); }
+.icon-mute { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe837;&nbsp;'); }
+.icon-mic { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe838;&nbsp;'); }
+.icon-endtime { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe839;&nbsp;'); }
+.icon-starttime { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83a;&nbsp;'); }
+.icon-calendar-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83b;&nbsp;'); }
+.icon-calendar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83c;&nbsp;'); }
+.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83d;&nbsp;'); }
+.icon-sliders { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83e;&nbsp;'); }
+.icon-services { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe83f;&nbsp;'); }
+.icon-service { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe840;&nbsp;'); }
+.icon-phone { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe841;&nbsp;'); }
+.icon-file-pdf { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe842;&nbsp;'); }
+.icon-file-word { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe843;&nbsp;'); }
+.icon-file-excel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe844;&nbsp;'); }
+.icon-doc-text { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe845;&nbsp;'); }
+.icon-trash { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe846;&nbsp;'); }
+.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe847;&nbsp;'); }
+.icon-comment { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe848;&nbsp;'); }
+.icon-chat { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe849;&nbsp;'); }
+.icon-chat-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84a;&nbsp;'); }
+.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84b;&nbsp;'); }
+.icon-bell-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84c;&nbsp;'); }
+.icon-attention-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84d;&nbsp;'); }
+.icon-print { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84e;&nbsp;'); }
+.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe84f;&nbsp;'); }
+.icon-forward { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe850;&nbsp;'); }
+.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe851;&nbsp;'); }
+.icon-reply-all { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe852;&nbsp;'); }
+.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe853;&nbsp;'); }
+.icon-tag { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe854;&nbsp;'); }
+.icon-tags { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe855;&nbsp;'); }
+.icon-lock-open-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe856;&nbsp;'); }
+.icon-lock-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe857;&nbsp;'); }
+.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe858;&nbsp;'); }
+.icon-home { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe859;&nbsp;'); }
+.icon-info { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85a;&nbsp;'); }
+.icon-help { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85b;&nbsp;'); }
+.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85c;&nbsp;'); }
+.icon-flapping { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85d;&nbsp;'); }
+.icon-rewind { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85e;&nbsp;'); }
+.icon-chart-line { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe85f;&nbsp;'); }
+.icon-bell-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe860;&nbsp;'); }
+.icon-bell-off-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe861;&nbsp;'); }
+.icon-plug { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe862;&nbsp;'); }
+.icon-eye-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe863;&nbsp;'); }
+.icon-arrows-cw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe864;&nbsp;'); }
+.icon-cw { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe865;&nbsp;'); }
+.icon-host { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe866;&nbsp;'); }
+.icon-thumbs-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe867;&nbsp;'); }
+.icon-thumbs-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe868;&nbsp;'); }
+.icon-spinner { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe869;&nbsp;'); }
+.icon-attach { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86a;&nbsp;'); }
+.icon-keyboard { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86b;&nbsp;'); }
+.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86c;&nbsp;'); }
+.icon-wifi { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86d;&nbsp;'); }
+.icon-moon { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86e;&nbsp;'); }
+.icon-chart-pie { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe86f;&nbsp;'); }
+.icon-chart-area { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe870;&nbsp;'); }
+.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe871;&nbsp;'); }
+.icon-beaker { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe872;&nbsp;'); }
+.icon-magic { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe873;&nbsp;'); }
+.icon-spin6 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe874;&nbsp;'); }
+.icon-down-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe875;&nbsp;'); }
+.icon-left-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe876;&nbsp;'); }
+.icon-right-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe877;&nbsp;'); }
+.icon-up-small { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe878;&nbsp;'); }
+.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe879;&nbsp;'); }
+.icon-angle-double-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87a;&nbsp;'); }
+.icon-angle-double-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87b;&nbsp;'); }
+.icon-circle { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87c;&nbsp;'); }
+.icon-info-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87d;&nbsp;'); }
+.icon-twitter { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87e;&nbsp;'); }
+.icon-facebook-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe87f;&nbsp;'); }
+.icon-gplus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe880;&nbsp;'); }
+.icon-attention-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe881;&nbsp;'); }
+.icon-check { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe883;&nbsp;'); }
+.icon-reschedule { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe884;&nbsp;'); }
+.icon-warning-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe885;&nbsp;'); }
+.icon-th-list { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf009;&nbsp;'); }
+.icon-th-thumb-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf00b;&nbsp;'); }
+.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09b;&nbsp;'); }
+.icon-angle-double-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf102;&nbsp;'); }
+.icon-angle-double-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf103;&nbsp;'); }
+.icon-angle-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf104;&nbsp;'); }
+.icon-angle-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf105;&nbsp;'); }
+.icon-angle-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf106;&nbsp;'); }
+.icon-angle-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf107;&nbsp;'); }
+.icon-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1da;&nbsp;'); }
+.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); } \ No newline at end of file
diff --git a/application/fonts/fontello-ifont/css/ifont.css b/application/fonts/fontello-ifont/css/ifont.css
new file mode 100644
index 0000000..afabb48
--- /dev/null
+++ b/application/fonts/fontello-ifont/css/ifont.css
@@ -0,0 +1,201 @@
+@font-face {
+ font-family: 'ifont';
+ src: url('../font/ifont.eot?95568481');
+ src: url('../font/ifont.eot?95568481#iefix') format('embedded-opentype'),
+ url('../font/ifont.woff2?95568481') format('woff2'),
+ url('../font/ifont.woff?95568481') format('woff'),
+ url('../font/ifont.ttf?95568481') format('truetype'),
+ url('../font/ifont.svg?95568481#ifont') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
+/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
+/*
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ @font-face {
+ font-family: 'ifont';
+ src: url('../font/ifont.svg?95568481#ifont') format('svg');
+ }
+}
+*/
+
+ [class^="icon-"]:before, [class*=" icon-"]:before {
+ font-family: "ifont";
+ font-style: normal;
+ font-weight: normal;
+ speak: none;
+
+ display: inline-block;
+ text-decoration: inherit;
+ width: 1em;
+ margin-right: .2em;
+ text-align: center;
+ /* opacity: .8; */
+
+ /* For safety - reset parent styles, that can break glyph codes*/
+ font-variant: normal;
+ text-transform: none;
+
+ /* fix buttons height, for twitter bootstrap */
+ line-height: 1em;
+
+ /* Animation center compensation - margins should be symmetric */
+ /* remove if not needed */
+ margin-left: .2em;
+
+ /* you can be more comfortable with increased icons size */
+ /* font-size: 120%; */
+
+ /* Font smoothing. That was taken from TWBS */
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ /* Uncomment for 3D effect */
+ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
+}
+
+.icon-dashboard:before { content: '\e800'; } /* '' */
+.icon-user:before { content: '\e801'; } /* '' */
+.icon-users:before { content: '\e802'; } /* '' */
+.icon-ok:before { content: '\e803'; } /* '' */
+.icon-cancel:before { content: '\e804'; } /* '' */
+.icon-plus:before { content: '\e805'; } /* '' */
+.icon-minus:before { content: '\e806'; } /* '' */
+.icon-folder-empty:before { content: '\e807'; } /* '' */
+.icon-download:before { content: '\e808'; } /* '' */
+.icon-upload:before { content: '\e809'; } /* '' */
+.icon-git:before { content: '\e80a'; } /* '' */
+.icon-cubes:before { content: '\e80b'; } /* '' */
+.icon-database:before { content: '\e80c'; } /* '' */
+.icon-gauge:before { content: '\e80d'; } /* '' */
+.icon-sitemap:before { content: '\e80e'; } /* '' */
+.icon-sort-name-up:before { content: '\e80f'; } /* '' */
+.icon-sort-name-down:before { content: '\e810'; } /* '' */
+.icon-megaphone:before { content: '\e811'; } /* '' */
+.icon-bug:before { content: '\e812'; } /* '' */
+.icon-tasks:before { content: '\e813'; } /* '' */
+.icon-filter:before { content: '\e814'; } /* '' */
+.icon-off:before { content: '\e815'; } /* '' */
+.icon-book:before { content: '\e816'; } /* '' */
+.icon-paste:before { content: '\e817'; } /* '' */
+.icon-scissors:before { content: '\e818'; } /* '' */
+.icon-globe:before { content: '\e819'; } /* '' */
+.icon-cloud:before { content: '\e81a'; } /* '' */
+.icon-flash:before { content: '\e81b'; } /* '' */
+.icon-barchart:before { content: '\e81c'; } /* '' */
+.icon-down-dir:before { content: '\e81d'; } /* '' */
+.icon-up-dir:before { content: '\e81e'; } /* '' */
+.icon-left-dir:before { content: '\e81f'; } /* '' */
+.icon-right-dir:before { content: '\e820'; } /* '' */
+.icon-down-open:before { content: '\e821'; } /* '' */
+.icon-right-open:before { content: '\e822'; } /* '' */
+.icon-up-open:before { content: '\e823'; } /* '' */
+.icon-left-open:before { content: '\e824'; } /* '' */
+.icon-up-big:before { content: '\e825'; } /* '' */
+.icon-right-big:before { content: '\e826'; } /* '' */
+.icon-left-big:before { content: '\e827'; } /* '' */
+.icon-down-big:before { content: '\e828'; } /* '' */
+.icon-resize-full-alt:before { content: '\e829'; } /* '' */
+.icon-resize-full:before { content: '\e82a'; } /* '' */
+.icon-resize-small:before { content: '\e82b'; } /* '' */
+.icon-move:before { content: '\e82c'; } /* '' */
+.icon-resize-horizontal:before { content: '\e82d'; } /* '' */
+.icon-resize-vertical:before { content: '\e82e'; } /* '' */
+.icon-zoom-in:before { content: '\e82f'; } /* '' */
+.icon-block:before { content: '\e830'; } /* '' */
+.icon-zoom-out:before { content: '\e831'; } /* '' */
+.icon-lightbulb:before { content: '\e832'; } /* '' */
+.icon-clock:before { content: '\e833'; } /* '' */
+.icon-volume-up:before { content: '\e834'; } /* '' */
+.icon-volume-down:before { content: '\e835'; } /* '' */
+.icon-volume-off:before { content: '\e836'; } /* '' */
+.icon-mute:before { content: '\e837'; } /* '' */
+.icon-mic:before { content: '\e838'; } /* '' */
+.icon-endtime:before { content: '\e839'; } /* '' */
+.icon-starttime:before { content: '\e83a'; } /* '' */
+.icon-calendar-empty:before { content: '\e83b'; } /* '' */
+.icon-calendar:before { content: '\e83c'; } /* '' */
+.icon-wrench:before { content: '\e83d'; } /* '' */
+.icon-sliders:before { content: '\e83e'; } /* '' */
+.icon-services:before { content: '\e83f'; } /* '' */
+.icon-service:before { content: '\e840'; } /* '' */
+.icon-phone:before { content: '\e841'; } /* '' */
+.icon-file-pdf:before { content: '\e842'; } /* '' */
+.icon-file-word:before { content: '\e843'; } /* '' */
+.icon-file-excel:before { content: '\e844'; } /* '' */
+.icon-doc-text:before { content: '\e845'; } /* '' */
+.icon-trash:before { content: '\e846'; } /* '' */
+.icon-comment-empty:before { content: '\e847'; } /* '' */
+.icon-comment:before { content: '\e848'; } /* '' */
+.icon-chat:before { content: '\e849'; } /* '' */
+.icon-chat-empty:before { content: '\e84a'; } /* '' */
+.icon-bell:before { content: '\e84b'; } /* '' */
+.icon-bell-alt:before { content: '\e84c'; } /* '' */
+.icon-attention-alt:before { content: '\e84d'; } /* '' */
+.icon-print:before { content: '\e84e'; } /* '' */
+.icon-edit:before { content: '\e84f'; } /* '' */
+.icon-forward:before { content: '\e850'; } /* '' */
+.icon-reply:before { content: '\e851'; } /* '' */
+.icon-reply-all:before { content: '\e852'; } /* '' */
+.icon-eye:before { content: '\e853'; } /* '' */
+.icon-tag:before { content: '\e854'; } /* '' */
+.icon-tags:before { content: '\e855'; } /* '' */
+.icon-lock-open-alt:before { content: '\e856'; } /* '' */
+.icon-lock-open:before { content: '\e857'; } /* '' */
+.icon-lock:before { content: '\e858'; } /* '' */
+.icon-home:before { content: '\e859'; } /* '' */
+.icon-info:before { content: '\e85a'; } /* '' */
+.icon-help:before { content: '\e85b'; } /* '' */
+.icon-search:before { content: '\e85c'; } /* '' */
+.icon-flapping:before { content: '\e85d'; } /* '' */
+.icon-rewind:before { content: '\e85e'; } /* '' */
+.icon-chart-line:before { content: '\e85f'; } /* '' */
+.icon-bell-off:before { content: '\e860'; } /* '' */
+.icon-bell-off-empty:before { content: '\e861'; } /* '' */
+.icon-plug:before { content: '\e862'; } /* '' */
+.icon-eye-off:before { content: '\e863'; } /* '' */
+.icon-arrows-cw:before { content: '\e864'; } /* '' */
+.icon-cw:before { content: '\e865'; } /* '' */
+.icon-host:before { content: '\e866'; } /* '' */
+.icon-thumbs-up:before { content: '\e867'; } /* '' */
+.icon-thumbs-down:before { content: '\e868'; } /* '' */
+.icon-spinner:before { content: '\e869'; } /* '' */
+.icon-attach:before { content: '\e86a'; } /* '' */
+.icon-keyboard:before { content: '\e86b'; } /* '' */
+.icon-menu:before { content: '\e86c'; } /* '' */
+.icon-wifi:before { content: '\e86d'; } /* '' */
+.icon-moon:before { content: '\e86e'; } /* '' */
+.icon-chart-pie:before { content: '\e86f'; } /* '' */
+.icon-chart-area:before { content: '\e870'; } /* '' */
+.icon-chart-bar:before { content: '\e871'; } /* '' */
+.icon-beaker:before { content: '\e872'; } /* '' */
+.icon-magic:before { content: '\e873'; } /* '' */
+.icon-spin6:before { content: '\e874'; } /* '' */
+.icon-down-small:before { content: '\e875'; } /* '' */
+.icon-left-small:before { content: '\e876'; } /* '' */
+.icon-right-small:before { content: '\e877'; } /* '' */
+.icon-up-small:before { content: '\e878'; } /* '' */
+.icon-pin:before { content: '\e879'; } /* '' */
+.icon-angle-double-left:before { content: '\e87a'; } /* '' */
+.icon-angle-double-right:before { content: '\e87b'; } /* '' */
+.icon-circle:before { content: '\e87c'; } /* '' */
+.icon-info-circled:before { content: '\e87d'; } /* '' */
+.icon-twitter:before { content: '\e87e'; } /* '' */
+.icon-facebook-squared:before { content: '\e87f'; } /* '' */
+.icon-gplus-squared:before { content: '\e880'; } /* '' */
+.icon-attention-circled:before { content: '\e881'; } /* '' */
+.icon-check:before { content: '\e883'; } /* '' */
+.icon-reschedule:before { content: '\e884'; } /* '' */
+.icon-warning-empty:before { content: '\e885'; } /* '' */
+.icon-th-list:before { content: '\f009'; } /* '' */
+.icon-th-thumb-empty:before { content: '\f00b'; } /* '' */
+.icon-github-circled:before { content: '\f09b'; } /* '' */
+.icon-angle-double-up:before { content: '\f102'; } /* '' */
+.icon-angle-double-down:before { content: '\f103'; } /* '' */
+.icon-angle-left:before { content: '\f104'; } /* '' */
+.icon-angle-right:before { content: '\f105'; } /* '' */
+.icon-angle-up:before { content: '\f106'; } /* '' */
+.icon-angle-down:before { content: '\f107'; } /* '' */
+.icon-history:before { content: '\f1da'; } /* '' */
+.icon-binoculars:before { content: '\f1e5'; } /* '' */ \ No newline at end of file
diff --git a/application/fonts/fontello-ifont/demo.html b/application/fonts/fontello-ifont/demo.html
new file mode 100644
index 0000000..c3a67d4
--- /dev/null
+++ b/application/fonts/fontello-ifont/demo.html
@@ -0,0 +1,519 @@
+<!DOCTYPE html>
+<html>
+ <head><!--[if lt IE 9]><script language="javascript" type="text/javascript" src="//html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
+ <meta charset="UTF-8"><style>/*
+ * Bootstrap v2.2.1
+ *
+ * Copyright 2012 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world @twitter by @mdo and @fat.
+ */
+.clearfix {
+ *zoom: 1;
+}
+.clearfix:before,
+.clearfix:after {
+ display: table;
+ content: "";
+ line-height: 0;
+}
+.clearfix:after {
+ clear: both;
+}
+html {
+ font-size: 100%;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+}
+a:focus {
+ outline: thin dotted #333;
+ outline: 5px auto -webkit-focus-ring-color;
+ outline-offset: -2px;
+}
+a:hover,
+a:active {
+ outline: 0;
+}
+button,
+input,
+select,
+textarea {
+ margin: 0;
+ font-size: 100%;
+ vertical-align: middle;
+}
+button,
+input {
+ *overflow: visible;
+ line-height: normal;
+}
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+}
+body {
+ margin: 0;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 20px;
+ color: #333;
+ background-color: #fff;
+}
+a {
+ color: #08c;
+ text-decoration: none;
+}
+a:hover {
+ color: #005580;
+ text-decoration: underline;
+}
+.row {
+ margin-left: -20px;
+ *zoom: 1;
+}
+.row:before,
+.row:after {
+ display: table;
+ content: "";
+ line-height: 0;
+}
+.row:after {
+ clear: both;
+}
+[class*="span"] {
+ float: left;
+ min-height: 1px;
+ margin-left: 20px;
+}
+.container,
+.navbar-static-top .container,
+.navbar-fixed-top .container,
+.navbar-fixed-bottom .container {
+ width: 940px;
+}
+.span12 {
+ width: 940px;
+}
+.span11 {
+ width: 860px;
+}
+.span10 {
+ width: 780px;
+}
+.span9 {
+ width: 700px;
+}
+.span8 {
+ width: 620px;
+}
+.span7 {
+ width: 540px;
+}
+.span6 {
+ width: 460px;
+}
+.span5 {
+ width: 380px;
+}
+.span4 {
+ width: 300px;
+}
+.span3 {
+ width: 220px;
+}
+.span2 {
+ width: 140px;
+}
+.span1 {
+ width: 60px;
+}
+[class*="span"].pull-right,
+.row-fluid [class*="span"].pull-right {
+ float: right;
+}
+.container {
+ margin-right: auto;
+ margin-left: auto;
+ *zoom: 1;
+}
+.container:before,
+.container:after {
+ display: table;
+ content: "";
+ line-height: 0;
+}
+.container:after {
+ clear: both;
+}
+p {
+ margin: 0 0 10px;
+}
+.lead {
+ margin-bottom: 20px;
+ font-size: 21px;
+ font-weight: 200;
+ line-height: 30px;
+}
+small {
+ font-size: 85%;
+}
+h1 {
+ margin: 10px 0;
+ font-family: inherit;
+ font-weight: bold;
+ line-height: 20px;
+ color: inherit;
+ text-rendering: optimizelegibility;
+}
+h1 small {
+ font-weight: normal;
+ line-height: 1;
+ color: #999;
+}
+h1 {
+ line-height: 40px;
+}
+h1 {
+ font-size: 38.5px;
+}
+h1 small {
+ font-size: 24.5px;
+}
+body {
+ margin-top: 90px;
+}
+.header {
+ position: fixed;
+ top: 0;
+ left: 50%;
+ margin-left: -480px;
+ background-color: #fff;
+ border-bottom: 1px solid #ddd;
+ padding-top: 10px;
+ z-index: 10;
+}
+.footer {
+ color: #ddd;
+ font-size: 12px;
+ text-align: center;
+ margin-top: 20px;
+}
+.footer a {
+ color: #ccc;
+ text-decoration: underline;
+}
+.the-icons {
+ font-size: 14px;
+ line-height: 24px;
+}
+.switch {
+ position: absolute;
+ right: 0;
+ bottom: 10px;
+ color: #666;
+}
+.switch input {
+ margin-right: 0.3em;
+}
+.codesOn .i-name {
+ display: none;
+}
+.codesOn .i-code {
+ display: inline;
+}
+.i-code {
+ display: none;
+}
+@font-face {
+ font-family: 'ifont';
+ src: url('./font/ifont.eot?64148803');
+ src: url('./font/ifont.eot?64148803#iefix') format('embedded-opentype'),
+ url('./font/ifont.woff?64148803') format('woff'),
+ url('./font/ifont.ttf?64148803') format('truetype'),
+ url('./font/ifont.svg?64148803#ifont') format('svg');
+ font-weight: normal;
+ font-style: normal;
+ }
+
+
+ .demo-icon
+ {
+ font-family: "ifont";
+ font-style: normal;
+ font-weight: normal;
+ speak: none;
+
+ display: inline-block;
+ text-decoration: inherit;
+ width: 1em;
+ margin-right: .2em;
+ text-align: center;
+ /* opacity: .8; */
+
+ /* For safety - reset parent styles, that can break glyph codes*/
+ font-variant: normal;
+ text-transform: none;
+
+ /* fix buttons height, for twitter bootstrap */
+ line-height: 1em;
+
+ /* Animation center compensation - margins should be symmetric */
+ /* remove if not needed */
+ margin-left: .2em;
+
+ /* You can be more comfortable with increased icons size */
+ /* font-size: 120%; */
+
+ /* Font smoothing. That was taken from TWBS */
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ /* Uncomment for 3D effect */
+ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
+ }
+ </style>
+ <link rel="stylesheet" href="css/animation.css"><!--[if IE 7]><link rel="stylesheet" href="css/" + font.fontname + "-ie7.css"><![endif]-->
+ <script>
+ function toggleCodes(on) {
+ var obj = document.getElementById('icons');
+
+ if (on) {
+ obj.className += ' codesOn';
+ } else {
+ obj.className = obj.className.replace(' codesOn', '');
+ }
+ }
+
+ </script>
+ </head>
+ <body>
+ <div class="container header">
+ <h1>ifont <small>font demo</small></h1>
+ <label class="switch">
+ <input type="checkbox" onclick="toggleCodes(this.checked)">show codes
+ </label>
+ </div>
+ <div class="container" id="icons">
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe800"><i class="demo-icon icon-dashboard">&#xe800;</i> <span class="i-name">icon-dashboard</span><span class="i-code">0xe800</span></div>
+ <div class="the-icons span3" title="Code: 0xe801"><i class="demo-icon icon-user">&#xe801;</i> <span class="i-name">icon-user</span><span class="i-code">0xe801</span></div>
+ <div class="the-icons span3" title="Code: 0xe802"><i class="demo-icon icon-users">&#xe802;</i> <span class="i-name">icon-users</span><span class="i-code">0xe802</span></div>
+ <div class="the-icons span3" title="Code: 0xe803"><i class="demo-icon icon-ok">&#xe803;</i> <span class="i-name">icon-ok</span><span class="i-code">0xe803</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe804"><i class="demo-icon icon-cancel">&#xe804;</i> <span class="i-name">icon-cancel</span><span class="i-code">0xe804</span></div>
+ <div class="the-icons span3" title="Code: 0xe805"><i class="demo-icon icon-plus">&#xe805;</i> <span class="i-name">icon-plus</span><span class="i-code">0xe805</span></div>
+ <div class="the-icons span3" title="Code: 0xe806"><i class="demo-icon icon-minus">&#xe806;</i> <span class="i-name">icon-minus</span><span class="i-code">0xe806</span></div>
+ <div class="the-icons span3" title="Code: 0xe807"><i class="demo-icon icon-folder-empty">&#xe807;</i> <span class="i-name">icon-folder-empty</span><span class="i-code">0xe807</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe808"><i class="demo-icon icon-download">&#xe808;</i> <span class="i-name">icon-download</span><span class="i-code">0xe808</span></div>
+ <div class="the-icons span3" title="Code: 0xe809"><i class="demo-icon icon-upload">&#xe809;</i> <span class="i-name">icon-upload</span><span class="i-code">0xe809</span></div>
+ <div class="the-icons span3" title="Code: 0xe80a"><i class="demo-icon icon-git">&#xe80a;</i> <span class="i-name">icon-git</span><span class="i-code">0xe80a</span></div>
+ <div class="the-icons span3" title="Code: 0xe80b"><i class="demo-icon icon-cubes">&#xe80b;</i> <span class="i-name">icon-cubes</span><span class="i-code">0xe80b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe80c"><i class="demo-icon icon-database">&#xe80c;</i> <span class="i-name">icon-database</span><span class="i-code">0xe80c</span></div>
+ <div class="the-icons span3" title="Code: 0xe80d"><i class="demo-icon icon-gauge">&#xe80d;</i> <span class="i-name">icon-gauge</span><span class="i-code">0xe80d</span></div>
+ <div class="the-icons span3" title="Code: 0xe80e"><i class="demo-icon icon-sitemap">&#xe80e;</i> <span class="i-name">icon-sitemap</span><span class="i-code">0xe80e</span></div>
+ <div class="the-icons span3" title="Code: 0xe80f"><i class="demo-icon icon-sort-name-up">&#xe80f;</i> <span class="i-name">icon-sort-name-up</span><span class="i-code">0xe80f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe810"><i class="demo-icon icon-sort-name-down">&#xe810;</i> <span class="i-name">icon-sort-name-down</span><span class="i-code">0xe810</span></div>
+ <div class="the-icons span3" title="Code: 0xe811"><i class="demo-icon icon-megaphone">&#xe811;</i> <span class="i-name">icon-megaphone</span><span class="i-code">0xe811</span></div>
+ <div class="the-icons span3" title="Code: 0xe812"><i class="demo-icon icon-bug">&#xe812;</i> <span class="i-name">icon-bug</span><span class="i-code">0xe812</span></div>
+ <div class="the-icons span3" title="Code: 0xe813"><i class="demo-icon icon-tasks">&#xe813;</i> <span class="i-name">icon-tasks</span><span class="i-code">0xe813</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe814"><i class="demo-icon icon-filter">&#xe814;</i> <span class="i-name">icon-filter</span><span class="i-code">0xe814</span></div>
+ <div class="the-icons span3" title="Code: 0xe815"><i class="demo-icon icon-off">&#xe815;</i> <span class="i-name">icon-off</span><span class="i-code">0xe815</span></div>
+ <div class="the-icons span3" title="Code: 0xe816"><i class="demo-icon icon-book">&#xe816;</i> <span class="i-name">icon-book</span><span class="i-code">0xe816</span></div>
+ <div class="the-icons span3" title="Code: 0xe817"><i class="demo-icon icon-paste">&#xe817;</i> <span class="i-name">icon-paste</span><span class="i-code">0xe817</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe818"><i class="demo-icon icon-scissors">&#xe818;</i> <span class="i-name">icon-scissors</span><span class="i-code">0xe818</span></div>
+ <div class="the-icons span3" title="Code: 0xe819"><i class="demo-icon icon-globe">&#xe819;</i> <span class="i-name">icon-globe</span><span class="i-code">0xe819</span></div>
+ <div class="the-icons span3" title="Code: 0xe81a"><i class="demo-icon icon-cloud">&#xe81a;</i> <span class="i-name">icon-cloud</span><span class="i-code">0xe81a</span></div>
+ <div class="the-icons span3" title="Code: 0xe81b"><i class="demo-icon icon-flash">&#xe81b;</i> <span class="i-name">icon-flash</span><span class="i-code">0xe81b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe81c"><i class="demo-icon icon-barchart">&#xe81c;</i> <span class="i-name">icon-barchart</span><span class="i-code">0xe81c</span></div>
+ <div class="the-icons span3" title="Code: 0xe81d"><i class="demo-icon icon-down-dir">&#xe81d;</i> <span class="i-name">icon-down-dir</span><span class="i-code">0xe81d</span></div>
+ <div class="the-icons span3" title="Code: 0xe81e"><i class="demo-icon icon-up-dir">&#xe81e;</i> <span class="i-name">icon-up-dir</span><span class="i-code">0xe81e</span></div>
+ <div class="the-icons span3" title="Code: 0xe81f"><i class="demo-icon icon-left-dir">&#xe81f;</i> <span class="i-name">icon-left-dir</span><span class="i-code">0xe81f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe820"><i class="demo-icon icon-right-dir">&#xe820;</i> <span class="i-name">icon-right-dir</span><span class="i-code">0xe820</span></div>
+ <div class="the-icons span3" title="Code: 0xe821"><i class="demo-icon icon-down-open">&#xe821;</i> <span class="i-name">icon-down-open</span><span class="i-code">0xe821</span></div>
+ <div class="the-icons span3" title="Code: 0xe822"><i class="demo-icon icon-right-open">&#xe822;</i> <span class="i-name">icon-right-open</span><span class="i-code">0xe822</span></div>
+ <div class="the-icons span3" title="Code: 0xe823"><i class="demo-icon icon-up-open">&#xe823;</i> <span class="i-name">icon-up-open</span><span class="i-code">0xe823</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe824"><i class="demo-icon icon-left-open">&#xe824;</i> <span class="i-name">icon-left-open</span><span class="i-code">0xe824</span></div>
+ <div class="the-icons span3" title="Code: 0xe825"><i class="demo-icon icon-up-big">&#xe825;</i> <span class="i-name">icon-up-big</span><span class="i-code">0xe825</span></div>
+ <div class="the-icons span3" title="Code: 0xe826"><i class="demo-icon icon-right-big">&#xe826;</i> <span class="i-name">icon-right-big</span><span class="i-code">0xe826</span></div>
+ <div class="the-icons span3" title="Code: 0xe827"><i class="demo-icon icon-left-big">&#xe827;</i> <span class="i-name">icon-left-big</span><span class="i-code">0xe827</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe828"><i class="demo-icon icon-down-big">&#xe828;</i> <span class="i-name">icon-down-big</span><span class="i-code">0xe828</span></div>
+ <div class="the-icons span3" title="Code: 0xe829"><i class="demo-icon icon-resize-full-alt">&#xe829;</i> <span class="i-name">icon-resize-full-alt</span><span class="i-code">0xe829</span></div>
+ <div class="the-icons span3" title="Code: 0xe82a"><i class="demo-icon icon-resize-full">&#xe82a;</i> <span class="i-name">icon-resize-full</span><span class="i-code">0xe82a</span></div>
+ <div class="the-icons span3" title="Code: 0xe82b"><i class="demo-icon icon-resize-small">&#xe82b;</i> <span class="i-name">icon-resize-small</span><span class="i-code">0xe82b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe82c"><i class="demo-icon icon-move">&#xe82c;</i> <span class="i-name">icon-move</span><span class="i-code">0xe82c</span></div>
+ <div class="the-icons span3" title="Code: 0xe82d"><i class="demo-icon icon-resize-horizontal">&#xe82d;</i> <span class="i-name">icon-resize-horizontal</span><span class="i-code">0xe82d</span></div>
+ <div class="the-icons span3" title="Code: 0xe82e"><i class="demo-icon icon-resize-vertical">&#xe82e;</i> <span class="i-name">icon-resize-vertical</span><span class="i-code">0xe82e</span></div>
+ <div class="the-icons span3" title="Code: 0xe82f"><i class="demo-icon icon-zoom-in">&#xe82f;</i> <span class="i-name">icon-zoom-in</span><span class="i-code">0xe82f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe830"><i class="demo-icon icon-block">&#xe830;</i> <span class="i-name">icon-block</span><span class="i-code">0xe830</span></div>
+ <div class="the-icons span3" title="Code: 0xe831"><i class="demo-icon icon-zoom-out">&#xe831;</i> <span class="i-name">icon-zoom-out</span><span class="i-code">0xe831</span></div>
+ <div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-lightbulb">&#xe832;</i> <span class="i-name">icon-lightbulb</span><span class="i-code">0xe832</span></div>
+ <div class="the-icons span3" title="Code: 0xe833"><i class="demo-icon icon-clock">&#xe833;</i> <span class="i-name">icon-clock</span><span class="i-code">0xe833</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-volume-up">&#xe834;</i> <span class="i-name">icon-volume-up</span><span class="i-code">0xe834</span></div>
+ <div class="the-icons span3" title="Code: 0xe835"><i class="demo-icon icon-volume-down">&#xe835;</i> <span class="i-name">icon-volume-down</span><span class="i-code">0xe835</span></div>
+ <div class="the-icons span3" title="Code: 0xe836"><i class="demo-icon icon-volume-off">&#xe836;</i> <span class="i-name">icon-volume-off</span><span class="i-code">0xe836</span></div>
+ <div class="the-icons span3" title="Code: 0xe837"><i class="demo-icon icon-mute">&#xe837;</i> <span class="i-name">icon-mute</span><span class="i-code">0xe837</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe838"><i class="demo-icon icon-mic">&#xe838;</i> <span class="i-name">icon-mic</span><span class="i-code">0xe838</span></div>
+ <div class="the-icons span3" title="Code: 0xe839"><i class="demo-icon icon-endtime">&#xe839;</i> <span class="i-name">icon-endtime</span><span class="i-code">0xe839</span></div>
+ <div class="the-icons span3" title="Code: 0xe83a"><i class="demo-icon icon-starttime">&#xe83a;</i> <span class="i-name">icon-starttime</span><span class="i-code">0xe83a</span></div>
+ <div class="the-icons span3" title="Code: 0xe83b"><i class="demo-icon icon-calendar-empty">&#xe83b;</i> <span class="i-name">icon-calendar-empty</span><span class="i-code">0xe83b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe83c"><i class="demo-icon icon-calendar">&#xe83c;</i> <span class="i-name">icon-calendar</span><span class="i-code">0xe83c</span></div>
+ <div class="the-icons span3" title="Code: 0xe83d"><i class="demo-icon icon-wrench">&#xe83d;</i> <span class="i-name">icon-wrench</span><span class="i-code">0xe83d</span></div>
+ <div class="the-icons span3" title="Code: 0xe83e"><i class="demo-icon icon-sliders">&#xe83e;</i> <span class="i-name">icon-sliders</span><span class="i-code">0xe83e</span></div>
+ <div class="the-icons span3" title="Code: 0xe83f"><i class="demo-icon icon-services">&#xe83f;</i> <span class="i-name">icon-services</span><span class="i-code">0xe83f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe840"><i class="demo-icon icon-service">&#xe840;</i> <span class="i-name">icon-service</span><span class="i-code">0xe840</span></div>
+ <div class="the-icons span3" title="Code: 0xe841"><i class="demo-icon icon-phone">&#xe841;</i> <span class="i-name">icon-phone</span><span class="i-code">0xe841</span></div>
+ <div class="the-icons span3" title="Code: 0xe842"><i class="demo-icon icon-file-pdf">&#xe842;</i> <span class="i-name">icon-file-pdf</span><span class="i-code">0xe842</span></div>
+ <div class="the-icons span3" title="Code: 0xe843"><i class="demo-icon icon-file-word">&#xe843;</i> <span class="i-name">icon-file-word</span><span class="i-code">0xe843</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe844"><i class="demo-icon icon-file-excel">&#xe844;</i> <span class="i-name">icon-file-excel</span><span class="i-code">0xe844</span></div>
+ <div class="the-icons span3" title="Code: 0xe845"><i class="demo-icon icon-doc-text">&#xe845;</i> <span class="i-name">icon-doc-text</span><span class="i-code">0xe845</span></div>
+ <div class="the-icons span3" title="Code: 0xe846"><i class="demo-icon icon-trash">&#xe846;</i> <span class="i-name">icon-trash</span><span class="i-code">0xe846</span></div>
+ <div class="the-icons span3" title="Code: 0xe847"><i class="demo-icon icon-comment-empty">&#xe847;</i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xe847</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe848"><i class="demo-icon icon-comment">&#xe848;</i> <span class="i-name">icon-comment</span><span class="i-code">0xe848</span></div>
+ <div class="the-icons span3" title="Code: 0xe849"><i class="demo-icon icon-chat">&#xe849;</i> <span class="i-name">icon-chat</span><span class="i-code">0xe849</span></div>
+ <div class="the-icons span3" title="Code: 0xe84a"><i class="demo-icon icon-chat-empty">&#xe84a;</i> <span class="i-name">icon-chat-empty</span><span class="i-code">0xe84a</span></div>
+ <div class="the-icons span3" title="Code: 0xe84b"><i class="demo-icon icon-bell">&#xe84b;</i> <span class="i-name">icon-bell</span><span class="i-code">0xe84b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe84c"><i class="demo-icon icon-bell-alt">&#xe84c;</i> <span class="i-name">icon-bell-alt</span><span class="i-code">0xe84c</span></div>
+ <div class="the-icons span3" title="Code: 0xe84d"><i class="demo-icon icon-attention-alt">&#xe84d;</i> <span class="i-name">icon-attention-alt</span><span class="i-code">0xe84d</span></div>
+ <div class="the-icons span3" title="Code: 0xe84e"><i class="demo-icon icon-print">&#xe84e;</i> <span class="i-name">icon-print</span><span class="i-code">0xe84e</span></div>
+ <div class="the-icons span3" title="Code: 0xe84f"><i class="demo-icon icon-edit">&#xe84f;</i> <span class="i-name">icon-edit</span><span class="i-code">0xe84f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe850"><i class="demo-icon icon-forward">&#xe850;</i> <span class="i-name">icon-forward</span><span class="i-code">0xe850</span></div>
+ <div class="the-icons span3" title="Code: 0xe851"><i class="demo-icon icon-reply">&#xe851;</i> <span class="i-name">icon-reply</span><span class="i-code">0xe851</span></div>
+ <div class="the-icons span3" title="Code: 0xe852"><i class="demo-icon icon-reply-all">&#xe852;</i> <span class="i-name">icon-reply-all</span><span class="i-code">0xe852</span></div>
+ <div class="the-icons span3" title="Code: 0xe853"><i class="demo-icon icon-eye">&#xe853;</i> <span class="i-name">icon-eye</span><span class="i-code">0xe853</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe854"><i class="demo-icon icon-tag">&#xe854;</i> <span class="i-name">icon-tag</span><span class="i-code">0xe854</span></div>
+ <div class="the-icons span3" title="Code: 0xe855"><i class="demo-icon icon-tags">&#xe855;</i> <span class="i-name">icon-tags</span><span class="i-code">0xe855</span></div>
+ <div class="the-icons span3" title="Code: 0xe856"><i class="demo-icon icon-lock-open-alt">&#xe856;</i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xe856</span></div>
+ <div class="the-icons span3" title="Code: 0xe857"><i class="demo-icon icon-lock-open">&#xe857;</i> <span class="i-name">icon-lock-open</span><span class="i-code">0xe857</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe858"><i class="demo-icon icon-lock">&#xe858;</i> <span class="i-name">icon-lock</span><span class="i-code">0xe858</span></div>
+ <div class="the-icons span3" title="Code: 0xe859"><i class="demo-icon icon-home">&#xe859;</i> <span class="i-name">icon-home</span><span class="i-code">0xe859</span></div>
+ <div class="the-icons span3" title="Code: 0xe85a"><i class="demo-icon icon-info">&#xe85a;</i> <span class="i-name">icon-info</span><span class="i-code">0xe85a</span></div>
+ <div class="the-icons span3" title="Code: 0xe85b"><i class="demo-icon icon-help">&#xe85b;</i> <span class="i-name">icon-help</span><span class="i-code">0xe85b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe85c"><i class="demo-icon icon-search">&#xe85c;</i> <span class="i-name">icon-search</span><span class="i-code">0xe85c</span></div>
+ <div class="the-icons span3" title="Code: 0xe85d"><i class="demo-icon icon-flapping">&#xe85d;</i> <span class="i-name">icon-flapping</span><span class="i-code">0xe85d</span></div>
+ <div class="the-icons span3" title="Code: 0xe85e"><i class="demo-icon icon-rewind">&#xe85e;</i> <span class="i-name">icon-rewind</span><span class="i-code">0xe85e</span></div>
+ <div class="the-icons span3" title="Code: 0xe85f"><i class="demo-icon icon-chart-line">&#xe85f;</i> <span class="i-name">icon-chart-line</span><span class="i-code">0xe85f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe860"><i class="demo-icon icon-bell-off">&#xe860;</i> <span class="i-name">icon-bell-off</span><span class="i-code">0xe860</span></div>
+ <div class="the-icons span3" title="Code: 0xe861"><i class="demo-icon icon-bell-off-empty">&#xe861;</i> <span class="i-name">icon-bell-off-empty</span><span class="i-code">0xe861</span></div>
+ <div class="the-icons span3" title="Code: 0xe862"><i class="demo-icon icon-plug">&#xe862;</i> <span class="i-name">icon-plug</span><span class="i-code">0xe862</span></div>
+ <div class="the-icons span3" title="Code: 0xe863"><i class="demo-icon icon-eye-off">&#xe863;</i> <span class="i-name">icon-eye-off</span><span class="i-code">0xe863</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe864"><i class="demo-icon icon-arrows-cw">&#xe864;</i> <span class="i-name">icon-arrows-cw</span><span class="i-code">0xe864</span></div>
+ <div class="the-icons span3" title="Code: 0xe865"><i class="demo-icon icon-cw">&#xe865;</i> <span class="i-name">icon-cw</span><span class="i-code">0xe865</span></div>
+ <div class="the-icons span3" title="Code: 0xe866"><i class="demo-icon icon-host">&#xe866;</i> <span class="i-name">icon-host</span><span class="i-code">0xe866</span></div>
+ <div class="the-icons span3" title="Code: 0xe867"><i class="demo-icon icon-thumbs-up">&#xe867;</i> <span class="i-name">icon-thumbs-up</span><span class="i-code">0xe867</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe868"><i class="demo-icon icon-thumbs-down">&#xe868;</i> <span class="i-name">icon-thumbs-down</span><span class="i-code">0xe868</span></div>
+ <div class="the-icons span3" title="Code: 0xe869"><i class="demo-icon icon-spinner">&#xe869;</i> <span class="i-name">icon-spinner</span><span class="i-code">0xe869</span></div>
+ <div class="the-icons span3" title="Code: 0xe86a"><i class="demo-icon icon-attach">&#xe86a;</i> <span class="i-name">icon-attach</span><span class="i-code">0xe86a</span></div>
+ <div class="the-icons span3" title="Code: 0xe86b"><i class="demo-icon icon-keyboard">&#xe86b;</i> <span class="i-name">icon-keyboard</span><span class="i-code">0xe86b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe86c"><i class="demo-icon icon-menu">&#xe86c;</i> <span class="i-name">icon-menu</span><span class="i-code">0xe86c</span></div>
+ <div class="the-icons span3" title="Code: 0xe86d"><i class="demo-icon icon-wifi">&#xe86d;</i> <span class="i-name">icon-wifi</span><span class="i-code">0xe86d</span></div>
+ <div class="the-icons span3" title="Code: 0xe86e"><i class="demo-icon icon-moon">&#xe86e;</i> <span class="i-name">icon-moon</span><span class="i-code">0xe86e</span></div>
+ <div class="the-icons span3" title="Code: 0xe86f"><i class="demo-icon icon-chart-pie">&#xe86f;</i> <span class="i-name">icon-chart-pie</span><span class="i-code">0xe86f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe870"><i class="demo-icon icon-chart-area">&#xe870;</i> <span class="i-name">icon-chart-area</span><span class="i-code">0xe870</span></div>
+ <div class="the-icons span3" title="Code: 0xe871"><i class="demo-icon icon-chart-bar">&#xe871;</i> <span class="i-name">icon-chart-bar</span><span class="i-code">0xe871</span></div>
+ <div class="the-icons span3" title="Code: 0xe872"><i class="demo-icon icon-beaker">&#xe872;</i> <span class="i-name">icon-beaker</span><span class="i-code">0xe872</span></div>
+ <div class="the-icons span3" title="Code: 0xe873"><i class="demo-icon icon-magic">&#xe873;</i> <span class="i-name">icon-magic</span><span class="i-code">0xe873</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe874"><i class="demo-icon icon-spin6 animate-spin">&#xe874;</i> <span class="i-name">icon-spin6</span><span class="i-code">0xe874</span></div>
+ <div class="the-icons span3" title="Code: 0xe875"><i class="demo-icon icon-down-small">&#xe875;</i> <span class="i-name">icon-down-small</span><span class="i-code">0xe875</span></div>
+ <div class="the-icons span3" title="Code: 0xe876"><i class="demo-icon icon-left-small">&#xe876;</i> <span class="i-name">icon-left-small</span><span class="i-code">0xe876</span></div>
+ <div class="the-icons span3" title="Code: 0xe877"><i class="demo-icon icon-right-small">&#xe877;</i> <span class="i-name">icon-right-small</span><span class="i-code">0xe877</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe878"><i class="demo-icon icon-up-small">&#xe878;</i> <span class="i-name">icon-up-small</span><span class="i-code">0xe878</span></div>
+ <div class="the-icons span3" title="Code: 0xe879"><i class="demo-icon icon-pin">&#xe879;</i> <span class="i-name">icon-pin</span><span class="i-code">0xe879</span></div>
+ <div class="the-icons span3" title="Code: 0xe87a"><i class="demo-icon icon-angle-double-left">&#xe87a;</i> <span class="i-name">icon-angle-double-left</span><span class="i-code">0xe87a</span></div>
+ <div class="the-icons span3" title="Code: 0xe87b"><i class="demo-icon icon-angle-double-right">&#xe87b;</i> <span class="i-name">icon-angle-double-right</span><span class="i-code">0xe87b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe87c"><i class="demo-icon icon-circle">&#xe87c;</i> <span class="i-name">icon-circle</span><span class="i-code">0xe87c</span></div>
+ <div class="the-icons span3" title="Code: 0xe87d"><i class="demo-icon icon-info-circled">&#xe87d;</i> <span class="i-name">icon-info-circled</span><span class="i-code">0xe87d</span></div>
+ <div class="the-icons span3" title="Code: 0xe87e"><i class="demo-icon icon-twitter">&#xe87e;</i> <span class="i-name">icon-twitter</span><span class="i-code">0xe87e</span></div>
+ <div class="the-icons span3" title="Code: 0xe87f"><i class="demo-icon icon-facebook-squared">&#xe87f;</i> <span class="i-name">icon-facebook-squared</span><span class="i-code">0xe87f</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe880"><i class="demo-icon icon-gplus-squared">&#xe880;</i> <span class="i-name">icon-gplus-squared</span><span class="i-code">0xe880</span></div>
+ <div class="the-icons span3" title="Code: 0xe881"><i class="demo-icon icon-attention-circled">&#xe881;</i> <span class="i-name">icon-attention-circled</span><span class="i-code">0xe881</span></div>
+ <div class="the-icons span3" title="Code: 0xe883"><i class="demo-icon icon-check">&#xe883;</i> <span class="i-name">icon-check</span><span class="i-code">0xe883</span></div>
+ <div class="the-icons span3" title="Code: 0xe884"><i class="demo-icon icon-reschedule">&#xe884;</i> <span class="i-name">icon-reschedule</span><span class="i-code">0xe884</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xe885"><i class="demo-icon icon-warning-empty">&#xe885;</i> <span class="i-name">icon-warning-empty</span><span class="i-code">0xe885</span></div>
+ <div class="the-icons span3" title="Code: 0xf009"><i class="demo-icon icon-th-list">&#xf009;</i> <span class="i-name">icon-th-list</span><span class="i-code">0xf009</span></div>
+ <div class="the-icons span3" title="Code: 0xf00b"><i class="demo-icon icon-th-thumb-empty">&#xf00b;</i> <span class="i-name">icon-th-thumb-empty</span><span class="i-code">0xf00b</span></div>
+ <div class="the-icons span3" title="Code: 0xf09b"><i class="demo-icon icon-github-circled">&#xf09b;</i> <span class="i-name">icon-github-circled</span><span class="i-code">0xf09b</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xf102"><i class="demo-icon icon-angle-double-up">&#xf102;</i> <span class="i-name">icon-angle-double-up</span><span class="i-code">0xf102</span></div>
+ <div class="the-icons span3" title="Code: 0xf103"><i class="demo-icon icon-angle-double-down">&#xf103;</i> <span class="i-name">icon-angle-double-down</span><span class="i-code">0xf103</span></div>
+ <div class="the-icons span3" title="Code: 0xf104"><i class="demo-icon icon-angle-left">&#xf104;</i> <span class="i-name">icon-angle-left</span><span class="i-code">0xf104</span></div>
+ <div class="the-icons span3" title="Code: 0xf105"><i class="demo-icon icon-angle-right">&#xf105;</i> <span class="i-name">icon-angle-right</span><span class="i-code">0xf105</span></div>
+ </div>
+ <div class="row">
+ <div class="the-icons span3" title="Code: 0xf106"><i class="demo-icon icon-angle-up">&#xf106;</i> <span class="i-name">icon-angle-up</span><span class="i-code">0xf106</span></div>
+ <div class="the-icons span3" title="Code: 0xf107"><i class="demo-icon icon-angle-down">&#xf107;</i> <span class="i-name">icon-angle-down</span><span class="i-code">0xf107</span></div>
+ <div class="the-icons span3" title="Code: 0xf1da"><i class="demo-icon icon-history">&#xf1da;</i> <span class="i-name">icon-history</span><span class="i-code">0xf1da</span></div>
+ <div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
+ </div>
+ </div>
+ <div class="container footer">Generated by <a href="http://fontello.com">fontello.com</a></div>
+ </body>
+</html> \ No newline at end of file
diff --git a/application/fonts/fontello-ifont/font/ifont.eot b/application/fonts/fontello-ifont/font/ifont.eot
new file mode 100644
index 0000000..091db2f
--- /dev/null
+++ b/application/fonts/fontello-ifont/font/ifont.eot
Binary files differ
diff --git a/application/fonts/fontello-ifont/font/ifont.svg b/application/fonts/fontello-ifont/font/ifont.svg
new file mode 100644
index 0000000..9257938
--- /dev/null
+++ b/application/fonts/fontello-ifont/font/ifont.svg
@@ -0,0 +1,298 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>Copyright (C) 2020 by original authors @ fontello.com</metadata>
+<defs>
+<font id="ifont" horiz-adv-x="1000" >
+<font-face font-family="ifont" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
+<missing-glyph horiz-adv-x="1000" />
+<glyph glyph-name="dashboard" unicode="&#xe800;" d="M286 154v-108q0-22-16-37t-38-16h-178q-23 0-38 16t-16 37v108q0 22 16 38t38 15h178q23 0 38-15t16-38z m0 285v-107q0-22-16-38t-38-15h-178q-23 0-38 15t-16 38v107q0 23 16 38t38 16h178q23 0 38-16t16-38z m357-285v-108q0-22-16-37t-38-16h-178q-23 0-38 16t-16 37v108q0 22 16 38t38 15h178q23 0 38-15t16-38z m-357 571v-107q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v107q0 22 16 38t38 16h178q23 0 38-16t16-38z m357-286v-107q0-22-16-38t-38-15h-178q-23 0-38 15t-16 38v107q0 23 16 38t38 16h178q23 0 38-16t16-38z m357-285v-108q0-22-16-37t-38-16h-178q-22 0-38 16t-16 37v108q0 22 16 38t38 15h178q23 0 38-15t16-38z m-357 571v-107q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v107q0 22 16 38t38 16h178q23 0 38-16t16-38z m357-286v-107q0-22-16-38t-38-15h-178q-22 0-38 15t-16 38v107q0 23 16 38t38 16h178q23 0 38-16t16-38z m0 286v-107q0-22-16-38t-38-16h-178q-22 0-38 16t-16 38v107q0 22 16 38t38 16h178q23 0 38-16t16-38z" horiz-adv-x="1000" />
+
+<glyph glyph-name="user" unicode="&#xe801;" d="M714 69q0-60-35-104t-84-44h-476q-49 0-84 44t-35 104q0 48 5 90t17 85 33 73 52 50 76 19q73-72 174-72t175 72q42 0 75-19t52-50 33-73 18-85 4-90z m-143 495q0-88-62-151t-152-63-151 63-63 151 63 152 151 63 152-63 62-152z" horiz-adv-x="714.3" />
+
+<glyph glyph-name="users" unicode="&#xe802;" d="M331 350q-90-3-148-71h-75q-45 0-77 22t-31 66q0 197 69 197 4 0 25-11t54-24 66-12q38 0 75 13-3-21-3-37 0-78 45-143z m598-356q0-66-41-105t-108-39h-488q-68 0-108 39t-41 105q0 30 2 58t8 61 14 61 24 54 35 45 48 30 62 11q6 0 24-12t41-26 59-27 76-12 75 12 60 27 41 26 24 12q34 0 62-11t47-30 35-45 24-54 15-61 8-61 2-58z m-572 713q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z m393-214q0-89-63-152t-151-62-152 62-63 152 63 151 152 63 151-63 63-151z m321-126q0-43-31-66t-77-22h-75q-57 68-147 71 45 65 45 143 0 16-3 37 37-13 74-13 33 0 67 12t54 24 24 11q69 0 69-197z m-71 340q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="ok" unicode="&#xe803;" d="M352-10l-334 333 158 160 176-174 400 401 159-160z" horiz-adv-x="928" />
+
+<glyph glyph-name="cancel" unicode="&#xe804;" d="M799 116l-156-157-234 235-235-235-156 157 234 234-234 234 156 157 235-235 234 235 156-157-234-234z" horiz-adv-x="817" />
+
+<glyph glyph-name="plus" unicode="&#xe805;" d="M911 462l0-223-335 0 0-336-223 0 0 336-335 0 0 223 335 0 0 335 223 0 0-335 335 0z" horiz-adv-x="928" />
+
+<glyph glyph-name="minus" unicode="&#xe806;" d="M18 239l0 223 893 0 0-223-893 0z" horiz-adv-x="928" />
+
+<glyph glyph-name="folder-empty" unicode="&#xe807;" d="M464 685l447 0 0-669q0-47-33-80t-79-33l-669 0q-46 0-79 33t-33 80l0 781 446 0 0-112z m-334 0l0-223 669 0 0 112-446 0 0 111-223 0z m669-669l0 335-669 0 0-335 669 0z" horiz-adv-x="928" />
+
+<glyph glyph-name="download" unicode="&#xe808;" d="M714 100q0 15-10 25t-25 11-25-11-11-25 11-25 25-11 25 11 10 25z m143 0q0 15-10 25t-26 11-25-11-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-37t-38-16h-821q-23 0-38 16t-16 37v179q0 22 16 38t38 16h259l75-76q33-32 76-32t76 32l76 76h259q22 0 38-16t16-38z m-182 318q10-23-8-39l-250-250q-10-11-25-11t-25 11l-250 250q-17 16-8 39 10 21 33 21h143v250q0 15 11 25t25 11h143q14 0 25-11t10-25v-250h143q24 0 33-21z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="upload" unicode="&#xe809;" d="M714 29q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m143 0q0 14-10 25t-26 10-25-10-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-38t-38-16h-821q-23 0-38 16t-16 38v179q0 22 16 38t38 15h238q12-31 39-51t62-20h143q34 0 61 20t40 51h238q22 0 38-15t16-38z m-182 361q-9-22-33-22h-143v-250q0-15-10-25t-25-11h-143q-15 0-25 11t-11 25v250h-143q-23 0-33 22-9 22 8 39l250 250q10 10 25 10t25-10l250-250q18-17 8-39z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="git" unicode="&#xe80a;" d="M332 5q0 56-92 56-88 0-88-58 0-57 96-57 84 0 84 59z m-33 422q0 34-17 56t-49 23q-69 0-69-81 0-75 69-75 66 0 66 77z m150 180v-112q-20-7-44-13 9-24 9-47 0-70-41-120t-110-63q-22-5-33-15t-11-33q0-17 13-28t32-18 44-12 48-15 44-21 32-35 13-55q0-170-203-170-38 0-72 7t-65 23-49 46-18 71q0 92 102 125v3q-38 22-38 70 0 61 35 76v3q-40 13-66 60t-27 93q0 77 53 129t131 51q54 0 100-26 54 0 121 26z m178-491h-124q2 25 2 74v340q0 53-2 72h124q-3-19-3-69v-343q0-49 3-74z m335 124v-110q-40-22-97-22-35 0-60 12t-39 27-22 44-10 51-2 58v196h1v2q-4 0-11 0t-10 1q-12 0-33-3v106h54v42q0 30-4 50h127q-3-23-3-92h95v-106q-8 0-24 1t-24 1h-47v-204q0-73 48-73 34 0 61 19z m-321 528q0-32-22-57t-54-24q-32 0-54 24t-23 57q0 33 22 57t55 25q33 0 54-25t22-57z" horiz-adv-x="1000" />
+
+<glyph glyph-name="cubes" unicode="&#xe80b;" d="M357-61l214 107v176l-214-92v-191z m-36 254l226 96-226 97-225-97z m608-254l214 107v176l-214-92v-191z m-36 254l225 96-225 97-226-97z m-250 163l214 92v149l-214-92v-149z m-36 212l246 105-246 106-246-106z m607-289v-233q0-20-10-37t-29-26l-250-125q-14-8-32-8t-32 8l-250 125q-2 1-4 2-1-1-4-2l-250-125q-14-8-32-8t-31 8l-250 125q-19 9-29 26t-11 37v233q0 21 12 39t32 26l242 104v223q0 22 12 40t31 26l250 107q13 6 28 6t28-6l250-107q20-9 32-26t12-40v-223l242-104q20-8 32-26t11-39z" horiz-adv-x="1285.7" />
+
+<glyph glyph-name="database" unicode="&#xe80c;" d="M429 421q132 0 247 24t181 71v-95q0-38-57-71t-157-52-214-19-215 19-156 52-58 71v95q66-47 181-71t248-24z m0-428q132 0 247 24t181 71v-95q0-39-57-72t-157-52-214-19-215 19-156 52-58 72v95q66-47 181-71t248-24z m0 214q132 0 247 24t181 71v-95q0-38-57-71t-157-52-214-20-215 20-156 52-58 71v95q66-47 181-71t248-24z m0 643q116 0 214-19t157-52 57-72v-71q0-39-57-72t-157-52-214-19-215 19-156 52-58 72v71q0 39 58 72t156 52 215 19z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="gauge" unicode="&#xe80d;" d="M214 207q0 30-21 51t-50 21-51-21-21-51 21-50 51-21 50 21 21 50z m107 250q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m239-268l57 213q3 14-5 27t-21 16-27-3-17-22l-56-213q-33-3-60-25t-35-55q-11-43 11-81t66-50 81 11 50 66q9 33-4 65t-40 51z m369 18q0 30-21 51t-51 21-50-21-21-51 21-50 50-21 51 21 21 50z m-358 357q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m250-107q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m179-250q0-145-79-269-10-17-30-17h-782q-20 0-30 17-79 123-79 269 0 102 40 194t106 160 160 107 194 39 194-39 160-107 106-160 40-194z" horiz-adv-x="1000" />
+
+<glyph glyph-name="sitemap" unicode="&#xe80e;" d="M1000 154v-179q0-22-16-38t-38-16h-178q-22 0-38 16t-16 38v179q0 22 16 38t38 15h53v107h-285v-107h53q23 0 38-15t16-38v-179q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v179q0 22 16 38t38 15h53v107h-285v-107h53q23 0 38-15t16-38v-179q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v179q0 22 16 38t38 15h53v107q0 29 21 51t51 21h285v107h-53q-23 0-38 16t-16 37v179q0 22 16 38t38 16h178q23 0 38-16t16-38v-179q0-22-16-37t-38-16h-53v-107h285q29 0 51-21t21-51v-107h53q23 0 38-15t16-38z" horiz-adv-x="1000" />
+
+<glyph glyph-name="sort-name-up" unicode="&#xe80f;" d="M665 622h98l-40 122-6 26q-2 9-2 11h-2l-1-11q0 0-2-10t-5-16z m-254-576q0-6-6-13l-178-178q-5-5-13-5-6 0-12 5l-179 179q-8 9-4 19 4 11 17 11h107v768q0 8 5 13t13 5h107q8 0 13-5t5-13v-768h107q8 0 13-5t5-13z m466-66v-130h-326v50l206 295q7 11 12 16l6 5v1q-1 0-3 0t-5 0q-6-2-16-2h-130v-64h-67v128h317v-50l-206-296q-4-4-12-14l-6-7v-1l8 1q5 2 16 2h139v66h67z m50 501v-60h-161v60h42l-26 80h-136l-26-80h42v-60h-160v60h39l128 369h91l128-369h39z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="sort-name-down" unicode="&#xe810;" d="M665 51h98l-40 122-6 26q-2 9-2 11h-2l-1-11q0-1-2-10t-5-16z m-254-5q0-6-6-13l-178-178q-5-5-13-5-6 0-12 5l-179 179q-8 9-4 19 4 11 17 11h107v768q0 8 5 13t13 5h107q8 0 13-5t5-13v-768h107q8 0 13-5t5-13z m516-137v-59h-161v59h42l-26 80h-136l-26-80h42v-59h-160v59h39l128 370h91l128-370h39z m-50 643v-131h-326v51l206 295q7 10 12 15l6 5v2q-1 0-3-1t-5 0q-6-2-16-2h-130v-64h-67v128h317v-50l-206-295q-4-5-12-15l-6-5v-2l8 2q5 0 16 0h139v67h67z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="megaphone" unicode="&#xe811;" d="M929 493q29 0 50-21t21-51-21-50-50-21v-214q0-29-22-50t-50-22q-233 194-453 212-32-10-51-36t-17-57 22-51q-11-19-13-37t4-32 19-31 26-28 35-28q-17-32-63-46t-94-7-73 31q-4 13-17 49t-18 53-12 50-9 56 2 55 12 62h-68q-36 0-63 26t-26 63v107q0 37 26 63t63 26h268q243 0 500 215 29 0 50-22t22-50v-214z m-72-337v532q-220-168-428-191v-151q210-23 428-190z" horiz-adv-x="1000" />
+
+<glyph glyph-name="bug" unicode="&#xe812;" d="M911 314q0-14-11-25t-25-10h-125q0-96-37-162l116-117q10-11 10-25t-10-25q-10-11-25-11t-25 11l-111 110q-3-3-8-7t-24-16-36-21-46-16-54-7v500h-71v-500q-29 0-57 7t-49 19-36 22-25 18l-8 8-102-116q-11-12-27-12-13 0-24 9-11 10-11 25t8 26l113 127q-32 63-32 153h-125q-15 0-25 10t-11 25 11 25 25 11h125v164l-97 97q-11 10-11 25t11 25 25 10 25-10l97-97h471l96 97q11 10 25 10t26-10 10-25-10-25l-97-97v-164h125q15 0 25-11t11-25z m-268 322h-357q0 74 52 126t126 52 127-52 52-126z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="tasks" unicode="&#xe813;" d="M571 64h358v72h-358v-72z m-214 286h572v71h-572v-71z m357 286h215v71h-215v-71z m286-465v-142q0-15-11-25t-25-11h-928q-15 0-25 11t-11 25v142q0 15 11 26t25 10h928q15 0 25-10t11-26z m0 286v-143q0-14-11-25t-25-10h-928q-15 0-25 10t-11 25v143q0 15 11 25t25 11h928q15 0 25-11t11-25z m0 286v-143q0-14-11-25t-25-11h-928q-15 0-25 11t-11 25v143q0 14 11 25t25 11h928q15 0 25-11t11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="filter" unicode="&#xe814;" d="M783 685q9-22-8-39l-275-275v-414q0-23-22-33-7-3-14-3-15 0-25 11l-143 143q-10 11-10 25v271l-275 275q-18 17-8 39 9 22 33 22h714q23 0 33-22z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="off" unicode="&#xe815;" d="M857 350q0-87-34-166t-91-137-137-92-166-34-167 34-136 92-92 137-34 166q0 102 45 191t126 151q24 18 54 14t46-28q18-23 14-53t-28-47q-54-41-84-101t-30-127q0-58 23-111t61-91 91-61 111-23 110 23 92 61 61 91 22 111q0 68-30 127t-84 101q-23 18-28 47t14 53q17 24 47 28t53-14q81-61 126-151t45-191z m-357 429v-358q0-29-21-50t-50-21-51 21-21 50v358q0 29 21 50t51 21 50-21 21-50z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="book" unicode="&#xe816;" d="M915 583q22-31 10-72l-154-505q-10-36-42-60t-69-25h-515q-43 0-83 30t-55 74q-14 37-1 71 0 2 1 15t3 20q0 5-2 12t-2 11q1 6 5 12t9 13 9 13q13 21 25 51t17 51q2 6 0 17t0 16q2 6 9 15t10 13q12 20 23 51t14 51q1 5-1 17t0 16q2 7 12 17t13 13q10 14 23 47t16 54q0 4-2 14t-1 15q1 4 5 10t10 13 10 11q4 7 9 17t8 20 9 20 11 18 15 13 20 6 26-3l0-1q21 5 28 5h425q41 0 64-32t10-72l-153-506q-20-66-40-85t-72-20h-485q-15 0-21-8-6-9-1-24 14-39 81-39h515q16 0 31 9t20 23l167 550q4 13 3 32 21-8 33-24z m-594-1q-2-7 1-12t11-6h339q8 0 15 6t9 12l12 36q2 7-1 12t-12 6h-339q-7 0-14-6t-9-12z m-46-143q-3-7 1-12t11-6h339q7 0 14 6t10 12l11 36q3 7-1 13t-11 5h-339q-7 0-14-5t-10-13z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="paste" unicode="&#xe817;" d="M429-79h500v358h-233q-22 0-37 15t-16 38v232h-214v-643z m142 804v36q0 7-5 12t-12 6h-393q-7 0-13-6t-5-12v-36q0-7 5-13t13-5h393q7 0 12 5t5 13z m143-375h167l-167 167v-167z m286-71v-375q0-23-16-38t-38-16h-535q-23 0-38 16t-16 38v89h-303q-23 0-38 16t-16 37v750q0 23 16 38t38 16h607q22 0 38-16t15-38v-183q12-7 20-15l228-228q16-15 27-42t11-49z" horiz-adv-x="1000" />
+
+<glyph glyph-name="scissors" unicode="&#xe818;" d="M536 350q14 0 25-11t10-25-10-25-25-10-25 10-11 25 11 25 25 11z m167-36l283-222q16-11 14-31-3-20-19-28l-72-36q-7-4-16-4-10 0-17 4l-385 216-62-36q-4-3-7-3 8-28 6-54-4-43-31-83t-74-69q-74-47-154-47-76 0-124 44-51 47-44 116 4 42 31 82t73 69q74 47 155 47 46 0 84-18 5 8 13 13l68 40-68 41q-8 5-13 12-38-17-84-17-81 0-155 47-46 30-73 69t-31 82q-3 33 8 63t36 52q47 44 124 44 80 0 154-47 46-29 74-68t31-83q2-27-6-54 3-1 7-3l62-37 385 216q7 5 17 5 9 0 16-4l72-36q16-9 19-28 2-20-14-32z m-380 145q26 24 12 61t-59 65q-52 33-107 33-42 0-63-20-26-24-12-60t59-66q51-33 107-33 41 0 63 20z m-47-415q45 28 59 65t-12 60q-22 20-63 20-56 0-107-33-45-28-59-65t12-60q21-20 63-20 55 0 107 33z m99 342l54-33v7q0 20 18 31l8 4-44 26-15-14q-1-2-5-6t-7-7q-1-1-2-2t-2-1z m125-125l54-18 410 321-71 36-429-240v-64l-89-53 5-5q1-1 4-3 2-2 6-7t6-6l15-15z m393-232l71 35-290 228-99-77q-1-2-7-4z" horiz-adv-x="1000" />
+
+<glyph glyph-name="globe" unicode="&#xe819;" d="M429 779q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m153-291q-2-1-6-5t-7-6q1 0 2 3t3 6 2 4q3 4 12 8 8 4 29 7 19 5 29-6-1 1 5 7t8 7q2 1 8 3t9 4l1 12q-7-1-10 4t-3 12q0-2-4-5 0 4-2 5t-7-1-5-1q-5 2-8 5t-5 9-2 8q-1 3-5 6t-5 6q-1 1-2 3t-1 4-3 3-3 1-4-3-4-5-2-3q-2 1-4 1t-2-1-3-1-3-2q-1-2-4-2t-5-1q8 3-1 6-5 2-9 2 6 2 5 6t-5 8h3q-1 2-5 5t-10 5-7 3q-5 3-19 5t-18 1q-3-4-3-6t2-8 2-7q1-3-3-7t-3-7q0-4 7-9t6-12q-2-4-9-9t-9-6q-3-5-1-11t6-9q1-1 1-2t-2-3-3-2-4-2l-1-1q-7-3-12 3t-7 15q-4 14-9 17-13 4-16-1-3 7-23 15-14 5-33 2 4 0 0 8-4 9-10 7 1 3 2 10t0 7q2 8 7 13 1 1 4 5t5 7 1 4q19-3 28 6 2 3 6 9t6 10q5 3 8 3t8-3 8-3q8-1 8 6t-4 11q7 0 2 10-2 4-5 5-6 2-15-3-4-2 2-4-1 0-6-6t-9-10-9 3q0 0-3 7t-5 8q-5 0-9-9 1 5-6 9t-14 4q11 7-4 15-4 3-12 3t-11-2q-2-4-3-7t3-4 6-3 6-2 5-2q8-6 5-8-1 0-5-2t-6-2-4-2q-1-3 0-8t-1-8q-3 3-5 10t-4 9q4-5-14-3l-5 0q-3 0-9-1t-12-1-7 5q-3 4 0 11 0 2 2 1-2 2-6 5t-6 5q-25-8-52-23 3 0 6 1 3 1 8 4t5 3q19 7 24 4l3 2q7-9 11-14-4 3-17 1-11-3-12-7 4-6 2-10-2 2-6 6t-8 6-8 3q-9 0-13-1-81-45-131-124 4-4 7-4 2-1 3-5t1-6 6 1q5-4 2-10 1 0 25-15 10-10 11-12 2-6-5-10-1 1-5 5t-5 2q-2-3 0-10t6-7q-4 0-5-9t-2-20 0-13l1-1q-2-6 3-19t12-11q-7-1 11-24 3-4 4-5 2-1 7-4t9-6 5-5q2-3 6-13t8-13q-2-3 5-11t6-13q-1 0-2-1t-1 0q2-4 9-8t8-7q1-2 1-6t2-6 4-1q2 11-13 35-8 13-9 16-2 2-4 8t-2 8q1 0 3 0t5-2 4-3 1-1q-1-4 1-10t7-10 10-11 6-7q4-4 8-11t0-8q5 0 11-5t10-11q3-5 4-15t3-13q1-4 5-8t7-5l9-5t7-3q3-2 10-6t12-7q6-2 9-2t8 1 8 2q8 1 16-8t12-12q20-10 30-6-1 0 1-4t4-9 5-8 3-5q3-3 10-8t10-8q4 2 4 5-1-5 4-11t10-6q8 2 8 18-17-8-27 10 0 0-2 3t-2 5-1 4 0 5 2 1q5 0 6 2t-1 7-2 8q-1 4-6 11t-7 8q-3-5-9-4t-9 5q0-1-1-3t-1-4q-7 0-8 0 1 2 1 10t2 13q1 2 3 6t5 9 2 7-3 5-9 1q-11 0-15-11-1-2-2-6t-2-6-5-4q-4-2-14-1t-13 3q-8 4-13 16t-5 20q0 6 1 15t2 14-3 14q2 1 5 5t5 6q2 1 3 1t3 0 2 1 1 3q0 1-2 2-1 1-2 1 4-1 16 1t15-1q9-6 12 1 0 1-1 6t0 7q3-15 16-5 2-1 9-3t9-2q2-1 4-3t3-3 3 0 5 4q5-8 7-13 6-23 10-25 4-2 6-1t3 5 0 8-1 7l-1 5v10l0 4q-8 2-10 7t0 10 9 10q0 1 4 2t9 4 7 4q12 11 8 20 4 0 6 5 0 0-2 2t-5 2-2 2q5 2 1 8 3 2 4 7t4 5q5-6 12-1 5 5 1 9 2 4 11 6t10 5q4-1 5 1t0 7 2 7q2 2 9 5t7 2l9 7q2 2 0 2 10-1 18 6 5 6-4 11 2 4-1 5t-9 4q2 0 7 0t5 1q9 5-3 9-10 2-24-7z m-91-490q115 21 195 106-1 2-7 2t-7 2q-10 4-13 5 1 4-1 7t-5 5-7 5-6 4q-1 1-4 3t-4 3-4 2-5 2-5-1l-2-1q-2 0-3-1t-3-2-2-1 0-2q-12 10-20 13-3 0-6 3t-6 4-6 0-6-3q-3-3-4-9t-1-7q-4 3 0 10t1 10q-1 3-6 2t-6-2-7-5-5-3-4-3-5-5q-2-2-4-6t-2-6q-1 2-7 3t-5 3q1-5 2-19t3-22q4-17-7-26-15-14-16-23-2-12 7-14 0-4-5-12t-4-12q0-3 2-9z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="cloud" unicode="&#xe81a;" d="M1071 207q0-89-62-151t-152-63h-607q-103 0-177 73t-73 177q0 74 40 135t104 91q-1 16-1 24 0 118 84 202t202 84q88 0 159-49t105-129q39 35 93 35 59 0 101-42t42-101q0-42-23-77 72-17 119-75t46-134z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="flash" unicode="&#xe81b;" d="M494 534q10-11 4-24l-302-646q-7-14-23-14-2 0-8 1-9 3-14 11t-3 16l110 451-226-56q-2-1-7-1-10 0-17 7-10 8-7 21l112 461q2 8 9 13t15 5h183q11 0 18-7t7-17q0-4-2-10l-96-258 221 54q5 2 7 2 11 0 19-9z" horiz-adv-x="500" />
+
+<glyph glyph-name="barchart" unicode="&#xe81c;" d="M143 46v-107q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v107q0 8 5 13t13 5h107q8 0 13-5t5-13z m214 72v-179q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v179q0 8 5 13t13 5h107q8 0 13-5t5-13z m214 143v-322q0-8-5-13t-12-5h-108q-7 0-12 5t-5 13v322q0 8 5 13t12 5h108q7 0 12-5t5-13z m215 214v-536q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v536q0 8 5 13t13 5h107q8 0 13-5t5-13z m214 286v-822q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v822q0 8 5 13t13 5h107q8 0 13-5t5-13z" horiz-adv-x="1000" />
+
+<glyph glyph-name="down-dir" unicode="&#xe81d;" d="M571 457q0-14-10-25l-250-250q-11-11-25-11t-25 11l-250 250q-11 11-11 25t11 25 25 11h500q14 0 25-11t10-25z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="up-dir" unicode="&#xe81e;" d="M571 171q0-14-10-25t-25-10h-500q-15 0-25 10t-11 25 11 26l250 250q10 10 25 10t25-10l250-250q10-11 10-26z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="left-dir" unicode="&#xe81f;" d="M357 600v-500q0-14-10-25t-26-11-25 11l-250 250q-10 11-10 25t10 25l250 250q11 11 25 11t26-11 10-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="right-dir" unicode="&#xe820;" d="M321 350q0-14-10-25l-250-250q-11-11-25-11t-25 11-11 25v500q0 15 11 25t25 11 25-11l250-250q10-10 10-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="down-open" unicode="&#xe821;" d="M939 399l-414-413q-10-11-25-11t-25 11l-414 413q-11 11-11 26t11 25l93 92q10 11 25 11t25-11l296-296 296 296q11 11 25 11t26-11l92-92q11-11 11-25t-11-26z" horiz-adv-x="1000" />
+
+<glyph glyph-name="right-open" unicode="&#xe822;" d="M618 361l-414-415q-11-10-25-10t-25 10l-93 93q-11 11-11 25t11 25l296 297-296 296q-11 11-11 25t11 25l93 93q10 11 25 11t25-11l414-414q10-11 10-25t-10-25z" horiz-adv-x="714.3" />
+
+<glyph glyph-name="up-open" unicode="&#xe823;" d="M939 107l-92-92q-11-10-26-10t-25 10l-296 297-296-297q-11-10-25-10t-25 10l-93 92q-11 11-11 26t11 25l414 414q11 10 25 10t25-10l414-414q11-11 11-25t-11-26z" horiz-adv-x="1000" />
+
+<glyph glyph-name="left-open" unicode="&#xe824;" d="M654 682l-297-296 297-297q10-10 10-25t-10-25l-93-93q-11-10-25-10t-25 10l-414 415q-11 10-11 25t11 25l414 414q10 11 25 11t25-11l93-93q10-10 10-25t-10-25z" horiz-adv-x="714.3" />
+
+<glyph glyph-name="up-big" unicode="&#xe825;" d="M899 308q0-28-21-50l-41-42q-22-21-51-21-30 0-50 21l-165 164v-393q0-29-20-47t-51-19h-71q-30 0-51 19t-21 47v393l-164-164q-20-21-50-21t-50 21l-42 42q-21 21-21 50 0 30 21 51l363 363q20 21 50 21 30 0 51-21l363-363q21-22 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="right-big" unicode="&#xe826;" d="M821 314q0-30-20-50l-363-364q-22-20-51-20-29 0-50 20l-42 42q-22 21-22 51t22 51l163 163h-393q-29 0-47 21t-18 51v71q0 30 18 51t47 20h393l-163 165q-22 20-22 50t22 50l42 42q21 21 50 21 29 0 51-21l363-363q20-20 20-51z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="left-big" unicode="&#xe827;" d="M857 350v-71q0-30-18-51t-47-21h-393l164-164q21-20 21-50t-21-50l-42-43q-21-20-51-20-29 0-50 20l-364 364q-20 21-20 50 0 29 20 51l364 363q21 21 50 21 29 0 51-21l42-41q21-22 21-51t-21-51l-164-164h393q29 0 47-20t18-51z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="down-big" unicode="&#xe828;" d="M899 386q0-30-21-50l-363-364q-22-21-51-21-29 0-50 21l-363 364q-21 20-21 50 0 29 21 51l41 41q22 21 51 21 29 0 50-21l164-164v393q0 29 21 50t51 22h71q29 0 50-22t21-50v-393l165 164q20 21 50 21 29 0 51-21l41-41q21-22 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="resize-full-alt" unicode="&#xe829;" d="M716 548l-198-198 198-198 80 80q17 18 39 8 22-9 22-33v-250q0-14-10-25t-26-11h-250q-23 0-32 23-10 21 7 38l81 81-198 198-198-198 80-81q17-17 8-38-10-23-33-23h-250q-15 0-25 11t-11 25v250q0 24 22 33 22 10 39-8l80-80 198 198-198 198-80-80q-11-11-25-11-7 0-14 3-22 9-22 33v250q0 14 11 25t25 11h250q23 0 33-23 9-21-8-38l-80-81 198-198 198 198-81 81q-17 17-7 38 9 23 32 23h250q15 0 26-11t10-25v-250q0-24-22-33-7-3-14-3-14 0-25 11z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="resize-full" unicode="&#xe82a;" d="M421 261q0-7-5-13l-185-185 80-81q10-10 10-25t-10-25-25-11h-250q-15 0-25 11t-11 25v250q0 15 11 25t25 11 25-11l80-80 186 185q5 6 12 6t13-6l64-63q5-6 5-13z m436 482v-250q0-15-10-25t-26-11-25 11l-80 80-185-185q-6-6-13-6t-13 6l-64 64q-5 5-5 12t5 13l186 185-81 81q-10 10-10 25t10 25 25 11h250q15 0 26-11t10-25z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="resize-small" unicode="&#xe82b;" d="M429 314v-250q0-14-11-25t-25-10-25 10l-81 81-185-186q-5-5-13-5t-12 5l-64 64q-6 6-6 13t6 13l185 185-80 80q-11 11-11 25t11 25 25 11h250q14 0 25-11t11-25z m421 375q0-7-6-12l-185-186 80-80q11-11 11-25t-11-25-25-11h-250q-14 0-25 11t-10 25v250q0 14 10 25t25 10 25-10l81-80 185 185q6 5 13 5t13-5l63-64q6-5 6-13z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="move" unicode="&#xe82c;" d="M1000 350q0-14-11-25l-142-143q-11-11-26-11t-25 11-10 25v72h-215v-215h72q14 0 25-10t11-25-11-25l-143-143q-10-11-25-11t-25 11l-143 143q-11 10-11 25t11 25 25 10h72v215h-215v-72q0-14-10-25t-25-11-25 11l-143 143q-11 11-11 25t11 25l143 143q10 11 25 11t25-11 10-25v-72h215v215h-72q-14 0-25 10t-11 25 11 26l143 142q11 11 25 11t25-11l143-142q11-11 11-26t-11-25-25-10h-72v-215h215v72q0 14 10 25t25 11 26-11l142-143q11-10 11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="resize-horizontal" unicode="&#xe82d;" d="M1000 350q0-14-11-25l-142-143q-11-11-26-11t-25 11-10 25v72h-572v-72q0-14-10-25t-25-11-25 11l-143 143q-11 11-11 25t11 25l143 143q10 11 25 11t25-11 10-25v-72h572v72q0 14 10 25t25 11 26-11l142-143q11-10 11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="resize-vertical" unicode="&#xe82e;" d="M393 671q0-14-11-25t-25-10h-71v-572h71q15 0 25-10t11-25-11-25l-143-143q-10-11-25-11t-25 11l-143 143q-10 10-10 25t10 25 25 10h72v572h-72q-14 0-25 10t-10 25 10 26l143 142q11 11 25 11t25-11l143-142q11-11 11-26z" horiz-adv-x="428.6" />
+
+<glyph glyph-name="zoom-in" unicode="&#xe82f;" d="M571 404v-36q0-7-5-13t-12-5h-125v-125q0-7-6-13t-12-5h-36q-7 0-13 5t-5 13v125h-125q-7 0-12 5t-6 13v36q0 7 6 12t12 5h125v125q0 8 5 13t13 5h36q7 0 12-5t6-13v-125h125q7 0 12-5t5-12z m72-18q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-21-50t-51-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="block" unicode="&#xe830;" d="M732 352q0 90-48 164l-421-420q76-50 166-50 62 0 118 25t96 65 65 97 24 119z m-557-167l421 421q-75 50-167 50-83 0-153-40t-110-111-41-153q0-91 50-167z m682 167q0-88-34-168t-91-137-137-92-166-34-167 34-137 92-91 137-34 168 34 167 91 137 137 91 167 34 166-34 137-91 91-137 34-167z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="zoom-out" unicode="&#xe831;" d="M571 404v-36q0-7-5-13t-12-5h-322q-7 0-12 5t-6 13v36q0 7 6 12t12 5h322q7 0 12-5t5-12z m72-18q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-21-50t-51-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="lightbulb" unicode="&#xe832;" d="M411 529q0-8-6-13t-12-5-13 5-5 13q0 25-30 39t-59 14q-7 0-13 5t-5 13 5 13 13 5q28 0 55-9t49-30 21-50z m89 0q0 40-19 74t-50 57-69 35-76 12-76-12-69-35-50-57-20-74q0-57 38-101 6-6 17-18t17-19q72-85 79-166h127q8 81 79 166 6 6 17 19t17 18q38 44 38 101z m71 0q0-87-57-150-25-27-42-48t-33-54-19-60q26-15 26-46 0-20-13-35 13-15 13-36 0-29-25-45 8-13 8-26 0-26-18-40t-43-14q-11-25-34-39t-48-15-49 15-33 39q-26 0-44 14t-17 40q0 13 7 26-25 16-25 45 0 21 14 36-14 15-14 35 0 31 26 46-2 28-19 60t-33 54-41 48q-58 63-58 150 0 55 25 103t65 79 92 49 104 19 104-19 91-49 66-79 24-103z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="clock" unicode="&#xe833;" d="M500 546v-250q0-7-5-12t-13-5h-178q-8 0-13 5t-5 12v36q0 8 5 13t13 5h125v196q0 8 5 13t12 5h36q8 0 13-5t5-13z m232-196q0 83-41 152t-110 111-152 41-153-41-110-111-41-152 41-152 110-111 153-41 152 41 110 111 41 152z m125 0q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="volume-up" unicode="&#xe834;" d="M429 654v-608q0-14-11-25t-25-10-25 10l-186 186h-146q-15 0-25 11t-11 25v214q0 15 11 25t25 11h146l186 186q10 10 25 10t25-10 11-25z m214-304q0-42-24-79t-63-52q-5-3-14-3-14 0-25 10t-10 26q0 12 6 20t17 14 19 12 16 21 6 31-6 32-16 20-19 13-17 13-6 20q0 15 10 26t25 10q9 0 14-3 39-15 63-52t24-79z m143 0q0-85-48-158t-125-105q-7-3-14-3-15 0-26 11t-10 25q0 22 21 33 32 16 43 25 41 30 64 75t23 97-23 97-64 75q-11 9-43 25-21 11-21 33 0 14 10 25t25 11q8 0 15-3 78-33 125-105t48-158z m143 0q0-128-71-236t-189-158q-7-3-14-3-15 0-25 11t-11 25q0 20 22 33 4 2 12 6t13 6q25 14 46 28 68 51 107 127t38 161-38 161-107 127q-21 15-46 28-4 3-13 6t-12 6q-22 13-22 33 0 15 11 25t25 11q7 0 14-3 118-51 189-158t71-236z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="volume-down" unicode="&#xe835;" d="M429 654v-608q0-14-11-25t-25-10-25 10l-186 186h-146q-15 0-25 11t-11 25v214q0 15 11 25t25 11h146l186 186q10 10 25 10t25-10 11-25z m214-304q0-42-24-79t-63-52q-5-3-14-3-14 0-25 10t-10 26q0 12 6 20t17 14 19 12 16 21 6 31-6 32-16 20-19 13-17 13-6 20q0 15 10 26t25 10q9 0 14-3 39-15 63-52t24-79z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="volume-off" unicode="&#xe836;" d="M429 654v-608q0-14-11-25t-25-10-25 10l-186 186h-146q-15 0-25 11t-11 25v214q0 15 11 25t25 11h146l186 186q10 10 25 10t25-10 11-25z" horiz-adv-x="428.6" />
+
+<glyph glyph-name="mute" unicode="&#xe837;" d="M151 323l-56-57q-24 58-24 120v71q0 15 11 25t25 11 25-11 11-25v-71q0-30 8-63z m622 336l-202-202v-71q0-74-52-126t-126-53q-31 0-61 11l-53-54q54-28 114-28 103 0 177 73t73 177v71q0 15 11 25t25 11 25-11 10-25v-71q0-124-82-215t-203-104v-74h142q15 0 26-11t10-25-10-25-26-11h-357q-14 0-25 11t-10 25 10 25 25 11h143v74q-70 7-131 45l-142-142q-5-6-13-6t-12 6l-46 46q-6 5-6 13t6 12l689 689q5 6 12 6t13-6l46-46q6-5 6-13t-6-12z m-212 73l-347-346v285q0 74 53 127t126 52q57 0 103-33t65-85z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="mic" unicode="&#xe838;" d="M643 457v-71q0-124-82-215t-204-104v-74h143q15 0 25-11t11-25-11-25-25-11h-357q-15 0-25 11t-11 25 11 25 25 11h143v74q-121 13-204 104t-82 215v71q0 15 11 25t25 11 25-11 10-25v-71q0-103 74-177t176-73 177 73 73 177v71q0 15 11 25t25 11 25-11 11-25z m-143 214v-285q0-74-52-126t-127-53-126 53-52 126v285q0 74 52 127t126 52 127-52 52-127z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="endtime" unicode="&#xe839;" d="M661 350q0-14-11-25l-303-304q-11-10-26-10t-25 10-10 25v161h-250q-15 0-25 11t-11 25v214q0 15 11 25t25 11h250v161q0 14 10 25t25 10 26-10l303-304q11-10 11-25z m196 196v-392q0-67-47-114t-114-47h-178q-7 0-13 5t-5 13q0 2-1 11t0 15 2 13 5 11 12 3h178q37 0 64 27t26 63v392q0 37-26 64t-64 26h-174t-6 0-6 2-5 3-4 5-1 8q0 2-1 11t0 15 2 13 5 11 12 3h178q67 0 114-47t47-114z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="starttime" unicode="&#xe83a;" d="M357 46q0-2 1-11t0-14-2-14-5-11-12-3h-178q-67 0-114 47t-47 114v392q0 67 47 114t114 47h178q8 0 13-5t5-13q0-2 1-11t0-15-2-13-5-11-12-3h-178q-37 0-63-26t-27-64v-392q0-37 27-63t63-27h174t6 0 7-2 4-3 4-5 1-8z m518 304q0-14-11-25l-303-304q-11-10-25-10t-25 10-11 25v161h-250q-14 0-25 11t-11 25v214q0 15 11 25t25 11h250v161q0 14 11 25t25 10 25-10l303-304q11-10 11-25z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="calendar-empty" unicode="&#xe83b;" d="M71-79h786v572h-786v-572z m215 679v161q0 8-5 13t-13 5h-36q-8 0-13-5t-5-13v-161q0-8 5-13t13-5h36q8 0 13 5t5 13z m428 0v161q0 8-5 13t-13 5h-35q-8 0-13-5t-5-13v-161q0-8 5-13t13-5h35q8 0 13 5t5 13z m215 36v-715q0-29-22-50t-50-21h-786q-29 0-50 21t-21 50v715q0 29 21 50t50 21h72v54q0 37 26 63t63 26h36q37 0 63-26t26-63v-54h214v54q0 37 27 63t63 26h35q37 0 64-26t26-63v-54h71q29 0 50-21t22-50z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="calendar" unicode="&#xe83c;" d="M71-79h161v161h-161v-161z m197 0h178v161h-178v-161z m-197 197h161v178h-161v-178z m197 0h178v178h-178v-178z m-197 214h161v161h-161v-161z m411-411h179v161h-179v-161z m-214 411h178v161h-178v-161z m428-411h161v161h-161v-161z m-214 197h179v178h-179v-178z m-196 482v161q0 7-6 12t-12 6h-36q-7 0-12-6t-6-12v-161q0-7 6-13t12-5h36q7 0 12 5t6 13z m410-482h161v178h-161v-178z m-214 214h179v161h-179v-161z m214 0h161v161h-161v-161z m18 268v161q0 7-5 12t-13 6h-35q-7 0-13-6t-5-12v-161q0-7 5-13t13-5h35q8 0 13 5t5 13z m215 36v-715q0-29-22-50t-50-21h-786q-29 0-50 21t-21 50v715q0 29 21 50t50 21h72v54q0 37 26 63t63 26h36q37 0 63-26t26-63v-54h214v54q0 37 27 63t63 26h35q37 0 64-26t26-63v-54h71q29 0 50-21t22-50z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="wrench" unicode="&#xe83d;" d="M214 29q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m360 234l-381-381q-21-20-50-20-29 0-51 20l-59 61q-21 20-21 50 0 29 21 51l380 380q22-55 64-97t97-64z m354 243q0-22-13-59-27-75-92-122t-144-46q-104 0-177 73t-73 177 73 176 177 74q32 0 67-10t60-26q9-6 9-15t-9-16l-163-94v-125l108-60q2 2 44 27t75 45 40 20q8 0 13-5t5-14z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="sliders" unicode="&#xe83e;" d="M196 64v-71h-196v71h196z m197 72q14 0 25-11t11-25v-143q0-14-11-25t-25-11h-143q-14 0-25 11t-11 25v143q0 15 11 25t25 11h143z m89 214v-71h-482v71h482z m-357 286v-72h-125v72h125z m732-572v-71h-411v71h411z m-536 643q15 0 26-10t10-26v-142q0-15-10-25t-26-11h-142q-15 0-25 11t-11 25v142q0 15 11 26t25 10h142z m358-286q14 0 25-10t10-25v-143q0-15-10-25t-25-11h-143q-15 0-25 11t-11 25v143q0 14 11 25t25 10h143z m178-71v-71h-125v71h125z m0 286v-72h-482v72h482z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="services" unicode="&#xe83f;" d="M500 350q0 59-42 101t-101 42-101-42-42-101 42-101 101-42 101 42 42 101z m429-286q0 29-22 51t-50 21-50-21-21-51q0-29 21-50t50-21 51 21 21 50z m0 572q0 29-22 50t-50 21-50-21-21-50q0-30 21-51t50-21 51 21 21 51z m-215-235v-103q0-6-4-11t-8-6l-87-14q-6-19-18-42 19-27 50-64 4-6 4-11 0-7-4-11-12-17-46-50t-43-33q-7 0-12 4l-64 50q-21-11-43-17-6-60-13-87-4-13-17-13h-104q-6 0-11 4t-5 10l-13 85q-19 6-42 18l-66-50q-4-4-11-4-6 0-12 4-80 75-80 90 0 5 4 10 5 8 23 30t26 34q-13 24-20 46l-85 13q-5 1-9 5t-4 11v104q0 5 4 10t9 6l86 14q7 19 18 42-19 27-50 64-4 6-4 11 0 7 4 12 12 16 46 49t44 33q6 0 12-4l64-50q19 10 43 18 6 60 13 86 3 13 16 13h104q6 0 11-4t6-10l13-85q19-6 42-17l65 49q5 4 12 4 6 0 11-4 81-75 81-90 0-4-4-10-7-9-24-30t-25-34q13-27 19-46l85-12q6-2 9-6t4-11z m357-298v-78q0-9-83-17-6-15-16-29 28-63 28-77 0-2-2-4-68-40-69-40-5 0-26 27t-29 37q-11-1-17-1t-17 1q-7-11-29-37t-25-27q-1 0-69 40-3 2-3 4 0 14 29 77-10 14-17 29-83 8-83 17v78q0 9 83 18 7 16 17 29-29 63-29 77 0 2 3 4 2 1 19 11t33 19 17 9q4 0 25-26t29-38q12 1 17 1t17-1q28 40 51 63l4 1q2 0 69-39 2-2 2-4 0-14-28-77 9-13 16-29 83-9 83-18z m0 572v-78q0-9-83-18-6-15-16-29 28-63 28-77 0-2-2-4-68-39-69-39-5 0-26 26t-29 38q-11-1-17-1t-17 1q-7-12-29-38t-25-26q-1 0-69 39-3 2-3 4 0 14 29 77-10 14-17 29-83 9-83 18v78q0 9 83 17 7 16 17 29-29 63-29 77 0 2 3 4 2 1 19 11t33 19 17 9q4 0 25-26t29-37q12 1 17 1t17-1q28 39 51 62l4 1q2 0 69-39 2-2 2-4 0-14-28-77 9-13 16-29 83-8 83-17z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="service" unicode="&#xe840;" d="M571 350q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 16 20 16h124q7 0 13-5t7-12l15-103q28-9 51-20l79 59q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-12 0-7-4-13-9-12-29-37t-30-40q15-28 23-54l102-16q7-1 12-7t4-13z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="phone" unicode="&#xe841;" d="M786 158q0-15-6-39t-12-38q-11-28-68-60-52-28-103-28-15 0-30 2t-32 7-26 8-31 11-28 10q-54 20-97 47-71 44-148 120t-120 148q-27 43-46 97-2 5-10 28t-12 31-8 26-7 32-2 29q0 52 29 104 31 57 59 68 14 6 38 12t39 6q8 0 12-2 10-3 30-42 6-11 16-31t20-35 17-30q2-2 10-14t12-20 4-16q0-11-16-27t-35-31-34-30-16-25q0-5 3-13t4-11 8-14 7-10q42-77 97-132t131-97q1 0 10-6t14-8 11-5 13-2q10 0 25 16t30 34 31 35 28 16q7 0 15-4t20-12 14-10q14-8 30-17t36-20 30-17q39-19 42-29 2-4 2-12z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="file-pdf" unicode="&#xe842;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-287 331q18-14 47-31 33 4 65 4 82 0 99-27 9-13 1-29 0-1-1-1l-1-2v0q-3-21-39-21-27 0-64 11t-73 29q-123-13-219-46-85-146-135-146-8 0-15 4l-14 7q0 0-3 2-6 6-4 20 5 23 32 51t73 54q8 5 13-3 1-1 1-2 29 47 60 110 38 76 58 146-13 46-17 89t4 71q6 22 23 22h12q13 0 20-8 10-12 5-38-1-3-2-4 0-2 0-5v-17q-1-68-8-107 31-91 82-133z m-321-229q29 13 76 88-29-22-49-47t-27-41z m222 513q-9-23-2-73 1 4 4 24 0 2 4 24 1 3 3 5-1 0-1 1-1 1-1 2 0 12-7 20 0-1 0-1v-2z m-70-368q76 30 159 45-1 0-7 5t-9 8q-43 37-71 98-15-48-46-110-17-31-26-46z m361 9q-13 13-78 13 42-16 69-16 8 0 10 1 0 0-1 2z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="file-word" unicode="&#xe843;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-656 500v-59h39l92-369h88l72 271q4 11 5 25 2 9 2 14h2l1-14q1-1 2-11t3-14l72-271h89l91 369h39v59h-167v-59h50l-55-245q-3-11-4-25l-1-12h-3q0 2 0 4t-1 4 0 4q-1 2-2 11t-3 14l-81 304h-63l-81-304q-1-5-2-13t-2-12l-2-12h-2l-2 12q-1 14-3 25l-56 245h50v59h-167z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="file-excel" unicode="&#xe844;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-547 131v-59h157v59h-42l58 90q3 4 5 9t5 8 2 2h1q0-2 3-6 1-2 2-4t3-4 4-5l60-90h-43v-59h163v59h-38l-107 152 108 158h38v59h-156v-59h41l-57-89q-2-4-6-9t-5-8l-1-1h-1q0 2-3 5-3 6-9 13l-59 89h42v59h-162v-59h38l106-152-109-158h-38z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="doc-text" unicode="&#xe845;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-572 483q0 7 5 12t13 5h393q8 0 13-5t5-12v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36z m411-125q8 0 13-5t5-13v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36q0 8 5 13t13 5h393z m0-143q8 0 13-5t5-13v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36q0 8 5 13t13 5h393z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="trash" unicode="&#xe846;" d="M286 439v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m143 0v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m142 0v-321q0-8-5-13t-12-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q7 0 12-5t5-13z m72-404v529h-500v-529q0-12 4-22t8-15 6-5h464q2 0 6 5t8 15 4 22z m-375 601h250l-27 65q-4 5-9 6h-177q-6-1-10-6z m518-18v-36q0-8-5-13t-13-5h-54v-529q0-46-26-80t-63-34h-464q-37 0-63 33t-27 79v531h-53q-8 0-13 5t-5 13v36q0 8 5 13t13 5h172l39 93q9 21 31 35t44 15h178q23 0 44-15t30-35l39-93h173q8 0 13-5t5-13z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="comment-empty" unicode="&#xe847;" d="M500 636q-114 0-213-39t-157-105-59-142q0-62 40-119t113-98l48-28-15-53q-13-51-39-97 85 36 154 96l24 21 32-3q38-5 72-5 114 0 213 39t157 105 59 142-59 142-157 105-213 39z m500-286q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12h-3q-8 0-15 6t-9 15v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 97 67 179t182 130 251 48 251-48 182-130 67-179z" horiz-adv-x="1000" />
+
+<glyph glyph-name="comment" unicode="&#xe848;" d="M1000 350q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12-10-1-17 5t-10 16v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 73 40 139t106 114 160 76 194 28q136 0 251-48t182-130 67-179z" horiz-adv-x="1000" />
+
+<glyph glyph-name="chat" unicode="&#xe849;" d="M786 421q0-77-53-143t-143-104-197-38q-48 0-98 9-70-49-155-72-21-5-48-9h-2q-6 0-12 5t-6 12q-1 1-1 3t1 4 1 3l1 3t2 3 2 3 3 3 2 2q3 3 13 14t15 16 12 17 14 21 11 25q-69 40-108 98t-40 125q0 78 53 144t143 104 197 38 197-38 143-104 53-144z m214-142q0-67-40-126t-108-98q5-14 11-25t14-21 13-16 14-17 13-14q0 0 2-2t3-3 2-3 2-3l1-3t1-3 1-4-1-3q-2-8-7-13t-12-4q-28 4-48 9-86 23-156 72-50-9-98-9-151 0-263 74 32-3 49-3 90 0 172 25t148 72q69 52 107 119t37 141q0 43-13 85 72-39 114-99t42-128z" horiz-adv-x="1000" />
+
+<glyph glyph-name="chat-empty" unicode="&#xe84a;" d="M393 636q-85 0-160-29t-118-79-44-107q0-45 30-88t83-73l54-32-19-46q19 11 34 21l25 18 30-6q43-8 85-8 85 0 160 29t118 79 43 106-43 107-118 79-160 29z m0 71q106 0 197-38t143-104 53-144-53-143-143-104-197-38q-48 0-98 9-70-49-155-72-21-5-48-9h-2q-6 0-12 5t-6 12q-1 1-1 3t1 4 1 3l1 3t2 3 2 3 3 3 2 2q3 3 13 14t15 16 12 17 14 21 11 25q-69 40-108 98t-40 125q0 78 53 144t143 104 197 38z m459-652q5-14 11-25t14-21 13-16 14-17 13-14q0 0 2-2t3-3 2-3 2-3l1-3t1-3 1-4-1-3q-2-8-7-13t-12-4q-28 4-48 9-86 23-156 72-50-9-98-9-151 0-263 74 32-3 49-3 90 0 172 25t148 72q69 52 107 119t37 141q0 43-13 85 72-39 114-99t42-128q0-67-40-126t-108-98z" horiz-adv-x="1000" />
+
+<glyph glyph-name="bell" unicode="&#xe84b;" d="M509-96q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m-372 160h726q-149 168-149 465 0 28-13 58t-39 58-67 45-95 17-95-17-67-45-39-58-13-58q0-297-149-465z m827 0q0-29-21-50t-50-21h-250q0-59-42-101t-101-42-101 42-42 101h-250q-29 0-50 21t-21 50q28 24 51 49t47 67 42 89 27 115 11 145q0 84 66 157t171 89q-5 10-5 21 0 23 16 38t38 16 38-16 16-38q0-11-5-21 106-16 171-89t66-157q0-78 11-145t28-115 41-89 48-67 50-49z" horiz-adv-x="1000" />
+
+<glyph glyph-name="bell-alt" unicode="&#xe84c;" d="M509-96q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m455 160q0-29-21-50t-50-21h-250q0-59-42-101t-101-42-101 42-42 101h-250q-29 0-50 21t-21 50q28 24 51 49t47 67 42 89 27 115 11 145q0 84 66 157t171 89q-5 10-5 21 0 23 16 38t38 16 38-16 16-38q0-11-5-21 106-16 171-89t66-157q0-78 11-145t28-115 41-89 48-67 50-49z" horiz-adv-x="1000" />
+
+<glyph glyph-name="attention-alt" unicode="&#xe84d;" d="M286 154v-125q0-15-11-25t-25-11h-143q-14 0-25 11t-11 25v125q0 14 11 25t25 10h143q15 0 25-10t11-25z m17 589l-16-429q-1-14-12-25t-25-10h-143q-14 0-25 10t-12 25l-15 429q-1 14 10 25t24 11h179q14 0 25-11t10-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="print" unicode="&#xe84e;" d="M214-7h500v143h-500v-143z m0 357h500v214h-89q-22 0-38 16t-16 38v89h-357v-357z m643-36q0 15-10 25t-26 11-25-11-10-25 10-25 25-10 26 10 10 25z m72 0v-232q0-7-6-12t-12-6h-125v-89q0-22-16-38t-38-16h-536q-22 0-37 16t-16 38v89h-125q-7 0-13 6t-5 12v232q0 44 32 76t75 31h36v304q0 22 16 38t37 16h375q23 0 50-12t42-26l85-85q15-16 27-43t11-49v-143h35q45 0 76-31t32-76z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="edit" unicode="&#xe84f;" d="M496 189l64 65-85 85-64-65v-31h53v-54h32z m245 402q-9 9-18 0l-196-196q-9-9 0-18t18 0l196 196q9 9 0 18z m45-331v-106q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-8-8-18-4-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v70q0 7 5 12l36 36q8 8 20 4t11-16z m-54 411l161-160-375-375h-161v160z m248-73l-51-52-161 161 51 52q16 15 38 15t38-15l85-85q16-16 16-38t-16-38z" horiz-adv-x="1000" />
+
+<glyph glyph-name="forward" unicode="&#xe850;" d="M1000 493q0-15-11-25l-285-286q-11-11-25-11t-25 11-11 25v143h-125q-55 0-98-3t-86-12-74-24-59-39-45-56-27-77-10-101q0-31 3-69 0-4 2-13t1-15q0-8-5-14t-13-6q-9 0-15 10-4 5-8 12t-7 17-6 13q-71 159-71 252 0 111 30 186 90 225 488 225h125v143q0 14 11 25t25 10 25-10l285-286q11-11 11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="reply" unicode="&#xe851;" d="M1000 225q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" horiz-adv-x="1000" />
+
+<glyph glyph-name="reply-all" unicode="&#xe852;" d="M357 246v-39q0-23-22-33-7-3-14-3-15 0-25 11l-285 286q-11 10-11 25t11 25l285 286q17 17 39 8 22-10 22-33v-39l-221-222q-11-11-11-25t11-25z m643-21q0-32-9-74t-22-77-27-70-22-51l-11-22q-5-10-16-10-3 0-5 1-14 4-13 19 24 223-59 315-36 40-95 62t-150 29v-140q0-23-21-33-8-3-14-3-15 0-25 11l-286 286q-11 10-11 25t11 25l286 286q16 17 39 8 21-10 21-33v-147q230-15 335-123 94-96 94-284z" horiz-adv-x="1000" />
+
+<glyph glyph-name="eye" unicode="&#xe853;" d="M929 314q-85 132-213 197 34-58 34-125 0-103-73-177t-177-73-177 73-73 177q0 67 34 125-128-65-213-197 75-114 187-182t242-68 243 68 186 182z m-402 215q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m473-215q0-19-11-38-78-129-210-206t-279-77-279 77-210 206q-11 19-11 38t11 39q78 128 210 205t279 78 279-78 210-205q11-20 11-39z" horiz-adv-x="1000" />
+
+<glyph glyph-name="tag" unicode="&#xe854;" d="M250 600q0 30-21 51t-50 20-51-20-21-51 21-50 51-21 50 21 21 50z m595-321q0-30-20-51l-274-274q-22-21-51-21-30 0-50 21l-399 399q-21 21-36 57t-15 65v232q0 29 21 50t50 22h233q29 0 65-15t57-36l399-399q20-21 20-50z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="tags" unicode="&#xe855;" d="M250 600q0 30-21 51t-50 20-51-20-21-51 21-50 51-21 50 21 21 50z m595-321q0-30-20-51l-274-274q-22-21-51-21-30 0-50 21l-399 399q-21 21-36 57t-15 65v232q0 29 21 50t50 22h233q29 0 65-15t57-36l399-399q20-21 20-50z m215 0q0-30-21-51l-274-274q-22-21-51-21-20 0-33 8t-29 25l262 262q21 21 21 51 0 29-21 50l-399 399q-21 21-57 36t-65 15h125q29 0 65-15t57-36l399-399q21-21 21-50z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="lock-open-alt" unicode="&#xe856;" d="M589 421q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v179q0 103 74 177t176 73 177-73 73-177q0-14-10-25t-25-11h-36q-14 0-25 11t-11 25q0 59-42 101t-101 42-101-42-41-101v-179h410z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="lock-open" unicode="&#xe857;" d="M929 529v-143q0-15-11-25t-25-11h-36q-14 0-25 11t-11 25v143q0 59-41 101t-101 41-101-41-42-101v-108h53q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h375v108q0 103 73 176t177 74 176-74 74-176z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="lock" unicode="&#xe858;" d="M179 421h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="home" unicode="&#xe859;" d="M786 296v-267q0-15-11-25t-25-11h-214v214h-143v-214h-214q-15 0-25 11t-11 25v267q0 1 0 2t0 2l321 264 321-264q1-1 1-4z m124 39l-34-41q-5-5-12-6h-2q-7 0-12 3l-386 322-386-322q-7-4-13-3-7 1-12 6l-35 41q-4 6-3 13t6 12l401 334q18 15 42 15t43-15l136-113v108q0 8 5 13t13 5h107q8 0 13-5t5-13v-227l122-102q6-4 6-12t-4-13z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="info" unicode="&#xe85a;" d="M357 100v-71q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v71q0 15 11 25t25 11h35v214h-35q-15 0-25 11t-11 25v71q0 15 11 25t25 11h214q15 0 25-11t11-25v-321h35q15 0 26-11t10-25z m-71 643v-107q0-15-11-25t-25-11h-143q-14 0-25 11t-11 25v107q0 14 11 25t25 11h143q15 0 25-11t11-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="help" unicode="&#xe85b;" d="M393 149v-134q0-9-7-15t-15-7h-134q-9 0-16 7t-7 15v134q0 9 7 16t16 6h134q9 0 15-6t7-16z m176 335q0-30-8-56t-20-43-31-33-32-25-34-19q-23-13-38-37t-15-37q0-10-7-18t-16-9h-134q-8 0-14 11t-6 20v26q0 46 37 87t79 60q33 16 47 32t14 42q0 24-26 41t-60 18q-36 0-60-16-20-14-60-64-7-9-17-9-7 0-14 4l-91 70q-8 6-9 14t3 16q89 148 259 148 45 0 90-17t81-46 59-72 23-88z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="search" unicode="&#xe85c;" d="M643 386q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="flapping" unicode="&#xe85d;" d="M372 582q-34-52-77-153-12 25-20 41t-23 35-28 32-36 19-45 8h-125q-8 0-13 5t-5 13v107q0 8 5 13t13 5h125q139 0 229-125z m628-446q0-8-5-13l-179-179q-5-5-12-5-8 0-13 6t-5 12v107q-18 0-48 0t-45-1-41 1-39 3-36 6-35 10-32 16-33 22-31 30-31 39q33 52 76 152 12-25 20-40t23-36 28-31 35-20 46-8h143v107q0 8 5 13t13 5q6 0 13-5l178-178q5-5 5-13z m0 500q0-8-5-13l-179-179q-5-5-12-5-8 0-13 6t-5 12v107h-143q-27 0-49-8t-38-25-29-34-25-44q-18-34-43-95-16-37-28-62t-30-59-36-55-41-47-50-38-60-23-71-10h-125q-8 0-13 5t-5 13v107q0 8 5 13t13 5h125q27 0 48 9t39 25 28 34 26 43q17 35 43 96 16 36 28 62t30 58 36 56 41 46 50 39 59 23 72 9h143v107q0 8 5 13t13 5q6 0 13-5l178-178q5-5 5-13z" horiz-adv-x="1000" />
+
+<glyph glyph-name="rewind" unicode="&#xe85e;" d="M532 736q170 0 289-120t119-290-119-290-289-120q-142 0-252 88l70 74q84-60 182-60 126 0 216 90t90 218-90 218-216 90q-124 0-214-87t-92-211l142 0-184-204-184 204 124 0q2 166 122 283t286 117z" horiz-adv-x="940" />
+
+<glyph glyph-name="chart-line" unicode="&#xe85f;" d="M1143-7v-72h-1143v858h71v-786h1072z m-72 696v-242q0-12-10-17t-20 4l-68 68-353-353q-6-6-13-6t-13 6l-130 130-232-233-107 108 327 326q5 6 12 6t13-6l130-130 259 259-67 68q-9 8-5 19t17 11h243q7 0 12-5t5-13z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="bell-off" unicode="&#xe860;" d="M869 375q35-199 167-311 0-29-21-50t-51-21h-250q0-59-42-101t-101-42-100 42-42 100z m-298-480q9 0 9 9t-9 8q-32 0-56 24t-24 57q0 9-9 9t-9-9q0-41 29-70t69-28z m560 893q4-6 4-14t-6-12l-1045-905q-5-5-13-4t-12 6l-47 53q-4 6-4 14t6 12l104 90q-11 17-11 36 28 24 51 49t47 67 42 89 28 115 11 145q0 84 65 157t171 89q-4 10-4 21 0 23 16 38t37 16 38-16 16-38q0-11-4-21 69-10 122-46t82-88l234 202q5 5 13 4t12-6z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="bell-off-empty" unicode="&#xe861;" d="M580-96q0 8-9 8-32 0-56 24t-24 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m-299 265l489 424q-23 49-74 82t-125 32q-51 0-94-17t-68-45-38-58-14-58q0-215-76-360z m755-105q0-29-21-50t-51-21h-250q0-59-42-101t-101-42-100 42-42 100l83 72h422q-92 105-126 256l61 55q35-199 167-311z m48 777l47-53q4-6 4-14t-6-12l-1045-905q-5-5-13-4t-12 6l-47 53q-4 6-4 14t6 12l104 90q-11 17-11 36 28 24 51 49t47 67 42 89 28 115 11 145q0 84 65 157t171 89q-4 10-4 21 0 23 16 38t37 16 38-16 16-38q0-11-4-21 69-10 122-46t82-88l234 202q5 5 13 4t12-6z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="plug" unicode="&#xe862;" d="M979 597q21-21 21-50t-21-51l-223-223 83-84-89-89q-91-91-217-104t-230 56l-202-202h-101v101l202 202q-69 103-56 230t104 217l89 89 84-83 223 223q21 21 51 21t50-21 21-50-21-51l-223-223 131-131 223 223q22 21 51 21t50-21z" horiz-adv-x="1000" />
+
+<glyph glyph-name="eye-off" unicode="&#xe863;" d="M310 105l43 79q-48 35-76 88t-27 114q0 67 34 125-128-65-213-197 94-144 239-209z m217 424q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m202 106q0-4 0-5-59-105-176-316t-176-316l-28-50q-5-9-15-9-7 0-75 39-9 6-9 16 0 7 25 49-80 36-147 96t-117 137q-11 17-11 38t11 39q86 131 212 207t277 76q50 0 100-10l31 54q5 9 15 9 3 0 10-3t18-9 18-10 18-10 10-7q9-5 9-15z m21-249q0-78-44-142t-117-91l157 280q4-25 4-47z m250-72q0-19-11-38-22-36-61-81-84-96-194-149t-234-53l41 74q119 10 219 76t169 171q-65 100-158 164l35 63q53-36 102-85t81-103q11-19 11-39z" horiz-adv-x="1000" />
+
+<glyph glyph-name="arrows-cw" unicode="&#xe864;" d="M843 261q0-3 0-4-36-150-150-243t-267-93q-81 0-157 31t-136 88l-72-72q-11-11-25-11t-25 11-11 25v250q0 14 11 25t25 11h250q14 0 25-11t10-25-10-25l-77-77q40-36 90-57t105-20q74 0 139 37t104 99q6 10 30 66 4 13 16 13h107q8 0 13-6t5-12z m14 446v-250q0-14-10-25t-26-11h-250q-14 0-25 11t-10 25 10 25l77 77q-82 77-194 77-75 0-140-37t-104-99q-6-10-29-66-5-13-17-13h-111q-7 0-13 6t-5 12v4q36 150 151 243t268 93q81 0 158-31t137-88l72 72q11 11 25 11t26-11 10-25z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="cw" unicode="&#xe865;" d="M408 760q168 0 287-116t123-282l122 0-184-206-184 206 144 0q-4 124-94 210t-214 86q-126 0-216-90t-90-218q0-126 90-216t216-90q104 0 182 60l70-76q-110-88-252-88-168 0-288 120t-120 290 120 290 288 120z" horiz-adv-x="940" />
+
+<glyph glyph-name="host" unicode="&#xe866;" d="M232 136q-37 0-63 26t-26 63v393q0 37 26 63t63 26h607q37 0 63-26t27-63v-393q0-37-27-63t-63-26h-607z m-18 482v-393q0-7 6-13t12-5h607q8 0 13 5t5 13v393q0 7-5 12t-13 6h-607q-7 0-12-6t-6-12z m768-518h89v-54q0-22-26-37t-63-16h-893q-36 0-63 16t-26 37v54h982z m-402-54q9 0 9 9t-9 9h-89q-9 0-9-9t9-9h89z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="thumbs-up" unicode="&#xe867;" d="M143 100q0 15-11 25t-25 11-25-11-11-25 11-25 25-11 25 11 11 25z m643 321q0 29-22 50t-50 22h-196q0 32 27 89t26 89q0 55-17 81t-72 27q-14-15-21-48t-17-70-33-61q-13-13-43-51-2-3-13-16t-18-23-19-24-22-25-22-19-22-15-20-6h-18v-357h18q7 0 18-1t18-4 21-6 20-7 20-6 16-6q118-41 191-41h67q107 0 107 93 0 15-2 31 16 9 26 30t10 41-10 38q29 28 29 67 0 14-5 31t-14 26q18 1 30 26t12 45z m71 1q0-50-27-91 5-18 5-38 0-43-21-81 1-12 1-24 0-56-33-99 0-78-48-123t-126-45h-72q-54 0-106 13t-121 36q-65 23-77 23h-161q-29 0-50 21t-21 50v357q0 30 21 51t50 21h153q20 13 77 86 32 42 60 72 13 14 19 48t17 70 35 60q22 21 50 21 47 0 84-18t57-57 20-104q0-51-27-107h98q58 0 101-42t42-100z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="thumbs-down" unicode="&#xe868;" d="M143 600q0 15-11 25t-25 11-25-11-11-25 11-25 25-11 25 11 11 25z m643-321q0 19-12 45t-30 26q8 10 14 27t5 31q0 38-29 66 10 17 10 38 0 21-10 41t-26 30q2 16 2 31 0 47-27 70t-76 23h-71q-73 0-191-41-3-1-16-5t-20-7-20-7-21-6-18-4-18-1h-18v-357h18q9 0 20-5t22-15 22-20 22-25 19-24 18-22 13-17q30-38 43-51 23-24 33-61t17-70 21-48q54 0 72 27t17 81q0 33-26 89t-27 89h196q28 0 50 22t22 50z m71-1q0-57-42-100t-101-42h-98q27-55 27-107 0-66-20-104-19-39-57-57t-84-18q-28 0-50 21-19 18-30 45t-14 51-10 47-17 36q-27 28-60 71-57 73-77 86h-153q-29 0-50 21t-21 51v357q0 29 21 50t50 21h161q12 0 77 23 72 24 125 36t111 13h63q78 0 126-44t48-121v-3q33-43 33-99 0-12-1-24 21-38 21-80 0-21-5-39 27-41 27-91z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="spinner" unicode="&#xe869;" d="M294 72q0-29-21-50t-51-21q-29 0-50 21t-21 50q0 30 21 51t50 21 51-21 21-51z m277-115q0-29-20-50t-51-21-50 21-21 50 21 51 50 21 51-21 20-51z m-392 393q0-30-21-50t-51-21-50 21-21 50 21 51 50 20 51-20 21-51z m670-278q0-29-21-50t-50-21q-30 0-51 21t-20 50 20 51 51 21 50-21 21-51z m-538 556q0-37-26-63t-63-26-63 26-26 63 26 63 63 26 63-26 26-63z m653-278q0-30-21-50t-50-21-51 21-21 50 21 51 51 20 50-20 21-51z m-357 393q0-45-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m296-115q0-52-37-88t-88-37q-52 0-88 37t-37 88q0 51 37 88t88 37q51 0 88-37t37-88z" horiz-adv-x="1000" />
+
+<glyph glyph-name="attach" unicode="&#xe86a;" d="M784 77q0-65-45-109t-109-44q-75 0-131 55l-434 434q-63 64-63 151 0 89 62 150t150 62q88 0 152-63l338-338q5-5 5-12 0-9-17-26t-26-17q-7 0-12 5l-339 339q-44 43-101 43-59 0-100-42t-40-101q0-58 42-101l433-433q35-36 81-36 36 0 59 24t24 59q0 46-35 81l-325 324q-14 14-33 14-16 0-27-11t-11-27q0-18 14-33l229-228q6-6 6-13 0-9-18-26t-26-17q-6 0-12 5l-229 229q-35 34-35 83 0 46 32 78t77 32q49 0 84-35l324-325q56-54 56-131z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="keyboard" unicode="&#xe86b;" d="M214 198v-53q0-9-9-9h-53q-9 0-9 9v53q0 9 9 9h53q9 0 9-9z m72 143v-53q0-9-9-9h-125q-9 0-9 9v53q0 9 9 9h125q9 0 9-9z m-72 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m572-286v-53q0-9-9-9h-482q-9 0-9 9v53q0 9 9 9h482q9 0 9-9z m-357 143v-53q0-9-9-9h-54q-9 0-9 9v53q0 9 9 9h54q9 0 9-9z m-72 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m214-143v-53q0-9-8-9h-54q-9 0-9 9v53q0 9 9 9h54q8 0 8-9z m-71 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m214-143v-53q0-9-9-9h-53q-9 0-9 9v53q0 9 9 9h53q9 0 9-9z m215-143v-53q0-9-9-9h-54q-9 0-9 9v53q0 9 9 9h54q9 0 9-9z m-286 286v-54q0-9-9-9h-54q-9 0-9 9v54q0 9 9 9h54q9 0 9-9z m143 0v-54q0-9-9-9h-54q-9 0-9 9v54q0 9 9 9h54q9 0 9-9z m143 0v-196q0-9-9-9h-125q-9 0-9 9v53q0 9 9 9h62v134q0 9 9 9h54q9 0 9-9z m71-420v500h-929v-500h929z m71 500v-500q0-29-20-50t-51-21h-929q-29 0-50 21t-21 50v500q0 30 21 51t50 21h929q30 0 51-21t20-51z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="menu" unicode="&#xe86c;" d="M857 100v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-14-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="wifi" unicode="&#xe86d;" d="M571 0q-11 0-51 41t-41 52q0 18 35 30t57 13 58-13 35-30q0-11-41-52t-52-41z m151 151q-1 0-22 14t-57 28-72 14-71-14-57-28-22-14q-10 0-52 42t-42 52q0 7 5 13 44 43 109 67t130 25 131-25 109-67q5-6 5-13 0-10-42-52t-52-42z m152 152q-6 0-12 5-76 58-141 86t-150 27q-47 0-95-12t-83-29-63-35-44-30-18-12q-9 0-51 42t-42 52q0 7 6 12 74 74 178 115t212 40 213-40 178-115q6-5 6-12 0-10-42-52t-52-42z m152 151q-6 0-13 5-99 88-207 132t-235 45-234-45-207-132q-7-5-13-5-9 0-51 42t-43 52q0 7 6 13 104 104 248 161t294 57 295-57 248-161q5-6 5-13 0-10-42-52t-51-42z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="moon" unicode="&#xe86e;" d="M704 123q-30-5-61-5-102 0-188 50t-137 137-50 188q0 107 58 199-112-33-183-128t-72-214q0-72 29-139t76-113 114-77 139-28q80 0 152 34t123 96z m114 47q-53-113-159-181t-230-68q-87 0-167 34t-136 92-92 137-34 166q0 85 32 163t87 135 132 92 161 38q25 1 34-22 11-23-8-40-48-43-73-101t-26-122q0-83 41-152t111-111 152-41q66 0 127 29 23 10 40-7 8-8 10-19t-2-22z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="chart-pie" unicode="&#xe86f;" d="M429 353l304-304q-59-61-138-94t-166-34q-117 0-216 58t-155 156-58 215 58 215 155 156 216 58v-426z m104-3h431q0-88-33-167t-94-138z m396 71h-429v429q117 0 215-57t156-156 58-216z" horiz-adv-x="1000" />
+
+<glyph glyph-name="chart-area" unicode="&#xe870;" d="M1143-7v-72h-1143v858h71v-786h1072z m-214 571l142-500h-928v322l250 321 321-321z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="chart-bar" unicode="&#xe871;" d="M357 350v-286h-143v286h143z m214 286v-572h-142v572h142z m572-643v-72h-1143v858h71v-786h1072z m-357 500v-429h-143v429h143z m214 214v-643h-143v643h143z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="beaker" unicode="&#xe872;" d="M852 42q31-50 12-85t-78-36h-643q-59 0-78 36t12 85l280 443v222h-36q-14 0-25 11t-10 25 10 25 25 11h286q15 0 25-11t11-25-11-25-25-11h-36v-222z m-435 405l-151-240h397l-152 240-11 17v243h-71v-243z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="magic" unicode="&#xe873;" d="M664 526l164 163-60 60-164-163z m250 163q0-15-10-25l-718-718q-10-10-25-10t-25 10l-111 111q-10 10-10 25t10 25l718 718q10 10 25 10t25-10l111-111q10-10 10-25z m-754 106l54-16-54-17-17-55-17 55-55 17 55 16 17 55z m195-90l109-34-109-33-34-109-33 109-109 33 109 34 33 109z m519-267l55-17-55-16-17-55-17 55-54 16 54 17 17 55z m-357 357l54-16-54-17-17-55-17 55-54 17 54 16 17 55z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="spin6" unicode="&#xe874;" d="M855 9c-189-190-520-172-705 13-190 190-200 494-28 695 11 13 21 26 35 34 36 23 85 18 117-13 30-31 35-76 16-112-5-9-9-15-16-22-140-151-145-379-8-516 153-153 407-121 542 34 106 122 142 297 77 451-83 198-305 291-510 222l0 1c236 82 492-24 588-252 71-167 37-355-72-493-11-15-23-29-36-42z" horiz-adv-x="1000" />
+
+<glyph glyph-name="down-small" unicode="&#xe875;" d="M505 346q15-15 15-37t-15-37l-245-245-245 245q-15 15-15 37t15 37 37 15 37-15l120-119 0 395q0 21 15 36t36 15 37-15 16-36l0-395 120 119q15 15 36 15t36-15z" horiz-adv-x="520" />
+
+<glyph glyph-name="left-small" unicode="&#xe876;" d="M595 403q21 0 36-16t15-37-15-37-36-15l-395 0 119-119q15-15 15-37t-15-37-36-15q-23 0-38 15l-245 245 245 245q15 15 37 15t37-15 15-37-15-37l-119-118 395 0z" horiz-adv-x="646" />
+
+<glyph glyph-name="right-small" unicode="&#xe877;" d="M328 595q15 15 36 15t37-15l245-245-245-245q-15-15-36-15-22 0-37 15t-15 37 15 37l120 119-395 0q-22 0-37 15t-16 37 16 37 37 16l395 0-120 118q-15 15-15 37t15 37z" horiz-adv-x="646" />
+
+<glyph glyph-name="up-small" unicode="&#xe878;" d="M260 673l245-245q15-15 15-37t-15-37-36-15-36 15l-120 120 0-395q0-21-16-37t-37-15-36 15-15 37l0 395-120-120q-15-15-37-15t-37 15-15 37 15 37z" horiz-adv-x="520" />
+
+<glyph glyph-name="pin" unicode="&#xe879;" d="M573 37q0-23-15-38t-37-15q-21 0-37 16l-169 169-315-236 236 315-168 169q-24 23-12 56 14 32 48 32 157 0 270 57 90 45 151 171 9 24 36 32t50-13l208-209q21-23 14-50t-32-36q-127-63-172-152-56-110-56-268z" horiz-adv-x="834" />
+
+<glyph glyph-name="angle-double-left" unicode="&#xe87a;" d="M350 82q0-7-6-13l-28-28q-5-5-12-5t-13 5l-260 261q-6 5-6 12t6 13l260 260q5 6 13 6t12-6l28-28q6-5 6-13t-6-12l-219-220 219-219q6-6 6-13z m214 0q0-7-5-13l-28-28q-6-5-13-5t-13 5l-260 261q-6 5-6 12t6 13l260 260q6 6 13 6t13-6l28-28q5-5 5-13t-5-12l-220-220 220-219q5-6 5-13z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="angle-double-right" unicode="&#xe87b;" d="M332 314q0-7-5-12l-261-261q-5-5-12-5t-13 5l-28 28q-6 6-6 13t6 13l219 219-219 220q-6 5-6 12t6 13l28 28q5 6 13 6t12-6l261-260q5-5 5-13z m214 0q0-7-5-12l-260-261q-6-5-13-5t-13 5l-28 28q-5 6-5 13t5 13l219 219-219 220q-5 5-5 12t5 13l28 28q6 6 13 6t13-6l260-260q5-5 5-13z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="circle" unicode="&#xe87c;" d="M857 350q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="info-circled" unicode="&#xe87d;" d="M454 810q190 2 326-130t140-322q2-190-131-327t-323-141q-190-2-327 131t-139 323q-4 190 130 327t324 139z m52-152q-42 0-65-24t-23-50q-2-28 15-44t49-16q38 0 61 22t23 54q0 58-60 58z m-120-594q30 0 84 26t106 78l-18 24q-48-36-72-36-14 0-4 38l42 160q26 96-22 96-30 0-89-29t-115-75l16-26q52 34 74 34 12 0 0-34l-36-152q-26-104 34-104z" horiz-adv-x="920" />
+
+<glyph glyph-name="twitter" unicode="&#xe87e;" d="M904 622q-37-54-90-93 0-8 0-23 0-73-21-145t-64-139-103-117-144-82-181-30q-151 0-276 81 19-2 43-2 126 0 224 77-59 1-105 36t-64 89q19-3 34-3 24 0 48 6-63 13-104 62t-41 115v2q38-21 82-23-37 25-59 64t-22 86q0 49 25 91 68-83 164-133t208-55q-5 21-5 41 0 75 53 127t127 53q79 0 132-57 61 12 115 44-21-64-80-100 52 6 104 28z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="facebook-squared" unicode="&#xe87f;" d="M696 779q67 0 114-48t47-113v-536q0-66-47-113t-114-48h-104v333h111l16 129h-127v83q0 31 13 46t51 16l68 1v115q-35 5-100 5-75 0-121-44t-45-127v-95h-112v-129h112v-333h-297q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="gplus-squared" unicode="&#xe880;" d="M512 345q0 15-4 36h-202v-74h122q-2-13-10-28t-21-29-37-25-54-10q-55 0-94 40t-39 95 39 95 94 40q52 0 86-33l58 57q-60 55-144 55-89 0-151-62t-63-152 63-151 151-63q92 0 149 58t57 151z m192-26h61v62h-61v61h-61v-61h-61v-62h61v-61h61v61z m153 299v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="attention-circled" unicode="&#xe881;" d="M429 779q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m71-696v106q0 8-5 13t-12 5h-107q-8 0-13-5t-6-13v-106q0-8 6-13t13-6h107q7 0 12 6t5 13z m-1 192l10 346q0 7-6 10-5 5-13 5h-123q-8 0-13-5-6-3-6-10l10-346q0-6 5-10t14-4h103q8 0 13 4t6 10z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="check" unicode="&#xe883;" d="M786 331v-177q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-6-5-13-5-1 0-5 1-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v141q0 8 5 13l36 35q6 6 13 6 3 0 7-2 11-4 11-16z m129 273l-455-454q-13-14-31-14t-32 14l-240 240q-14 13-14 31t14 32l61 62q14 13 32 13t32-13l147-147 361 361q13 13 31 13t32-13l62-61q13-14 13-32t-13-32z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="reschedule" unicode="&#xe884;" d="M186 140l116 116 0-292-276 16 88 86q-116 122-114 290t120 288q100 100 240 116l4-102q-100-16-172-88-88-88-90-213t84-217z m332 598l276-16-88-86q116-122 114-290t-120-288q-96-98-240-118l-2 104q98 16 170 88 88 88 90 213t-84 217l-114-116z" horiz-adv-x="820" />
+
+<glyph glyph-name="warning-empty" unicode="&#xe885;" d="M514 701q-49 0-81-55l-308-513q-32-55-11-95t87-40l625 0q65 0 87 40t-12 95l-307 513q-33 55-80 55z m0 105q106 0 169-107l308-513q63-105 12-199-52-93-177-93l-625 0q-123 0-177 93-53 92 11 199l309 513q62 107 170 107z m-69-652q0 69 69 69 67 0 67-69 0-67-67-67-69 0-69 67z m146 313q0-14-6-29l-71-179q-44 108-73 179-6 15-6 29 0 32 23 55t56 24 55-24 22-55z" horiz-adv-x="1026" />
+
+<glyph glyph-name="th-list" unicode="&#xf009;" d="M0 62q0-30 21-51t51-21h32q30 0 51 21t21 51-21 51-51 21h-32q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h32q30 0 51 21t21 51-21 51-51 21h-32q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h32q30 0 51 21t21 51-21 51-51 21h-32q-30 0-51-21t-21-51z m234-576q0-30 21-51t51-21h559q30 0 51 21t21 51-21 51-51 21h-559q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h559q30 0 51 21t21 51-21 51-51 21h-559q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h559q30 0 51 21t21 51-21 51-51 21h-559q-30 0-51-21t-21-51z" horiz-adv-x="937.5" />
+
+<glyph glyph-name="th-thumb-empty" unicode="&#xf00b;" d="M0-66v286q0 22 15 37t37 16h286q21 0 37-16t15-37v-286q0-21-15-36t-37-15h-286q-22 0-37 15t-15 36z m0 546v286q0 21 15 36t37 15h286q21 0 37-15t15-36v-286q0-22-15-37t-37-16h-286q-21 0-37 16t-15 37z m88-510h214v214h-214v-214z m0 546h214v213h-214v-213z m459-582v286q0 22 15 37t37 16h286q21 0 37-16t15-37v-286q0-21-15-36t-37-15h-286q-21 0-37 15t-15 36z m0 546v286q0 21 15 36t37 15h286q22 0 37-15t15-36v-286q0-22-15-37t-37-16h-286q-21 0-37 16t-15 37z m88-510h215v214h-215v-214z m0 546h215v213h-215v-213z" horiz-adv-x="937.5" />
+
+<glyph glyph-name="github-circled" unicode="&#xf09b;" d="M429 779q116 0 215-58t156-156 57-215q0-140-82-252t-211-155q-15-3-22 4t-7 17q0 1 0 43t0 75q0 54-29 79 32 3 57 10t53 22 45 37 30 58 11 84q0 67-44 115 21 51-4 114-16 5-46-6t-51-25l-21-13q-52 15-107 15t-108-15q-8 6-23 15t-47 22-47 7q-25-63-5-114-44-48-44-115 0-47 12-83t29-59 45-37 52-22 57-10q-21-20-27-58-12-5-25-8t-32-3-36 12-31 35q-11 18-27 29t-28 14l-11 1q-12 0-16-2t-3-7 5-8 7-6l4-3q12-6 24-21t18-29l6-13q7-21 24-34t37-17 39-3 31 1l13 3q0-22 0-50t1-30q0-10-8-17t-22-4q-129 43-211 155t-82 252q0 117 58 215t155 156 216 58z m-267-616q2 4-3 7-6 1-8-1-1-4 4-7 5-3 7 1z m18-19q4 3-1 9-6 5-9 2-4-3 1-9 5-6 9-2z m16-25q6 4 0 11-4 7-9 3-5-3 0-10t9-4z m24-23q4 4-2 10-7 7-11 2-5-5 2-11 6-6 11-1z m32-14q1 6-8 9-8 2-10-4t7-9q8-3 11 4z m35-3q0 7-10 6-9 0-9-6 0-7 10-6 9 0 9 6z m32 5q-1 7-10 5-9-1-8-8t10-4 8 7z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="angle-double-up" unicode="&#xf102;" d="M600 118q0-7-6-13l-28-28q-5-5-12-5t-13 5l-220 219-219-219q-5-5-13-5t-12 5l-28 28q-6 6-6 13t6 13l260 260q5 5 12 5t13-5l260-260q6-6 6-13z m0 214q0-7-6-13l-28-28q-5-5-12-5t-13 5l-220 220-219-220q-5-5-13-5t-12 5l-28 28q-6 6-6 13t6 13l260 260q5 6 12 6t13-6l260-260q6-6 6-13z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="angle-double-down" unicode="&#xf103;" d="M600 368q0-7-6-13l-260-260q-5-6-13-6t-12 6l-260 260q-6 6-6 13t6 13l28 28q5 5 12 5t13-5l219-220 220 220q5 5 13 5t12-5l28-28q6-6 6-13z m0 214q0-7-6-13l-260-260q-5-5-13-5t-12 5l-260 260q-6 6-6 13t6 13l28 28q5 6 12 6t13-6l219-219 220 219q5 6 13 6t12-6l28-28q6-6 6-13z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="angle-left" unicode="&#xf104;" d="M350 546q0-7-6-12l-219-220 219-219q6-6 6-13t-6-13l-28-28q-5-5-12-5t-13 5l-260 261q-6 5-6 12t6 13l260 260q5 6 13 6t12-6l28-28q6-5 6-13z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="angle-right" unicode="&#xf105;" d="M332 314q0-7-5-12l-261-261q-5-5-12-5t-13 5l-28 28q-6 6-6 13t6 13l219 219-219 220q-6 5-6 12t6 13l28 28q5 6 13 6t12-6l261-260q5-5 5-13z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="angle-up" unicode="&#xf106;" d="M600 189q0-7-6-12l-28-28q-5-6-12-6t-13 6l-220 219-219-219q-5-6-13-6t-12 6l-28 28q-6 5-6 12t6 13l260 260q5 6 12 6t13-6l260-260q6-5 6-13z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="angle-down" unicode="&#xf107;" d="M600 439q0-7-6-12l-260-261q-5-5-13-5t-12 5l-260 261q-6 5-6 12t6 13l28 28q5 6 12 6t13-6l219-219 220 219q5 6 13 6t12-6l28-28q6-5 6-13z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="history" unicode="&#xf1da;" d="M857 350q0-87-34-166t-91-137-137-92-166-34q-96 0-183 41t-147 114q-4 6-4 13t5 11l76 77q6 5 14 5 9-1 13-7 41-53 100-82t126-29q58 0 110 23t92 61 61 91 22 111-22 111-61 91-92 61-110 23q-55 0-105-20t-90-57l77-77q17-16 8-38-10-23-33-23h-250q-15 0-25 11t-11 25v250q0 24 22 33 22 10 39-8l72-72q60 57 137 88t159 31q87 0 166-34t137-92 91-137 34-166z m-357 161v-250q0-8-5-13t-13-5h-178q-8 0-13 5t-5 13v35q0 8 5 13t13 5h125v197q0 8 5 13t12 5h36q8 0 13-5t5-13z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="binoculars" unicode="&#xf1e5;" d="M393 671v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />
+</font>
+</defs>
+</svg> \ No newline at end of file
diff --git a/application/fonts/fontello-ifont/font/ifont.ttf b/application/fonts/fontello-ifont/font/ifont.ttf
new file mode 100644
index 0000000..2853b70
--- /dev/null
+++ b/application/fonts/fontello-ifont/font/ifont.ttf
Binary files differ
diff --git a/application/fonts/fontello-ifont/font/ifont.woff b/application/fonts/fontello-ifont/font/ifont.woff
new file mode 100644
index 0000000..d6d485d
--- /dev/null
+++ b/application/fonts/fontello-ifont/font/ifont.woff
Binary files differ
diff --git a/application/fonts/fontello-ifont/font/ifont.woff2 b/application/fonts/fontello-ifont/font/ifont.woff2
new file mode 100644
index 0000000..948e103
--- /dev/null
+++ b/application/fonts/fontello-ifont/font/ifont.woff2
Binary files differ
diff --git a/application/fonts/icingaweb.md b/application/fonts/icingaweb.md
new file mode 100644
index 0000000..5699f07
--- /dev/null
+++ b/application/fonts/icingaweb.md
@@ -0,0 +1,9 @@
+# fontello-ifont font files moved
+
+New target is: public/font
+
+The font directory has been moved to the public structure because of
+Internet Explorer version 8 compatibility. The common way for browsers is to
+include the binary embeded font type in the javascript. IE8 falls back and
+include one of the provided font sources. Therefore it is important to have
+the font files available public and exported by the HTTP server.
diff --git a/application/forms/Account/ChangePasswordForm.php b/application/forms/Account/ChangePasswordForm.php
new file mode 100644
index 0000000..5bca11c
--- /dev/null
+++ b/application/forms/Account/ChangePasswordForm.php
@@ -0,0 +1,123 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Account;
+
+use Icinga\Authentication\User\DbUserBackend;
+use Icinga\Data\Filter\Filter;
+use Icinga\User;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for changing user passwords
+ */
+class ChangePasswordForm extends Form
+{
+ /**
+ * The user backend
+ *
+ * @var DbUserBackend
+ */
+ protected $backend;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setSubmitLabel($this->translate('Update Account'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'password',
+ 'old_password',
+ array(
+ 'label' => $this->translate('Old Password'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'new_password',
+ array(
+ 'label' => $this->translate('New Password'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'new_password_confirmation',
+ array(
+ 'label' => $this->translate('Confirm New Password'),
+ 'required' => true,
+ 'validators' => array(
+ array('identical', false, array('new_password'))
+ )
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ $backend = $this->getBackend();
+ $backend->update(
+ $backend->getBaseTable(),
+ array('password' => $this->getElement('new_password')->getValue()),
+ Filter::where('user_name', $this->Auth()->getUser()->getUsername())
+ );
+ Notification::success($this->translate('Account updated'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+ if (! $valid) {
+ return false;
+ }
+
+ $oldPasswordEl = $this->getElement('old_password');
+
+ if (! $this->backend->authenticate($this->Auth()->getUser(), $oldPasswordEl->getValue())) {
+ $oldPasswordEl->addError($this->translate('Old password is invalid'));
+ $this->markAsError();
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the user backend
+ *
+ * @return DbUserBackend
+ */
+ public function getBackend()
+ {
+ return $this->backend;
+ }
+
+ /**
+ * Set the user backend
+ *
+ * @param DbUserBackend $backend
+ *
+ * @return $this
+ */
+ public function setBackend(DbUserBackend $backend)
+ {
+ $this->backend = $backend;
+ return $this;
+ }
+}
diff --git a/application/forms/AcknowledgeApplicationStateMessageForm.php b/application/forms/AcknowledgeApplicationStateMessageForm.php
new file mode 100644
index 0000000..61f5824
--- /dev/null
+++ b/application/forms/AcknowledgeApplicationStateMessageForm.php
@@ -0,0 +1,75 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Application\Hook\ApplicationStateHook;
+use Icinga\Web\ApplicationStateCookie;
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+
+class AcknowledgeApplicationStateMessageForm extends Form
+{
+ public function init()
+ {
+ $this->setAction(Url::fromPath('application-state/acknowledge-message'));
+ $this->setAttrib('class', 'application-state-acknowledge-message-control');
+ $this->setRedirectUrl('application-state/summary');
+ }
+
+ public function addSubmitButton()
+ {
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ [
+ 'class' => 'link-button spinner',
+ 'decorators' => [
+ 'ViewHelper',
+ ['HtmlTag', ['tag' => 'div', 'class' => 'control-group form-controls']]
+ ],
+ 'escape' => false,
+ 'ignore' => true,
+ 'label' => $this->getView()->icon('cancel'),
+ 'title' => $this->translate('Acknowledge message'),
+ 'type' => 'submit'
+ ]
+ );
+ return $this;
+ }
+
+ public function createElements(array $formData = [])
+ {
+ $this->addElements(
+ [
+ [
+ 'hidden',
+ 'id',
+ [
+ 'required' => true,
+ 'validators' => ['NotEmpty'],
+ 'decorators' => ['ViewHelper']
+ ]
+ ]
+ ]
+ );
+
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ $cookie = new ApplicationStateCookie();
+
+ $ack = $cookie->getAcknowledgedMessages();
+ $ack[] = $this->getValue('id');
+
+ $active = ApplicationStateHook::getAllMessages();
+
+ $cookie->setAcknowledgedMessages(array_keys(array_intersect_key($active, array_flip($ack))));
+
+ $this->getResponse()->setCookie($cookie);
+
+ return true;
+ }
+}
diff --git a/application/forms/ActionForm.php b/application/forms/ActionForm.php
new file mode 100644
index 0000000..5b5b6ed
--- /dev/null
+++ b/application/forms/ActionForm.php
@@ -0,0 +1,78 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Web\Form;
+
+class ActionForm extends Form
+{
+ /**
+ * The icon shown on the button
+ *
+ * @var string
+ */
+ protected $icon = 'arrows-cw';
+
+ /**
+ * Set the icon to show on the button
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setIcon($name)
+ {
+ $this->icon = (string) $name;
+ return $this;
+ }
+
+ public function init()
+ {
+ $this->setAttrib('class', 'inline');
+ $this->setUidDisabled(true);
+ $this->setDecorators(['FormElements', 'Form']);
+ }
+
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'hidden',
+ 'identifier',
+ [
+ 'required' => true,
+ 'decorators' => ['ViewHelper']
+ ]
+ );
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ [
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-button spinner',
+ 'value' => 'btn_submit',
+ 'decorators' => ['ViewHelper'],
+ 'label' => $this->getView()->icon($this->icon),
+ 'title' => $this->getDescription()
+ ]
+ );
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid) {
+ $valid = ConfigFormEventsHook::runIsValid($this);
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ ConfigFormEventsHook::runOnSuccess($this);
+ }
+}
diff --git a/application/forms/Announcement/AcknowledgeAnnouncementForm.php b/application/forms/Announcement/AcknowledgeAnnouncementForm.php
new file mode 100644
index 0000000..85fecdc
--- /dev/null
+++ b/application/forms/Announcement/AcknowledgeAnnouncementForm.php
@@ -0,0 +1,92 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Announcement;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Web\Announcement\AnnouncementCookie;
+use Icinga\Web\Announcement\AnnouncementIniRepository;
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+
+class AcknowledgeAnnouncementForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setAction(Url::fromPath('announcements/acknowledge'));
+ $this->setAttrib('class', 'acknowledge-announcement-control');
+ $this->setRedirectUrl('layout/announcements');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addSubmitButton()
+ {
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ array(
+ 'class' => 'link-button spinner',
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'escape' => false,
+ 'ignore' => true,
+ 'label' => $this->getView()->icon('cancel'),
+ 'title' => $this->translate('Acknowledge this announcement'),
+ 'type' => 'submit'
+ )
+ );
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData = array())
+ {
+ $this->addElements(
+ array(
+ array(
+ 'hidden',
+ 'hash',
+ array(
+ 'required' => true,
+ 'validators' => array('NotEmpty'),
+ 'decorators' => array('ViewHelper')
+ )
+ )
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ $cookie = new AnnouncementCookie();
+ $repo = new AnnouncementIniRepository();
+ $query = $repo->findActive();
+ $filter = array();
+ foreach ($cookie->getAcknowledged() as $hash) {
+ $filter[] = Filter::expression('hash', '=', $hash);
+ }
+ $query->addFilter(Filter::matchAny($filter));
+ $acknowledged = array();
+ foreach ($query as $row) {
+ $acknowledged[] = $row->hash;
+ }
+ $acknowledged[] = $this->getElement('hash')->getValue();
+ $cookie->setAcknowledged($acknowledged);
+ $this->getResponse()->setCookie($cookie);
+ return true;
+ }
+}
diff --git a/application/forms/Announcement/AnnouncementForm.php b/application/forms/Announcement/AnnouncementForm.php
new file mode 100644
index 0000000..4da47e2
--- /dev/null
+++ b/application/forms/Announcement/AnnouncementForm.php
@@ -0,0 +1,135 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Announcement;
+
+use DateTime;
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\RepositoryForm;
+
+/**
+ * Create, update and delete announcements
+ */
+class AnnouncementForm extends RepositoryForm
+{
+ protected function fetchEntry()
+ {
+ $entry = parent::fetchEntry();
+ if ($entry !== false) {
+ if ($entry->start !== null) {
+ $entry->start = (new DateTime())->setTimestamp($entry->start);
+ }
+ if ($entry->end !== null) {
+ $entry->end = (new DateTime())->setTimestamp($entry->end);
+ }
+ }
+
+ return $entry;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function createInsertElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'author',
+ array(
+ 'disabled' => ! $this->getRequest()->isApiRequest(),
+ 'required' => true,
+ 'value' => Auth::getInstance()->getUser()->getUsername()
+ )
+ );
+ $this->addElement(
+ 'textarea',
+ 'message',
+ array(
+ 'description' => $this->translate('The message to display to users'),
+ 'label' => $this->translate('Message'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'dateTimePicker',
+ 'start',
+ array(
+ 'description' => $this->translate('The time to display the announcement from'),
+ 'label' => $this->translate('Start'),
+ 'placeholder' => new DateTime('tomorrow'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'dateTimePicker',
+ 'end',
+ array(
+ 'description' => $this->translate('The time to display the announcement until'),
+ 'label' => $this->translate('End'),
+ 'placeholder' => new DateTime('tomorrow +1day'),
+ 'required' => true
+ )
+ );
+
+ $this->setTitle($this->translate('Create a new announcement'));
+ $this->setSubmitLabel($this->translate('Create'));
+ }
+ /**
+ * {@inheritDoc}
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+ $this->setTitle(sprintf($this->translate('Edit announcement %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove announcement %s?'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Yes'));
+ $this->setAttrib('class', 'icinga-controls');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function createFilter()
+ {
+ return Filter::where('id', $this->getIdentifier());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getInsertMessage($success)
+ {
+ return $success
+ ? $this->translate('Announcement created')
+ : $this->translate('Failed to create announcement');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getUpdateMessage($success)
+ {
+ return $success
+ ? $this->translate('Announcement updated')
+ : $this->translate('Failed to update announcement');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getDeleteMessage($success)
+ {
+ return $success
+ ? $this->translate('Announcement removed')
+ : $this->translate('Failed to remove announcement');
+ }
+}
diff --git a/application/forms/Authentication/LoginForm.php b/application/forms/Authentication/LoginForm.php
new file mode 100644
index 0000000..87b32ab
--- /dev/null
+++ b/application/forms/Authentication/LoginForm.php
@@ -0,0 +1,214 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Authentication;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Application\Hook\AuthenticationHook;
+use Icinga\Application\Logger;
+use Icinga\Authentication\Auth;
+use Icinga\Authentication\User\ExternalBackend;
+use Icinga\Common\Database;
+use Icinga\Exception\Http\HttpBadRequestException;
+use Icinga\User;
+use Icinga\Web\Form;
+use Icinga\Web\RememberMe;
+use Icinga\Web\Url;
+
+/**
+ * Form for user authentication
+ */
+class LoginForm extends Form
+{
+ use Database;
+
+ const DEFAULT_CLASSES = 'icinga-controls';
+
+ /**
+ * Redirect URL
+ */
+ const REDIRECT_URL = 'dashboard';
+
+ public static $defaultElementDecorators = [
+ ['ViewHelper', ['separator' => '']],
+ ['Help', []],
+ ['Errors', ['separator' => '']],
+ ['HtmlTag', ['tag' => 'div', 'class' => 'control-group']]
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setRequiredCue(null);
+ $this->setName('form_login');
+ $this->setSubmitLabel($this->translate('Login'));
+ $this->setProgressLabel($this->translate('Logging in'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'username',
+ array(
+ 'autocapitalize' => 'off',
+ 'autocomplete' => 'username',
+ 'class' => false === isset($formData['username']) ? 'autofocus' : '',
+ 'placeholder' => $this->translate('Username'),
+ 'required' => true
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'autocomplete' => 'current-password',
+ 'placeholder' => $this->translate('Password'),
+ 'class' => isset($formData['username']) ? 'autofocus' : ''
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'rememberme',
+ [
+ 'label' => $this->translate('Stay logged in'),
+ 'decorators' => [
+ ['ViewHelper', ['separator' => '']],
+ ['Label', [
+ 'tag' => 'span',
+ 'separator' => '',
+ 'class' => 'control-label',
+ 'placement' => 'APPEND'
+ ]],
+ ['Help', []],
+ ['Errors', ['separator' => '']],
+ ['HtmlTag', ['tag' => 'div', 'class' => 'control-group remember-me-box']]
+ ]
+ ]
+ );
+ if (! RememberMe::isSupported()) {
+ $this->getElement('rememberme')
+ ->setAttrib('disabled', true)
+ ->setDescription($this->translate(
+ 'Staying logged in requires a database configuration backend'
+ . ' and an appropriate OpenSSL encryption method'
+ ));
+ }
+
+ $this->addElement(
+ 'hidden',
+ 'redirect',
+ array(
+ 'value' => Url::fromRequest()->getParam('redirect')
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRedirectUrl()
+ {
+ $redirect = null;
+ if ($this->created) {
+ $redirect = $this->getElement('redirect')->getValue();
+ }
+
+ if (empty($redirect) || strpos($redirect, 'authentication/logout') !== false) {
+ $redirect = static::REDIRECT_URL;
+ }
+
+ $redirectUrl = Url::fromPath($redirect);
+ if ($redirectUrl->isExternal()) {
+ throw new HttpBadRequestException('nope');
+ }
+
+ return $redirectUrl;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ $auth = Auth::getInstance();
+ $authChain = $auth->getAuthChain();
+ $authChain->setSkipExternalBackends(true);
+ $user = new User($this->getElement('username')->getValue());
+ if (! $user->hasDomain()) {
+ $user->setDomain(Config::app()->get('authentication', 'default_domain'));
+ }
+ $password = $this->getElement('password')->getValue();
+ $authenticated = $authChain->authenticate($user, $password);
+ if ($authenticated) {
+ $auth->setAuthenticated($user);
+ if ($this->getElement('rememberme')->isChecked()) {
+ try {
+ $rememberMe = RememberMe::fromCredentials($user->getUsername(), $password);
+ $this->getResponse()->setCookie($rememberMe->getCookie());
+ $rememberMe->persist();
+ } catch (Exception $e) {
+ Logger::error('Failed to let user "%s" stay logged in: %s', $user->getUsername(), $e);
+ }
+ }
+
+ // Call provided AuthenticationHook(s) after successful login
+ AuthenticationHook::triggerLogin($user);
+ $this->getResponse()->setRerenderLayout(true);
+ return true;
+ }
+ switch ($authChain->getError()) {
+ case $authChain::EEMPTY:
+ $this->addError($this->translate(
+ 'No authentication methods available.'
+ . ' Did you create authentication.ini when setting up Icinga Web 2?'
+ ));
+ break;
+ case $authChain::EFAIL:
+ $this->addError($this->translate(
+ 'All configured authentication methods failed.'
+ . ' Please check the system log or Icinga Web 2 log for more information.'
+ ));
+ break;
+ /** @noinspection PhpMissingBreakStatementInspection */
+ case $authChain::ENOTALL:
+ $this->addError($this->translate(
+ 'Please note that not all authentication methods were available.'
+ . ' Check the system log or Icinga Web 2 log for more information.'
+ ));
+ // Move to default
+ default:
+ $this->getElement('password')->addError($this->translate('Incorrect username or password'));
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onRequest()
+ {
+ $auth = Auth::getInstance();
+ $onlyExternal = true;
+ // TODO(el): This may be set on the auth chain once iterated. See Auth::authExternal().
+ foreach ($auth->getAuthChain() as $backend) {
+ if (! $backend instanceof ExternalBackend) {
+ $onlyExternal = false;
+ }
+ }
+ if ($onlyExternal) {
+ $this->addError($this->translate(
+ 'You\'re currently not authenticated using any of the web server\'s authentication mechanisms.'
+ . ' Make sure you\'ll configure such, otherwise you\'ll not be able to login.'
+ ));
+ }
+ }
+}
diff --git a/application/forms/AutoRefreshForm.php b/application/forms/AutoRefreshForm.php
new file mode 100644
index 0000000..122f635
--- /dev/null
+++ b/application/forms/AutoRefreshForm.php
@@ -0,0 +1,83 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Application\Logger;
+use Icinga\User\Preferences;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Session;
+use Icinga\Web\Url;
+
+/**
+ * Form class to adjust user auto refresh preferences
+ */
+class AutoRefreshForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_auto_refresh');
+ // Post against the current location
+ $this->setAction('');
+ }
+
+ /**
+ * Adjust preferences and persist them
+ *
+ * @see Form::onSuccess()
+ */
+ public function onSuccess()
+ {
+ /** @var Preferences $preferences */
+ $preferences = $this->getRequest()->getUser()->getPreferences();
+ $icingaweb = $preferences->get('icingaweb');
+
+ if ((bool) $preferences->getValue('icingaweb', 'auto_refresh', true) === false) {
+ $icingaweb['auto_refresh'] = '1';
+ $notification = $this->translate('Auto refresh successfully enabled');
+ } else {
+ $icingaweb['auto_refresh'] = '0';
+ $notification = $this->translate('Auto refresh successfully disabled');
+ }
+ $preferences->icingaweb = $icingaweb;
+
+ Session::getSession()->user->setPreferences($preferences);
+ Notification::success($notification);
+
+ $this->getResponse()->setHeader('X-Icinga-Rerender-Layout', 'yes');
+ $this->setRedirectUrl(Url::fromRequest()->without('renderLayout'));
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $preferences = $this->getRequest()->getUser()->getPreferences();
+
+ if ((bool) $preferences->getValue('icingaweb', 'auto_refresh', true) === false) {
+ $value = $this->translate('Enable auto refresh');
+ } else {
+ $value = $this->translate('Disable auto refresh');
+ }
+
+ $this->addElements(array(
+ array(
+ 'button',
+ 'btn_submit',
+ array(
+ 'ignore' => true,
+ 'type' => 'submit',
+ 'value' => $value,
+ 'decorators' => array('ViewHelper'),
+ 'escape' => false,
+ 'class' => 'link-like'
+ )
+ )
+ ));
+ }
+}
diff --git a/application/forms/Config/General/ApplicationConfigForm.php b/application/forms/Config/General/ApplicationConfigForm.php
new file mode 100644
index 0000000..21f76a1
--- /dev/null
+++ b/application/forms/Config/General/ApplicationConfigForm.php
@@ -0,0 +1,105 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Application\Icinga;
+use Icinga\Data\ResourceFactory;
+use Icinga\Web\Form;
+
+/**
+ * Configuration form for general application options
+ *
+ * This form is not used directly but as subform to the {@link GeneralConfigForm}.
+ */
+class ApplicationConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_application');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'checkbox',
+ 'global_show_stacktraces',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Show Stacktraces'),
+ 'description' => $this->translate(
+ 'Set whether to show an exception\'s stacktrace by default. This can also'
+ . ' be set in a user\'s preferences with the appropriate permission.'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'global_show_application_state_messages',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Show Application State Messages'),
+ 'description' => $this->translate(
+ "Set whether to show application state messages."
+ . " This can also be set in a user's preferences."
+ )
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'security_use_strict_csp',
+ [
+ 'label' => $this->translate('Enable strict content security policy'),
+ 'description' => $this->translate(
+ 'Set whether to to use strict content security policy (CSP).'
+ . ' This setting helps to protect from cross-site scripting (XSS).'
+ )
+ ]
+ );
+
+ $this->addElement(
+ 'text',
+ 'global_module_path',
+ array(
+ 'label' => $this->translate('Module Path'),
+ 'required' => true,
+ 'value' => implode(':', Icinga::app()->getModuleManager()->getModuleDirs()),
+ 'description' => $this->translate(
+ 'Contains the directories that will be searched for available modules, separated by '
+ . 'colons. Modules that don\'t exist in these directories can still be symlinked in '
+ . 'the module folder, but won\'t show up in the list of disabled modules.'
+ )
+ )
+ );
+
+ $backends = array_keys(ResourceFactory::getResourceConfigs()->toArray());
+ $backends = array_combine($backends, $backends);
+
+ $this->addElement(
+ 'select',
+ 'global_config_resource',
+ array(
+ 'required' => true,
+ 'multiOptions' => array_merge(
+ ['' => sprintf(' - %s - ', $this->translate('Please choose'))],
+ $backends
+ ),
+ 'disable' => [''],
+ 'value' => '',
+ 'label' => $this->translate('Configuration Database')
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php b/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php
new file mode 100644
index 0000000..0ff6c32
--- /dev/null
+++ b/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php
@@ -0,0 +1,46 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Web\Form;
+
+/**
+ * Configuration form for the default domain for authentication
+ *
+ * This form is not used directly but as subform to the {@link GeneralConfigForm}.
+ */
+class DefaultAuthenticationDomainConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_authentication');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'authentication_default_domain',
+ array(
+ 'label' => $this->translate('Default Login Domain'),
+ 'description' => $this->translate(
+ 'If a user logs in without specifying any domain (e.g. "jdoe" instead of "jdoe@example.com"),'
+ . ' this default domain will be assumed for the user. Note that if none your LDAP authentication'
+ . ' backends are configured to be responsible for this domain or if none of your authentication'
+ . ' backends holds usernames with the domain part, users will not be able to login.'
+ )
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/General/LoggingConfigForm.php b/application/forms/Config/General/LoggingConfigForm.php
new file mode 100644
index 0000000..bbc7723
--- /dev/null
+++ b/application/forms/Config/General/LoggingConfigForm.php
@@ -0,0 +1,142 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Application\Logger;
+use Icinga\Application\Logger\Writer\SyslogWriter;
+use Icinga\Application\Platform;
+use Icinga\Web\Form;
+
+/**
+ * Configuration form for logging options
+ *
+ * This form is not used directly but as subform for the {@link GeneralConfigForm}.
+ */
+class LoggingConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_logging');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $defaultType = getenv('ICINGAWEB_OFFICIAL_DOCKER_IMAGE') ? 'php' : 'syslog';
+
+ $this->addElement(
+ 'select',
+ 'logging_log',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Logging Type'),
+ 'description' => $this->translate('The type of logging to utilize.'),
+ 'value' => $defaultType,
+ 'multiOptions' => array(
+ 'syslog' => 'Syslog',
+ 'php' => $this->translate('Webserver Log', 'app.config.logging.type'),
+ 'file' => $this->translate('File', 'app.config.logging.type'),
+ 'none' => $this->translate('None', 'app.config.logging.type')
+ )
+ )
+ );
+
+ if (! isset($formData['logging_log']) || $formData['logging_log'] !== 'none') {
+ $this->addElement(
+ 'select',
+ 'logging_level',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Logging Level'),
+ 'description' => $this->translate('The maximum logging level to emit.'),
+ 'multiOptions' => array(
+ Logger::$levels[Logger::ERROR] => $this->translate('Error', 'app.config.logging.level'),
+ Logger::$levels[Logger::WARNING] => $this->translate('Warning', 'app.config.logging.level'),
+ Logger::$levels[Logger::INFO] => $this->translate('Information', 'app.config.logging.level'),
+ Logger::$levels[Logger::DEBUG] => $this->translate('Debug', 'app.config.logging.level')
+ )
+ )
+ );
+ }
+
+ if (! isset($formData['logging_log']) || in_array($formData['logging_log'], array('syslog', 'php'))) {
+ $this->addElement(
+ 'text',
+ 'logging_application',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Application Prefix'),
+ 'description' => $this->translate(
+ 'The name of the application by which to prefix log messages.'
+ ),
+ 'requirement' => $this->translate('The application prefix must not contain whitespace.'),
+ 'value' => 'icingaweb2',
+ 'validators' => array(
+ array(
+ 'Regex',
+ false,
+ array(
+ 'pattern' => '/^\S+$/',
+ 'messages' => array(
+ 'regexNotMatch' => $this->translate(
+ 'The application prefix must not contain whitespace.'
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+ if ((isset($formData['logging_log']) ? $formData['logging_log'] : $defaultType) === 'syslog') {
+ if (Platform::isWindows()) {
+ /* @see https://secure.php.net/manual/en/function.openlog.php */
+ $this->addElement(
+ 'hidden',
+ 'logging_facility',
+ array(
+ 'value' => 'user',
+ 'disabled' => true
+ )
+ );
+ } else {
+ $facilities = array_keys(SyslogWriter::$facilities);
+ $this->addElement(
+ 'select',
+ 'logging_facility',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Facility'),
+ 'description' => $this->translate('The syslog facility to utilize.'),
+ 'value' => 'user',
+ 'multiOptions' => array_combine($facilities, $facilities)
+ )
+ );
+ }
+ }
+ } elseif (isset($formData['logging_log']) && $formData['logging_log'] === 'file') {
+ $this->addElement(
+ 'text',
+ 'logging_file',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('File path'),
+ 'description' => $this->translate('The full path to the log file to write messages to.'),
+ 'value' => '/var/log/icingaweb2/icingaweb2.log',
+ 'validators' => array('WritablePathValidator')
+ )
+ );
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/General/ThemingConfigForm.php b/application/forms/Config/General/ThemingConfigForm.php
new file mode 100644
index 0000000..54ef2b1
--- /dev/null
+++ b/application/forms/Config/General/ThemingConfigForm.php
@@ -0,0 +1,78 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Web\Form;
+use Icinga\Web\StyleSheet;
+
+/**
+ * Configuration form for theming options
+ *
+ * This form is not used directly but as subform for the {@link GeneralConfigForm}.
+ */
+class ThemingConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_theming');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $themes = Icinga::app()->getThemes();
+ $themes[StyleSheet::DEFAULT_THEME] .= ' (' . $this->translate('default') . ')';
+
+ $this->addElement(
+ 'select',
+ 'themes_default',
+ array(
+ 'description' => $this->translate('The default theme', 'Form element description'),
+ 'disabled' => count($themes) < 2 ? 'disabled' : null,
+ 'label' => $this->translate('Default Theme', 'Form element label'),
+ 'multiOptions' => $themes,
+ 'value' => StyleSheet::DEFAULT_THEME
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'themes_disabled',
+ array(
+ 'description' => $this->translate(
+ 'Check this box for disallowing users to change the theme. If a default theme is set, it will be'
+ . ' used nonetheless',
+ 'Form element description'
+ ),
+ 'label' => $this->translate('Users Can\'t Change Theme', 'Form element label')
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ if ($values['themes_default'] === '' || $values['themes_default'] === StyleSheet::DEFAULT_THEME) {
+ $values['themes_default'] = null;
+ }
+ if (! $values['themes_disabled']) {
+ $values['themes_disabled'] = null;
+ }
+ return $values;
+ }
+}
diff --git a/application/forms/Config/GeneralConfigForm.php b/application/forms/Config/GeneralConfigForm.php
new file mode 100644
index 0000000..5f15512
--- /dev/null
+++ b/application/forms/Config/GeneralConfigForm.php
@@ -0,0 +1,40 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use Icinga\Forms\Config\General\ApplicationConfigForm;
+use Icinga\Forms\Config\General\DefaultAuthenticationDomainConfigForm;
+use Icinga\Forms\Config\General\LoggingConfigForm;
+use Icinga\Forms\Config\General\ThemingConfigForm;
+use Icinga\Forms\ConfigForm;
+
+/**
+ * Configuration form for application-wide options
+ */
+class GeneralConfigForm extends ConfigForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $appConfigForm = new ApplicationConfigForm();
+ $loggingConfigForm = new LoggingConfigForm();
+ $themingConfigForm = new ThemingConfigForm();
+ $domainConfigForm = new DefaultAuthenticationDomainConfigForm();
+ $this->addSubForm($appConfigForm->create($formData));
+ $this->addSubForm($loggingConfigForm->create($formData));
+ $this->addSubForm($themingConfigForm->create($formData));
+ $this->addSubForm($domainConfigForm->create($formData));
+ }
+}
diff --git a/application/forms/Config/Resource/DbResourceForm.php b/application/forms/Config/Resource/DbResourceForm.php
new file mode 100644
index 0000000..c9d7601
--- /dev/null
+++ b/application/forms/Config/Resource/DbResourceForm.php
@@ -0,0 +1,239 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Icinga\Application\Platform;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying database resources
+ */
+class DbResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_db');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ $dbChoices = array();
+ if (Platform::hasMysqlSupport()) {
+ $dbChoices['mysql'] = 'MySQL';
+ }
+ if (Platform::hasPostgresqlSupport()) {
+ $dbChoices['pgsql'] = 'PostgreSQL';
+ }
+ if (Platform::hasMssqlSupport()) {
+ $dbChoices['mssql'] = 'MSSQL';
+ }
+ if (Platform::hasIbmSupport()) {
+ $dbChoices['ibm'] = 'IBM (DB2)';
+ }
+ if (Platform::hasOracleSupport()) {
+ $dbChoices['oracle'] = 'Oracle';
+ }
+ if (Platform::hasOciSupport()) {
+ $dbChoices['oci'] = 'Oracle (OCI8)';
+ }
+ if (Platform::hasSqliteSupport()) {
+ $dbChoices['sqlite'] = 'SQLite';
+ }
+
+ $offerPostgres = false;
+ $offerMysql = false;
+ $dbChoice = isset($formData['db']) ? $formData['db'] : key($dbChoices);
+ if ($dbChoice === 'pgsql') {
+ $offerPostgres = true;
+ } elseif ($dbChoice === 'mysql') {
+ $offerMysql = true;
+ }
+
+ if ($dbChoice === 'oracle' || $dbChoice === 'oci') {
+ $hostIsRequired = false;
+ } else {
+ $hostIsRequired = true;
+ }
+
+ $socketInfo = '';
+ if ($offerPostgres) {
+ $socketInfo = $this->translate(
+ 'For using unix domain sockets, specify the path to the unix domain socket directory'
+ );
+ } elseif ($offerMysql) {
+ $socketInfo = $this->translate(
+ 'For using unix domain sockets, specify localhost'
+ );
+ }
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'db',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Database Type'),
+ 'description' => $this->translate('The type of SQL database'),
+ 'multiOptions' => $dbChoices
+ )
+ );
+ if ($dbChoice === 'sqlite') {
+ $this->addElement(
+ 'text',
+ 'dbname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Name'),
+ 'description' => $this->translate('The name of the database to use')
+ )
+ );
+ } else {
+ $this->addElement(
+ 'text',
+ 'host',
+ array (
+ 'required' => $hostIsRequired,
+ 'label' => $this->translate('Host'),
+ 'description' => $this->translate('The hostname of the database')
+ . ($socketInfo ? '. ' . $socketInfo : ''),
+ 'value' => $hostIsRequired ? 'localhost' : ''
+ )
+ );
+ $this->addElement(
+ 'number',
+ 'port',
+ array(
+ 'description' => $this->translate('The port to use'),
+ 'label' => $this->translate('Port'),
+ 'preserveDefault' => true,
+ 'required' => $offerPostgres,
+ 'value' => $offerPostgres ? 5432 : null
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'dbname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Name'),
+ 'description' => $this->translate('The name of the database to use')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'username',
+ array (
+ 'required' => true,
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate('The user name to use for authentication')
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'renderPassword' => true,
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate('The password to use for authentication'),
+ 'autocomplete' => 'new-password'
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'charset',
+ array (
+ 'description' => $this->translate('The character set for the database'),
+ 'label' => $this->translate('Character Set')
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'use_ssl',
+ array(
+ 'autosubmit' => true,
+ 'label' => $this->translate('Use SSL'),
+ 'description' => $this->translate(
+ 'Whether to encrypt the connection or to authenticate using certificates'
+ )
+ )
+ );
+ if (isset($formData['use_ssl']) && $formData['use_ssl']) {
+ if (defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')) {
+ $this->addElement(
+ 'checkbox',
+ 'ssl_do_not_verify_server_cert',
+ array(
+ 'label' => $this->translate('SSL Do Not Verify Server Certificate'),
+ 'description' => $this->translate(
+ 'Whether to disable verification of the server certificate'
+ )
+ )
+ );
+ }
+ $this->addElement(
+ 'text',
+ 'ssl_key',
+ array(
+ 'label' => $this->translate('SSL Key'),
+ 'description' => $this->translate('The client key file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_cert',
+ array(
+ 'label' => $this->translate('SSL Certificate'),
+ 'description' => $this->translate('The certificate file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_ca',
+ array(
+ 'label' => $this->translate('SSL CA'),
+ 'description' => $this->translate('The CA certificate file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_capath',
+ array(
+ 'label' => $this->translate('SSL CA Path'),
+ 'description' => $this->translate(
+ 'The trusted CA certificates in PEM format directory path'
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_cipher',
+ array(
+ 'label' => $this->translate('SSL Cipher'),
+ 'description' => $this->translate('The list of permissible ciphers')
+ )
+ );
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/FileResourceForm.php b/application/forms/Config/Resource/FileResourceForm.php
new file mode 100644
index 0000000..b98f1b4
--- /dev/null
+++ b/application/forms/Config/Resource/FileResourceForm.php
@@ -0,0 +1,67 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Zend_Validate_Callback;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying file resources
+ */
+class FileResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_file');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'filename',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Filepath'),
+ 'description' => $this->translate('The filename to fetch information from'),
+ 'validators' => array('ReadablePathValidator')
+ )
+ );
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ return @preg_match($value, '') !== false;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('"%value%" is not a valid regular expression.'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+ $this->addElement(
+ 'text',
+ 'fields',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Pattern'),
+ 'description' => $this->translate('The pattern by which to identify columns.'),
+ 'requirement' => $this->translate('The column pattern must be a valid regular expression.'),
+ 'validators' => array($callbackValidator)
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/LdapResourceForm.php b/application/forms/Config/Resource/LdapResourceForm.php
new file mode 100644
index 0000000..7ffccdc
--- /dev/null
+++ b/application/forms/Config/Resource/LdapResourceForm.php
@@ -0,0 +1,129 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+use Icinga\Protocol\Ldap\LdapConnection;
+
+/**
+ * Form class for adding/modifying ldap resources
+ */
+class LdapResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_ldap');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $defaultPort = ! array_key_exists('encryption', $formData) || $formData['encryption'] !== LdapConnection::LDAPS
+ ? 389
+ : 636;
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'hostname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Host'),
+ 'description' => $this->translate(
+ 'The hostname or address of the LDAP server to use for authentication.'
+ . ' You can also provide multiple hosts separated by a space'
+ ),
+ 'value' => 'localhost'
+ )
+ );
+ $this->addElement(
+ 'number',
+ 'port',
+ array(
+ 'required' => true,
+ 'preserveDefault' => true,
+ 'label' => $this->translate('Port'),
+ 'description' => $this->translate('The port of the LDAP server to use for authentication'),
+ 'value' => $defaultPort
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'encryption',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Encryption'),
+ 'description' => $this->translate(
+ 'Whether to encrypt communication. Choose STARTTLS or LDAPS for encrypted communication or'
+ . ' none for unencrypted communication'
+ ),
+ 'multiOptions' => array(
+ 'none' => $this->translate('None', 'resource.ldap.encryption'),
+ LdapConnection::STARTTLS => 'STARTTLS',
+ LdapConnection::LDAPS => 'LDAPS'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'root_dn',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Root DN'),
+ 'description' => $this->translate(
+ 'Only the root and its child nodes will be accessible on this resource.'
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'bind_dn',
+ array(
+ 'label' => $this->translate('Bind DN'),
+ 'description' => $this->translate(
+ 'The user dn to use for querying the ldap server. Leave the dn and password empty for attempting'
+ . ' an anonymous bind'
+ )
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'bind_pw',
+ array(
+ 'renderPassword' => true,
+ 'label' => $this->translate('Bind Password'),
+ 'description' => $this->translate('The password to use for querying the ldap server')
+ )
+ );
+
+ $this->addElement(
+ 'number',
+ 'timeout',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('Timeout'),
+ 'description' => $this->translate('Connection timeout for every LDAP connection'),
+ 'value' => 5 // see LdapConnection::__construct()
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/SshResourceForm.php b/application/forms/Config/Resource/SshResourceForm.php
new file mode 100644
index 0000000..a15dc8c
--- /dev/null
+++ b/application/forms/Config/Resource/SshResourceForm.php
@@ -0,0 +1,148 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Icinga\Application\Icinga;
+use Icinga\Data\ConfigObject;
+use Icinga\Forms\Config\ResourceConfigForm;
+use Icinga\Web\Form;
+use Icinga\Util\File;
+use Zend_Validate_Callback;
+
+/**
+ * Form class for adding/modifying ssh identity resources
+ */
+class SshResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_ssh');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('User'),
+ 'description' => $this->translate(
+ 'User to log in as on the remote Icinga instance. Please note that key-based SSH login must be'
+ . ' possible for this user'
+ )
+ )
+ );
+
+ if ($this->getRequest()->getActionName() != 'editresource') {
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ if (substr(ltrim($value), 0, 7) === 'file://'
+ || openssl_pkey_get_private($value) === false
+ ) {
+ return false;
+ }
+
+ return true;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('The given SSH key is invalid'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+
+ $this->addElement(
+ 'textarea',
+ 'private_key',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Private Key'),
+ 'description' => $this->translate('The private key which will be used for the SSH connections'),
+ 'class' => 'resource ssh-identity',
+ 'validators' => array($callbackValidator)
+ )
+ );
+ } else {
+ $resourceName = $formData['name'];
+ $this->addElement(
+ 'note',
+ 'private_key_note',
+ array(
+ 'escape' => false,
+ 'label' => $this->translate('Private Key'),
+ 'value' => sprintf(
+ '<a href="%1$s" data-base-target="_next" title="%2$s" aria-label="%2$s">%3$s</a>',
+ $this->getView()->url('config/removeresource', array('resource' => $resourceName)),
+ $this->getView()->escape(sprintf($this->translate(
+ 'Remove the %s resource'
+ ), $resourceName)),
+ $this->translate('To modify the private key you must recreate this resource.')
+ )
+ )
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Remove the assigned key to the resource
+ *
+ * @param ConfigObject $config
+ *
+ * @return bool
+ */
+ public static function beforeRemove(ConfigObject $config)
+ {
+ $file = $config->private_key;
+
+ if (file_exists($file)) {
+ unlink($file);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Creates the assigned key to the resource
+ *
+ * @param ResourceConfigForm $form
+ *
+ * @return bool
+ */
+ public static function beforeAdd(ResourceConfigForm $form)
+ {
+ $configDir = Icinga::app()->getConfigDir();
+ $user = $form->getElement('user')->getValue();
+
+ $filePath = join(DIRECTORY_SEPARATOR, [$configDir, 'ssh', sha1($user)]);
+ if (! file_exists($filePath)) {
+ $file = File::create($filePath, 0600);
+ } else {
+ $form->error(
+ sprintf($form->translate('The private key for the user "%s" already exists.'), $user)
+ );
+ return false;
+ }
+
+ $file->fwrite($form->getElement('private_key')->getValue());
+
+ $form->getElement('private_key')->setValue($filePath);
+
+ return true;
+ }
+}
diff --git a/application/forms/Config/ResourceConfigForm.php b/application/forms/Config/ResourceConfigForm.php
new file mode 100644
index 0000000..c2d0d18
--- /dev/null
+++ b/application/forms/Config/ResourceConfigForm.php
@@ -0,0 +1,442 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use Icinga\Application\Config;
+use InvalidArgumentException;
+use Icinga\Application\Platform;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Data\ResourceFactory;
+use Icinga\Forms\ConfigForm;
+use Icinga\Forms\Config\Resource\DbResourceForm;
+use Icinga\Forms\Config\Resource\FileResourceForm;
+use Icinga\Forms\Config\Resource\LdapResourceForm;
+use Icinga\Forms\Config\Resource\SshResourceForm;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Zend_Form_Element;
+
+class ResourceConfigForm extends ConfigForm
+{
+ /**
+ * Bogus password when inspecting password elements
+ *
+ * @var string
+ */
+ protected static $dummyPassword = '_web_form_5847ed1b5b8ca';
+
+ /**
+ * If the global config must be updated because a resource has been changed, this is the updated global config
+ *
+ * @var Config|null
+ */
+ protected $updatedAppConfig = null;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->setValidatePartial(true);
+ }
+
+ /**
+ * Return a form object for the given resource type
+ *
+ * @param string $type The resource type for which to return a form
+ *
+ * @return Form
+ */
+ public function getResourceForm($type)
+ {
+ if ($type === 'db') {
+ return new DbResourceForm();
+ } elseif ($type === 'ldap') {
+ return new LdapResourceForm();
+ } elseif ($type === 'file') {
+ return new FileResourceForm();
+ } elseif ($type === 'ssh') {
+ return new SshResourceForm();
+ } else {
+ throw new InvalidArgumentException(sprintf($this->translate('Invalid resource type "%s" provided'), $type));
+ }
+ }
+
+ /**
+ * Add a particular resource
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $values The values to extend the configuration with
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case the resource does already exist
+ */
+ public function add(array $values)
+ {
+ $name = isset($values['name']) ? $values['name'] : '';
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Resource name missing'));
+ } elseif ($this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Resource already exists'));
+ }
+
+ unset($values['name']);
+ $this->config->setSection($name, $values);
+ return $this;
+ }
+
+ /**
+ * Edit a particular resource
+ *
+ * @param string $name The name of the resource to edit
+ * @param array $values The values to edit the configuration with
+ *
+ * @return ConfigObject The edited configuration
+ *
+ * @throws InvalidArgumentException In case the resource does not exist
+ */
+ public function edit($name, array $values)
+ {
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Old resource name missing'));
+ } elseif (! ($newName = isset($values['name']) ? $values['name'] : '')) {
+ throw new InvalidArgumentException($this->translate('New resource name missing'));
+ } elseif (! $this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Unknown resource provided'));
+ }
+
+ $resourceConfig = $this->config->getSection($name);
+ $this->config->removeSection($name);
+ unset($values['name']);
+ $this->config->setSection($newName, $resourceConfig->merge($values));
+
+ if ($newName !== $name) {
+ $appConfig = Config::app();
+ $section = $appConfig->getSection('global');
+ if ($section->config_resource === $name) {
+ $section->config_resource = $newName;
+ $this->updatedAppConfig = $appConfig->setSection('global', $section);
+ }
+ }
+
+ return $resourceConfig;
+ }
+
+ /**
+ * Remove a particular resource
+ *
+ * @param string $name The name of the resource to remove
+ *
+ * @return ConfigObject The removed resource configuration
+ *
+ * @throws InvalidArgumentException In case the resource does not exist
+ */
+ public function remove($name)
+ {
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Resource name missing'));
+ } elseif (! $this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Unknown resource provided'));
+ }
+
+ $resourceConfig = $this->config->getSection($name);
+ $resourceForm = $this->getResourceForm($resourceConfig->type);
+ if (method_exists($resourceForm, 'beforeRemove')) {
+ $resourceForm::beforeRemove($resourceConfig);
+ }
+
+ $this->config->removeSection($name);
+ return $resourceConfig;
+ }
+
+ /**
+ * Add or edit a resource and save the configuration
+ *
+ * Performs a connectivity validation using the submitted values. A checkbox is
+ * added to the form to skip the check if it fails and redirection is aborted.
+ *
+ * @see Form::onSuccess()
+ */
+ public function onSuccess()
+ {
+ $resourceForm = $this->getResourceForm($this->getElement('type')->getValue());
+
+ if (($el = $this->getElement('force_creation')) === null || false === $el->isChecked()) {
+ $inspection = static::inspectResource($this);
+ if ($inspection !== null && $inspection->hasError()) {
+ $this->error($inspection->getError());
+ $this->addElement($this->getForceCreationCheckbox());
+ return false;
+ }
+ }
+
+ $resource = $this->request->getQuery('resource');
+ try {
+ if ($resource === null) { // create new resource
+ if (method_exists($resourceForm, 'beforeAdd')) {
+ if (! $resourceForm::beforeAdd($this)) {
+ return false;
+ }
+ }
+ $this->add(static::transformEmptyValuesToNull($this->getValues()));
+ $message = $this->translate('Resource "%s" has been successfully created');
+ } else { // edit existing resource
+ $this->edit($resource, static::transformEmptyValuesToNull($this->getValues()));
+ $message = $this->translate('Resource "%s" has been successfully changed');
+ }
+ } catch (InvalidArgumentException $e) {
+ Notification::error($e->getMessage());
+ return false;
+ }
+
+ if ($this->save()) {
+ Notification::success(sprintf($message, $this->getElement('name')->getValue()));
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Populate the form in case a resource is being edited
+ *
+ * @see Form::onRequest()
+ *
+ * @throws ConfigurationError In case the backend name is missing in the request or is invalid
+ */
+ public function onRequest()
+ {
+ $resource = $this->request->getQuery('resource');
+ if ($resource !== null) {
+ if ($resource === '') {
+ throw new ConfigurationError($this->translate('Resource name missing'));
+ } elseif (! $this->config->hasSection($resource)) {
+ throw new ConfigurationError($this->translate('Unknown resource provided'));
+ }
+ $configValues = $this->config->getSection($resource)->toArray();
+ $configValues['name'] = $resource;
+ $this->populate($configValues);
+ foreach ($this->getElements() as $element) {
+ if ($element->getType() === 'Zend_Form_Element_Password' && strlen($element->getValue())) {
+ $element->setValue(static::$dummyPassword);
+ }
+ }
+ }
+ }
+
+ /**
+ * Return a checkbox to be displayed at the beginning of the form
+ * which allows the user to skip the connection validation
+ *
+ * @return Zend_Form_Element
+ */
+ protected function getForceCreationCheckbox()
+ {
+ return $this->createElement(
+ 'checkbox',
+ 'force_creation',
+ array(
+ 'order' => 0,
+ 'ignore' => true,
+ 'label' => $this->translate('Force Changes'),
+ 'description' => $this->translate('Check this box to enforce changes without connectivity validation')
+ )
+ );
+ }
+
+ /**
+ * @see Form::createElemeents()
+ */
+ public function createElements(array $formData)
+ {
+ $resourceType = isset($formData['type']) ? $formData['type'] : 'db';
+
+ $resourceTypes = array(
+ 'file' => $this->translate('File'),
+ 'ssh' => $this->translate('SSH Identity'),
+ );
+ if ($resourceType === 'ldap' || Platform::hasLdapSupport()) {
+ $resourceTypes['ldap'] = 'LDAP';
+ }
+ if ($resourceType === 'db' || Platform::hasDatabaseSupport()) {
+ $resourceTypes['db'] = $this->translate('SQL Database');
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Resource Type'),
+ 'description' => $this->translate('The type of resource'),
+ 'multiOptions' => $resourceTypes,
+ 'value' => $resourceType
+ )
+ );
+
+ if (isset($formData['force_creation']) && $formData['force_creation']) {
+ // In case another error occured and the checkbox was displayed before
+ $this->addElement($this->getForceCreationCheckbox());
+ }
+
+ $this->addElements($this->getResourceForm($resourceType)->createElements($formData)->getElements());
+ }
+
+ /**
+ * Create a resource by using the given form's values and return its inspection results
+ *
+ * @param Form $form
+ *
+ * @return ?Inspection
+ */
+ public static function inspectResource(Form $form)
+ {
+ if ($form->getValue('type') !== 'ssh') {
+ $resource = ResourceFactory::createResource(new ConfigObject($form->getValues()));
+ if ($resource instanceof Inspectable) {
+ return $resource->inspect();
+ }
+ }
+ }
+
+ /**
+ * Run the configured resource's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'resource_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if ($this->getElement('resource_validation')->isChecked() && parent::isValid($formData)) {
+ $inspection = static::inspectResource($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $e;
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'resource_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+
+ $this->setAttrib('data-progress-element', 'resource-progress');
+ $this->addElement(
+ 'note',
+ 'resource-progress',
+ array(
+ 'decorators' => array(
+ 'ViewHelper',
+ array('Spinner', array('id' => 'resource-progress'))
+ )
+ )
+ );
+
+ $this->addDisplayGroup(
+ array('btn_submit', 'resource_validation', 'resource-progress'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ $resource = $this->request->getQuery('resource');
+ if ($resource !== null && $this->config->hasSection($resource)) {
+ $resourceConfig = $this->config->getSection($resource)->toArray();
+ foreach ($this->getElements() as $element) {
+ if ($element->getType() === 'Zend_Form_Element_Password') {
+ $name = $element->getName();
+ if (isset($values[$name]) && $values[$name] === static::$dummyPassword) {
+ if (isset($resourceConfig[$name])) {
+ $values[$name] = $resourceConfig[$name];
+ } else {
+ unset($values[$name]);
+ }
+ }
+ }
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function writeConfig(Config $config)
+ {
+ parent::writeConfig($config);
+ if ($this->updatedAppConfig !== null) {
+ $this->updatedAppConfig->saveIni();
+ }
+ }
+}
diff --git a/application/forms/Config/User/CreateMembershipForm.php b/application/forms/Config/User/CreateMembershipForm.php
new file mode 100644
index 0000000..6c74c8c
--- /dev/null
+++ b/application/forms/Config/User/CreateMembershipForm.php
@@ -0,0 +1,192 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\User;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for creating one or more group memberships
+ */
+class CreateMembershipForm extends Form
+{
+ /**
+ * The user group backends to fetch groups from
+ *
+ * Each backend must implement the Icinga\Data\Extensible and Icinga\Data\Selectable interface.
+ *
+ * @var array
+ */
+ protected $backends;
+
+ /**
+ * The username to create memberships for
+ *
+ * @var string
+ */
+ protected $userName;
+
+ /**
+ * Set the user group backends to fetch groups from
+ *
+ * @param array $backends
+ *
+ * @return $this
+ */
+ public function setBackends($backends)
+ {
+ $this->backends = $backends;
+ return $this;
+ }
+
+ /**
+ * Set the username to create memberships for
+ *
+ * @param string $userName
+ *
+ * @return $this
+ */
+ public function setUsername($userName)
+ {
+ $this->userName = $userName;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ $query = $this->createDataSource()->select()->from('group', array('group_name', 'backend_name'));
+
+ $options = array();
+ foreach ($query as $row) {
+ $options[$row->backend_name . ';' . $row->group_name] = $row->group_name . ' (' . $row->backend_name . ')';
+ }
+
+ $this->addElement(
+ 'multiselect',
+ 'groups',
+ array(
+ 'required' => true,
+ 'multiOptions' => $options,
+ 'label' => $this->translate('Groups'),
+ 'description' => sprintf(
+ $this->translate('Select one or more groups where to add %s as member'),
+ $this->userName
+ ),
+ 'class' => 'grant-permissions'
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Create memberships for %s'), $this->userName));
+ $this->setSubmitLabel($this->translate('Create'));
+ }
+
+ /**
+ * Instantly redirect back in case the user is already a member of all groups
+ */
+ public function onRequest()
+ {
+ if ($this->createDataSource()->select()->from('group')->count() === 0) {
+ Notification::info(sprintf($this->translate('User %s is already a member of all groups'), $this->userName));
+ $this->getResponse()->redirectAndExit($this->getRedirectUrl());
+ }
+ }
+
+ /**
+ * Create the memberships for the user
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ $backendMap = array();
+ foreach ($this->backends as $backend) {
+ $backendMap[$backend->getName()] = $backend;
+ }
+
+ $single = null;
+ $groupName = null;
+ foreach ($this->getValue('groups') as $backendAndGroup) {
+ list($backendName, $groupName) = explode(';', $backendAndGroup, 2);
+ try {
+ $backendMap[$backendName]->insert(
+ 'group_membership',
+ array(
+ 'group_name' => $groupName,
+ 'user_name' => $this->userName
+ )
+ );
+ } catch (Exception $e) {
+ Notification::error(sprintf(
+ $this->translate('Failed to add "%s" as group member for "%s"'),
+ $this->userName,
+ $groupName
+ ));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ $single = $single === null;
+ }
+
+ if ($single) {
+ Notification::success(
+ sprintf($this->translate('Membership for group %s created successfully'), $groupName)
+ );
+ } else {
+ Notification::success($this->translate('Memberships created successfully'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Create and return a data source to fetch all groups from all backends where the user is not already a member of
+ *
+ * @return ArrayDatasource
+ */
+ protected function createDataSource()
+ {
+ $groups = $failures = array();
+ foreach ($this->backends as $backend) {
+ try {
+ $memberships = $backend
+ ->select()
+ ->from('group_membership', array('group_name'))
+ ->where('user_name', $this->userName)
+ ->fetchColumn();
+ foreach ($backend->select(array('group_name')) as $row) {
+ if (! in_array($row->group_name, $memberships)) { // TODO(jom): Apply this as native query filter
+ $row->backend_name = $backend->getName();
+ $groups[] = $row;
+ }
+ }
+ } catch (Exception $e) {
+ $failures[] = array($backend->getName(), $e);
+ }
+ }
+
+ if (empty($groups) && !empty($failures)) {
+ // In case there are only failures, throw the very first exception again
+ throw $failures[0][1];
+ } elseif (! empty($failures)) {
+ foreach ($failures as $failure) {
+ Logger::error($failure[1]);
+ Notification::warning(sprintf(
+ $this->translate('Failed to fetch any groups from backend %s. Please check your log'),
+ $failure[0]
+ ));
+ }
+ }
+
+ return new ArrayDatasource($groups);
+ }
+}
diff --git a/application/forms/Config/User/UserForm.php b/application/forms/Config/User/UserForm.php
new file mode 100644
index 0000000..fb2ef4d
--- /dev/null
+++ b/application/forms/Config/User/UserForm.php
@@ -0,0 +1,210 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\User;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\RepositoryForm;
+use Icinga\Web\Notification;
+
+class UserForm extends RepositoryForm
+{
+ /**
+ * Create and add elements to this form to insert or update a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createInsertElements(array $formData)
+ {
+ $this->addElement(
+ 'checkbox',
+ 'is_active',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Active'),
+ 'description' => $this->translate('Prevents the user from logging in if unchecked')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Username')
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Password')
+ )
+ );
+
+ $this->setTitle($this->translate('Add a new user'));
+ $this->setSubmitLabel($this->translate('Add'));
+ }
+
+ /**
+ * Create and add elements to this form to update a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'description' => $this->translate('Leave empty for not updating the user\'s password'),
+ 'label' => $this->translate('Password'),
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Edit user %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+
+ /**
+ * Update a user
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ if (parent::onUpdateSuccess()) {
+ if (($newName = $this->getValue('user_name')) !== $this->getIdentifier()) {
+ $this->getRedirectUrl()->setParam('user', $newName);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Retrieve all form element values
+ *
+ * Strips off the password if null or the empty string.
+ *
+ * @param bool $suppressArrayNotation
+ *
+ * @return array
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ // before checking if password values is empty
+ // we have to check that the password field is set
+ // otherwise an error is thrown
+ if (isset($values['password']) && ! $values['password']) {
+ unset($values['password']);
+ }
+
+ return $values;
+ }
+
+ /**
+ * Create and add elements to this form to delete a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove user %s?'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Yes'));
+ $this->setAttrib('class', 'icinga-controls');
+ }
+
+ /**
+ * Create and return a filter to use when updating or deleting a user
+ *
+ * @return Filter
+ */
+ protected function createFilter()
+ {
+ return Filter::where('user_name', $this->getIdentifier());
+ }
+
+ /**
+ * Return a notification message to use when inserting a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getInsertMessage($success)
+ {
+ if ($success) {
+ return $this->translate('User added successfully');
+ } else {
+ return $this->translate('Failed to add user');
+ }
+ }
+
+ /**
+ * Return a notification message to use when updating a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getUpdateMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('User "%s" has been edited'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to edit user "%s"'), $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Return a notification message to use when deleting a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getDeleteMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('User "%s" has been removed'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to remove user "%s"'), $this->getIdentifier());
+ }
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ if (parent::onSuccess() === false) {
+ return false;
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+}
diff --git a/application/forms/Config/UserBackend/DbBackendForm.php b/application/forms/Config/UserBackend/DbBackendForm.php
new file mode 100644
index 0000000..693ea14
--- /dev/null
+++ b/application/forms/Config/UserBackend/DbBackendForm.php
@@ -0,0 +1,82 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserBackend;
+
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying database user backends
+ */
+class DbBackendForm extends Form
+{
+ /**
+ * The database resource names the user can choose from
+ *
+ * @var array
+ */
+ protected $resources;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend_db');
+ }
+
+ /**
+ * Set the resource names the user can choose from
+ *
+ * @param array $resources The resources to choose from
+ *
+ * @return $this
+ */
+ public function setResources(array $resources)
+ {
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others'
+ )
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Connection'),
+ 'description' => $this->translate(
+ 'The database connection to use for authenticating with this provider'
+ ),
+ 'multiOptions' => !empty($this->resources)
+ ? array_combine($this->resources, $this->resources)
+ : array()
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => 'db'
+ )
+ );
+ }
+}
diff --git a/application/forms/Config/UserBackend/ExternalBackendForm.php b/application/forms/Config/UserBackend/ExternalBackendForm.php
new file mode 100644
index 0000000..f4a4639
--- /dev/null
+++ b/application/forms/Config/UserBackend/ExternalBackendForm.php
@@ -0,0 +1,83 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserBackend;
+
+use Zend_Validate_Callback;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying user backends of type "external"
+ */
+class ExternalBackendForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend_external');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others'
+ )
+ )
+ );
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ return @preg_match($value, '') !== false;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('"%value%" is not a valid regular expression.'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+ $this->addElement(
+ 'text',
+ 'strip_username_regexp',
+ array(
+ 'label' => $this->translate('Filter Pattern'),
+ 'description' => $this->translate(
+ 'The filter to use to strip specific parts off from usernames.'
+ . ' Leave empty if you do not want to strip off anything.'
+ ),
+ 'requirement' => $this->translate('The filter pattern must be a valid regular expression.'),
+ 'validators' => array($callbackValidator)
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => 'external'
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * Validate the configuration by creating a backend and requesting the user count
+ *
+ * Returns always true as backends of type "external" are just "passive" backends.
+ *
+ * @param Form $form The form to fetch the configuration values from
+ *
+ * @return bool Whether validation succeeded or not
+ */
+ public static function isValidUserBackend(Form $form)
+ {
+ return true;
+ }
+}
diff --git a/application/forms/Config/UserBackend/LdapBackendForm.php b/application/forms/Config/UserBackend/LdapBackendForm.php
new file mode 100644
index 0000000..e7804cc
--- /dev/null
+++ b/application/forms/Config/UserBackend/LdapBackendForm.php
@@ -0,0 +1,414 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserBackend;
+
+use Exception;
+use Icinga\Data\ResourceFactory;
+use Icinga\Protocol\Ldap\LdapCapabilities;
+use Icinga\Protocol\Ldap\LdapConnection;
+use Icinga\Protocol\Ldap\LdapException;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying LDAP user backends
+ */
+class LdapBackendForm extends Form
+{
+ /**
+ * The ldap resource names the user can choose from
+ *
+ * @var array
+ */
+ protected $resources;
+
+ /**
+ * Default values for the form elements
+ *
+ * @var string[]
+ */
+ protected $suggestions = array();
+
+ /**
+ * Cache for {@link getLdapCapabilities()}
+ *
+ * @var LdapCapabilities
+ */
+ protected $ldapCapabilities;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend_ldap');
+ }
+
+ /**
+ * Set the resource names the user can choose from
+ *
+ * @param array $resources The resources to choose from
+ *
+ * @return $this
+ */
+ public function setResources(array $resources)
+ {
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $isAd = isset($formData['type']) ? $formData['type'] === 'msldap' : false;
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others.'
+ ),
+ 'value' => $this->getSuggestion('name')
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('LDAP Connection'),
+ 'description' => $this->translate(
+ 'The LDAP connection to use for authenticating with this provider.'
+ ),
+ 'multiOptions' => !empty($this->resources)
+ ? array_combine($this->resources, $this->resources)
+ : array(),
+ 'value' => $this->getSuggestion('resource')
+ )
+ );
+
+ if (! $isAd && !empty($this->resources)) {
+ $this->addElement(
+ 'button',
+ 'discovery_btn',
+ array(
+ 'class' => 'control-button',
+ 'type' => 'submit',
+ 'value' => 'discovery_btn',
+ 'label' => $this->translate('Discover', 'A button to discover LDAP capabilities'),
+ 'title' => $this->translate(
+ 'Push to fill in the chosen connection\'s default settings.'
+ ),
+ 'decorators' => array(
+ array('ViewHelper', array('separator' => '')),
+ array('Spinner'),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ }
+
+ if ($isAd) {
+ // ActiveDirectory defaults
+ $userClass = 'user';
+ $filter = '!(objectClass=computer)';
+ $userNameAttribute = 'sAMAccountName';
+ } else {
+ // OpenLDAP defaults
+ $userClass = 'inetOrgPerson';
+ $filter = null;
+ $userNameAttribute = 'uid';
+ }
+
+ $this->addElement(
+ 'text',
+ 'user_class',
+ array(
+ 'preserveDefault' => true,
+ 'required' => ! $isAd,
+ 'ignore' => $isAd,
+ 'disabled' => $isAd ?: null,
+ 'label' => $this->translate('LDAP User Object Class'),
+ 'description' => $this->translate('The object class used for storing users on the LDAP server.'),
+ 'value' => $this->getSuggestion('user_class', $userClass)
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'value' => $this->getSuggestion('filter', $filter),
+ 'label' => $this->translate('LDAP Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up users using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression.'
+ . ' (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ // This is not meant to be a full syntax check. It will just
+ // ensure that we can safely strip unnecessary parentheses.
+ $v = trim($v);
+ return ! $v || $v[0] !== '(' || (
+ strpos($v, ')(') !== false ? substr($v, -2) === '))' : substr($v, -1) === ')'
+ );
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter is invalid. Please check your syntax.')
+ )
+ )
+ )
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'required' => ! $isAd,
+ 'ignore' => $isAd,
+ 'disabled' => $isAd ?: null,
+ 'label' => $this->translate('LDAP User Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing the user name on the LDAP server.'
+ ),
+ 'value' => $this->getSuggestion('user_name_attribute', $userNameAttribute)
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => $this->getSuggestion('backend', $isAd ? 'msldap' : 'ldap')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'required' => false,
+ 'label' => $this->translate('LDAP Base DN'),
+ 'description' => $this->translate(
+ 'The path where users can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $this->getSuggestion('base_dn')
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'domain',
+ array(
+ 'label' => $this->translate('Domain'),
+ 'description' => $this->translate(
+ 'The domain the LDAP server is responsible for upon authentication.'
+ . ' Note that if you specify a domain here,'
+ . ' the LDAP backend only authenticates users who specify a domain upon login.'
+ . ' If the domain of the user matches the domain configured here, this backend is responsible for'
+ . ' authenticating the user based on the username without the domain part.'
+ . ' If your LDAP backend holds usernames with a domain part or if it is not necessary in your setup'
+ . ' to authenticate users based on their domains, leave this field empty.'
+ ),
+ 'preserveDefault' => true,
+ 'value' => $this->getSuggestion('domain')
+ )
+ );
+
+ $this->addElement(
+ 'button',
+ 'btn_discover_domain',
+ array(
+ 'class' => 'control-button',
+ 'type' => 'submit',
+ 'value' => 'discovery_btn',
+ 'label' => $this->translate('Discover the domain'),
+ 'title' => $this->translate(
+ 'Push to disover and fill in the domain of the LDAP server.'
+ ),
+ 'decorators' => array(
+ array('ViewHelper', array('separator' => '')),
+ array('Spinner'),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ }
+
+ public function isValidPartial(array $formData)
+ {
+ $isAd = isset($formData['type']) && $formData['type'] === 'msldap';
+ $baseDn = null;
+ $hasAdOid = false;
+ $discoverySuccessful = false;
+
+ if (! $isAd && ! empty($this->resources) && isset($formData['discovery_btn'])
+ && $formData['discovery_btn'] === 'discovery_btn') {
+ $discoverySuccessful = true;
+ try {
+ $capabilities = $this->getLdapCapabilities($formData);
+ $baseDn = $capabilities->getDefaultNamingContext();
+ $hasAdOid = $capabilities->isActiveDirectory();
+ } catch (Exception $e) {
+ $this->warning(sprintf(
+ $this->translate('Failed to discover the chosen LDAP connection: %s'),
+ $e->getMessage()
+ ));
+ $discoverySuccessful = false;
+ }
+ }
+
+ if ($discoverySuccessful) {
+ if ($isAd || $hasAdOid) {
+ // ActiveDirectory defaults
+ $userClass = 'user';
+ $filter = '!(objectClass=computer)';
+ $userNameAttribute = 'sAMAccountName';
+ } else {
+ // OpenLDAP defaults
+ $userClass = 'inetOrgPerson';
+ $filter = null;
+ $userNameAttribute = 'uid';
+ }
+
+ $formData['user_class'] = $userClass;
+
+ if (! isset($formData['filter']) || $formData['filter'] === '') {
+ $formData['filter'] = $filter;
+ }
+
+ $formData['user_name_attribute'] = $userNameAttribute;
+
+ if ($baseDn !== null && (! isset($formData['base_dn']) || $formData['base_dn'] === '')) {
+ $formData['base_dn'] = $baseDn;
+ }
+ }
+
+ if (isset($formData['btn_discover_domain']) && $formData['btn_discover_domain'] === 'discovery_btn') {
+ try {
+ $formData['domain'] = $this->discoverDomain($formData);
+ } catch (LdapException $e) {
+ $this->error($e->getMessage());
+ }
+ }
+
+ return parent::isValidPartial($formData);
+ }
+
+ /**
+ * Get the LDAP capabilities of either the resource specified by the user or the default one
+ *
+ * @param string[] $formData
+ *
+ * @return LdapCapabilities
+ */
+ protected function getLdapCapabilities(array $formData)
+ {
+ if ($this->ldapCapabilities === null) {
+ $this->ldapCapabilities = ResourceFactory::create(
+ isset($formData['resource']) ? $formData['resource'] : reset($this->resources)
+ )->bind()->getCapabilities();
+ }
+
+ return $this->ldapCapabilities;
+ }
+
+ /**
+ * Discover the domain the LDAP server is responsible for
+ *
+ * @param string[] $formData
+ *
+ * @return string
+ */
+ protected function discoverDomain(array $formData)
+ {
+ $cap = $this->getLdapCapabilities($formData);
+
+ if ($cap->isActiveDirectory()) {
+ $netBiosName = $cap->getNetBiosName();
+ if ($netBiosName !== null) {
+ return $netBiosName;
+ }
+ }
+
+ return $this->defaultNamingContextToFQDN($cap);
+ }
+
+ /**
+ * Get the default naming context as FQDN
+ *
+ * @param LdapCapabilities $cap
+ *
+ * @return string|null
+ */
+ protected function defaultNamingContextToFQDN(LdapCapabilities $cap)
+ {
+ $defaultNamingContext = $cap->getDefaultNamingContext();
+ if ($defaultNamingContext !== null) {
+ $validationMatches = array();
+ if (preg_match('/\bdc=[^,]+(?:,dc=[^,]+)*$/', strtolower($defaultNamingContext), $validationMatches)) {
+ $splitMatches = array();
+ preg_match_all('/dc=([^,]+)/', $validationMatches[0], $splitMatches);
+ return implode('.', $splitMatches[1]);
+ }
+ }
+ }
+
+ /**
+ * Get the default values for the form elements
+ *
+ * @return string[]
+ */
+ public function getSuggestions()
+ {
+ return $this->suggestions;
+ }
+
+ /**
+ * Get the default value for the given form element or the given default
+ *
+ * @param string $element
+ * @param string $default
+ *
+ * @return string
+ */
+ public function getSuggestion($element, $default = null)
+ {
+ return isset($this->suggestions[$element]) ? $this->suggestions[$element] : $default;
+ }
+
+ /**
+ * Set the default values for the form elements
+ *
+ * @param string[] $suggestions
+ *
+ * @return $this
+ */
+ public function setSuggestions(array $suggestions)
+ {
+ $this->suggestions = $suggestions;
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserBackendConfigForm.php b/application/forms/Config/UserBackendConfigForm.php
new file mode 100644
index 0000000..fdca657
--- /dev/null
+++ b/application/forms/Config/UserBackendConfigForm.php
@@ -0,0 +1,482 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use InvalidArgumentException;
+use Icinga\Application\Config;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Forms\ConfigForm;
+use Icinga\Forms\Config\UserBackend\ExternalBackendForm;
+use Icinga\Forms\Config\UserBackend\DbBackendForm;
+use Icinga\Forms\Config\UserBackend\LdapBackendForm;
+use Icinga\Web\Form;
+
+/**
+ * Form for managing user backends
+ */
+class UserBackendConfigForm extends ConfigForm
+{
+ /**
+ * The available user backend resources split by type
+ *
+ * @var array
+ */
+ protected $resources;
+
+ /**
+ * The backend to load when displaying the form for the first time
+ *
+ * @var string
+ */
+ protected $backendToLoad;
+
+ /**
+ * The loaded custom backends list
+ *
+ * @var array
+ */
+ protected $customBackends;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->setValidatePartial(true);
+ $this->customBackends = UserBackend::getCustomBackendConfigForms();
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param Config $resourceConfig The resource configuration
+ *
+ * @return $this
+ *
+ * @throws ConfigurationError In case there are no valid resources for authentication available
+ */
+ public function setResourceConfig(Config $resourceConfig)
+ {
+ $resources = array();
+ foreach ($resourceConfig as $name => $resource) {
+ if (in_array($resource->type, array('db', 'ldap'))) {
+ $resources[$resource->type][] = $name;
+ }
+ }
+
+ if (empty($resources)) {
+ $externalBackends = $this->config->toArray();
+ array_walk(
+ $externalBackends,
+ function (&$authBackendCfg) {
+ if (! isset($authBackendCfg['backend']) || $authBackendCfg['backend'] !== 'external') {
+ $authBackendCfg = null;
+ }
+ }
+ );
+ if (count(array_filter($externalBackends)) > 0 && (
+ $this->backendToLoad === null || !isset($externalBackends[$this->backendToLoad])
+ )) {
+ throw new ConfigurationError($this->translate(
+ 'Could not find any valid user backend resources.'
+ . ' Please configure a resource for authentication first.'
+ ));
+ }
+ }
+
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Return a form object for the given backend type
+ *
+ * @param string $type The backend type for which to return a form
+ *
+ * @return Form
+ *
+ * @throws InvalidArgumentException In case the given backend type is invalid
+ */
+ public function getBackendForm($type)
+ {
+ switch ($type) {
+ case 'db':
+ $form = new DbBackendForm();
+ $form->setResources(isset($this->resources['db']) ? $this->resources['db'] : array());
+ break;
+ case 'ldap':
+ case 'msldap':
+ $form = new LdapBackendForm();
+ $form->setResources(isset($this->resources['ldap']) ? $this->resources['ldap'] : array());
+ break;
+ case 'external':
+ $form = new ExternalBackendForm();
+ break;
+ default:
+ if (isset($this->customBackends[$type])) {
+ return new $this->customBackends[$type]();
+ }
+
+ throw new InvalidArgumentException(
+ sprintf($this->translate('Invalid backend type "%s" provided'), $type)
+ );
+ }
+
+ return $form;
+ }
+
+ /**
+ * Populate the form with the given backend's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function load($name)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $this->backendToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new user backend
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a backend name
+ * @throws IcingaException In case a backend with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ }
+
+ $backendName = $data['name'];
+ if ($this->config->hasSection($backendName)) {
+ throw new IcingaException(
+ $this->translate('A user backend with the name "%s" does already exist'),
+ $backendName
+ );
+ }
+
+ unset($data['name']);
+ $this->config->setSection($backendName, $data);
+ return $this;
+ }
+
+ /**
+ * Edit a user backend
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function edit($name, array $data)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $backendConfig = $this->config->getSection($name);
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $this->config->removeSection($name);
+ $name = $data['name'];
+ }
+
+ unset($data['name']);
+ }
+
+ $backendConfig->merge($data);
+ $this->config->setSection($name, $backendConfig);
+ return $this;
+ }
+
+ /**
+ * Remove a user backend
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function delete($name)
+ {
+ $this->config->removeSection($name);
+ return $this;
+ }
+
+ /**
+ * Move the given user backend up or down in order
+ *
+ * @param string $name The name of the backend to be moved
+ * @param int $position The new (absolute) position of the backend
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function move($name, $position)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $backendOrder = $this->config->keys();
+ array_splice($backendOrder, array_search($name, $backendOrder), 1);
+ array_splice($backendOrder, $position, 0, $name);
+
+ $newConfig = array();
+ foreach ($backendOrder as $backendName) {
+ $newConfig[$backendName] = $this->config->getSection($backendName);
+ }
+
+ $config = Config::fromArray($newConfig);
+ $this->config = $config->setConfigFile($this->config->getConfigFile());
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $backendTypes = array();
+ $backendType = isset($formData['type']) ? $formData['type'] : null;
+
+ if (isset($this->resources['db'])) {
+ $backendTypes['db'] = $this->translate('Database');
+ }
+ if (isset($this->resources['ldap'])) {
+ $backendTypes['ldap'] = 'LDAP';
+ $backendTypes['msldap'] = 'ActiveDirectory';
+ }
+
+ $externalBackends = array_filter(
+ $this->config->toArray(),
+ function ($authBackendCfg) {
+ return isset($authBackendCfg['backend']) && $authBackendCfg['backend'] === 'external';
+ }
+ );
+ if ($backendType === 'external' || empty($externalBackends)) {
+ $backendTypes['external'] = $this->translate('External');
+ }
+
+ $customBackendTypes = array_keys($this->customBackends);
+ $backendTypes += array_combine($customBackendTypes, $customBackendTypes);
+
+ if ($backendType === null) {
+ $backendType = key($backendTypes);
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'ignore' => true,
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate(
+ 'The type of the resource to use for this authenticaton provider'
+ ),
+ 'multiOptions' => $backendTypes
+ )
+ );
+
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ // In case another error occured and the checkbox was displayed before
+ $this->addSkipValidationCheckbox();
+ }
+
+ $this->addSubForm($this->getBackendForm($backendType)->create($formData), 'backend_form');
+ }
+
+ /**
+ * Populate the configuration of the backend to load
+ */
+ public function onRequest()
+ {
+ if ($this->backendToLoad) {
+ $data = $this->config->getSection($this->backendToLoad)->toArray();
+ $data['name'] = $this->backendToLoad;
+ $data['type'] = $data['backend'];
+ $this->populate($data);
+ }
+ }
+
+ /**
+ * Return whether the given values are valid
+ *
+ * @param array $formData The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ if (($el = $this->getElement('skip_validation')) === null || false === $el->isChecked()) {
+ $inspection = static::inspectUserBackend($this);
+ if ($inspection && $inspection->hasError()) {
+ $this->error($inspection->getError());
+ if ($el === null) {
+ $this->addSkipValidationCheckbox();
+ }
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Create a user backend by using the given form's values and return its inspection results
+ *
+ * Returns null for non-inspectable backends.
+ *
+ * @param Form $form
+ *
+ * @return Inspection|null
+ */
+ public static function inspectUserBackend(Form $form)
+ {
+ $backend = UserBackend::create(null, new ConfigObject($form->getValues()));
+ if ($backend instanceof Inspectable) {
+ return $backend->inspect();
+ }
+ }
+
+ /**
+ * Add a checkbox to the form by which the user can skip the connection validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'order' => 0,
+ 'ignore' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate(
+ 'Check this box to enforce changes without validating that authentication is possible.'
+ )
+ )
+ );
+ }
+
+ /**
+ * Run the configured backend's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (! parent::isValidPartial($formData)) {
+ return false;
+ }
+
+ if ($this->getElement('backend_validation')->isChecked() && parent::isValid($formData)) {
+ $inspection = static::inspectUserBackend($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $e;
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'backend_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ $this->addDisplayGroup(
+ array('btn_submit', 'backend_validation'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserBackendReorderForm.php b/application/forms/Config/UserBackendReorderForm.php
new file mode 100644
index 0000000..019c032
--- /dev/null
+++ b/application/forms/Config/UserBackendReorderForm.php
@@ -0,0 +1,86 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use Icinga\Application\Config;
+use Icinga\Forms\ConfigForm;
+use Icinga\Exception\NotFoundError;
+use Icinga\Web\Notification;
+
+class UserBackendReorderForm extends ConfigForm
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_reorder_authbackend');
+ $this->setViewScript('form/reorder-authbackend.phtml');
+ }
+
+ /**
+ * Return the ordered backend names
+ *
+ * @return array
+ */
+ public function getBackendOrder()
+ {
+ return $this->config->keys();
+ }
+
+ /**
+ * Return the ordered backend configuration
+ *
+ * @return Config
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ // This adds just a dummy element to be able to utilize Form::getValue as part of onSuccess()
+ $this->addElement('hidden', 'backend_newpos');
+ }
+
+ /**
+ * Update the user backend order and save the configuration
+ */
+ public function onSuccess()
+ {
+ $newPosData = $this->getValue('backend_newpos');
+ if ($newPosData) {
+ $configForm = $this->getConfigForm();
+ list($backendName, $position) = explode('|', $newPosData, 2);
+
+ try {
+ if ($configForm->move($backendName, $position)->save()) {
+ Notification::success($this->translate('Authentication order updated'));
+ } else {
+ return false;
+ }
+ } catch (NotFoundError $_) {
+ Notification::error(sprintf($this->translate('User backend "%s" not found'), $backendName));
+ }
+ }
+ }
+
+ /**
+ * Return the config form for user backends
+ *
+ * @return ConfigForm
+ */
+ protected function getConfigForm()
+ {
+ $form = new UserBackendConfigForm();
+ $form->setIniConfig($this->config);
+ return $form;
+ }
+}
diff --git a/application/forms/Config/UserGroup/AddMemberForm.php b/application/forms/Config/UserGroup/AddMemberForm.php
new file mode 100644
index 0000000..cda9d52
--- /dev/null
+++ b/application/forms/Config/UserGroup/AddMemberForm.php
@@ -0,0 +1,183 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Exception;
+use Icinga\Data\Extensible;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Selectable;
+use Icinga\Exception\NotFoundError;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for adding one or more group members
+ */
+class AddMemberForm extends Form
+{
+ /**
+ * The data source to fetch users from
+ *
+ * @var Selectable
+ */
+ protected $ds;
+
+ /**
+ * The user group backend to use
+ *
+ * @var Extensible
+ */
+ protected $backend;
+
+ /**
+ * The group to add members for
+ *
+ * @var string
+ */
+ protected $groupName;
+
+ /**
+ * Set the data source to fetch users from
+ *
+ * @param Selectable $ds
+ *
+ * @return $this
+ */
+ public function setDataSource(Selectable $ds)
+ {
+ $this->ds = $ds;
+ return $this;
+ }
+
+ /**
+ * Set the user group backend to use
+ *
+ * @param Extensible $backend
+ *
+ * @return $this
+ */
+ public function setBackend(Extensible $backend)
+ {
+ $this->backend = $backend;
+ return $this;
+ }
+
+ /**
+ * Set the group to add members for
+ *
+ * @param string $groupName
+ *
+ * @return $this
+ */
+ public function setGroupName($groupName)
+ {
+ $this->groupName = $groupName;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ // TODO(jom): Fetching already existing members to prevent the user from mistakenly creating duplicate
+ // memberships (no matter whether the data source permits it or not, a member does never need to be
+ // added more than once) should be kept at backend level (GroupController::fetchUsers) but this does
+ // not work currently as our ldap protocol stuff is unable to handle our filter implementation..
+ $members = $this->backend
+ ->select()
+ ->from('group_membership', array('user_name'))
+ ->where('group_name', $this->groupName)
+ ->fetchColumn();
+ $filter = empty($members) ? Filter::matchAll() : Filter::not(Filter::where('user_name', $members));
+
+ $users = $this->ds->select()->from('user', array('user_name'))->applyFilter($filter)->fetchColumn();
+ if (! empty($users)) {
+ $this->addElement(
+ 'multiselect',
+ 'user_name',
+ array(
+ 'multiOptions' => array_combine($users, $users),
+ 'label' => $this->translate('Backend Users'),
+ 'description' => $this->translate(
+ 'Select one or more users (fetched from your user backends) to add as group member'
+ ),
+ 'class' => 'grant-permissions'
+ )
+ );
+ }
+
+ $this->addElement(
+ 'textarea',
+ 'users',
+ array(
+ 'required' => empty($users),
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate(
+ 'Provide one or more usernames separated by comma to add as group member'
+ )
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Add members for group %s'), $this->groupName));
+ $this->setSubmitLabel($this->translate('Add'));
+ }
+
+ /**
+ * Insert the members for the group
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ $userNames = $this->getValue('user_name') ?: array();
+ if (($users = $this->getValue('users'))) {
+ $userNames = array_merge($userNames, array_map('trim', explode(',', $users)));
+ }
+
+ if (empty($userNames)) {
+ $this->info($this->translate(
+ 'Please provide at least one username, either by choosing one '
+ . 'in the list or by manually typing one in the text box below'
+ ));
+ return false;
+ }
+
+ $single = null;
+ $userName = null;
+ foreach ($userNames as $userName) {
+ try {
+ $this->backend->insert(
+ 'group_membership',
+ array(
+ 'group_name' => $this->groupName,
+ 'user_name' => $userName
+ )
+ );
+ } catch (NotFoundError $e) {
+ throw $e; // Trigger 404, the group name is initially accessed as GET parameter
+ } catch (Exception $e) {
+ Notification::error(sprintf(
+ $this->translate('Failed to add "%s" as group member for "%s"'),
+ $userName,
+ $this->groupName
+ ));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ $single = $single === null;
+ }
+
+ if ($single) {
+ Notification::success(sprintf($this->translate('Group member "%s" added successfully'), $userName));
+ } else {
+ Notification::success($this->translate('Group members added successfully'));
+ }
+
+ return true;
+ }
+}
diff --git a/application/forms/Config/UserGroup/DbUserGroupBackendForm.php b/application/forms/Config/UserGroup/DbUserGroupBackendForm.php
new file mode 100644
index 0000000..daea8de
--- /dev/null
+++ b/application/forms/Config/UserGroup/DbUserGroupBackendForm.php
@@ -0,0 +1,79 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Data\ResourceFactory;
+use Icinga\Web\Form;
+
+/**
+ * Form for managing database user group backends
+ */
+class DbUserGroupBackendForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_dbusergroupbackend');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this user group backend that is used to differentiate it from others'
+ )
+ )
+ );
+
+ $resourceNames = $this->getDatabaseResourceNames();
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Connection'),
+ 'description' => $this->translate('The database connection to use for this backend'),
+ 'multiOptions' => empty($resourceNames) ? array() : array_combine($resourceNames, $resourceNames)
+ )
+ );
+
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true, // Prevents the element from being submitted, see #7717
+ 'value' => 'db'
+ )
+ );
+ }
+
+ /**
+ * Return the names of all configured database resources
+ *
+ * @return array
+ */
+ protected function getDatabaseResourceNames()
+ {
+ $names = array();
+ foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
+ if (strtolower($config->type) === 'db') {
+ $names[] = $name;
+ }
+ }
+
+ return $names;
+ }
+}
diff --git a/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
new file mode 100644
index 0000000..10c069a
--- /dev/null
+++ b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
@@ -0,0 +1,370 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Authentication\UserGroup\LdapUserGroupBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Protocol\Ldap\LdapConnection;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for managing LDAP user group backends
+ */
+class LdapUserGroupBackendForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_ldapusergroupbackend');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this user group backend that is used to differentiate it from others'
+ )
+ )
+ );
+
+ $resourceNames = $this->getLdapResourceNames();
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('LDAP Connection'),
+ 'description' => $this->translate('The LDAP connection to use for this backend.'),
+ 'multiOptions' => array_combine($resourceNames, $resourceNames)
+ )
+ );
+ $resource = ResourceFactory::create(
+ isset($formData['resource']) && in_array($formData['resource'], $resourceNames)
+ ? $formData['resource']
+ : $resourceNames[0]
+ );
+
+ $userBackendNames = $this->getLdapUserBackendNames($resource);
+ if (! empty($userBackendNames)) {
+ $userBackends = array_combine($userBackendNames, $userBackendNames);
+ $userBackends['none'] = $this->translate('None', 'usergroupbackend.ldap.user_backend');
+ } else {
+ $userBackends = array('none' => $this->translate('None', 'usergroupbackend.ldap.user_backend'));
+ }
+ $this->addElement(
+ 'select',
+ 'user_backend',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('User Backend'),
+ 'description' => $this->translate('The user backend to link with this user group backend.'),
+ 'multiOptions' => $userBackends
+ )
+ );
+
+ $groupBackend = new LdapUserGroupBackend($resource);
+ if ($formData['type'] === 'ldap') {
+ $defaults = $groupBackend->getOpenLdapDefaults();
+ $groupConfigDisabled = $userConfigDisabled = null; // MUST BE null, do NOT change this to false!
+ } else { // $formData['type'] === 'msldap'
+ $defaults = $groupBackend->getActiveDirectoryDefaults();
+ $groupConfigDisabled = $userConfigDisabled = true;
+ }
+
+ if ($formData['type'] === 'msldap') {
+ $this->addElement(
+ 'checkbox',
+ 'nested_group_search',
+ array(
+ 'description' => $this->translate(
+ 'Check this box for nested group search in Active Directory based on the user'
+ ),
+ 'label' => $this->translate('Nested Group Search')
+ )
+ );
+ } else {
+ // This is required to purge already present options
+ $this->addElement('hidden', 'nested_group_search', array('disabled' => true));
+ }
+
+ $this->createGroupConfigElements($defaults, $groupConfigDisabled);
+ if (count($userBackends) === 1 || (isset($formData['user_backend']) && $formData['user_backend'] === 'none')) {
+ $this->createUserConfigElements($defaults, $userConfigDisabled);
+ } else {
+ $this->createHiddenUserConfigElements();
+ }
+
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true, // Prevents the element from being submitted, see #7717
+ 'value' => $formData['type']
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements to this form required for the group configuration
+ *
+ * @param ConfigObject $defaults
+ * @param null|bool $disabled
+ */
+ protected function createGroupConfigElements(ConfigObject $defaults, $disabled)
+ {
+ $this->addElement(
+ 'text',
+ 'group_class',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Object Class'),
+ 'description' => $this->translate('The object class used for storing groups on the LDAP server.'),
+ 'value' => $defaults->group_class
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'label' => $this->translate('LDAP Group Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up groups using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression, without'
+ . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ return strpos($v, '(') !== 0;
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.')
+ )
+ )
+ )
+ ),
+ 'value' => $defaults->group_filter
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing a group\'s name on the LDAP server.'
+ ),
+ 'value' => $defaults->group_name_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_member_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Member Attribute'),
+ 'description' => $this->translate('The attribute name used for storing a group\'s members.'),
+ 'value' => $defaults->group_member_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('LDAP Group Base DN'),
+ 'description' => $this->translate(
+ 'The path where groups can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $defaults->base_dn
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements to this form required for the user configuration
+ *
+ * @param ConfigObject $defaults
+ * @param null|bool $disabled
+ */
+ protected function createUserConfigElements(ConfigObject $defaults, $disabled)
+ {
+ $this->addElement(
+ 'text',
+ 'user_class',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP User Object Class'),
+ 'description' => $this->translate('The object class used for storing users on the LDAP server.'),
+ 'value' => $defaults->user_class
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'label' => $this->translate('LDAP User Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up users using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression, without'
+ . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ return strpos($v, '(') !== 0;
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.')
+ )
+ )
+ )
+ ),
+ 'value' => $defaults->user_filter
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP User Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing a user\'s name on the LDAP server.'
+ ),
+ 'value' => $defaults->user_name_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('LDAP User Base DN'),
+ 'description' => $this->translate(
+ 'The path where users can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $defaults->user_base_dn
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'domain',
+ array(
+ 'label' => $this->translate('Domain'),
+ 'description' => $this->translate(
+ 'The domain the LDAP server is responsible for.'
+ )
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements for the user configuration as hidden inputs
+ *
+ * This is required to purge already present options when unlinking a group backend with a user backend.
+ */
+ protected function createHiddenUserConfigElements()
+ {
+ $this->addElement('hidden', 'user_class', array('disabled' => true));
+ $this->addElement('hidden', 'user_filter', array('disabled' => true));
+ $this->addElement('hidden', 'user_name_attribute', array('disabled' => true));
+ $this->addElement('hidden', 'user_base_dn', array('disabled' => true));
+ $this->addElement('hidden', 'domain', array('disabled' => true));
+ }
+
+ /**
+ * Return the names of all configured LDAP resources
+ *
+ * @return array
+ */
+ protected function getLdapResourceNames()
+ {
+ $names = array();
+ foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
+ if (in_array(strtolower($config->type), array('ldap', 'msldap'))) {
+ $names[] = $name;
+ }
+ }
+
+ if (empty($names)) {
+ Notification::error(
+ $this->translate('No LDAP resources available. Please configure an LDAP resource first.')
+ );
+ $this->getResponse()->redirectAndExit('config/createresource');
+ }
+
+ return $names;
+ }
+
+ /**
+ * Return the names of all configured LDAP user backends
+ *
+ * @param LdapConnection $resource
+ *
+ * @return array
+ */
+ protected function getLdapUserBackendNames(LdapConnection $resource)
+ {
+ $names = array();
+ foreach (UserBackend::getBackendConfigs() as $name => $config) {
+ if (in_array(strtolower($config->backend), array('ldap', 'msldap'))) {
+ $backendResource = ResourceFactory::create($config->resource);
+ if ($backendResource->getHostname() === $resource->getHostname()
+ && $backendResource->getPort() === $resource->getPort()
+ ) {
+ $names[] = $name;
+ }
+ }
+ }
+
+ return $names;
+ }
+}
diff --git a/application/forms/Config/UserGroup/UserGroupBackendForm.php b/application/forms/Config/UserGroup/UserGroupBackendForm.php
new file mode 100644
index 0000000..9ee4032
--- /dev/null
+++ b/application/forms/Config/UserGroup/UserGroupBackendForm.php
@@ -0,0 +1,314 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Authentication\UserGroup\UserGroupBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Web\Form;
+use InvalidArgumentException;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\ConfigForm;
+
+/**
+ * Form for managing user group backends
+ */
+class UserGroupBackendForm extends ConfigForm
+{
+ protected $validatePartial = true;
+
+ /**
+ * The backend to load when displaying the form for the first time
+ *
+ * @var string
+ */
+ protected $backendToLoad;
+
+ /**
+ * Known custom backends
+ *
+ * @var array
+ */
+ protected $customBackends;
+
+ /**
+ * Create a user group backend by using the given form's values and return its inspection results
+ *
+ * Returns null for non-inspectable backends.
+ *
+ * @param Form $form
+ *
+ * @return Inspection|null
+ */
+ public static function inspectUserBackend(Form $form)
+ {
+ $backend = UserGroupBackend::create(null, new ConfigObject($form->getValues()));
+ if ($backend instanceof Inspectable) {
+ return $backend->inspect();
+ }
+ }
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_usergroupbackend');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->customBackends = UserGroupBackend::getCustomBackendConfigForms();
+ }
+
+ /**
+ * Return a form object for the given backend type
+ *
+ * @param string $type The backend type for which to return a form
+ *
+ * @return Form
+ *
+ * @throws InvalidArgumentException In case the given backend type is invalid
+ */
+ public function getBackendForm($type)
+ {
+ switch ($type) {
+ case 'db':
+ return new DbUserGroupBackendForm();
+ case 'ldap':
+ case 'msldap':
+ return new LdapUserGroupBackendForm();
+ default:
+ if (isset($this->customBackends[$type])) {
+ return new $this->customBackends[$type]();
+ }
+
+ throw new InvalidArgumentException(
+ sprintf($this->translate('Invalid backend type "%s" provided'), $type)
+ );
+ }
+ }
+
+ /**
+ * Populate the form with the given backend's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function load($name)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user group backend called "%s" found', $name);
+ }
+
+ $this->backendToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new user group backend
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a backend name
+ * @throws IcingaException In case a backend with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ }
+
+ $backendName = $data['name'];
+ if ($this->config->hasSection($backendName)) {
+ throw new IcingaException('A user group backend with the name "%s" does already exist', $backendName);
+ }
+
+ unset($data['name']);
+ $this->config->setSection($backendName, $data);
+ return $this;
+ }
+
+ /**
+ * Edit a user group backend
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function edit($name, array $data)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user group backend called "%s" found', $name);
+ }
+
+ $backendConfig = $this->config->getSection($name);
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $this->config->removeSection($name);
+ $name = $data['name'];
+ }
+
+ unset($data['name']);
+ }
+
+ $this->config->setSection($name, $backendConfig->merge($data));
+ return $this;
+ }
+
+ /**
+ * Remove a user group backend
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function delete($name)
+ {
+ $this->config->removeSection($name);
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $backendTypes = array(
+ 'db' => $this->translate('Database'),
+ 'ldap' => 'LDAP',
+ 'msldap' => 'ActiveDirectory'
+ );
+
+ $customBackendTypes = array_keys($this->customBackends);
+ $backendTypes += array_combine($customBackendTypes, $customBackendTypes);
+
+ $backendType = isset($formData['type']) ? $formData['type'] : null;
+ if ($backendType === null) {
+ $backendType = key($backendTypes);
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'ignore' => true,
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate('The type of this user group backend'),
+ 'multiOptions' => $backendTypes
+ )
+ );
+
+ $this->addSubForm($this->getBackendForm($backendType)->create($formData), 'backend_form');
+ }
+
+ /**
+ * Populate the configuration of the backend to load
+ */
+ public function onRequest()
+ {
+ if ($this->backendToLoad) {
+ $data = $this->config->getSection($this->backendToLoad)->toArray();
+ $data['type'] = $data['backend'];
+ $data['name'] = $this->backendToLoad;
+ $this->populate($data);
+ }
+ }
+
+ /**
+ * Run the configured backend's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (isset($formData['backend_validation']) && parent::isValid($formData)) {
+ $inspection = static::inspectUserBackend($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $e;
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'backend_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ $this->addDisplayGroup(
+ array('btn_submit', 'backend_validation'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserGroup/UserGroupForm.php b/application/forms/Config/UserGroup/UserGroupForm.php
new file mode 100644
index 0000000..b944e97
--- /dev/null
+++ b/application/forms/Config/UserGroup/UserGroupForm.php
@@ -0,0 +1,158 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\RepositoryForm;
+use Icinga\Web\Notification;
+
+class UserGroupForm extends RepositoryForm
+{
+ /**
+ * Create and add elements to this form to insert or update a group
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createInsertElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'group_name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Group Name')
+ )
+ );
+
+ if ($this->shouldInsert()) {
+ $this->setTitle($this->translate('Add a new group'));
+ $this->setSubmitLabel($this->translate('Add'));
+ } else { // $this->shouldUpdate()
+ $this->setTitle(sprintf($this->translate('Edit group %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+ }
+
+ /**
+ * Update a group
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ if (parent::onUpdateSuccess()) {
+ if (($newName = $this->getValue('group_name')) !== $this->getIdentifier()) {
+ $this->getRedirectUrl()->setParam('group', $newName);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Create and add elements to this form to delete a group
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove group %s?'), $this->getIdentifier()));
+ $this->addDescription($this->translate(
+ 'Note that all users that are currently a member of this group will'
+ . ' have their membership cleared automatically.'
+ ));
+ $this->setSubmitLabel($this->translate('Yes'));
+ $this->setAttrib('class', 'icinga-form icinga-controls');
+ }
+
+ /**
+ * Create and return a filter to use when updating or deleting a group
+ *
+ * @return Filter
+ */
+ protected function createFilter()
+ {
+ return Filter::where('group_name', $this->getIdentifier());
+ }
+
+ /**
+ * Return a notification message to use when inserting a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getInsertMessage($success)
+ {
+ if ($success) {
+ return $this->translate('Group added successfully');
+ } else {
+ return $this->translate('Failed to add group');
+ }
+ }
+
+ /**
+ * Return a notification message to use when updating a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getUpdateMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('Group "%s" has been edited'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to edit group "%s"'), $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Return a notification message to use when deleting a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getDeleteMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('Group "%s" has been removed'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to remove group "%s"'), $this->getIdentifier());
+ }
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ if (parent::onSuccess() === false) {
+ return false;
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+}
diff --git a/application/forms/ConfigForm.php b/application/forms/ConfigForm.php
new file mode 100644
index 0000000..8b0c5f9
--- /dev/null
+++ b/application/forms/ConfigForm.php
@@ -0,0 +1,192 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Exception;
+use Zend_Form_Decorator_Abstract;
+use Icinga\Application\Config;
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form base-class providing standard functionality for configuration forms
+ */
+class ConfigForm extends Form
+{
+ /**
+ * The configuration to work with
+ *
+ * @var Config
+ */
+ protected $config;
+
+ /**
+ * {@inheritdoc}
+ *
+ * Values from subforms are directly added to the returned values array instead of being grouped by the subforms'
+ * names.
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ foreach (array_keys($this->_subForms) as $name) {
+ // Zend returns values from subforms grouped by their names, but we want them flat
+ $values = array_merge($values, $values[$name]);
+ unset($values[$name]);
+ }
+ return $values;
+ }
+
+ /**
+ * Set the configuration to use when populating the form or when saving the user's input
+ *
+ * @param Config $config The configuration to use
+ *
+ * @return $this
+ */
+ public function setIniConfig(Config $config)
+ {
+ $this->config = $config;
+ return $this;
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ $sections = array();
+ foreach (static::transformEmptyValuesToNull($this->getValues()) as $sectionAndPropertyName => $value) {
+ list($section, $property) = explode('_', $sectionAndPropertyName, 2);
+ $sections[$section][$property] = $value;
+ }
+
+ foreach ($sections as $section => $config) {
+ if ($this->isEmptyConfig($config)) {
+ $this->config->removeSection($section);
+ } else {
+ $this->config->setSection($section, $config);
+ }
+ }
+
+ if ($this->save()) {
+ Notification::success($this->translate('New configuration has successfully been stored'));
+ } else {
+ return false;
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+
+ public function onRequest()
+ {
+ $values = array();
+ foreach ($this->config as $section => $properties) {
+ foreach ($properties as $name => $value) {
+ $values[$section . '_' . $name] = $value;
+ }
+ }
+
+ $this->populate($values);
+ }
+
+ /**
+ * Persist the current configuration to disk
+ *
+ * If an error occurs the user is shown a view describing the issue and displaying the raw INI configuration.
+ *
+ * @return bool Whether the configuration could be persisted
+ */
+ public function save()
+ {
+ try {
+ $this->writeConfig($this->config);
+ } catch (ConfigurationError $e) {
+ $this->addError($e->getMessage());
+
+ return false;
+ } catch (Exception $e) {
+ $this->addDecorator('ViewScript', array(
+ 'viewModule' => 'default',
+ 'viewScript' => 'showConfiguration.phtml',
+ 'errorMessage' => $e->getMessage(),
+ 'configString' => $this->config,
+ 'filePath' => $this->config->getConfigFile(),
+ 'placement' => Zend_Form_Decorator_Abstract::PREPEND
+ ));
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Write the configuration to disk
+ *
+ * @param Config $config
+ */
+ protected function writeConfig(Config $config)
+ {
+ $config->saveIni();
+ }
+
+ /**
+ * Get whether the given config is empty or has only empty values
+ *
+ * @param array|Config $config
+ *
+ * @return bool
+ */
+ protected function isEmptyConfig($config)
+ {
+ if ($config instanceof Config) {
+ $config = $config->toArray();
+ }
+
+ foreach ($config as $value) {
+ if ($value !== null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Transform all empty values of the given array to null
+ *
+ * @param array $values
+ *
+ * @return array
+ */
+ public static function transformEmptyValuesToNull(array $values)
+ {
+ array_walk($values, function (&$v) {
+ if ($v === '' || $v === false || $v === array()) {
+ $v = null;
+ }
+ });
+
+ return $values;
+ }
+}
diff --git a/application/forms/ConfirmRemovalForm.php b/application/forms/ConfirmRemovalForm.php
new file mode 100644
index 0000000..39fc661
--- /dev/null
+++ b/application/forms/ConfirmRemovalForm.php
@@ -0,0 +1,38 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Web\Form;
+
+/**
+ * Form for confirming removal of an object
+ */
+class ConfirmRemovalForm extends Form
+{
+ const DEFAULT_CLASSES = 'icinga-controls';
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_confirm_removal');
+ $this->getSubmitLabel() ?: $this->setSubmitLabel($this->translate('Confirm Removal'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton();
+
+ if (($submit = $this->getElement('btn_submit')) !== null) {
+ $class = $submit->getAttrib('class');
+ $submit->setAttrib('class', empty($class) ? 'autofocus' : $class . ' autofocus');
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Control/LimiterControlForm.php b/application/forms/Control/LimiterControlForm.php
new file mode 100644
index 0000000..88adf4b
--- /dev/null
+++ b/application/forms/Control/LimiterControlForm.php
@@ -0,0 +1,134 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Control;
+
+use Icinga\Web\Form;
+
+/**
+ * Limiter control form
+ */
+class LimiterControlForm extends Form
+{
+ /**
+ * CSS class for the limiter control
+ *
+ * @var string
+ */
+ const CSS_CLASS_LIMITER = 'limiter-control icinga-controls inline';
+
+ /**
+ * Default limit
+ *
+ * @var int
+ */
+ const DEFAULT_LIMIT = 50;
+
+ /**
+ * Selectable default limits
+ *
+ * @var int[]
+ */
+ public static $limits = array(
+ 10 => '10',
+ 25 => '25',
+ 50 => '50',
+ 100 => '100',
+ 500 => '500'
+ );
+
+ public static $defaultElementDecorators = [
+ ['Label', ['tag' => 'span', 'separator' => '']],
+ ['ViewHelper', ['separator' => '']],
+ ];
+
+ /**
+ * Default limit for this instance
+ *
+ * @var int|null
+ */
+ protected $defaultLimit;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setAttrib('class', static::CSS_CLASS_LIMITER);
+ }
+
+ /**
+ * Get the default limit
+ *
+ * @return int
+ */
+ public function getDefaultLimit()
+ {
+ return $this->defaultLimit !== null ? $this->defaultLimit : static::DEFAULT_LIMIT;
+ }
+
+ /**
+ * Set the default limit
+ *
+ * @param int $defaultLimit
+ *
+ * @return $this
+ */
+ public function setDefaultLimit($defaultLimit)
+ {
+ $defaultLimit = (int) $defaultLimit;
+
+ if (! isset(static::$limits[$defaultLimit])) {
+ static::$limits[$defaultLimit] = $defaultLimit;
+ }
+
+ $this->defaultLimit = $defaultLimit;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRedirectUrl()
+ {
+ return $this->getRequest()->getUrl()
+ ->setParam('limit', $this->getElement('limit')->getValue())
+ ->without('page');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $options = static::$limits;
+ $pageSize = (int) $this->getRequest()->getUrl()->getParam('limit', $this->getDefaultLimit());
+
+ if (! isset($options[$pageSize])) {
+ $options[$pageSize] = $pageSize;
+ }
+
+ $this->addElement(
+ 'select',
+ 'limit',
+ array(
+ 'autosubmit' => true,
+ 'escape' => false,
+ 'label' => '#',
+ 'multiOptions' => $options,
+ 'value' => $pageSize
+ )
+ );
+ }
+
+ /**
+ * Limiter control is always successful
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ return true;
+ }
+}
diff --git a/application/forms/Dashboard/DashletForm.php b/application/forms/Dashboard/DashletForm.php
new file mode 100644
index 0000000..1af65a9
--- /dev/null
+++ b/application/forms/Dashboard/DashletForm.php
@@ -0,0 +1,171 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Dashboard;
+
+use Icinga\Web\Form;
+use Icinga\Web\Form\Validator\InternalUrlValidator;
+use Icinga\Web\Form\Validator\UrlValidator;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Dashboard;
+use Icinga\Web\Widget\Dashboard\Dashlet;
+
+/**
+ * Form to add an url a dashboard pane
+ */
+class DashletForm extends Form
+{
+ /**
+ * @var Dashboard
+ */
+ private $dashboard;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_dashboard_addurl');
+ if (! $this->getSubmitLabel()) {
+ $this->setSubmitLabel($this->translate('Add To Dashboard'));
+ }
+ $this->setAction(Url::fromRequest());
+ }
+
+ /**
+ * Build AddUrl form elements
+ *
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $groupElements = array();
+ $panes = array();
+
+ if ($this->dashboard) {
+ $panes = $this->dashboard->getPaneKeyTitleArray();
+ }
+
+ $sectionNameValidator = ['Callback', true, [
+ 'callback' => function ($value) {
+ if (strpos($value, '[') === false && strpos($value, ']') === false) {
+ return true;
+ }
+ },
+ 'messages' => [
+ 'callbackValue' => $this->translate('Brackets ([, ]) cannot be used here')
+ ]
+ ]];
+
+ $this->addElement(
+ 'hidden',
+ 'org_pane',
+ array(
+ 'required' => false
+ )
+ );
+
+ $this->addElement(
+ 'hidden',
+ 'org_dashlet',
+ array(
+ 'required' => false
+ )
+ );
+
+ $this->addElement(
+ 'textarea',
+ 'url',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Url'),
+ 'description' => $this->translate(
+ 'Enter url to be loaded in the dashlet. You can paste the full URL, including filters.'
+ ),
+ 'validators' => array(new UrlValidator(), new InternalUrlValidator())
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'dashlet',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Dashlet Title'),
+ 'description' => $this->translate('Enter a title for the dashlet.'),
+ 'validators' => [$sectionNameValidator]
+ )
+ );
+ $this->addElement(
+ 'note',
+ 'note',
+ array(
+ 'decorators' => array(
+ array('HtmlTag', array('tag' => 'hr'))
+ )
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'create_new_pane',
+ array(
+ 'autosubmit' => true,
+ 'required' => false,
+ 'label' => $this->translate('New dashboard'),
+ 'description' => $this->translate('Check this box if you want to add the dashlet to a new dashboard')
+ )
+ );
+ if (empty($panes) || ((isset($formData['create_new_pane']) && $formData['create_new_pane'] != false))) {
+ $this->addElement(
+ 'text',
+ 'pane',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('New Dashboard Title'),
+ 'description' => $this->translate('Enter a title for the new dashboard'),
+ 'validators' => [$sectionNameValidator]
+ )
+ );
+ } else {
+ $this->addElement(
+ 'select',
+ 'pane',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Dashboard'),
+ 'multiOptions' => $panes,
+ 'description' => $this->translate('Select a dashboard you want to add the dashlet to')
+ )
+ );
+ }
+ }
+
+ /**
+ * @param \Icinga\Web\Widget\Dashboard $dashboard
+ */
+ public function setDashboard(Dashboard $dashboard)
+ {
+ $this->dashboard = $dashboard;
+ }
+
+ /**
+ * @return \Icinga\Web\Widget\Dashboard
+ */
+ public function getDashboard()
+ {
+ return $this->dashboard;
+ }
+
+ /**
+ * @param Dashlet $dashlet
+ */
+ public function load(Dashlet $dashlet)
+ {
+ $this->populate(array(
+ 'pane' => $dashlet->getPane()->getTitle(),
+ 'org_pane' => $dashlet->getPane()->getName(),
+ 'dashlet' => $dashlet->getTitle(),
+ 'org_dashlet' => $dashlet->getName(),
+ 'url' => $dashlet->getUrl()->getRelativeUrl()
+ ));
+ }
+}
diff --git a/application/forms/LdapDiscoveryForm.php b/application/forms/LdapDiscoveryForm.php
new file mode 100644
index 0000000..5c7fc87
--- /dev/null
+++ b/application/forms/LdapDiscoveryForm.php
@@ -0,0 +1,34 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Web\Form;
+
+class LdapDiscoveryForm extends Form
+{
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('form_ldap_discovery');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'domain',
+ array(
+ 'label' => $this->translate('Search Domain'),
+ 'description' => $this->translate('Search this domain for records of available servers.'),
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/MigrationForm.php b/application/forms/MigrationForm.php
new file mode 100644
index 0000000..c5d517f
--- /dev/null
+++ b/application/forms/MigrationForm.php
@@ -0,0 +1,143 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Icinga\Application\MigrationManager;
+use ipl\Html\Attributes;
+use ipl\Html\FormElement\CheckboxElement;
+use ipl\Html\FormElement\FieldsetElement;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\I18n\Translation;
+use ipl\Validator\CallbackValidator;
+use ipl\Web\Common\CsrfCounterMeasure;
+use ipl\Web\Common\FormUid;
+use ipl\Web\Compat\CompatForm;
+use ipl\Web\FormDecorator\IcingaFormDecorator;
+use PDOException;
+
+class MigrationForm extends CompatForm
+{
+ use CsrfCounterMeasure;
+ use FormUid;
+ use Translation;
+
+ protected $defaultAttributes = [
+ 'class' => ['icinga-form', 'migration-form', 'icinga-controls'],
+ 'name' => 'migration-form'
+ ];
+
+ /** @var bool Whether to allow changing the current database user and password */
+ protected $renderDatabaseUserChange = false;
+
+ public function hasBeenSubmitted(): bool
+ {
+ if (! $this->hasBeenSent()) {
+ return false;
+ }
+
+ $pressedButton = $this->getPressedSubmitElement();
+
+ return $pressedButton && strpos($pressedButton->getName(), 'migrate-') !== false;
+ }
+
+ public function setRenderDatabaseUserChange(bool $value = true): self
+ {
+ $this->renderDatabaseUserChange = $value;
+
+ return $this;
+ }
+
+ public function hasDefaultElementDecorator()
+ {
+ // The base implementation registers a decorator we don't want here
+ return false;
+ }
+
+ protected function assemble(): void
+ {
+ $this->addHtml($this->createUidElement());
+
+ if ($this->renderDatabaseUserChange) {
+ $mm = MigrationManager::instance();
+ $newDbSetup = new FieldsetElement('database_setup', ['required' => true]);
+ $newDbSetup
+ ->setDefaultElementDecorator(new IcingaFormDecorator())
+ ->addElement('text', 'username', [
+ 'required' => true,
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate(
+ 'A user which is able to create and/or alter the database schema.'
+ )
+ ])
+ ->addElement('password', 'password', [
+ 'required' => true,
+ 'autocomplete' => 'new-password',
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate('The password for the database user defined above.'),
+ 'validators' => [
+ new CallbackValidator(function ($_, CallbackValidator $validator) use ($mm, $newDbSetup): bool {
+ /** @var array<string, string> $values */
+ $values = $this->getValue('database_setup');
+ /** @var CheckboxElement $checkBox */
+ $checkBox = $newDbSetup->getElement('grant_privileges');
+ $canIssueGrants = $checkBox->isChecked();
+ $elevationConfig = [
+ 'username' => $values['username'],
+ 'password' => $values['password']
+ ];
+
+ try {
+ if (! $mm->validateDatabasePrivileges($elevationConfig, $canIssueGrants)) {
+ $validator->addMessage(sprintf(
+ $this->translate(
+ 'The provided credentials cannot be used to execute "%s" SQL commands'
+ . ' and/or grant the missing privileges to other users.'
+ ),
+ implode(' ,', $mm->getRequiredDatabasePrivileges())
+ ));
+
+ return false;
+ }
+ } catch (PDOException $e) {
+ $validator->addMessage($e->getMessage());
+
+ return false;
+ }
+
+ return true;
+ })
+ ]
+ ])
+ ->addElement('checkbox', 'grant_privileges', [
+ 'required' => false,
+ 'label' => $this->translate('Grant Missing Privileges'),
+ 'description' => $this->translate(
+ 'Allows to automatically grant the required privileges to the database user specified'
+ . ' in the respective resource config. If you do not want to provide additional credentials'
+ . ' each time, you can enable this and Icinga Web will grant the active database user the'
+ . ' missing privileges.'
+ )
+ ]);
+
+ $this->addHtml(
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'change-database-user-description']),
+ new HtmlElement('span', null, Text::create(sprintf(
+ $this->translate(
+ 'It seems that the currently used database user does not have the required privileges to'
+ . ' execute the %s SQL commands. Please provide an alternative user'
+ . ' that has the appropriate credentials to resolve this issue.'
+ ),
+ implode(', ', $mm->getRequiredDatabasePrivileges())
+ )))
+ )
+ );
+
+ $this->addElement($newDbSetup);
+ }
+ }
+}
diff --git a/application/forms/Navigation/DashletForm.php b/application/forms/Navigation/DashletForm.php
new file mode 100644
index 0000000..6575fd7
--- /dev/null
+++ b/application/forms/Navigation/DashletForm.php
@@ -0,0 +1,35 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Navigation;
+
+class DashletForm extends NavigationItemForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'pane',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Pane'),
+ 'description' => $this->translate('The name of the dashboard pane in which to display this dashlet')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'url',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Url'),
+ 'description' => $this->translate(
+ 'The url to load in the dashlet. For external urls, make sure to prepend'
+ . ' an appropriate protocol identifier (e.g. http://example.tld)'
+ )
+ )
+ );
+ }
+}
diff --git a/application/forms/Navigation/MenuItemForm.php b/application/forms/Navigation/MenuItemForm.php
new file mode 100644
index 0000000..c9fa729
--- /dev/null
+++ b/application/forms/Navigation/MenuItemForm.php
@@ -0,0 +1,31 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Navigation;
+
+class MenuItemForm extends NavigationItemForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $requiresParentSelection = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ parent::createElements($formData);
+
+ // Remove _self and _next as for menu entries only _main is valid
+ $this->getElement('target')->removeMultiOption('_self');
+ $this->getElement('target')->removeMultiOption('_next');
+
+ $parentElement = $this->getParent()->getElement('parent');
+ if ($parentElement !== null) {
+ $parentElement->setDescription($this->translate(
+ 'The parent menu to assign this menu entry to. Select "None" to make this a main menu entry'
+ ));
+ }
+ }
+}
diff --git a/application/forms/Navigation/NavigationConfigForm.php b/application/forms/Navigation/NavigationConfigForm.php
new file mode 100644
index 0000000..0c4ae32
--- /dev/null
+++ b/application/forms/Navigation/NavigationConfigForm.php
@@ -0,0 +1,853 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Navigation;
+
+use InvalidArgumentException;
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Application\Icinga;
+use Icinga\Authentication\Auth;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Forms\ConfigForm;
+use Icinga\User;
+use Icinga\Util\StringHelper;
+use Icinga\Web\Form;
+
+/**
+ * Form for managing navigation items
+ */
+class NavigationConfigForm extends ConfigForm
+{
+ /**
+ * The class namespace where to locate navigation type forms
+ *
+ * @var string
+ */
+ const FORM_NS = 'Forms\\Navigation';
+
+ /**
+ * The secondary configuration to write
+ *
+ * This is always the reduced configuration and is only written to
+ * disk once the main configuration has been successfully written.
+ *
+ * @var Config
+ */
+ protected $secondaryConfig;
+
+ /**
+ * The navigation item to load when displaying the form for the first time
+ *
+ * @var string
+ */
+ protected $itemToLoad;
+
+ /**
+ * The user for whom to manage navigation items
+ *
+ * @var User
+ */
+ protected $user;
+
+ /**
+ * The user's navigation configuration
+ *
+ * @var Config
+ */
+ protected $userConfig;
+
+ /**
+ * The shared navigation configuration
+ *
+ * @var Config
+ */
+ protected $shareConfig;
+
+ /**
+ * The available navigation item types
+ *
+ * @var array
+ */
+ protected $itemTypes;
+
+ private $defaultUrl;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_navigation');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ /**
+ * Set the user for whom to manage navigation items
+ *
+ * @param User $user
+ *
+ * @return $this
+ */
+ public function setUser(User $user)
+ {
+ $this->user = $user;
+ return $this;
+ }
+
+ /**
+ * Return the user for whom to manage navigation items
+ *
+ * @return User
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * Set the user's navigation configuration
+ *
+ * @param Config $config
+ *
+ * @return $this
+ */
+ public function setUserConfig(Config $config)
+ {
+ $config->getConfigObject()->setKeyColumn('name');
+ $this->userConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Return the user's navigation configuration
+ *
+ * @param string $type
+ *
+ * @return Config
+ */
+ public function getUserConfig($type = null)
+ {
+ if ($this->userConfig === null || $type !== null) {
+ if ($type === null) {
+ throw new ProgrammingError('You need to pass a type if no user configuration is set');
+ }
+
+ $this->setUserConfig(Config::navigation($type, $this->getUser()->getUsername()));
+ }
+
+ return $this->userConfig;
+ }
+
+ /**
+ * Set the shared navigation configuration
+ *
+ * @param Config $config
+ *
+ * @return $this
+ */
+ public function setShareConfig(Config $config)
+ {
+ $config->getConfigObject()->setKeyColumn('name');
+ $this->shareConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Return the shared navigation configuration
+ *
+ * @param string $type
+ *
+ * @return Config
+ */
+ public function getShareConfig($type = null)
+ {
+ if ($this->shareConfig === null) {
+ if ($type === null) {
+ throw new ProgrammingError('You need to pass a type if no share configuration is set');
+ }
+
+ $this->setShareConfig(Config::navigation($type));
+ }
+
+ return $this->shareConfig;
+ }
+
+ /**
+ * Set the available navigation item types
+ *
+ * @param array $itemTypes
+ *
+ * @return $this
+ */
+ public function setItemTypes(array $itemTypes)
+ {
+ $this->itemTypes = $itemTypes;
+ return $this;
+ }
+
+ /**
+ * Return the available navigation item types
+ *
+ * @return array
+ */
+ public function getItemTypes()
+ {
+ return $this->itemTypes ?: array();
+ }
+
+ /**
+ * Return a list of available parent items for the given type of navigation item
+ *
+ * @param string $type
+ * @param string $owner
+ *
+ * @return array
+ */
+ public function listAvailableParents($type, $owner = null)
+ {
+ $children = $this->itemToLoad ? $this->getFlattenedChildren($this->itemToLoad) : array();
+
+ $names = array();
+ foreach ($this->getShareConfig($type) as $sectionName => $sectionConfig) {
+ if ((string) $sectionName !== $this->itemToLoad
+ && $sectionConfig->owner === ($owner ?: $this->getUser()->getUsername())
+ && ! in_array($sectionName, $children, true)
+ ) {
+ $names[] = $sectionName;
+ }
+ }
+
+ foreach ($this->getUserConfig($type) as $sectionName => $sectionConfig) {
+ if ((string) $sectionName !== $this->itemToLoad
+ && ! in_array($sectionName, $children, true)
+ ) {
+ $names[] = $sectionName;
+ }
+ }
+
+ return $names;
+ }
+
+ /**
+ * Recursively return all children of the given navigation item
+ *
+ * @param string $name
+ *
+ * @return array
+ */
+ protected function getFlattenedChildren($name)
+ {
+ $config = $this->getConfigForItem($name);
+ if ($config === null) {
+ return array();
+ }
+
+ $children = array();
+ foreach ($config->toArray() as $sectionName => $sectionConfig) {
+ if (isset($sectionConfig['parent']) && $sectionConfig['parent'] === $name) {
+ $children[] = $sectionName;
+ $children = array_merge($children, $this->getFlattenedChildren($sectionName));
+ }
+ }
+
+ return $children;
+ }
+
+ /**
+ * Populate the form with the given navigation item's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ */
+ public function load($name)
+ {
+ if ($this->getConfigForItem($name) === null) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ }
+
+ $this->itemToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new navigation item
+ *
+ * The navigation item to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a navigation item name or type
+ * @throws IcingaException In case a navigation item with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ } elseif (! isset($data['type'])) {
+ throw new InvalidArgumentException('Key \'type\' missing');
+ }
+
+ $shared = false;
+ $config = $this->getUserConfig($data['type']);
+ if ((isset($data['users']) && $data['users']) || (isset($data['groups']) && $data['groups'])) {
+ if ($this->getUser()->can('user/share/navigation')) {
+ $data['owner'] = $this->getUser()->getUsername();
+ $config = $this->getShareConfig($data['type']);
+ $shared = true;
+ } else {
+ unset($data['users']);
+ unset($data['groups']);
+ }
+ } elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'], $data['type'])) {
+ $data['owner'] = $this->getUser()->getUsername();
+ $config = $this->getShareConfig($data['type']);
+ $shared = true;
+ }
+
+ $itemName = $data['name'];
+ $exists = $config->hasSection($itemName);
+ if (! $exists) {
+ if ($shared) {
+ $exists = $this->getUserConfig($data['type'])->hasSection($itemName);
+ } else {
+ $exists = (bool) $this->getShareConfig($data['type'])
+ ->select()
+ ->where('name', $itemName)
+ ->where('owner', $this->getUser()->getUsername())
+ ->count();
+ }
+ }
+
+ if ($exists) {
+ throw new IcingaException(
+ $this->translate('A navigation item with the name "%s" does already exist'),
+ $itemName
+ );
+ }
+
+ unset($data['name']);
+ $config->setSection($itemName, $data);
+ $this->setIniConfig($config);
+ return $this;
+ }
+
+ /**
+ * Edit a navigation item
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ * @throws IcingaException In case a navigation item with the same name already exists
+ */
+ public function edit($name, array $data)
+ {
+ $config = $this->getConfigForItem($name);
+ if ($config === null) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ } else {
+ $itemConfig = $config->getSection($name);
+ }
+
+ $shared = false;
+ if ($this->hasBeenShared($name)) {
+ if (isset($data['parent']) && $data['parent']
+ ? ! $this->hasBeenShared($data['parent'])
+ : ((! isset($data['users']) || ! $data['users']) && (! isset($data['groups']) || ! $data['groups']))
+ ) {
+ // It is shared but shouldn't anymore
+ $config = $this->unshare($name, isset($data['parent']) ? $data['parent'] : null);
+ }
+ } elseif ((isset($data['users']) && $data['users']) || (isset($data['groups']) && $data['groups'])) {
+ if ($this->getUser()->can('user/share/navigation')) {
+ // It is not shared yet but should be
+ $this->secondaryConfig = $config;
+ $config = $this->getShareConfig();
+ $data['owner'] = $this->getUser()->getUsername();
+ $shared = true;
+ } else {
+ unset($data['users']);
+ unset($data['groups']);
+ }
+ } elseif (isset($data['parent']) && $data['parent'] && $this->hasBeenShared($data['parent'])) {
+ // Its parent is shared so should it itself
+ $this->secondaryConfig = $config;
+ $config = $this->getShareConfig();
+ $data['owner'] = $this->getUser()->getUsername();
+ $shared = true;
+ }
+
+ $oldName = null;
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $oldName = $name;
+ $name = $data['name'];
+
+ $exists = $config->hasSection($name);
+ if (! $exists) {
+ $ownerName = $itemConfig->owner ?: $this->getUser()->getUsername();
+ if ($shared || $this->hasBeenShared($oldName)) {
+ if ($ownerName === $this->getUser()->getUsername()) {
+ $exists = $this->getUserConfig()->hasSection($name);
+ } else {
+ $exists = Config::navigation($itemConfig->type, $ownerName)->hasSection($name);
+ }
+ } else {
+ $exists = (bool) $this->getShareConfig()
+ ->select()
+ ->where('name', $name)
+ ->where('owner', $ownerName)
+ ->count();
+ }
+ }
+
+ if ($exists) {
+ throw new IcingaException(
+ $this->translate('A navigation item with the name "%s" does already exist'),
+ $name
+ );
+ }
+ }
+
+ unset($data['name']);
+ }
+
+ $itemConfig->merge($data);
+
+ if ($shared) {
+ // Share all descendant children
+ foreach ($this->getFlattenedChildren($oldName ?: $name) as $child) {
+ $childConfig = $this->secondaryConfig->getSection($child);
+ $this->secondaryConfig->removeSection($child);
+ $childConfig->owner = $this->getUser()->getUsername();
+ $config->setSection($child, $childConfig);
+ }
+ }
+
+ if ($oldName) {
+ // Update the parent name on all direct children
+ foreach ($config as $sectionConfig) {
+ if ($sectionConfig->parent === $oldName) {
+ $sectionConfig->parent = $name;
+ }
+ }
+
+ $config->removeSection($oldName);
+ }
+
+ if ($this->secondaryConfig !== null) {
+ $this->secondaryConfig->removeSection($oldName ?: $name);
+ }
+
+ $config->setSection($name, $itemConfig);
+ $this->setIniConfig($config);
+ return $this;
+ }
+
+ /**
+ * Remove a navigation item
+ *
+ * @param string $name
+ *
+ * @return ConfigObject The navigation item's config
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ * @throws IcingaException In case the navigation item has still children
+ */
+ public function delete($name)
+ {
+ $config = $this->getConfigForItem($name);
+ if ($config === null) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ }
+
+ $children = $this->getFlattenedChildren($name);
+ if (! empty($children)) {
+ throw new IcingaException(
+ $this->translate(
+ 'Unable to delete navigation item "%s". There'
+ . ' are other items dependent from it: %s'
+ ),
+ $name,
+ join(', ', $children)
+ );
+ }
+
+ $section = $config->getSection($name);
+ $config->removeSection($name);
+ $this->setIniConfig($config);
+ return $section;
+ }
+
+ /**
+ * Unshare the given navigation item
+ *
+ * @param string $name
+ * @param string $parent
+ *
+ * @return Config The new config of the given navigation item
+ *
+ * @throws NotFoundError In case no navigation item with the given name is found
+ * @throws IcingaException In case the navigation item has a parent assigned to it
+ */
+ public function unshare($name, $parent = null)
+ {
+ $config = $this->getShareConfig();
+ if (! $config->hasSection($name)) {
+ throw new NotFoundError('No navigation item called "%s" found', $name);
+ }
+
+ $itemConfig = $config->getSection($name);
+ if ($parent === null) {
+ $parent = $itemConfig->parent;
+ }
+
+ if ($parent && $this->hasBeenShared($parent)) {
+ throw new IcingaException(
+ $this->translate(
+ 'Unable to unshare navigation item "%s". It is dependent from item "%s".'
+ . ' Dependent items can only be unshared by unsharing their parent'
+ ),
+ $name,
+ $parent
+ );
+ }
+
+ $children = $this->getFlattenedChildren($name);
+ $config->removeSection($name);
+ $this->secondaryConfig = $config;
+
+ if (! $itemConfig->owner || $itemConfig->owner === $this->getUser()->getUsername()) {
+ $config = $this->getUserConfig();
+ } else {
+ $config = Config::navigation($itemConfig->type, $itemConfig->owner);
+ }
+
+ foreach ($children as $child) {
+ $childConfig = $this->secondaryConfig->getSection($child);
+ unset($childConfig->owner);
+ $this->secondaryConfig->removeSection($child);
+ $config->setSection($child, $childConfig);
+ }
+
+ unset($itemConfig->owner);
+ unset($itemConfig->users);
+ unset($itemConfig->groups);
+
+ $config->setSection($name, $itemConfig);
+ $this->setIniConfig($config);
+ return $config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $shared = false;
+ $itemTypes = $this->getItemTypes();
+ $itemType = isset($formData['type']) ? $formData['type'] : key($itemTypes);
+ if ($itemType === null) {
+ throw new ProgrammingError(
+ 'This should actually not happen. Create a bug report at https://github.com/icinga/icingaweb2'
+ . ' or remove this assertion if you know what you\'re doing'
+ );
+ }
+
+ $itemForm = $this->getItemForm($itemType);
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Name'),
+ 'description' => $this->translate(
+ 'The name of this navigation item that is used to differentiate it from others'
+ )
+ )
+ );
+
+ if ((! $itemForm->requiresParentSelection() || ! isset($formData['parent']) || ! $formData['parent'])
+ && $this->getUser()->can('user/share/navigation')
+ ) {
+ $checked = isset($formData['shared']) ? null : (isset($formData['users']) || isset($formData['groups']));
+
+ $this->addElement(
+ 'checkbox',
+ 'shared',
+ array(
+ 'autosubmit' => true,
+ 'ignore' => true,
+ 'value' => $checked,
+ 'label' => $this->translate('Shared'),
+ 'description' => $this->translate('Tick this box to share this item with others')
+ )
+ );
+
+ if ($checked || (isset($formData['shared']) && $formData['shared'])) {
+ $shared = true;
+ $this->addElement(
+ 'textarea',
+ 'users',
+ array(
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate(
+ 'Comma separated list of usernames to share this item with'
+ )
+ )
+ );
+ $this->addElement(
+ 'textarea',
+ 'groups',
+ array(
+ 'label' => $this->translate('Groups'),
+ 'description' => $this->translate(
+ 'Comma separated list of group names to share this item with'
+ )
+ )
+ );
+ }
+ }
+
+ if (empty($itemTypes) || count($itemTypes) === 1) {
+ $this->addElement(
+ 'hidden',
+ 'type',
+ array(
+ 'value' => $itemType
+ )
+ );
+ } else {
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Type'),
+ 'description' => $this->translate('The type of this navigation item'),
+ 'multiOptions' => $itemTypes
+ )
+ );
+ }
+
+ if (! $shared && $itemForm->requiresParentSelection()) {
+ if ($this->itemToLoad && $this->hasBeenShared($this->itemToLoad)) {
+ $itemConfig = $this->getShareConfig()->getSection($this->itemToLoad);
+ $availableParents = $this->listAvailableParents($itemType, $itemConfig->owner);
+ } else {
+ $availableParents = $this->listAvailableParents($itemType);
+ }
+
+ $this->addElement(
+ 'select',
+ 'parent',
+ array(
+ 'allowEmpty' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Parent'),
+ 'description' => $this->translate(
+ 'The parent item to assign this navigation item to. '
+ . 'Select "None" to make this a main navigation item'
+ ),
+ 'multiOptions' => ['' => $this->translate('None', 'No parent for a navigation item')]
+ + (empty($availableParents) ? [] : array_combine($availableParents, $availableParents))
+ )
+ );
+ } else {
+ $this->addElement('hidden', 'parent', ['disabled' => true]);
+ }
+
+ $this->addSubForm($itemForm, 'item_form');
+ $itemForm->create($formData); // May require a parent which gets set by addSubForm()
+ }
+
+ /**
+ * DO NOT USE! This will be removed soon, very soon...
+ */
+ public function setDefaultUrl($url)
+ {
+ $this->defaultUrl = $url;
+ }
+
+ /**
+ * Populate the configuration of the navigation item to load
+ */
+ public function onRequest()
+ {
+ if ($this->itemToLoad) {
+ $data = $this->getConfigForItem($this->itemToLoad)->getSection($this->itemToLoad)->toArray();
+ $data['name'] = $this->itemToLoad;
+ $this->populate($data);
+ } elseif ($this->defaultUrl !== null) {
+ $this->populate(array('url' => $this->defaultUrl));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ $valid = true;
+ if (isset($formData['users']) && $formData['users']) {
+ $parsedUserRestrictions = array();
+ foreach (Auth::getInstance()->getRestrictions('application/share/users') as $userRestriction) {
+ $parsedUserRestrictions[] = array_map('trim', explode(',', $userRestriction));
+ }
+
+ if (! empty($parsedUserRestrictions)) {
+ $desiredUsers = array_map('trim', explode(',', $formData['users']));
+ array_unshift($parsedUserRestrictions, $desiredUsers);
+ $forbiddenUsers = call_user_func_array('array_diff', $parsedUserRestrictions);
+ if (! empty($forbiddenUsers)) {
+ $valid = false;
+ $this->getElement('users')->addError(
+ sprintf(
+ $this->translate(
+ 'You are not permitted to share this navigation item with the following users: %s'
+ ),
+ implode(', ', $forbiddenUsers)
+ )
+ );
+ }
+ }
+ }
+
+ if (isset($formData['groups']) && $formData['groups']) {
+ $parsedGroupRestrictions = array();
+ foreach (Auth::getInstance()->getRestrictions('application/share/groups') as $groupRestriction) {
+ $parsedGroupRestrictions[] = array_map('trim', explode(',', $groupRestriction));
+ }
+
+ if (! empty($parsedGroupRestrictions)) {
+ $desiredGroups = array_map('trim', explode(',', $formData['groups']));
+ array_unshift($parsedGroupRestrictions, $desiredGroups);
+ $forbiddenGroups = call_user_func_array('array_diff', $parsedGroupRestrictions);
+ if (! empty($forbiddenGroups)) {
+ $valid = false;
+ $this->getElement('groups')->addError(
+ sprintf(
+ $this->translate(
+ 'You are not permitted to share this navigation item with the following groups: %s'
+ ),
+ implode(', ', $forbiddenGroups)
+ )
+ );
+ }
+ }
+ }
+
+ return $valid;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function writeConfig(Config $config)
+ {
+ parent::writeConfig($config);
+
+ if ($this->secondaryConfig !== null) {
+ $this->config = $this->secondaryConfig; // Causes the config being displayed to the user in case of an error
+ parent::writeConfig($this->secondaryConfig);
+ }
+ }
+
+ /**
+ * Return the navigation configuration the given item is a part of
+ *
+ * @param string $name
+ *
+ * @return Config|null In case the item is not part of any configuration
+ */
+ protected function getConfigForItem($name)
+ {
+ if ($this->getUserConfig()->hasSection($name)) {
+ return $this->getUserConfig();
+ } elseif ($this->getShareConfig()->hasSection($name)) {
+ if ($this->getShareConfig()->get($name, 'owner') === $this->getUser()->getUsername()
+ || $this->getUser()->can('user/share/navigation')
+ ) {
+ return $this->getShareConfig();
+ }
+ }
+ }
+
+ /**
+ * Return whether the given navigation item has been shared
+ *
+ * @param string $name
+ * @param string $type
+ *
+ * @return bool
+ */
+ protected function hasBeenShared($name, $type = null)
+ {
+ return $this->getShareConfig($type) === $this->getConfigForItem($name);
+ }
+
+ /**
+ * Return the form for the given type of navigation item
+ *
+ * @param string $type
+ *
+ * @return Form
+ */
+ protected function getItemForm($type)
+ {
+ $className = StringHelper::cname($type, '-') . 'Form';
+
+ $form = null;
+ $classPath = null;
+ foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) {
+ $classPath = 'Icinga\\Module\\'
+ . ucfirst($module->getName())
+ . '\\'
+ . static::FORM_NS
+ . '\\'
+ . $className;
+ if (class_exists($classPath)) {
+ $form = new $classPath();
+ break;
+ }
+ }
+
+ if ($form === null) {
+ $classPath = 'Icinga\\' . static::FORM_NS . '\\' . $className;
+ if (class_exists($classPath)) {
+ $form = new $classPath();
+ }
+ }
+
+ if ($form === null) {
+ Logger::debug(
+ 'Failed to find custom navigation item form %s for item %s. Using form NavigationItemForm now',
+ $className,
+ $type
+ );
+
+ $form = new NavigationItemForm();
+ } elseif (! $form instanceof NavigationItemForm) {
+ throw new ProgrammingError('Class %s must inherit from NavigationItemForm', $classPath);
+ }
+
+ return $form;
+ }
+}
diff --git a/application/forms/Navigation/NavigationItemForm.php b/application/forms/Navigation/NavigationItemForm.php
new file mode 100644
index 0000000..6cf15e7
--- /dev/null
+++ b/application/forms/Navigation/NavigationItemForm.php
@@ -0,0 +1,114 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Navigation;
+
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+
+class NavigationItemForm extends Form
+{
+ /**
+ * Whether to create a select input to choose a parent for a navigation item of a particular type
+ *
+ * @var bool
+ */
+ protected $requiresParentSelection = false;
+
+ /**
+ * Return whether to create a select input to choose a parent for a navigation item of a particular type
+ *
+ * @return bool
+ */
+ public function requiresParentSelection()
+ {
+ return $this->requiresParentSelection;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'select',
+ 'target',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Target'),
+ 'description' => $this->translate('The target where to open this navigation item\'s url'),
+ 'multiOptions' => array(
+ '_blank' => $this->translate('New Window'),
+ '_next' => $this->translate('New Column'),
+ '_main' => $this->translate('Single Column'),
+ '_self' => $this->translate('Current Column')
+ )
+ )
+ );
+
+ $this->addElement(
+ 'textarea',
+ 'url',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Url'),
+ 'description' => $this->translate(
+ 'The url of this navigation item. Leave blank if only the name should be displayed.'
+ . ' For urls with username and password and for all external urls,'
+ . ' make sure to prepend an appropriate protocol identifier (e.g. http://example.tld)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($url) {
+ // Matches if the given url contains obviously
+ // a username but not any protocol identifier
+ return !preg_match('#^((?=[^/@]).)+@.*$#', $url);
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate(
+ 'Missing protocol identifier'
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'icon',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Icon'),
+ 'description' => $this->translate(
+ 'The icon of this navigation item. Leave blank if you do not want a icon being displayed'
+ )
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ // The regex here specifically matches the port-macro as it's the only one preventing Url::fromPath() from
+ // successfully parsing the given url. Any other macro such as for the scheme or host simply gets identified
+ // as path which is just fine in this case.
+ if (isset($values['url']) && $values['url'] && !preg_match('~://.+:\d*?(\$.+\$)~', $values['url'])) {
+ $url = Url::fromPath($values['url']);
+ if ($url->getBasePath() === $this->getRequest()->getBasePath()) {
+ $values['url'] = $url->getRelativeUrl();
+ } else {
+ $values['url'] = $url->getAbsoluteUrl();
+ }
+ }
+
+ return $values;
+ }
+}
diff --git a/application/forms/PreferenceForm.php b/application/forms/PreferenceForm.php
new file mode 100644
index 0000000..3e431db
--- /dev/null
+++ b/application/forms/PreferenceForm.php
@@ -0,0 +1,485 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use DateTimeZone;
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Authentication\Auth;
+use Icinga\User\Preferences;
+use Icinga\User\Preferences\PreferencesStore;
+use Icinga\Util\TimezoneDetect;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+use Icinga\Web\Session;
+use Icinga\Web\StyleSheet;
+use ipl\Html\HtmlElement;
+use ipl\I18n\GettextTranslator;
+use ipl\I18n\Locale;
+use ipl\I18n\StaticTranslator;
+
+/**
+ * Form class to adjust user preferences
+ */
+class PreferenceForm extends Form
+{
+ /**
+ * The preferences to work with
+ *
+ * @var Preferences
+ */
+ protected $preferences;
+
+ /**
+ * The preference store to use
+ *
+ * @var PreferencesStore
+ */
+ protected $store;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_preferences');
+ $this->setSubmitLabel($this->translate('Save to the Preferences'));
+ }
+
+ /**
+ * Set preferences to work with
+ *
+ * @param Preferences $preferences The preferences to work with
+ *
+ * @return $this
+ */
+ public function setPreferences(Preferences $preferences)
+ {
+ $this->preferences = $preferences;
+ return $this;
+ }
+
+ /**
+ * Set the preference store to use
+ *
+ * @param PreferencesStore $store The preference store to use
+ *
+ * @return $this
+ */
+ public function setStore(PreferencesStore $store)
+ {
+ $this->store = $store;
+ return $this;
+ }
+
+ /**
+ * Persist preferences
+ *
+ * @return $this
+ */
+ public function save()
+ {
+ $this->store->save($this->preferences);
+ return $this;
+ }
+
+ /**
+ * Adjust preferences and persist them
+ *
+ * @see Form::onSuccess()
+ */
+ public function onSuccess()
+ {
+ $currentPreferences = $this->Auth()->getUser()->getPreferences();
+ $oldTheme = $currentPreferences->getValue('icingaweb', 'theme');
+ $oldMode = $currentPreferences->getValue('icingaweb', 'theme_mode');
+ $oldLocale = $currentPreferences->getValue('icingaweb', 'language');
+ $defaultTheme = Config::app()->get('themes', 'default', StyleSheet::DEFAULT_THEME);
+
+ $this->preferences = new Preferences($this->store ? $this->store->load() : array());
+ $webPreferences = $this->preferences->get('icingaweb');
+ foreach ($this->getValues() as $key => $value) {
+ if ($value === ''
+ || $value === null
+ || $value === 'autodetect'
+ || ($key === 'theme' && $value === $defaultTheme)
+ ) {
+ if (isset($webPreferences[$key])) {
+ unset($webPreferences[$key]);
+ }
+ } else {
+ $webPreferences[$key] = $value;
+ }
+ }
+ $this->preferences->icingaweb = $webPreferences;
+ Session::getSession()->user->setPreferences($this->preferences);
+
+ if ((($theme = $this->getElement('theme')) !== null
+ && ($theme = $theme->getValue()) !== $oldTheme
+ && ($theme !== $defaultTheme || $oldTheme !== null))
+ || (($mode = $this->getElement('theme_mode')) !== null
+ && ($mode->getValue()) !== $oldMode)
+ ) {
+ $this->getResponse()->setReloadCss(true);
+ }
+
+ if (($locale = $this->getElement('language')) !== null
+ && $locale->getValue() !== 'autodetect'
+ && $locale->getValue() !== $oldLocale
+ ) {
+ $this->getResponse()->setHeader('X-Icinga-Redirect-Http', 'yes');
+ }
+
+ try {
+ if ($this->store && $this->getElement('btn_submit')->isChecked()) {
+ $this->save();
+ Notification::success($this->translate('Preferences successfully saved'));
+ } else {
+ Notification::success($this->translate('Preferences successfully saved for the current session'));
+ }
+ } catch (Exception $e) {
+ Logger::error($e);
+ Notification::error($e->getMessage());
+ }
+ }
+
+ /**
+ * Populate preferences
+ *
+ * @see Form::onRequest()
+ */
+ public function onRequest()
+ {
+ $auth = Auth::getInstance();
+ $values = $auth->getUser()->getPreferences()->get('icingaweb');
+
+ if (! isset($values['language'])) {
+ $values['language'] = 'autodetect';
+ }
+
+ if (! isset($values['timezone'])) {
+ $values['timezone'] = 'autodetect';
+ }
+
+ if (! isset($values['auto_refresh'])) {
+ $values['auto_refresh'] = '1';
+ }
+
+ $this->populate($values);
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ if (setlocale(LC_ALL, 0) === 'C') {
+ $this->warning(
+ $this->translate(
+ 'Your language setting is not applied because your platform is missing the corresponding locale.'
+ . ' Make sure to install the correct language pack and restart your web server afterwards.'
+ ),
+ false
+ );
+ }
+
+ $themeFile = StyleSheet::getThemeFile(Config::app()->get('themes', 'default'));
+ if (! (bool) Config::app()->get('themes', 'disabled', false)) {
+ $themes = Icinga::app()->getThemes();
+ if (count($themes) > 1) {
+ $defaultTheme = Config::app()->get('themes', 'default', StyleSheet::DEFAULT_THEME);
+ if (isset($themes[$defaultTheme])) {
+ $themes[$defaultTheme] .= ' (' . $this->translate('default') . ')';
+ }
+ $this->addElement(
+ 'select',
+ 'theme',
+ array(
+ 'label' => $this->translate('Theme', 'Form element label'),
+ 'multiOptions' => $themes,
+ 'autosubmit' => true,
+ 'value' => $this->preferences->getValue(
+ 'icingaweb',
+ 'theme',
+ $defaultTheme
+ )
+ )
+ );
+ }
+ }
+
+ if (isset($formData['theme'])) {
+ $themeFile = StyleSheet::getThemeFile($formData['theme']);
+ }
+
+ $disabled = [];
+ if ($themeFile !== null) {
+ $file = @file_get_contents($themeFile);
+ if ($file && strpos($file, StyleSheet::LIGHT_MODE_IDENTIFIER) === false) {
+ $disabled = ['', 'light', 'system'];
+ }
+ }
+
+ $this->addElement(
+ 'radio',
+ 'theme_mode',
+ [
+ 'class' => 'theme-mode-input',
+ 'label' => $this->translate('Theme Mode'),
+ 'multiOptions' => [
+ '' => HtmlElement::create(
+ 'img',
+ ['src' => $this->getView()->href('img/theme-mode-thumbnail-dark.svg')]
+ ) . HtmlElement::create('span', [], $this->translate('Dark')),
+ 'light' => HtmlElement::create(
+ 'img',
+ ['src' => $this->getView()->href('img/theme-mode-thumbnail-light.svg')]
+ ) . HtmlElement::create('span', [], $this->translate('Light')),
+ 'system' => HtmlElement::create(
+ 'img',
+ ['src' => $this->getView()->href('img/theme-mode-thumbnail-system.svg')]
+ ) . HtmlElement::create('span', [], $this->translate('System'))
+ ],
+ 'disable' => $disabled,
+ 'escape' => false,
+ 'decorators' => array_merge(
+ array_slice(self::$defaultElementDecorators, 0, -1),
+ [['HtmlTag', ['tag' => 'div', 'class' => 'control-group theme-mode']]]
+ )
+ ]
+ );
+
+ /** @var GettextTranslator $translator */
+ $translator = StaticTranslator::$instance;
+
+ $languages = array();
+ $availableLocales = $translator->listLocales();
+
+ $locale = $this->getLocale($availableLocales);
+ if ($locale !== null) {
+ $languages['autodetect'] = sprintf($this->translate('Browser (%s)', 'preferences.form'), $locale);
+ }
+
+ $availableLocales[] = $translator->getDefaultLocale();
+ foreach ($availableLocales as $language) {
+ $languages[$language] = $language;
+ }
+
+ $tzList = array();
+ $tzList['autodetect'] = sprintf(
+ $this->translate('Browser (%s)', 'preferences.form'),
+ $this->getDefaultTimezone()
+ );
+ foreach (DateTimeZone::listIdentifiers() as $tz) {
+ $tzList[$tz] = $tz;
+ }
+
+ $this->addElement(
+ 'select',
+ 'language',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Your Current Language'),
+ 'description' => $this->translate('Use the following language to display texts and messages'),
+ 'multiOptions' => $languages,
+ 'value' => substr(setlocale(LC_ALL, 0), 0, 5)
+ )
+ );
+
+ $this->addElement(
+ 'select',
+ 'timezone',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Your Current Timezone'),
+ 'description' => $this->translate('Use the following timezone for dates and times'),
+ 'multiOptions' => $tzList,
+ 'value' => $this->getDefaultTimezone()
+ )
+ );
+
+ $this->addElement(
+ 'select',
+ 'show_application_state_messages',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Show application state messages'),
+ 'description' => $this->translate('Whether to show application state messages.'),
+ 'multiOptions' => [
+ 'system' => (bool) Config::app()->get('global', 'show_application_state_messages', true)
+ ? $this->translate('System (Yes)')
+ : $this->translate('System (No)'),
+ 1 => $this->translate('Yes'),
+ 0 => $this->translate('No')],
+ 'value' => 'system'
+ )
+ );
+
+ if (Auth::getInstance()->hasPermission('user/application/stacktraces')) {
+ $this->addElement(
+ 'checkbox',
+ 'show_stacktraces',
+ array(
+ 'value' => $this->getDefaultShowStacktraces(),
+ 'label' => $this->translate('Show Stacktraces'),
+ 'description' => $this->translate('Set whether to show an exception\'s stacktrace.')
+ )
+ );
+ }
+
+ $this->addElement(
+ 'checkbox',
+ 'show_benchmark',
+ array(
+ 'label' => $this->translate('Use benchmark')
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'auto_refresh',
+ array(
+ 'required' => false,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Enable auto refresh'),
+ 'description' => $this->translate(
+ 'This option allows you to enable or to disable the global page content auto refresh'
+ ),
+ 'value' => 1
+ )
+ );
+
+ if (isset($formData['auto_refresh']) && $formData['auto_refresh']) {
+ $this->addElement(
+ 'select',
+ 'auto_refresh_speed',
+ [
+ 'required' => false,
+ 'label' => $this->translate('Auto refresh speed'),
+ 'description' => $this->translate(
+ 'This option allows you to speed up or to slow down the global page content auto refresh'
+ ),
+ 'multiOptions' => [
+ '0.5' => $this->translate('Fast', 'refresh_speed'),
+ '' => $this->translate('Default', 'refresh_speed'),
+ '2' => $this->translate('Moderate', 'refresh_speed'),
+ '4' => $this->translate('Slow', 'refresh_speed')
+ ],
+ 'value' => ''
+ ]
+ );
+ }
+
+ $this->addElement(
+ 'number',
+ 'default_page_size',
+ array(
+ 'label' => $this->translate('Default page size'),
+ 'description' => $this->translate('Default number of items per page for list views'),
+ 'placeholder' => 25,
+ 'min' => 25,
+ 'step' => 1
+ )
+ );
+
+ if ($this->store) {
+ $this->addElement(
+ 'submit',
+ 'btn_submit',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Save to the Preferences'),
+ 'decorators' => array('ViewHelper'),
+ 'class' => 'btn-primary'
+ )
+ );
+ }
+
+ $this->addElement(
+ 'submit',
+ 'btn_submit_session',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Save for the current Session'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+
+ $this->setAttrib('data-progress-element', 'preferences-progress');
+ $this->addElement(
+ 'note',
+ 'preferences-progress',
+ array(
+ 'decorators' => array(
+ 'ViewHelper',
+ array('Spinner', array('id' => 'preferences-progress'))
+ )
+ )
+ );
+
+ $this->addDisplayGroup(
+ array('btn_submit', 'btn_submit_session', 'preferences-progress'),
+ 'submit_buttons',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+ }
+
+ public function addSubmitButton()
+ {
+ return $this;
+ }
+
+ public function isSubmitted()
+ {
+ if (parent::isSubmitted()) {
+ return true;
+ }
+
+ return $this->getElement('btn_submit_session')->isChecked();
+ }
+
+ /**
+ * Return the current default timezone
+ *
+ * @return string
+ */
+ protected function getDefaultTimezone()
+ {
+ $detect = new TimezoneDetect();
+ if ($detect->success()) {
+ return $detect->getTimezoneName();
+ } else {
+ return @date_default_timezone_get();
+ }
+ }
+
+ /**
+ * Return the preferred locale based on the given HTTP header and the available translations
+ *
+ * @return string|null
+ */
+ protected function getLocale($availableLocales)
+ {
+ return isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])
+ ? (new Locale())->getPreferred($_SERVER['HTTP_ACCEPT_LANGUAGE'], $availableLocales)
+ : null;
+ }
+
+ /**
+ * Return the default global setting for show_stacktraces
+ *
+ * @return bool
+ */
+ protected function getDefaultShowStacktraces()
+ {
+ return Config::app()->get('global', 'show_stacktraces', true);
+ }
+}
diff --git a/application/forms/RepositoryForm.php b/application/forms/RepositoryForm.php
new file mode 100644
index 0000000..8e4665d
--- /dev/null
+++ b/application/forms/RepositoryForm.php
@@ -0,0 +1,453 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms;
+
+use Exception;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\NotFoundError;
+use Icinga\Repository\Repository;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form base-class providing standard functionality for extensible, updatable and reducible repositories
+ */
+abstract class RepositoryForm extends Form
+{
+ /**
+ * Insert mode
+ */
+ const MODE_INSERT = 0;
+
+ /**
+ * Update mode
+ */
+ const MODE_UPDATE = 1;
+
+ /**
+ * Delete mode
+ */
+ const MODE_DELETE = 2;
+
+ /**
+ * The repository being worked with
+ *
+ * @var Repository
+ */
+ protected $repository;
+
+ /**
+ * The target being worked with
+ *
+ * @var mixed
+ */
+ protected $baseTable;
+
+ /**
+ * How to interact with the repository
+ *
+ * @var int
+ */
+ protected $mode;
+
+ /**
+ * The name of the entry being handled when in mode update or delete
+ *
+ * @var string
+ */
+ protected $identifier;
+
+ /**
+ * The data of the entry to pre-populate the form with when in mode insert or update
+ *
+ * @var array
+ */
+ protected $data;
+
+ /**
+ * Set the repository to work with
+ *
+ * @param Repository $repository
+ *
+ * @return $this
+ */
+ public function setRepository(Repository $repository)
+ {
+ $this->repository = $repository;
+ return $this;
+ }
+
+ /**
+ * Return the target being worked with
+ *
+ * @return mixed
+ */
+ protected function getBaseTable()
+ {
+ if ($this->baseTable === null) {
+ return $this->repository->getBaseTable();
+ }
+
+ return $this->baseTable;
+ }
+
+ /**
+ * Return the name of the entry to handle
+ *
+ * @return string
+ */
+ protected function getIdentifier()
+ {
+ return $this->identifier;
+ }
+
+ /**
+ * Return the current data of the entry being handled
+ *
+ * @return array
+ */
+ protected function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * Return whether an entry should be inserted
+ *
+ * @return bool
+ */
+ public function shouldInsert()
+ {
+ return $this->mode === self::MODE_INSERT;
+ }
+
+ /**
+ * Return whether an entry should be udpated
+ *
+ * @return bool
+ */
+ public function shouldUpdate()
+ {
+ return $this->mode === self::MODE_UPDATE;
+ }
+
+ /**
+ * Return whether an entry should be deleted
+ *
+ * @return bool
+ */
+ public function shouldDelete()
+ {
+ return $this->mode === self::MODE_DELETE;
+ }
+
+ /**
+ * Add a new entry
+ *
+ * @param array $data The defaults to use, if any
+ *
+ * @return $this
+ */
+ public function add(array $data = null)
+ {
+ $this->mode = static::MODE_INSERT;
+ $this->data = $data;
+ return $this;
+ }
+
+ /**
+ * Edit an entry
+ *
+ * @param string $name The entry's name
+ * @param array $data The entry's current data
+ *
+ * @return $this
+ */
+ public function edit($name, array $data = null)
+ {
+ $this->mode = static::MODE_UPDATE;
+ $this->identifier = $name;
+ $this->data = $data;
+ return $this;
+ }
+
+ /**
+ * Remove an entry
+ *
+ * @param string $name The entry's name
+ *
+ * @return $this
+ */
+ public function remove($name)
+ {
+ $this->mode = static::MODE_DELETE;
+ $this->identifier = $name;
+ return $this;
+ }
+
+ /**
+ * Fetch and return the entry to pre-populate the form with when in mode update
+ *
+ * @return false|object
+ */
+ protected function fetchEntry()
+ {
+ return $this->repository
+ ->select()
+ ->from($this->getBaseTable())
+ ->applyFilter($this->createFilter())
+ ->fetchRow();
+ }
+
+ /**
+ * Return whether the entry supposed to be removed exists
+ *
+ * @return bool
+ */
+ protected function entryExists()
+ {
+ $count = $this->repository
+ ->select()
+ ->from($this->getBaseTable())
+ ->addFilter($this->createFilter())
+ ->count();
+ return $count > 0;
+ }
+
+ /**
+ * Insert the new entry
+ */
+ protected function insertEntry()
+ {
+ $this->repository->insert($this->getBaseTable(), $this->getValues());
+ }
+
+ /**
+ * Update the entry
+ */
+ protected function updateEntry()
+ {
+ $this->repository->update($this->getBaseTable(), $this->getValues(), $this->createFilter());
+ }
+
+ /**
+ * Delete the entry
+ */
+ protected function deleteEntry()
+ {
+ $this->repository->delete($this->getBaseTable(), $this->createFilter());
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ if ($this->shouldInsert()) {
+ $this->createInsertElements($formData);
+ } elseif ($this->shouldUpdate()) {
+ $this->createUpdateElements($formData);
+ } elseif ($this->shouldDelete()) {
+ $this->createDeleteElements($formData);
+ }
+ }
+
+ /**
+ * Prepare the form for the requested mode
+ */
+ public function onRequest()
+ {
+ if ($this->shouldInsert()) {
+ $this->onInsertRequest();
+ } elseif ($this->shouldUpdate()) {
+ $this->onUpdateRequest();
+ } elseif ($this->shouldDelete()) {
+ $this->onDeleteRequest();
+ }
+ }
+
+ /**
+ * Prepare the form for mode insert
+ *
+ * Populates the form with the data passed to add().
+ */
+ protected function onInsertRequest()
+ {
+ $data = $this->getData();
+ if (! empty($data)) {
+ $this->setDefaults($data);
+ }
+ }
+
+ /**
+ * Prepare the form for mode update
+ *
+ * Populates the form with either the data passed to edit() or tries to fetch it from the repository.
+ *
+ * @throws NotFoundError In case the entry to update cannot be found
+ */
+ protected function onUpdateRequest()
+ {
+ $data = $this->getData();
+ if ($data === null) {
+ $row = $this->fetchEntry();
+ if ($row === false) {
+ throw new NotFoundError('Entry "%s" not found', $this->getIdentifier());
+ }
+
+ $data = get_object_vars($row);
+ }
+
+ $this->setDefaults($data);
+ }
+
+ /**
+ * Prepare the form for mode delete
+ *
+ * Verifies that the repository contains the entry to delete.
+ *
+ * @throws NotFoundError In case the entry to delete cannot be found
+ */
+ protected function onDeleteRequest()
+ {
+ if (! $this->entryExists()) {
+ throw new NotFoundError('Entry "%s" not found', $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Apply the requested mode on the repository
+ *
+ * @return ?bool
+ */
+ public function onSuccess()
+ {
+ if ($this->shouldInsert()) {
+ return $this->onInsertSuccess();
+ } elseif ($this->shouldUpdate()) {
+ return $this->onUpdateSuccess();
+ } elseif ($this->shouldDelete()) {
+ return $this->onDeleteSuccess();
+ }
+ }
+
+ /**
+ * Apply mode insert on the repository
+ *
+ * @return bool
+ */
+ protected function onInsertSuccess()
+ {
+ try {
+ $this->insertEntry();
+ } catch (Exception $e) {
+ Notification::error($this->getInsertMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getInsertMessage(true));
+ return true;
+ }
+
+ /**
+ * Apply mode update on the repository
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ try {
+ $this->updateEntry();
+ } catch (Exception $e) {
+ Notification::error($this->getUpdateMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getUpdateMessage(true));
+ return true;
+ }
+
+ /**
+ * Apply mode delete on the repository
+ *
+ * @return bool
+ */
+ protected function onDeleteSuccess()
+ {
+ try {
+ $this->deleteEntry();
+ } catch (Exception $e) {
+ Notification::error($this->getDeleteMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getDeleteMessage(true));
+ return true;
+ }
+
+ /**
+ * Create and add elements to this form to insert an entry
+ *
+ * @param array $formData The data sent by the user
+ */
+ abstract protected function createInsertElements(array $formData);
+
+ /**
+ * Create and add elements to this form to update an entry
+ *
+ * Calls createInsertElements() by default. Overwrite this to add different elements when in mode update.
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+ }
+
+ /**
+ * Create and add elements to this form to delete an entry
+ *
+ * @param array $formData The data sent by the user
+ */
+ abstract protected function createDeleteElements(array $formData);
+
+ /**
+ * Create and return a filter to use when selecting, updating or deleting an entry
+ *
+ * @return Filter
+ */
+ abstract protected function createFilter();
+
+ /**
+ * Return a notification message to use when inserting an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getInsertMessage($success);
+
+ /**
+ * Return a notification message to use when updating an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getUpdateMessage($success);
+
+ /**
+ * Return a notification message to use when deleting an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getDeleteMessage($success);
+}
diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php
new file mode 100644
index 0000000..58387f7
--- /dev/null
+++ b/application/forms/Security/RoleForm.php
@@ -0,0 +1,632 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Security;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Manager;
+use Icinga\Authentication\AdmissionLoader;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\ConfigForm;
+use Icinga\Forms\RepositoryForm;
+use Icinga\Util\StringHelper;
+use Icinga\Web\Notification;
+use ipl\Web\Widget\Icon;
+
+/**
+ * Form for managing roles
+ */
+class RoleForm extends RepositoryForm
+{
+ /**
+ * The name to use instead of `*`
+ */
+ const WILDCARD_NAME = 'allAndEverything';
+
+ /**
+ * The prefix used to deny a permission
+ */
+ const DENY_PREFIX = 'no-';
+
+ /**
+ * Provided permissions by currently installed modules
+ *
+ * @var array
+ */
+ protected $providedPermissions;
+
+ /**
+ * Provided restrictions by currently installed modules
+ *
+ * @var array
+ */
+ protected $providedRestrictions;
+
+ public function init()
+ {
+ $this->setAttrib('class', self::DEFAULT_CLASSES . ' role-form');
+
+ list($this->providedPermissions, $this->providedRestrictions) = static::collectProvidedPrivileges();
+ }
+
+ protected function createFilter()
+ {
+ return Filter::where('name', $this->getIdentifier());
+ }
+
+ public function filterName($value, $allowBrackets = false)
+ {
+ return parent::filterName($value, $allowBrackets) . '_element';
+ }
+
+ public function createInsertElements(array $formData = array())
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ [
+ 'required' => true,
+ 'label' => $this->translate('Role Name'),
+ 'description' => $this->translate('The name of the role')
+ ]
+ );
+ $this->addElement(
+ 'select',
+ 'parent',
+ [
+ 'label' => $this->translate('Inherit From'),
+ 'description' => $this->translate('Choose a role from which to inherit privileges'),
+ 'value' => '',
+ 'multiOptions' => array_merge(
+ ['' => $this->translate('None', 'parent role')],
+ $this->collectRoles()
+ )
+ ]
+ );
+ $this->addElement(
+ 'textarea',
+ 'users',
+ [
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate('Comma-separated list of users that are assigned to the role')
+ ]
+ );
+ $this->addElement(
+ 'textarea',
+ 'groups',
+ [
+ 'label' => $this->translate('Groups'),
+ 'description' => $this->translate('Comma-separated list of groups that are assigned to the role')
+ ]
+ );
+ $this->addElement(
+ 'checkbox',
+ self::WILDCARD_NAME,
+ [
+ 'autosubmit' => true,
+ 'label' => $this->translate('Administrative Access'),
+ 'description' => $this->translate('Everything is allowed')
+ ]
+ );
+ $this->addElement(
+ 'checkbox',
+ 'unrestricted',
+ [
+ 'autosubmit' => true,
+ 'uncheckedValue' => null,
+ 'label' => $this->translate('Unrestricted Access'),
+ 'description' => $this->translate('Access to any data is completely unrestricted')
+ ]
+ );
+
+ $hasAdminPerm = isset($formData[self::WILDCARD_NAME]) && $formData[self::WILDCARD_NAME];
+ $isUnrestricted = isset($formData['unrestricted']) && $formData['unrestricted'];
+ foreach ($this->providedPermissions as $moduleName => $permissionList) {
+ $this->sortPermissions($permissionList);
+
+ $anythingGranted = false;
+ $anythingRefused = false;
+ $anythingRestricted = false;
+
+ $elements = [$moduleName . '_header'];
+ // The actual element is added last
+
+ $elements[] = 'permission_header';
+ $this->addElement('note', 'permission_header', [
+ 'decorators' => [['Callback', ['callback' => function () {
+ return '<h4>' . $this->translate('Permissions') . '</h4>'
+ . $this->getView()->icon('ok', $this->translate(
+ 'Grant access by toggling a switch below'
+ ))
+ . $this->getView()->icon('cancel', $this->translate(
+ 'Deny access by toggling a switch below'
+ ));
+ }]], ['HtmlTag', ['tag' => 'div']]]
+ ]);
+
+ $hasFullPerm = false;
+ foreach ($permissionList as $name => $spec) {
+ $elementName = $this->filterName($name);
+
+ if (isset($formData[$elementName]) && $formData[$elementName]) {
+ $anythingGranted = true;
+ }
+
+ if ($hasFullPerm || $hasAdminPerm) {
+ $elementName .= '_fake';
+ }
+
+ $denyCheckbox = null;
+ if (! isset($spec['isFullPerm'])
+ && substr($name, 0, strlen(self::DENY_PREFIX)) !== self::DENY_PREFIX
+ ) {
+ $denyCheckbox = $this->createElement('checkbox', $this->filterName(self::DENY_PREFIX . $name), [
+ 'decorators' => ['ViewHelper']
+ ]);
+ $this->addElement($denyCheckbox);
+ $this->removeFromIteration($denyCheckbox->getName());
+
+ if (isset($formData[$denyCheckbox->getName()]) && $formData[$denyCheckbox->getName()]) {
+ $anythingRefused = true;
+ }
+ }
+
+ $elements[] = $elementName;
+ $this->addElement(
+ 'checkbox',
+ $elementName,
+ [
+ 'ignore' => $hasFullPerm || $hasAdminPerm,
+ 'autosubmit' => isset($spec['isFullPerm']),
+ 'disabled' => $hasFullPerm || $hasAdminPerm ?: null,
+ 'value' => $hasFullPerm || $hasAdminPerm,
+ 'label' => isset($spec['label'])
+ ? $spec['label']
+ : join('', iterator_to_array(call_user_func(function ($segments) {
+ foreach ($segments as $segment) {
+ if ($segment[0] === '/') {
+ // Adds a zero-width char after each slash to help browsers break onto newlines
+ yield '/&#8203;';
+ yield '<span class="no-wrap">' . substr($segment, 1) . '</span>';
+ } else {
+ yield '<em>' . $segment . '</em>';
+ }
+ }
+ }, preg_split(
+ '~(/[^/]+)~',
+ $name,
+ -1,
+ PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
+ )))),
+ 'description' => isset($spec['description']) ? $spec['description'] : $name,
+ 'decorators' => array_merge(
+ array_slice(self::$defaultElementDecorators, 0, 3),
+ [['Callback', ['callback' => function () use ($denyCheckbox) {
+ return $denyCheckbox ? $denyCheckbox->render() : '';
+ }]]],
+ array_slice(self::$defaultElementDecorators, 3)
+ )
+ ]
+ )
+ ->getElement($elementName)
+ ->getDecorator('Label')
+ ->setOption('escape', false);
+
+ if ($hasFullPerm || $hasAdminPerm) {
+ // Add a hidden element to preserve the configured permission value
+ $this->addElement('hidden', $this->filterName($name));
+ }
+
+ if (isset($spec['isFullPerm'])) {
+ $filteredName = $this->filterName($name);
+ $hasFullPerm = isset($formData[$filteredName]) && $formData[$filteredName];
+ }
+ }
+
+ if (isset($this->providedRestrictions[$moduleName])) {
+ $elements[] = 'restriction_header';
+ $this->addElement('note', 'restriction_header', [
+ 'value' => '<h4>' . $this->translate('Restrictions') . '</h4>',
+ 'decorators' => ['ViewHelper']
+ ]);
+
+ foreach ($this->providedRestrictions[$moduleName] as $name => $spec) {
+ $elementName = $this->filterName($name);
+
+ if (isset($formData[$elementName]) && $formData[$elementName]) {
+ $anythingRestricted = true;
+ }
+
+ $elements[] = $elementName;
+ $this->addElement(
+ 'text',
+ $elementName,
+ [
+ 'label' => isset($spec['label'])
+ ? $spec['label']
+ : join('', iterator_to_array(call_user_func(function ($segments) {
+ foreach ($segments as $segment) {
+ if ($segment[0] === '/') {
+ // Add zero-width char after each slash to help browsers break onto newlines
+ yield '/&#8203;';
+ yield '<span class="no-wrap">' . substr($segment, 1) . '</span>';
+ } else {
+ yield '<em>' . $segment . '</em>';
+ }
+ }
+ }, preg_split(
+ '~(/[^/]+)~',
+ $name,
+ -1,
+ PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
+ )))),
+ 'description' => $spec['description'],
+ 'class' => $isUnrestricted ? 'unrestricted-role' : '',
+ 'readonly' => $isUnrestricted ?: null
+ ]
+ )
+ ->getElement($elementName)
+ ->getDecorator('Label')
+ ->setOption('escape', false);
+ }
+ }
+
+ $this->addElement(
+ 'note',
+ $moduleName . '_header',
+ [
+ 'decorators' => ['ViewHelper'],
+ 'value' => '<summary class="collapsible-control">'
+ . '<span>' . ($moduleName !== 'application'
+ ? sprintf('%s <em>%s</em>', $moduleName, $this->translate('Module'))
+ : 'Icinga Web 2'
+ ) . '</span>'
+ . '<span class="privilege-preview">'
+ . ($hasAdminPerm || $anythingGranted ? new Icon('check-circle', ['class' => 'granted']) : '')
+ . ($anythingRefused ? new Icon('times-circle', ['class' => 'refused']) : '')
+ . (! $isUnrestricted && $anythingRestricted
+ ? new Icon('filter', ['class' => 'restricted'])
+ : ''
+ )
+ . '</span>'
+ . new Icon('angles-down', ['class' => 'collapse-icon'])
+ . new Icon('angles-left', ['class' => 'expand-icon'])
+ . '</summary>'
+ ]
+ );
+
+ $this->addDisplayGroup($elements, $moduleName . '_elements', [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', [
+ 'tag' => 'details',
+ 'class' => 'collapsible'
+ ]],
+ ['Fieldset']
+ ]
+ ]);
+ }
+ }
+
+ protected function createDeleteElements(array $formData)
+ {
+ }
+
+ public function fetchEntry()
+ {
+ $role = parent::fetchEntry();
+ if ($role === false) {
+ return false;
+ }
+
+ $values = [
+ 'parent' => $role->parent,
+ 'name' => $role->name,
+ 'users' => $role->users,
+ 'groups' => $role->groups,
+ 'unrestricted' => $role->unrestricted,
+ self::WILDCARD_NAME => $role->permissions && preg_match('~(?>^|,)\*(?>$|,)~', $role->permissions)
+ ];
+
+ if (! empty($role->permissions) || ! empty($role->refusals)) {
+ $permissions = StringHelper::trimSplit($role->permissions);
+ $refusals = StringHelper::trimSplit($role->refusals);
+
+ list($permissions, $newRefusals) = AdmissionLoader::migrateLegacyPermissions($permissions);
+ if (! empty($newRefusals)) {
+ array_push($refusals, ...$newRefusals);
+ }
+
+ foreach ($this->providedPermissions as $moduleName => $permissionList) {
+ $hasFullPerm = false;
+ foreach ($permissionList as $name => $spec) {
+ if (in_array($name, $permissions, true)) {
+ $values[$this->filterName($name)] = 1;
+
+ if (isset($spec['isFullPerm'])) {
+ $hasFullPerm = true;
+ }
+ }
+
+ if (in_array($name, $refusals, true)) {
+ $values[$this->filterName(self::DENY_PREFIX . $name)] = 1;
+ }
+ }
+
+ if ($hasFullPerm) {
+ unset($values[$this->filterName(Manager::MODULE_PERMISSION_NS . $moduleName)]);
+ }
+ }
+ }
+
+ foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
+ foreach ($restrictionList as $name => $spec) {
+ if (isset($role->$name)) {
+ $values[$this->filterName($name)] = $role->$name;
+ }
+ }
+ }
+
+ return (object) $values;
+ }
+
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+
+ foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
+ foreach ($restrictionList as $name => $spec) {
+ $elementName = $this->filterName($name);
+ if (isset($values[$elementName])) {
+ $values[$name] = $values[$elementName];
+ unset($values[$elementName]);
+ }
+ }
+ }
+
+ $permissions = [];
+ if (isset($values[self::WILDCARD_NAME]) && $values[self::WILDCARD_NAME]) {
+ $permissions[] = '*';
+ }
+
+ $refusals = [];
+ foreach ($this->providedPermissions as $moduleName => $permissionList) {
+ $hasFullPerm = false;
+ foreach ($permissionList as $name => $spec) {
+ $elementName = $this->filterName($name);
+ if (isset($values[$elementName]) && $values[$elementName]) {
+ $permissions[] = $name;
+
+ if (isset($spec['isFullPerm'])) {
+ $hasFullPerm = true;
+ }
+ }
+
+ $denyName = $this->filterName(self::DENY_PREFIX . $name);
+ if (isset($values[$denyName]) && $values[$denyName]) {
+ $refusals[] = $name;
+ }
+
+ unset($values[$elementName], $values[$denyName]);
+ }
+
+ $modulePermission = Manager::MODULE_PERMISSION_NS . $moduleName;
+ if ($hasFullPerm && ! in_array($modulePermission, $permissions, true)) {
+ $permissions[] = $modulePermission;
+ }
+ }
+
+ unset($values[self::WILDCARD_NAME]);
+ $values['refusals'] = join(',', $refusals);
+ $values['permissions'] = join(',', $permissions);
+ return ConfigForm::transformEmptyValuesToNull($values);
+ }
+
+ protected function getInsertMessage($success)
+ {
+ return $success ? $this->translate('Role created') : $this->translate('Role creation failed');
+ }
+
+ protected function getUpdateMessage($success)
+ {
+ return $success ? $this->translate('Role updated') : $this->translate('Role update failed');
+ }
+
+ protected function getDeleteMessage($success)
+ {
+ return $success ? $this->translate('Role removed') : $this->translate('Role removal failed');
+ }
+
+ protected function sortPermissions(&$permissions)
+ {
+ return uksort($permissions, function ($a, $b) use ($permissions) {
+ if (isset($permissions[$a]['isUsagePerm'])) {
+ return isset($permissions[$b]['isFullPerm']) ? 1 : -1;
+ } elseif (isset($permissions[$b]['isUsagePerm'])) {
+ return isset($permissions[$a]['isFullPerm']) ? -1 : 1;
+ }
+
+ $aParts = explode('/', $a);
+ $bParts = explode('/', $b);
+
+ do {
+ $a = array_shift($aParts);
+ $b = array_shift($bParts);
+ } while ($a === $b);
+
+ return strnatcmp($a ?? '', $b ?? '');
+ });
+ }
+
+ protected function collectRoles()
+ {
+ // Function to get all connected children. Used to avoid reference loops
+ $getChildren = function ($name, $children = []) use (&$getChildren) {
+ foreach ($this->repository->select()->where('parent', $name) as $child) {
+ if (isset($children[$child->name])) {
+ // Don't follow already established loops here,
+ // the user should be able to solve such in the UI
+ continue;
+ }
+
+ $children[$child->name] = true;
+ $children = $getChildren($child->name, $children);
+ }
+
+ return $children;
+ };
+
+ $children = $this->getIdentifier() !== null ? $getChildren($this->getIdentifier()) : [];
+
+ $names = [];
+ foreach ($this->repository->select() as $role) {
+ if ($role->name !== $this->getIdentifier() && ! isset($children[$role->name])) {
+ $names[] = $role->name;
+ }
+ }
+
+ return array_combine($names, $names);
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ if (parent::onSuccess() === false) {
+ return false;
+ }
+
+ if ($this->getIdentifier() && ($newName = $this->getValue('name')) !== $this->getIdentifier()) {
+ $this->repository->update(
+ $this->getBaseTable(),
+ ['parent' => $newName],
+ Filter::where('parent', $this->getIdentifier())
+ );
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+
+ /**
+ * Collect permissions and restrictions provided by Icinga Web 2 and modules
+ *
+ * @return array[$permissions, $restrictions]
+ */
+ public static function collectProvidedPrivileges()
+ {
+ $providedPermissions['application'] = [
+ 'application/announcements' => [
+ 'description' => t('Allow to manage announcements')
+ ],
+ 'application/log' => [
+ 'description' => t('Allow to view the application log')
+ ],
+ 'config/*' => [
+ 'description' => t('Allow full config access')
+ ],
+ 'config/general' => [
+ 'description' => t('Allow to adjust the general configuration')
+ ],
+ 'config/modules' => [
+ 'description' => t('Allow to enable/disable and configure modules')
+ ],
+ 'config/resources' => [
+ 'description' => t('Allow to manage resources')
+ ],
+ 'config/navigation' => [
+ 'description' => t('Allow to view and adjust shared navigation items')
+ ],
+ 'config/access-control/*' => [
+ 'description' => t('Allow to fully manage access-control')
+ ],
+ 'config/access-control/users' => [
+ 'description' => t('Allow to manage user accounts')
+ ],
+ 'config/access-control/groups' => [
+ 'description' => t('Allow to manage user groups')
+ ],
+ 'config/access-control/roles' => [
+ 'description' => t('Allow to manage roles')
+ ],
+ 'user/*' => [
+ 'description' => t('Allow all account related functionalities')
+ ],
+ 'user/password-change' => [
+ 'description' => t('Allow password changes in the account preferences')
+ ],
+ 'user/application/stacktraces' => [
+ 'description' => t('Allow to adjust in the preferences whether to show stacktraces')
+ ],
+ 'user/share/navigation' => [
+ 'description' => t('Allow to share navigation items')
+ ],
+ 'application/sessions' => [
+ 'description' => t('Allow to manage user sessions')
+ ],
+ 'application/migrations' => [
+ 'description' => t('Allow to apply pending application migrations')
+ ]
+ ];
+
+ $providedRestrictions['application'] = [
+ 'application/share/users' => [
+ 'description' => t('Restrict which users this role can share items and information with')
+ ],
+ 'application/share/groups' => [
+ 'description' => t('Restrict which groups this role can share items and information with')
+ ]
+ ];
+
+ $mm = Icinga::app()->getModuleManager();
+ foreach ($mm->listInstalledModules() as $moduleName) {
+ $modulePermission = Manager::MODULE_PERMISSION_NS . $moduleName;
+ $providedPermissions[$moduleName][$modulePermission] = [
+ 'isUsagePerm' => true,
+ 'label' => t('General Module Access'),
+ 'description' => sprintf(t('Allow access to module %s'), $moduleName)
+ ];
+
+ $module = $mm->getModule($moduleName, false);
+ $permissions = $module->getProvidedPermissions();
+
+ $providedPermissions[$moduleName][$moduleName . '/*'] = [
+ 'isFullPerm' => true,
+ 'label' => t('Full Module Access')
+ ];
+
+ foreach ($permissions as $permission) {
+ /** @var object $permission */
+ $providedPermissions[$moduleName][$permission->name] = [
+ 'description' => $permission->description
+ ];
+ }
+
+ foreach ($module->getProvidedRestrictions() as $restriction) {
+ $providedRestrictions[$moduleName][$restriction->name] = [
+ 'description' => $restriction->description
+ ];
+ }
+ }
+
+ return [$providedPermissions, $providedRestrictions];
+ }
+}
diff --git a/application/layouts/scripts/body.phtml b/application/layouts/scripts/body.phtml
new file mode 100644
index 0000000..87b570b
--- /dev/null
+++ b/application/layouts/scripts/body.phtml
@@ -0,0 +1,98 @@
+<?php
+
+use Icinga\Web\Url;
+use Icinga\Web\Notification;
+use Icinga\Authentication\Auth;
+use ipl\Html\HtmlString;
+use ipl\Web\Widget\Icon;
+
+$moduleName = $this->layout()->moduleName;
+if ($moduleName !== 'default') {
+ $moduleClass = ' icinga-module module-' . $moduleName;
+} else {
+ $moduleClass = '';
+}
+
+$refresh = '';
+if ($this->layout()->autorefreshInterval) {
+ $refresh = ' data-icinga-refresh="' . $this->layout()->autorefreshInterval . '"';
+}
+
+if ($this->layout()->inlineLayout) {
+ $inlineLayoutScript = $this->layout()->inlineLayout . '.phtml';
+} else {
+ $inlineLayoutScript = 'inline.phtml';
+}
+
+?>
+<div id="header">
+ <div id="announcements" class="container">
+ <?= $this->widget('announcements') ?>
+ </div>
+</div>
+<div id="content-wrapper">
+<?php if (! $this->layout()->isIframe): ?>
+ <div id="sidebar">
+ <div id="header-logo-container">
+ <?= $this->qlink(
+ '',
+ Auth::getInstance()->isAuthenticated() ? 'dashboard' : '',
+ null,
+ array(
+ 'aria-hidden' => 'true',
+ 'data-base-target' => '_main',
+ 'id' => 'header-logo'
+ )
+ ); ?>
+ <div id="mobile-menu-toggle">
+ <button type="button"><?= $this->icon('menu') ?><?= $this->icon('cancel') ?></button>
+ </div>
+ </div>
+ <?= $this->render('parts/navigation.phtml'); ?>
+ </div>
+<?php endif ?>
+ <div id="main" role="main">
+ <div id="col1"
+ class="container<?= $moduleClass ?>"
+ <?php if ($moduleName): ?>
+ data-icinga-module="<?= $moduleName ?>"
+ <?php endif ?>
+ data-icinga-url="<?= $this->escape(Url::fromRequest()->without('renderLayout')->getAbsoluteUrl()); ?>"
+ <?= $refresh; ?>
+ >
+ <?= $this->render($inlineLayoutScript) ?>
+ </div>
+ <div id="col2" class="container"></div>
+ <div id="col3" class="container"></div>
+ </div>
+</div>
+<div id="footer">
+ <ul role="alert" id="notifications"><?php
+
+ $notifications = Notification::getInstance();
+ if ($notifications->hasMessages()) {
+ foreach ($notifications->popMessages() as $m) {
+ switch ($m->type) {
+ case 'success':
+ $icon = new HtmlString(new Icon('check-circle'));
+ break;
+ case 'error':
+ $icon = new HtmlString(new Icon('times'));
+ break;
+ case 'warning':
+ $icon = new HtmlString(new Icon('exclamation-triangle'));
+ break;
+ case 'info':
+ $icon = new HtmlString(new Icon('info-circle'));
+ break;
+ default:
+ $icon = '';
+ break;
+ }
+
+ echo '<li class="' . $m->type . '">' . $icon . $this->escape($m->message) . '</li>';
+ }
+ }
+ ?></ul>
+ <div id="application-state-summary" class="container" data-icinga-url="<?= $this->url('application-state/summary') ?>" data-last-update="-1" data-icinga-refresh="60"></div>
+</div>
diff --git a/application/layouts/scripts/external-logout.phtml b/application/layouts/scripts/external-logout.phtml
new file mode 100644
index 0000000..19b7e32
--- /dev/null
+++ b/application/layouts/scripts/external-logout.phtml
@@ -0,0 +1,34 @@
+<?php
+
+use ipl\I18n\Locale;
+use ipl\I18n\GettextTranslator;
+use ipl\I18n\StaticTranslator;
+
+/** @var GettextTranslator $translator */
+$translator = StaticTranslator::$instance;
+
+$lang = (new Locale())->parseLocale($translator->getLocale())->language;
+$showFullscreen = $this->layout()->showFullscreen;
+$innerLayoutScript = $this->layout()->innerLayout . '.phtml';
+
+?><!DOCTYPE html>
+<html class="no-js" lang="<?= $lang ?>">
+<head>
+ <meta charset="utf-8">
+ <meta name="google" value="notranslate">
+ <meta http-equiv="cleartype" content="on">
+ <title><?= $this->title ? $this->escape($this->title) : $this->defaultTitle ?></title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="application-name" content="Icinga Web 2">
+ <meta name="apple-mobile-web-app-title" content="Icinga">
+ <link rel="mask-icon" href="<?= $this->baseUrl('img/website-icon.svg') ?>" color="#0096BF">
+ <link type="image/png" rel="shortcut icon" href="<?= $this->baseUrl('img/favicon.png') ?>" />
+ <link rel="apple-touch-icon" href="<?= $this->baseUrl('img/touch-icon.png') ?>">
+</head>
+<body id="body">
+<div id="layout" class="default-layout<?php if ($showFullscreen): ?> fullscreen-layout<?php endif ?>">
+ <?= $this->render($innerLayoutScript); ?>
+</div>
+</body>
+</html>
diff --git a/application/layouts/scripts/guest-error.phtml b/application/layouts/scripts/guest-error.phtml
new file mode 100644
index 0000000..49cdd68
--- /dev/null
+++ b/application/layouts/scripts/guest-error.phtml
@@ -0,0 +1,10 @@
+<div id="guest-error">
+ <div class="centered-ghost">
+ <div class="centered-content">
+ <div id="icinga-logo" aria-hidden="true"></div>
+ <div id="guest-error-message">
+ <?= $this->render('inline.phtml') ?>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/application/layouts/scripts/inline.phtml b/application/layouts/scripts/inline.phtml
new file mode 100644
index 0000000..47d5672
--- /dev/null
+++ b/application/layouts/scripts/inline.phtml
@@ -0,0 +1,2 @@
+<?= $this->layout()->content ?>
+<?= $this->layout()->benchmark ?>
diff --git a/application/layouts/scripts/layout.phtml b/application/layouts/scripts/layout.phtml
new file mode 100644
index 0000000..33ede0b
--- /dev/null
+++ b/application/layouts/scripts/layout.phtml
@@ -0,0 +1,83 @@
+<?php
+
+use ipl\I18n\Locale;
+use ipl\I18n\GettextTranslator;
+use ipl\I18n\StaticTranslator;
+use ipl\Web\Widget\Icon;
+
+if (array_key_exists('_dev', $_GET)) {
+ $jsfile = 'js/icinga.dev.js';
+ $cssfile = 'css/icinga.css';
+} else {
+ $jsfile = 'js/icinga.min.js';
+ $cssfile = 'css/icinga.min.css';
+}
+
+/** @var GettextTranslator $translator */
+$translator = StaticTranslator::$instance;
+
+$lang = (new Locale())->parseLocale($translator->getLocale())->language;
+$timezone = date_default_timezone_get();
+$isIframe = $this->layout()->isIframe;
+$showFullscreen = $this->layout()->showFullscreen;
+$iframeClass = $isIframe ? ' iframe' : '';
+$innerLayoutScript = $this->layout()->innerLayout . '.phtml';
+
+?><!DOCTYPE html>
+<html
+ class="no-js<?= $iframeClass ?>" lang="<?= $lang ?>"
+ data-icinga-window-name="<?= $this->protectId('Icinga') ?>"
+ data-icinga-timezone="<?= $timezone ?>"
+ data-icinga-base-url="<?= $this->baseUrl(); ?>"
+ <?php if ($isIframe): ?>
+ data-icinga-is-iframe
+ <?php endif ?>
+>
+<head>
+ <meta charset="utf-8">
+ <meta name="google" value="notranslate">
+ <meta http-equiv="cleartype" content="on">
+ <title><?= $this->title ? $this->escape($this->title) . ' :: ' : '' ?><?= $this->defaultTitle ?></title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="application-name" content="Icinga Web 2">
+ <meta name="apple-mobile-web-app-title" content="Icinga">
+ <link rel="mask-icon" href="<?= $this->baseUrl('img/website-icon.svg') ?>" color="#0096BF">
+<?php if ($isIframe): ?>
+ <base target="_parent">
+<?php else: ?>
+ <base href="<?= $this->baseUrl(); ?>/">
+<?php endif ?>
+ <link rel="stylesheet" href="<?= $this->href($cssfile) ?>" media="all" type="text/css" />
+ <link type="image/png" rel="shortcut icon" href="<?= $this->baseUrl('img/favicon.png') ?>" />
+ <link rel="apple-touch-icon" href="<?= $this->baseUrl('img/touch-icon.png') ?>">
+</head>
+<body id="body" class="loading">
+<pre id="responsive-debug"></pre>
+<div id="layout" class="default-layout<?php if ($showFullscreen): ?> fullscreen-layout<?php endif ?>">
+<?= $this->render($innerLayoutScript); ?>
+</div>
+<div id="collapsible-control-ghost" class="collapsible-control">
+ <button type="button">
+ <!-- TODO: Accessibility attributes are missing since usage of the Icon class -->
+ <?= new Icon('angle-double-down', ['class' => 'expand-icon', 'title' => $this->translate('Expand')]) ?>
+ <?= new Icon('angle-double-up', ['class' => 'collapse-icon', 'title' => $this->translate('Collapse')]) ?>
+ </button>
+</div>
+<div id="modal-ghost">
+ <div>
+ <section class="modal-window">
+ <div class="modal-header">
+ <h1></h1>
+ <button type="button"><?= $this->icon('cancel') ?></button>
+ </div>
+ <div class="modal-area">
+ <div id="modal-content" data-base-target="modal-content" tabindex data-no-icinga-ajax></div>
+ </div>
+ </section>
+ </div>
+</div>
+<script type="text/javascript" src="<?= $this->href($jsfile) ?>"></script>
+<script type="text/javascript" src="<?= $this->href('js/bootstrap.js') ?>"></script>
+</body>
+</html>
diff --git a/application/layouts/scripts/parts/navigation.phtml b/application/layouts/scripts/parts/navigation.phtml
new file mode 100644
index 0000000..dd973f5
--- /dev/null
+++ b/application/layouts/scripts/parts/navigation.phtml
@@ -0,0 +1,35 @@
+<?php
+
+use Icinga\Web\Menu;
+
+// Don't render a menu for unauthenticated users unless menu is auth aware
+if (! $this->auth()->isAuthenticated()) {
+ return;
+}
+
+?>
+<div class="skip-links">
+ <h1 class="sr-only"><?= t('Accessibility Skip Links') ?></h1>
+ <ul>
+ <li>
+ <a href="#main"><?= t('Skip to Content') ?></a>
+ </li>
+ <li>
+ <?= $this->layout()->autoRefreshForm ?>
+ </li>
+ </ul>
+</div>
+<div id="menu" data-last-update="-1" data-base-target="_main" class="container"
+ data-icinga-url="<?= $this->href('layout/menu') ?>" data-icinga-refresh="15">
+ <?= $this->partial(
+ 'layout/menu.phtml',
+ 'default',
+ array(
+ 'menuRenderer' => (new Menu())->getRenderer()->setUseStandardItemRenderer()
+ )
+ ) ?>
+</div>
+<button id="toggle-sidebar" title="<?= $this->translate('Toggle Menu') ?>">
+ <i id="close-sidebar" class="icon-angle-double-left"></i>
+ <i id="open-sidebar" class="icon-angle-double-right"></i>
+</button>
diff --git a/application/layouts/scripts/pdf.phtml b/application/layouts/scripts/pdf.phtml
new file mode 100644
index 0000000..87d07f8
--- /dev/null
+++ b/application/layouts/scripts/pdf.phtml
@@ -0,0 +1,44 @@
+<?php
+
+use Icinga\Application\Icinga;
+use Icinga\Web\StyleSheet;
+
+
+$moduleName = $this->layout()->moduleName;
+if ($moduleName !== 'default') {
+ $moduleClass = ' icinga-module module-' . $moduleName;
+} else {
+ $moduleClass = '';
+}
+
+$logoPath = Icinga::app()->getBootstrapDirectory() . '/img/icinga-logo-big-dark.png';
+$logo = base64_encode(file_get_contents($logoPath));
+
+
+?><!DOCTYPE html>
+<html>
+<head>
+<style>
+<?= StyleSheet::forPdf() ?>
+</style>
+<base href="<?= $this->serverUrl() ?>">
+</head>
+<body>
+<div id="header">
+ <table>
+ <tbody>
+ <tr>
+ <th class="title"><?= $this->escape($this->title) ?></th>
+ <td style="text-align: right;"><img width="75" src="data:image/png;base64,<?= $logo ?>"></td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+<div id="footer">
+ <div class="page-number"></div>
+</div>
+<div class="<?= $moduleClass ?>">
+ <?= $this->render('inline.phtml') ?>
+</div>
+</body>
+</html>
diff --git a/application/views/helpers/CreateTicketLinks.php b/application/views/helpers/CreateTicketLinks.php
new file mode 100644
index 0000000..dda55a6
--- /dev/null
+++ b/application/views/helpers/CreateTicketLinks.php
@@ -0,0 +1,23 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+/**
+ * Helper for creating ticket links from ticket hooks
+ */
+class Zend_View_Helper_CreateTicketLinks extends Zend_View_Helper_Abstract
+{
+ /**
+ * Create ticket links form ticket hooks
+ *
+ * @param string $text
+ *
+ * @return string
+ * @see \Icinga\Application\Hook\TicketHook::createLinks()
+ */
+ public function createTicketLinks($text)
+ {
+ $tickets = $this->view->tickets;
+ /** @var \Icinga\Application\Hook\TicketHook|array|null $tickets */
+ return ! empty($tickets) ? $tickets->createLinks($text) : $text;
+ }
+}
diff --git a/application/views/helpers/FormDate.php b/application/views/helpers/FormDate.php
new file mode 100644
index 0000000..39e6d94
--- /dev/null
+++ b/application/views/helpers/FormDate.php
@@ -0,0 +1,46 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+/**
+ * Render date input controls
+ */
+class Zend_View_Helper_FormDate extends Zend_View_Helper_FormElement
+{
+ /**
+ * Render the date input control
+ *
+ * @param string $name
+ * @param int $value
+ * @param array $attribs
+ *
+ * @return string The rendered date input control
+ */
+ public function formDate($name, $value = null, $attribs = null)
+ {
+ $info = $this->_getInfo($name, $value, $attribs);
+
+ extract($info); // name, id, value, attribs, options, listsep, disable
+ /** @var string $id */
+ /** @var bool $disable */
+
+ $disabled = '';
+ if ($disable) {
+ $disabled = ' disabled="disabled"';
+ }
+
+ /** @var \Icinga\Web\View $view */
+ $view = $this->view;
+
+ $html5 = sprintf(
+ '<input type="date" name="%s" id="%s" value="%s"%s%s%s',
+ $view->escape($name),
+ $view->escape($id),
+ $view->escape($value),
+ $disabled,
+ $this->_htmlAttribs($attribs),
+ $this->getClosingBracket()
+ );
+
+ return $html5;
+ }
+}
diff --git a/application/views/helpers/FormDateTime.php b/application/views/helpers/FormDateTime.php
new file mode 100644
index 0000000..de5eb4b
--- /dev/null
+++ b/application/views/helpers/FormDateTime.php
@@ -0,0 +1,63 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+/**
+ * Render date-and-time input controls
+ */
+class Zend_View_Helper_FormDateTime extends Zend_View_Helper_FormElement
+{
+ /**
+ * Format date and time
+ *
+ * @param DateTime $dateTime
+ * @param bool $local
+ *
+ * @return string
+ */
+ public function formatDate(DateTime $dateTime, $local)
+ {
+ $format = (bool) $local === true ? 'Y-m-d\TH:i:s' : DateTime::RFC3339;
+ return $dateTime->format($format);
+ }
+
+ /**
+ * Render the date-and-time input control
+ *
+ * @param string $name The element name
+ * @param DateTime $value The default timestamp
+ * @param array $attribs Attributes for the element tag
+ *
+ * @return string The element XHTML
+ */
+ public function formDateTime($name, $value = null, $attribs = null)
+ {
+ $info = $this->_getInfo($name, $value, $attribs);
+ extract($info); // name, id, value, attribs, options, listsep, disable
+ /** @var string $id */
+ /** @var bool $disable */
+ $disabled = '';
+ if ($disable) {
+ $disabled = ' disabled="disabled"';
+ }
+ if ($value instanceof DateTime) {
+ // If value was valid, it's a DateTime object
+ $value = $this->formatDate($value, $attribs['local']);
+ }
+ if (isset($attribs['placeholder']) && $attribs['placeholder'] instanceof DateTime) {
+ $attribs['placeholder'] = $this->formatDate($attribs['placeholder'], $attribs['local']);
+ }
+ $type = $attribs['local'] === true ? 'datetime-local' : 'datetime';
+ unset($attribs['local']); // Unset local to not render it again in $this->_htmlAttribs($attribs)
+ $html5 = sprintf(
+ '<input type="%s" data-use-datetime-picker name="%s" id="%s" step="1" value="%s"%s%s%s',
+ $type,
+ $this->view->escape($name),
+ $this->view->escape($id),
+ $this->view->escape($value),
+ $disabled,
+ $this->_htmlAttribs($attribs),
+ $this->getClosingBracket()
+ );
+ return $html5;
+ }
+}
diff --git a/application/views/helpers/FormNumber.php b/application/views/helpers/FormNumber.php
new file mode 100644
index 0000000..f447180
--- /dev/null
+++ b/application/views/helpers/FormNumber.php
@@ -0,0 +1,77 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+/**
+ * Render number input controls
+ */
+class Zend_View_Helper_FormNumber extends Zend_View_Helper_FormElement
+{
+ /**
+ * Format a number
+ *
+ * @param $number
+ *
+ * @return string
+ */
+ public function formatNumber($number)
+ {
+ if (empty($number)) {
+ return $number;
+ }
+ return $this->view->escape(
+ sprintf(
+ ctype_digit((string) $number) ? '%d' : '%F',
+ $number
+ )
+ );
+ }
+
+ /**
+ * Render the number input control
+ *
+ * @param string $name
+ * @param int $value
+ * @param array $attribs
+ *
+ * @return string The rendered number input control
+ */
+ public function formNumber($name, $value = null, $attribs = null)
+ {
+ $info = $this->_getInfo($name, $value, $attribs);
+ extract($info); // name, id, value, attribs, options, listsep, disable
+ /** @var string $id */
+ /** @var bool $disable */
+ $disabled = '';
+ if ($disable) {
+ $disabled = ' disabled="disabled"';
+ }
+ $min = '';
+ if (isset($attribs['min'])) {
+ $min = sprintf(' min="%s"', $this->formatNumber($attribs['min']));
+ }
+ unset($attribs['min']); // Unset min to not render it again in $this->_htmlAttribs($attribs)
+ $max = '';
+ if (isset($attribs['max'])) {
+ $max = sprintf(' max="%s"', $this->formatNumber($attribs['max']));
+ }
+ unset($attribs['max']); // Unset max to not render it again in $this->_htmlAttribs($attribs)
+ $step = '';
+ if (isset($attribs['step'])) {
+ $step = sprintf(' step="%s"', $attribs['step'] === 'any' ? 'any' : $this->formatNumber($attribs['step']));
+ }
+ unset($attribs['step']); // Unset step to not render it again in $this->_htmlAttribs($attribs)
+ $html5 = sprintf(
+ '<input type="number" name="%s" id="%s" value="%s"%s%s%s%s%s%s',
+ $this->view->escape($name),
+ $this->view->escape($id),
+ $this->view->escape($this->formatNumber($value)),
+ $min,
+ $max,
+ $step,
+ $disabled,
+ $this->_htmlAttribs($attribs),
+ $this->getClosingBracket()
+ );
+ return $html5;
+ }
+}
diff --git a/application/views/helpers/FormTime.php b/application/views/helpers/FormTime.php
new file mode 100644
index 0000000..39d1b83
--- /dev/null
+++ b/application/views/helpers/FormTime.php
@@ -0,0 +1,46 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+/**
+ * Render time input controls
+ */
+class Zend_View_Helper_FormTime extends Zend_View_Helper_FormElement
+{
+ /**
+ * Render the time input control
+ *
+ * @param string $name
+ * @param int $value
+ * @param array $attribs
+ *
+ * @return string The rendered time input control
+ */
+ public function formTime($name, $value = null, $attribs = null)
+ {
+ $info = $this->_getInfo($name, $value, $attribs);
+
+ extract($info); // name, id, value, attribs, options, listsep, disable
+ /** @var string $id */
+ /** @var bool $disable */
+
+ $disabled = '';
+ if ($disable) {
+ $disabled = ' disabled="disabled"';
+ }
+
+ /** @var \Icinga\Web\View $view */
+ $view = $this->view;
+
+ $html5 = sprintf(
+ '<input type="time" name="%s" id="%s" value="%s"%s%s%s',
+ $view->escape($name),
+ $view->escape($id),
+ $view->escape($value),
+ $disabled,
+ $this->_htmlAttribs($attribs),
+ $this->getClosingBracket()
+ );
+
+ return $html5;
+ }
+}
diff --git a/application/views/helpers/ProtectId.php b/application/views/helpers/ProtectId.php
new file mode 100644
index 0000000..f6dc226
--- /dev/null
+++ b/application/views/helpers/ProtectId.php
@@ -0,0 +1,13 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+/**
+ * Class Zend_View_Helper_Util
+ */
+class Zend_View_Helper_ProtectId extends Zend_View_Helper_Abstract
+{
+ public function protectId($id)
+ {
+ return Zend_Controller_Front::getInstance()->getRequest()->protectId($id);
+ }
+}
diff --git a/application/views/helpers/Util.php b/application/views/helpers/Util.php
new file mode 100644
index 0000000..7a3e410
--- /dev/null
+++ b/application/views/helpers/Util.php
@@ -0,0 +1,68 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+/**
+ * Class Zend_View_Helper_Util
+ */
+class Zend_View_Helper_Util extends Zend_View_Helper_Abstract
+{
+ public function util()
+ {
+ return $this;
+ }
+
+ public static function showTimeSince($timestamp)
+ {
+ if (! $timestamp) {
+ return 'unknown';
+ }
+ $duration = time() - $timestamp;
+ if ($duration > 3600 * 24 * 3) {
+ if (date('Y') === date('Y', $timestamp)) {
+ return date('d.m.', $timestamp);
+ }
+ return date('m.Y', $timestamp);
+ }
+ return self::showHourMin($duration);
+ }
+
+ public static function showHourMin($sec)
+ {
+ $min = floor($sec / 60);
+ if ($min < 60) {
+ return $min . 'm ' . ($sec % 60) . 's';
+ }
+ $hour = floor($min / 60);
+ if ($hour < 24) {
+ return date('H:i', time() - $sec);
+ }
+ return floor($hour / 24) . 'd ' . ($hour % 24) . 'h';
+ }
+
+ public static function showSeconds($sec)
+ {
+ // Todo: localization
+ if ($sec < 1) {
+ return round($sec * 1000) . 'ms';
+ }
+ if ($sec < 60) {
+ return $sec . 's';
+ }
+ return floor($sec / 60) . 'm ' . ($sec % 60) . 's';
+ }
+
+ public static function showTime($timestamp)
+ {
+ // Todo: localization
+ if ($timestamp < 86400) {
+ return 'undef';
+ }
+ if (date('Ymd') === date('Ymd', $timestamp)) {
+ return date('H:i:s', $timestamp);
+ }
+ if (date('Y') === date('Y', $timestamp)) {
+ return date('H:i d.m.', $timestamp);
+ }
+ return date('H:i d.m.Y', $timestamp);
+ }
+}
diff --git a/application/views/scripts/about/index.phtml b/application/views/scripts/about/index.phtml
new file mode 100644
index 0000000..805e723
--- /dev/null
+++ b/application/views/scripts/about/index.phtml
@@ -0,0 +1,199 @@
+<?php
+
+use Icinga\Application\MigrationManager;
+use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer;
+use ipl\Html\HtmlElement;
+use ipl\Web\Widget\Icon;
+use ipl\Web\Widget\StateBadge;
+
+?>
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div id="about" class="content">
+
+ <?= $this->img('img/icinga-logo-big.svg', null, array('class' => 'icinga-logo', 'width' => 194)) ?>
+
+ <section>
+ <table class="name-value-table">
+ <?php if (isset($version['appVersion'])): ?>
+ <tr>
+ <th><?= $this->translate('Icinga Web 2 Version') ?></th>
+ <td><?= $this->escape($version['appVersion']) ?></td>
+ </tr>
+ <?php endif ?>
+ <?php if (isset($version['gitCommitID'])): ?>
+ <tr>
+ <th><?= $this->translate('Git commit') ?></th>
+ <td><?= $this->escape($version['gitCommitID']) ?></td>
+ </tr>
+ <?php endif ?>
+ <tr>
+ <th><?= $this->translate('PHP Version') ?></th>
+ <td><?= $this->escape(PHP_VERSION) ?></td>
+ </tr>
+ <?php if (isset($version['gitCommitDate'])): ?>
+ <tr>
+ <th><?= $this->translate('Git commit date') ?></th>
+ <td><?= $this->escape($version['gitCommitDate']) ?></td>
+ </tr>
+ <?php endif ?>
+ </table>
+
+ <div class="external-links">
+ <div class="col">
+ <?=
+ HtmlElement::create('a', [
+ 'href' => 'https://icinga.com/support/',
+ 'target' => '_blank',
+ 'title' => $this->translate('Get Icinga Support')
+ ], [
+ new Icon('life-ring'),
+ $this->translate('Get Icinga Support'),
+ ]
+ );
+ ?>
+ </div>
+ <div class="col">
+ <?=
+ HtmlElement::create('a', [
+ 'href' => 'https://icinga.com/community/',
+ 'target' => '_blank',
+ 'title' => $this->translate('Icinga Community')
+ ], [
+ new Icon('globe-europe'),
+ $this->translate('Icinga Community'),
+ ]
+ );
+ ?>
+ </div>
+ <div class="col">
+ <?=
+ HtmlElement::create('a', [
+ 'href' => 'https://github.com/icinga/icingaweb2/issues',
+ 'target' => '_blank',
+ 'title' => $this->translate('Icinga Community')
+ ], [
+ new Icon('bullhorn'),
+ $this->translate('Report a bug'),
+ ]
+ );
+ ?>
+ </div>
+ <div class="col">
+ <?=
+ HtmlElement::create('a', [
+ 'href' => 'https://icinga.com/docs/icinga-web-2/'
+ . (isset($version['docVersion']) ? $version['docVersion'] : 'latest'),
+ 'target' => '_blank',
+ 'title' => $this->translate('Icinga Documentation')
+ ], [
+ new Icon('book'),
+ $this->translate('Icinga Documentation'),
+ ]
+ );
+ ?>
+ </div>
+ </div>
+
+ <?php $mm = MigrationManager::instance(); if ($mm->hasPendingMigrations()): ?>
+ <div class="pending-migrations clearfix">
+ <h2><?= $this->translate('Pending Migrations') ?></h2>
+ <table class="name-value-table migrations">
+ <?php foreach ($mm->getPendingMigrations() as $migration): ?>
+ <tr>
+ <th><?= $this->escape($migration->getName()) ?></th>
+ <td><?=
+ new StateBadge(
+ count($migration->getMigrations()),
+ BadgeNavigationItemRenderer::STATE_PENDING
+ );
+ ?></td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ <?= $this->qlink(
+ $this->translate('Show all'),
+ 'migrations',
+ null,
+ ['title' => $this->translate('Show all pending migrations')]
+ ) ?>
+ </div>
+ <?php endif ?>
+
+ <h2><?= $this->translate('Loaded Libraries') ?></h2>
+ <table class="name-value-table" data-base-target="_next">
+ <?php foreach ($libraries as $library): ?>
+ <tr>
+ <th>
+ <?= $this->escape($library->getName()) ?>
+ </th>
+ <td>
+ <?= $this->escape($library->getVersion()) ?: '-' ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+
+ <h2><?= $this->translate('Loaded Modules') ?></h2>
+ <table class="name-value-table" data-base-target="_next">
+ <?php foreach ($modules as $module): ?>
+ <tr>
+ <th>
+ <?= $this->escape($module->getName()) ?>
+ </th>
+ <td>
+ <td>
+ <?= $this->escape($module->getVersion()) ?>
+ </td>
+ <td>
+ <?php if ($this->hasPermission('config/modules')): ?>
+ <?= $this->qlink(
+ $this->translate('Configure'),
+ 'config/module/',
+ array('name' => $module->getName()),
+ array('title' => sprintf($this->translate('Show the overview of the %s module'), $module->getName()))
+ ) ?>
+ <?php endif ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ </section>
+
+ <footer>
+ <div class="about-copyright">
+ <?= $this->translate('Copyright') ?>
+ <span>&copy; 2013-<?= date('Y') ?></span>
+ <?= $this->qlink(
+ 'Icinga GmbH',
+ 'https://icinga.com',
+ null,
+ array(
+ 'target' => '_blank'
+ )
+ ) ?>
+ </div>
+ <div class="about-social">
+ <?= $this->qlink(
+ null,
+ 'https://www.twitter.com/icinga',
+ null,
+ array(
+ 'target' => '_blank',
+ 'icon' => 'twitter',
+ 'title' => $this->translate('Icinga on Twitter')
+ )
+ ) ?> <?= $this->qlink(
+ null,
+ 'https://www.facebook.com/icinga',
+ null,
+ array(
+ 'target' => '_blank',
+ 'icon' => 'facebook-squared',
+ 'title' => $this->translate('Icinga on Facebook')
+ )
+ ) ?>
+ </div>
+ </footer>
+</div>
diff --git a/application/views/scripts/account/index.phtml b/application/views/scripts/account/index.phtml
new file mode 100644
index 0000000..efc2bcb
--- /dev/null
+++ b/application/views/scripts/account/index.phtml
@@ -0,0 +1,11 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+<?php if (isset($changePasswordForm)): ?>
+ <h1><?= $this->translate('Account') ?></h1>
+ <?= $changePasswordForm ?>
+<?php endif ?>
+ <h1><?= $this->translate('Preferences') ?></h1>
+ <?= $form ?>
+</div>
diff --git a/application/views/scripts/announcements/index.phtml b/application/views/scripts/announcements/index.phtml
new file mode 100644
index 0000000..ff87c66
--- /dev/null
+++ b/application/views/scripts/announcements/index.phtml
@@ -0,0 +1,71 @@
+<?php if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php if ($this->hasPermission('application/announcements')) {
+ echo $this->qlink(
+ $this->translate('Create a New Announcement') ,
+ 'announcements/new',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new announcement')
+ )
+ );
+} ?>
+<?php if (empty($this->announcements)): ?>
+ <p><?= $this->translate('No announcements found.') ?></p>
+</div>
+<?php return; endif ?>
+ <table data-base-target="_next" class="table-row-selectable common-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Author') ?></th>
+ <th><?= $this->translate('Message') ?></th>
+ <th><?= $this->translate('Start') ?></th>
+ <th><?= $this->translate('End') ?></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($this->announcements as $announcement): /** @var object $announcement */ ?>
+ <tr>
+ <td><?= $this->escape($announcement->author) ?></td>
+ <?php if ($this->hasPermission('application/announcements')): ?>
+ <td>
+ <a href="<?= $this->href('announcements/update', array('id' => $announcement->id)) ?>">
+ <?= $this->ellipsis($this->escape($announcement->message), 100) ?>
+ </a>
+ </td>
+ <?php else: ?>
+ <td><?= $this->ellipsis($this->escape($announcement->message), 100) ?></td>
+ <?php endif ?>
+ <td><?= $this->formatDateTime($announcement->start) ?></td>
+ <td><?= $this->formatDateTime($announcement->end) ?></td>
+ <?php if ($this->hasPermission('application/announcements')): ?>
+ <td class="icon-col"><?= $this->qlink(
+ null,
+ 'announcements/remove',
+ array('id' => $announcement->id),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => $this->translate('Remove this announcement')
+ )
+ ) ?></td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+</div>
diff --git a/application/views/scripts/authentication/login.phtml b/application/views/scripts/authentication/login.phtml
new file mode 100644
index 0000000..167a468
--- /dev/null
+++ b/application/views/scripts/authentication/login.phtml
@@ -0,0 +1,74 @@
+<div id="login">
+ <div class="login-form" data-base-target="layout">
+ <div role="status" class="sr-only">
+ <?= $this->translate(
+ 'Welcome to Icinga Web 2. For users of the screen reader Jaws full and expectant compliant'
+ . ' accessibility is possible only with use of the Firefox browser. VoiceOver on Mac OS X is tested on'
+ . ' Chrome, Safari and Firefox.'
+ ) ?>
+ </div>
+ <div class="logo-wrapper"><div id="icinga-logo" aria-hidden="true"></div></div>
+ <?php if ($requiresSetup): ?>
+ <p class="config-note"><?= sprintf(
+ $this->translate(
+ 'It appears that you did not configure Icinga Web 2 yet so it\'s not possible to log in without any defined '
+ . 'authentication method. Please define a authentication method by following the instructions in the'
+ . ' %1$sdocumentation%3$s or by using our %2$sweb-based setup-wizard%3$s.'
+ ),
+ '<a href="https://icinga.com/docs/icinga-web-2/latest/doc/05-Authentication/#authentication" title="'
+ . $this->translate('Icinga Web 2 Documentation') . '">',
+ '<a href="' . $this->href('setup') . '" title="' . $this->translate('Icinga Web 2 Setup-Wizard') . '">',
+ '</a>'
+ ) ?></p>
+ <?php endif ?>
+ <?= $this->form ?>
+ <div id="login-footer">
+ <p>Icinga Web 2 &copy; 2013-<?= date('Y') ?></p>
+ <?= $this->qlink($this->translate('icinga.com'), 'https://icinga.com') ?>
+ </div>
+ </div>
+ <ul id="social">
+ <li>
+ <?= $this->qlink(
+ null,
+ 'https://twitter.com/icinga',
+ null,
+ array(
+ 'target' => '_blank',
+ 'icon' => 'twitter',
+ 'title' => $this->translate('Icinga on Twitter')
+ )
+ ) ?>
+ </li>
+ <li>
+ <?= $this->qlink(
+ null,
+ 'https://www.facebook.com/icinga',
+ null,
+ array(
+ 'target' => '_blank',
+ 'icon' => 'facebook-squared',
+ 'title' => $this->translate('Icinga on Facebook')
+ )
+ ) ?>
+ </li>
+ <li><?= $this->qlink(
+ null,
+ 'https://github.com/Icinga',
+ null,
+ array(
+ 'target' => '_blank',
+ 'icon' => 'github-circled',
+ 'title' => $this->translate('Icinga on GitHub')
+ )
+ ) ?>
+ </li>
+ </ul>
+</div>
+<div id="orb-analytics" class="orb" ><?= $this->img('img/orb-analytics.png'); ?></div>
+<div id="orb-automation" class="orb"><?= $this->img('img/orb-automation.png'); ?></div>
+<div id="orb-cloud" class="orb"><?= $this->img('img/orb-cloud.png'); ?></div>
+<div id="orb-icinga" class="orb"><?= $this->img('img/orb-icinga.png'); ?></div>
+<div id="orb-infrastructure" class="orb"><?= $this->img('img/orb-infrastructure.png'); ?></div>
+<div id="orb-metrics" class="orb" ><?= $this->img('img/orb-metrics.png'); ?></div>
+<div id="orb-notifactions" class="orb"><?= $this->img('img/orb-notifications.png'); ?></div>
diff --git a/application/views/scripts/authentication/logout.phtml b/application/views/scripts/authentication/logout.phtml
new file mode 100644
index 0000000..501ed20
--- /dev/null
+++ b/application/views/scripts/authentication/logout.phtml
@@ -0,0 +1,64 @@
+<?php
+
+use Icinga\Util\Csp;
+
+?>
+<!--
+ This view provides a workaround to logout from an external authentication provider, in case external
+ authentication was configured (the default is to handle authentications internally in Icingaweb2).
+
+ The <a href="http://tools.ietf.org/html/rfc2617">Http Basic and Digest Authentication</a> is not
+ designed to handle logout. When the user has provided valid credentials, the client is adviced to include these
+ in every further request until the browser was closed. To allow logout and to allow the user to change the
+ logged-in user this JavaScript provides a workaround to force a new authentication prompt in most browsers.
+-->
+<div class="content">
+ <div id="icinga-logo" aria-hidden="true"></div>
+ <div class="alert alert-warning" id="logout-in-progress">
+ <b><?= $this->translate('Logging out...'); ?></b>
+ <p>
+ <?= $this->translate(
+ 'If this message does not disappear, it might be necessary to quit the'
+ . ' current session manually by clearing the cache, or by closing the current'
+ . ' browser session.'
+ ); ?>
+ </p>
+ </div>
+ <div id="logout-successful" class="alert alert-success" hidden><?= $this->translate('Logout successful'); ?></div>
+
+ <div class="container">
+ <a href="<?= $this->href('dashboard'); ?>"><?= $this->translate('Login'); ?></a>
+ </div>
+</div>
+<script type="text/javascript" src="<?= $this->href('js/logout.js'); ?>"></script>
+<style type="text/css" nonce="<?= Csp::getStyleNonce(); ?>">
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+ background-color: #0095bf;
+ color: white;
+ }
+ .content {
+ text-align: center;
+ }
+
+ #icinga-logo {
+ background-image: url('../img/icinga-logo-big.svg');
+ background-position: center bottom;
+ background-repeat: no-repeat;
+ background-size: contain;
+ height: 177px;
+ margin-top: 10em;
+ width: 100%;
+ }
+
+ #logout-in-progress {
+ margin: 2em 0 1em;
+ font-size: 2em;
+ font-weight: bold;
+ }
+
+ .container a {
+ color: white;
+ font-size: 1.5em;
+ }
+</style>
diff --git a/application/views/scripts/config/devtools.phtml b/application/views/scripts/config/devtools.phtml
new file mode 100644
index 0000000..245a71a
--- /dev/null
+++ b/application/views/scripts/config/devtools.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+<?= $this->tabs ?>
+</div>
+<table class="avp">
+<tr><th><?= $this->translate('UI Debug') ?></th><td><a href="javascript:void(0);" onclick="icinga.ui.toggleDebug();"><?= $this->translate('toggle') ?></td></tr>
+</table>
diff --git a/application/views/scripts/config/general.phtml b/application/views/scripts/config/general.phtml
new file mode 100644
index 0000000..13a8ed9
--- /dev/null
+++ b/application/views/scripts/config/general.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?= $form ?>
+</div>
diff --git a/application/views/scripts/config/module-configuration-error.phtml b/application/views/scripts/config/module-configuration-error.phtml
new file mode 100644
index 0000000..85fb128
--- /dev/null
+++ b/application/views/scripts/config/module-configuration-error.phtml
@@ -0,0 +1,28 @@
+<?php
+ $action = (isset($this->action)) ? $this->action : 'do something with';
+ $moduleName = $this->moduleName;
+ $exceptionMessage = $this->exceptionMessage;
+?>
+<?= $this->tabs->render($this); ?>
+<br/>
+<div>
+ <h1>Could not <?= $action; ?> module "<?= $moduleName; ?>"</h1>
+ <p>
+ While operation the following error occurred:
+ <br />
+ <?= $exceptionMessage; ?>
+ </p>
+</div>
+
+<p>
+ This could have one or more of the following reasons:
+<ul>
+ <li>No file permissions to write into module directory</li>
+ <li>Errors on filesystems: Mount points, operational errors </li>
+ <li>General application error</li>
+</ul>
+</p>
+
+<p>
+ Details can be seen in your application log (if you don't have access to this file, call your administrator in this case).
+</p> \ No newline at end of file
diff --git a/application/views/scripts/config/module.phtml b/application/views/scripts/config/module.phtml
new file mode 100644
index 0000000..6d41ab2
--- /dev/null
+++ b/application/views/scripts/config/module.phtml
@@ -0,0 +1,136 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<div class="content">
+ <?php if (! $module): ?>
+ <?= $this->translate('There is no such module installed.') ?>
+ <?php return; endif ?>
+ <?php
+ $requiredMods = $module->getRequiredModules();
+ $requiredLibs = $module->getRequiredLibraries();
+ $restrictions = $module->getProvidedRestrictions();
+ $permissions = $module->getProvidedPermissions();
+ $unmetDependencies = $moduleManager->hasUnmetDependencies($module->getName());
+ $isIcingadbSupported = isset($requiredMods['icingadb']);
+ $state = $moduleData->enabled ? ($moduleData->loaded ? 'enabled' : 'failed') : 'disabled';
+ ?>
+ <table class="name-value-table">
+ <tr>
+ <th><?= $this->escape($this->translate('Name')) ?></th>
+ <td><?= $this->escape($module->getName()) ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('State') ?></th>
+ <td>
+ <?= $state ?>
+ <?php if (isset($this->toggleForm)): ?>
+ <?php if ($moduleData->enabled || ! $unmetDependencies): ?>
+ <?= $this->toggleForm ?>
+ <?php else: ?>
+ <?= $this->icon('attention-alt', $this->translate('Module can\'t be enabled due to unmet dependencies')) ?>
+ <?php endif ?>
+ <?php endif ?>
+ </td>
+ <tr>
+ <th><?= $this->escape($this->translate('Version')) ?></th>
+ <td><?= $this->escape($module->getVersion()) ?></td>
+ </tr>
+ <?php if (isset($moduleGitCommitId) && $moduleGitCommitId !== false): ?>
+ <tr>
+ <th><?= $this->escape($this->translate('Git commit')) ?></th>
+ <td><?= $this->escape($moduleGitCommitId) ?></td>
+ </tr>
+ <?php endif ?>
+ <tr>
+ <th><?= $this->escape($this->translate('Description')) ?></th>
+ <td>
+ <strong><?= $this->escape($module->getTitle()) ?></strong><br>
+ <?= nl2br($this->escape($module->getDescription())) ?>
+ </td>
+ </tr>
+ <tr>
+ <th><?= $this->escape($this->translate('Dependencies')) ?></th>
+ <td class="module-dependencies">
+ <?php if (empty($requiredLibs) && empty($requiredMods)): ?>
+ <?= $this->translate('This module has no dependencies') ?>
+ <?php else: ?>
+ <?php if ($unmetDependencies): ?>
+ <strong class="unmet-dependencies">
+ <?= $this->translate('Unmet dependencies found! Module can\'t be enabled unless all dependencies are met.') ?>
+ </strong>
+ <?php endif ?>
+ <?php if (! empty($requiredLibs)): ?>
+ <table class="name-value-table">
+ <caption><?= $this->translate('Libraries') ?></caption>
+ <?php foreach ($requiredLibs as $libraryName => $versionString): ?>
+ <tr>
+ <th><?= $this->escape($libraryName) ?></th>
+ <td>
+ <?php if ($libraries->has($libraryName, $versionString === true ? null : $versionString)): ?>
+ <?= $versionString === true ? '*' : $this->escape($versionString) ?>
+ <?php else: ?>
+ <span class="missing"><?= $versionString === true ? '*' : $this->escape($versionString) ?></span>
+ <?php if (($library = $libraries->get($libraryName)) !== null): ?>
+ (<?= $library->getVersion() ?>)
+ <?php endif ?>
+ <?php endif ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ <?php endif ?>
+ <?php if (! empty($requiredMods)): ?>
+ <table class="name-value-table">
+ <caption><?= $this->translate('Modules') ?></caption>
+ <?php foreach ($requiredMods as $moduleName => $versionString): ?>
+ <?php if ($moduleName === 'monitoring' && $isIcingadbSupported && $moduleManager->has('icingadb', $requiredMods['icingadb'])) : ?>
+ <?php continue; ?>
+ <?php endif ?>
+ <tr>
+ <th><?= $this->escape($moduleName) ?></th>
+ <td>
+ <?php if ($moduleManager->has($moduleName, $versionString === true ? null : $versionString)): ?>
+ <?= $versionString === true ? '*' : $this->escape($versionString) ?>
+ <?php else: ?>
+ <span <?= ($moduleName === 'icingadb' && isset($requiredMods['monitoring']) && $moduleManager->has('monitoring', $requiredMods['monitoring'])) ? 'class="optional"' : 'class="missing"' ?>>
+ <?= $versionString === true ? '*' : $this->escape($versionString) ?>
+ </span>
+ <?php if (! $moduleManager->hasInstalled($moduleName)): ?>
+ (<?= $this->translate('not installed') ?>)
+ <?php else: ?>
+ (<?= $moduleManager->getModule($moduleName, false)->getVersion() ?><?= $moduleManager->hasEnabled($moduleName) ? '' : ', ' . $this->translate('disabled') ?>)
+ <?php endif ?>
+ <?php endif ?>
+ </td>
+ <?php if ($moduleName === 'monitoring' && $isIcingadbSupported) : ?>
+ <td class="or-separator"><?= $this->translate('or') ?></td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ <?php endif ?>
+ <?php endif ?>
+ </td>
+ </tr>
+ <?php if (! empty($permissions)): ?>
+ <tr>
+ <th><?= $this->escape($this->translate('Permissions')) ?></th>
+ <td>
+ <?php foreach ($permissions as $permission): ?>
+ <strong><?= $this->escape($permission->name) ?></strong>: <?= $this->escape($permission->description) ?><br />
+ <?php endforeach ?>
+ </td>
+ </tr>
+ <?php endif ?>
+ <?php if (! empty($restrictions)): ?>
+ <tr>
+ <th><?= $this->escape($this->translate('Restrictions')) ?></th>
+ <td>
+ <?php foreach ($restrictions as $restriction): ?>
+ <strong><?= $this->escape($restriction->name) ?></strong>: <?= $this->escape($restriction->description) ?><br />
+ <?php endforeach ?>
+ </td>
+ </tr>
+ <?php endif ?>
+ </table>
+</div>
diff --git a/application/views/scripts/config/modules.phtml b/application/views/scripts/config/modules.phtml
new file mode 100644
index 0000000..b13b378
--- /dev/null
+++ b/application/views/scripts/config/modules.phtml
@@ -0,0 +1,42 @@
+<?php if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+</div>
+<?php endif ?>
+<div class="content">
+ <table class="table-row-selectable common-table" data-base-target="_next">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Module') ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($modules as $module): ?>
+ <tr>
+ <td>
+ <?php if (! $module->installed) {
+ $this->icon('flash', sprintf($this->translate('Module %s is dangling'), $module->name));
+ } elseif ($module->enabled && $module->loaded) {
+ echo $this->icon('thumbs-up', sprintf($this->translate('Module %s is enabled'), $module->name));
+ } elseif (! $module->enabled) {
+ echo $this->icon('block', sprintf($this->translate('Module %s is disabled'), $module->name));
+ } else { // ! $module->loaded
+ echo $this->icon('block', sprintf($this->translate('Module %s has failed to load'), $module->name));
+ }
+
+ echo $this->qlink(
+ $module->name,
+ 'config/module',
+ array('name' => $module->name),
+ array(
+ 'class' => 'rowaction',
+ 'title' => sprintf($this->translate('Show the overview of the %s module'), $module->name)
+ )
+ ); ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+</div>
diff --git a/application/views/scripts/config/resource.phtml b/application/views/scripts/config/resource.phtml
new file mode 100644
index 0000000..317c115
--- /dev/null
+++ b/application/views/scripts/config/resource.phtml
@@ -0,0 +1,73 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?= $this->qlink(
+ $this->translate('Create a New Resource') ,
+ 'config/createresource',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new resource')
+ )
+ ) ?>
+ <table class="table-row-selectable common-table" data-base-target="_next">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Resource') ?></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+<?php foreach ($this->resources as $name => $value): ?>
+ <tr>
+ <td>
+ <?php
+ switch ($value->type) {
+ case 'db':
+ $icon = 'database';
+ break;
+ case 'ldap':
+ $icon = 'sitemap';
+ break;
+ case 'ssh':
+ $icon = 'user';
+ break;
+ case 'file':
+ case 'ini':
+ $icon = 'doc-text';
+ break;
+ default:
+ $icon = 'edit';
+ break;
+ }
+ ?>
+ <?= $this->qlink(
+ $name,
+ 'config/editresource',
+ array('resource' => $name),
+ array(
+ 'icon' => $icon,
+ 'title' => sprintf($this->translate('Edit resource %s'), $name)
+ )
+ ) ?>
+ </td>
+ <td class="icon-col text-right">
+ <?= $this->qlink(
+ '',
+ 'config/removeresource',
+ array('resource' => $name),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove resource %s'), $name)
+ )
+ ) ?>
+ </td>
+ </tr>
+<?php endforeach ?>
+ </tbody>
+ </table>
+</div>
diff --git a/application/views/scripts/config/resource/create.phtml b/application/views/scripts/config/resource/create.phtml
new file mode 100644
index 0000000..13a8ed9
--- /dev/null
+++ b/application/views/scripts/config/resource/create.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?= $form ?>
+</div>
diff --git a/application/views/scripts/config/resource/modify.phtml b/application/views/scripts/config/resource/modify.phtml
new file mode 100644
index 0000000..13a8ed9
--- /dev/null
+++ b/application/views/scripts/config/resource/modify.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?= $form ?>
+</div>
diff --git a/application/views/scripts/config/resource/remove.phtml b/application/views/scripts/config/resource/remove.phtml
new file mode 100644
index 0000000..13a8ed9
--- /dev/null
+++ b/application/views/scripts/config/resource/remove.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?= $form ?>
+</div>
diff --git a/application/views/scripts/config/userbackend/reorder.phtml b/application/views/scripts/config/userbackend/reorder.phtml
new file mode 100644
index 0000000..c77fd2e
--- /dev/null
+++ b/application/views/scripts/config/userbackend/reorder.phtml
@@ -0,0 +1,75 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?php if ($this->auth()->hasPermission('config/access-control/users')): ?>
+ <h1><?= $this->translate('User Backends') ?></h1>
+ <?= $this->qlink(
+ $this->translate('Create a New User Backend') ,
+ 'config/createuserbackend',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new user backend')
+ )
+ ) ?>
+ <?= $form ?>
+ <?php endif ?>
+
+ <?php if ($this->auth()->hasPermission('config/access-control/groups')): ?>
+ <h1><?= $this->translate('User Group Backends') ?></h1>
+ <?= $this->qlink(
+ $this->translate('Create a New User Group Backend') ,
+ 'usergroupbackend/create',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new user group backend')
+ )
+ ) ?>
+<?php if (! count($backendNames)) { return; } ?>
+ <table class="table-row-selectable common-table" data-base-target="_next">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Backend') ?></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+<?php foreach ($backendNames as $backendName => $config):
+ $type = $config->get('backend');
+?>
+ <tr>
+ <td>
+ <?= $this->qlink(
+ $backendName,
+ 'usergroupbackend/edit',
+ array('backend' => $backendName),
+ array(
+ 'icon' => $type === 'external' ? 'magic' : ($type === 'ldap' || $type === 'msldap' ? 'sitemap' : 'database'),
+ 'title' => sprintf($this->translate('Edit user group backend %s'), $backendName)
+ )
+ ); ?>
+ </td>
+ <td class="icon-col text-right">
+ <?= $this->qlink(
+ null,
+ 'usergroupbackend/remove',
+ array('backend' => $backendName),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove user group backend %s'), $backendName)
+ )
+ ) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+ <?php endif ?>
+</div>
diff --git a/application/views/scripts/dashboard/error.phtml b/application/views/scripts/dashboard/error.phtml
new file mode 100644
index 0000000..9396b49
--- /dev/null
+++ b/application/views/scripts/dashboard/error.phtml
@@ -0,0 +1,13 @@
+<div class="content">
+ <h1><?= $this->translate('Could not save dashboard'); ?></h1>
+ <p>
+ <?= $this->translate('Please copy the following dashboard snippet to '); ?>
+ <strong><?= $this->config->getConfigFile(); ?>;</strong>.
+ <br>
+ <?= $this->translate('Make sure that the webserver can write to this file.'); ?>
+ </p>
+ <pre><?= $this->config; ?></pre>
+ <hr>
+ <h2><?= $this->translate('Error details'); ?></h2>
+ <p><?= $this->error->getMessage(); ?></p>
+</div> \ No newline at end of file
diff --git a/application/views/scripts/dashboard/index.phtml b/application/views/scripts/dashboard/index.phtml
new file mode 100644
index 0000000..1d56114
--- /dev/null
+++ b/application/views/scripts/dashboard/index.phtml
@@ -0,0 +1,26 @@
+<div class="controls">
+<?php if (! $this->compact): ?>
+<?= $this->tabs ?>
+<?php endif ?>
+</div>
+<?php if ($this->dashboard): ?>
+ <div class="dashboard content">
+ <?= $this->dashboard ?>
+ </div>
+<?php else: ?>
+ <div class="content">
+ <h1><?= $this->escape($this->translate('Welcome to Icinga Web!')) ?></h1>
+ <p>
+ <?php if (! $this->hasPermission('config/modules')) {
+ echo $this->escape($this->translate(
+ 'Currently there is no dashlet available. Please contact the administrator.'
+ ));
+ } else {
+ printf(
+ $this->escape($this->translate('Currently there is no dashlet available. This might change once you enabled some of the available %s.')),
+ $this->qlink($this->translate('modules'), 'config/modules')
+ );
+ } ?>
+ </p>
+ </div>
+<?php endif ?>
diff --git a/application/views/scripts/dashboard/new-dashlet.phtml b/application/views/scripts/dashboard/new-dashlet.phtml
new file mode 100644
index 0000000..b265a25
--- /dev/null
+++ b/application/views/scripts/dashboard/new-dashlet.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<div class="content">
+ <?= $this->form; ?>
+</div> \ No newline at end of file
diff --git a/application/views/scripts/dashboard/remove-dashlet.phtml b/application/views/scripts/dashboard/remove-dashlet.phtml
new file mode 100644
index 0000000..b265a25
--- /dev/null
+++ b/application/views/scripts/dashboard/remove-dashlet.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<div class="content">
+ <?= $this->form; ?>
+</div> \ No newline at end of file
diff --git a/application/views/scripts/dashboard/remove-pane.phtml b/application/views/scripts/dashboard/remove-pane.phtml
new file mode 100644
index 0000000..b265a25
--- /dev/null
+++ b/application/views/scripts/dashboard/remove-pane.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<div class="content">
+ <?= $this->form; ?>
+</div> \ No newline at end of file
diff --git a/application/views/scripts/dashboard/rename-pane.phtml b/application/views/scripts/dashboard/rename-pane.phtml
new file mode 100644
index 0000000..b265a25
--- /dev/null
+++ b/application/views/scripts/dashboard/rename-pane.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<div class="content">
+ <?= $this->form; ?>
+</div> \ No newline at end of file
diff --git a/application/views/scripts/dashboard/settings.phtml b/application/views/scripts/dashboard/settings.phtml
new file mode 100644
index 0000000..52d4f14
--- /dev/null
+++ b/application/views/scripts/dashboard/settings.phtml
@@ -0,0 +1,91 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<div class="content">
+ <h1><?= t('Dashboard Settings'); ?></h1>
+
+ <table class="avp action" data-base-target="_next">
+ <thead>
+ <tr>
+ <th>
+ <strong><?= t('Dashlet Name') ?></strong>
+ </th>
+ <th>
+ <strong><?= t('Url') ?></strong>
+ </th>
+ <th class="icon-col">&nbsp;</th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($this->dashboard->getPanes() as $pane): ?>
+ <?php if ($pane->getDisabled()) continue; ?>
+ <tr>
+ <th colspan="2">
+ <?php if ($pane->isUserWidget()): ?>
+ <?= $this->qlink(
+ $pane->getName(),
+ 'dashboard/rename-pane',
+ array('pane' => $pane->getName()),
+ array('title' => sprintf($this->translate('Edit pane %s'), $pane->getName()))
+ ) ?>
+ <?php else: ?>
+ <?= $this->escape($pane->getName()) ?>
+ <?php endif ?>
+ </th>
+ <th>
+ <?= $this->qlink(
+ '',
+ 'dashboard/remove-pane',
+ array('pane' => $pane->getName()),
+ array(
+ 'icon' => 'trash',
+ 'title' => sprintf($this->translate('Remove pane %s'), $pane->getName())
+ )
+ ); ?>
+ </th>
+ </tr>
+ <?php $dashlets = $pane->getDashlets(); ?>
+ <?php if(empty($dashlets)): ?>
+ <tr>
+ <td colspan="3">
+ <?= $this->translate('No dashlets added to dashboard') ?>.
+ </td>
+ </tr>
+ <?php else: ?>
+ <?php foreach ($dashlets as $dashlet): ?>
+ <?php if ($dashlet->getDisabled()) continue; ?>
+ <tr>
+ <td>
+ <?= $this->qlink(
+ $dashlet->getTitle(),
+ 'dashboard/update-dashlet',
+ array('pane' => $pane->getName(), 'dashlet' => $dashlet->getName()),
+ array('title' => sprintf($this->translate('Edit dashlet %s'), $dashlet->getTitle()))
+ ); ?>
+ </td>
+ <td>
+ <?= $this->qlink(
+ $dashlet->getUrl()->getRelativeUrl(),
+ $dashlet->getUrl()->getRelativeUrl(),
+ null,
+ array('title' => sprintf($this->translate('Show dashlet %s'), $dashlet->getTitle()))
+ ); ?>
+ </td>
+ <td>
+ <?= $this->qlink(
+ '',
+ 'dashboard/remove-dashlet',
+ array('pane' => $pane->getName(), 'dashlet' => $dashlet->getName()),
+ array(
+ 'icon' => 'trash',
+ 'title' => sprintf($this->translate('Remove dashlet %s from pane %s'), $dashlet->getTitle(), $pane->getTitle())
+ )
+ ); ?>
+ </td>
+ </tr>
+ <?php endforeach; ?>
+ <?php endif; ?>
+ <?php endforeach; ?>
+ </tbody>
+ </table>
+</div>
diff --git a/application/views/scripts/dashboard/update-dashlet.phtml b/application/views/scripts/dashboard/update-dashlet.phtml
new file mode 100644
index 0000000..b265a25
--- /dev/null
+++ b/application/views/scripts/dashboard/update-dashlet.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<div class="content">
+ <?= $this->form; ?>
+</div> \ No newline at end of file
diff --git a/application/views/scripts/error/error.phtml b/application/views/scripts/error/error.phtml
new file mode 100644
index 0000000..5f4579a
--- /dev/null
+++ b/application/views/scripts/error/error.phtml
@@ -0,0 +1,106 @@
+<?php if (! $this->compact && ! $hideControls): ?>
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php
+if (isset($stackTraces)) {
+ foreach ($messages as $i => $message) {
+ echo '<p tabindex="-1" class="autofocus error-message">' . nl2br($this->escape($message)) . '</p>'
+ . '<hr>'
+ . '<pre>' . $this->escape($stackTraces[$i]) . '</pre>';
+ }
+} else {
+ foreach ($messages as $message) {
+ echo '<p tabindex="-1" class="autofocus error-message">' . nl2br($this->escape($message)) . '</p>';
+ }
+}
+
+$libraries = \Icinga\Application\Icinga::app()->getLibraries();
+$coreReason = [];
+$modReason = [];
+
+if (isset($requiredVendor, $requiredProject) && $requiredVendor && $requiredProject) {
+ // TODO: I don't like this, can we define requirements somewhere else?
+ $coreDeps = ['icinga-php-library' => '>= 0.13', 'icinga-php-thirdparty' => '>= 0.12'];
+
+ foreach ($coreDeps as $libraryName => $requiredVersion) {
+ if (! $libraries->has($libraryName)) {
+ $coreReason[] = sprintf($this->translate(
+ 'Library "%s" is required and missing. Please install a version of it matching the required one: %s'
+ ), $libraryName, $requiredVersion);
+ } elseif (! $libraries->has($libraryName, $requiredVersion) && $libraries->get($libraryName)->isRequired($requiredVendor, $requiredProject)) {
+ $coreReason[] = sprintf($this->translate(
+ 'Library "%s" is required and installed, but its version (%s) does not satisfy the required one: %s'
+ ), $libraryName, $libraries->get($libraryName)->getVersion() ?: '-', $requiredVersion);
+ }
+ }
+
+ if (! empty($coreReason)) {
+ array_unshift($coreReason, $this->translate('You have unmet dependencies. Please check Icinga Web 2\'s installation instructions.'));
+ }
+}
+
+if (isset($module)) {
+ $manager = \Icinga\Application\Icinga::app()->getModuleManager();
+ if ($manager->hasUnmetDependencies($module->getName())) {
+ if (isset($requiredModule) && $requiredModule && isset($module->getRequiredModules()[$requiredModule])) {
+ if (! $manager->hasInstalled($requiredModule)) {
+ $modReason[] = sprintf($this->translate(
+ 'Module "%s" is required and missing. Please install a version of it matching the required one: %s'
+ ), $requiredModule, $module->getRequiredModules()[$requiredModule]);
+ } elseif (! $manager->hasEnabled($requiredModule)) {
+ $modReason[] = sprintf($this->translate(
+ 'Module "%s" is required and installed, but not enabled. Please enable module "%1$s".'
+ ), $requiredModule);
+ } elseif (! $manager->has($requiredModule, $module->getRequiredModules()[$requiredModule])) {
+ $modReason[] = sprintf($this->translate(
+ 'Module "%s" is required and installed, but its version (%s) does not satisfy the required one: %s'
+ ), $requiredModule, $manager->getModule($requiredModule, false)->getVersion(), $module->getRequiredModules()[$requiredModule]);
+ }
+ } elseif (isset($requiredVendor, $requiredProject) && $requiredVendor && $requiredProject) {
+ foreach ($module->getRequiredLibraries() as $libraryName => $requiredVersion) {
+ if (! $libraries->has($libraryName)) {
+ $modReason[] = sprintf($this->translate(
+ 'Library "%s" is required and missing. Please install a version of it matching the required one: %s'
+ ), $libraryName, $requiredVersion);
+ } elseif (! $libraries->has($libraryName, $requiredVersion) && $libraries->get($libraryName)->isRequired($requiredVendor, $requiredProject)) {
+ $modReason[] = sprintf($this->translate(
+ 'Library "%s" is required and installed, but its version (%s) does not satisfy the required one: %s'
+ ), $libraryName, $libraries->get($libraryName)->getVersion() ?: '-', $requiredVersion);
+ }
+ }
+ }
+
+ if (! empty($modReason)) {
+ array_unshift($modReason, sprintf($this->translate(
+ 'This error might have occurred because module "%s" has unmet dependencies.'
+ . ' Please check it\'s installation instructions and install missing dependencies.'
+ ), $module->getName()));
+ }
+ }
+}
+
+// The following doesn't use ipl\Html because that's what the error possibly is about
+?>
+<?php if (! empty($coreReason)): ?>
+<div class="error-reason">
+<?php endif ?>
+<?php foreach ($coreReason as $msg): ?>
+ <p><?= $msg ?></p>
+<?php endforeach ?>
+<?php if (! empty($coreReason)): ?>
+</div>
+<?php endif ?>
+
+<?php if (! empty($modReason)): ?>
+<div class="error-reason">
+<?php endif ?>
+<?php foreach ($modReason as $msg): ?>
+ <p><?= $msg ?></p>
+<?php endforeach ?>
+<?php if (! empty($modReason)): ?>
+</div>
+<?php endif ?>
+</div>
diff --git a/application/views/scripts/filter/index.phtml b/application/views/scripts/filter/index.phtml
new file mode 100644
index 0000000..5e6a63d
--- /dev/null
+++ b/application/views/scripts/filter/index.phtml
@@ -0,0 +1,11 @@
+<?php
+
+echo $this->form;
+
+if ($this->tree) {
+ echo $this->tree->render($this);
+ echo '<br/><pre><code>';
+ echo $this->sqlString;
+ echo '</pre></code>';
+ print_r($this->params);
+} \ No newline at end of file
diff --git a/application/views/scripts/form/reorder-authbackend.phtml b/application/views/scripts/form/reorder-authbackend.phtml
new file mode 100644
index 0000000..34b10b3
--- /dev/null
+++ b/application/views/scripts/form/reorder-authbackend.phtml
@@ -0,0 +1,83 @@
+<form id="<?=
+$this->escape($form->getId())
+?>" name="<?=
+$this->escape($form->getName())
+?>" enctype="<?=
+$this->escape($form->getEncType())
+?>" method="<?=
+$this->escape($form->getMethod())
+?>" action="<?=
+$this->escape($form->getAction())
+?>">
+ <table class="table-row-selectable common-table" data-base-target="_next">
+ <thead>
+ <th><?= $this->translate('Backend') ?></th>
+ <th></th>
+ <th></th>
+ </thead>
+ <tbody>
+<?php
+ $backendNames = $form->getBackendOrder();
+ $backendConfigs = $form->getConfig();
+ for ($i = 0; $i < count($backendNames); $i++):
+ $type = $backendConfigs->getSection($backendNames[$i])->get('backend');
+?>
+ <tr>
+ <td class="action">
+ <?= $this->qlink(
+ $backendNames[$i],
+ 'config/edituserbackend',
+ array('backend' => $backendNames[$i]),
+ array(
+ 'icon' => $type === 'external' ?
+ 'magic' : ($type === 'ldap' || $type === 'msldap' ? 'sitemap' : 'database'),
+ 'class' => 'rowaction',
+ 'title' => sprintf($this->translate('Edit user backend %s'), $backendNames[$i])
+ )
+ ) ?>
+ </td>
+ <td class="icon-col text-right">
+ <?= $this->qlink(
+ '',
+ 'config/removeuserbackend',
+ array('backend' => $backendNames[$i]),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove user backend %s'), $backendNames[$i])
+ )
+ ) ?>
+ </td>
+ <td class="icon-col text-right" data-base-target="_self">
+<?php if ($i > 0): ?>
+ <button type="submit" name="backend_newpos" class="link-button icon-only animated move-up" value="<?= $this->escape(
+ $backendNames[$i] . '|' . ($i - 1)
+ ) ?>" title="<?= $this->translate(
+ 'Move up in authentication order'
+ ) ?>" aria-label="<?= $this->escape(sprintf(
+ $this->translate('Move user backend %s upwards'),
+ $backendNames[$i]
+ )) ?>">
+ <?= $this->icon('up-small') ?>
+ </button>
+<?php endif ?>
+<?php if ($i + 1 < count($backendNames)): ?>
+ <button type="submit" name="backend_newpos" class="link-button icon-only animated move-down" value="<?= $this->escape(
+ $backendNames[$i] . '|' . ($i + 1)
+ ) ?>" title="<?= $this->translate(
+ 'Move down in authentication order'
+ ) ?>" aria-label="<?= $this->escape(sprintf(
+ $this->translate('Move user backend %s downwards'),
+ $backendNames[$i]
+ )) ?>">
+ <?= $this->icon('down-small') ?>
+ </button>
+<?php endif ?>
+ </td>
+ </tr>
+<?php endfor ?>
+ </tbody>
+ </table>
+ <?= $form->getElement($form->getTokenElementName()) ?>
+ <?= $form->getElement($form->getUidElementName()) ?>
+</form>
diff --git a/application/views/scripts/group/form.phtml b/application/views/scripts/group/form.phtml
new file mode 100644
index 0000000..cbf0659
--- /dev/null
+++ b/application/views/scripts/group/form.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= $tabs->showOnlyCloseButton(); ?>
+</div>
+<div class="content">
+ <?= $form; ?>
+</div> \ No newline at end of file
diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml
new file mode 100644
index 0000000..d362db4
--- /dev/null
+++ b/application/views/scripts/group/list.phtml
@@ -0,0 +1,96 @@
+<?php
+
+use Icinga\Data\Extensible;
+use Icinga\Data\Reducible;
+
+if (! $this->compact): ?>
+<div class="controls separated">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->backendSelection ?>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php
+
+if (! isset($backend)) {
+ echo $this->translate('No backend found which is able to list user groups') . '</div>';
+ return;
+} else {
+ $extensible = $this->hasPermission('config/access-control/groups') && $backend instanceof Extensible;
+ $reducible = $this->hasPermission('config/access-control/groups') && $backend instanceof Reducible;
+}
+?>
+
+<?php if ($extensible): ?>
+ <?= $this->qlink(
+ $this->translate('Add a New User Group'),
+ 'group/add',
+ array('backend' => $backend->getName()),
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus'
+ )
+ ) ?>
+<?php endif ?>
+
+<?php if (! $groups->hasResult()): ?>
+ <p><?= $this->translate('No user groups found matching the filter'); ?></p>
+</div>
+<?php return; endif ?>
+ <table data-base-target="_next" class="table-row-selectable common-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('User Group'); ?></th>
+ <?php if ($reducible): ?>
+ <th><?= $this->translate('Remove'); ?></th>
+ <?php endif ?>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($groups as $group): ?>
+ <tr>
+ <td>
+ <?= $this->qlink(
+ $group->group_name,
+ 'group/show',
+ array(
+ 'backend' => $backend->getName(),
+ 'group' => $group->group_name
+ ),
+ array(
+ 'title' => sprintf(
+ $this->translate('Show detailed information for user group %s'),
+ $group->group_name
+ )
+ )
+ ); ?>
+ </td>
+ <?php if ($reducible): ?>
+ <td class="icon-col">
+ <?= $this->qlink(
+ null,
+ 'group/remove',
+ array(
+ 'backend' => $backend->getName(),
+ 'group' => $group->group_name
+ ),
+ array(
+ 'class' => 'action-link',
+ 'title' => sprintf($this->translate('Remove user group %s'), $group->group_name),
+ 'icon' => 'cancel'
+ )
+ ); ?>
+ </td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+</div>
diff --git a/application/views/scripts/group/show.phtml b/application/views/scripts/group/show.phtml
new file mode 100644
index 0000000..75f0b75
--- /dev/null
+++ b/application/views/scripts/group/show.phtml
@@ -0,0 +1,108 @@
+<?php
+
+use Icinga\Data\Extensible;
+use Icinga\Data\Updatable;
+
+$extensible = $this->hasPermission('config/access-control/groups') && $backend instanceof Extensible;
+
+$editLink = null;
+if ($this->hasPermission('config/access-control/groups') && $backend instanceof Updatable) {
+ $editLink = $this->qlink(
+ null,
+ 'group/edit',
+ array(
+ 'backend' => $backend->getName(),
+ 'group' => $group->group_name
+ ),
+ array(
+ 'title' => sprintf($this->translate('Edit group %s'), $group->group_name),
+ 'class' => 'group-edit',
+ 'icon' => 'edit'
+ )
+ );
+}
+
+?>
+<div class="controls separated">
+<?php if (! $this->compact): ?>
+ <?= $tabs; ?>
+<?php endif ?>
+ <h2 class="clearfix"><?= $this->escape($group->group_name) ?><span class="pull-right"><?= $editLink ?></span></h2>
+ <table class="name-value-table">
+ <tr>
+ <th><?= $this->translate('Created at'); ?></th>
+ <td><?= $group->created_at === null ? '-' : $this->formatDateTime($group->created_at); ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Last modified'); ?></th>
+ <td><?= $group->last_modified === null ? '-' : $this->formatDateTime($group->last_modified); ?></td>
+ </tr>
+ </table>
+<?php if (! $this->compact): ?>
+ <h2><?= $this->translate('Members'); ?></h2>
+ <div class="sort-controls-container">
+ <?= $this->limiter; ?>
+ <?= $this->paginator; ?>
+ <?= $this->sortBox; ?>
+ </div>
+ <?= $this->filterEditor; ?>
+<?php endif ?>
+</div>
+<div class="content">
+<?php if ($extensible): ?>
+ <?= $this->qlink(
+ $this->translate('Add New Member'),
+ 'group/addmember',
+ array(
+ 'backend' => $backend->getName(),
+ 'group' => $group->group_name
+ ),
+ array(
+ 'icon' => 'plus',
+ 'class' => 'button-link'
+ )
+ ) ?>
+<?php endif ?>
+
+<?php if (! $members->hasResult()): ?>
+ <p><?= $this->translate('No group member found matching the filter'); ?></p>
+</div>
+<?php return; endif ?>
+
+ <table data-base-target="_next" class="table-row-selectable common-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Username'); ?></th>
+ <?php if (isset($removeForm)): ?>
+ <th><?= $this->translate('Remove'); ?></th>
+ <?php endif ?>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($members as $member): ?>
+ <tr>
+ <td>
+ <?php if (
+ $this->hasPermission('config/access-control/users')
+ && ($userBackend = $backend->getUserBackendName($member->user_name)) !== null
+ ): ?>
+ <?= $this->qlink($member->user_name, 'user/show', array(
+ 'backend' => $userBackend,
+ 'user' => $member->user_name
+ ), array(
+ 'title' => sprintf($this->translate('Show detailed information about %s'), $member->user_name)
+ )); ?>
+ <?php else: ?>
+ <?= $this->escape($member->user_name); ?>
+ <?php endif ?>
+ </td>
+ <?php if (isset($removeForm)): ?>
+ <td class="icon-col" data-base-target="_self">
+ <?php $removeForm->getElement('user_name')->setValue($member->user_name); echo $removeForm; ?>
+ </td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+</div>
diff --git a/application/views/scripts/iframe/index.phtml b/application/views/scripts/iframe/index.phtml
new file mode 100644
index 0000000..96e9de7
--- /dev/null
+++ b/application/views/scripts/iframe/index.phtml
@@ -0,0 +1,8 @@
+<?php if (! $compact): ?>
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<?php endif ?>
+<div class="iframe-container">
+ <iframe src="<?= $this->escape($url) ?>" frameborder="no"></iframe>
+</div>
diff --git a/application/views/scripts/index/welcome.phtml b/application/views/scripts/index/welcome.phtml
new file mode 100644
index 0000000..496dec9
--- /dev/null
+++ b/application/views/scripts/index/welcome.phtml
@@ -0,0 +1,2 @@
+<h1>Welcome to Icinga!</h1>
+You should install/configure some <a href="<?= $this->href('config/modules');?>">modules</a> now!
diff --git a/application/views/scripts/inline.phtml b/application/views/scripts/inline.phtml
new file mode 100644
index 0000000..2534d44
--- /dev/null
+++ b/application/views/scripts/inline.phtml
@@ -0,0 +1,2 @@
+<?= $this->layout()->content ?>
+
diff --git a/application/views/scripts/joystickPagination.phtml b/application/views/scripts/joystickPagination.phtml
new file mode 100644
index 0000000..a8c24c9
--- /dev/null
+++ b/application/views/scripts/joystickPagination.phtml
@@ -0,0 +1,162 @@
+<?php
+
+use Icinga\Web\Url;
+
+$showText = $this->translate('%s: Show %s %u to %u out of %u', 'pagination.joystick');
+$xAxisPages = $xAxisPaginator->getPages('all');
+$yAxisPages = $yAxisPaginator->getPages('all');
+
+$flipUrl = Url::fromRequest();
+if ($flipUrl->getParam('flipped')) {
+ $flipUrl->remove('flipped');
+} else {
+ $flipUrl->setParam('flipped');
+}
+if ($flipUrl->hasParam('page')) {
+ $flipUrl->setParam('page', implode(',', array_reverse(explode(',', $flipUrl->getParam('page')))));
+}
+if ($flipUrl->hasParam('limit')) {
+ $flipUrl->setParam('limit', implode(',', array_reverse(explode(',', $flipUrl->getParam('limit')))));
+}
+
+$totalYAxisPages = $yAxisPaginator->count();
+$currentYAxisPage = $yAxisPaginator->getCurrentPageNumber();
+$prevYAxisPage = $currentYAxisPage > 1 ? $currentYAxisPage - 1 : null;
+$nextYAxisPage = $currentYAxisPage < $totalYAxisPages ? $currentYAxisPage + 1 : null;
+
+$totalXAxisPages = $xAxisPaginator->count();
+$currentXAxisPage = $xAxisPaginator->getCurrentPageNumber();
+$prevXAxisPage = $currentXAxisPage > 1 ? $currentXAxisPage - 1 : null;
+$nextXAxisPage = $currentXAxisPage < $totalXAxisPages ? $currentXAxisPage + 1 : null;
+
+?>
+
+<table class="joystick-pagination">
+ <tbody>
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <?php if ($prevYAxisPage): ?>
+ <?= $this->qlink(
+ '',
+ Url::fromRequest(),
+ array(
+ 'page' => $currentXAxisPage . ',' . $prevYAxisPage
+ ),
+ array(
+ 'icon' => 'up-open',
+ 'data-base-target' => '_self',
+ 'title' => sprintf(
+ $showText,
+ $this->translate('Y-Axis', 'pagination.joystick'),
+ $this->translate('hosts', 'pagination.joystick'),
+ ($prevYAxisPage - 1) * $yAxisPages->itemCountPerPage + 1,
+ $prevYAxisPage * $yAxisPages->itemCountPerPage,
+ $yAxisPages->totalItemCount
+ )
+ )
+ ); ?>
+ <?php else: ?>
+ <?= $this->icon('up-open'); ?>
+ <?php endif ?>
+ </td>
+ <td>&nbsp;</td>
+ </tr>
+ <tr>
+ <td>
+ <?php if ($prevXAxisPage): ?>
+ <?= $this->qlink(
+ '',
+ Url::fromRequest(),
+ array(
+ 'page' => $prevXAxisPage . ',' . $currentYAxisPage
+ ),
+ array(
+ 'icon' => 'left-open',
+ 'data-base-target' => '_self',
+ 'title' => sprintf(
+ $showText,
+ $this->translate('X-Axis', 'pagination.joystick'),
+ $this->translate('services', 'pagination.joystick'),
+ ($prevXAxisPage - 1) * $xAxisPages->itemCountPerPage + 1,
+ $prevXAxisPage * $xAxisPages->itemCountPerPage,
+ $xAxisPages->totalItemCount
+ )
+ )
+ ); ?>
+ <?php else: ?>
+ <?= $this->icon('left-open'); ?>
+ <?php endif ?>
+ </td>
+ <?php if ($this->flippable): ?>
+ <td><?= $this->qlink(
+ '',
+ $flipUrl,
+ null,
+ array(
+ 'icon' => 'arrows-cw',
+ 'data-base-target' => '_self',
+ 'title' => $this->translate('Flip grid axes')
+ )
+ ) ?></td>
+ <?php else: ?>
+ <td>&nbsp;</td>
+ <?php endif ?>
+ <td>
+ <?php if ($nextXAxisPage): ?>
+ <?= $this->qlink(
+ '',
+ Url::fromRequest(),
+ array(
+ 'page' => $nextXAxisPage . ',' . $currentYAxisPage
+ ),
+ array(
+ 'icon' => 'right-open',
+ 'data-base-target' => '_self',
+ 'title' => sprintf(
+ $showText,
+ $this->translate('X-Axis', 'pagination.joystick'),
+ $this->translate('services', 'pagination.joystick'),
+ $currentXAxisPage * $xAxisPages->itemCountPerPage + 1,
+ $nextXAxisPage === $xAxisPages->last ? $xAxisPages->totalItemCount : $nextXAxisPage * $xAxisPages->itemCountPerPage,
+ $xAxisPages->totalItemCount
+ )
+ ),
+ false
+ ); ?>
+ <?php else: ?>
+ <?= $this->icon('right-open'); ?>
+ <?php endif ?>
+ </td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <?php if ($nextYAxisPage): ?>
+ <?= $this->qlink(
+ '',
+ Url::fromRequest(),
+ array(
+ 'page' => $currentXAxisPage . ',' . $nextYAxisPage
+ ),
+ array(
+ 'icon' => 'down-open',
+ 'data-base-target' => '_self',
+ 'title' => sprintf(
+ $showText,
+ $this->translate('Y-Axis', 'pagination.joystick'),
+ $this->translate('hosts', 'pagination.joystick'),
+ $currentYAxisPage * $yAxisPages->itemCountPerPage + 1,
+ $nextYAxisPage === $yAxisPages->last ? $yAxisPages->totalItemCount : $nextYAxisPage * $yAxisPages->itemCountPerPage,
+ $yAxisPages->totalItemCount
+ )
+ )
+ ); ?>
+ <?php else: ?>
+ <?= $this->icon('down-open'); ?>
+ <?php endif ?>
+ </td>
+ <td>&nbsp;</td>
+ </tr>
+ </tbody>
+</table>
diff --git a/application/views/scripts/layout/announcements.phtml b/application/views/scripts/layout/announcements.phtml
new file mode 100644
index 0000000..3be6b83
--- /dev/null
+++ b/application/views/scripts/layout/announcements.phtml
@@ -0,0 +1 @@
+<?= $this->widget('announcements') ?>
diff --git a/application/views/scripts/layout/menu.phtml b/application/views/scripts/layout/menu.phtml
new file mode 100644
index 0000000..dfb544d
--- /dev/null
+++ b/application/views/scripts/layout/menu.phtml
@@ -0,0 +1,20 @@
+<?php
+
+use Icinga\Web\Navigation\ConfigMenu;
+use Icinga\Web\Widget\SearchDashboard;
+
+$searchDashboard = new SearchDashboard();
+$searchDashboard->setUser($this->Auth()->getUser());
+
+if ($searchDashboard->search('dummy')->getPane('search')->hasDashlets()): ?>
+ <form action="<?= $this->href('search') ?>" method="get" role="search" class="search-control">
+ <input type="text" name="q" id="search" class="search search-input" required
+ placeholder="<?= $this->translate('Search') ?> &hellip;"
+ autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
+ <button class="search-reset icon-cancel" type="reset"></button>
+ </form>
+<?php endif; ?>
+<?= $menuRenderer->setCssClass('primary-nav')->setElementTag('nav')->setHeading(t('Navigation')); ?>
+<nav class="config-menu">
+ <?= new ConfigMenu() ?>
+</nav>
diff --git a/application/views/scripts/list/applicationlog.phtml b/application/views/scripts/list/applicationlog.phtml
new file mode 100644
index 0000000..ca41c33
--- /dev/null
+++ b/application/views/scripts/list/applicationlog.phtml
@@ -0,0 +1,29 @@
+<?php if (! $this->compact): ?>
+<div class="controls separated">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ </div>
+</div>
+<?php endif ?>
+<div class="content">
+<?php if ($this->logData !== null): ?>
+ <table class="action">
+ <tbody>
+ <?php foreach ($this->logData as $value): ?>
+ <?php $datetime = new Datetime($value->datetime) ?>
+ <tr>
+ <td>
+ <?= $this->escape($datetime->format('d.m. H:i')) ?><br />
+ <?= $this->escape($value->loglevel) ?>
+ </td>
+ <td>
+ <?= nl2br($this->escape($value->message), false) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+<?php endif ?>
+</div>
diff --git a/application/views/scripts/mixedPagination.phtml b/application/views/scripts/mixedPagination.phtml
new file mode 100644
index 0000000..e92a9c9
--- /dev/null
+++ b/application/views/scripts/mixedPagination.phtml
@@ -0,0 +1,79 @@
+<?php if ($this->pageCount <= 1) return; ?>
+<div class="pagination-control" role="navigation">
+ <h2 id="<?= $this->protectId('pagination') ?>" class="sr-only" tabindex="-1"><?= $this->translate('Pagination') ?></h2>
+ <ul class="nav tab-nav">
+ <?php if (isset($this->previous)): ?>
+ <?php $label = sprintf(
+ $this->translate('Show rows %u to %u of %u'),
+ ($this->current - 2) * $this->itemCountPerPage + 1,
+ ($this->current - 1) * $this->itemCountPerPage,
+ $this->totalItemCount
+ ) ?>
+ <li class="nav-item">
+ <a href="<?= $this->escape($this->url()->overwriteParams(array('page' => $this->previous))->getAbsoluteUrl()) ?>"
+ title="<?= $label ?>"
+ aria-label="<?= $label ?>"
+ class="previous-page">
+ <?= $this->icon('angle-double-left') ?>
+ </a>
+ </li>
+ <?php else: ?>
+ <li class="nav-item disabled" aria-hidden="true">
+ <span class="previous-page">
+ <span class="sr-only"><?= $this->translate('Previous page') ?></span>
+ <?= $this->icon('angle-double-left') ?>
+ </span>
+ </li>
+ <?php endif ?>
+ <?php foreach ($this->pagesInRange as $page): ?>
+ <?php if ($page === '...'): ?>
+ <li class="nav-item disabled">
+ <span>...</span>
+ </li>
+ <?php else: ?>
+ <?php
+ $end = $page * $this->itemCountPerPage;
+ if ($end > $this->totalItemCount) {
+ $end = $this->totalItemCount;
+ }
+ $label = sprintf(
+ $this->translate('Show rows %u to %u of %u'),
+ ($page - 1) * $this->itemCountPerPage + 1,
+ $end,
+ $this->totalItemCount
+ );
+ ?>
+ <li<?= $page === $this->current ? ' class="active nav-item"' : ' class="nav-item"' ?>>
+ <a href="<?= $this->escape($this->url()->overwriteParams(array('page' => $page))->getAbsoluteUrl()) ?>"
+ title="<?= $label ?>"
+ aria-label="<?= $label ?>">
+ <?= $page ?>
+ </a>
+ </li>
+ <?php endif ?>
+ <?php endforeach ?>
+ <?php if (isset($this->next)): ?>
+ <?php $label = sprintf(
+ $this->translate('Show rows %u to %u of %u'),
+ $this->current * $this->itemCountPerPage + 1,
+ ($this->current + 1) * $this->itemCountPerPage,
+ $this->totalItemCount
+ ) ?>
+ <li class="nav-item">
+ <a href="<?= $this->escape($this->url()->overwriteParams(array('page' => $this->next))->getAbsoluteUrl()) ?>"
+ title="<?= $label ?>"
+ aria-label="<?= $label ?>"
+ class="next-page">
+ <?= $this->icon('angle-double-right') ?>
+ </a>
+ </li>
+ <?php else: ?>
+ <li class="disabled nav-item" aria-hidden="true">
+ <span class="next-page">
+ <span class="sr-only"><?= $this->translate('Next page') ?></span>
+ <?= $this->icon('angle-double-right') ?>
+ </span>
+ </li>
+ <?php endif ?>
+ </ul>
+</div>
diff --git a/application/views/scripts/navigation/dashboard.phtml b/application/views/scripts/navigation/dashboard.phtml
new file mode 100644
index 0000000..f069882
--- /dev/null
+++ b/application/views/scripts/navigation/dashboard.phtml
@@ -0,0 +1,27 @@
+<?php
+
+use ipl\Web\Widget\Icon;
+
+?>
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?php foreach ($navigation as $item): /** @var \Icinga\Web\Navigation\NavigationItem $item */?>
+ <a class="dashboard-link" href="<?= $this->url($item->getUrl(), $item->getUrlParameters()) ?>"<?= $this->propertiesToString($item->getAttributes()) ?>>
+ <div class="link-icon">
+ <?php
+ if (substr($item->getUrl()->getPath(), 0, 9) === 'icingadb/') {
+ echo new Icon($item->getIcon(), [ 'aria-hidden' => 1]);
+ } else {
+ echo $this->icon($item->getIcon() ?: 'forward', null, array('aria-hidden' => true));
+ }
+ ?>
+ </div>
+ <div class="link-meta">
+ <div class="link-label"><?= $this->escape($item->getLabel()) ?></div>
+ <div class="link-description"><?= $this->escape($item->getDescription() ?: sprintf('Open %s', strtolower($item->getLabel()))) ?></div>
+ </div>
+ </a>
+ <?php endforeach ?>
+</div>
diff --git a/application/views/scripts/navigation/index.phtml b/application/views/scripts/navigation/index.phtml
new file mode 100644
index 0000000..bf08562
--- /dev/null
+++ b/application/views/scripts/navigation/index.phtml
@@ -0,0 +1,78 @@
+<?php if (! $this->compact): ?>
+<div class="controls separated">
+ <?= $this->tabs ?>
+ <div class="grid">
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+ <?= $this->qlink(
+ $this->translate('Create a New Navigation Item') ,
+ 'navigation/add',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new navigation item')
+ )
+ ) ?>
+<?php if (count($items) === 0): ?>
+ <p><?= $this->translate('You did not create any navigation item yet.') ?></p>
+</div>
+<?php return; endif ?>
+ <table class="table-row-selectable common-table" data-base-target="_next">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Navigation') ?></th>
+ <th><?= $this->translate('Type') ?></th>
+ <th><?= $this->translate('Shared') ?></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+<?php foreach ($items as $item): ?>
+ <tr>
+ <td>
+ <?= $this->qlink(
+ $item->name,
+ 'navigation/edit',
+ array(
+ 'name' => $item->name,
+ 'type' => $item->type
+ ),
+ array(
+ 'title' => sprintf($this->translate('Edit navigation item %s'), $item->name)
+ )
+ ) ?>
+ </td>
+ <td>
+ <?= $item->type && isset($types[$item->type])
+ ? $this->escape($types[$item->type])
+ : $this->escape($this->translate('Unknown')) ?>
+ </td>
+ <td class="icon-col">
+ <?= $item->owner ? $this->translate('Yes') : $this->translate('No') ?>
+ </td>
+ <td class="icon-col text-right">
+ <?= $this->qlink(
+ '',
+ 'navigation/remove',
+ array(
+ 'name' => $item->name,
+ 'type' => $item->type
+ ),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove navigation item %s'), $item->name)
+ )
+ ) ?>
+ </td>
+ </tr>
+<?php endforeach ?>
+ </tbody>
+ </table>
+</div>
diff --git a/application/views/scripts/navigation/shared.phtml b/application/views/scripts/navigation/shared.phtml
new file mode 100644
index 0000000..e9e9164
--- /dev/null
+++ b/application/views/scripts/navigation/shared.phtml
@@ -0,0 +1,68 @@
+<?php
+
+use Icinga\Web\Url;
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs; ?>
+ <div class="grid">
+ <?= $this->sortBox ?>
+ </div>
+</div>
+<?php endif ?>
+<div class="content" data-base-target="_next">
+<?php if (count($items) === 0): ?>
+ <p><?= $this->translate('There are currently no navigation items being shared'); ?></p>
+<?php else: ?>
+ <table class="table-row-selectable common-table">
+ <thead>
+ <th><?= $this->translate('Shared Navigation'); ?></th>
+ <th><?= $this->translate('Type'); ?></th>
+ <th><?= $this->translate('Owner'); ?></th>
+ <th><?= $this->translate('Unshare'); ?></th>
+ </thead>
+ <tbody>
+ <?php foreach ($items as $item): ?>
+ <tr>
+ <td><?= $this->qlink(
+ $item->name,
+ 'navigation/edit',
+ array(
+ 'name' => $item->name,
+ 'type' => $item->type,
+ 'owner' => $item->owner,
+ 'referrer' => 'shared'
+ ),
+ array(
+ 'title' => sprintf($this->translate('Edit shared navigation item %s'), $item->name)
+ )
+ ); ?></td>
+ <td><?= $item->type && isset($types[$item->type])
+ ? $this->escape($types[$item->type])
+ : $this->escape($this->translate('Unknown')); ?></td>
+ <td><?= $this->escape($item->owner); ?></td>
+ <?php if ($item->parent): ?>
+ <td><?= $this->icon(
+ 'block',
+ sprintf(
+ $this->translate(
+ 'This is a child of the navigation item %1$s. You can'
+ . ' only unshare this item by unsharing %1$s'
+ ),
+ $item->parent
+ )
+ ); ?></td>
+ <?php else: ?>
+ <td data-base-target="_self" class="remove-nav-item"><?= $removeForm
+ ->setDefault('name', $item->name)
+ ->setAction(Url::fromPath(
+ 'navigation/unshare',
+ array('type' => $item->type, 'owner' => $item->owner)
+ )); ?></td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+<?php endif ?>
+</div>
diff --git a/application/views/scripts/pivottablePagination.phtml b/application/views/scripts/pivottablePagination.phtml
new file mode 100644
index 0000000..ce18014
--- /dev/null
+++ b/application/views/scripts/pivottablePagination.phtml
@@ -0,0 +1,48 @@
+<?php
+
+use Icinga\Web\Url;
+
+if ($xAxisPaginator->count() <= 1 && $yAxisPaginator->count() <= 1) {
+ return; // Display this pagination only if there are multiple pages
+}
+
+$fromTo = t('%s: %d to %d of %d (on the %s-axis)');
+$xAxisPages = $xAxisPaginator->getPages('all');
+$yAxisPages = $yAxisPaginator->getPages('all');
+
+?>
+
+<div class="pivot-pagination">
+ <span><?= t('Navigation'); ?></span>
+ <table>
+ <tbody>
+<?php foreach ($yAxisPages->pagesInRange as $yAxisPage): ?>
+ <tr>
+<?php foreach ($xAxisPages->pagesInRange as $xAxisPage): ?>
+ <td<?= $xAxisPage === $xAxisPages->current && $yAxisPage === $yAxisPages->current ? ' class="active"' : ''; ?>>
+<?php if ($xAxisPage !== $xAxisPages->current || $yAxisPage !== $yAxisPages->current): ?>
+ <a href="<?= Url::fromRequest()->overwriteParams(
+ array('page' => $xAxisPage . ',' . $yAxisPage)
+ )->getAbsoluteUrl(); ?>" title="<?= sprintf(
+ $fromTo,
+ t('Hosts'),
+ ($yAxisPage - 1) * $yAxisPages->itemCountPerPage + 1,
+ $yAxisPage === $yAxisPages->last ? $yAxisPages->totalItemCount : $yAxisPage * $yAxisPages->itemCountPerPage,
+ $yAxisPages->totalItemCount,
+ 'y'
+ ) . '; ' . sprintf(
+ $fromTo,
+ t('Services'),
+ ($xAxisPage - 1) * $xAxisPages->itemCountPerPage + 1,
+ $xAxisPage === $xAxisPages->last ? $xAxisPages->totalItemCount : $xAxisPage * $xAxisPages->itemCountPerPage,
+ $xAxisPages->totalItemCount,
+ 'x'
+ ); ?>"></a>
+<?php endif ?>
+ </td>
+<?php endforeach ?>
+ </tr>
+<?php endforeach ?>
+ </tbody>
+ </table>
+</div>
diff --git a/application/views/scripts/role/list.phtml b/application/views/scripts/role/list.phtml
new file mode 100644
index 0000000..352e3e2
--- /dev/null
+++ b/application/views/scripts/role/list.phtml
@@ -0,0 +1,65 @@
+<div class="controls separated">
+ <?= $tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<div class="content">
+ <?= $this->qlink(
+ $this->translate('Create a New Role') ,
+ 'role/add',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new role')
+ )
+ ) ?>
+<?php /** @var \Icinga\Application\Config $roles */ if (! $roles->hasResult()): ?>
+ <p><?= $this->translate('No roles found.') ?></p>
+<?php return; endif ?>
+ <table class="table-row-selectable common-table" data-base-target="_next">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Name') ?></th>
+ <th><?= $this->translate('Users') ?></th>
+ <th><?= $this->translate('Groups') ?></th>
+ <th><?= $this->translate('Inherits From') ?></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+<?php foreach ($roles as $name => $role): /** @var object $role */ ?>
+ <tr>
+ <td>
+ <?= $this->qlink(
+ $name,
+ 'role/edit',
+ array('role' => $name),
+ array('title' => sprintf($this->translate('Edit role %s'), $name))
+ ) ?>
+ </td>
+ <td><?= $this->escape($role->users) ?></td>
+ <td><?= $this->escape($role->groups) ?></td>
+ <td><?= $this->escape($role->parent) ?></td>
+ <td class="icon-col text-right">
+ <?= $this->qlink(
+ '',
+ 'role/remove',
+ array('role' => $name),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove role %s'), $name)
+ )
+ ) ?>
+ </td>
+ </tr>
+<?php endforeach ?>
+ </tbody>
+ </table>
+</div>
diff --git a/application/views/scripts/search/hint.phtml b/application/views/scripts/search/hint.phtml
new file mode 100644
index 0000000..d54c0b2
--- /dev/null
+++ b/application/views/scripts/search/hint.phtml
@@ -0,0 +1,8 @@
+<div class="content">
+<h1><?= $this->translate("I'm ready to search, waiting for your input") ?></h1>
+<p><strong><?= $this->translate('Hint') ?>: </strong><?= $this->translate(
+ 'Please use the asterisk (*) as a placeholder for wildcard searches.'
+ . " For convenience I'll always add a wildcard in front and after your"
+ . ' search string.'
+) ?></p>
+</div>
diff --git a/application/views/scripts/search/index.phtml b/application/views/scripts/search/index.phtml
new file mode 100644
index 0000000..321597e
--- /dev/null
+++ b/application/views/scripts/search/index.phtml
@@ -0,0 +1,7 @@
+<div class="controls">
+<?= $this->dashboard->getTabs() ?>
+</div>
+
+<div class="content dashboard">
+<?= $this->dashboard ?>
+</div>
diff --git a/application/views/scripts/showConfiguration.phtml b/application/views/scripts/showConfiguration.phtml
new file mode 100644
index 0000000..682b349
--- /dev/null
+++ b/application/views/scripts/showConfiguration.phtml
@@ -0,0 +1,27 @@
+<div>
+ <h4><?= $this->translate('Saving Configuration Failed'); ?></h4>
+ <p>
+ <?= sprintf(
+ $this->translate('The file %s couldn\'t be stored. (Error: "%s")'),
+ $this->escape($filePath),
+ $this->escape($errorMessage)
+ ); ?>
+ <br>
+ <?= $this->translate('This could have one or more of the following reasons:'); ?>
+ </p>
+ <ul>
+ <li><?= $this->translate('You don\'t have file-system permissions to write to the file'); ?></li>
+ <li><?= $this->translate('Something went wrong while writing the file'); ?></li>
+ <li><?= $this->translate('There\'s an application error preventing you from persisting the configuration'); ?></li>
+ </ul>
+</div>
+<p>
+ <?= $this->translate('Details can be found in the application log. (If you don\'t have access to this log, call your administrator in this case)'); ?>
+ <br>
+ <?= $this->translate('In case you can access the file by yourself, you can open it and insert the config manually:'); ?>
+</p>
+<p>
+ <pre>
+ <code><?= $this->escape($configString); ?></code>
+ </pre>
+</p>
diff --git a/application/views/scripts/simple-form.phtml b/application/views/scripts/simple-form.phtml
new file mode 100644
index 0000000..9bcba74
--- /dev/null
+++ b/application/views/scripts/simple-form.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <?= $form->create()->setTitle(null) // @TODO(el): create() has to be called because the UserForm is setting the title there ?>
+</div>
diff --git a/application/views/scripts/user/form.phtml b/application/views/scripts/user/form.phtml
new file mode 100644
index 0000000..cbf0659
--- /dev/null
+++ b/application/views/scripts/user/form.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= $tabs->showOnlyCloseButton(); ?>
+</div>
+<div class="content">
+ <?= $form; ?>
+</div> \ No newline at end of file
diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml
new file mode 100644
index 0000000..bdb5f1a
--- /dev/null
+++ b/application/views/scripts/user/list.phtml
@@ -0,0 +1,90 @@
+<?php
+
+use Icinga\Data\Extensible;
+use Icinga\Data\Reducible;
+
+if (! $this->compact): ?>
+<div class="controls separated">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->backendSelection ?>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php
+
+if (! isset($backend)) {
+ echo $this->translate('No backend found which is able to list users') . '</div>';
+ return;
+} else {
+ $extensible = $this->hasPermission('config/access-control/users') && $backend instanceof Extensible;
+ $reducible = $this->hasPermission('config/access-control/users') && $backend instanceof Reducible;
+}
+?>
+
+<?php if ($extensible): ?>
+ <?= $this->qlink(
+ $this->translate('Add a New User') ,
+ 'user/add',
+ array('backend' => $backend->getName()),
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus'
+ )
+ ) ?>
+<?php endif ?>
+
+<?php if (! $users->hasResult()): ?>
+ <p><?= $this->translate('No users found matching the filter') ?></p>
+</div>
+<?php return; endif ?>
+
+ <table data-base-target="_next" class="table-row-selectable common-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Username') ?></th>
+ <?php if ($reducible): ?>
+ <th><?= $this->translate('Remove') ?></th>
+ <?php endif ?>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($users as $user): ?>
+ <tr>
+ <td><?= $this->qlink(
+ $user->user_name,
+ 'user/show',
+ array(
+ 'backend' => $backend->getName(),
+ 'user' => $user->user_name
+ ),
+ array(
+ 'title' => sprintf($this->translate('Show detailed information about %s'), $user->user_name)
+ )
+ ) ?></td>
+ <?php if ($reducible): ?>
+ <td class="icon-col"><?= $this->qlink(
+ null,
+ 'user/remove',
+ array(
+ 'backend' => $backend->getName(),
+ 'user' => $user->user_name
+ ),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove user %s'), $user->user_name)
+ )
+ ) ?></td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+</div>
diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml
new file mode 100644
index 0000000..b19c15a
--- /dev/null
+++ b/application/views/scripts/user/show.phtml
@@ -0,0 +1,138 @@
+<?php
+
+use Icinga\Data\Updatable;
+use Icinga\Data\Reducible;
+use Icinga\Data\Selectable;
+
+?>
+<div class="controls separated">
+<?php if (! $this->compact): ?>
+ <?= $tabs; ?>
+<?php endif ?>
+ <h2><?= $this->escape($user->user_name) ?></h2>
+ <?php
+ if ($this->hasPermission('config/access-control/users') && $backend instanceof Updatable) {
+ echo $this->qlink(
+ $this->translate('Edit User'),
+ 'user/edit',
+ array(
+ 'backend' => $backend->getName(),
+ 'user' => $user->user_name
+ ),
+ array(
+ 'class' => 'button-link',
+ 'icon' => 'edit',
+ 'title' => sprintf($this->translate('Edit user %s'), $user->user_name)
+ )
+ );
+ }
+ ?>
+ <table class="name-value-table">
+ <tr>
+ <th><?= $this->translate('State'); ?></th>
+ <td><?= $user->is_active === null ? '-' : ($user->is_active ? $this->translate('Active') : $this->translate('Inactive')); ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Created at'); ?></th>
+ <td><?= $user->created_at === null ? '-' : $this->formatDateTime($user->created_at); ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Last modified'); ?></th>
+ <td><?= $user->last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Role Memberships'); ?></th>
+ <td>
+ <?php $roles = $userObj->getRoles(); ?>
+ <?php if (! empty($roles)): ?>
+ <ul class="role-memberships">
+ <?php foreach($roles as $role): ?>
+ <li>
+ <?php if ($this->allowedToEditRoles): ?>
+ <?= $this->qlink(
+ $role->getName(),
+ 'role/edit',
+ ['role' => $role->getName()],
+ ['title' => sprintf($this->translate('Edit role %s'), $role->getName())]
+ );
+ $role === end($roles) ? print '' : print ', '; ?>
+ <?php else: ?>
+ <?= $role->getName() ?>
+ <?php endif ?>
+ </li>
+ <?php endforeach ?>
+ </ul>
+ <?php else: ?>
+ <p><?= $this->translate('No memberships found'); ?></p>
+ <?php endif ?>
+ </td>
+ </tr>
+ </table>
+<?php if (! $this->compact): ?>
+ <h2><?= $this->translate('Group Memberships'); ?></h2>
+ <div class="sort-controls-container">
+ <?= $this->limiter; ?>
+ <?= $this->paginator; ?>
+ <?= $this->sortBox; ?>
+ </div>
+ <?= $this->filterEditor; ?>
+<?php endif ?>
+</div>
+<div class="content">
+<?php if ($showCreateMembershipLink): ?>
+ <?= $this->qlink(
+ $this->translate('Create New Membership'),
+ 'user/createmembership',
+ array(
+ 'backend' => $backend->getName(),
+ 'user' => $user->user_name
+ ),
+ array(
+ 'icon' => 'plus',
+ 'class' => 'button-link'
+ )
+ ) ?>
+<?php endif ?>
+
+<?php if (! $memberships->hasResult()): ?>
+ <p><?= $this->translate('No memberships found matching the filter'); ?></p>
+</div>
+<?php return; endif ?>
+
+ <table data-base-target="_next" class="table-row-selectable common-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Group'); ?></th>
+ <th><?= $this->translate('Cancel', 'group.membership'); ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($memberships as $membership): ?>
+ <tr>
+ <td>
+ <?php if ($this->hasPermission('config/access-control/groups') && $membership->backend instanceof Selectable): ?>
+ <?= $this->qlink($membership->group_name, 'group/show', array(
+ 'backend' => $membership->backend->getName(),
+ 'group' => $membership->group_name
+ ), array(
+ 'title' => sprintf($this->translate('Show detailed information for group %s'), $membership->group_name)
+ )); ?>
+ <?php else: ?>
+ <?= $this->escape($membership->group_name); ?>
+ <?php endif ?>
+ </td>
+ <td class="icon-col" data-base-target="_self">
+ <?php if (isset($removeForm) && $membership->backend instanceof Reducible): ?>
+ <?= $removeForm->setAction($this->url('group/removemember', array(
+ 'backend' => $membership->backend->getName(),
+ 'group' => $membership->group_name
+ ))); ?>
+ <?php else: ?>
+ -
+ <?php endif ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+</div>
diff --git a/bin/icingacli b/bin/icingacli
new file mode 100755
index 0000000..056d521
--- /dev/null
+++ b/bin/icingacli
@@ -0,0 +1,7 @@
+#!/usr/bin/env php
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+require_once dirname(__DIR__) . '/library/Icinga/Application/Cli.php';
+
+Icinga\Application\Cli::start()->dispatch();
diff --git a/doc/01-About.md b/doc/01-About.md
new file mode 100644
index 0000000..7df51d1
--- /dev/null
+++ b/doc/01-About.md
@@ -0,0 +1,93 @@
+# About Icinga Web 2 <a id="about"></a>
+
+Icinga Web 2 is a powerful PHP framework for web applications that comes in a clean and reduced design.
+It's fast, responsive, accessible and easily extensible with modules.
+
+## Installation <a id="about-installation"></a>
+
+Icinga Web 2 can be installed easily from packages from the official package repositories.
+Setting it up is also easy with the web based setup wizard.
+
+See [here](02-Installation.md#installation) for more information about the installation.
+
+## Configuration <a id="about-configuration"></a>
+
+Icinga Web 2 can be configured via the user interface and .ini files.
+
+See [here](03-Configuration.md#configuration) for more information about the configuration.
+
+## Authentication <a id="about-authentication"></a>
+
+With Icinga Web 2 you can authenticate against relational databases, LDAP and more.
+These authentication methods can be easily configured (via the corresponding .ini file).
+
+See [here](05-Authentication.md#authentication) for more information about
+the different authentication methods available and how to configure them.
+
+## Authorization <a id="about-authorization"></a>
+
+In Icinga Web 2 there are permissions and restrictions to allow and deny (respectively)
+roles to view or to do certain things.
+These roles can be assigned to users and groups.
+
+See [here](06-Security.md#security) for more information about authorization
+and how to configure roles.
+
+## User preferences <a id="about-preferences"></a>
+
+Besides the global configuration each user has individual configuration options
+like the interface's language or the current timezone.
+They can be stored either in a database or in .ini files.
+
+See [here](07-Preferences.md#preferences) for more information about a user's preferences
+and how to configure their storage type.
+
+## Modules
+
+Modules extend Icinga Web 2 with additional functionality. They allow the integration of
+capabilities into existing views and even other modules. Be it a graph provider such as
+[Graphite](https://github.com/Icinga/icingaweb2-module-graphite) or a UI for the Icinga 2
+configuration like the [Director](https://github.com/Icinga/icingaweb2-module-director).
+
+See [here](08-Modules.md#modules) for information on how to install and configure modules.
+
+### The monitoring module <a id="about-monitoring"></a>
+
+> **Note for Icinga DB Users**
+>
+> This module is only for the IDO backend. Use [Icinga DB Web](https://github.com/Icinga/icingadb-web) instead.
+
+This is the core module for most Icinga Web 2 users.
+
+It provides an intuitive user interface for monitoring with Icinga 2.
+There are lots of list and detail views (e.g. for hosts and services)
+you can sort and filter depending on what you want to see.
+
+You can also control the monitoring process itself by sending external commands to Icinga.
+Most such actions (like rescheduling a check) can be done with just a single click in the UI.
+
+More details about this module can be found in [this chapter](../modules/monitoring/doc/01-About.md#monitoring-module-about).
+
+### Documentation <a id="about-documentation"></a>
+
+With the documentation module you can read the documentation of the framework (and any module) directly in the user interface.
+
+The module can also export the documentation to PDF.
+
+More details about this module can be found in [this chapter](../modules/doc/doc/01-About.md#doc-module-about).
+
+### Translation <a id="about-translation"></a>
+
+Icinga Web 2 and all modules by Icinga utilize gettext to provide translations into other languages from the default
+English (en_US). However, the actual language specific files (locales) are not separately included in every project.
+
+Icinga uses a central repository to manage locales: https://github.com/Icinga/L10n
+
+If you want to provide or update a translation for your own language, please head over there where you will find
+[instructions](https://github.com/Icinga/L10n/blob/main/CONTRIBUTING.md) on how to contribute.
+
+## Accessibility <a id="about-accessibility"></a>
+
+In the Icinga Web 2 interface even the blind can see -
+easy navigation with a screen reader and specific themes for different kinds of vision deficiencies
+make it possible for everyone to monitor their systems without impairments.
diff --git a/doc/02-Installation.md b/doc/02-Installation.md
new file mode 100644
index 0000000..6bf9ee0
--- /dev/null
+++ b/doc/02-Installation.md
@@ -0,0 +1,537 @@
+<!-- {% if index %} -->
+# Installation <a id="installation"></a>
+
+The preferred way of installing Icinga Web 2 is to use the official package repositories depending on which operating
+system and distribution you are running.
+
+Please follow the steps listed for your operating system. Packages for distributions other than the ones
+listed here may also be available. Please refer to [icinga.com/get-started/download](https://icinga.com/get-started/download/#community)
+for a full list of available community repositories.
+
+## Browser Support
+
+Icinga Web 2 and modules made by Icinga don't require a particular browser or set of browsers. The
+vendor of the browser in question doesn't matter much. However, the features a browser supports do.
+
+This generally applies to CSS and Javascript features. Since there a plethora of features in each
+category which Icinga Web 2 and modules may require, we will only mention the most prominent feature
+or sub-category here:
+
+* For CSS this is [the flexible box layout module](https://caniuse.com/flexbox)
+* For Javascript it is [the ECMAScript 2015 specification](https://caniuse.com/es6)
+
+If your desired browser and its version is showing up in green when visiting the respective link,
+it's probably okay to use it for Icinga Web 2.
+
+## Upgrade <a id="upgrade"></a>
+
+In case you are upgrading from an older version of Icinga Web 2
+please make sure to read the [upgrading](80-Upgrading.md#upgrading) section
+thoroughly.
+<!-- {% elif not from_source %} -->
+
+## Installation Requirements <a id="installation-requirements"></a>
+
+* [Icinga 2](https://icinga.com/docs/icinga-2) and [Icinga DB](https://icinga.com/docs/icinga-db) to
+ monitor your infrastructure
+* A web server, e.g. Apache or Nginx
+* PHP version ≥ 7.2
+
+### Optional Requirements
+
+* The [pdfexport](https://github.com/Icinga/icingaweb2-module-pdfexport) module (≥0.10) is required for the
+ export to PDF
+* LDAP PHP library when using Active Directory or LDAP for authentication
+
+## Add Icinga Package Repository <a id="add-icinga-package-repository"></a>
+
+You need to add the Icinga repository to your package management configuration for installing Icinga Web 2.
+If you've already configured your OS to use the Icinga repository for installing Icinga 2, you may skip this step.
+
+<!-- {% if debian %} -->
+### Debian Repository <a id="ubuntu-repository"></a>
+
+```bash
+apt-get update
+apt-get -y install apt-transport-https wget gnupg
+
+wget -O - https://packages.icinga.com/icinga.key | apt-key add -
+
+DIST=$(awk -F"[)(]+" '/VERSION=/ {print $2}' /etc/os-release); \
+ echo "deb https://packages.icinga.com/debian icinga-${DIST} main" > \
+ /etc/apt/sources.list.d/${DIST}-icinga.list
+ echo "deb-src https://packages.icinga.com/debian icinga-${DIST} main" >> \
+ /etc/apt/sources.list.d/${DIST}-icinga.list
+
+apt-get update
+```
+<!-- {% endif %} -->
+
+<!-- {% if ubuntu %} -->
+### Ubuntu Repository <a id="ubuntu-repository"></a>
+
+```bash
+apt-get update
+apt-get -y install apt-transport-https wget gnupg
+
+wget -O - https://packages.icinga.com/icinga.key | apt-key add -
+
+. /etc/os-release; if [ ! -z ${UBUNTU_CODENAME+x} ]; then DIST="${UBUNTU_CODENAME}"; else DIST="$(lsb_release -c| awk '{print $2}')"; fi; \
+ echo "deb https://packages.icinga.com/ubuntu icinga-${DIST} main" > \
+ /etc/apt/sources.list.d/${DIST}-icinga.list
+ echo "deb-src https://packages.icinga.com/ubuntu icinga-${DIST} main" >> \
+ /etc/apt/sources.list.d/${DIST}-icinga.list
+
+apt-get update
+```
+<!-- {% endif %} -->
+
+<!-- {% if centos %} -->
+### CentOS Repository <a id="centos-repository"></a>
+
+```bash
+rpm --import https://packages.icinga.com/icinga.key
+wget https://packages.icinga.com/centos/ICINGA-release.repo -O /etc/yum.repos.d/ICINGA-release.repo
+```
+
+The packages for CentOS depend on other packages which are distributed
+as part of the [EPEL repository](https://fedoraproject.org/wiki/EPEL).
+
+CentOS 7:
+
+```bash
+yum install epel-release
+```
+
+Since Icinga Web v2.5 we also require a **newer PHP version** than what is available
+in RedHat itself. You need to enable the SCL repository, so that the dependencies
+can pull in the newer PHP.
+
+```bash
+yum install centos-release-scl
+```
+<!-- {% endif %} -->
+
+<!-- {% if rhel %} -->
+### RHEL Repository <a id="rhel-repository"></a>
+
+!!! info
+
+ A paid repository subscription is required for RHEL repositories. Get more information on
+ [icinga.com/subscription](https://icinga.com/subscription)
+
+ Don't forget to fill in the username and password section with your credentials in the local .repo file.
+
+```bash
+rpm --import https://packages.icinga.com/icinga.key
+wget https://packages.icinga.com/subscription/rhel/ICINGA-release.repo -O /etc/yum.repos.d/ICINGA-release.repo
+```
+
+If you are using RHEL you need to additionally enable the `optional` and `codeready-builder`
+repository before installing the [EPEL rpm package](https://fedoraproject.org/wiki/EPEL#How_can_I_use_these_extra_packages.3F).
+
+#### RHEL 8
+
+```bash
+ARCH=$( /bin/arch )
+
+subscription-manager repos --enable rhel-8-server-optional-rpms
+subscription-manager repos --enable "codeready-builder-for-rhel-8-${ARCH}-rpms"
+
+dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
+```
+
+#### RHEL 7
+Since Icinga Web v2.5 we also require a **newer PHP version** than what is available
+in RedHat itself. You need to enable the SCL repository, so that the dependencies
+can pull in the newer PHP.
+
+```bash
+subscription-manager repos --enable rhel-7-server-optional-rpms
+subscription-manager repos --enable rhel-server-rhscl-7-rpms
+
+yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
+```
+<!-- {% endif %} -->
+
+<!-- {% if sles %} -->
+### SLES Repository <a id="rhel-repository"></a>
+
+!!! info
+
+ A paid repository subscription is required for RHEL repositories. Get more information on
+ [icinga.com/subscription](https://icinga.com/subscription)
+
+ Don't forget to fill in the username and password section with your credentials in the local .repo file.
+
+```bash
+rpm --import https://packages.icinga.com/icinga.key
+
+zypper ar https://packages.icinga.com/subscription/sles/ICINGA-release.repo
+zypper ref
+```
+
+You need to additionally enable a couple of SLES repositories to fulfill dependencies:
+
+```bash
+source /etc/os-release
+
+SUSEConnect -p sle-module-desktop-applications/$VERSION_ID/x86_64
+SUSEConnect -p sle-module-development-tools/$VERSION_ID/x86_64
+SUSEConnect -p sle-module-web-scripting/$VERSION_ID/x86_64
+SUSEConnect -p PackageHub/$VERSION_ID/x86_64
+```
+<!-- {% endif %} -->
+
+<!-- {% if amazon_linux %} -->
+### Amazon Linux 2 Repository <a id="amazon-linux-2-repository"></a>
+
+!!! info
+
+ A paid repository subscription is required for Amazon Linux repositories. Get more information on
+ [icinga.com/subscription](https://icinga.com/subscription)
+
+ Don't forget to fill in the username and password section with your credentials in the local .repo file.
+
+```bash
+rpm --import https://packages.icinga.com/icinga.key
+wget https://packages.icinga.com/subscription/amazon/ICINGA-release.repo -O /etc/yum.repos.d/ICINGA-release.repo
+```
+
+You need to install and enable the `amazon-linux-extras` repository to meet the requirements of
+Icinga Web 2 on Amazon Linux 2:
+
+```bash
+yum install -y amazon-linux-extras
+
+amazon-linux-extras enable php8.0
+```
+<!-- {% endif %} -->
+
+## Install Icinga Web 2 <a id="install-icingaweb2"></a>
+
+You can install Icinga Web 2 by using your distribution's package manager to install the `icingaweb2` package.
+The additional package `icingacli` is necessary to follow further steps in this guide.
+
+<!-- {% if debian %} -->
+<!-- {% if not icingaDocs %} -->
+#### Debian
+<!-- {% endif %} -->
+```bash
+apt-get install icingaweb2 icingacli
+```
+<!-- {% endif %} -->
+
+<!-- {% if ubuntu %} -->
+<!-- {% if not icingaDocs %} -->
+#### Ubuntu
+<!-- {% endif %} -->
+```bash
+apt-get install icingaweb2 libapache2-mod-php icingacli
+```
+
+The additional package `libapache2-mod-php` is necessary on Ubuntu to automatically
+install a web server and PHP and make Icinga Web 2 work out-of-the-box.
+<!-- {% endif %} -->
+
+<!-- {% if centos or rhel or amazon_linux %} -->
+!!! tip
+
+ If you have [SELinux](90-SELinux.md) enabled, the package `icingaweb2-selinux` is also required.
+<!-- {% endif %} -->
+
+<!-- {% if centos %} -->
+<!-- {% if not icingaDocs %} -->
+#### CentOS
+<!-- {% endif %} -->
+```
+yum install icingaweb2 icingacli
+```
+<!-- {% endif %} -->
+
+<!-- {% if rhel %} -->
+<!-- {% if not icingaDocs %} -->
+#### RHEL
+<!-- {% endif %} -->
+#### RHEL 8
+```bash
+dnf install icingaweb2 icingacli
+```
+
+#### RHEL 7
+```bash
+yum install icingaweb2 icingacli
+```
+<!-- {% endif %} -->
+
+<!-- {% if sles %} -->
+<!-- {% if not icingaDocs %} -->
+#### SLES
+<!-- {% endif %} -->
+```bash
+zypper install icingaweb2 icingacli
+```
+<!-- {% endif %} -->
+
+<!-- {% if amazon_linux %} -->
+<!-- {% if not icingaDocs %} -->
+#### Amazon Linux 2
+<!-- {% endif %} -->
+```bash
+yum install icingaweb2 icingacli
+```
+<!-- {% endif %} -->
+
+## Install the Web Server <a id="install-the-web-server"></a>
+
+Ensure that you have a web server with PHP installed before proceeding,
+such as Apache or Nginx with PHP version ≥ 7.2. Depending on your operating system,
+you may need to install and configure the web server separately.
+An Apache configuration file to serve Icinga Web is already installed.
+If you want to use Nginx, you must manually create a configuration file using the following command.
+Save the output as a new file in the web server configuration directory:
+
+```bash
+icingacli setup config webserver nginx --document-root /usr/share/icingaweb2/public
+```
+
+## Prepare Web Setup <a id="prepare-web-setup-from-package"></a>
+
+You can set up Icinga Web 2 quickly and easily with the Icinga Web 2 setup wizard which is available the first time
+you visit Icinga Web 2 in your browser. When using the web setup you are required to authenticate using a token.
+In order to generate a token use the `icingacli`:
+
+```bash
+icingacli setup token create
+```
+
+In case you do not remember the token you can show it using the `icingacli`:
+
+```bash
+icingacli setup token show
+```
+
+<!-- {% if debian or ubuntu %} -->
+You need to manually create a database and a database user prior to starting the web wizard.
+This is due to local security restrictions whereas the web wizard cannot create a database/user through
+a local unix domain socket.
+
+```bash
+MariaDB [mysql]> CREATE DATABASE icingaweb2;
+
+MariaDB [mysql]> GRANT ALL ON icingaweb2.* TO icingaweb2@localhost IDENTIFIED BY 'CHANGEME';
+```
+
+You may also create a separate administrative account with all privileges instead.
+
+!!! note
+
+ This is only required if you are using a local database as authentication type.
+<!-- {% endif %} -->
+
+### Start Web Setup <a id="start-web-setup-from-package"></a>
+
+Finally visit Icinga Web 2 in your browser to access the setup wizard and complete the installation:
+`/icingaweb2/setup`.
+
+<!-- {% if debian or ubuntu %} -->
+!!! hint
+
+ Use the same database, user and password details created above when asked.
+<!-- {% endif %} -->
+
+The setup wizard automatically detects the required packages. In case one of them is missing,
+e.g. a PHP module, please install the package, restart your webserver and reload the setup page.
+
+<!-- {% if sles %} -->
+!!! note
+
+ If you're using php-fpm on SLES 15 SP2 onwards, `/etc/icingaweb2` may not be writable.
+ That's because the default systemd unit file for php-fpm has `ProtectSystem=full`
+ enabled. You want to lookup/add the systemd setting `ReadWritePaths=` in this case and
+ add `/etc/icingaweb2` to it. Alternatively you can also define a different configuration
+ directory using the environment variable `ICINGAWEB_CONFIGDIR`.
+<!-- {% endif %} -->
+
+<!-- {% if centos or rhel or amazon_linux %} -->
+!!! note
+
+ If you have SELinux enabled, please ensure to either have the selinux package for Icinga Web 2 installed, or disable it.
+<!-- {% endif %} -->
+
+<!-- {% else %} --><!-- {# end from_source elif #} -->
+<!-- {% if not icingaDocs %} -->
+## Installing Icinga Web 2 from Source <a id="installing-from-source"></a>
+<!-- {% endif %} -->
+
+Although the preferred way of installing Icinga Web 2 is to use packages, it is also possible to install Icinga Web 2
+directly from source.
+
+### Getting the Source <a id="getting-the-source"></a>
+
+First of all, you need to download the sources.
+
+Git clone:
+
+```bash
+cd /usr/share/
+git clone https://github.com/Icinga/icingaweb2.git icingaweb2
+```
+
+Tarball download (latest [release](https://github.com/Icinga/icingaweb2/releases/latest)):
+
+```bash
+cd /usr/share
+wget https://github.com/Icinga/icingaweb2/archive/v2.9.5.zip
+unzip v2.9.5.zip
+mv icingaweb2-2.9.5 icingaweb2
+```
+
+### Installing Requirements from Source <a id="installing-from-source-requirements"></a>
+
+You will need to install certain dependencies depending on your setup:
+
+* [Icinga 2](https://github.com/Icinga/icinga2) and [Icinga DB](https://github.com/Icinga/icingadb) to
+ monitor your infrastructure
+* A web server, e.g. Apache or Nginx
+* PHP version ≥ 7.2
+* [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (≥ 0.13)
+* [Icinga PHP Thirdparty](https://github.com/Icinga/icinga-php-thirdparty) (≥ 0.12)
+* The following PHP modules must be installed: cURL, json, gettext, fileinfo, intl, dom, OpenSSL and xml
+* The [pdfexport](https://github.com/Icinga/icingaweb2-module-pdfexport) module (≥0.10) is required for the
+ export to PDF
+* LDAP PHP library when using Active Directory or LDAP for authentication
+* MySQL or PostgreSQL PHP libraries
+
+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.
+
+Example for RHEL/CentOS/Fedora:
+
+```bash
+yum install httpd mysql-server
+yum install php php-gd php-intl
+```
+
+The setup wizard will check the pre-requisites later on.
+
+
+### Installing Icinga Web 2 <a id="installing-from-source-example"></a>
+
+Choose a target directory and move Icinga Web 2 there.
+
+```bash
+mv icingaweb2 /usr/share/icingaweb2
+```
+
+### Configuring the Web Server <a id="configuring-web-server"></a>
+
+Use `icingacli` to generate web server configuration for either Apache or nginx.
+
+**Apache**:
+```bash
+./bin/icingacli setup config webserver apache --document-root /usr/share/icingaweb2/public
+```
+
+**nginx**:
+```bash
+./bin/icingacli setup config webserver nginx --document-root /usr/share/icingaweb2/public
+```
+
+Save the output as new file in your webserver's configuration directory.
+
+Example for Apache on RHEL or CentOS:
+```bash
+./bin/icingacli setup config webserver apache --document-root /usr/share/icingaweb2/public > /etc/httpd/conf.d/icingaweb2.conf
+```
+
+Example for Apache on SUSE:
+```bash
+./bin/icingacli setup config webserver apache --document-root /usr/share/icingaweb2/public > /etc/apache2/conf.d/icingaweb2.conf
+```
+
+Example for Apache on Debian Jessie:
+```bash
+./bin/icingacli setup config webserver apache --document-root /usr/share/icingaweb2/public > /etc/apache2/conf-available/icingaweb2.conf
+a2enconf icingaweb2
+```
+
+Example for Apache on Alpine Linux:
+```bash
+icingacli setup config webserver apache --document-root /usr/share/webapps/icingaweb2/public > /etc/apache2/conf.d/icingaweb2.conf
+```
+### Preparing Icinga Web 2 Setup <a id="preparing-web-setup-from-source"></a>
+
+You can set up Icinga Web 2 quickly and easily with the Icinga Web 2 setup wizard which is available the first time
+you visit Icinga Web 2 in your browser. Please follow the steps listed below for preparing the web setup.
+
+Because both web and CLI must have access to configuration and logs, permissions will be managed using a special
+system group. The web server user and CLI user have to be added to this system group.
+
+Add the system group `icingaweb2` in the first place.
+
+**Fedora, RHEL, CentOS, SLES and OpenSUSE**:
+```bash
+groupadd -r icingaweb2
+```
+
+**Debian and Ubuntu**:
+```bash
+addgroup --system icingaweb2
+```
+
+Add your web server's user to the system group `icingaweb2`
+and restart the web server:
+
+**Fedora, RHEL and CentOS**:
+```bash
+usermod -a -G icingaweb2 apache
+service httpd restart
+```
+
+**SLES and OpenSUSE**:
+```bash
+usermod -A icingaweb2 wwwrun
+service apache2 restart
+```
+
+**Debian and Ubuntu**:
+```bash
+usermod -a -G icingaweb2 www-data
+service apache2 restart
+```
+
+**Alpine Linux**:
+```bash
+gpasswd -a apache icingaweb2
+rc-service apache2 restart
+```
+
+
+Use `icingacli` to create the configuration directory which defaults to **/etc/icingaweb2**:
+```bash
+./bin/icingacli setup config directory
+```
+
+
+When using the web setup you are required to authenticate using a token. In order to generate a token use the
+`icingacli`:
+```bash
+./bin/icingacli setup token create
+```
+
+In case you do not remember the token you can show it using the `icingacli`:
+```bash
+./bin/icingacli setup token show
+```
+
+### Icinga Web 2 Setup Wizard <a id="web-setup-from-source-wizard"></a>
+
+Finally visit Icinga Web 2 in your browser to access the setup wizard and complete the installation:
+`/icingaweb2/setup`.
+
+Paste the previously generated token and follow the steps on-screen. Then you are done here.
+
+If you prefer to set up the configuration manually, follow the
+[Icinga Web 2 Manual Configuration instructions](20-Advanced-Topics.md#web-setup-manual-from-source-config)
+<!-- {% endif %} --><!-- {# end index if #} -->
diff --git a/doc/02-Installation.md.d/01-Debian.md b/doc/02-Installation.md.d/01-Debian.md
new file mode 100644
index 0000000..9ff5665
--- /dev/null
+++ b/doc/02-Installation.md.d/01-Debian.md
@@ -0,0 +1,3 @@
+# Install Icinga Web 2 on Debian
+<!-- {% set debian = True %} -->
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/02-Installation.md.d/02-Ubuntu.md b/doc/02-Installation.md.d/02-Ubuntu.md
new file mode 100644
index 0000000..74b7075
--- /dev/null
+++ b/doc/02-Installation.md.d/02-Ubuntu.md
@@ -0,0 +1,3 @@
+# Install Icinga Web 2 on Ubuntu
+<!-- {% set ubuntu = True %} -->
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/02-Installation.md.d/03-CentOS.md b/doc/02-Installation.md.d/03-CentOS.md
new file mode 100644
index 0000000..0be7b1c
--- /dev/null
+++ b/doc/02-Installation.md.d/03-CentOS.md
@@ -0,0 +1,3 @@
+# Install Icinga Web 2 on CentOS
+<!-- {% set centos = True %} -->
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/02-Installation.md.d/04-RHEL.md b/doc/02-Installation.md.d/04-RHEL.md
new file mode 100644
index 0000000..cab5194
--- /dev/null
+++ b/doc/02-Installation.md.d/04-RHEL.md
@@ -0,0 +1,3 @@
+# Install Icinga Web 2 on RHEL
+<!-- {% set rhel = True %} -->
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/02-Installation.md.d/05-SLES.md b/doc/02-Installation.md.d/05-SLES.md
new file mode 100644
index 0000000..f17ab34
--- /dev/null
+++ b/doc/02-Installation.md.d/05-SLES.md
@@ -0,0 +1,3 @@
+# Install Icinga 2 on SLES
+<!-- {% set sles = True %} -->
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/02-Installation.md.d/06-Amazon-Linux.md b/doc/02-Installation.md.d/06-Amazon-Linux.md
new file mode 100644
index 0000000..d9cb7f2
--- /dev/null
+++ b/doc/02-Installation.md.d/06-Amazon-Linux.md
@@ -0,0 +1,3 @@
+# Install Icinga Web 2 on Amazon Linux
+<!-- {% set amazon_linux = True %} -->
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/02-Installation.md.d/07-From-Source.md b/doc/02-Installation.md.d/07-From-Source.md
new file mode 100644
index 0000000..540335f
--- /dev/null
+++ b/doc/02-Installation.md.d/07-From-Source.md
@@ -0,0 +1,3 @@
+# Install Icinga Web 2 from Source
+<!-- {% set from_source = True %} -->
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/03-Configuration.md b/doc/03-Configuration.md
new file mode 100644
index 0000000..0918aac
--- /dev/null
+++ b/doc/03-Configuration.md
@@ -0,0 +1,88 @@
+# Configuration <a id="configuration"></a>
+
+## Overview <a id="configuration-overview"></a>
+
+Apart from its web configuration capabilities, the local configuration is
+stored in `/etc/icingaweb2` by default (depending on your configuration setup).
+
+File/Directory | Description
+------------------------------------------------------- | ---------------------------------
+[config.ini](03-Configuration.md#configuration-general) | General configuration (global, logging, themes, etc.)
+[resources.ini](04-Resources.md#resources) | Global resources (Icinga Web 2 database for preferences and authentication, Icinga 2 IDO database)
+[roles.ini](06-Security.md#security-roles) | User specific roles (e.g. `administrators`) and permissions
+[authentication.ini](05-Authentication.md) | Authentication backends (e.g. database)
+enabledModules | Symlinks to enabled modules
+modules | Directory for module specific configuration
+
+
+## General Configuration <a id="configuration-general"></a>
+
+Navigate into **Configuration > Application > General **.
+
+This configuration is stored in the `config.ini` file in `/etc/icingaweb2`.
+
+### Global Configuration <a id="configuration-general-global"></a>
+
+
+Option | Description
+-------------------------|-----------------------------------------------
+show\_stacktraces | **Optional.** Whether to show debug stacktraces. Defaults to `0`.
+module\_path | **Optional.** Specifies the directories where modules can be installed. Multiple directories must be separated with colons.
+config\_resource | **Required.** Specify a defined [resource](04-Resources.md#resources-configuration-database) name.
+
+
+Example for storing the user preferences in the database resource `icingaweb_db`:
+
+```
+[global]
+show_stacktraces = "0"
+config_resource = "icingaweb_db"
+module_path = "/usr/share/icingaweb2/modules"
+```
+
+### Security Configuration <a id="configuration-general-security"></a>
+
+| Option | Description |
+|------------------|---------------------------------------------------------------------------------------------------------------------------------------|
+| use\_strict\_csp | **Optional.** Set this to `1` to enable strict [Content Security Policy](20-Advanced-Topics.md#advanced-topics-csp). Defaults to `0`. |
+
+Example:
+
+```
+[security]
+use_strict_csp = "1"
+```
+
+### Logging Configuration <a id="configuration-general-logging"></a>
+
+Option | Description
+-------------------------|-----------------------------------------------
+log | **Optional.** Specifies the logging type. Can be set to `syslog`, `file`, `php` (web server's error log) or `none`.
+level | **Optional.** Specifies the logging level. Can be set to `ERROR`, `WARNING`, `INFORMATION` or `DEBUG`.
+file | **Optional.** Specifies the log file path if `log` is set to `file`.
+application | **Optional.** Specifies the application name if `log` is set to `syslog`.
+facility | **Optional.** Specifies the syslog facility if `log` is set to `syslog`. Can be set to `user`, `local0` to `local7`. Defaults to `user`.
+
+Example for more verbose debug logging into a file:
+
+```
+[logging]
+log = "file"
+level = "DEBUG"
+file = "/usr/share/icingaweb2/log/icingaweb2.log"
+```
+
+### Theme Configuration <a id="configuration-general-theme"></a>
+
+Option | Description
+-------------------------|-----------------------------------------------
+default | **Optional.** Choose the default theme. Can be set to `Icinga`, `high-contrast`, `Winter`, 'colorblind' or your own installed theme. Defaults to `Icinga`. Note that this setting is case-sensitive because it refers to the filename of the theme.
+disabled | **Optional.** Set this to `1` if users should not be allowed to change their theme. Defaults to `0`.
+
+Example:
+
+```
+[themes]
+disabled = "1"
+default = "high-contrast"
+```
diff --git a/doc/04-Resources.md b/doc/04-Resources.md
new file mode 100644
index 0000000..ca362fa
--- /dev/null
+++ b/doc/04-Resources.md
@@ -0,0 +1,136 @@
+# Resources <a id="resources"></a>
+
+The configuration file `resources.ini` contains information about data sources that can be referenced in other
+configuration files. This allows you to manage all data sources at one central place, avoiding the need to edit several
+different files when the information about a data source changes.
+
+## Configuration <a id="resources-configuration"></a>
+
+Each section in `resources.ini` represents a data source with the section name being the identifier used to
+reference this specific data source. Depending on the data source type, the sections define different directives.
+The available data source types are `db`, `ldap` and `ssh` which will described in detail in the following
+paragraphs.
+
+Type | Description
+-------------------------|-----------------------------------------------
+db | A [database](04-Resources.md#resources-configuration-database) resource (e.g. Icinga 2 DB IDO or Icinga Web 2 user preferences)
+ldap | An [LDAP](04-Resources.md#resources-configuration-ldap) resource for authentication.
+ssh | Manage [SSH](04-Resources.md#resources-configuration-ssh) keys for remote access (e.g. command transport).
+
+
+### Database <a id="resources-configuration-database"></a>
+
+A Database resource defines a connection to a SQL database which
+can contain users and groups to handle authentication and authorization, monitoring data or user preferences.
+
+Option | Description
+------------------------------------|------------
+type | **Required.** Specifies the resource type. Must be set to `db`.
+db | **Required.** Database type. In most cases `mysql` or `pgsql`.
+host | **Required.** Connect to the database server on the given host. For using unix domain sockets, specify `localhost` for MySQL and the path to the unix domain socket directory for PostgreSQL.
+port | **Required.** Port number to use. MySQL defaults to `3306`, PostgreSQL defaults to `5432`. Mandatory for connections to a PostgreSQL database.
+username | **Required.** The database username.
+password | **Required.** The database password.
+dbname | **Required.** The database name.
+charset | **Optional.** The character set for the database connection.
+use\_ssl | **Optional.** Use SSL. Enables the following SSL options.
+ssl\_do\_not\_verify\_server\_cert | **Optional.** Disable validation of the server certificate. Only available for the `mysql` database and on PHP versions > 5.6.
+ssl\_cert | **Optional.** The file path to the SSL certificate. Only available for the `mysql` database.
+ssl\_key | **Optional.** The file path to the SSL key. Only available for the `mysql` database.
+ssl\_ca | **Optional.** The file path to the SSL certificate authority. Only available for the `mysql` database.
+ssl\_capath | **Optional.** The file path to the directory that contains the trusted SSL CA certificates, which are stored in PEM format.Only available for the `mysql` database.
+ssl\_cipher | **Optional.** A list of one or more permissible ciphers to use for SSL encryption, in a format understood by OpenSSL. For example: `DHE-RSA-AES256-SHA:AES128-SHA`. Only available for the `mysql` database.
+
+
+#### Example <a id="resources-configuration-database-example"></a>
+
+The name in brackets defines the resource name.
+
+```
+[icingaweb-mysql-tcp]
+type = db
+db = mysql
+host = 127.0.0.1
+port = 3306
+username = icingaweb
+password = icingaweb
+dbname = icingaweb
+
+[icingaweb-mysql-socket]
+type = db
+db = mysql
+host = localhost
+username = icingaweb
+password = icingaweb
+dbname = icingaweb
+
+[icingaweb-pgsql-socket]
+type = db
+db = pgsql
+host = /var/run/postgresql
+port = 5432
+username = icingaweb
+password = icingaweb
+dbname = icingaweb
+```
+
+### LDAP <a id="resources-configuration-ldap"></a>
+
+A LDAP resource represents a tree in a LDAP directory.
+LDAP is usually used for authentication and authorization.
+
+Option | Description
+-------------------------|-----------------------------------------------
+type | **Required.** Specifies the resource type. Must be set to `ldap`.
+hostname | **Required.** Connect to the LDAP server on the given host. You can also provide multiple hosts separated by a space.
+port | **Required.** Port number to use for the connection.
+root\_dn | **Required.** Root object of the tree, e.g. `ou=people,dc=icinga,dc=org`.
+bind\_dn | **Required.** The user to use when connecting to the server.
+bind\_pw | **Required.** The password to use when connecting to the server.
+encryption | **Optional.** Type of encryption to use: `none` (default), `starttls`, `ldaps`.
+timeout | **Optional.** Connection timeout for every LDAP connection. Defaults to `5`.
+disable_server_side_sort | **Optional.** Disable server side sorting. Defaults to automatic detection whether the server supports this.
+
+#### Server Side Sorting <a id="ldap-server-side-sort"></a>
+
+Icinga Web automatically detects whether the LDAP server supports server side sorting.
+If that is not the case, results get sorted on the client side.
+There are LDAP servers though which report that they support this feature in general but have it disabled for certain
+fields. This may lead to failures. With `disable_server_side_sort` it is possible to disable server side sorting and it
+has precedence over the automatic detection.
+
+#### Example <a id="resources-configuration-ldap-example"></a>
+
+The name in brackets defines the resource name.
+
+```
+[ad]
+type = ldap
+hostname = localhost
+port = 389
+root_dn = "ou=people,dc=icinga,dc=org"
+bind_dn = "cn=admin,ou=people,dc=icinga,dc=org"
+bind_pw = admin
+```
+
+### SSH <a id="resources-configuration-ssh"></a>
+
+A SSH resource contains the information about the user and the private key location, which can be used for the key-based
+ssh authentication.
+
+Option | Description
+-------------------------|-----------------------------------------------
+type | **Required.** Specifies the resource type. Must be set to `ssh`.
+user | **Required.** The username to use when connecting to the server.
+private\_key | **Required.** The path to the private key of the user.
+
+#### Example <a id="resources-configuration-ssh-example"></a>
+
+The name in brackets defines the resource name.
+
+```
+[ssh]
+type = "ssh"
+user = "ssh-user"
+private_key = "/etc/icingaweb2/ssh/ssh-user"
+```
diff --git a/doc/05-Authentication.md b/doc/05-Authentication.md
new file mode 100644
index 0000000..5923a8c
--- /dev/null
+++ b/doc/05-Authentication.md
@@ -0,0 +1,293 @@
+# Authentication <a id="authentication"></a>
+
+You can authenticate against Active Directory, LDAP, a MySQL or a PostgreSQL database or delegate
+authentication to the web server.
+
+Authentication methods can be chained to set up fallback authentication methods
+or if users are spread over multiple places.
+
+## Configuration <a id="authentication-configuration"></a>
+
+Navigate into **Configuration > Application > Authentication **.
+
+Authentication methods are configured in the `/etc/icingaweb2/authentication.ini` file.
+
+Each section in the authentication configuration represents a single authentication method.
+
+The order of entries in the authentication configuration determines the order of the authentication methods.
+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 <a id="authentication-configuration-external-authentication"></a>
+
+Authentication to the web server can be delegated with the `autologin` section
+which specifies an external backend.
+
+Option | Description
+-------------------------|-----------------------------------------------
+backend | **Required.** Specifies the backend type. Must be set to `external`.
+strip\_username\_regexp | **Optional.** Regular expression to strip off specific user name parts.
+
+Example:
+
+```
+# vim /etc/icingaweb2/authentication.ini
+
+[autologin]
+backend = external
+```
+
+If your web server is not configured for authentication though, the `autologin` section has no effect.
+
+### Example Configuration for Apache and Basic Authentication <a id="authentication-configuration-external-authentication-example"></a>
+
+The following example will show you how to enable external authentication in Apache
+using basic authentication.
+
+#### Create Basic Auth User <a id="authentication-configuration-external-authentication-example-user"></a>
+
+You can use the tool `htpasswd` to generate basic authentication credentials. This example writes the
+user credentials into the `.http-users` file.
+
+The following command creates a new file which adds 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
+```
+
+#### Apache Configuration <a id="authentication-configuration-external-authentication-example-apache"></a>
+
+Add the following configuration to the `&lt;Directory&gt;` directive in the `icingaweb2.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.
+
+Example on CentOS 7:
+
+```
+systemctl restart httpd
+```
+
+## Active Directory or LDAP Authentication <a id="authentication-configuration-ad-or-ldap-authentication"></a>
+
+If you want to authenticate against Active Directory or LDAP, you have to define an
+[LDAP resource](04-Resources.md#resources-configuration-ldap).
+This is referenced as data source for the Active Directory or LDAP configuration method.
+
+### LDAP <a id="authentication-configuration-ldap-authentication"></a>
+
+Option | Description
+-------------------------|-----------------------------------------------
+backend | **Required.** Specifies the backend type. Must be set to `ldap`.
+resource | **Required.** The name of the LDAP resource defined in [resources.ini](04-Resources.md#resources).
+user\_class | **Optional.** LDAP user class. Defaults to `inetOrgPerson`.
+user\_name\_attribute | **Optional.** LDAP attribute which contains the username. Defaults to `uid`.
+filter | **Optional.** LDAP search filter. Requires `user_class` and `user_name_attribute`.
+
+> **Note for SELinux**
+>
+> If you run into problems connecting with LDAP and have SELinux enabled, take a look [here](90-SELinux.md#selinux-optional-booleans).
+
+Example:
+
+```
+# vim /etc/icingaweb2/authentication.ini
+
+[auth_ldap]
+backend = ldap
+resource = my_ldap
+user_class = inetOrgPerson
+user_name_attribute = uid
+filter = "memberOf=cn=icinga_users,cn=groups,cn=accounts,dc=icinga,dc=org"
+```
+
+If `user_name_attribute` specifies multiple values all of them must be unique.
+Please keep in mind that a user will be logged in with the exact user id used to authenticate
+with Icinga Web 2 (e.g. an alias) ignoring the actual primary user id.
+
+### Active Directory <a id="authentication-configuration-ad-authentication"></a>
+
+Option | Description
+-------------------------|-----------------------------------------------
+backend | **Required.** Specifies the backend type. Must be set to `msldap`.
+resource | **Required.** The name of the LDAP resource defined in [resources.ini](04-Resources.md#resources).
+user\_class | **Optional.** LDAP user class. Defaults to `user`.
+user\_name\_attribute | **Optional.** LDAP attribute which contains the username. Defaults to `sAMAccountName`.
+filter | **Optional.** LDAP search filter. Requires `user_class` and `user_name_attribute`.
+
+Example:
+
+```
+# vim /etc/icingaweb2/authentication.ini
+
+[auth_ad]
+backend = msldap
+resource = my_ad
+```
+
+## Database Authentication <a id="authentication-configuration-db-authentication"></a>
+
+If you want to authenticate against a MySQL or a PostgreSQL database, you have to define a
+[database resource](04-Resources.md#resources-configuration-database) which will be referenced as data source for the database
+authentication method.
+
+Option | Description
+-------------------------|-----------------------------------------------
+backend | **Required.** Specifies the backend type. Must be set to `db`.
+resource | **Required.** The name of the database resource defined in [resources.ini](04-Resources.md#resources). |
+
+Example:
+
+```
+# vim /etc/icingaweb2/authentication.ini
+
+[auth_db]
+backend = db
+resource = icingaweb-mysql
+```
+
+Please read [this chapter](20-Advanced-Topics.md#advanced-topics-authentication-tips-manual-user-database-auth)
+in order to manually create users directly inside the database.
+
+
+## Groups <a id="authentication-configuration-groups"></a>
+
+Navigate into **Configuration > Application > Authentication **.
+
+Group configuration is stored in the `/etc/icingaweb2/groups.ini` file.
+
+### LDAP Groups <a id="authentication-configuration-groups-ldap"></a>
+
+Option | Description
+-------------------------|-----------------------------------------------
+backend | **Required.** Specifies the backend type. Can be set to `ldap`, `msldap`.
+resource | **Required.** The name of the LDAP resource defined in [resources.ini](04-Resources.md#resources).
+domain | **Optional.** The domain the LDAP server is responsible for. See [Domain-aware Authentication](05-Authentication.md#domain-aware-auth).
+user\_class | **Optional.** LDAP user class. Defaults to `inetOrgPerson` with `msldap` and `user` with `ldap`.
+user\_name\_attribute | **Optional.** LDAP attribute which contains the username. Defaults to `sAMAccountName` with `msldap` and `uid` with `ldap`.
+user\_base\_dn | **Optional.** The path where users can be found on the LDAP server.
+base_dn | **Optional.** LDAP base dn for groups. Leave empty to select all groups available using the specified resource.
+group\_class | **Optional.** LDAP group class. Defaults to `group`.
+group\_member\_attribute | **Optional.** LDAP attribute where a group's members are stored. Defaults to `member`.
+group\_name\_attribute | **Optional.** LDAP attribute which contains the groupname. Defaults to `sAMAccountName` with `msldap` and `gid` with `ldap`.
+group\_filter | **Optional.** LDAP group search filter. Requires `group_class` and `group_name_attribute`.
+nested\_group\_search | **Optional.** Enable nested group search in Active Directory based on the user. Defaults to `0`. Only available with `backend` type `msldap`.
+
+Example for Active Directory groups:
+
+```
+# vim /etc/icingaweb2/groups.ini
+
+[active directory]
+backend = "msldap"
+resource = "auth_ad"
+group_class = "group"
+user_class = "user"
+user_name_attribute = "userPrincipalName"
+```
+
+Example for Active Directory using the group backend resource `ad_company`.
+It also references the defined user backend resource `ad_users_company`.
+
+```
+# vim /etc/icingaweb2/groups.ini
+
+[ad_groups_company]
+backend = "msldap"
+resource = "ad_company"
+user_backend = "ad_users_company"
+nested_group_search = "1"
+base_dn = "ou=Icinga,ou=Groups,dc=company,dc=com"
+```
+
+### Database Groups <a id="authentication-configuration-groups-database"></a>
+
+Option | Description
+-------------------------|-----------------------------------------------
+backend | **Required.** Specifies the backend type. Must be set to `db`.
+resource | **Required.** The name of the database resource defined in [resources.ini](04-Resources.md#resources).
+
+Example:
+
+```
+# vim /etc/icingaweb2/groups.ini
+
+[icingaweb2]
+backend = "db"
+resource = "icingaweb_db"
+```
+
+
+## Domain-aware Authentication <a id="domain-aware-auth"></a>
+
+If there are multiple LDAP/AD authentication backends with distinct domains, you should make Icinga Web 2 aware of the
+domains. This is possible since version 2.5 and can be done by configuring each LDAP/AD backend's domain. You can also
+use the GUI for this purpose. This enables you to automatically discover a suitable value based on your LDAP server's
+configuration. (AD: NetBIOS name, other LDAP: domain in DNS-notation)
+
+**Example:**
+
+```
+# vim /etc/icingaweb2/authentication.ini
+
+[auth_icinga]
+backend = ldap
+resource = icinga_ldap
+user_class = inetOrgPerson
+user_name_attribute = uid
+filter = "memberOf=cn=icinga_users,cn=groups,cn=accounts,dc=icinga,dc=com"
+domain = "icinga.com"
+
+[auth_example]
+backend = msldap
+resource = example_ad
+domain = EXAMPLE
+```
+
+If you configure the domains like above, the icinga.com user "jdoe" will have to log in as "jdoe@icinga.com" and the
+EXAMPLE employee "rroe" will have to log in as "rroe@EXAMPLE". They could also log in as "EXAMPLE\\rroe", but this gets
+converted to "rroe@EXAMPLE" as soon as the user logs in.
+
+> **Caution!**
+>
+> Enabling domain-awareness or changing domains in existing setups requires migration of the usernames in the Icinga Web 2
+> configuration. Consult `icingacli --help migrate config users` for details.
+
+### Default Domain <a id="default-auth-domain"></a>
+
+For the sake of simplicity a default domain can be configured (in `config.ini`).
+
+**Example:**
+
+```
+# vim /etc/icingaweb2/config.ini
+
+[authentication]
+default_domain = "icinga.com"
+```
+
+If you configure the default domain like above, the user "jdoe@icinga.com" will be able to just type "jdoe" as username
+while logging in.
+
+### How it works <a id="domain-aware-auth-process"></a>
+
+### Active Directory <a id="domain-aware-auth-ad"></a>
+
+When the user "jdoe@ICINGA" logs in, Icinga Web 2 walks through all configured authentication backends until it finds
+one which is responsible for that user -- e.g. an Active Directory backend with the domain "ICINGA". Then Icinga Web 2
+asks that backend to authenticate the user with the sAMAccountName "jdoe".
+
+### SQL Database <a id="domain-aware-auth-sqldb"></a>
+
+When the user "jdoe@icinga.com" logs in, Icinga Web 2 walks through all configured authentication backends until it
+finds one which is responsible for that user -- e.g. a MySQL backend (SQL database backends aren't domain-aware). Then
+Icinga Web 2 asks that backend to authenticate the user with the username "jdoe@icinga.com".
diff --git a/doc/06-Security.md b/doc/06-Security.md
new file mode 100644
index 0000000..b8d7cf2
--- /dev/null
+++ b/doc/06-Security.md
@@ -0,0 +1,242 @@
+# Security
+
+Access control is a vital part of configuring Icinga Web 2 securely. It is important that not every user that has
+access to Icinga Web 2 can perform any action or see any host and service. Allow only a small group of administrators
+to change the Icinga Web 2 configuration to prevent mis-configuration and security breaches. Define different rules
+to users and groups of users which should only see a part of the monitoring environment they're in charge of.
+
+This chapter will describe how to configure such rules in Icinga Web 2 and how permissions, refusals, restrictions
+and role inheritance work.
+
+## Basics
+
+Icinga Web 2 access control is done by defining **roles** that associate privileges with **users** and **groups**.
+Privileges of a role consist of **permissions**, **refusals** and **restrictions**. A role can **inherit** privileges
+from another role.
+
+### Role Memberships
+
+A role is tied to users or groups of users. Upon login, a user's roles are identified by the username or names of
+groups the user is a member of.
+
+> **Note**
+>
+> Since Icinga Web 2, users in the Icinga configuration and the web authentication are separated, to allow use of
+> external authentication providers. This means that users and groups defined in the Icinga configuration are not
+> available to Icinga Web 2. It uses its own authentication backend to fetch users and groups from,
+> [which must be configured separately](05-Authentication.md#authentication).
+
+### Privileges
+
+Permissions are used to grant access. Whether this means that a user can see a certain area or perform a distinct
+action is fully up to the permission in question. Without granting a permission, the user will lack access and won't
+see the area or perform the action.
+
+Refusals are used to deny access. So they're the exact opposite of permissions. Most permissions can be refused.
+Refusing a permission will block the user's access no matter if another role grants the permission. Refusals
+override permissions.
+
+Restrictions are expressions that limit access. What this exactly means is up to how the restriction is being utilized.
+Without any restriction, a user is supposed to see *everything*. A user that occupies multiple roles, which all define
+a restriction of the same type, will see *more*.
+
+## Roles
+
+A user can occupy multiple roles. Permissions and restrictions stack up in this case, thus will grant *more* access.
+Refusals still override permissions however. A refusal of one role negates the granted permission of any other role.
+
+### Configuration
+
+Roles can be changed either through the UI, by navigating to the page **Configuration > Authentication > Roles**,
+or by editing the configuration file `/etc/icingaweb2/roles.ini`.
+
+#### Example
+
+The following shows a role definition from the configuration file mentioned above:
+
+```
+[winadmin]
+users = "jdoe, janedoe"
+groups = "admin"
+permissions = "config/*, module/monitoring, monitoring/commands/schedule-check"
+refusals = "config/authentication"
+monitoring/filter/objects = "host_name=*win*"
+```
+
+This describes a role with the name `winadmin`. The users `jdoe` and `janedoe` are members of it. Just like the
+members of group `admin` are. Full configuration access is granted, except of the authentication configuration,
+which is forbidden. It also grants access to the *monitoring* module which includes the ability to re-schedule
+checks, but only on objects related to hosts whose name contain `win`.
+
+#### Syntax
+
+Each role is defined as a section, with the name of the role as section name. The following
+options can be defined for each role in a default Icinga Web 2 installation:
+
+Name | Description
+--------------------------|-----------------------------------------------
+parent | The name of the role from which to inherit privileges.
+users | Comma-separated list of **usernames** that should occupy this role.
+groups | Comma-separated list of **group names** whose users should occupy this role.
+permissions | Comma-separated list of **permissions** granted by this role.
+refusals | Comma-separated list of **permissions** refused by this role.
+unrestricted | If set to `1`, owners of this role are not restricted in any way (Default: `0`)
+monitoring/filter/objects | **Filter expression** that restricts the access to monitoring objects.
+
+### Administrative Roles
+
+Roles that have the wildcard `*` as permission, have full access and don't need any further permissions. However,
+they are still affected by refusals.
+
+Unrestricted roles are supposed to allow users to access data without being limited to a subset of it. Once a user
+occupies an unrestricted role, restrictions of the same and any other role are ignored.
+
+### Inheritance
+
+A role can inherit privileges from another role. Privileges are then combined the same way as if a user occupies
+all roles in the inheritance path. Or to rephrase that, each role shares its members with all of its parents.
+
+## Permissions
+
+Each permission in Icinga Web 2 is denoted by a **namespaced key**, which is used to group permissions. All permissions
+that affect the configuration of Icinga Web 2, are in a namespace called **config**, while all configuration options
+that affect modules are covered by the permission `config/modules`.
+
+**Wildcards** can be used to grant all permissions in a certain namespace. The permission `config/*` grants access to
+all configuration options. Just specifying a wildcard `*` will grant all permissions.
+
+Access to modules is restricted to users who have the related module permission granted. Icinga Web 2 provides
+a module permission in the format `module/<moduleName>` for each installed module.
+
+### General Permissions
+
+Name | Permits
+-----------------------------|-----------------------------------------------
+\* | allow everything, including module-specific permissions
+application/announcements | allow to manage announcements
+application/log | allow to view the application log
+config/\* | allow full config access
+config/access-control/\* | allow to fully manage access control
+config/access-control/groups | allow to manage groups
+config/access-control/roles | allow to manage roles
+config/access-control/users | allow to manage user accounts
+config/general | allow to adjust the general configuration
+config/modules | allow to enable/disable and configure modules
+config/navigation | allow to view and adjust shared navigation items
+config/resources | allow to manage resources
+user/\* | allow all account related functionalities
+user/application/stacktraces | allow to adjust in the preferences whether to show stacktraces
+user/password-change | allow password changes in the account preferences
+user/share/navigation | allow to share navigation items
+module/`<moduleName>` | allow access to module `<moduleName>` (e.g. `module/monitoring`)
+
+### Monitoring Module Permissions
+
+The built-in monitoring module defines an additional set of permissions, that
+is described in detail in the monitoring module documentation.
+
+## Restrictions
+
+Restrictions can be used to define what a user can see by specifying an expression that applies to a defined set of
+data. By default, when no restrictions are defined, a user will be able to see the entire data that is available.
+
+The syntax of the expression used to define a particular restriction varies. This can be a comma-separated list of
+terms, or [a full-blown filter](06-Security.md#filter-expressions). For more details on particular restrictions,
+check the table below or the module's documentation providing the restriction.
+
+### General Restrictions
+
+Name | Applies to
+--------------------------|------------------------------------------------------------------------------------------
+application/share/users | which users a user can share navigation items with (comma-separated list of usernames)
+application/share/groups | which groups a user can share navigation items with (comma-separated list of group names)
+
+### Username placeholder
+
+It is possible to reference the local username (without the domain part) of the user in restrictions. To accomplish
+this, put the macro `$user.local_name$` in the restriction where you want it to appear.
+
+This can come in handy if you have e.g. an attribute on hosts or services defining which user is responsible for it:
+`_host_deputy=$user.local_name$|_service_deputy=$user.local_name$`
+
+### 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.
+
+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
+filter expressions containing AND and OR-Clauses.
+
+The filter expression will be **implicitly** added as an **AND-Clause** to each query on
+the filtered data. The following shows the filter expression `host_name=*win*` being applied on `monitoring/filter/objects`.
+
+
+Regular filter query:
+
+ AND-- service_problem = 1
+ |
+ +--- service_handled = 0
+
+
+With our restriction applied, any user affected by this restrictions will see the
+results of this query instead:
+
+
+ AND-- host_name = *win*
+ |
+ +--AND-- service_problem = 1
+ |
+ +--- service_handled = 0
+
+#### Stacking Filters
+
+When multiple roles assign restrictions to the same user, either directly or indirectly
+through a group, all filters will be combined using an **OR-Clause**, resulting in the final
+expression:
+
+
+ AND-- OR-- $FILTER1
+ | |
+ | +-- $FILTER2
+ | |
+ | +-- $FILTER3
+ |
+ +--AND-- service_problem = 1
+ |
+ +--- service_handled = 0
+
+
+As a result, a user is be able to see hosts that are matched by **ANY** of
+the filter expressions. The following examples will show the usefulness of this behavior:
+
+#### Example 1: Negation
+
+```
+[winadmin]
+groups = "windows-admins"
+monitoring/filter/objects = "host_name=*win*"
+```
+
+Will display only hosts and services whose host name contains **win**.
+
+```
+[webadmin]
+groups = "web-admins"
+monitoring/filter/objects = "host_name!=*win*"
+```
+
+Will only match hosts and services whose host name does **not** contain **win**
+
+Notice that because of the behavior of two stacking filters, a user that is member of **windows-admins** and **web-admins**, will now be able to see both, Windows and non-Windows hosts and services.
+
+#### Example 2: Hostgroups
+
+```
+[unix-server]
+groups = "unix-admins"
+monitoring/filter/objects = "(hostgroup_name=bsd-servers|hostgroup_name=linux-servers)"
+```
+
+This role allows all members of the group unix-admins to see hosts and services
+that are part of the host-group linux-servers or the host-group bsd-servers.
diff --git a/doc/07-Preferences.md b/doc/07-Preferences.md
new file mode 100644
index 0000000..73abead
--- /dev/null
+++ b/doc/07-Preferences.md
@@ -0,0 +1,21 @@
+# Preferences <a id="preferences"></a>
+
+Preferences are settings a user can set for their account only,
+for example the language and time zone.
+
+Preferences can be stored either in a MySQL or in a PostgreSQL database. The database must be configured.
+
+## Configuration <a id="preferences-configuration"></a>
+
+The preference configuration backend is defined in the global [config.ini](03-Configuration.md#configuration-general-global) file.
+
+You have to define a [database resource](04-Resources.md#resources-configuration-database)
+which will be referenced as resource for the preferences storage.
+
+You need to add the following section to the global [config.ini](03-Configuration.md#configuration-general-global) file
+in order to store preferences in a database.
+
+```
+[global]
+config_resource = "icingaweb_db"
+```
diff --git a/doc/08-Modules.md b/doc/08-Modules.md
new file mode 100644
index 0000000..6c81c5b
--- /dev/null
+++ b/doc/08-Modules.md
@@ -0,0 +1,69 @@
+# Modules
+
+## Installation
+
+A module should be installed in one of the [configured module paths](03-Configuration.md#general-configuration).
+The default path in most installations is `/usr/share/icingaweb2/modules`.
+
+Each directory in there contains the files for a particular module. The directory's name has to be the one
+that is provided by the module's documentation. If there is none provided, it is usually the name of the
+module in all lowercase. Some modules may use a name prefixed with `icingaweb2-module-`. If this is the case,
+the directory's name should be that, but without the prefix.
+(e.g. `icingaweb2-module-map` turns into `/usr/share/icingaweb2/modules/map`)
+
+> **Note:**
+>
+> Remember to ensure that your web-server can access those files. Though, read permission only.
+
+Once a module's files are in place, it needs to be enabled first before it can be used. This can either be done in
+the UI at `Configuration -> Modules` or by using the icingacli: `icingacli module enable map`
+
+In order for other non-admin users to access the module's functionality, it is required to permit access first.
+This is done by granting the permission `module/<module-name>`. (e.g. `module/map`)
+
+### Module Specific Instructions
+
+A module may require further installation steps. Whether these need to be performed before enabling the module,
+should be provided by the module's documentation. In any case, don't forget to apply these as well, otherwise
+the module will most likely not function correctly.
+
+### Examples
+
+There are sample installation instructions provided for your convenience:
+
+**Sample Tarball installation**
+
+```sh
+MODULE_NAME="map"
+MODULE_VERSION="v1.1.0"
+MODULE_AUTHOR="nbuchwitz"
+MODULES_PATH="/usr/share/icingaweb2/modules"
+MODULE_PATH="${MODULES_PATH}/${MODULE_NAME}"
+RELEASES="https://github.com/${MODULE_AUTHOR}/icingaweb2-module-${MODULE_NAME}/archive"
+mkdir "$MODULE_PATH" \
+&& wget -q $RELEASES/${MODULE_VERSION}.tar.gz -O - \
+ | tar xfz - -C "$MODULE_PATH" --strip-components 1
+icingacli module enable "${MODULE_NAME}"
+```
+
+**Sample GIT installation**
+
+```sh
+MODULE_NAME="map"
+MODULE_VERSION="v1.1.0"
+MODULE_AUTHOR="nbuchwitz"
+REPO="https://github.com/${MODULE_AUTHOR}/icingaweb2-module-${MODULE_NAME}"
+MODULES_PATH="/usr/share/icingaweb2/modules"
+git clone ${REPO} "${MODULES_PATH}/${MODULE_NAME}" --branch "${MODULE_VERSION}"
+icingacli module enable "${MODULE_NAME}"
+```
+
+## Configuration
+
+A module may also require configuration. Most modules provide additional tabs at their configuration page.
+This is accessible in the UI at `Configuration -> Modules`. If not, and something isn't working, check the
+module's documentation again for hints.
+
+If you need access to a module's configuration files directly, they should be in a subdirectory `modules`
+of Icinga Web 2's configuration directory. That is usually `/etc/icingaweb2/modules`. Each directory in
+there should be named the same as its installation path. (e.g. `/etc/icingaweb2/modules/map`)
diff --git a/doc/15-Auditing.md b/doc/15-Auditing.md
new file mode 100644
index 0000000..c44cbfe
--- /dev/null
+++ b/doc/15-Auditing.md
@@ -0,0 +1,14 @@
+# Auditing <a id="auditing"></a>
+
+Auditing in Icinga Web 2 is possible with a separate [module](https://github.com/Icinga/icingaweb2-module-audit).
+
+This module provides different logging facilities to store/record activities by Icinga Web 2 users.
+
+Icinga Web 2 currently emits the following activities:
+
+## Authentication
+
+Activity | Additional Data
+---------|----------------
+login | username
+logout | username
diff --git a/doc/20-Advanced-Topics.md b/doc/20-Advanced-Topics.md
new file mode 100644
index 0000000..ade5aff
--- /dev/null
+++ b/doc/20-Advanced-Topics.md
@@ -0,0 +1,440 @@
+# Advanced Topics <a id="advanced-topics"></a>
+
+This chapter provides details for advanced Icinga Web 2 topics.
+
+* [Global URL parameters](20-Advanced-Topics.md#global-url-parameters)
+* [VirtualHost configuration](20-Advanced-Topics.md#virtualhost-configuration)
+* [Content Security Policy (CSP)](20-Advanced-Topics.md#advanced-topics-csp)
+* [Advanced Authentication Tips](20-Advanced-Topics.md#advanced-topics-authentication-tips)
+* [Source installation](20-Advanced-Topics.md#installing-from-source)
+* [Automated setup](20-Advanced-Topics.md#web-setup-automation)
+* [Kiosk Mode Configuration](20-Advanced-Topics.md#kiosk-mode)
+* [Customizing the Landing Page](20-Advanced-Topics.md#landing-page)
+
+## Global URL Parameters <a id="global-url-parameters"></a>
+
+Parameters starting with `_` are for development purposes only.
+
+Parameter | Value | Description
+------------------|---------------|--------------------------------
+showFullscreen | - | Hides the left menu and optimizes the layout for full screen resolution.
+showCompact | - | Provides a compact view. Hides the title and upper menu. This is helpful to embed a dashboard item into an external iframe.
+format | json/csv/sql | Selected views can be exported as JSON or CSV. This also is available in the upper menu. You can also export the SQL queries for manual analysis.
+\_dev | 0/1 | Whether the server should return compressed or full JS/CSS files. This helps debugging browser console errors.
+
+
+
+Examples for `showFullscreen`:
+
+http://localhost/icingaweb2/dashboard?showFullscreen
+http://localhost/icingaweb2/monitoring/list/services?service_problem=1&sort=service_severity&showFullscreen
+
+Examples for `showCompact`:
+
+http://localhost/icingaweb2/dashboard?showCompact&showFullscreen
+http://localhost/icingaweb2/monitoring/list/services?service_problem=1&sort=service_severity&showCompact
+
+Examples for `format`:
+
+http://localhost/icingaweb2/monitoring/list/services?format=json
+http://localhost/icingaweb2/monitoring/list/services?service_problem=1&sort=service_severity&dir=desc&format=csv
+
+
+## VirtualHost Configuration <a id="virtualhost-configuration"></a>
+
+This describes how to run Icinga Web 2 on your FQDN's `/` entry point without
+any redirect to `/icingaweb2`.
+
+### VirtualHost Configuration for Apache <a id="virtualhost-configuration-apache"></a>
+
+Use the setup CLI commands to generate the default Apache configuration which serves
+Icinga Web 2 underneath `/icingaweb2`.
+
+The next steps are to create the VirtualHost configuration:
+
+* Copy the `<Directory "/usr/share/icingaweb2/public">` into the main VHost configuration. Don't forget to correct the indent.
+* Set the `DocumentRoot` variable to look into `/usr/share/icingaweb2/public`
+* Modify the `RewriteBase` variable to use `/` instead of `/icingaweb2`
+
+Example on RHEL/CentOS:
+
+```
+vim /etc/httpd/conf.d/web.icinga.com.conf
+
+<VirtualHost *:80>
+ ServerName web.icinga.com
+
+ ## Vhost docroot
+ # modified for Icinga Web 2
+ DocumentRoot "/usr/share/icingaweb2/public"
+
+ ## Rewrite rules
+ RewriteEngine On
+
+ <Directory "/usr/share/icingaweb2/public">
+ Options SymLinksIfOwnerMatch
+ AllowOverride None
+
+ <IfModule mod_authz_core.c>
+ # Apache 2.4
+ <RequireAll>
+ Require all granted
+ </RequireAll>
+ </IfModule>
+
+ <IfModule !mod_authz_core.c>
+ # Apache 2.2
+ Order allow,deny
+ Allow from all
+ </IfModule>
+
+ SetEnv ICINGAWEB_CONFIGDIR "/etc/icingaweb2"
+
+ EnableSendfile Off
+
+ <IfModule mod_rewrite.c>
+ RewriteEngine on
+ # modified base
+ RewriteBase /
+ RewriteCond %{REQUEST_FILENAME} -s [OR]
+ RewriteCond %{REQUEST_FILENAME} -l [OR]
+ RewriteCond %{REQUEST_FILENAME} -d
+ RewriteRule ^.*$ - [NC,L]
+ RewriteRule ^.*$ index.php [NC,L]
+ </IfModule>
+
+ <IfModule !mod_rewrite.c>
+ DirectoryIndex error_norewrite.html
+ ErrorDocument 404 /error_norewrite.html
+ </IfModule>
+ </Directory>
+</VirtualHost>
+```
+
+Reload Apache and open the FQDN in your web browser.
+
+```
+systemctl reload httpd
+```
+
+### Content Security Policy (CSP) <a id="advanced-topics-csp"></a>
+
+Elevate your security standards to an even higher level by enabling the [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) for Icinga Web.
+Enabling strict CSP can prevent your Icinga Web environment from becoming a potential target of [Cross-Site Scripting (XSS)](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting)
+and data injection attacks. After enabling this feature Icinga Web defines all the required CSP headers. Subsequently,
+only content coming from Icinga Web's own origin is accepted, inline JS is prohibited, and inline CSS is accepted only
+if it contains the nonce set in the response header.
+
+We decided against enabling this by default as we cannot guarantee that all the modules out there will function correctly.
+Therefore, you have to manually enable this policy explicitly and accept the risks that this might break some of
+the Icinga Web modules. Icinga Web and all it's components listed below, on the other hand, fully support strict CSP. If
+that's not the case, please submit an issue on GitHub in the respective repositories.
+
+Here is a list of all Icinga Web components that are capable of strict CSP.
+
+| Name | CSP supported since |
+|-----------------------------------|-------------------------------------------------------------------------------------------|
+| Icinga DB Web | [v1.1.0](https://github.com/Icinga/icingadb-web/releases/tag/v1.1.0) |
+| Icinga Reporting | [v1.0.0](https://github.com/Icinga/icingaweb2-module-reporting/releases/tag/v1.0.0) |
+| Icinga IDO Reports | [v0.10.1](https://github.com/Icinga/icingaweb2-module-idoreports/releases/tag/v0.10.1) |
+| Icinga Cube | [v1.3.2](https://github.com/Icinga/icingaweb2-module-cube/releases/tag/v1.3.2) |
+| Icinga Business Process Modeling | [v2.5.0](https://github.com/Icinga/icingaweb2-module-businessprocess/releases/tag/v2.5.0) |
+| Icinga Certificate Monitoring | [v1.3.0](https://github.com/Icinga/icingaweb2-module-x509/releases/tag/v1.3.0) |
+| Icinga PDF Export | [v0.10.2](https://github.com/Icinga/icingaweb2-module-pdfexport/releases/tag/v0.10.2) |
+| Icinga Web Jira Integration | [v1.3.2](https://github.com/Icinga/icingaweb2-module-jira/releases/tag/v1.3.2) |
+| Icinga Web Graphite Integration | [v1.3.0](https://github.com/Icinga/icingaweb2-module-graphite/releases/tag/v1.2.4) |
+| Icinga Web GenericTTS Integration | [v2.1.0](https://github.com/Icinga/icingaweb2-module-generictts/releases/tag/v2.1.0) |
+| Icinga Web Nagvis Integration | [v1.2.0](https://github.com/Icinga/icingaweb2-module-nagvis/releases/tag/v1.2.0) |
+| Icinga Web AWS Integration | [v1.1.0](https://github.com/Icinga/icingaweb2-module-aws/releases/tag/v1.1.0) |
+
+
+## Advanced Authentication Tips <a id="advanced-topics-authentication-tips"></a>
+
+### Manual User Creation for Database Authentication Backend <a id="advanced-topics-authentication-tips-manual-user-database-auth"></a>
+
+Icinga Web 2 v2.5+ uses the [native password hash algorithm](https://php.net/manual/en/faq.passwords.php)
+provided by PHP 5.6+.
+
+In order to generate a password, run the following command with the PHP CLI >= 5.6:
+
+```
+php -r 'echo password_hash("yourtopsecretpassword", PASSWORD_DEFAULT);'
+```
+
+Please note that the hashed output changes each time. This is expected.
+
+Insert the user into the database using the generated password hash.
+
+```
+INSERT INTO icingaweb_user (name, active, password_hash) VALUES ('icingaadmin', 1, '$2y$10$bEKU6.1bRYjE7wxktqfeO.IGV9pYAkDBeXEbjMFSNs26lKTI0JQ1q');
+```
+
+#### Puppet <a id="advanced-topics-authentication-tips-manual-user-database-auth-puppet"></a>
+
+Please do note that the `$` character needs to be escaped with a leading backslash in your
+Puppet manifests.
+
+Example from [puppet-icingaweb2](https://github.com/Icinga/puppet-icingaweb2):
+
+```
+ exec { 'create default user':
+ command => "mysql -h '${db_host}' -P '${db_port}' -u '${db_username}' -p'${db_password}' '${db_name}' -Ns -e 'INSERT INTO icingaweb_user (name, active, password_hash) VALUES (\"icingaadmin\", 1, \"\$2y\$10\$QnXfBjl1RE6TqJcY85ZKJuP9AvAV3ont9QihMTFQ/D/vHmAWaz.lG\")'",
+ refreshonly => true,
+ }
+```
+
+
+## Icinga Web 2 Manual Setup <a id="web-setup-manual-from-source"></a>
+
+If you have chosen not to run the setup wizard, you will need further knowledge
+about
+
+* manual creation of the Icinga Web 2 database `icingaweb2` including a default user (optional as authentication and session backend)
+* additional configuration for the application
+* additional configuration for the monitoring module (e.g. the IDO database and external command pipe from Icinga 2)
+
+This comes in handy if you are planning to deploy Icinga Web 2 automatically using
+Puppet, Ansible, Chef, etc.
+
+> **Warning**
+>
+> 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 as described in the
+> [installation instructions](02-Installation.md) once and then collect the generated
+> configuration as well as sql dumps.
+
+### Icinga Web 2 Manual Database Setup <a id="web-setup-manual-from-source-database"></a>
+
+Create the database and add a new user as shown below for MySQL/MariaDB:
+
+```
+sudo mysql -p
+
+CREATE DATABASE icingaweb2;
+GRANT CREATE, SELECT, INSERT, UPDATE, DELETE, DROP, ALTER, CREATE VIEW, INDEX, EXECUTE ON icingaweb2.* TO 'icingaweb2'@'localhost' IDENTIFIED BY 'icingaweb2';
+quit
+
+mysql -p icingaweb2 < /usr/share/icingaweb2/schema/mysql.schema.sql
+```
+
+
+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.
+
+```
+mysql -p icingaweb2
+
+INSERT INTO icingaweb_user (name, active, password_hash) VALUES ('icingaadmin', 1, '$1$EzxLOFDr$giVx3bGhVm4lDUAw6srGX1');
+quit
+```
+
+### Icinga Web 2 Manual Configuration <a id="web-setup-manual-from-source-config"></a>
+
+
+[resources.ini](04-Resources.md#resources) providing the details for the Icinga Web 2 and
+Icinga 2 IDO database configuration. Example for MySQL:
+
+```
+vim /etc/icingaweb2/resources.ini
+
+[icingaweb2]
+type = "db"
+db = "mysql"
+host = "localhost"
+port = "3306"
+dbname = "icingaweb2"
+username = "icingaweb2"
+password = "icingaweb2"
+
+
+[icinga2]
+type = "db"
+db = "mysql"
+host = "localhost"
+port = "3306"
+dbname = "icinga"
+username = "icinga"
+password = "icinga"
+```
+
+[config.ini](03-Configuration.md#configuration) defining general application settings.
+
+```
+vim /etc/icingaweb2/config.ini
+
+[logging]
+log = "syslog"
+level = "ERROR"
+application = "icingaweb2"
+
+
+[preferences]
+type = "db"
+resource = "icingaweb2"
+```
+
+[authentication.ini](05-Authentication.md#authentication) for e.g. using the previously created database.
+
+```
+vim /etc/icingaweb2/authentication.ini
+
+[icingaweb2]
+backend = "db"
+resource = "icingaweb2"
+```
+
+
+[roles.ini](06-Security.md#security) granting the previously added `icingaadmin` user all permissions.
+
+```
+vim /etc/icingaweb2/roles.ini
+
+[admins]
+users = "icingaadmin"
+permissions = "*"
+```
+
+### Icinga Web 2 Manual Configuration Monitoring Module <a id="web-setup-manual-from-source-config-monitoring-module"></a>
+
+
+**config.ini** defining additional security settings.
+
+```
+vim /etc/icingaweb2/modules/monitoring/config.ini
+
+[security]
+protected_customvars = "*pw*,*pass*,community"
+```
+
+**backends.ini** referencing the Icinga 2 DB IDO resource.
+
+```
+vim /etc/icingaweb2/modules/monitoring/backends.ini
+
+[icinga2]
+type = "ido"
+resource = "icinga2"
+```
+
+**commandtransports.ini** defining the Icinga 2 API command transport.
+
+```
+vim /etc/icingaweb2/modules/monitoring/commandtransports.ini
+
+[icinga2]
+transport = "api"
+host = "localhost"
+port = "5665"
+username = "api"
+password = "api"
+```
+
+### Icinga Web 2 Manual Setup Login <a id="web-setup-manual-from-source-login"></a>
+
+Finally visit Icinga Web 2 in your browser to login as `icingaadmin` user: `/icingaweb2`.
+
+## Automating the Installation of Icinga Web 2 <a id="web-setup-automation"></a>
+
+Prior to creating your own script, please look into the official resources
+which may help you already:
+
+* [Puppet module](https://icinga.com/products/integrations/puppet/)
+* [Chef cookbook](https://icinga.com/products/integrations/chef/)
+
+If you are automating the installation of Icinga Web 2, you may want to skip the wizard and do things yourself.
+These are the steps you'd need to take assuming you are using MySQL/MariaDB. If you are using PostgreSQL please adapt
+accordingly. Note you need to have successfully completed the Icinga 2 installation, installed the Icinga Web 2 packages
+and all the other steps described above first.
+
+1. Install PHP dependencies: `php`, `php-intl`, `php-imagick`, `php-gd`, `php-mysql`, `php-curl`, `php-mbstring` used
+by Icinga Web 2.
+2. Create a database for Icinga Web 2, i.e. `icingaweb2`.
+3. Import the database schema: `mysql -D icingaweb2 < /usr/share/icingaweb2/schema/mysql.schema.sql`.
+4. Insert administrator user in the `icingaweb2` database:
+`INSERT INTO icingaweb_user (name, active, password_hash) VALUES ('admin', 1, '<hash>')`, where `<hash>` is the output
+of `php -r 'echo password_hash("yourtopsecretpassword", PASSWORD_DEFAULT);'`.
+5. Make sure the `ido-mysql` and `api` features are enabled in Icinga 2: `icinga2 feature enable ido-mysql` and
+`icinga2 feature enable api`.
+6. Generate Apache/nginx config. This command will print an apache config for you on stdout:
+`icingacli setup config webserver apache`. Similarly for nginx. You need to place that configuration in the right place,
+for example `/etc/apache2/sites-enabled/icingaweb2.conf`.
+7. Add `www-data` user to `icingaweb2` group if not done already (`usermod -a -G icingaweb2 www-data`).
+8. Create the Icinga Web 2 configuration in `/etc/icingaweb2`. The directory can be easily created with:
+`icingacli setup config directory`. This command ensures that the directory has the appropriate ownership and
+permissions. If you want to create the directory manually, make sure to chown the group to `icingaweb2` and set the
+access mode to `2770`.
+
+The structure of the configurations looks like the following:
+
+```
+/etc/icingaweb2/
+/etc/icingaweb2/authentication.ini
+/etc/icingaweb2/modules
+/etc/icingaweb2/modules/monitoring
+/etc/icingaweb2/modules/monitoring/config.ini
+/etc/icingaweb2/modules/monitoring/instances.ini
+/etc/icingaweb2/modules/monitoring/backends.ini
+/etc/icingaweb2/roles.ini
+/etc/icingaweb2/config.ini
+/etc/icingaweb2/enabledModules
+/etc/icingaweb2/enabledModules/monitoring
+/etc/icingaweb2/enabledModules/doc
+/etc/icingaweb2/resources.ini
+```
+
+Have a look [here](20-Advanced-Topics.md#web-setup-manual-from-source-config) for the contents of the files.
+
+## Kiosk Mode Configuration <a id="kiosk-mode"></a>
+
+Be aware that when you create a kiosk user every person who has access to the kiosk is able to perform tasks on it.
+Therefore you would need to create a user in the `roles.ini` in `/etc/icingaweb2/roles.ini`.
+
+```
+[kioskusers]
+users = "kiosk"
+```
+
+If you need special permissions you should add those permissions to the user via the admin account in icingaweb2 to the role of the kiosk user.
+
+For the Dashboard system where you want to display the kiosk you can add also the following part into the `icingaweb2.conf`.
+So it starts directly into the kiosk mode.
+If you want to show a specific Dashboard you can enforce this onto the kiosk user via the [enforceddashboard](https://github.com/Thomas-Gelf/icingaweb2-module-enforceddashboard) module.
+
+```
+<ifmodule mod_authz_core.c>
+ # Apache 2.4
+ SetEnvIf Remote_Addr "X.X.X.X" REMOTE_USER=kiosk
+ <requireall>
+ Require all granted
+ </requireall>
+</ifmodule>
+```
+
+Replace Remote_Addr with the IP where the kiosk user is accessing the Web to restrict further usage from other IPs.
+
+## Customizing the Landing Page <a id="landing-page"></a>
+
+The default landing page of `dashboard` can be customized using the environment variable `ICINGAWEB_LANDING_PAGE`.
+
+Example on RHEL/CentOS:
+
+```
+vim /etc/httpd/conf.d/web.icinga.com.conf
+
+<VirtualHost *:80>
+
+ ...
+
+ <Directory "/usr/share/icingaweb2/public">
+
+ ...
+
+ SetEnv ICINGAWEB_LANDING_PAGE "icingadb/services/grid?problems"
+
+ ...
+
+ </Directory>
+</VirtualHost>
+```
diff --git a/doc/60-Hooks.md b/doc/60-Hooks.md
new file mode 100644
index 0000000..2dc645d
--- /dev/null
+++ b/doc/60-Hooks.md
@@ -0,0 +1,49 @@
+# Hooks
+
+## ConfigFormEventsHook
+
+The `ConfigFormEventsHook` allows developers to hook into the handling of configuration forms. It provides three methods:
+
+* `appliesTo()`
+* `isValid()`
+* `onSuccess()`
+
+`appliesTo()` determines whether the hook should run for a given configuration form.
+Developers should use `instanceof` checks in order to decide whether the hook should run or not.
+If `appliesTo()` returns `false`, `isValid()` and `onSuccess()` won't get called for this hook.
+
+`isValid()` is called after the configuration form has been validated successfully.
+An exception thrown here indicates form errors and prevents the config from being stored.
+The exception's error message is shown in the frontend automatically.
+If there are multiple hooks indicating errors, every error will be displayed.
+
+`onSuccess()` is called after the configuration has been stored successfully.
+Form handling can't be interrupted here. Any exception will be caught, logged and notified.
+
+Hook example:
+
+```php
+namespace Icinga\Module\Acme\ProvidedHook;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Forms\ConfigForm;
+use Icinga\Forms\Security\RoleForm;
+
+class ConfigFormEvents extends ConfigFormEventsHook
+{
+ public function appliesTo(ConfigForm $form)
+ {
+ return $form instanceof RoleForm;
+ }
+
+ public function onSuccess(ConfigForm $form)
+ {
+ $this->updateMyModuleConfig();
+ }
+
+ protected function updateMyModuleConfig()
+ {
+ // ...
+ }
+}
+```
diff --git a/doc/70-Troubleshooting.md b/doc/70-Troubleshooting.md
new file mode 100644
index 0000000..d71e08a
--- /dev/null
+++ b/doc/70-Troubleshooting.md
@@ -0,0 +1,17 @@
+# Troubleshooting <a id="troubleshooting"></a>
+
+## PageSpeed Module Incompatibility <a id="pagespeed-incompatibility"></a>
+
+It seems that Web 2 is not compatible with the PageSpeed module. Please disable the PageSpeed module using one of the
+following methods.
+
+**Apache**:
+```
+ModPagespeedDisallow "*/icingaweb2/*"
+```
+
+**Nginx**:
+```
+pagespeed Disallow "*/icingaweb2/*";
+```
+
diff --git a/doc/80-Upgrading.md b/doc/80-Upgrading.md
new file mode 100644
index 0000000..c6f4b7b
--- /dev/null
+++ b/doc/80-Upgrading.md
@@ -0,0 +1,435 @@
+# Upgrading Icinga Web 2 <a id="upgrading"></a>
+
+Specific version upgrades are described below. Please note that upgrades are incremental. An upgrade from
+v2.6 to v2.8 requires to follow the instructions for v2.7 too.
+
+## Upgrading to Icinga Web 2.12.0
+
+**Database Schema**
+
+With the latest Icinga Web versions, you no longer need to manually import sql upgrade scripts. Icinga Web `>= 2.12`
+offers you the possibility to perform such migrations in an easy way. You can find and apply all pending migrations
+of your Icinga Web environment in the menu at `System -> Migrations`.
+
+You can still apply the `2.12.0.sql` upgrade script manually, depending on your database vendor.
+For package installations you can find this file in `/usr/share/icingaweb2/schema/*-upgrades/`.
+
+## Upgrading to Icinga Web 2.11.x
+
+**General**
+
+* Support for Internet Explorer 11 has been removed.
+* The Vagrant file and all its assets have been removed.
+
+**Database Schema**
+
+* Please apply the `v2.11.0.sql` upgrade script depending on your database vendor.
+ As of version `2.11.4`, upgrade scripts can be found at `/usr/share/icingaweb2/schema/*-upgrades/`.
+ Older versions install these files to `/usr/share/doc/icingaweb2/schema/*-upgrades/` for RPM-based systems
+ and `/usr/share/icingaweb2/etc/schema/*-upgrades/` for Debian or Ubuntu.
+
+**Breaking changes**
+
+* The `user:local_name` macro in restrictions has been removed. Use `user.local_name` now.
+* User preferences stored in INI files are not loaded anymore. Migrate yours with
+ `icingacli migrate preferences` before the upgrade, if you haven't already.
+
+**Framework changes affecting third-party code**
+
+* When loading library CSS assets, CSS files and LESS files are handled differently now. Only the latter
+ is parsed as LESS.
+* jQuery is not bundled anymore as it's now part of the library icinga-php-thirdparty v0.11.0. It's shipped there
+ in version 3.6.0. (Previously bundled was jQuery 3.4.1)
+* All the following classes have been removed:
+ * `Icinga\User\Preferences\Store\IniStore`: Preferences in INI files are not supported anymore.
+ * `Icinga\User\Preferences\Store\DbStore`: Its methods have been added to the `PreferencesStore` class.
+ * `Icinga\Util\String`: Use `Icinga\Util\StringHelper` instead.
+ * `Icinga\Util\Translator`: Use `\ipl\I18n\StaticTranslator::$instance` or `\ipl\I18n\Translation` instead.
+ * `Icinga\Module\Migrate\Clicommands\DashboardCommand`: Deleted without substitution.
+ * `Icinga\Web\Hook\TicketHook`: Use `Icinga\Application\Hook\TicketHook` instead.
+ * `Icinga\Web\Hook\GrapherHook`: Use `Icinga\Application\Hook\GrapherHook` instead.
+ * `Icinga\Module\Monitoring\Environment`: Not in use.
+ * `Icinga\Module\Monitoring\Backend`: Use `Icinga\Module\Monitoring\Backend\MonitoringBackend` instead.
+* All the following methods have been removed:
+ * `loader.js.addUrlFlag()`: Use `Icinga.Utils.addUrlFlag()` instead.
+ * `Url::setBaseUrl()`: Please create a new url from scratch instead.
+ * `Url::getBaseUrl()`: Use either `Url::getBasePath()` or `Url::getAbsoluteUrl()` now.
+ * `ApplicationBootstrap::setupZendAutoloader()`: Since it does nothing, all usages removed.
+ * `ApplicationBootstrap::listLocales()`: Use `\ipl\I18n\GettextTranslator::listLocales()` instead.
+ * `Module::registerHook()`: Use `provideHook()` instead.
+ * `Web::getMenu()`: Instantiate the menu class `new Menu()` directly instead.
+ * `AesCrypt::encryptToBase64()`: Use `AesCrypt::encrypt()` instead as it also returns a base64 encoded string.
+ * `AesCrypt::decryptFromBase64()`: Use `AesCrypt::decrypt()` instead as it also returns a base64 decoded string.
+ * `InlinePie::disableNoScript()`: Empty method.
+ * `SimpleQuery::paginate()`: Use `Icinga\Web\Controller::setupPaginationControl()` and/or `Icinga\Web\Widget\Paginator` instead.
+ * `LdapConnection::connect()`: The connection is established lazily since .. a long time.
+ * `MonitoredObject::matches()`: Use `$filter->matches($object)` instead.
+ * `MonitoredObject::fromParams()`: Deleted without substitution.
+ * `DataView::fromRequest()`: Use `$backend->select()->from($viewName)` instead.
+ * `DataView::sort()`: Use `DataView::order()` instead.
+ * `MonitoringBackend::createBackend()`: Use `MonitoringBackend::instance()` instead.
+ * `DbConnection::getConnection()`: Use `Connection::getDbAdapter()` instead.
+ * `DbQuery::renderFilter()`: Use `DbConnection::renderFilter()` instead.
+ * `DbQuery::whereToSql()`: Use `DbConnection::renderFilter()` instead.
+
+## Upgrading to Icinga Web 2 2.10.x
+
+**General**
+
+* The theme "solarized-dark" has been removed due to the introduction of the new default dark mode.
+
+**Deprecations**
+
+* Builtin support for PDF exports using the `dompdf` library will be dropped with version 2.12.
+ It is highly recommended to use [Icinga PDF Export](https://github.com/Icinga/icingaweb2-module-pdfexport)
+ instead.
+
+**Discontinued package updates**
+
+* We will stop offering major updates for Debian 9 (Stretch) starting with version 2.11.
+ However, versions 2.9 and 2.10 will continue to receive minor updates on this platform.
+
+[icinga.com](https://icinga.com/subscription/support-details/) provides an overview about
+currently supported distributions.
+
+**Framework changes affecting third-party code**
+
+* Asset support for modules (#3961) introduced with v2.8 has now been removed.
+* `expandable-toggle`-support has been removed. Use `class="collapsible" data-visible-height=0`
+ to achieve the same effect. (Available since v2.7.0)
+* The `.var()` LESS mixin and the LESS function `extract-variable-default` have been removed (introduced with v2.9)
+
+## Upgrading to Icinga Web 2 2.9.1
+
+**Database Schema**
+
+* Please apply the `v2.9.1.sql` upgrade script depending on your database vendor.
+ In package installations this file can be found in `/usr/share/doc/icingaweb2/schema/*-upgrades/`
+ (Debian/Ubuntu: `/usr/share/icingaweb2/etc/schema/*-upgrades/`).
+
+## Upgrading to Icinga Web 2 2.9.x
+
+**Installation**
+
+* Icinga Web 2 now requires the [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (>= 0.6)
+ and [Icinga PHP Thirdparty](https://github.com/Icinga/icinga-php-thirdparty) (>= 0.10). Please make sure to
+ install both when upgrading. We provide packages for them and if you've installed Icinga Web 2 already by
+ package they should be installed automatically during the upgrade.
+* [Icinga Business Process Modelling](https://github.com/Icinga/icingaweb2-module-businessprocess/releases/tag/v2.3.1)
+ has been updated to v2.3.1. If you're using this module, this version is required when upgrading.
+
+**General**
+
+* For database connections to the IDO running on MySQL, a default charset (`latin1`) is now applied.
+ If you had previously problems with special characters and umlauts and you've set this charset
+ already manually, no change is required. However, if your IDO resource configuration has another
+ charset configured than this, it is highly recommended to clear this setting. Otherwise the default
+ won't apply and characters may still be shown incorrectly in the UI.
+
+**Database Schema**
+
+* Icinga Web 2 now permits its users to stay logged in. This requires a new database table.
+ * Please apply the `v2.9.0.sql` upgrade script depending on your database vendor.
+ In package installations this file can be found in `/usr/share/doc/icingaweb2/schema/*-upgrades/`
+ (Debian/Ubuntu: `/usr/share/icingaweb2/etc/schema/*-upgrades/`).
+
+**Breaking changes**
+
+* Password changes are not allowed by default anymore
+ * The fake refusal `no-user/password-change` has now been changed to a grant `user/password-change`.
+ Any user that had `no-user/password-change` previously still cannot change passwords. Though any
+ user that didn't have this *permission*, needs to be granted `user/password-change` now in order
+ to change passwords.
+
+**Deprecations**
+
+* Support for EOL PHP versions (5.6, 7.0, 7.1 and 7.2) will be removed with version 2.11
+* Support for Internet Explorer will be completely removed with version 2.11
+ * New features after v2.9 will already not (necessarily) be available in Internet Explorer
+* `user.local_name` replaces the `user:local_name` macro in restrictions, and the latter will be removed with
+ version 2.11
+* The configuration backend type `INI` is not configurable anymore. **A database is now mandatory.**
+ * Existing configurations using this configuration backend type will stop working with the
+ release of v2.11.
+ * To migrate your local user preferences to database, enable the `migrate` module and use the command
+ `icingacli migrate preferences`. If you already setup the configuration database, it will work right
+ away. If not, pass it the resource you'd like to use as configuration database with `--resource=`.
+ * Note that this only applies to user preferences. Other configurations are still stored
+ in `.ini` files. (#3770)
+* The Vagrant file and all its assets will be removed with version 2.11
+
+**Framework changes affecting third-party code**
+
+* The `jquery-migrate` compatibility layer for Javascript code working with jQuery 2.x has been removed.
+ It has been introduced with v2.7 when we upgraded jQuery to v3.4.1 in order to allow module developers
+ a seamless upgrade chance. If a module still has UI glitches after an upgrade to v2.9, please contact
+ the module developer.
+* The method `getHtmlForEvent` of the `EventDetailsExtensionHook` previously received the host or service
+ object of an event. Now the actual event object is passed to it instead.
+* Asset support for modules (#3961) introduced with v2.8 has now been deprecated in favor of library
+ support (#4272) and will be removed with v2.10. We don't expect broad usage of this feature since
+ it's been introduced with the latest major version, so it's already being removed with the next one.
+
+## Upgrading to Icinga Web 2 2.8.x
+
+**Changes in packaging and dependencies**
+
+Valid for distributions:
+
+* RHEL / CentOS 7
+ * Upgrade to PHP 7.3 via RedHat SCL
+
+After upgrading to version 2.8.0 you'll get the new `rh-php73` dependency installed. This is a drop-in replacement
+for the previous `rh-php71` dependency and only requires the setup of a new fpm service and possibly some copying
+of customized configurations.
+
+**php.ini or php-fpm settings** you have tuned in the past need to be copied over to the new path:
+
+From `/etc/opt/rh/rh-php71/` to `/etc/opt/rh/rh-php73/`.
+
+Don't forget to also install any additional **php-modules** for PHP 7.3 you've had previously installed
+for e.g. Icinga Web 2 modules.
+
+There's also a new **service** included which replaces the previous one for php-fpm:
+
+Stop the old service: `systemctl stop rh-php71-php-fpm.service`
+Start the new service: `systemctl start rh-php73-php-fpm.service`
+
+You can now safely remove the previous dependency if you like:
+
+`yum remove rh-php71*`
+
+**Discontinued package updates**
+
+Icinga Web 2 v2.8+ is not supported on these platforms:
+
+* RHEL / CentOS 6
+* Debian 8 Jessie
+* Ubuntu 16.04 LTS (Xenial Xerus)
+
+Please consider an upgrade of your central Icinga system to a newer distribution release.
+
+[icinga.com](https://icinga.com/subscription/support-details/) provides an overview about
+currently supported distributions.
+
+**Framework changes affecting third-party code**
+
+* Url parameter `view=compact` is now deprecated. `showCompact` should be used instead.
+ Details are in pull request [#4164](https://github.com/Icinga/icingaweb2/pull/4164).
+* Form elements of type checkbox now need to be checked prior submission if they're
+ required. Previously setting `required => true` didn't cause the browser to complain
+ if such a checkbox wasn't checked. Browsers now do complain if so.
+* The general layout now uses flexbox instead of fixed positioning. This applies to the
+ `#header`, `#sidebar`, `#main`, `#footer`, `#col1`, `#col2` and a column's controls.
+ `#sidebar` and `#main` are now additionally wrapped in a new container `#content-wrapper`.
+
+## Upgrading to Icinga Web 2 2.7.x <a id="upgrading-to-2.7.x"></a>
+
+**Breaking changes**
+
+* We've upgraded jQuery to version 3.4.1. If you're a module developer, please add `?_dev` to your address bar to check
+ for log messages emitted by jquery-migrate. (https://github.com/jquery/jquery-migrate) Your javascript code will still
+ work, though jquery-migrate will notify you if you're utilizing deprecated/removed functions. jquery-migrate will be
+ removed with Icinga Web v2.8 and code not adjusted accordingly will stop working.
+* If you're using a language other than english and you've adjusted or disabled module dashboards, you'll need to
+ update all of your `dashboard.ini` files. A CLI command exists to assist you with this task. Enable the `migrate`
+ module and run the following on the host where these files exist: `icingacli migrate dashboard sections --verbose`
+
+## Upgrading to Icinga Web 2 2.6.x <a id="upgrading-to-2.6.x"></a>
+
+* Icinga Web 2 version 2.6.x does not introduce any backward incompatible change.
+
+## Upgrading to Icinga Web 2 2.5.x <a id="upgrading-to-2.5.x"></a>
+
+> **Attention**
+>
+> Icinga Web 2 v2.5 requires **at least PHP 5.6**.
+
+**Breaking changes**
+
+* Hash marks (`#`) in INI files are no longer recognized as comments by
+ [parse_ini_file](https://secure.php.net/manual/en/function.parse-ini-file.php) since PHP 7.0.
+* Existing sessions of logged-in users do no longer work as expected due to a change in the `User` data structure.
+ Everyone who was logged in before the upgrade has to log out once.
+
+**Changes in packaging and dependencies**
+
+Valid for distributions:
+
+* RHEL / CentOS 6 + 7
+ * Upgrading to PHP 7.0 / 7.1 via RedHat SCL (new dependency)
+ * See [Upgrading to FPM](02-Installation.md#upgrading-to-fpm) for manual steps that
+ are required
+* SUSE SLE 12
+ * Upgrading PHP to >= 5.6.0 via the alternative packages.
+ You might have to confirm the replacement of PHP < 5.6 - but that
+ should work with any other PHP app as well.
+ * Make sure to enable the new Apache module `a2enmod php7` and restart `apache2`
+
+**Discontinued package updates**
+
+Icinga Web 2 v2.5+ is not supported on these platforms:
+
+* Debian 7 wheezy
+* Ubuntu 14.04 LTS (trusty)
+* SUSE SLE 11 (all service packs)
+
+Please consider an upgrade of your central Icinga system to a newer distribution release.
+
+[packages.icinga.com](https://packages.icinga.com) provides an overview about currently supported distributions.
+
+**Database schema**
+
+Icinga Web 2 v2.5.0 requires a schema update for the database. The database schema has been adjusted to support
+usernames up to 254 characters. This is necessary to support the new domain-aware authentication feature.
+
+Continue here for [MySQL](80-Upgrading.md#upgrading-mysql-db) and [PostgreSQL](80-Upgrading.md#upgrading-pgsql-db).
+
+## Upgrading to Icinga Web 2 2.4.x <a id="upgrading-to-2.4.x"></a>
+
+* Icinga Web 2 version 2.4.x does not introduce any backward incompatible change.
+
+## Upgrading to Icinga Web 2 2.3.x <a id="upgrading-to-2.3.x"></a>
+
+* Icinga Web 2 version 2.3.x does not introduce any backward incompatible change.
+
+## Upgrading to Icinga Web 2 2.2.0 <a id="upgrading-to-2.2.0"></a>
+
+* 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 <a id="upgrading-to-2.1.x"></a>
+
+* 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 <a id="upgrading-to-2.0.0"></a>
+
+* Icinga Web 2 installations from package on RHEL/CentOS 7 now depend on `php-ZendFramework` which is available through
+ the [EPEL repository](https://fedoraproject.org/wiki/EPEL). Before, Zend was installed as Icinga Web 2 vendor library
+ through the package `icingaweb2-vendor-zend`. After upgrading, please make sure to remove the package
+ `icingaweb2-vendor-zend`.
+
+* Icinga Web 2 version 2.0.0 requires permissions for accessing modules. Those permissions are automatically generated
+ for each installed module in the format `module/<moduleName>`. Administrators have to grant the module permissions to
+ users and/or user groups in the roles configuration for permitting access to specific modules.
+ In addition, restrictions provided by modules are now configurable for each installed module too. Before,
+ a module had to be enabled before having the possibility to configure restrictions.
+
+* The **instances.ini** configuration file provided by the monitoring module
+ has been renamed to **commandtransports.ini**. The content and location of
+ the file remains unchanged.
+
+* The location of a user's preferences has been changed from
+ **&lt;config-dir&gt;/preferences/&lt;username&gt;.ini** to
+ **&lt;config-dir&gt;/preferences/&lt;username&gt;/config.ini**.
+ The content of the file remains unchanged.
+
+## Upgrading to Icinga Web 2 Release Candidate 1 <a id="upgrading-to-rc1"></a>
+
+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.
+
+## Upgrading to Icinga Web 2 Beta 3 <a id="upgrading-to-beta3"></a>
+
+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 <a id="upgrading-to-beta2"></a>
+
+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`.
+
+## Upgrading the MySQL Database <a id="upgrading-mysql-db"></a>
+
+If you installed Icinga Web 2 from package, please check the upgrade scripts located in
+**/usr/share/doc/icingaweb2/schema/mysql-upgrades** (Debian/Ubuntu: **/usr/share/icingaweb2/etc/schema/mysql-upgrades**)
+to update your database schema.
+In case you installed Icinga Web 2 from source, please find the upgrade scripts in **etc/schema/mysql-upgrades**.
+
+> **Note**
+>
+> If there isn't an upgrade file for your current version available, there's nothing to do.
+
+Apply all database schema upgrade files incrementally.
+
+```
+# mysql -u root -p icingaweb2 < /usr/share/doc/icingaweb2/schema/mysql-upgrades/<version>.sql
+```
+
+**Example:** You are upgrading Icinga Web 2 from version `2.4.0` to `2.5.0`. Look into
+the `upgrade` directory:
+
+```
+$ ls /usr/share/doc/icingaweb2/schema/mysql-upgrades/
+2.0.0beta3-2.0.0rc1.sql 2.5.0.sql
+```
+
+The upgrade file `2.5.0.sql` must be applied for the v2.5.0 release. If there are multiple
+upgrade files involved, apply them incrementally.
+
+```
+# mysql -u root -p icinga < /usr/share/doc/icingaweb2/schema/mysql-upgrades/2.5.0.sql
+```
+
+## Upgrading the PostgreSQL Database <a id="upgrading-pgsql-db"></a>
+
+If you installed Icinga Web 2 from package, please check the upgrade scripts located in
+**/usr/share/doc/icingaweb2/schema/pgsql-upgrades** (Debian/Ubuntu: **/usr/share/icingaweb2/etc/schema/pgsql-upgrades**)
+to update your database schema.
+In case you installed Icinga Web 2 from source, please find the upgrade scripts in **etc/schema/pgsql-upgrades**.
+
+> **Note**
+>
+> If there isn't an upgrade file for your current version available, there's nothing to do.
+
+Apply all database schema upgrade files incrementally.
+
+```
+# export PGPASSWORD=icingaweb2
+# psql -U icingaweb2 -d icingaweb2 < /usr/share/doc/icingaweb2/schema/pgsql-upgrades/<version>.sql
+```
+
+**Example:** You are upgrading Icinga Web 2 from version `2.4.0` to `2.5.0`. Look into
+the `upgrade` directory:
+
+```
+$ ls /usr/share/doc/icingaweb2/schema/pgsql-upgrades/
+2.0.0beta3-2.0.0rc1.sql 2.5.0.sql
+```
+
+The upgrade file `2.5.0.sql` must be applied for the v2.5.0 release. If there are multiple
+upgrade files involved, apply them incrementally.
+
+```
+# export PGPASSWORD=icingaweb2
+# psql -U icingaweb2 -d icingaweb2 < /usr/share/doc/icingaweb2/schema/pgsql-upgrades/2.5.0.sql
+```
diff --git a/doc/90-SELinux.md b/doc/90-SELinux.md
new file mode 100644
index 0000000..d19ca82
--- /dev/null
+++ b/doc/90-SELinux.md
@@ -0,0 +1,76 @@
+# SELinux <a id="selinux"></a>
+
+## Introduction <a id="selinux-introduction"></a>
+
+SELinux is a mandatory access control (MAC) system on Linux which adds a fine granular permission system for access
+to all resources on the system such as files, devices, networks and inter-process communication.
+
+The most important questions are answered briefly in the [FAQ of the SELinux Project](https://selinuxproject.org/page/FAQ).
+For more details on SELinux and how to actually use and administrate it on your systems have a look at
+[Red Hat Enterprise Linux 7 - SELinux User's and Administrator's Guide](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/SELinux_Users_and_Administrators_Guide/index.html).
+For a simplified (and funny) introduction download the [SELinux Coloring Book](https://github.com/mairin/selinux-coloring-book).
+
+
+## Policy <a id="selinux-policy"></a>
+
+Icinga Web 2 is providing its own SELinux policy for RPM-based systems running the targeted policy
+which confines Icinga Web 2 with support for all its modules.
+
+The policy for Icinga Web 2 will also require the policy for Icinga 2 which provides access to its interfaces.
+It covers only the scenario running Icinga Web 2 in Apache HTTP Server with mod_php.
+
+Use your distribution's package manager to install the `icingaweb2-selinux` package.
+
+## General <a id="selinux-policy-general"></a>
+
+When the SELinux policy package for Icinga Web 2 is installed, it creates its own type of apache content and labels its
+configuration `icingaweb2_config_t` to allow confining access to it.
+
+## Types <a id="selinux-policy-types"></a>
+
+The configuration is labeled `icingaweb2_config_t` and other services can request access to it by using the interfaces
+`icingaweb2_read_config` and `icingaweb2_manage_config`.
+Files requiring read access are labeled `icingaweb2_content_t`. Files requiring write access are labeled
+`icingaweb2_rw_content_t`.
+
+## Booleans <a id="selinux-policy-booleans"></a>
+
+SELinux is based on the least level of access required for a service to run. Using booleans you can grant more access in
+a defined way. The Icinga Web 2 policy package provides the following booleans.
+
+**httpd_can_manage_icingaweb2_config**
+
+Having this boolean enabled allows httpd to write to the configuration labeled `icingaweb2_config_t`. This is enabled by
+default. If not needed, you can disable it for more security. But this will disable all web based configuration of
+Icinga Web 2.
+
+### Optional Booleans <a id="selinux-optional-booleans"></a>
+
+The Icinga Web 2 policy package does not enable booleans not required by default. In order to allow these things,
+you'll need to enable them manually. (i.e. with the tool `setsebool`)
+
+**Ldap**
+If you want to allow httpd to connect to the ldap port, you must turn on the `httpd_can_connect_ldap` boolean.
+Disabled by default.
+
+## Bugreports <a id="selinux-bugreports"></a>
+
+If you experience any problems while running SELinux in enforcing mode try to reproduce it in permissive mode. If the
+problem persists, it is not related to SELinux because in permissive mode SELinux will not deny anything.
+
+When filing a bug report please add the following information additionally to the
+[common ones](https://icinga.com/icinga/faq/):
+* Output of `semodule -l | grep -e icinga2 -e icingaweb2 -e nagios -e apache`
+* Output of `semanage boolean -l | grep icinga`
+* Output of `ps -eZ | grep httpd`
+* Output of `audit2allow -li /var/log/audit/audit.log`
+
+If access to a file is blocked and you can tell which one, please provided the output of `ls -lZ /path/to/file` and the
+directory above.
+
+If asked for full audit.log, add `-w /etc/shadow -p w` to `/etc/audit/rules.d/audit.rules` and restart the audit daemon.
+Reproduce the problem and add `/var/log/audit/audit.log` to the bug report. The added audit rule includes
+the path of files where access was denied.
+
+If asked to provide full audit log with dontaudit rules disabled, execute `semodule -DB` before reproducing the problem.
+After that enable the rules again to prevent auditd spamming your logfile by executing `semodule -B`.
diff --git a/doc/accessibility/ifont-mute.html b/doc/accessibility/ifont-mute.html
new file mode 100644
index 0000000..f8252e3
--- /dev/null
+++ b/doc/accessibility/ifont-mute.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html class="no-js" lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <title>Accessibility: Muted Icon Fonts</title>
+ <meta name="description" content="Accessible icon fonts">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <style type="text/css">
+ .icon-star:before {
+ content: "★";
+ }
+ </style>
+</head>
+<body>
+<a href="#">
+ <i aria-hidden="true" class="icon-star"></i>
+ Visit top rated article
+</a>
+</body>
+</html>
diff --git a/doc/accessibility/ifont.html b/doc/accessibility/ifont.html
new file mode 100644
index 0000000..32f1221
--- /dev/null
+++ b/doc/accessibility/ifont.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html class="no-js" lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <title>Accessibility: Icon Fonts</title>
+ <meta name="description" content="Accessible icon fonts">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <style type="text/css">
+ .icon-star:before {
+ content: "★";
+ }
+ </style>
+</head>
+<body>
+ <i role="img" class="icon-star" aria-label="Top rated article" title="Top rated article"></i>
+</body>
+</html> \ No newline at end of file
diff --git a/doc/accessibility/link-labels.html b/doc/accessibility/link-labels.html
new file mode 100644
index 0000000..439adb8
--- /dev/null
+++ b/doc/accessibility/link-labels.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<html class="no-js" lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <title>Accessibility: Link Labels</title>
+ <meta name="description" content="Accessible links">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+</head>
+<body>
+ <a href="/monitoring/host/show?host=localhost"
+ title="Show detailed information about the host localhost"
+ aria-label="Show detailed information about the host localhost">localhost</a>
+</body>
+</html> \ No newline at end of file
diff --git a/doc/accessibility/required-form-elements.html b/doc/accessibility/required-form-elements.html
new file mode 100644
index 0000000..86fc937
--- /dev/null
+++ b/doc/accessibility/required-form-elements.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html class="no-js" lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <title>Accessibility: Required form elements</title>
+ <meta name="description" content="Required form elements">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+</head>
+<body>
+ <form>
+ <label>
+ Enter some text:&nbsp;
+ <input type="text" name="some_text" value="" aria-required="true" required>
+ </label>
+ <input type="submit" name="btn_submit" value="Submit">
+ </form>
+</body>
+</html> \ No newline at end of file
diff --git a/doc/accessibility/skip-content.html b/doc/accessibility/skip-content.html
new file mode 100644
index 0000000..3c1b2b4
--- /dev/null
+++ b/doc/accessibility/skip-content.html
@@ -0,0 +1,179 @@
+<!doctype html>
+<html class="no-js" lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <title>Accessibility: Skip Links</title>
+ <meta name="description" content="Accessible skip links">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <style type="text/css">
+ .sr-only {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ }
+ .clearfix {
+ *zoom: 1;
+ }
+ .clearfix:before,
+ .clearfix:after {
+ display: table;
+ content: "";
+ line-height: 0;
+ }
+ .clearfix:after {
+ clear: both;
+ }
+ body {
+ margin: 0;
+ padding: 0;
+ }
+ .head {
+ background-color: #049baf;
+ padding: 5px;
+ }
+ .nav {
+ width: 200px;
+ float: left;
+ margin: 0 20px 20px 0;
+ padding: 0 20px 20px 0;
+ }
+ .container {
+ margin: 10px 0 0 0;
+ }
+ .content {
+ overflow: auto;
+ }
+ .skip-links {
+ position: absolute;
+ opacity: 1;
+ }
+ .skip-links-inline {
+ margin-top: -3.5em;
+ }
+ .skip-links ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ }
+ .skip-links ul li {
+ display: inline;
+ margin: 0;
+ padding: 0;
+ }
+ .skip-links ul li a {
+ position: absolute;
+ display: block;
+ left: -999em;
+ width: 200px;
+ padding: 0.6em;
+ background-color: white;
+ }
+ .skip-links ul li a:focus {
+ left: 0;
+ }
+ </style>
+</head>
+<body>
+ <div class="head">
+ <div class="skip-links">
+ <nav>
+ <h1 class="sr-only">Skip Links</h1>
+ <ul>
+ <li><a tabindex="0" href="#content">Skip to Content</a></li>
+ <li><a tabindex="0" href="#searchField">Skip to Search</a></li>
+ <li><a tabindex="0" href="#navigation">Skip to Navigation</a></li>
+ </ul>
+ </nav>
+ </div>
+ <div id="logo">
+ <a href="skip-content.html">
+ <img width="92" height="32" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFwAAAAgCAYAAACfDx/iAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAABcSAAAXEgFnn9JSAAAAB3RJTUUH3gkEDx0B5aU5bgAACCxJREFUaN7tmn2slmUdx7+/5+UcPFhAeQR0SSnOEodFWSPZdGxGZsWhiZFZq+myl5WZDWchIW4VODa3cDPNNWfFS9aquWmWyaJss1qDcE1txstKFPCEHA7ynPM8n/7gdx1/XN73cx7o0B9xru3e/dzX+/W9vtfv7Xqk40zAdOAhYAjYClzk+RpPY5iAKlAHnuS1ado4Qu1T5TjaVCVNlnRRyGv5e+E4pGMPOJKGJA1k35L0z3FIx16kmL9XZOJk07gMHz0l8GRmEVSZ2WvyA6BmZgD3SPqXpG1m9iBQM7PhcVjbM1aAAV3AJ4CzIpPbtFsInBO+a+MMHz3VnK4AWyW9RVIX0COpATRz5rvcr0s6VdJOoCqpaWbDRSclKFrzehSdnJNGabromCjpXElNz59tZs2c6WYmM2tJ6pU0aGZDZjayKUUget40STNi3kl7EtLCgZuBg8A6YFkmJq4A7gDuAj4GLE2b4SKpnXL9NHAAeAW4H6j5qRiX5cGx6QZuB3qBfwRLpAU0gQawIGunCD7Q5e9mZs3Mcn1x0gNdcSAqDtwZDlDD3feWP8P+BniXb1AVqGT9ngXM9XrD/gDMcU/15FSaQf62gIabfAI+GxSrZZ5mSg+bWW8A+UJJl3udbZK2SLpJ0hqv8pjnlZqhUcb/PypWK2B8zS2RJyS9s03bpgN7naTTJPVIetzMNsW4i5k1gV5Jp5nZ33IbHzAzo+z0FWyERUsnbky7TYonqmSzO5rHaP3HuRQZErWSTahJOrPjXTNbFQaueB+tZOlI2ivpYKrOkdXFDXmzpI9IOsdDBX+X9EMz2wPUzWwokcHNz14vGwE/OWNer0/SeyRNkPSSpMfM7Amg4ic53/Q0jzdKWixpppNuu6SNZrYb6DKzRhvQU9+zzWxrJyQYYbgrzb/SPg0lOZ4rzUwvzAB2ugV0ZWS/v1cFnZCn5cEaSlbPg172k9iP/54FPO96J6YG8Dvg1NBPJIiALwY9k6fVYT2lJyeEq3/teqo6qq4KE1rbAeDDcdIFHmwFWBwsnPXZItd72aGC/g/6+zuhz4luXvYnWgMTfJw5AdzhAsABdjjo1YwYa9rMY9DfG9t54MBs4OXQx9udvB1bL2c4SIcKGJjY/cGySQTAPxra/SiUf8HzXvH3duAmYGkwRdO4lzjLJzkAgwHwHn8PBIsI4F7gBuBh/z7s780Zsfqy8n3A14FbgGc9L5m2i3xNloVFzM3omJ7MT2C72Er6fX4Ju5vA16Lz0wbwqyLgnt8D/CnkbwziLLH/8VD+K8+b4gQYAdzzbwygtYDzMlCvCeVN4OIw3qNhnN8WzOOhUP7nPG7kprS5c5efqJmFsZQCVzxNdrukr7rCmyepW9IOSY9IqqcFHaMZV5V0uqRZ/r1f0uecqYdCvas9EilJT4fYe1FaIqkhqUvSMjN7GqhLGgZOMbMfAEskXeH1F0j6vaRJrlzlMf1rgQmSDgf5fI2kfa5En8nmYa5IP+mxpWS5Vf33bZI+fpTFknmKlchYYE2bk3C9K9daEcvbMLwKvDWyJsjh/ISdCVwavNbXlzB8R5CdF2R9pfDDl8OY93q7qeHEPucnyArmMR14v5/MWJ5OUH8QSRtCn7t9DSP4VHJmm1ky2T4l6VtAdxAdUUv/TNI6SVeV2a8dplZijAfHYtkeSZsljRZjj+MfTv6F92XpPYo53PJnZB4Bk+fN7BEzG5REKAdY5CfFwsnc5thOlTTfyww44mkC50taJmk6sE/SfZIOmdnetJsOaiTWfEmLJH3IZdszbY58budbG8CiudXwumMRB+i4j2Cvf1fSG7xtXdIvzexul+84Jtf5HOuSVrk9v0HSBd7dCjN7INnxNVcwT2Ue5EJJHygKpQYG/kLSXyQ95TJz7Nzfo1nOiQK2zfgt4FZJn3HWVxyXPuDHkvolVYEZki4LOuAe34y7JN3i7c4G5prZHwDVJK0tUGqStNLvKaPHGNk3aGZzkrg5BrHCaCGG4AFW0mL/S7F1POnZ5EFmc5wkqd893qud2UkkbfL1NdzASBu11sMkVpN0acmA57rmHQgXEzGmQbBSjuUWxzpheHK39epfMP6XEVQzs/XADpf3v3E9MiEwviVpZSBQTdKbMkmB13sHcKGZbalIeqFk3AEfhDJQXMFSdtvThuFRCU52Nli24Cbwc9/Yy04Utg6MObl69OoFuST90cw2B+AkqeHs/lJ2YodcYadn2NvUvf/r03FZHRo0kw0q6admdiBn9xgt8t+Sdvr3TEkLHOBuVy74rdKHvf7qgtDwWKRBFx3mFkWfy+9uSV0O7LUOWpcTK/kGSx2riqRvSrrYpcUl/sxPIHu6EpiS7MnvZZ7khrG42Ciyw0P5feFSIwGcypZn87nZ8yeV2OHbgx0+MwaNgscY7fDvh7ars7FWhrY3ZDdWd4ZwQCuUWZEL717o1lBvcZzQNOB9wOn+Xc+jYccJeIylrA/lU4C9WRyjKOC0E3id95UAH8gA3xWCXeeVAP6V0Pf9WUBsVwfz2OOOUhV4IJTf4XOrlIQ4bg2k2pLszYqZ7TazR83sRf8eOkbZXJZeDL/3BhndL+ndOvL3uC4/ns0g1uru+MwzswNm1jKz/V4+MRtjl8tfuU6qHr1mqsEtl6TngjV0UNJcLy+bxz5J7zWzF1yR10Nf69ySa+UXG97/7UE/nXJC70oDy/qAG0vuUc1ZsD+ImJeBFQWBIgGTgW8DZ2dx8m8A8/KAWjaPJcDnC/IrIQj2kouApgekbovzCKJtOXB5UVy8YJ1vA+4Epv4HBqUeGKwVd68AAAAASUVORK5CYII=" />
+ </a>
+ </div>
+ </div>
+ <div class="container clearfix">
+ <div class="nav">
+ <h1 class="sr-only">Navigation</h1>
+
+ <div id="search">
+ <h2 class="sr-only">Search</h2>
+ <form>
+ <fieldset>
+ <label for="searchField">Search</label>
+ <input type="text" name="searchField" id="searchField" />
+ </fieldset>
+ </form>
+ </div>
+ <nav>
+ <h2 id="navigation" tabindex="0" class="sr-only">Site Links</h2>
+ <ul>
+ <li><a href="#">Link1</a></li>
+ <li><a href="#">Link2</a></li>
+ <li><a href="#">Link3</a></li>
+ </ul>
+ </nav>
+ </div>
+ <div class="content">
+ <h1 tabindex="0" id="content">Content</h1>
+ <div class="skip-links skip-links-inline">
+ <nav>
+ <ul>
+ <li><a tabindex="0" href="#content2">Skip content</a></li>
+ </ul>
+ </nav>
+ </div>
+ <p>
+ Organised prehistoric cultures began developing on Bulgarian lands
+ during the Neolithic period. Its ancient history saw the presence
+ of the Thracians and later the Greeks and Romans. The emergence of
+ a unified Bulgarian state dates back to the establishment of the
+ <a href="#">First Bulgarian Empire</a>
+ in 681 CE, which dominated most of the
+ Balkans and functioned as a cultural hub for Slavs during the
+ Middle Ages.
+ </p>
+ <p>
+ With the downfall of the Second Bulgarian Empire in 1396, its
+ <a href="#">territories came under Ottoman</a>
+ rule for nearly five centuries.
+ The Russo-Turkish War (1877–78) led to the formation of the Third
+ Bulgarian State. The following years saw several conflicts with its
+ neighbours, which prompted Bulgaria to align with Germany in both
+ world wars.
+ </p>
+ <p>
+ In 1946 it became a single-party socialist state as part of the
+ Soviet-led Eastern Bloc. In December 1989 the ruling Communist
+ Party allowed multi-party elections, which subsequently led to
+ Bulgaria's transition into a democracy and a market-based economy.
+ </p>
+
+ <h1 tabindex="0" id="content2">Content2</h1>
+ <p>
+ The development of Final Fantasy VIII began in 1997, during the
+ English localization process of Final Fantasy VII. It was produced
+ <a href="#">by Shinji Hashimoto</a>,
+ and directed by Yoshinori Kitase. The music
+ was scored by regular series composer Nobuo Uematsu, and in a
+ series first a vocal piece was written as the game's theme, "Eyes
+ on Me", performed by Faye Wong.
+ </p>
+ <p>
+ The game was positively received by
+ critics,
+ <a href="#">who praised the originality </a>
+ and scope of the game. It was
+ voted the 22nd-best game of all time in 2006 by readers of the
+ Japanese magazine Famitsu. The game was a commercial success;
+ thirteen weeks after its release,
+ </p>
+ </div>
+ </div>
+</body>
+</html> \ No newline at end of file
diff --git a/doc/accessibility/svg.html b/doc/accessibility/svg.html
new file mode 100644
index 0000000..8ee548f
--- /dev/null
+++ b/doc/accessibility/svg.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html class="no-js" lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <title>Accessibility: SVGs</title>
+ <meta name="description" content="Accessible icon fonts">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+</head>
+<body>
+<svg version="1.2" role="img" aria-labelledby="title desc" tabindex="0">
+ <title id="title">A Circle</title>
+ <desc id="desc">A red circle with a black border.</desc>
+ <circle cy="50" cx="50" r="50" stroke="black" stroke-width="1" fill="red" />
+</svg>
+</body>
+</html>
+
+
diff --git a/doc/accessibility/text-cue-for-required-form-control-labels.html b/doc/accessibility/text-cue-for-required-form-control-labels.html
new file mode 100644
index 0000000..1dd38eb
--- /dev/null
+++ b/doc/accessibility/text-cue-for-required-form-control-labels.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<html class="no-js" lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <title>Accessibility: Text cue for required form control labels</title>
+ <meta name="description" content="Text cue for required form control labels">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <style type="text/css">
+ label.required span.required-indicator:after {
+ content: " *";
+ }
+ .sr-only {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ }
+ </style>
+</head>
+<body>
+ <form>
+ <label class="required">
+ Enter some text
+ <span class="required-indicator" aria-hidden="true"></span>
+ <span class="sr-only"> (required)</span>
+ <input type="text" name="some_text" value="" aria-required="true" required>
+ </label>
+ <input type="submit" name="btn_submit" value="Submit">
+ </form>
+</body>
+</html> \ No newline at end of file
diff --git a/doc/phpdoc.xml b/doc/phpdoc.xml
new file mode 100644
index 0000000..0d9b207
--- /dev/null
+++ b/doc/phpdoc.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<phpdoc>
+ <title>Icinga Web 2</title>
+ <parser>
+ <target>./api</target>
+ <extensions>
+ <extension>php</extension>
+ </extensions>
+ </parser>
+ <visibility>public,private,protected</visibility>
+ <transformer>
+ <target>./api</target>
+ </transformer>
+ <logging>
+ <level>quiet</level>
+ </logging>
+ <transformations>
+ <template name="responsive" />
+ </transformations>
+ <files>
+ <directory>../library/Icinga</directory>
+ <directory>../library/application</directory>
+ <directory>../modules/*/library</directory>
+ <directory>../modules/*/application</directory>
+ </files>
+</phpdoc>
diff --git a/doc/res/GraphExample#1.png b/doc/res/GraphExample#1.png
new file mode 100644
index 0000000..fc3fe57
--- /dev/null
+++ b/doc/res/GraphExample#1.png
Binary files differ
diff --git a/doc/res/GraphExample#2.png b/doc/res/GraphExample#2.png
new file mode 100644
index 0000000..f81c4fb
--- /dev/null
+++ b/doc/res/GraphExample#2.png
Binary files differ
diff --git a/doc/res/GraphExample#3.png b/doc/res/GraphExample#3.png
new file mode 100644
index 0000000..0aed97f
--- /dev/null
+++ b/doc/res/GraphExample#3.png
Binary files differ
diff --git a/doc/res/GraphExample#4.png b/doc/res/GraphExample#4.png
new file mode 100644
index 0000000..1555ae3
--- /dev/null
+++ b/doc/res/GraphExample#4.png
Binary files differ
diff --git a/doc/res/GraphExample#5.png b/doc/res/GraphExample#5.png
new file mode 100644
index 0000000..ec659ae
--- /dev/null
+++ b/doc/res/GraphExample#5.png
Binary files differ
diff --git a/doc/res/GraphExample#6.png b/doc/res/GraphExample#6.png
new file mode 100644
index 0000000..1bd44f6
--- /dev/null
+++ b/doc/res/GraphExample#6.png
Binary files differ
diff --git a/doc/res/GraphExample#7.1.png b/doc/res/GraphExample#7.1.png
new file mode 100644
index 0000000..8f67832
--- /dev/null
+++ b/doc/res/GraphExample#7.1.png
Binary files differ
diff --git a/doc/res/GraphExample#7.png b/doc/res/GraphExample#7.png
new file mode 100644
index 0000000..41b762e
--- /dev/null
+++ b/doc/res/GraphExample#7.png
Binary files differ
diff --git a/doc/res/GraphExample#8.png b/doc/res/GraphExample#8.png
new file mode 100644
index 0000000..4c2928e
--- /dev/null
+++ b/doc/res/GraphExample#8.png
Binary files differ
diff --git a/doc/res/GraphExample#9.png b/doc/res/GraphExample#9.png
new file mode 100644
index 0000000..18b1898
--- /dev/null
+++ b/doc/res/GraphExample#9.png
Binary files differ
diff --git a/doc/res/gitlab-job-artifacts.png b/doc/res/gitlab-job-artifacts.png
new file mode 100644
index 0000000..ba7af9a
--- /dev/null
+++ b/doc/res/gitlab-job-artifacts.png
Binary files differ
diff --git a/doc/res/gitlab-rpm-package-pipeline-jobs.png b/doc/res/gitlab-rpm-package-pipeline-jobs.png
new file mode 100644
index 0000000..ed8d50a
--- /dev/null
+++ b/doc/res/gitlab-rpm-package-pipeline-jobs.png
Binary files differ
diff --git a/doc/res/monitoring-module-preview.png b/doc/res/monitoring-module-preview.png
new file mode 100644
index 0000000..d07596b
--- /dev/null
+++ b/doc/res/monitoring-module-preview.png
Binary files differ
diff --git a/etc/bash_completion.d/icingacli b/etc/bash_completion.d/icingacli
new file mode 100644
index 0000000..f9be7bc
--- /dev/null
+++ b/etc/bash_completion.d/icingacli
@@ -0,0 +1,10 @@
+_icingacli_completion()
+{
+ local cur opts
+ opts="${COMP_WORDS[*]}"
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ COMPREPLY=($($opts --autocomplete --autoindex $COMP_CWORD < /dev/null))
+ return 0
+}
+
+complete -F _icingacli_completion icingacli
diff --git a/icingaweb2.ruleset.xml b/icingaweb2.ruleset.xml
new file mode 100644
index 0000000..f1bcb89
--- /dev/null
+++ b/icingaweb2.ruleset.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<!-- PHP Codesniffer ruleset configuration -->
+<ruleset name="icingaweb2">
+ <description>The default PSR-2 standard with specifically excluded non-critical sniffs</description>
+ <!-- Include the whole PSR-2 standard -->
+ <rule ref="PSR2"/>
+ <!-- Exclude patterns for PSR-2 Sniffs -->
+ <rule ref="PSR2.Methods.MethodDeclaration.Underscore">
+ <severity>0</severity>
+ </rule>
+ <rule ref="PSR2.Classes.PropertyDeclaration.Underscore">
+ <severity>0</severity>
+ </rule>
+ <rule ref="PSR1.Methods.CamelCapsMethodName.NotCamelCaps">
+ <severity>0</severity>
+ </rule>
+ <rule ref="PSR1.Files.SideEffects.FoundWithSymbols">
+ <exclude-pattern>library/Icinga/Application/Cli.php</exclude-pattern>
+ <exclude-pattern>library/Icinga/Application/Test.php</exclude-pattern>
+ <exclude-pattern>library/Icinga/Application/StaticWeb.php</exclude-pattern>
+ <exclude-pattern>library/Icinga/Application/EmbeddedWeb.php</exclude-pattern>
+ <exclude-pattern>library/Icinga/Application/functions.php</exclude-pattern>
+ <exclude-pattern>library/Icinga/Application/LegacyWeb.php</exclude-pattern>
+ <exclude-pattern>library/Icinga/Application/Web.php</exclude-pattern>
+ <exclude-pattern>library/Icinga/File/Pdf.php</exclude-pattern>
+ <exclude-pattern>library/Icinga/Util/LessParser.php</exclude-pattern>
+ <exclude-pattern>modules/doc/library/Doc/Renderer/DocSectionRenderer.php</exclude-pattern>
+ <exclude-pattern>modules/monitoring/library/Monitoring/Plugin.php</exclude-pattern>
+ </rule>
+ <rule ref="PSR1.Classes.ClassDeclaration.MultipleClasses">
+ <exclude-pattern>*/test/php/*</exclude-pattern>
+ </rule>
+ <rule ref="PSR1.Files.SideEffects.FoundWithSymbols">
+ <exclude-pattern>*/test/php/*</exclude-pattern>
+ </rule>
+ <rule ref="PSR2.Namespaces.UseDeclaration.UseAfterNamespace">
+ <exclude-pattern>*/test/php/*</exclude-pattern>
+ <exclude-pattern>*/library/Icinga/Test/BaseTestCase.php</exclude-pattern>
+ </rule>
+ <rule ref="PSR1.Classes.ClassDeclaration.MissingNamespace">
+ <exclude-pattern>*/application/views/helpers/*</exclude-pattern>
+ <exclude-pattern>*/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php</exclude-pattern>
+ </rule>
+ <rule ref="Squiz.Classes.ValidClassName.NotCamelCaps">
+ <exclude-pattern>*/application/views/helpers/*</exclude-pattern>
+ <exclude-pattern>*/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php</exclude-pattern>
+ </rule>
+ <rule ref="Generic.Files.LineLength.TooLong">
+ <exclude-pattern>*/modules/monitoring/library/Monitoring/Backend/Ido/Query/*</exclude-pattern>
+ <exclude-pattern>*/modules/monitoring/library/Monitoring/Backend/Livestatus/Query/*</exclude-pattern>
+ </rule>
+</ruleset>
diff --git a/library/Icinga/Application/ApplicationBootstrap.php b/library/Icinga/Application/ApplicationBootstrap.php
new file mode 100644
index 0000000..e484f6c
--- /dev/null
+++ b/library/Icinga/Application/ApplicationBootstrap.php
@@ -0,0 +1,747 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application;
+
+use DirectoryIterator;
+use ErrorException;
+use Exception;
+use Icinga\Application\ProvidedHook\DbMigration;
+use ipl\I18n\GettextTranslator;
+use ipl\I18n\StaticTranslator;
+use LogicException;
+use Icinga\Application\Modules\Manager as ModuleManager;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\NotReadableError;
+use Icinga\Exception\IcingaException;
+
+/**
+ * This class bootstraps a thin Icinga application layer
+ *
+ * Usage example for CLI:
+ * <code>
+ * use Icinga\Application\Cli;
+
+ * Cli::start();
+ * </code>
+ *
+ * Usage example for Icinga Web application:
+ * <code>
+ * use Icinga\Application\Web;
+ * Web::start()->dispatch();
+ * </code>
+ *
+ * Usage example for Icinga-Web 1.x compatibility mode:
+ * <code>
+ * use Icinga\Application\LegacyWeb;
+ * LegacyWeb::start()->setIcingaWebBasedir(ICINGAWEB_BASEDIR)->dispatch();
+ * </code>
+ */
+abstract class ApplicationBootstrap
+{
+ /**
+ * Base directory
+ *
+ * Parent folder for at least application, bin, modules and public
+ *
+ * @var string
+ */
+ protected $baseDir;
+
+ /**
+ * Application directory
+ *
+ * @var string
+ */
+ protected $appDir;
+
+ /**
+ * Icinga library directory
+ *
+ * @var string
+ */
+ protected $libDir;
+
+ /**
+ * Configuration directory
+ *
+ * @var string
+ */
+ protected $configDir;
+
+ /**
+ * Locale directory
+ *
+ * @var string
+ */
+ protected $localeDir;
+
+ /**
+ * Common storage directory
+ *
+ * @var string
+ */
+ protected $storageDir;
+
+ /**
+ * External library paths
+ *
+ * @var string[]
+ */
+ protected $libraryPaths;
+
+ /**
+ * Loaded external libraries
+ *
+ * @var Libraries
+ */
+ protected $libraries;
+
+ /**
+ * Icinga class loader
+ *
+ * @var ClassLoader
+ */
+ private $loader;
+
+ /**
+ * Config object
+ *
+ * @var Config
+ */
+ protected $config;
+
+ /**
+ * Module manager
+ *
+ * @var ModuleManager
+ */
+ private $moduleManager;
+
+ /**
+ * Flag indicates we're on cli environment
+ *
+ * @var bool
+ */
+ protected $isCli = false;
+
+ /**
+ * Flag indicates we're on web environment
+ *
+ * @var bool
+ */
+ protected $isWeb = false;
+
+ /**
+ * Whether Icinga Web 2 requires setup
+ *
+ * @var bool
+ */
+ protected $requiresSetup = false;
+
+ /**
+ * Constructor
+ *
+ * @param string $baseDir Icinga Web 2 base directory
+ * @param string $configDir Path to Icinga Web 2's configuration files
+ * @param string $storageDir Path to Icinga Web 2's stored files
+ */
+ protected function __construct($baseDir = null, $configDir = null, $storageDir = null)
+ {
+ if ($baseDir === null) {
+ $baseDir = dirname($this->getBootstrapDirectory());
+ }
+ $this->baseDir = $baseDir;
+ $this->appDir = $baseDir . '/application';
+ if (substr(__DIR__, 0, 8) === 'phar:///') {
+ $this->libDir = dirname(dirname(__DIR__));
+ } else {
+ $this->libDir = realpath(__DIR__ . '/../..');
+ }
+
+ $this->setupAutoloader();
+
+ if ($configDir === null) {
+ $configDir = getenv('ICINGAWEB_CONFIGDIR');
+ if ($configDir === false) {
+ $configDir = Platform::isWindows()
+ ? $baseDir . '/config'
+ : '/etc/icingaweb2';
+ }
+ }
+ $canonical = realpath($configDir);
+ $this->configDir = $canonical ? $canonical : $configDir;
+
+ if ($storageDir === null) {
+ $storageDir = getenv('ICINGAWEB_STORAGEDIR');
+ if ($storageDir === false) {
+ $storageDir = Platform::isWindows()
+ ? $baseDir . '/storage'
+ : '/var/lib/icingaweb2';
+ }
+ }
+ $canonical = realpath($storageDir);
+ $this->storageDir = $canonical ? $canonical : $storageDir;
+
+ if ($this->libraryPaths === null) {
+ $libraryPaths = getenv('ICINGAWEB_LIBDIR');
+ if ($libraryPaths !== false) {
+ $this->libraryPaths = array_filter(array_map(
+ 'realpath',
+ explode(':', $libraryPaths)
+ ), 'is_dir');
+ } else {
+ $this->libraryPaths = is_dir('/usr/share/icinga-php')
+ ? ['/usr/share/icinga-php']
+ : [];
+ }
+ }
+
+ Icinga::setApp($this);
+
+ require_once dirname(__FILE__) . '/functions.php';
+ }
+
+ /**
+ * Bootstrap interface method for concrete bootstrap objects
+ *
+ * @return mixed
+ */
+ abstract protected function bootstrap();
+
+ /**
+ * Get loaded external libraries
+ *
+ * @return Libraries
+ */
+ public function getLibraries()
+ {
+ return $this->libraries;
+ }
+
+ /**
+ * Getter for module manager
+ *
+ * @return ModuleManager
+ */
+ public function getModuleManager()
+ {
+ return $this->moduleManager;
+ }
+
+ /**
+ * Getter for class loader
+ *
+ * @return ClassLoader
+ */
+ public function getLoader()
+ {
+ return $this->loader;
+ }
+
+ /**
+ * Getter for configuration object
+ *
+ * @return Config
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Flag indicates we're on cli environment
+ *
+ * @return bool
+ */
+ public function isCli()
+ {
+ return $this->isCli;
+ }
+
+ /**
+ * Flag indicates we're on web environment
+ *
+ * @return bool
+ */
+ public function isWeb()
+ {
+ return $this->isWeb;
+ }
+
+ /**
+ * Helper to glue directories together
+ *
+ * @param string $dir
+ * @param string $subdir
+ *
+ * @return string
+ */
+ private function getDirWithSubDir($dir, $subdir = null)
+ {
+ if ($subdir !== null) {
+ $dir .= '/' . ltrim($subdir, '/');
+ }
+
+ return $dir;
+ }
+
+ /**
+ * Get the base directory
+ *
+ * @param string $subDir Optional sub directory to get
+ *
+ * @return string
+ */
+ public function getBaseDir($subDir = null)
+ {
+ return $this->getDirWithSubDir($this->baseDir, $subDir);
+ }
+
+ /**
+ * Get the application directory
+ *
+ * @param string $subDir Optional sub directory to get
+ *
+ * @return string
+ */
+ public function getApplicationDir($subDir = null)
+ {
+ return $this->getDirWithSubDir($this->appDir, $subDir);
+ }
+
+ /**
+ * Get the configuration directory
+ *
+ * @param string $subDir Optional sub directory to get
+ *
+ * @return string
+ */
+ public function getConfigDir($subDir = null)
+ {
+ return $this->getDirWithSubDir($this->configDir, $subDir);
+ }
+
+ /**
+ * Get the common storage directory
+ *
+ * @param string $subDir Optional sub directory to get
+ *
+ * @return string
+ */
+ public function getStorageDir($subDir = null)
+ {
+ return $this->getDirWithSubDir($this->storageDir, $subDir);
+ }
+
+ /**
+ * Get the Icinga library directory
+ *
+ * @param string $subDir Optional sub directory to get
+ *
+ * @return string
+ */
+ public function getLibraryDir($subDir = null)
+ {
+ return $this->getDirWithSubDir($this->libDir, $subDir);
+ }
+
+ /**
+ * Get the path to the bootstrapping directory
+ *
+ * This is usually /public for Web and EmbeddedWeb and /bin for the CLI
+ *
+ * @return string
+ *
+ * @throws LogicException If the base directory can not be detected
+ */
+ public function getBootstrapDirectory()
+ {
+ $script = $_SERVER['SCRIPT_FILENAME'];
+ $canonical = realpath($script);
+ if ($canonical !== false) {
+ $dir = dirname($canonical);
+ } elseif (substr($script, -14) === '/webrouter.php') {
+ // If Icinga Web 2 is served using PHP's built-in webserver with our webrouter.php script, the $_SERVER
+ // variable SCRIPT_FILENAME is set to DOCUMENT_ROOT/webrouter.php which is not a valid path to
+ // realpath but DOCUMENT_ROOT here still is the bootstrapping directory
+ $dir = dirname($script);
+ } else {
+ throw new LogicException('Can\'t detected base directory');
+ }
+ return $dir;
+ }
+
+ /**
+ * Start the bootstrap
+ *
+ * @param string $baseDir Icinga Web 2 base directory
+ * @param string $configDir Path to Icinga Web 2's configuration files
+ *
+ * @return static
+ */
+ public static function start($baseDir = null, $configDir = null)
+ {
+ $application = new static($baseDir, $configDir);
+ $application->bootstrap();
+ return $application;
+ }
+
+ /**
+ * Setup Icinga class loader
+ *
+ * @return $this
+ */
+ public function setupAutoloader()
+ {
+ require_once $this->libDir . '/Icinga/Application/ClassLoader.php';
+
+ $this->loader = new ClassLoader();
+ $this->loader->registerNamespace('Icinga', $this->libDir . '/Icinga');
+ $this->loader->registerNamespace('Icinga', $this->libDir . '/Icinga', $this->appDir);
+ $this->loader->register();
+
+ return $this;
+ }
+
+ /**
+ * Setup module manager
+ *
+ * @return $this
+ */
+ protected function setupModuleManager()
+ {
+ $paths = $this->getAvailableModulePaths();
+ $this->moduleManager = new ModuleManager(
+ $this,
+ $this->configDir . '/enabledModules',
+ $paths
+ );
+ return $this;
+ }
+
+ protected function getAvailableModulePaths()
+ {
+ $paths = [];
+
+ $configured = getenv('ICINGAWEB_MODULES_DIR');
+ if (! $configured) {
+ $configured = $this->config->get('global', 'module_path', $this->baseDir . '/modules');
+ }
+
+ $nextIsPhar = false;
+ foreach (explode(PATH_SEPARATOR, $configured) as $path) {
+ if ($path === 'phar') {
+ $nextIsPhar = true;
+ continue;
+ }
+
+ if ($nextIsPhar) {
+ $nextIsPhar = false;
+ $paths[] = 'phar:' . $path;
+ } else {
+ $paths[] = $path;
+ }
+ }
+
+ return $paths;
+ }
+
+ /**
+ * Load all enabled modules
+ *
+ * @return $this
+ */
+ protected function loadEnabledModules()
+ {
+ try {
+ $this->moduleManager->loadEnabledModules();
+ } catch (NotReadableError $e) {
+ Logger::error(new IcingaException('Cannot load enabled modules. An exception was thrown:', $e));
+ }
+ return $this;
+ }
+
+ /**
+ * Load the setup module if Icinga Web 2 requires setup or the setup token exists
+ *
+ * @return $this
+ */
+ protected function loadSetupModuleIfNecessary()
+ {
+ if (! @file_exists($this->config->resolvePath('authentication.ini'))) {
+ $this->requiresSetup = true;
+ if ($this->moduleManager->hasInstalled('setup')) {
+ $this->moduleManager->loadModule('setup');
+ }
+ } elseif ($this->setupTokenExists()) {
+ // Load setup module but do not require setup
+ if ($this->moduleManager->hasInstalled('setup')) {
+ $this->moduleManager->loadModule('setup');
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Get whether Icinga Web 2 requires setup
+ *
+ * @return bool
+ */
+ public function requiresSetup()
+ {
+ return $this->requiresSetup;
+ }
+
+ /**
+ * Get whether the setup token exists
+ *
+ * @return bool
+ */
+ public function setupTokenExists()
+ {
+ return @file_exists($this->config->resolvePath('setup.token'));
+ }
+
+ /**
+ * Load external libraries
+ *
+ * @return $this
+ */
+ protected function loadLibraries()
+ {
+ $this->libraries = new Libraries();
+ foreach ($this->libraryPaths as $libraryPath) {
+ foreach (new DirectoryIterator($libraryPath) as $path) {
+ if (! $path->isDot() && is_dir($path->getRealPath())) {
+ $this->libraries->registerPath($path->getPathname())
+ ->registerAutoloader();
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Setup default logging
+ *
+ * @return $this
+ */
+ protected function setupLogging()
+ {
+ Logger::create(
+ new ConfigObject(
+ array(
+ 'log' => 'syslog'
+ )
+ )
+ );
+ return $this;
+ }
+
+ /**
+ * Load Configuration
+ *
+ * @return $this
+ */
+ protected function loadConfig()
+ {
+ Config::$configDir = $this->configDir;
+
+ try {
+ $this->config = Config::app();
+ } catch (NotReadableError $e) {
+ Logger::error(new IcingaException('Cannot load application configuration. An exception was thrown:', $e));
+ $this->config = new Config();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Error handling configuration
+ *
+ * @return $this
+ */
+ protected function setupErrorHandling()
+ {
+ error_reporting(E_ALL | E_STRICT);
+ ini_set('display_startup_errors', 1);
+ ini_set('display_errors', 1);
+ set_error_handler(function ($errno, $errstr, $errfile, $errline) {
+ if (! (error_reporting() & $errno)) {
+ // Error was suppressed with the @-operator
+ return false; // Continue with the normal error handler
+ }
+ switch ($errno) {
+ case E_NOTICE:
+ case E_WARNING:
+ case E_STRICT:
+ case E_RECOVERABLE_ERROR:
+ throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
+ }
+ return false; // Continue with the normal error handler
+ });
+ return $this;
+ }
+
+ /**
+ * Set up logger
+ *
+ * @return $this
+ */
+ protected function setupLogger()
+ {
+ if ($this->config->hasSection('logging')) {
+ $loggingConfig = $this->config->getSection('logging');
+
+ try {
+ Logger::create($loggingConfig);
+ } catch (ConfigurationError $e) {
+ Logger::getInstance()->registerConfigError($e->getMessage());
+
+ try {
+ Logger::getInstance()->setLevel($loggingConfig->get('level', Logger::ERROR));
+ } catch (ConfigurationError $e) {
+ Logger::getInstance()->registerConfigError($e->getMessage());
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set up the user backend factory
+ *
+ * @return $this
+ */
+ protected function setupUserBackendFactory()
+ {
+ try {
+ UserBackend::setConfig(Config::app('authentication'));
+ } catch (NotReadableError $e) {
+ Logger::error(
+ new IcingaException('Cannot load user backend configuration. An exception was thrown:', $e)
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Detect the timezone
+ *
+ * @return null|string
+ */
+ protected function detectTimezone()
+ {
+ return null;
+ }
+
+ /**
+ * Set up the timezone
+ *
+ * @return $this
+ */
+ final protected function setupTimezone()
+ {
+ $timezone = $this->detectTimezone();
+ if ($timezone === null || @date_default_timezone_set($timezone) === false) {
+ date_default_timezone_set(@date_default_timezone_get());
+ }
+ return $this;
+ }
+
+ /**
+ * Detect the locale
+ *
+ * @return null|string
+ */
+ protected function detectLocale()
+ {
+ return null;
+ }
+
+ /**
+ * Prepare internationalization using gettext
+ *
+ * @return $this
+ */
+ protected function prepareInternationalization()
+ {
+ StaticTranslator::$instance = (new GettextTranslator())
+ ->setDefaultDomain('icinga');
+
+ return $this;
+ }
+
+ /**
+ * Set up internationalization using gettext
+ *
+ * @return $this
+ */
+ final protected function setupInternationalization()
+ {
+ /** @var GettextTranslator $translator */
+ $translator = StaticTranslator::$instance;
+
+ if ($this->hasLocales()) {
+ $translator->addTranslationDirectory($this->getLocaleDir(), 'icinga');
+ }
+
+ $locale = $this->detectLocale();
+ if ($locale === null) {
+ $locale = $translator->getDefaultLocale();
+ }
+
+ try {
+ $translator->setLocale($locale);
+ } catch (Exception $error) {
+ Logger::error($error);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return string Our locale directory
+ */
+ public function getLocaleDir()
+ {
+ if ($this->localeDir === null) {
+ $L10nLocales = getenv('ICINGAWEB_LOCALEDIR') ?: '/usr/share/icinga-L10n/locale';
+ if (file_exists($L10nLocales) && is_dir($L10nLocales)) {
+ $this->localeDir = $L10nLocales;
+ } else {
+ $this->localeDir = false;
+ }
+ }
+
+ return $this->localeDir;
+ }
+
+ /**
+ * return bool Whether Icinga Web has translations
+ */
+ public function hasLocales()
+ {
+ $localedir = $this->getLocaleDir();
+ return $localedir !== false && file_exists($localedir) && is_dir($localedir);
+ }
+
+ /**
+ * Register all hooks provided by the main application
+ *
+ * @return $this
+ */
+ protected function registerApplicationHooks(): self
+ {
+ Hook::register('DbMigration', DbMigration::class, DbMigration::class);
+
+ return $this;
+ }
+}
diff --git a/library/Icinga/Application/Benchmark.php b/library/Icinga/Application/Benchmark.php
new file mode 100644
index 0000000..32a05ec
--- /dev/null
+++ b/library/Icinga/Application/Benchmark.php
@@ -0,0 +1,300 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga\Application\Benchmark class
+ */
+namespace Icinga\Application;
+
+use Icinga\Util\Format;
+
+/**
+ * This class provides a simple and lightweight benchmark class
+ *
+ * <code>
+ * Benchmark::measure('Program started');
+ * // ...do something...
+ * Benchmark::measure('Task finieshed');
+ * Benchmark::dump();
+ * </code>
+ */
+class Benchmark
+{
+ const TIME = 0x01;
+ const MEMORY = 0x02;
+
+ protected static $instance;
+ protected $start;
+ protected $measures = array();
+
+ /**
+ * Add a measurement to your benchmark
+ *
+ * The same identifier can also be used multiple times
+ *
+ * @param string A comment identifying the current measurement
+ * @return void
+ */
+ public static function measure($message)
+ {
+ self::getInstance()->measures[] = (object) array(
+ 'timestamp' => microtime(true),
+ 'memory_real' => memory_get_usage(true),
+ 'memory' => memory_get_usage(),
+ 'message' => $message
+ );
+ }
+
+ /**
+ * Throws all measurements away
+ *
+ * This empties your measurement table and allows you to restart your
+ * benchmark from scratch
+ *
+ * @return void
+ */
+ public static function reset()
+ {
+ self::$instance = null;
+ }
+
+ /**
+ * Rerieve benchmark start time
+ *
+ * This will give you the timestamp of your first measurement
+ *
+ * @return float
+ */
+ public static function getStartTime()
+ {
+ return self::getInstance()->start;
+ }
+
+ /**
+ * Dump benchmark data
+ *
+ * Will dump a text table if running on CLI and a simple HTML table
+ * otherwise. Use Benchmark::TIME and Benchmark::MEMORY to choose whether
+ * you prefer to show either time or memory or both in your output
+ *
+ * @param ?int $what Whether to get time and/or memory summary
+ */
+ public static function dump($what = null)
+ {
+ if (Icinga::app()->isCli()) {
+ echo self::renderToText($what);
+ } else {
+ echo self::renderToHtml($what);
+ }
+ }
+
+ /**
+ * Render benchmark data to a simple text table
+ *
+ * Use Benchmark::TIME and Icinga::MEMORY to choose whether you prefer to
+ * show either time or memory or both in your output
+ *
+ * @param ?int $what Whether to get time and/or memory summary
+ * @return string
+ */
+ public static function renderToText($what = null)
+ {
+ $data = self::prepareDataForRendering($what);
+ $sep = '+';
+ $title = '|';
+ foreach ($data->columns as & $col) {
+ $col->format = ' %'
+ . ($col->align === 'right' ? '' : '-')
+ . $col->maxlen . 's |';
+
+ $sep .= str_repeat('-', $col->maxlen) . '--+';
+ $title .= sprintf($col->format, $col->title);
+ }
+
+ $out = $sep . "\n" . $title . "\n" . $sep . "\n";
+ foreach ($data->rows as & $row) {
+ $r = '|';
+ foreach ($data->columns as $key => & $col) {
+ $r .= sprintf($col->format, $row[$key]);
+ }
+ $out .= $r . "\n";
+ }
+
+ $out .= $sep . "\n";
+ return $out;
+ }
+
+ /**
+ * Render benchmark data to a simple HTML table
+ *
+ * Use Benchmark::TIME and Benchmark::MEMORY to choose whether you prefer
+ * to show either time or memory or both in your output
+ *
+ * @param ?int $what Whether to get time and/or memory summary
+ *
+ * @return string
+ */
+ public static function renderToHtml($what = null)
+ {
+ $data = self::prepareDataForRendering($what);
+
+ // TODO: Move formatting to CSS file
+ $html = '<table class="benchmark">' . "\n" . '<tr>';
+ foreach ($data->columns as & $col) {
+ if ($col->title === 'Time') {
+ continue;
+ }
+ $html .= sprintf(
+ '<td align="%s">%s</td>',
+ $col->align,
+ htmlspecialchars($col->title)
+ );
+ }
+ $html .= "</tr>\n";
+
+ foreach ($data->rows as & $row) {
+ $html .= '<tr>';
+ foreach ($data->columns as $key => & $col) {
+ if ($col->title === 'Time') {
+ continue;
+ }
+ $html .= sprintf(
+ '<td align="%s">%s</td>',
+ $col->align,
+ $row[$key]
+ );
+ }
+ $html .= "</tr>\n";
+ }
+ $html .= "</table>\n";
+ return $html;
+ }
+
+ /**
+ * Prepares benchmark data for output
+ *
+ * Use Benchmark::TIME and Benchmark::MEMORY to choose whether you prefer
+ * to have either time or memory or both in your output
+ *
+ * @param ?int $what Whether to get time and/or memory summary
+ *
+ * @return object
+ */
+ protected static function prepareDataForRendering($what = null)
+ {
+ if ($what === null) {
+ $what = self::TIME | self::MEMORY;
+ }
+
+ $columns = array(
+ (object) array(
+ 'title' => 'Time',
+ 'align' => 'left',
+ 'maxlen' => 4
+ ),
+ (object) array(
+ 'title' => 'Description',
+ 'align' => 'left',
+ 'maxlen' => 11
+ )
+ );
+ if ($what & self::TIME) {
+ $columns[] = (object) array(
+ 'title' => 'Off (ms)',
+ 'align' => 'right',
+ 'maxlen' => 11
+ );
+ $columns[] = (object) array(
+ 'title' => 'Dur (ms)',
+ 'align' => 'right',
+ 'maxlen' => 13
+ );
+ }
+ if ($what & self::MEMORY) {
+ $columns[] = (object) array(
+ 'title' => 'Mem (diff)',
+ 'align' => 'right',
+ 'maxlen' => 10
+ );
+ $columns[] = (object) array(
+ 'title' => 'Mem (total)',
+ 'align' => 'right',
+ 'maxlen' => 11
+ );
+ }
+
+ $bench = self::getInstance();
+ $last = $bench->start;
+ $rows = array();
+ $lastmem = 0;
+ foreach ($bench->measures as $m) {
+ $micro = sprintf(
+ '%03d',
+ round(($m->timestamp - floor($m->timestamp)) * 1000)
+ );
+ $vals = array(
+ date('H:i:s', (int) $m->timestamp) . '.' . $micro,
+ $m->message
+ );
+
+ if ($what & self::TIME) {
+ $m->relative = $m->timestamp - $bench->start;
+ $m->offset = $m->timestamp - $last;
+ $last = $m->timestamp;
+ $vals[] = sprintf('%0.3f', $m->relative * 1000);
+ $vals[] = sprintf('%0.3f', $m->offset * 1000);
+ }
+
+ if ($what & self::MEMORY) {
+ $mem = $m->memory - $lastmem;
+ $lastmem = $m->memory;
+ $vals[] = Format::bytes($mem);
+ $vals[] = Format::bytes($m->memory);
+ }
+
+ $row = & $rows[];
+ foreach ($vals as $col => $val) {
+ $row[$col] = $val;
+ $columns[$col]->maxlen = max(
+ strlen($val),
+ $columns[$col]->maxlen
+ );
+ }
+ }
+
+ return (object) array(
+ 'columns' => $columns,
+ 'rows' => $rows
+ );
+ }
+
+ /**
+ * Singleton
+ *
+ * Benchmark is run only once, but you are not allowed to directly access
+ * the getInstance() method
+ *
+ * @return self
+ */
+ protected static function getInstance()
+ {
+ if (self::$instance === null) {
+ self::$instance = new Benchmark();
+ self::$instance->start = microtime(true);
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Constructor
+ *
+ * Singleton usage is enforced, the only way to instantiate Benchmark is by
+ * starting your measurements
+ *
+ * @return void
+ */
+ protected function __construct()
+ {
+ }
+}
diff --git a/library/Icinga/Application/ClassLoader.php b/library/Icinga/Application/ClassLoader.php
new file mode 100644
index 0000000..71b4d3e
--- /dev/null
+++ b/library/Icinga/Application/ClassLoader.php
@@ -0,0 +1,306 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application;
+
+/**
+ * PSR-4 class loader
+ */
+class ClassLoader
+{
+ /**
+ * Namespace separator
+ */
+ const NAMESPACE_SEPARATOR = '\\';
+
+ /**
+ * Icinga Web 2 module namespace prefix
+ */
+ const MODULE_PREFIX = 'Icinga\\Module\\';
+
+ /**
+ * Icinga Web 2 module namespace prefix length
+ *
+ * Helps to make substr/strpos operations even faster
+ */
+ const MODULE_PREFIX_LENGTH = 14;
+
+ /**
+ * A hardcoded class/subdir map for application ns prefixes
+ *
+ * When a module registers with an application directory, those
+ * namespace prefixes (after the module prefix) will be looked up
+ * in the corresponding application subdirectories
+ *
+ * @var array
+ */
+ protected $applicationPrefixes = array(
+ 'Clicommands' => 'clicommands',
+ 'Controllers' => 'controllers',
+ 'Forms' => 'forms'
+ );
+
+ /**
+ * Whether we already instantiated the ZF autoloader
+ *
+ * @var boolean
+ */
+ protected $gotZend = false;
+
+ /**
+ * Namespaces
+ *
+ * @var array
+ */
+ private $namespaces = array();
+
+ /**
+ * Application directories
+ *
+ * @var array
+ */
+ private $applicationDirectories = array();
+
+ /**
+ * Register a base directory for a namespace prefix
+ *
+ * Application directory is optional and provides additional lookup
+ * logic for hardcoded namespaces like "Forms"
+ *
+ * @param string $namespace
+ * @param string $directory
+ * @param string $appDirectory
+ *
+ * @return $this
+ */
+ public function registerNamespace($namespace, $directory, $appDirectory = null)
+ {
+ $this->namespaces[$namespace] = $directory;
+
+ if ($appDirectory !== null) {
+ $this->applicationDirectories[$namespace] = $appDirectory;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Test whether a namespace exists
+ *
+ * @param string $namespace
+ *
+ * @return bool
+ */
+ public function hasNamespace($namespace)
+ {
+ return array_key_exists($namespace, $this->namespaces);
+ }
+
+ /**
+ * Get the source file of the given class or interface
+ *
+ * @param string $class Name of the class or interface
+ *
+ * @return string|null
+ */
+ public function getSourceFile($class)
+ {
+ if ($file = $this->getModuleSourceFile($class)) {
+ return $file;
+ }
+
+ foreach ($this->namespaces as $namespace => $dir) {
+ if ($class === strstr($class, "$namespace\\")) {
+ return $this->buildClassFilename($class, $namespace);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the source file of the given module class or interface
+ *
+ * @param string $class Module class or interface name
+ *
+ * @return string|null
+ */
+ protected function getModuleSourceFile($class)
+ {
+ if (! $this->classBelongsToModule($class)) {
+ return null;
+ }
+
+ $modules = Icinga::app()->getModuleManager();
+ $namespace = $this->extractModuleNamespace($class);
+
+ if ($this->hasNamespace($namespace)) {
+ return $this->buildClassFilename($class, $namespace);
+ } elseif (! $modules->loadedAllEnabledModules()) {
+ $moduleName = $this->extractModuleName($class);
+
+ if ($modules->hasEnabled($moduleName)) {
+ $modules->loadModule($moduleName);
+
+ return $this->buildClassFilename($class, $namespace);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Extract the Icinga module namespace from a given namespaced class name
+ *
+ * Does no validation, prefix must have been checked before
+ *
+ * @return string
+ */
+ protected function extractModuleNamespace($class)
+ {
+ return substr(
+ $class,
+ 0,
+ strpos($class, self::NAMESPACE_SEPARATOR, self::MODULE_PREFIX_LENGTH + 1)
+ );
+ }
+
+ /**
+ * Extract the Icinga module name from a given namespaced class name
+ *
+ * Does no validation, prefix must have been checked before
+ *
+ * @return string
+ */
+ public static function extractModuleName($class)
+ {
+ return lcfirst(
+ substr(
+ $class,
+ self::MODULE_PREFIX_LENGTH,
+ strpos(
+ $class,
+ self::NAMESPACE_SEPARATOR,
+ self::MODULE_PREFIX_LENGTH + 1
+ ) - self::MODULE_PREFIX_LENGTH
+ )
+ );
+ }
+
+ /**
+ * Whether the given class name belongs to a module namespace
+ *
+ * @return boolean
+ */
+ public static function classBelongsToModule($class)
+ {
+ return substr($class, 0, self::MODULE_PREFIX_LENGTH) === self::MODULE_PREFIX;
+ }
+
+ /**
+ * Prepare a filename string for the given class
+ *
+ * Expects the given namespace to be registered with a path name
+ *
+ * @return string
+ */
+ protected function buildClassFilename($class, $namespace)
+ {
+ $relNs = substr($class, strlen($namespace) + 1);
+
+ if ($this->namespaceHasApplictionDirectory($namespace)) {
+ $prefixSeparator = strpos($relNs, self::NAMESPACE_SEPARATOR);
+ $prefix = substr($relNs, 0, $prefixSeparator);
+
+ if ($this->isApplicationPrefix($prefix)) {
+ return $this->applicationDirectories[$namespace]
+ . DIRECTORY_SEPARATOR
+ . $this->applicationPrefixes[$prefix]
+ . $this->classToRelativePhpFilename(substr($relNs, $prefixSeparator));
+ }
+ }
+
+ return $this->namespaces[$namespace] . DIRECTORY_SEPARATOR . $this->classToRelativePhpFilename($relNs);
+ }
+
+ /**
+ * Return the relative file name for the given (namespaces) class
+ *
+ * @param string $class
+ *
+ * @return string
+ */
+ protected function classToRelativePhpFilename($class)
+ {
+ return str_replace(
+ self::NAMESPACE_SEPARATOR,
+ DIRECTORY_SEPARATOR,
+ $class
+ ) . '.php';
+ }
+
+ /**
+ * Whether given prefix (Forms, Controllers...) makes part of "application"
+ *
+ * @param string $prefix
+ *
+ * @return boolean
+ */
+ protected function isApplicationPrefix($prefix)
+ {
+ return array_key_exists($prefix, $this->applicationPrefixes);
+ }
+
+ /**
+ * Whether the given namespace registered an application directory
+ *
+ * @return boolean
+ */
+ protected function namespaceHasApplictionDirectory($namespace)
+ {
+ return array_key_exists($namespace, $this->applicationDirectories);
+ }
+
+ /**
+ * Load the given class or interface
+ *
+ * @param string $class Name of the class or interface
+ *
+ * @return bool Whether the class or interface has been loaded
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->getSourceFile($class)) {
+ if (file_exists($file)) {
+ require $file;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Register {@link loadClass()} as an autoloader
+ */
+ public function register()
+ {
+ spl_autoload_register(array($this, 'loadClass'));
+ }
+
+ /**
+ * Unregister {@link loadClass()} as an autoloader
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+ }
+
+ /**
+ * Unregister this as an autoloader
+ */
+ public function __destruct()
+ {
+ $this->unregister();
+ }
+}
diff --git a/library/Icinga/Application/Cli.php b/library/Icinga/Application/Cli.php
new file mode 100644
index 0000000..3b93738
--- /dev/null
+++ b/library/Icinga/Application/Cli.php
@@ -0,0 +1,211 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application;
+
+use Icinga\Application\Platform;
+use Icinga\Application\ApplicationBootstrap;
+use Icinga\Authentication\Auth;
+use Icinga\Cli\Params;
+use Icinga\Cli\Loader;
+use Icinga\Cli\Screen;
+use Icinga\Application\Logger;
+use Icinga\Application\Benchmark;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\ProgrammingError;
+use Icinga\User;
+
+require_once __DIR__ . '/ApplicationBootstrap.php';
+
+class Cli extends ApplicationBootstrap
+{
+ protected $isCli = true;
+
+ protected $params;
+
+ protected $showBenchmark = false;
+
+ protected $watchTimeout;
+
+ protected $cliLoader;
+
+ protected $verbose;
+
+ protected $debug;
+
+ protected function bootstrap()
+ {
+ $this->assertRunningOnCli();
+ $this->setupLogging()
+ ->setupErrorHandling()
+ ->loadLibraries()
+ ->loadConfig()
+ ->setupTimezone()
+ ->prepareInternationalization()
+ ->setupInternationalization()
+ ->parseBasicParams()
+ ->setupLogger()
+ ->setupModuleManager()
+ ->setupUserBackendFactory()
+ ->loadSetupModuleIfNecessary()
+ ->setupFakeAuthentication()
+ ->registerApplicationHooks();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setupLogging()
+ {
+ Logger::create(
+ new ConfigObject(
+ array(
+ 'log' => 'stderr'
+ )
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setupLogger()
+ {
+ $config = new ConfigObject();
+ $config->log = $this->params->shift('log', 'stderr');
+ if ($config->log === 'file') {
+ $config->file = $this->params->shiftRequired('log-path');
+ } elseif ($config->log === 'syslog') {
+ $config->application = 'icingacli';
+ }
+
+ if ($this->params->get('verbose', false)) {
+ $config->level = Logger::INFO;
+ } elseif ($this->params->get('debug', false)) {
+ $config->level = Logger::DEBUG;
+ } else {
+ $config->level = Logger::WARNING;
+ }
+
+ Logger::create($config);
+ return $this;
+ }
+
+ protected function setupFakeAuthentication()
+ {
+ Auth::getInstance()->setUser(new User('cli'));
+
+ return $this;
+ }
+
+ public function cliLoader()
+ {
+ if ($this->cliLoader === null) {
+ $this->cliLoader = new Loader($this);
+ }
+ return $this->cliLoader;
+ }
+
+ protected function parseBasicParams()
+ {
+ $this->params = Params::parse();
+ if ($this->params->shift('help')) {
+ $this->params->unshift('help');
+ }
+ if ($this->params->shift('version')) {
+ $this->params->unshift('version');
+ }
+ if ($this->params->shift('autocomplete')) {
+ $this->params->unshift('autocomplete');
+ }
+
+ $watch = $this->params->shift('watch');
+ if ($watch === true) {
+ $this->watchTimeout = 5;
+ } elseif (is_numeric($watch)) {
+ $this->watchTimeout = (int) $watch;
+ }
+
+ $this->debug = (int) $this->params->get('debug');
+ $this->verbose = (int) $this->params->get('verbose');
+
+ $this->showBenchmark = (bool) $this->params->shift('benchmark');
+ return $this;
+ }
+
+ public function getParams()
+ {
+ return $this->params;
+ }
+
+ public function dispatchModule($name, $basedir = null)
+ {
+ $this->getModuleManager()->loadModule($name, $basedir);
+ $this->cliLoader()->setModuleName($name);
+ $this->dispatch();
+ }
+
+ public function dispatch()
+ {
+ Benchmark::measure('Dispatching CLI command');
+
+ if ($this->watchTimeout === null) {
+ $this->dispatchOnce();
+ } else {
+ $this->dispatchEndless();
+ }
+ }
+
+ protected function dispatchOnce()
+ {
+ $loader = $this->cliLoader();
+ $loader->parseParams();
+ $result = $loader->dispatch();
+ Benchmark::measure('All done');
+ if ($this->showBenchmark) {
+ Benchmark::dump();
+ }
+ if ($result === false) {
+ exit(3);
+ }
+ }
+
+ protected function dispatchEndless()
+ {
+ $loader = $this->cliLoader();
+ $loader->parseParams();
+ $screen = Screen::instance();
+
+ while (true) {
+ Benchmark::measure('Watch mode - loop begins');
+ ob_start();
+ $params = clone($this->params);
+ $loader->dispatch($params);
+ Benchmark::measure('Dispatch done');
+ if ($this->showBenchmark) {
+ Benchmark::dump();
+ }
+ Benchmark::reset();
+ $out = ob_get_contents();
+ ob_end_clean();
+ echo $screen->clear() . $out;
+ sleep($this->watchTimeout);
+ }
+ }
+
+ /**
+ * Fail if Icinga has not been called on CLI
+ *
+ * @throws ProgrammingError
+ * @return void
+ */
+ protected function assertRunningOnCli()
+ {
+ if (Platform::isCli()) {
+ return;
+ }
+ throw new ProgrammingError('Icinga is not running on CLI');
+ }
+}
diff --git a/library/Icinga/Application/Config.php b/library/Icinga/Application/Config.php
new file mode 100644
index 0000000..80fe3b8
--- /dev/null
+++ b/library/Icinga/Application/Config.php
@@ -0,0 +1,498 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application;
+
+use Icinga\Exception\NotWritableError;
+use Iterator;
+use Countable;
+use LogicException;
+use UnexpectedValueException;
+use Icinga\Util\File;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Selectable;
+use Icinga\Data\SimpleQuery;
+use Icinga\File\Ini\IniWriter;
+use Icinga\File\Ini\IniParser;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotReadableError;
+use Icinga\Web\Navigation\Navigation;
+
+/**
+ * Container for INI like configuration and global registry of application and module related configuration.
+ */
+class Config implements Countable, Iterator, Selectable
+{
+ /**
+ * Configuration directory where ALL (application and module) configuration is located
+ *
+ * @var string
+ */
+ public static $configDir;
+
+ /**
+ * Application config instances per file
+ *
+ * @var array
+ */
+ protected static $app = array();
+
+ /**
+ * Module config instances per file
+ *
+ * @var array
+ */
+ protected static $modules = array();
+
+ /**
+ * Navigation config instances per type
+ *
+ * @var array
+ */
+ protected static $navigation = array();
+
+ /**
+ * The internal ConfigObject
+ *
+ * @var ConfigObject
+ */
+ protected $config;
+
+ /**
+ * The INI file this config has been loaded from or should be written to
+ *
+ * @var string
+ */
+ protected $configFile;
+
+ /**
+ * Create a new config
+ *
+ * @param ConfigObject $config The config object to handle
+ */
+ public function __construct(ConfigObject $config = null)
+ {
+ $this->config = $config !== null ? $config : new ConfigObject();
+ }
+
+ /**
+ * Return this config's file path
+ *
+ * @return string
+ */
+ public function getConfigFile()
+ {
+ return $this->configFile;
+ }
+
+ /**
+ * Set this config's file path
+ *
+ * @param string $filepath The path to the ini file
+ *
+ * @return $this
+ */
+ public function setConfigFile($filepath)
+ {
+ $this->configFile = $filepath;
+ return $this;
+ }
+
+ /**
+ * Return the internal ConfigObject
+ *
+ * @return ConfigObject
+ */
+ public function getConfigObject()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Provide a query for the internal config object
+ *
+ * @return SimpleQuery
+ */
+ public function select()
+ {
+ return $this->config->select();
+ }
+
+ /**
+ * Return the count of available sections
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return $this->select()->count();
+ }
+
+ /**
+ * Reset the current position of the internal config object
+ *
+ * @return void
+ */
+ public function rewind(): void
+ {
+ $this->config->rewind();
+ }
+
+ /**
+ * Return the section of the current iteration
+ *
+ * @return ConfigObject
+ */
+ public function current(): ConfigObject
+ {
+ return $this->config->current();
+ }
+
+ /**
+ * Return whether the position of the current iteration is valid
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ return $this->config->valid();
+ }
+
+ /**
+ * Return the section's name of the current iteration
+ *
+ * @return string
+ */
+ public function key(): string
+ {
+ return $this->config->key();
+ }
+
+ /**
+ * Advance the position of the current iteration and return the new section
+ *
+ * @return void
+ */
+ public function next(): void
+ {
+ $this->config->next();
+ }
+
+ /**
+ * Return whether this config has any sections
+ *
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ return $this->config->isEmpty();
+ }
+
+ /**
+ * Return this config's section names
+ *
+ * @return array
+ */
+ public function keys()
+ {
+ return $this->config->keys();
+ }
+
+ /**
+ * Return this config's data as associative array
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ return $this->config->toArray();
+ }
+
+ /**
+ * Return the value from a section's property
+ *
+ * @param string $section The section where the given property can be found
+ * @param string $key The section's property to fetch the value from
+ * @param mixed $default The value to return in case the section or the property is missing
+ *
+ * @return mixed
+ *
+ * @throws UnexpectedValueException In case the given section does not hold any configuration
+ */
+ public function get($section, $key, $default = null)
+ {
+ $value = $this->config->$section;
+ if ($value instanceof ConfigObject) {
+ $value = $value->$key;
+ } elseif ($value !== null) {
+ throw new UnexpectedValueException(
+ sprintf('Value "%s" is not of type "%s" or a sub-type of it', $value, get_class($this->config))
+ );
+ }
+
+ if ($value === null && $default !== null) {
+ $value = $default;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Return the given section
+ *
+ * @param string $name The section's name
+ *
+ * @return ConfigObject
+ */
+ public function getSection($name)
+ {
+ $section = $this->config->get($name);
+ return $section !== null ? $section : new ConfigObject();
+ }
+
+ /**
+ * Set or replace a section
+ *
+ * @param string $name
+ * @param array|ConfigObject $config
+ *
+ * @return $this
+ */
+ public function setSection($name, $config = null)
+ {
+ if ($config === null) {
+ $config = new ConfigObject();
+ } elseif (! $config instanceof ConfigObject) {
+ $config = new ConfigObject($config);
+ }
+
+ $this->config->$name = $config;
+ return $this;
+ }
+
+ /**
+ * Remove a section
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function removeSection($name)
+ {
+ unset($this->config->$name);
+ return $this;
+ }
+
+ /**
+ * Return whether the given section exists
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function hasSection($name)
+ {
+ return isset($this->config->$name);
+ }
+
+ /**
+ * Initialize a new config using the given array
+ *
+ * The returned config has no file associated to it.
+ *
+ * @param array $array The array to initialize the config with
+ *
+ * @return Config
+ */
+ public static function fromArray(array $array)
+ {
+ return new static(new ConfigObject($array));
+ }
+
+ /**
+ * Load configuration from the given INI file
+ *
+ * @param string $file The file to parse
+ *
+ * @throws NotReadableError When the file cannot be read
+ */
+ public static function fromIni($file)
+ {
+ $emptyConfig = new static();
+
+ $filepath = realpath($file);
+ if ($filepath === false) {
+ $emptyConfig->setConfigFile($file);
+ } elseif (is_readable($filepath)) {
+ return IniParser::parseIniFile($filepath);
+ } elseif (@file_exists($filepath)) {
+ throw new NotReadableError(t('Cannot read config file "%s". Permission denied'), $filepath);
+ }
+
+ return $emptyConfig;
+ }
+
+ /**
+ * Save configuration to the given INI file
+ *
+ * @param string|null $filePath The path to the INI file or null in case this config's path should be used
+ * @param int $fileMode The file mode to store the file with
+ *
+ * @throws LogicException In case this config has no path and none is passed in either
+ * @throws NotWritableError In case the INI file cannot be written
+ *
+ * @todo create basepath and throw NotWritableError in case its not possible
+ */
+ public function saveIni($filePath = null, $fileMode = 0660)
+ {
+ if ($filePath === null && $this->configFile) {
+ $filePath = $this->configFile;
+ } elseif ($filePath === null) {
+ throw new LogicException('You need to pass $filePath or set a path using Config::setConfigFile()');
+ }
+
+ if (! file_exists($filePath)) {
+ File::create($filePath, $fileMode);
+ }
+
+ $this->getIniWriter($filePath, $fileMode)->write();
+ }
+
+ /**
+ * Return a IniWriter for this config
+ *
+ * @param string|null $filePath
+ * @param int $fileMode
+ *
+ * @return IniWriter
+ */
+ protected function getIniWriter($filePath = null, $fileMode = null)
+ {
+ return new IniWriter($this, $filePath, $fileMode);
+ }
+
+ /**
+ * Prepend configuration base dir to the given relative path
+ *
+ * @param string $path A relative path
+ *
+ * @return string
+ */
+ public static function resolvePath($path)
+ {
+ return self::$configDir . DIRECTORY_SEPARATOR . ltrim($path, DIRECTORY_SEPARATOR);
+ }
+
+ /**
+ * Retrieve a application config
+ *
+ * @param string $configname The configuration name (without ini suffix) to read and return
+ * @param bool $fromDisk When set true, the configuration will be read from disk, even
+ * if it already has been read
+ *
+ * @return Config The requested configuration
+ */
+ public static function app($configname = 'config', $fromDisk = false)
+ {
+ if (! isset(self::$app[$configname]) || $fromDisk) {
+ self::$app[$configname] = static::fromIni(static::resolvePath($configname . '.ini'));
+ }
+
+ return self::$app[$configname];
+ }
+
+ /**
+ * Retrieve a module config
+ *
+ * @param string $modulename The name of the module where to look for the requested configuration
+ * @param string $configname The configuration name (without ini suffix) to read and return
+ * @param bool $fromDisk When set true, the configuration will be read from disk, even
+ * if it already has been read
+ *
+ * @return Config The requested configuration
+ */
+ public static function module($modulename, $configname = 'config', $fromDisk = false)
+ {
+ if (! isset(self::$modules[$modulename])) {
+ self::$modules[$modulename] = array();
+ }
+
+ if (! isset(self::$modules[$modulename][$configname]) || $fromDisk) {
+ self::$modules[$modulename][$configname] = static::fromIni(
+ static::resolvePath('modules/' . $modulename . '/' . $configname . '.ini')
+ );
+ }
+ return self::$modules[$modulename][$configname];
+ }
+
+ /**
+ * Retrieve a navigation config
+ *
+ * @param string $type The type identifier of the navigation item for which to return its config
+ * @param string $username A user's name or null if the shared config is desired
+ * @param bool $fromDisk If true, the configuration will be read from disk
+ *
+ * @return Config The requested configuration
+ */
+ public static function navigation($type, $username = null, $fromDisk = false)
+ {
+ if (! isset(self::$navigation[$type])) {
+ self::$navigation[$type] = array();
+ }
+
+ $branch = $username ?: 'shared';
+ $typeConfigs = self::$navigation[$type];
+ if (! isset($typeConfigs[$branch]) || $fromDisk) {
+ $typeConfigs[$branch] = static::fromIni(static::getNavigationConfigPath($type, $username));
+ }
+
+ return $typeConfigs[$branch];
+ }
+
+ /**
+ * Return the path to the configuration file for the given navigation item type and user
+ *
+ * @param string $type
+ * @param string $username
+ *
+ * @return string
+ *
+ * @throws IcingaException In case the given type is unknown
+ */
+ protected static function getNavigationConfigPath($type, $username = null)
+ {
+ $itemTypeConfig = Navigation::getItemTypeConfiguration();
+ if (! isset($itemTypeConfig[$type])) {
+ throw new IcingaException('Invalid navigation item type %s provided', $type);
+ }
+
+ if (isset($itemTypeConfig[$type]['config'])) {
+ $filename = $itemTypeConfig[$type]['config'] . '.ini';
+ } else {
+ $filename = $type . 's.ini';
+ }
+
+ if ($username) {
+ $path = static::resolvePath(implode(DIRECTORY_SEPARATOR, array('preferences', $username, $filename)));
+ if (realpath($path) === false) {
+ $path = static::resolvePath(implode(
+ DIRECTORY_SEPARATOR,
+ array('preferences', strtolower($username), $filename)
+ ));
+ }
+ } else {
+ $path = static::resolvePath('navigation' . DIRECTORY_SEPARATOR . $filename);
+ }
+ return $path;
+ }
+
+ /**
+ * Return this config rendered as a INI structured string
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->getIniWriter()->render();
+ }
+}
diff --git a/library/Icinga/Application/EmbeddedWeb.php b/library/Icinga/Application/EmbeddedWeb.php
new file mode 100644
index 0000000..9adb3a4
--- /dev/null
+++ b/library/Icinga/Application/EmbeddedWeb.php
@@ -0,0 +1,115 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application;
+
+require_once dirname(__FILE__) . '/ApplicationBootstrap.php';
+
+use Icinga\Web\Request;
+use Icinga\Web\Response;
+use ipl\I18n\NoopTranslator;
+use ipl\I18n\StaticTranslator;
+
+/**
+ * Use this if you want to make use of Icinga functionality in other web projects
+ *
+ * Usage example:
+ * <code>
+ * use Icinga\Application\EmbeddedWeb;
+ * EmbeddedWeb::start();
+ * </code>
+ */
+class EmbeddedWeb extends ApplicationBootstrap
+{
+ /**
+ * Request
+ *
+ * @var Request
+ */
+ protected $request;
+
+ /**
+ * Response
+ *
+ * @var Response
+ */
+ protected $response;
+
+ /**
+ * Get the request
+ *
+ * @return Request
+ */
+ public function getRequest()
+ {
+ return $this->request;
+ }
+
+ /**
+ * Get the response
+ *
+ * @return Response
+ */
+ public function getResponse()
+ {
+ return $this->response;
+ }
+
+ /**
+ * Embedded bootstrap parts
+ *
+ * @see ApplicationBootstrap::bootstrap
+ *
+ * @return $this
+ */
+ protected function bootstrap()
+ {
+ return $this
+ ->setupErrorHandling()
+ ->loadLibraries()
+ ->loadConfig()
+ ->setupLogging()
+ ->setupLogger()
+ ->setupRequest()
+ ->setupResponse()
+ ->setupTimezone()
+ ->prepareFakeInternationalization()
+ ->setupModuleManager()
+ ->loadEnabledModules()
+ ->registerApplicationHooks();
+ }
+
+ /**
+ * Set the request
+ *
+ * @return $this
+ */
+ protected function setupRequest()
+ {
+ $this->request = new Request();
+ return $this;
+ }
+
+ /**
+ * Set the response
+ *
+ * @return $this
+ */
+ protected function setupResponse()
+ {
+ $this->response = new Response();
+ return $this;
+ }
+
+ /**
+ * Prepare fake internationalization
+ *
+ * @return $this
+ */
+ protected function prepareFakeInternationalization()
+ {
+ StaticTranslator::$instance = new NoopTranslator();
+
+ return $this;
+ }
+}
diff --git a/library/Icinga/Application/Hook.php b/library/Icinga/Application/Hook.php
new file mode 100644
index 0000000..9720c6a
--- /dev/null
+++ b/library/Icinga/Application/Hook.php
@@ -0,0 +1,328 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application;
+
+use Exception;
+use Icinga\Authentication\Auth;
+use Icinga\Application\Modules\Manager;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Icinga Hook registry
+ *
+ * Modules making use of predefined hooks have to use this registry
+ *
+ * Usage:
+ * <code>
+ * Hook::register('grapher', 'My\\Grapher\\Class');
+ * </code>
+ */
+class Hook
+{
+ /**
+ * Our hook name registry
+ *
+ * @var array
+ */
+ protected static $hooks = array();
+
+ /**
+ * Hooks that have already been instantiated
+ *
+ * @var array
+ */
+ protected static $instances = array();
+
+ /**
+ * Namespace prefix
+ *
+ * @var string
+ */
+ public static $BASE_NS = 'Icinga\\Application\\Hook\\';
+
+ /**
+ * Append this string to base class
+ *
+ * All base classes renamed to *Hook
+ *
+ * @var string
+ */
+ public static $classSuffix = 'Hook';
+
+ /**
+ * Reset object state
+ */
+ public static function clean()
+ {
+ self::$hooks = array();
+ self::$instances = array();
+ self::$BASE_NS = 'Icinga\\Application\\Hook\\';
+ }
+
+ /**
+ * Whether someone registered itself for the given hook name
+ *
+ * @param string $name One of the predefined hook names
+ *
+ * @return bool
+ */
+ public static function has($name)
+ {
+ $name = self::normalizeHookName($name);
+
+ if (! array_key_exists($name, self::$hooks)) {
+ return false;
+ }
+
+ foreach (self::$hooks[$name] as $hook) {
+ list($class, $alwaysRun) = $hook;
+ if ($alwaysRun || self::hasPermission($class)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected static function normalizeHookName($name)
+ {
+ if (strpos($name, '\\') === false) {
+ $parts = explode('/', $name);
+ foreach ($parts as & $part) {
+ $part = ucfirst($part);
+ }
+
+ return implode('\\', $parts);
+ }
+
+ return $name;
+ }
+
+ /**
+ * Create or return an instance of a given hook
+ *
+ * TODO: Should return some kind of a hook interface
+ *
+ * @param string $name One of the predefined hook names
+ * @param string $key The identifier of a specific subtype
+ *
+ * @return mixed
+ */
+ public static function createInstance($name, $key)
+ {
+ $name = self::normalizeHookName($name);
+
+ if (!self::has($name)) {
+ return null;
+ }
+
+ if (isset(self::$instances[$name][$key])) {
+ return self::$instances[$name][$key];
+ }
+
+ $class = self::$hooks[$name][$key][0];
+
+ if (! class_exists($class)) {
+ throw new ProgrammingError(
+ 'Erraneous hook implementation, class "%s" does not exist',
+ $class
+ );
+ }
+ try {
+ $instance = new $class();
+ } catch (Exception $e) {
+ Logger::debug(
+ 'Hook "%s" (%s) (%s) failed, will be unloaded: %s',
+ $name,
+ $key,
+ $class,
+ $e->getMessage()
+ );
+ // TODO: Persist unloading for "some time" or "current session"
+ unset(self::$hooks[$name][$key]);
+ return null;
+ }
+
+ self::assertValidHook($instance, $name);
+ self::$instances[$name][$key] = $instance;
+ return $instance;
+ }
+
+ protected static function splitHookName($name)
+ {
+ $sep = '\\';
+ if (false === $module = strpos($name, $sep)) {
+ return array(null, $name);
+ }
+ return array(
+ substr($name, 0, $module),
+ substr($name, $module + 1)
+ );
+ }
+
+ /**
+ * Extract the Icinga module name from a given namespaced class name
+ *
+ * Does no validation, prefix must have been checked before
+ *
+ * Shameless copy of ClassLoader::extractModuleName()
+ *
+ * @param string $class The hook's class path
+ *
+ * @return string
+ */
+ protected static function extractModuleName($class)
+ {
+ return lcfirst(
+ substr(
+ $class,
+ ClassLoader::MODULE_PREFIX_LENGTH,
+ strpos(
+ $class,
+ ClassLoader::NAMESPACE_SEPARATOR,
+ ClassLoader::MODULE_PREFIX_LENGTH + 1
+ ) - ClassLoader::MODULE_PREFIX_LENGTH
+ )
+ );
+ }
+
+ /**
+ * Return whether the user has the permission to access the module which provides the given hook
+ *
+ * @param string $class The hook's class path
+ *
+ * @return bool
+ */
+ protected static function hasPermission($class)
+ {
+ if (Icinga::app()->isCli()) {
+ return true;
+ }
+
+ return Auth::getInstance()->hasPermission(
+ Manager::MODULE_PERMISSION_NS . self::extractModuleName($class)
+ );
+ }
+
+ /**
+ * Test for a valid class name
+ *
+ * @param mixed $instance
+ * @param string $name
+ *
+ * @throws ProgrammingError
+ */
+ private static function assertValidHook($instance, $name)
+ {
+ $name = self::normalizeHookName($name);
+
+ $suffix = self::$classSuffix; // 'Hook'
+ $base = self::$BASE_NS; // 'Icinga\\Web\\Hook\\'
+
+ list($module, $name) = self::splitHookName($name);
+
+ if ($module === null) {
+ $base_class = $base . ucfirst($name) . 'Hook';
+
+ // I'm unsure whether this makes sense. Unused and Wrong.
+ if (strpos($base_class, $suffix) === false) {
+ $base_class .= $suffix;
+ }
+ } else {
+ $base_class = 'Icinga\\Module\\'
+ . ucfirst($module)
+ . '\\Hook\\'
+ . ucfirst($name)
+ . $suffix;
+ }
+
+ if (!$instance instanceof $base_class) {
+ // This is a compatibility check. Should be removed one far day:
+ if ($module !== null) {
+ $compat_class = 'Icinga\\Module\\'
+ . ucfirst($module)
+ . '\\Web\\Hook\\'
+ . ucfirst($name)
+ . $suffix;
+
+ if ($instance instanceof $compat_class) {
+ return;
+ }
+ }
+
+ throw new ProgrammingError(
+ '%s is not an instance of %s',
+ get_class($instance),
+ $base_class
+ );
+ }
+ }
+
+ /**
+ * Return all instances of a specific name
+ *
+ * @param string $name One of the predefined hook names
+ *
+ * @return array
+ */
+ public static function all($name): array
+ {
+ $name = self::normalizeHookName($name);
+ if (! self::has($name)) {
+ return [];
+ }
+
+ foreach (self::$hooks[$name] as $key => $hook) {
+ list($class, $alwaysRun) = $hook;
+ if ($alwaysRun || self::hasPermission($class)) {
+ self::createInstance($name, $key);
+ }
+ }
+
+ return self::$instances[$name] ?? [];
+ }
+
+ /**
+ * Get the first hook
+ *
+ * @param string $name One of the predefined hook names
+ *
+ * @return null|mixed
+ */
+ public static function first($name)
+ {
+ $name = self::normalizeHookName($name);
+
+ if (self::has($name)) {
+ foreach (self::$hooks[$name] as $key => $hook) {
+ list($class, $alwaysRun) = $hook;
+ if ($alwaysRun || self::hasPermission($class)) {
+ return self::createInstance($name, $key);
+ }
+ }
+ }
+ }
+
+ /**
+ * Register a class
+ *
+ * @param string $name One of the predefined hook names
+ * @param string $key The identifier of a specific subtype
+ * @param string $class Your class name, must inherit one of the
+ * classes in the Icinga/Application/Hook folder
+ * @param bool $alwaysRun To run the hook always (e.g. without permission check)
+ */
+ public static function register($name, $key, $class, $alwaysRun = false)
+ {
+ $name = self::normalizeHookName($name);
+
+ if (!isset(self::$hooks[$name])) {
+ self::$hooks[$name] = array();
+ }
+
+ $class = ltrim($class, ClassLoader::NAMESPACE_SEPARATOR);
+
+ self::$hooks[$name][$key] = [$class, $alwaysRun];
+ }
+}
diff --git a/library/Icinga/Application/Hook/ApplicationStateHook.php b/library/Icinga/Application/Hook/ApplicationStateHook.php
new file mode 100644
index 0000000..be973fe
--- /dev/null
+++ b/library/Icinga/Application/Hook/ApplicationStateHook.php
@@ -0,0 +1,90 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+use Icinga\Application\Hook;
+use Icinga\Application\Logger;
+
+/**
+ * Application state hook base class
+ */
+abstract class ApplicationStateHook
+{
+ const ERROR = 'error';
+
+ private $messages = [];
+
+ final public function hasMessages()
+ {
+ return ! empty($this->messages);
+ }
+
+ final public function getMessages()
+ {
+ return $this->messages;
+ }
+
+ /**
+ * Add an error message
+ *
+ * The timestamp of the message is used for deduplication and thus must refer to the time when the error first
+ * occurred. Don't use {@link time()} here!
+ *
+ * @param string $id ID of the message. The ID must be prefixed with the module name
+ * @param int $timestamp Timestamp when the error first occurred
+ * @param string $message Error message
+ *
+ * @return $this
+ */
+ final public function addError($id, $timestamp, $message)
+ {
+ $id = trim($id);
+ $timestamp = (int) $timestamp;
+
+ if (! strlen($id)) {
+ throw new \InvalidArgumentException('ID expected.');
+ }
+
+ if (! $timestamp) {
+ throw new \InvalidArgumentException('Timestamp expected.');
+ }
+
+ $this->messages[sha1($id . $timestamp)] = [self::ERROR, $timestamp, $message];
+
+ return $this;
+ }
+
+ /**
+ * Override this method in order to provide application state messages
+ */
+ abstract public function collectMessages();
+
+ final public static function getAllMessages()
+ {
+ $messages = [];
+
+ if (! Hook::has('ApplicationState')) {
+ return $messages;
+ }
+
+ foreach (Hook::all('ApplicationState') as $hook) {
+ /** @var self $hook */
+ try {
+ $hook->collectMessages();
+ } catch (\Exception $e) {
+ Logger::error(
+ "Failed to collect messages from hook '%s'. An error occurred: %s",
+ get_class($hook),
+ $e
+ );
+ }
+
+ if ($hook->hasMessages()) {
+ $messages += $hook->getMessages();
+ }
+ }
+
+ return $messages;
+ }
+}
diff --git a/library/Icinga/Application/Hook/AuditHook.php b/library/Icinga/Application/Hook/AuditHook.php
new file mode 100644
index 0000000..e6209da
--- /dev/null
+++ b/library/Icinga/Application/Hook/AuditHook.php
@@ -0,0 +1,123 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+use Exception;
+use InvalidArgumentException;
+use Icinga\Authentication\Auth;
+use Icinga\Application\Hook;
+use Icinga\Application\Logger;
+
+abstract class AuditHook
+{
+ /**
+ * Log an activity to the audit log
+ *
+ * Propagates the given message details to all known hook implementations.
+ *
+ * @param string $type An arbitrary name identifying the type of activity
+ * @param string $message A detailed description possibly referencing parameters in $data
+ * @param array $data Additional information (How this is stored or used is up to each implementation)
+ * @param string $identity An arbitrary name identifying the responsible subject, defaults to the current user
+ * @param int $time A timestamp defining when the activity occurred, defaults to now
+ */
+ public static function logActivity($type, $message, array $data = null, $identity = null, $time = null)
+ {
+ if (! Hook::has('audit')) {
+ return;
+ }
+
+ if ($identity === null) {
+ $identity = Auth::getInstance()->getUser()->getUsername();
+ }
+
+ if ($time === null) {
+ $time = time();
+ }
+
+ foreach (Hook::all('audit') as $hook) {
+ /** @var self $hook */
+ try {
+ $formattedMessage = $message;
+ if ($data !== null) {
+ // Calling formatMessage on each hook is intended and allows
+ // intercepting message formatting while keeping it implicit
+ $formattedMessage = $hook->formatMessage($message, $data);
+ }
+
+ $hook->logMessage($time, $identity, $type, $formattedMessage, $data);
+ } catch (Exception $e) {
+ Logger::error(
+ 'Failed to propagate audit message to hook "%s". An error occurred: %s',
+ get_class($hook),
+ $e
+ );
+ }
+ }
+ }
+
+ /**
+ * Log a message to the audit log
+ *
+ * @param int $time A timestamp defining when the activity occurred
+ * @param string $identity An arbitrary name identifying the responsible subject
+ * @param string $type An arbitrary name identifying the type of activity
+ * @param string $message A detailed description of the activity
+ * @param array $data Additional activity information
+ */
+ abstract public function logMessage($time, $identity, $type, $message, array $data = null);
+
+ /**
+ * Substitute the given message with its accompanying data
+ *
+ * @param string $message
+ * @param array $messageData
+ *
+ * @return string
+ */
+ public function formatMessage($message, array $messageData)
+ {
+ return preg_replace_callback('/{{(.+?)}}/', function ($match) use ($messageData) {
+ return $this->extractMessageValue(explode('.', $match[1]), $messageData);
+ }, $message);
+ }
+
+ /**
+ * Extract the given value path from the given message data
+ *
+ * @param array $path
+ * @param array $messageData
+ *
+ * @return mixed
+ *
+ * @throws InvalidArgumentException In case of an invalid or missing format parameter
+ */
+ protected function extractMessageValue(array $path, array $messageData)
+ {
+ $key = array_shift($path);
+ if (array_key_exists($key, $messageData)) {
+ $value = $messageData[$key];
+ } else {
+ throw new InvalidArgumentException("Missing format parameter '$key'");
+ }
+
+ if (empty($path)) {
+ if (! is_scalar($value)) {
+ throw new InvalidArgumentException(
+ 'Invalid format parameter. Expected scalar for path "' . join('.', $path) . '".'
+ . ' Got "' . gettype($value) . '" instead'
+ );
+ }
+
+ return $value;
+ } elseif (! is_array($value)) {
+ throw new InvalidArgumentException(
+ 'Invalid format parameter. Expected array for path "'. join('.', $path) . '".'
+ . ' Got "' . gettype($value) . '" instead'
+ );
+ }
+
+ return $this->extractMessageValue($path, $value);
+ }
+}
diff --git a/library/Icinga/Application/Hook/AuthenticationHook.php b/library/Icinga/Application/Hook/AuthenticationHook.php
new file mode 100644
index 0000000..41cc661
--- /dev/null
+++ b/library/Icinga/Application/Hook/AuthenticationHook.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Icinga\Application\Hook;
+
+use Icinga\User;
+use Icinga\Web\Hook;
+use Icinga\Application\Logger;
+
+/**
+ * Icinga Web Authentication Hook base class
+ *
+ * This hook can be used to authenticate the user in a third party application.
+ * Extend this class if you want to perform arbitrary actions during the login and logout.
+ */
+abstract class AuthenticationHook
+{
+ /**
+ * Name of the hook
+ */
+ const NAME = 'authentication';
+
+ /**
+ * Triggered after login in Icinga Web and when calling login action even if already authenticated in Icinga Web
+ *
+ * @param User $user
+ */
+ public function onLogin(User $user)
+ {
+ }
+
+ /**
+ * Triggered before logout from Icinga Web
+ *
+ * @param User $user
+ */
+ public function onLogout(User $user)
+ {
+ }
+
+ /**
+ * Call the onLogin() method of all registered AuthHook(s)
+ *
+ * @param User $user
+ */
+ public static function triggerLogin(User $user)
+ {
+ /** @var AuthenticationHook $hook */
+ foreach (Hook::all(self::NAME) as $hook) {
+ try {
+ $hook->onLogin($user);
+ } catch (\Exception $e) {
+ // Avoid error propagation if login failed in third party application
+ Logger::error($e);
+ }
+ }
+ }
+
+ /**
+ * Call the onLogout() method of all registered AuthHook(s)
+ *
+ * @param User $user
+ */
+ public static function triggerLogout(User $user)
+ {
+ /** @var AuthenticationHook $hook */
+ foreach (Hook::all(self::NAME) as $hook) {
+ try {
+ $hook->onLogout($user);
+ } catch (\Exception $e) {
+ // Avoid error propagation if login failed in third party application
+ Logger::error($e);
+ }
+ }
+ }
+}
diff --git a/library/Icinga/Application/Hook/Common/DbMigrationStep.php b/library/Icinga/Application/Hook/Common/DbMigrationStep.php
new file mode 100644
index 0000000..54a1139
--- /dev/null
+++ b/library/Icinga/Application/Hook/Common/DbMigrationStep.php
@@ -0,0 +1,129 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Application\Hook\Common;
+
+use ipl\Sql\Connection;
+use RuntimeException;
+
+class DbMigrationStep
+{
+ /** @var string The sql script version the queries are loaded from */
+ protected $version;
+
+ /** @var string */
+ protected $scriptPath;
+
+ /** @var ?string */
+ protected $description;
+
+ /** @var ?string */
+ protected $lastState;
+
+ public function __construct(string $version, string $scriptPath)
+ {
+ $this->scriptPath = $scriptPath;
+ $this->version = $version;
+ }
+
+ /**
+ * Get the sql script version the queries are loaded from
+ *
+ * @return string
+ */
+ public function getVersion(): string
+ {
+ return $this->version;
+ }
+
+ /**
+ * Get upgrade script relative path name
+ *
+ * @return string
+ */
+ public function getScriptPath(): string
+ {
+ return $this->scriptPath;
+ }
+
+ /**
+ * Get the description of this database migration if any
+ *
+ * @return ?string
+ */
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ /**
+ * Set the description of this database migration
+ *
+ * @param ?string $description
+ *
+ * @return DbMigrationStep
+ */
+ public function setDescription(?string $description): self
+ {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ /**
+ * Get the last error message of this hook if any
+ *
+ * @return ?string
+ */
+ public function getLastState(): ?string
+ {
+ return $this->lastState;
+ }
+
+ /**
+ * Set the last error message
+ *
+ * @param ?string $message
+ *
+ * @return $this
+ */
+ public function setLastState(?string $message): self
+ {
+ $this->lastState = $message;
+
+ return $this;
+ }
+
+ /**
+ * Perform the sql migration
+ *
+ * @param Connection $conn
+ *
+ * @return $this
+ *
+ * @throws RuntimeException Throws an error in case of any database errors or when there is nothing to migrate
+ */
+ public function apply(Connection $conn): self
+ {
+ $statements = @file_get_contents($this->getScriptPath());
+ if ($statements === false) {
+ throw new RuntimeException(sprintf('Cannot load upgrade script %s', $this->getScriptPath()));
+ }
+
+ if (empty($statements)) {
+ throw new RuntimeException('Nothing to migrate');
+ }
+
+ if (preg_match('/\s*delimiter\s*(\S+)\s*$/im', $statements, $matches)) {
+ /** @var string $statements */
+ $statements = preg_replace('/\s*delimiter\s*(\S+)\s*$/im', '', $statements);
+ /** @var string $statements */
+ $statements = preg_replace('/' . preg_quote($matches[1], '/') . '$/m', ';', $statements);
+ }
+
+ $conn->exec($statements);
+
+ return $this;
+ }
+}
diff --git a/library/Icinga/Application/Hook/ConfigFormEventsHook.php b/library/Icinga/Application/Hook/ConfigFormEventsHook.php
new file mode 100644
index 0000000..05fa05d
--- /dev/null
+++ b/library/Icinga/Application/Hook/ConfigFormEventsHook.php
@@ -0,0 +1,137 @@
+<?php
+/* Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+use Icinga\Application\Hook;
+use Icinga\Application\Logger;
+use Icinga\Exception\IcingaException;
+use Icinga\Web\Form;
+
+/**
+ * Base class for config form event hooks
+ */
+abstract class ConfigFormEventsHook
+{
+ /** @var array Array of errors found while processing the form event hooks */
+ private static $lastErrors = [];
+
+ /**
+ * Get whether the hook applies to the given config form
+ *
+ * @param Form $form
+ *
+ * @return bool
+ */
+ public function appliesTo(Form $form)
+ {
+ return false;
+ }
+
+ /**
+ * isValid event hook
+ *
+ * Implement this method in order to run code after the form has been validated successfully.
+ * Throw an exception here if either the form is not valid or you want interrupt the form handling.
+ * The exception's message will be automatically added as form error message so that it will be
+ * displayed in the frontend.
+ *
+ * @param Form $form
+ *
+ * @throws \Exception If either the form is not valid or to interrupt the form handling
+ */
+ public function isValid(Form $form)
+ {
+ }
+
+ /**
+ * onSuccess event hook
+ *
+ * Implement this method in order to run code after the configuration form has been stored successfully.
+ * You can't interrupt the form handling here. Any exception will be caught, logged and notified.
+ *
+ * @param Form $form
+ */
+ public function onSuccess(Form $form)
+ {
+ }
+
+ /**
+ * Get an array of errors found while processing the form event hooks
+ *
+ * @return array
+ */
+ final public static function getLastErrors()
+ {
+ return self::$lastErrors;
+ }
+
+ /**
+ * Run all isValid hooks
+ *
+ * @param Form $form
+ *
+ * @return bool Returns false if any hook threw an exception
+ */
+ final public static function runIsValid(Form $form)
+ {
+ return self::runEventMethod('isValid', $form);
+ }
+
+ /**
+ * Run all onSuccess hooks
+ *
+ * @param Form $form
+ *
+ * @return bool Returns false if any hook threw an exception
+ */
+ final public static function runOnSuccess(Form $form)
+ {
+ return self::runEventMethod('onSuccess', $form);
+ }
+
+ private static function runEventMethod($eventMethod, Form $form)
+ {
+ self::$lastErrors = [];
+
+ if (! Hook::has('ConfigFormEvents')) {
+ return true;
+ }
+
+ $success = true;
+
+ foreach (Hook::all('ConfigFormEvents') as $hook) {
+ /** @var self $hook */
+ if (! $hook->runAppliesTo($form)) {
+ continue;
+ }
+
+ try {
+ $hook->$eventMethod($form);
+ } catch (\Exception $e) {
+ self::$lastErrors[] = $e->getMessage();
+
+ Logger::error("%s\n%s", $e, IcingaException::getConfidentialTraceAsString($e));
+
+ $success = false;
+ }
+ }
+
+ return $success;
+ }
+
+ private function runAppliesTo(Form $form)
+ {
+ try {
+ $appliesTo = $this->appliesTo($form);
+ } catch (\Exception $e) {
+ // Don't save exception to last errors because we do not want to disturb the user for messed up
+ // appliesTo checks
+ Logger::error("%s\n%s", $e, IcingaException::getConfidentialTraceAsString($e));
+
+ $appliesTo = false;
+ }
+
+ return $appliesTo === true;
+ }
+}
diff --git a/library/Icinga/Application/Hook/DbMigrationHook.php b/library/Icinga/Application/Hook/DbMigrationHook.php
new file mode 100644
index 0000000..f34bc0d
--- /dev/null
+++ b/library/Icinga/Application/Hook/DbMigrationHook.php
@@ -0,0 +1,421 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+use Countable;
+use DateTime;
+use DirectoryIterator;
+use Exception;
+use Icinga\Application\ClassLoader;
+use Icinga\Application\Hook\Common\DbMigrationStep;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Application\Modules\Module;
+use Icinga\Model\Schema;
+use Icinga\Web\Session;
+use ipl\I18n\Translation;
+use ipl\Orm\Query;
+use ipl\Sql\Adapter\Pgsql;
+use ipl\Sql\Connection;
+use ipl\Stdlib\Filter;
+use PDO;
+use SplFileInfo;
+use stdClass;
+
+/**
+ * Allows you to automatically perform database migrations.
+ *
+ * The version numbers of the sql migrations are determined by extracting the respective migration script names.
+ * It's required to place the sql migrate scripts below the respective following directories:
+ *
+ * `{IcingaApp,Module}::baseDir()/schema/{mysql,pgsql}-upgrades`
+ */
+abstract class DbMigrationHook implements Countable
+{
+ use Translation;
+
+ public const MYSQL_UPGRADE_DIR = 'schema/mysql-upgrades';
+
+ public const PGSQL_UPGRADE_DIR = 'schema/pgsql-upgrades';
+
+ /** @var string Fakes a module when this hook is implemented by the framework itself */
+ public const DEFAULT_MODULE = 'icingaweb2';
+
+ /** @var string Migration hook param name */
+ public const MIGRATION_PARAM = 'migration';
+
+ public const ALL_MIGRATIONS = 'all-migrations';
+
+ /** @var ?array<string, DbMigrationStep> All pending database migrations of this hook */
+ protected $migrations;
+
+ /** @var ?string The current version of this hook */
+ protected $version;
+
+ /**
+ * Get whether the specified table exists in the given database
+ *
+ * @param Connection $conn
+ * @param string $table
+ *
+ * @return bool
+ */
+ public static function tableExists(Connection $conn, string $table): bool
+ {
+ /** @var false|int $exists */
+ $exists = $conn->prepexec(
+ 'SELECT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = ?) AS result',
+ $table
+ )->fetchColumn();
+
+ return (bool) $exists;
+ }
+
+ /**
+ * Get whether the specified column exists in the provided table
+ *
+ * @param Connection $conn
+ * @param string $table
+ * @param string $column
+ *
+ * @return ?string
+ */
+ public static function getColumnType(Connection $conn, string $table, string $column): ?string
+ {
+ $pdoStmt = $conn->prepexec(
+ sprintf(
+ 'SELECT %s AS column_type, %s AS column_length FROM information_schema.columns'
+ . ' WHERE table_name = ? AND column_name = ?',
+ $conn->getAdapter() instanceof Pgsql ? 'udt_name' : 'column_type',
+ $conn->getAdapter() instanceof Pgsql ? 'character_maximum_length' : 'NULL'
+ ),
+ [$table, $column]
+ );
+
+ /** @var false|stdClass $result */
+ $result = $pdoStmt->fetch(PDO::FETCH_OBJ);
+ if ($result === false) {
+ return null;
+ }
+
+ if ($result->column_length !== null) {
+ $result->column_type .= '(' . $result->column_length . ')';
+ }
+
+ return $result->column_type;
+ }
+
+ /**
+ * Get the mysql collation name of the given column of the specified table
+ *
+ * @param Connection $conn
+ * @param string $table
+ * @param string $column
+ *
+ * @return ?string
+ */
+ public static function getColumnCollation(Connection $conn, string $table, string $column): ?string
+ {
+ if ($conn->getAdapter() instanceof Pgsql) {
+ return null;
+ }
+
+ /** @var false|string $collation */
+ $collation = $conn->prepexec(
+ 'SELECT collation_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?',
+ [$table, $column]
+ )->fetchColumn();
+
+ return ! $collation ? null : $collation;
+ }
+
+ /**
+ * Get statically provided descriptions of the individual migrate scripts
+ *
+ * @return string[]
+ */
+ abstract public function providedDescriptions(): array;
+
+ /**
+ * Get the full name of the component this hook is implemented by
+ *
+ * @return string
+ */
+ abstract public function getName(): string;
+
+ /**
+ * Get the current schema version of this migration hook
+ *
+ * @return string
+ */
+ abstract public function getVersion(): string;
+
+ /**
+ * Get a database connection
+ *
+ * @return Connection
+ */
+ abstract public function getDb(): Connection;
+
+ /**
+ * Get all the pending migrations of this hook
+ *
+ * @return DbMigrationStep[]
+ */
+ public function getMigrations(): array
+ {
+ if ($this->migrations === null) {
+ $this->migrations = [];
+
+ $this->load();
+ }
+
+ return $this->migrations ?? [];
+ }
+
+ /**
+ * Get the latest migrations limited by the given number
+ *
+ * @param int $limit
+ *
+ * @return DbMigrationStep[]
+ */
+ public function getLatestMigrations(int $limit): array
+ {
+ $migrations = $this->getMigrations();
+ if ($limit > 0) {
+ $migrations = array_slice($migrations, -$limit, null, true);
+ }
+
+ return array_reverse($migrations);
+ }
+
+ /**
+ * Apply all pending migrations of this hook
+ *
+ * @param ?Connection $conn Use the provided database connection to apply the migrations.
+ * Is only used to elevate database users with insufficient privileges.
+ *
+ * @return bool Whether the migration(s) have been successfully applied
+ */
+ final public function run(Connection $conn = null): bool
+ {
+ if (! $conn) {
+ $conn = $this->getDb();
+ }
+
+ foreach ($this->getMigrations() as $migration) {
+ try {
+ $migration->apply($conn);
+
+ $this->version = $migration->getVersion();
+ unset($this->migrations[$migration->getVersion()]);
+
+ $data = [
+ 'name' => $this->getName(),
+ 'version' => $migration->getVersion()
+ ];
+ AuditHook::logActivity(
+ 'migrations',
+ 'Migrated database schema of {{name}} to version {{version}}',
+ $data
+ );
+
+ $this->storeState($migration->getVersion(), null);
+ } catch (Exception $e) {
+ Logger::error(
+ "Failed to apply %s pending migration version %s \n%s",
+ $this->getName(),
+ $migration->getVersion(),
+ $e
+ );
+ Logger::debug($e->getTraceAsString());
+
+ static::insertFailedEntry(
+ $conn,
+ $migration->getVersion(),
+ $e->getMessage() . PHP_EOL . $e->getTraceAsString()
+ );
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Get whether this hook is implemented by a module
+ *
+ * @return bool
+ */
+ public function isModule(): bool
+ {
+ return ClassLoader::classBelongsToModule(static::class);
+ }
+
+ /**
+ * Get the name of the module this hook is implemented by
+ *
+ * @return string
+ */
+ public function getModuleName(): string
+ {
+ if (! $this->isModule()) {
+ return static::DEFAULT_MODULE;
+ }
+
+ return ClassLoader::extractModuleName(static::class);
+ }
+
+ /**
+ * Get the number of pending migrations of this hook
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return count($this->getMigrations());
+ }
+
+ /**
+ * Get a schema version query
+ *
+ * @return Query
+ */
+ abstract protected function getSchemaQuery(): Query;
+
+ protected function load(): void
+ {
+ $upgradeDir = static::MYSQL_UPGRADE_DIR;
+ if ($this->getDb()->getAdapter() instanceof Pgsql) {
+ $upgradeDir = static::PGSQL_UPGRADE_DIR;
+ }
+
+ if (! $this->isModule()) {
+ $path = Icinga::app()->getBaseDir();
+ } else {
+ $path = Module::get($this->getModuleName())->getBaseDir();
+ }
+
+ $descriptions = $this->providedDescriptions();
+ $version = $this->getVersion();
+ /** @var SplFileInfo $file */
+ foreach (new DirectoryIterator($path . DIRECTORY_SEPARATOR . $upgradeDir) as $file) {
+ if (preg_match('/^(v)?([^_]+)(?:_(\w+))?\.sql$/', $file->getFilename(), $m, PREG_UNMATCHED_AS_NULL)) {
+ [$_, $_, $migrateVersion, $description] = array_pad($m, 4, null);
+ /** @var string $migrateVersion */
+ if ($migrateVersion && version_compare($migrateVersion, $version, '>')) {
+ $migration = new DbMigrationStep($migrateVersion, $file->getRealPath());
+ if (isset($descriptions[$migrateVersion])) {
+ $migration->setDescription($descriptions[$migrateVersion]);
+ } elseif ($description) {
+ $migration->setDescription(str_replace('_', ' ', $description));
+ }
+
+ $migration->setLastState($this->loadLastState($migrateVersion));
+
+ $this->migrations[$migrateVersion] = $migration;
+ }
+ }
+ }
+
+ if ($this->migrations) {
+ // Sort all the migrations by their version numbers in ascending order.
+ uksort($this->migrations, function ($a, $b) {
+ return version_compare($a, $b);
+ });
+ }
+ }
+
+ /**
+ * Insert failed migration entry into the database or to the session
+ *
+ * @param Connection $conn
+ * @param string $version
+ * @param string $reason
+ *
+ * @return $this
+ */
+ protected function insertFailedEntry(Connection $conn, string $version, string $reason): self
+ {
+ $schemaQuery = $this->getSchemaQuery()
+ ->filter(Filter::equal('version', $version));
+
+ if (! static::getColumnType($conn, $schemaQuery->getModel()->getTableName(), 'success')) {
+ $this->storeState($version, $reason);
+ } else {
+ /** @var Schema $schema */
+ $schema = $schemaQuery->first();
+ if ($schema) {
+ $conn->update($schema->getTableName(), [
+ 'timestamp' => (new DateTime())->getTimestamp() * 1000.0,
+ 'success' => 'n',
+ 'reason' => $reason
+ ], ['id = ?' => $schema->id]);
+ } else {
+ $conn->insert($schemaQuery->getModel()->getTableName(), [
+ 'version' => $version,
+ 'timestamp' => (new DateTime())->getTimestamp() * 1000.0,
+ 'success' => 'n',
+ 'reason' => $reason
+ ]);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Store a failed state message in the session for the given version
+ *
+ * @param string $version
+ * @param ?string $reason
+ *
+ * @return $this
+ */
+ protected function storeState(string $version, ?string $reason): self
+ {
+ $session = Session::getSession()->getNamespace('migrations');
+ /** @var array<string, string> $states */
+ $states = $session->get($this->getModuleName(), []);
+ $states[$version] = $reason;
+
+ $session->set($this->getModuleName(), $states);
+
+ return $this;
+ }
+
+ /**
+ * Load last failed state from database/session for the given version
+ *
+ * @param string $version
+ *
+ * @return ?string
+ */
+ protected function loadLastState(string $version): ?string
+ {
+ $session = Session::getSession()->getNamespace('migrations');
+ /** @var array<string, string> $states */
+ $states = $session->get($this->getModuleName(), []);
+ if (! isset($states[$version])) {
+ $schemaQuery = $this->getSchemaQuery()
+ ->filter(Filter::equal('version', $version))
+ ->filter(Filter::all(Filter::equal('success', 'n')));
+
+ if (static::getColumnType($this->getDb(), $schemaQuery->getModel()->getTableName(), 'reason')) {
+ /** @var Schema $schema */
+ $schema = $schemaQuery->first();
+ if ($schema) {
+ return $schema->reason;
+ }
+ }
+
+ return null;
+ }
+
+ return $states[$version];
+ }
+}
diff --git a/library/Icinga/Application/Hook/GrapherHook.php b/library/Icinga/Application/Hook/GrapherHook.php
new file mode 100644
index 0000000..dfb2135
--- /dev/null
+++ b/library/Icinga/Application/Hook/GrapherHook.php
@@ -0,0 +1,111 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+
+/**
+ * Icinga Web Grapher Hook base class
+ *
+ * Extend this class if you want to integrate your graphing solution nicely into
+ * Icinga Web.
+ */
+abstract class GrapherHook extends WebBaseHook
+{
+ /**
+ * Whether this grapher provides previews
+ *
+ * @var bool
+ */
+ protected $hasPreviews = false;
+
+ /**
+ * Whether this grapher provides tiny previews
+ *
+ * @var bool
+ */
+ protected $hasTinyPreviews = false;
+
+ /**
+ * Constructor must live without arguments right now
+ *
+ * Therefore the constructor is final, we might change our opinion about
+ * this one far day
+ */
+ final public function __construct()
+ {
+ $this->init();
+ }
+
+ /**
+ * Overwrite this function if you want to do some initialization stuff
+ *
+ * @return void
+ */
+ protected function init()
+ {
+ }
+
+ /**
+ * Whether this grapher provides previews
+ *
+ * @return bool
+ */
+ public function hasPreviews()
+ {
+ return $this->hasPreviews;
+ }
+
+ /**
+ * Whether this grapher provides tiny previews
+ *
+ * @return bool
+ */
+ public function hasTinyPreviews()
+ {
+ return $this->hasTinyPreviews;
+ }
+
+ /**
+ * Whether a graph for the monitoring object exist
+ *
+ * @param MonitoredObject $object
+ *
+ * @return bool
+ */
+ abstract public function has(MonitoredObject $object);
+
+ /**
+ * Get a preview for the given object
+ *
+ * This function must return an empty string if no graph exists.
+ *
+ * @param MonitoredObject $object
+ *
+ * @return string
+ * @throws ProgrammingError
+ *
+ */
+ public function getPreviewHtml(MonitoredObject $object)
+ {
+ throw new ProgrammingError('This hook provide previews but it is not implemented');
+ }
+
+
+ /**
+ * Get a tiny preview for the given object
+ *
+ * This function must return an empty string if no graph exists.
+ *
+ * @param MonitoredObject $object
+ *
+ * @return string
+ * @throws ProgrammingError
+ */
+ public function getTinyPreviewHtml(MonitoredObject $object)
+ {
+ throw new ProgrammingError('This hook provide tiny previews but it is not implemented');
+ }
+}
diff --git a/library/Icinga/Application/Hook/HealthHook.php b/library/Icinga/Application/Hook/HealthHook.php
new file mode 100644
index 0000000..f6420b5
--- /dev/null
+++ b/library/Icinga/Application/Hook/HealthHook.php
@@ -0,0 +1,222 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+use Exception;
+use Icinga\Application\Hook;
+use Icinga\Application\Logger;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Exception\IcingaException;
+use ipl\Web\Url;
+use LogicException;
+
+abstract class HealthHook
+{
+ /** @var int */
+ const STATE_OK = 0;
+
+ /** @var int */
+ const STATE_WARNING = 1;
+
+ /** @var int */
+ const STATE_CRITICAL = 2;
+
+ /** @var int */
+ const STATE_UNKNOWN = 3;
+
+ /** @var int The overall state */
+ protected $state;
+
+ /** @var string Message describing the overall state */
+ protected $message;
+
+ /** @var array Available metrics */
+ protected $metrics;
+
+ /** @var Url Url to a graphical representation of the available metrics */
+ protected $url;
+
+ /**
+ * Get overall state
+ *
+ * @return int
+ */
+ public function getState()
+ {
+ return $this->state;
+ }
+
+ /**
+ * Set overall state
+ *
+ * @param int $state
+ *
+ * @return $this
+ */
+ public function setState($state)
+ {
+ $this->state = $state;
+
+ return $this;
+ }
+
+ /**
+ * Get the message describing the overall state
+ *
+ * @return string
+ */
+ public function getMessage()
+ {
+ return $this->message;
+ }
+
+ /**
+ * Set the message describing the overall state
+ *
+ * @param string $message
+ *
+ * @return $this
+ */
+ public function setMessage($message)
+ {
+ $this->message = $message;
+
+ return $this;
+ }
+
+ /**
+ * Get available metrics
+ *
+ * @return array
+ */
+ public function getMetrics()
+ {
+ return $this->metrics;
+ }
+
+ /**
+ * Set available metrics
+ *
+ * @param array $metrics
+ *
+ * @return $this
+ */
+ public function setMetrics(array $metrics)
+ {
+ $this->metrics = $metrics;
+
+ return $this;
+ }
+
+ /**
+ * Get the url to a graphical representation of the available metrics
+ *
+ * @return Url
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * Set the url to a graphical representation of the available metrics
+ *
+ * @param Url $url
+ *
+ * @return $this
+ */
+ public function setUrl(Url $url)
+ {
+ $this->url = $url;
+
+ return $this;
+ }
+
+ /**
+ * Collect available health data from hooks
+ *
+ * @return ArrayDatasource
+ */
+ final public static function collectHealthData()
+ {
+ $checks = [];
+ foreach (Hook::all('health') as $hook) {
+ /** @var self $hook */
+
+ try {
+ $hook->checkHealth();
+ $url = $hook->getUrl();
+ $state = $hook->getState();
+ $message = $hook->getMessage();
+ $metrics = $hook->getMetrics();
+ } catch (Exception $e) {
+ Logger::error('Failed to check health: %s', $e);
+
+ $state = self::STATE_UNKNOWN;
+ $message = IcingaException::describe($e);
+ $metrics = null;
+ $url = null;
+ }
+
+ $checks[] = (object) [
+ 'module' => $hook->getModuleName(),
+ 'name' => $hook->getName(),
+ 'url' => $url ? $url->getAbsoluteUrl() : null,
+ 'state' => $state,
+ 'message' => $message,
+ 'metrics' => (object) $metrics
+ ];
+ }
+
+ return (new ArrayDatasource($checks))
+ ->setKeyColumn('name');
+ }
+
+ /**
+ * Get the name of the hook
+ *
+ * Only used in API responses to differentiate it from other hooks of the same module.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ $classPath = get_class($this);
+ $parts = explode('\\', $classPath);
+ $className = array_pop($parts);
+
+ if (substr($className, -4) === 'Hook') {
+ $className = substr($className, 1, -4);
+ }
+
+ return strtolower($className[0]) . substr($className, 1);
+ }
+
+ /**
+ * Get the name of the module providing this hook
+ *
+ * @return string
+ *
+ * @throws LogicException
+ */
+ public function getModuleName()
+ {
+ $classPath = get_class($this);
+ if (substr($classPath, 0, 14) !== 'Icinga\\Module\\') {
+ throw new LogicException('Not a module hook');
+ }
+
+ $withoutPrefix = substr($classPath, 14);
+ return strtolower(substr($withoutPrefix, 0, strpos($withoutPrefix, '\\')));
+ }
+
+ /**
+ * Check health
+ *
+ * Implement this method and set the overall state, message, url and metrics.
+ *
+ * @return void
+ */
+ abstract public function checkHealth();
+}
diff --git a/library/Icinga/Application/Hook/PdfexportHook.php b/library/Icinga/Application/Hook/PdfexportHook.php
new file mode 100644
index 0000000..36e9f51
--- /dev/null
+++ b/library/Icinga/Application/Hook/PdfexportHook.php
@@ -0,0 +1,25 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+/**
+ * Base class for the PDF Export Hook
+ */
+abstract class PdfexportHook
+{
+ /**
+ * Get whether PDF export is supported
+ *
+ * @return bool
+ */
+ abstract public function isSupported();
+
+ /**
+ * Render the specified HTML to PDF and stream it to the client
+ *
+ * @param string $html The HTML to render to PDF
+ * @param string $filename The filename for the generated PDF
+ */
+ abstract public function streamPdfFromHtml($html, $filename);
+}
diff --git a/library/Icinga/Application/Hook/ThemeLoaderHook.php b/library/Icinga/Application/Hook/ThemeLoaderHook.php
new file mode 100644
index 0000000..5320dd5
--- /dev/null
+++ b/library/Icinga/Application/Hook/ThemeLoaderHook.php
@@ -0,0 +1,22 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+/**
+ * Provide an implementation of this hook to dynamically provide themes.
+ * Note that only the first registered hook is utilized. Also note that
+ * for ordinary themes this hook is not required. Place such in your
+ * module's theme path: <module-path>/public/css/themes
+ */
+abstract class ThemeLoaderHook
+{
+ /**
+ * Get the path for the given theme
+ *
+ * @param ?string $theme
+ *
+ * @return ?string The path or NULL if the theme is unknown
+ */
+ abstract public function getThemeFile(?string $theme): ?string;
+}
diff --git a/library/Icinga/Application/Hook/Ticket/TicketPattern.php b/library/Icinga/Application/Hook/Ticket/TicketPattern.php
new file mode 100644
index 0000000..e37fcc1
--- /dev/null
+++ b/library/Icinga/Application/Hook/Ticket/TicketPattern.php
@@ -0,0 +1,140 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Hook\Ticket;
+
+use ArrayAccess;
+
+/**
+ * A ticket pattern
+ *
+ * This class should be used by modules which provide implementations for the Web 2 ticket hook.
+ * Have a look at the GenericTTS module for a possible use case.
+ */
+class TicketPattern implements ArrayAccess
+{
+ /**
+ * The result of a performed ticket match
+ *
+ * @var array
+ */
+ protected $match = array();
+
+ /**
+ * The name of the TTS integration
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * The ticket pattern
+ *
+ * @var string
+ */
+ protected $pattern;
+
+ public function offsetExists($offset): bool
+ {
+ return isset($this->match[$offset]);
+ }
+
+ public function offsetGet($offset): ?string
+ {
+ return array_key_exists($offset, $this->match) ? $this->match[$offset] : null;
+ }
+
+ public function offsetSet($offset, $value): void
+ {
+ if ($offset === null) {
+ $this->match[] = $value;
+ } else {
+ $this->match[$offset] = $value;
+ }
+ }
+
+ public function offsetUnset($offset): void
+ {
+ 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
new file mode 100644
index 0000000..ceb3738
--- /dev/null
+++ b/library/Icinga/Application/Hook/TicketHook.php
@@ -0,0 +1,210 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+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 into Icinga Web 2.
+ */
+abstract class TicketHook
+{
+ /**
+ * Last error, if any
+ *
+ * @var string|null
+ */
+ protected $lastError;
+
+ /**
+ * Create a new ticket hook
+ *
+ * @see init() For hook initialization.
+ */
+ final public function __construct()
+ {
+ $this->init();
+ }
+
+ /**
+ * Overwrite this function for hook initialization, e.g. loading the hook's config
+ */
+ protected function init()
+ {
+ }
+
+ /**
+ * 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
+ *
+ * @param string $message Error message or error format string
+ * @param mixed ...$arg Format string argument
+ */
+ private function fail($message)
+ {
+ $args = array_slice(func_get_args(), 1);
+ $lastError = vsprintf($message, $args);
+ Logger::debug($lastError);
+ $this->lastError = $lastError;
+ }
+
+ /**
+ * Get the last error, if any
+ *
+ * @return string|null
+ */
+ public function getLastError()
+ {
+ return $this->lastError;
+ }
+
+ /**
+ * Create links w/ {@link createLink()} in the given text that matches to the subject from {@link getPattern()}
+ *
+ * In case of errors a debug message is recorded to the log and any subsequent call to {@link createLinks()} will
+ * be a no-op.
+ *
+ * @param string $text
+ *
+ * @return string
+ */
+ final public function createLinks($text)
+ {
+ if ($this->lastError !== null) {
+ return $text;
+ }
+
+ try {
+ $pattern = $this->getPattern();
+ } catch (Exception $e) {
+ $this->fail('Can\'t create ticket links: Retrieving the pattern failed: %s', IcingaException::describe($e));
+ return $text;
+ }
+
+ if (empty($pattern)) {
+ $this->fail('Can\'t create ticket links: Pattern is empty');
+ return $text;
+ }
+
+ if (is_array($pattern)) {
+ $text = $this->applyTicketPatterns($text, $pattern);
+ } else {
+ try {
+ $text = preg_replace_callback(
+ $pattern,
+ 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/Application/Hook/WebBaseHook.php b/library/Icinga/Application/Hook/WebBaseHook.php
new file mode 100644
index 0000000..09e8f4f
--- /dev/null
+++ b/library/Icinga/Application/Hook/WebBaseHook.php
@@ -0,0 +1,54 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Hook;
+
+use Zend_Controller_Action_HelperBroker;
+use Zend_View;
+
+/**
+ * Base class for web hooks
+ *
+ * The class provides access to the view
+ */
+class WebBaseHook
+{
+ /**
+ * View instance
+ *
+ * @var Zend_View
+ */
+ private $view;
+
+ /**
+ * Set the view instance
+ *
+ * @param Zend_View $view
+ *
+ * @return $this
+ */
+ public function setView(Zend_View $view)
+ {
+ $this->view = $view;
+
+ return $this;
+ }
+
+ /**
+ * Get the view instance
+ *
+ * @return Zend_View
+ */
+ public function getView()
+ {
+ if ($this->view === null) {
+ $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');
+ if ($viewRenderer->view === null) {
+ $viewRenderer->initView();
+ }
+ $this->view = $viewRenderer->view;
+ }
+
+ return $this->view;
+ }
+}
diff --git a/library/Icinga/Application/Icinga.php b/library/Icinga/Application/Icinga.php
new file mode 100644
index 0000000..ba54015
--- /dev/null
+++ b/library/Icinga/Application/Icinga.php
@@ -0,0 +1,49 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application;
+
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Icinga application container
+ */
+class Icinga
+{
+ /**
+ * @var ApplicationBootstrap
+ */
+ private static $app;
+
+ /**
+ * Getter for an application environment
+ *
+ * @return ApplicationBootstrap|Web
+ * @throws ProgrammingError
+ */
+ public static function app()
+ {
+ if (self::$app == null) {
+ throw new ProgrammingError('Icinga has never been started');
+ }
+
+ return self::$app;
+ }
+
+ /**
+ * Setter for an application environment
+ *
+ * @param ApplicationBootstrap $app
+ * @param bool $overwrite
+ *
+ * @throws ProgrammingError
+ */
+ public static function setApp(ApplicationBootstrap $app, $overwrite = false)
+ {
+ if (self::$app !== null && !$overwrite) {
+ throw new ProgrammingError('Cannot start Icinga twice');
+ }
+
+ self::$app = $app;
+ }
+}
diff --git a/library/Icinga/Application/LegacyWeb.php b/library/Icinga/Application/LegacyWeb.php
new file mode 100644
index 0000000..21181f7
--- /dev/null
+++ b/library/Icinga/Application/LegacyWeb.php
@@ -0,0 +1,33 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application;
+
+require_once dirname(__FILE__) . '/Web.php';
+
+use Exception;
+use Icinga\Exception\ProgrammingError;
+
+class LegacyWeb extends Web
+{
+ // IcingaWeb 1.x base dir
+ protected $legacyBasedir;
+
+ protected function bootstrap()
+ {
+ parent::bootstrap();
+ throw new ProgrammingError('Not yet');
+ // $this->setupIcingaLegacyWrapper();
+ }
+
+ /**
+ * Get the Icinga-Web 1.x base path
+ *
+ * @throws Exception
+ * @return self
+ */
+ public function getLecacyBasedir()
+ {
+ return $this->legacyBasedir;
+ }
+}
diff --git a/library/Icinga/Application/Libraries.php b/library/Icinga/Application/Libraries.php
new file mode 100644
index 0000000..8e4a79d
--- /dev/null
+++ b/library/Icinga/Application/Libraries.php
@@ -0,0 +1,91 @@
+<?php
+/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Application;
+
+use ArrayIterator;
+use IteratorAggregate;
+use Icinga\Application\Libraries\Library;
+use Traversable;
+
+class Libraries implements IteratorAggregate
+{
+ /** @var Library[] */
+ protected $libraries = [];
+
+ /**
+ * Iterate over registered libraries
+ *
+ * @return ArrayIterator
+ */
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->libraries);
+ }
+
+ /**
+ * Register a library from the given path
+ *
+ * @param string $path
+ *
+ * @return Library The registered library
+ */
+ public function registerPath($path)
+ {
+ $library = new Library($path);
+ $this->libraries[] = $library;
+
+ return $library;
+ }
+
+ /**
+ * Check if a library with the given name has been registered
+ *
+ * Passing a version constraint also verifies that the library's version matches.
+ *
+ * @param string $name
+ * @param string $version
+ *
+ * @return bool
+ */
+ public function has($name, $version = null)
+ {
+ $library = $this->get($name);
+ if ($library === null) {
+ return false;
+ } elseif ($version === null || $version === true) {
+ return true;
+ }
+
+ $operator = '=';
+ if (preg_match('/^([<>=]{1,2})\s*v?((?:[\d.]+)(?:\D+)?)$/', $version, $match)) {
+ $operator = $match[1];
+ $version = $match[2];
+ }
+
+ return version_compare($library->getVersion(), $version, $operator);
+ }
+
+ /**
+ * Get a library by name
+ *
+ * @param string $name
+ *
+ * @return Library|null
+ */
+ public function get($name)
+ {
+ $candidate = null;
+ foreach ($this->libraries as $library) {
+ $libraryName = $library->getName();
+ if ($libraryName === $name) {
+ return $library;
+ } elseif (strpos($libraryName, '/') !== false && explode('/', $libraryName)[1] === $name) {
+ // Also return libs which only partially match
+ $candidate = $library;
+ }
+ }
+
+ return $candidate;
+ }
+}
diff --git a/library/Icinga/Application/Libraries/Library.php b/library/Icinga/Application/Libraries/Library.php
new file mode 100644
index 0000000..63e50b2
--- /dev/null
+++ b/library/Icinga/Application/Libraries/Library.php
@@ -0,0 +1,259 @@
+<?php
+/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Application\Libraries;
+
+use CallbackFilterIterator;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\Json\JsonDecodeException;
+use Icinga\Util\Json;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+
+class Library
+{
+ /** @var string */
+ protected $path;
+
+ /** @var string */
+ protected $jsAssetPath;
+
+ /** @var string */
+ protected $cssAssetPath;
+
+ /** @var string */
+ protected $staticAssetPath;
+
+ /** @var string */
+ protected $version;
+
+ /** @var array */
+ protected $metaData;
+
+ /** @var array */
+ protected $assets;
+
+ /**
+ * Create a new Library
+ *
+ * @param string $path
+ */
+ public function __construct($path)
+ {
+ $this->path = $path;
+ }
+
+ /**
+ * Get this library's path
+ *
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * Get path of this library's JS assets
+ *
+ * @return string
+ */
+ public function getJsAssetPath()
+ {
+ $this->assets();
+ return $this->jsAssetPath;
+ }
+
+ /**
+ * Get path of this library's CSS assets
+ *
+ * @return string
+ */
+ public function getCssAssetPath()
+ {
+ $this->assets();
+ return $this->cssAssetPath;
+ }
+
+ /**
+ * Get path of this library's static assets
+ *
+ * @return string
+ */
+ public function getStaticAssetPath()
+ {
+ $this->assets();
+ return $this->staticAssetPath;
+ }
+
+ /**
+ * Get this library's name
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->metaData()['name'];
+ }
+
+ /**
+ * Get this library's version
+ *
+ * @return string
+ */
+ public function getVersion()
+ {
+ if ($this->version === null) {
+ if (isset($this->metaData()['version'])) {
+ $this->version = trim(ltrim($this->metaData()['version'], 'v'));
+ } else {
+ $versionFile = $this->path . DIRECTORY_SEPARATOR . 'VERSION';
+ if (file_exists($versionFile)) {
+ $this->version = trim(ltrim(file_get_contents($versionFile), 'v'));
+ } else {
+ $this->version = '';
+ }
+ }
+ }
+
+ return $this->version;
+ }
+
+ /**
+ * Check whether the given package is required
+ *
+ * @param string $vendor The vendor of the project
+ * @param string $project The project's name
+ *
+ * @return bool
+ */
+ public function isRequired($vendor, $project)
+ {
+ // Ensure the parts are lowercase and separated by dashes, not capital letters
+ $project = strtolower(join('-', preg_split('/\w(?=[A-Z])/', $project)));
+
+ return isset($this->metaData()['require'][strtolower($vendor) . '/' . $project]);
+ }
+
+ /**
+ * Get this library's JS assets
+ *
+ * @return string[] Asset paths
+ */
+ public function getJsAssets()
+ {
+ return $this->assets()['js'];
+ }
+
+ /**
+ * Get this library's CSS assets
+ *
+ * @return string[] Asset paths
+ */
+ public function getCssAssets()
+ {
+ return $this->assets()['css'];
+ }
+
+ /**
+ * Get this library's static assets
+ *
+ * @return string[] Asset paths
+ */
+ public function getStaticAssets()
+ {
+ return $this->assets()['static'];
+ }
+
+ /**
+ * Register this library's autoloader
+ *
+ * @return void
+ */
+ public function registerAutoloader()
+ {
+ $autoloaderPath = join(DIRECTORY_SEPARATOR, [$this->path, 'vendor', 'autoload.php']);
+ if (file_exists($autoloaderPath)) {
+ require_once $autoloaderPath;
+ }
+ }
+
+ /**
+ * Parse and return this library's metadata
+ *
+ * @return array
+ *
+ * @throws ConfigurationError
+ * @throws JsonDecodeException
+ */
+ protected function metaData()
+ {
+ if ($this->metaData === null) {
+ $metaData = @file_get_contents($this->path . DIRECTORY_SEPARATOR . 'composer.json');
+ if ($metaData === false) {
+ throw new ConfigurationError('Library at "%s" is not a composerized project', $this->path);
+ }
+
+ $this->metaData = Json::decode($metaData, true);
+ }
+
+ return $this->metaData;
+ }
+
+ /**
+ * Register and return this library's assets
+ *
+ * @return array
+ */
+ protected function assets()
+ {
+ if ($this->assets !== null) {
+ return $this->assets;
+ }
+
+ $listAssets = function ($type) {
+ $dir = join(DIRECTORY_SEPARATOR, [$this->path, 'asset', $type]);
+ if (! is_dir($dir)) {
+ return [];
+ }
+
+ $this->{$type . 'AssetPath'} = $dir;
+
+ $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
+ $dir,
+ RecursiveDirectoryIterator::CURRENT_AS_FILEINFO | RecursiveDirectoryIterator::SKIP_DOTS
+ ));
+ if ($type === 'static') {
+ return $iterator;
+ }
+
+ return new CallbackFilterIterator(
+ $iterator,
+ function ($path) use ($type) {
+ if ($type === 'js' && $path->getExtension() === 'js') {
+ return substr($path->getPathname(), -5 - strlen($type)) !== ".min.$type";
+ } elseif ($type === 'css'
+ && ($path->getExtension() === 'css' || $path->getExtension() === 'less')
+ ) {
+ return substr($path->getPathname(), -5 - strlen($type)) !== ".min.$type";
+ }
+
+ return false;
+ }
+ );
+ };
+
+ $this->assets = [];
+
+ $jsAssets = $listAssets('js');
+ $this->assets['js'] = is_array($jsAssets) ? $jsAssets : iterator_to_array($jsAssets);
+
+ $cssAssets = $listAssets('css');
+ $this->assets['css'] = is_array($cssAssets) ? $cssAssets : iterator_to_array($cssAssets);
+
+ $staticAssets = $listAssets('static');
+ $this->assets['static'] = is_array($staticAssets) ? $staticAssets : iterator_to_array($staticAssets);
+
+ return $this->assets;
+ }
+}
diff --git a/library/Icinga/Application/Logger.php b/library/Icinga/Application/Logger.php
new file mode 100644
index 0000000..937029c
--- /dev/null
+++ b/library/Icinga/Application/Logger.php
@@ -0,0 +1,349 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application;
+
+use Icinga\Data\ConfigObject;
+use Icinga\Application\Logger\Writer\FileWriter;
+use Icinga\Application\Logger\Writer\SyslogWriter;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\IcingaException;
+use Icinga\Util\Json;
+use Throwable;
+
+/**
+ * Logger
+ */
+class Logger
+{
+ /**
+ * Debug message
+ */
+ const DEBUG = 1;
+
+ /**
+ * Informational message
+ */
+ const INFO = 2;
+
+ /**
+ * Warning message
+ */
+ const WARNING = 4;
+
+ /**
+ * Error message
+ */
+ const ERROR = 8;
+
+ /**
+ * Log levels
+ *
+ * @var array
+ */
+ public static $levels = array(
+ Logger::DEBUG => 'DEBUG',
+ Logger::INFO => 'INFO',
+ Logger::WARNING => 'WARNING',
+ Logger::ERROR => 'ERROR'
+ );
+
+ /**
+ * This logger's instance
+ *
+ * @var static
+ */
+ protected static $instance;
+
+ /**
+ * Log writer
+ *
+ * @var \Icinga\Application\Logger\LogWriter
+ */
+ protected $writer;
+
+ /**
+ * Maximum level to emit
+ *
+ * @var int
+ */
+ protected $level;
+
+ /**
+ * Error messages to be displayed prior to any other log message
+ *
+ * @var array
+ */
+ protected $configErrors = array();
+
+ /**
+ * Create a new logger object
+ *
+ * @param ConfigObject $config
+ *
+ * @throws ConfigurationError If the logging configuration directive 'log' is missing or if the logging level is
+ * not defined
+ */
+ public function __construct(ConfigObject $config)
+ {
+ if ($config->log === null) {
+ throw new ConfigurationError('Required logging configuration directive \'log\' missing');
+ }
+
+ $this->setLevel($config->get('level', static::ERROR));
+
+ if (strtolower($config->get('log', 'syslog')) !== 'none') {
+ $this->writer = $this->createWriter($config);
+ }
+ }
+
+ /**
+ * Set the logging level to use
+ *
+ * @param mixed $level
+ *
+ * @return $this
+ *
+ * @throws ConfigurationError In case the given level is invalid
+ */
+ public function setLevel($level)
+ {
+ if (is_numeric($level)) {
+ $level = (int) $level;
+ if (! isset(static::$levels[$level])) {
+ throw new ConfigurationError(
+ 'Can\'t set logging level %d. Logging level is invalid. Use one of %s or one of the'
+ . ' Logger\'s constants.',
+ $level,
+ implode(', ', array_keys(static::$levels))
+ );
+ }
+
+ $this->level = $level;
+ } else {
+ $level = strtoupper($level);
+ $levels = array_flip(static::$levels);
+ if (! isset($levels[$level])) {
+ throw new ConfigurationError(
+ 'Can\'t set logging level "%s". Logging level is invalid. Use one of %s.',
+ $level,
+ implode(', ', array_keys($levels))
+ );
+ }
+
+ $this->level = $levels[$level];
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the logging level being used
+ *
+ * @return int
+ */
+ public function getLevel()
+ {
+ return $this->level;
+ }
+
+ /**
+ * Register the given message as config error
+ *
+ * Config errors are logged every time a log message is being logged.
+ *
+ * @param mixed $arg,... A string, exception or format-string + substitutions
+ *
+ * @return $this
+ */
+ public function registerConfigError()
+ {
+ if (func_num_args() > 0) {
+ $this->configErrors[] = static::formatMessage(func_get_args());
+ }
+
+ return $this;
+ }
+
+ /**
+ * Create a new logger object
+ *
+ * @param ConfigObject $config
+ *
+ * @return static
+ */
+ public static function create(ConfigObject $config)
+ {
+ static::$instance = new static($config);
+ return static::$instance;
+ }
+
+ /**
+ * Create a log writer
+ *
+ * @param ConfigObject $config The configuration to initialize the writer with
+ *
+ * @return \Icinga\Application\Logger\LogWriter The requested log writer
+ * @throws ConfigurationError If the requested writer cannot be found
+ */
+ protected function createWriter(ConfigObject $config)
+ {
+ $class = 'Icinga\\Application\\Logger\\Writer\\' . ucfirst(strtolower($config->log)) . 'Writer';
+ if (! class_exists($class)) {
+ throw new ConfigurationError(
+ 'Cannot find log writer of type "%s"',
+ $config->log
+ );
+ }
+ return new $class($config);
+ }
+
+ /**
+ * Log a message
+ *
+ * @param int $level The logging level
+ * @param string $message The log message
+ */
+ public function log($level, $message)
+ {
+ if ($this->writer !== null && $this->level <= $level) {
+ foreach ($this->configErrors as $error_message) {
+ $this->writer->log(static::ERROR, $error_message);
+ }
+
+ $this->writer->log($level, $message);
+ }
+ }
+
+ /**
+ * Return a string representation of the passed arguments
+ *
+ * This method provides three different processing techniques:
+ * - If the only passed argument is a string it is returned unchanged
+ * - If the only passed argument is an exception it is formatted as follows:
+ * <name> in <file>:<line> with message: <message>[ <- <name> ...]
+ * - If multiple arguments are passed the first is interpreted as format-string
+ * that gets substituted with the remaining ones which can be of any type
+ *
+ * @param array $arguments The arguments to format
+ *
+ * @return string The formatted result
+ */
+ protected static function formatMessage(array $arguments)
+ {
+ if (count($arguments) === 1) {
+ $message = $arguments[0];
+
+ if ($message instanceof Throwable) {
+ $messages = array();
+ $error = $message;
+ do {
+ $messages[] = IcingaException::describe($error);
+ } while ($error = $error->getPrevious());
+ $message = implode(' <- ', $messages);
+ }
+
+ return $message;
+ }
+
+ return vsprintf(
+ array_shift($arguments),
+ array_map(
+ function ($a) {
+ return is_string($a) ? $a : ($a instanceof Throwable
+ ? IcingaException::describe($a)
+ : Json::encode($a));
+ },
+ $arguments
+ )
+ );
+ }
+
+ /**
+ * Log a message with severity ERROR
+ *
+ * @param mixed $arg,... A string, exception or format-string + substitutions
+ */
+ public static function error()
+ {
+ if (static::$instance !== null && func_num_args() > 0) {
+ static::$instance->log(static::ERROR, static::formatMessage(func_get_args()));
+ }
+ }
+
+ /**
+ * Log a message with severity WARNING
+ *
+ * @param mixed $arg,... A string, exception or format-string + substitutions
+ */
+ public static function warning()
+ {
+ if (static::$instance !== null && func_num_args() > 0) {
+ static::$instance->log(static::WARNING, static::formatMessage(func_get_args()));
+ }
+ }
+
+ /**
+ * Log a message with severity INFO
+ *
+ * @param mixed $arg,... A string, exception or format-string + substitutions
+ */
+ public static function info()
+ {
+ if (static::$instance !== null && func_num_args() > 0) {
+ static::$instance->log(static::INFO, static::formatMessage(func_get_args()));
+ }
+ }
+
+ /**
+ * Log a message with severity DEBUG
+ *
+ * @param mixed $arg,... A string, exception or format-string + substitutions
+ */
+ public static function debug()
+ {
+ if (static::$instance !== null && func_num_args() > 0) {
+ static::$instance->log(static::DEBUG, static::formatMessage(func_get_args()));
+ }
+ }
+
+ /**
+ * Get the log writer to use
+ *
+ * @return \Icinga\Application\Logger\LogWriter
+ */
+ public function getWriter()
+ {
+ return $this->writer;
+ }
+
+ /**
+ * Is the logger writing to Syslog?
+ *
+ * @return bool
+ */
+ public static function writesToSyslog()
+ {
+ return static::$instance && static::$instance->getWriter() instanceof SyslogWriter;
+ }
+
+ /**
+ * Is the logger writing to a file?
+ *
+ * @return bool
+ */
+ public static function writesToFile()
+ {
+ return static::$instance && static::$instance->getWriter() instanceof FileWriter;
+ }
+
+ /**
+ * Get this' instance
+ *
+ * @return static
+ */
+ public static function getInstance()
+ {
+ return static::$instance;
+ }
+}
diff --git a/library/Icinga/Application/Logger/LogWriter.php b/library/Icinga/Application/Logger/LogWriter.php
new file mode 100644
index 0000000..019bdad
--- /dev/null
+++ b/library/Icinga/Application/Logger/LogWriter.php
@@ -0,0 +1,30 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Logger;
+
+use Icinga\Data\ConfigObject;
+
+/**
+ * Abstract class for writers that write messages to a log
+ */
+abstract class LogWriter
+{
+ /**
+ * @var ConfigObject
+ */
+ protected $config;
+
+ /**
+ * Create a new log writer initialized with the given configuration
+ */
+ public function __construct(ConfigObject $config)
+ {
+ $this->config = $config;
+ }
+
+ /**
+ * Log a message with the given severity
+ */
+ abstract public function log($severity, $message);
+}
diff --git a/library/Icinga/Application/Logger/Writer/FileWriter.php b/library/Icinga/Application/Logger/Writer/FileWriter.php
new file mode 100644
index 0000000..6b4ed54
--- /dev/null
+++ b/library/Icinga/Application/Logger/Writer/FileWriter.php
@@ -0,0 +1,80 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Logger\Writer;
+
+use Exception;
+use Icinga\Data\ConfigObject;
+use Icinga\Application\Logger;
+use Icinga\Application\Logger\LogWriter;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Util\File;
+
+/**
+ * Log to a file
+ */
+class FileWriter extends LogWriter
+{
+ /**
+ * Path to the file
+ *
+ * @var string
+ */
+ protected $file;
+
+ /**
+ * Create a new file log writer
+ *
+ * @param ConfigObject $config
+ *
+ * @throws ConfigurationError If the configuration directive 'file' is missing or if the path to 'file' does
+ * not exist or if writing to 'file' is not possible
+ */
+ public function __construct(ConfigObject $config)
+ {
+ if ($config->file === null) {
+ throw new ConfigurationError('Required logging configuration directive \'file\' missing');
+ }
+ $this->file = $config->file;
+
+ if (substr($this->file, 0, 6) !== 'php://' && ! file_exists(dirname($this->file))) {
+ throw new ConfigurationError(
+ 'Log path "%s" does not exist',
+ dirname($this->file)
+ );
+ }
+
+ try {
+ $this->write(''); // Avoid to handle such errors on every write access
+ } catch (Exception $e) {
+ throw new ConfigurationError(
+ 'Cannot write to log file "%s" (%s)',
+ $this->file,
+ $e->getMessage()
+ );
+ }
+ }
+
+ /**
+ * Log a message
+ *
+ * @param int $level The logging level
+ * @param string $message The log message
+ */
+ public function log($level, $message)
+ {
+ $this->write(date('c') . ' - ' . Logger::$levels[$level] . ' - ' . $message . PHP_EOL);
+ }
+
+ /**
+ * Write a message to the log
+ *
+ * @param string $message
+ */
+ protected function write($message)
+ {
+ $file = new File($this->file, 'a');
+ $file->fwrite($message);
+ $file->fflush();
+ }
+}
diff --git a/library/Icinga/Application/Logger/Writer/PhpWriter.php b/library/Icinga/Application/Logger/Writer/PhpWriter.php
new file mode 100644
index 0000000..dedb2bd
--- /dev/null
+++ b/library/Icinga/Application/Logger/Writer/PhpWriter.php
@@ -0,0 +1,39 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Logger\Writer;
+
+use Icinga\Application\Logger;
+use Icinga\Application\Logger\LogWriter;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\NotWritableError;
+
+/**
+ * Log to the webserver log, a file or syslog
+ *
+ * @see https://secure.php.net/manual/en/errorfunc.configuration.php#ini.error-log
+ */
+class PhpWriter extends LogWriter
+{
+ /**
+ * Prefix to prepend to each message
+ *
+ * @var string
+ */
+ protected $ident;
+
+ public function __construct(ConfigObject $config)
+ {
+ parent::__construct($config);
+ $this->ident = $config->get('application', 'icingaweb2');
+ }
+
+ public function log($severity, $message)
+ {
+ if (ini_get('error_log') === 'syslog') {
+ $message = str_replace("\n", ' ', $message);
+ }
+
+ error_log($this->ident . ': ' . Logger::$levels[$severity] . ' - ' . $message);
+ }
+}
diff --git a/library/Icinga/Application/Logger/Writer/StderrWriter.php b/library/Icinga/Application/Logger/Writer/StderrWriter.php
new file mode 100644
index 0000000..7df4278
--- /dev/null
+++ b/library/Icinga/Application/Logger/Writer/StderrWriter.php
@@ -0,0 +1,62 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Logger\Writer;
+
+use Icinga\Cli\Screen;
+use Icinga\Application\Logger;
+use Icinga\Application\Logger\LogWriter;
+
+/**
+ * Class to write log messages to STDERR
+ */
+class StderrWriter extends LogWriter
+{
+ /**
+ * The current Screen in use
+ *
+ * @var Screen
+ */
+ protected $screen;
+
+ /**
+ * Return the current Screen
+ *
+ * @return Screen
+ */
+ protected function screen()
+ {
+ if ($this->screen === null) {
+ $this->screen = Screen::instance(STDERR);
+ }
+
+ return $this->screen;
+ }
+
+ /**
+ * Log a message with the given severity
+ *
+ * @param int $severity The severity to use
+ * @param string $message The message to log
+ */
+ public function log($severity, $message)
+ {
+ $color = null;
+ switch ($severity) {
+ case Logger::ERROR:
+ $color = 'red';
+ break;
+ case Logger::WARNING:
+ $color = 'yellow';
+ break;
+ case Logger::INFO:
+ $color = 'green';
+ break;
+ case Logger::DEBUG:
+ $color = 'blue';
+ break;
+ }
+
+ file_put_contents('php://stderr', $this->screen()->colorize($message, $color) . "\n");
+ }
+}
diff --git a/library/Icinga/Application/Logger/Writer/StdoutWriter.php b/library/Icinga/Application/Logger/Writer/StdoutWriter.php
new file mode 100644
index 0000000..a6f43e5
--- /dev/null
+++ b/library/Icinga/Application/Logger/Writer/StdoutWriter.php
@@ -0,0 +1,13 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Logger\Writer;
+
+/**
+ * Deprecated, compat only.
+ *
+ * Use Icinga\Application\Logger\Writer\StderrWriter instead.
+ */
+class StdoutWriter extends StderrWriter
+{
+}
diff --git a/library/Icinga/Application/Logger/Writer/SyslogWriter.php b/library/Icinga/Application/Logger/Writer/SyslogWriter.php
new file mode 100644
index 0000000..93efc2a
--- /dev/null
+++ b/library/Icinga/Application/Logger/Writer/SyslogWriter.php
@@ -0,0 +1,90 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Logger\Writer;
+
+use Icinga\Data\ConfigObject;
+use Icinga\Application\Logger;
+use Icinga\Application\Logger\LogWriter;
+use Icinga\Exception\ConfigurationError;
+
+/**
+ * Log to the syslog service
+ */
+class SyslogWriter extends LogWriter
+{
+ /**
+ * Syslog facility
+ *
+ * @var int
+ */
+ protected $facility;
+
+ /**
+ * Prefix to prepend to each message
+ *
+ * @var string
+ */
+ protected $ident;
+
+ /**
+ * Known syslog facilities
+ *
+ * @var array
+ */
+ public static $facilities = array(
+ 'user' => LOG_USER,
+ 'local0' => LOG_LOCAL0,
+ 'local1' => LOG_LOCAL1,
+ 'local2' => LOG_LOCAL2,
+ 'local3' => LOG_LOCAL3,
+ 'local4' => LOG_LOCAL4,
+ 'local5' => LOG_LOCAL5,
+ 'local6' => LOG_LOCAL6,
+ 'local7' => LOG_LOCAL7
+ );
+
+ /**
+ * Log level to syslog severity map
+ *
+ * @var array
+ */
+ public static $severityMap = array(
+ Logger::ERROR => LOG_ERR,
+ Logger::WARNING => LOG_WARNING,
+ Logger::INFO => LOG_INFO,
+ Logger::DEBUG => LOG_DEBUG
+ );
+
+ /**
+ * Create a new syslog log writer
+ *
+ * @param ConfigObject $config
+ */
+ public function __construct(ConfigObject $config)
+ {
+ $this->ident = $config->get('application', 'icingaweb2');
+
+ $configuredFacility = $config->get('facility', 'user');
+ if (! isset(static::$facilities[$configuredFacility])) {
+ throw new ConfigurationError(
+ 'Invalid logging facility: "%s" (expected one of: %s)',
+ $configuredFacility,
+ implode(', ', array_keys(static::$facilities))
+ );
+ }
+ $this->facility = static::$facilities[$configuredFacility];
+ }
+
+ /**
+ * Log a message
+ *
+ * @param int $level The logging level
+ * @param string $message The log message
+ */
+ public function log($level, $message)
+ {
+ openlog($this->ident, LOG_PID, $this->facility);
+ syslog(static::$severityMap[$level], str_replace("\n", ' ', $message));
+ }
+}
diff --git a/library/Icinga/Application/MigrationManager.php b/library/Icinga/Application/MigrationManager.php
new file mode 100644
index 0000000..9d32896
--- /dev/null
+++ b/library/Icinga/Application/MigrationManager.php
@@ -0,0 +1,417 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Application;
+
+use Countable;
+use Generator;
+use Icinga\Application\Hook\DbMigrationHook;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Setup\Utils\DbTool;
+use Icinga\Module\Setup\WebWizard;
+use ipl\I18n\Translation;
+use ipl\Sql;
+use ReflectionClass;
+
+/**
+ * Migration manager allows you to manage all pending migrations in a structured way.
+ */
+final class MigrationManager implements Countable
+{
+ use Translation;
+
+ /** @var array<string, DbMigrationHook> All pending migration hooks */
+ protected $pendingMigrations;
+
+ /** @var MigrationManager */
+ private static $instance;
+
+ private function __construct()
+ {
+ }
+
+ /**
+ * Get the instance of this manager
+ *
+ * @return $this
+ */
+ public static function instance(): self
+ {
+ if (self::$instance === null) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Get all pending migrations
+ *
+ * @return array<string, DbMigrationHook>
+ */
+ public function getPendingMigrations(): array
+ {
+ if ($this->pendingMigrations === null) {
+ $this->load();
+ }
+
+ return $this->pendingMigrations;
+ }
+
+ /**
+ * Get whether there are any pending migrations
+ *
+ * @return bool
+ */
+ public function hasPendingMigrations(): bool
+ {
+ return $this->count() > 0;
+ }
+
+ public function hasMigrations(string $module): bool
+ {
+ if (! $this->hasPendingMigrations()) {
+ return false;
+ }
+
+ return isset($this->getPendingMigrations()[$module]);
+ }
+
+ /**
+ * Get pending migration matching the given module name
+ *
+ * @param string $module
+ *
+ * @return DbMigrationHook
+ *
+ * @throws NotFoundError When there are no pending migrations matching the given module name
+ */
+ public function getMigration(string $module): DbMigrationHook
+ {
+ if (! $this->hasMigrations($module)) {
+ throw new NotFoundError('There are no pending migrations matching the given name: %s', $module);
+ }
+
+ return $this->getPendingMigrations()[$module];
+ }
+
+ /**
+ * Get the number of all pending migrations
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return count($this->getPendingMigrations());
+ }
+
+ /**
+ * Apply all pending migrations matching the given migration module name
+ *
+ * @param string $module
+ *
+ * @return bool
+ */
+ public function applyByName(string $module): bool
+ {
+ $migration = $this->getMigration($module);
+ if ($migration->isModule() && $this->hasMigrations(DbMigrationHook::DEFAULT_MODULE)) {
+ return false;
+ }
+
+ return $this->apply($migration);
+ }
+
+ /**
+ * Apply the given migration hook
+ *
+ * @param DbMigrationHook $hook
+ * @param ?array<string, string> $elevateConfig
+ *
+ * @return bool
+ */
+ public function apply(DbMigrationHook $hook, array $elevateConfig = null): bool
+ {
+ if ($hook->isModule() && $this->hasMigrations(DbMigrationHook::DEFAULT_MODULE)) {
+ Logger::error(
+ 'Please apply the Icinga Web pending migration(s) first or apply all the migrations instead'
+ );
+
+ return false;
+ }
+
+ $conn = $hook->getDb();
+ if ($elevateConfig && ! $this->checkRequiredPrivileges($conn)) {
+ $conn = $this->elevateDatabaseConnection($conn, $elevateConfig);
+ }
+
+ if ($hook->run($conn)) {
+ unset($this->pendingMigrations[$hook->getModuleName()]);
+
+ Logger::info('Applied pending %s migrations successfully', $hook->getName());
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Apply all pending modules/framework migrations
+ *
+ * @param ?array<string, string> $elevateConfig
+ *
+ * @return bool
+ */
+ public function applyAll(array $elevateConfig = null): bool
+ {
+ $default = DbMigrationHook::DEFAULT_MODULE;
+ if ($this->hasMigrations($default)) {
+ $migration = $this->getMigration($default);
+ if (! $this->apply($migration, $elevateConfig)) {
+ return false;
+ }
+ }
+
+ $succeeded = true;
+ foreach ($this->getPendingMigrations() as $migration) {
+ if (! $this->apply($migration, $elevateConfig) && $succeeded) {
+ $succeeded = false;
+ }
+ }
+
+ return $succeeded;
+ }
+
+ /**
+ * Yield module and framework pending migrations separately
+ *
+ * @param bool $modules
+ *
+ * @return Generator<DbMigrationHook>
+ */
+ public function yieldMigrations(bool $modules = false): Generator
+ {
+ foreach ($this->getPendingMigrations() as $migration) {
+ if ($modules === $migration->isModule()) {
+ yield $migration;
+ }
+ }
+ }
+
+ /**
+ * Get the required database privileges for database migrations
+ *
+ * @return string[]
+ */
+ public function getRequiredDatabasePrivileges(): array
+ {
+ return ['CREATE','SELECT','INSERT','UPDATE','DELETE','DROP','ALTER','CREATE VIEW','INDEX','EXECUTE','USAGE'];
+ }
+
+ /**
+ * Verify whether all database users of all pending migrations do have the required SQL privileges
+ *
+ * @param ?array<string, string> $elevateConfig
+ * @param bool $canIssueGrant
+ *
+ * @return bool
+ */
+ public function validateDatabasePrivileges(array $elevateConfig = null, bool $canIssueGrant = false): bool
+ {
+ if (! $this->hasPendingMigrations()) {
+ return true;
+ }
+
+ foreach ($this->getPendingMigrations() as $migration) {
+ if (! $this->checkRequiredPrivileges($migration->getDb(), $elevateConfig, $canIssueGrant)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if there are missing grants for the Icinga Web database and fix them
+ *
+ * This fixes the following problems on existing installations:
+ * - Setups made by the wizard have no access to `icingaweb_schema`
+ * - Setups made by the wizard have no DDL grants
+ * - Setups done manually using the advanced documentation chapter have no DDL grants
+ *
+ * @param Sql\Connection $db
+ * @param array<string, string> $elevateConfig
+ */
+ public function fixIcingaWebMysqlGrants(Sql\Connection $db, array $elevateConfig): void
+ {
+ $wizardProperties = (new ReflectionClass(WebWizard::class))
+ ->getDefaultProperties();
+ /** @var array<int, string> $privileges */
+ $privileges = $wizardProperties['databaseUsagePrivileges'];
+ /** @var array<int, string> $tables */
+ $tables = $wizardProperties['databaseTables'];
+
+ $actualUsername = $db->getConfig()->username;
+ $db = $this->elevateDatabaseConnection($db, $elevateConfig);
+ $tool = $this->createDbTool($db);
+ $tool->connectToDb();
+
+ $isPgsql = $db->getAdapter() instanceof Sql\Adapter\Pgsql;
+ // PgSQL doesn't have SELECT privilege on a database level and granting the CREATE,CONNECT, and TEMPORARY
+ // privileges on a database doesn't permit a user to read data from a table. Hence, we have to grant the
+ // required database,schema and table privileges simultaneously.
+ if (! $isPgsql && $tool->checkPrivileges(['SELECT'], [], $actualUsername)) {
+ // Checks only database level grants. If this succeeds, the grants were issued manually.
+ if (! $tool->checkPrivileges($privileges, [], $actualUsername) && $tool->isGrantable($privileges)) {
+ // Any missing grant is now granted on database level as well, not to mix things up
+ $tool->grantPrivileges($privileges, [], $actualUsername);
+ }
+ } elseif (! $tool->checkPrivileges($privileges, $tables, $actualUsername) && $tool->isGrantable($privileges)) {
+ // The above ensures that if this fails, we can safely apply table level grants, as it's
+ // very likely that the existing grants were issued by the setup wizard
+ $tool->grantPrivileges($privileges, $tables, $actualUsername);
+ }
+ }
+
+ /**
+ * Create and return a DbTool instance
+ *
+ * @param Sql\Connection $db
+ *
+ * @return DbTool
+ */
+ private function createDbTool(Sql\Connection $db): DbTool
+ {
+ $config = $db->getConfig();
+
+ return new DbTool(array_merge([
+ 'db' => $config->db,
+ 'host' => $config->host,
+ 'port' => $config->port,
+ 'dbname' => $config->dbname,
+ 'username' => $config->username,
+ 'password' => $config->password,
+ 'charset' => $config->charset
+ ], $db->getAdapter()->getOptions($config)));
+ }
+
+ protected function load(): void
+ {
+ $this->pendingMigrations = [];
+
+ /** @var DbMigrationHook $hook */
+ foreach (Hook::all('DbMigration') as $hook) {
+ if (empty($hook->getMigrations())) {
+ continue;
+ }
+
+ $this->pendingMigrations[$hook->getModuleName()] = $hook;
+ }
+
+ ksort($this->pendingMigrations);
+ }
+
+ /**
+ * Check the required SQL privileges of the given connection
+ *
+ * @param Sql\Connection $conn
+ * @param ?array<string, string> $elevateConfig
+ * @param bool $canIssueGrants
+ *
+ * @return bool
+ */
+ protected function checkRequiredPrivileges(
+ Sql\Connection $conn,
+ array $elevateConfig = null,
+ bool $canIssueGrants = false
+ ): bool {
+ if ($elevateConfig) {
+ $conn = $this->elevateDatabaseConnection($conn, $elevateConfig);
+ }
+
+ $wizardProperties = (new ReflectionClass(WebWizard::class))
+ ->getDefaultProperties();
+ /** @var array<int, string> $tables */
+ $tables = $wizardProperties['databaseTables'];
+
+ $dbTool = $this->createDbTool($conn);
+ $dbTool->connectToDb();
+
+ $isPgsql = $conn->getAdapter() instanceof Sql\Adapter\Pgsql;
+ $privileges = $this->getRequiredDatabasePrivileges();
+ $dbPrivilegesGranted = $dbTool->checkPrivileges($privileges);
+ $tablePrivilegesGranted = $dbTool->checkPrivileges($privileges, $tables);
+ if (! $dbPrivilegesGranted && ($isPgsql || ! $tablePrivilegesGranted)) {
+ return false;
+ }
+
+ if ($isPgsql && ! $tablePrivilegesGranted) {
+ return false;
+ }
+
+ if ($canIssueGrants && ! $dbTool->isGrantable($privileges)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Override the database config of the given connection by the specified new config
+ *
+ * Overrides only the username and password of existing database connection.
+ *
+ * @param Sql\Connection $conn
+ * @param array<string, string> $elevateConfig
+ * @return Sql\Connection
+ */
+ protected function elevateDatabaseConnection(Sql\Connection $conn, array $elevateConfig): Sql\Connection
+ {
+ $config = clone $conn->getConfig();
+ $config->username = $elevateConfig['username'];
+ $config->password = $elevateConfig['password'];
+
+ return new Sql\Connection($config);
+ }
+
+ /**
+ * Get all pending migrations as an array
+ *
+ * @return array<string, mixed>
+ */
+ public function toArray(): array
+ {
+ $framework = [];
+ $serialize = function (DbMigrationHook $hook): array {
+ $serialized = [
+ 'name' => $hook->getName(),
+ 'module' => $hook->getModuleName(),
+ 'isModule' => $hook->isModule(),
+ 'migrated_version' => $hook->getVersion(),
+ 'migrations' => []
+ ];
+
+ foreach ($hook->getMigrations() as $migration) {
+ $serialized['migrations'][$migration->getVersion()] = [
+ 'path' => $migration->getScriptPath(),
+ 'error' => $migration->getLastState()
+ ];
+ }
+
+ return $serialized;
+ };
+
+ foreach ($this->yieldMigrations() as $migration) {
+ $framework[] = $serialize($migration);
+ }
+
+ $modules = [];
+ foreach ($this->yieldMigrations(true) as $migration) {
+ $modules[] = $serialize($migration);
+ }
+
+ return ['System' => $framework, 'Modules' => $modules];
+ }
+}
diff --git a/library/Icinga/Application/Modules/DashboardContainer.php b/library/Icinga/Application/Modules/DashboardContainer.php
new file mode 100644
index 0000000..f3c8bc6
--- /dev/null
+++ b/library/Icinga/Application/Modules/DashboardContainer.php
@@ -0,0 +1,58 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Modules;
+
+/**
+ * Container for module dashboards
+ */
+class DashboardContainer extends NavigationItemContainer
+{
+ /**
+ * This dashboard's dashlets
+ *
+ * @var array
+ */
+ protected $dashlets;
+
+ /**
+ * Set this dashboard's dashlets
+ *
+ * @param array $dashlets
+ *
+ * @return $this
+ */
+ public function setDashlets(array $dashlets)
+ {
+ $this->dashlets = $dashlets;
+ return $this;
+ }
+
+ /**
+ * Return this dashboard's dashlets
+ *
+ * @return array
+ */
+ public function getDashlets()
+ {
+ return $this->dashlets ?: array();
+ }
+
+ /**
+ * Add a new dashlet
+ *
+ * @param string $name
+ * @param string $url
+ * @param int $priority
+ *
+ * @return $this
+ */
+ public function add($name, $url, $priority = null)
+ {
+ $this->dashlets[$name] = [
+ 'url' => $url,
+ 'priority' => $priority
+ ];
+ return $this;
+ }
+}
diff --git a/library/Icinga/Application/Modules/Manager.php b/library/Icinga/Application/Modules/Manager.php
new file mode 100644
index 0000000..55d074d
--- /dev/null
+++ b/library/Icinga/Application/Modules/Manager.php
@@ -0,0 +1,698 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Modules;
+
+use Icinga\Application\ApplicationBootstrap;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Data\SimpleQuery;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\SystemPermissionException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\NotReadableError;
+
+/**
+ * Module manager that handles detecting, enabling and disabling of modules
+ *
+ * Modules can have 3 states:
+ * * installed, module exists but is disabled
+ * * enabled, module enabled and should be loaded
+ * * loaded, module enabled and loaded via the autoloader
+ *
+ */
+class Manager
+{
+ /**
+ * Namespace for module permissions
+ *
+ * @var string
+ */
+ const MODULE_PERMISSION_NS = 'module/';
+
+ /**
+ * Array of all installed module's base directories
+ *
+ * @var array
+ */
+ private $installedBaseDirs = array();
+
+ /**
+ * Array of all enabled modules base dirs
+ *
+ * @var array
+ */
+ private $enabledDirs = array();
+
+ /**
+ * Array of all module names that have been loaded
+ *
+ * @var array
+ */
+ private $loadedModules = array();
+
+ /**
+ * Reference to Icinga::app
+ *
+ * @var Icinga
+ */
+ private $app;
+
+ /**
+ * The directory that is used to detect enabled modules
+ *
+ * @var string
+ */
+ private $enableDir;
+
+ /**
+ * All paths to look for installed modules that can be enabled
+ *
+ * @var array
+ */
+ private $modulePaths = array();
+
+ /**
+ * Whether we loaded all enabled modules
+ *
+ * @var bool
+ */
+ private $loadedAllEnabledModules = false;
+
+ /**
+ * Create a new instance of the module manager
+ *
+ * @param ApplicationBootstrap $app
+ * @param string $enabledDir Enabled modules location. The application maintains symlinks within
+ * the given path
+ * @param array $availableDirs Installed modules location
+ **/
+ public function __construct($app, $enabledDir, array $availableDirs)
+ {
+ $this->app = $app;
+ $this->modulePaths = $availableDirs;
+ $this->enableDir = $enabledDir;
+ }
+
+ /**
+ * Query interface for the module manager
+ *
+ * @return SimpleQuery
+ */
+ public function select()
+ {
+ $source = new ArrayDatasource($this->getModuleInfo());
+ return $source->select();
+ }
+
+ /**
+ * Check for enabled modules
+ *
+ * Update the internal $enabledDirs property with the enabled modules.
+ *
+ * @throws ConfigurationError If module dir does not exist, is not a directory or not readable
+ */
+ private function detectEnabledModules()
+ {
+ if (! file_exists($parent = dirname($this->enableDir))) {
+ return;
+ }
+ if (! is_readable($parent)) {
+ throw new NotReadableError(
+ 'Cannot read enabled modules. Config directory "%s" is not readable',
+ $parent
+ );
+ }
+
+ if (! file_exists($this->enableDir)) {
+ return;
+ }
+ if (! is_dir($this->enableDir)) {
+ throw new NotReadableError(
+ 'Cannot read enabled modules. Module directory "%s" is not a directory',
+ $this->enableDir
+ );
+ }
+ if (! is_readable($this->enableDir)) {
+ throw new NotReadableError(
+ 'Cannot read enabled modules. Module directory "%s" is not readable',
+ $this->enableDir
+ );
+ }
+ if (($dh = opendir($this->enableDir)) !== false) {
+ $isPhar = substr($this->enableDir, 0, 8) === 'phar:///';
+ $this->enabledDirs = array();
+ while (($file = readdir($dh)) !== false) {
+ if ($file[0] === '.' || $file === 'README') {
+ continue;
+ }
+
+ $link = $this->enableDir . DIRECTORY_SEPARATOR . $file;
+ if (! $isPhar && ! is_link($link)) {
+ Logger::warning(
+ 'Found invalid module in enabledModule directory "%s": "%s" is not a symlink',
+ $this->enableDir,
+ $link
+ );
+ continue;
+ }
+
+ $dir = $isPhar ? $link : realpath($link);
+ if ($dir !== false && is_dir($dir)) {
+ $this->enabledDirs[$file] = $dir;
+ } else {
+ $this->enabledDirs[$file] = null;
+
+ Logger::warning(
+ 'Found invalid module in enabledModule directory "%s": "%s" points to non existing path "%s"',
+ $this->enableDir,
+ $link,
+ $dir
+ );
+ }
+
+ ksort($this->enabledDirs);
+ }
+ closedir($dh);
+ }
+ }
+
+ /**
+ * Try to set all enabled modules in loaded sate
+ *
+ * @return $this
+ * @see Manager::loadModule()
+ */
+ public function loadEnabledModules()
+ {
+ if (! $this->loadedAllEnabledModules) {
+ foreach ($this->listEnabledModules() as $name) {
+ $this->loadModule($name);
+ }
+
+ $this->loadedAllEnabledModules = true;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Whether we loaded all enabled modules
+ *
+ * @return bool
+ */
+ public function loadedAllEnabledModules()
+ {
+ return $this->loadedAllEnabledModules;
+ }
+
+
+ /**
+ * Try to load the module and register it in the application
+ *
+ * @param string $name The name of the module to load
+ * @param mixed $basedir Optional module base directory
+ *
+ * @return $this
+ */
+ public function loadModule($name, $basedir = null)
+ {
+ if ($this->hasLoaded($name)) {
+ return $this;
+ }
+
+ $module = null;
+ if ($basedir === null) {
+ $module = new Module($this->app, $name, $this->getModuleDir($name));
+ } else {
+ $module = new Module($this->app, $name, $basedir);
+ }
+
+ if ($name !== 'ipl' && $name !== 'reactbundle') {
+ $module->register();
+ }
+
+ $this->loadedModules[$name] = $module;
+ return $this;
+ }
+
+ /**
+ * Set the given module to the enabled state
+ *
+ * @param string $name The module to enable
+ * @param bool $force Whether to ignore unmet dependencies
+ *
+ * @return $this
+ * @throws ConfigurationError When trying to enable a module that is not installed
+ * @throws SystemPermissionException When insufficient permissions for the application exist
+ */
+ public function enableModule($name, $force = false)
+ {
+ if (! $this->hasInstalled($name)) {
+ throw new ConfigurationError(
+ 'Cannot enable module "%s". Module is not installed.',
+ $name
+ );
+ }
+
+ if (strtolower(substr($name, 0, 18)) === 'icingaweb2-module-') {
+ throw new ConfigurationError(
+ 'Cannot enable module "%s": Directory name does not match the module\'s name.'
+ . ' Please rename the module to "%s" before enabling.',
+ $name,
+ substr($name, 18)
+ );
+ }
+
+ if ($this->hasUnmetDependencies($name)) {
+ if ($force) {
+ Logger::warning(t('Enabling module "%s" although it has unmet dependencies'), $name);
+ } else {
+ throw new ConfigurationError(
+ t('Module "%s" can\'t be enabled. Module has unmet dependencies'),
+ $name
+ );
+ }
+ }
+
+ clearstatcache(true);
+ $target = $this->installedBaseDirs[$name];
+ $link = $this->enableDir . DIRECTORY_SEPARATOR . $name;
+
+ if (! is_dir($this->enableDir)) {
+ if (!@mkdir($this->enableDir, 0777, true)) {
+ $error = error_get_last();
+ throw new SystemPermissionException(
+ 'Failed to create enabledModules directory "%s" (%s)',
+ $this->enableDir,
+ $error['message']
+ );
+ }
+
+ chmod($this->enableDir, 02770);
+ } elseif (! is_writable($this->enableDir)) {
+ throw new SystemPermissionException(
+ 'Cannot enable module "%s". Check the permissions for the enabledModules directory: %s',
+ $name,
+ $this->enableDir
+ );
+ }
+
+ $this->loadedAllEnabledModules = false;
+
+ if (file_exists($link) && is_link($link)) {
+ return $this;
+ }
+
+ if (! @symlink($target, $link)) {
+ $error = error_get_last();
+ if (strstr($error["message"], "File exists") === false) {
+ throw new SystemPermissionException(
+ 'Cannot enable module "%s" at %s due to file system errors. '
+ . 'Please check path and mounting points because this is not a permission error. '
+ . 'Primary error was: %s',
+ $name,
+ $this->enableDir,
+ $error['message']
+ );
+ }
+ }
+
+ $this->enabledDirs[$name] = $link;
+ $this->loadModule($name);
+ return $this;
+ }
+
+ /**
+ * Disable the given module and remove its enabled state
+ *
+ * @param string $name The name of the module to disable
+ *
+ * @return $this
+ *
+ * @throws ConfigurationError When the module is not installed or it's not a symlink
+ * @throws SystemPermissionException When insufficient permissions for the application exist
+ */
+ public function disableModule($name)
+ {
+ if (! $this->hasEnabled($name)) {
+ throw new ConfigurationError(
+ 'Cannot disable module "%s". Module is not installed.',
+ $name
+ );
+ }
+
+ if (! is_writable($this->enableDir)) {
+ throw new SystemPermissionException(
+ 'Cannot disable module "%s". Check the permissions for the enabledModules directory: %s',
+ $name,
+ $this->enableDir
+ );
+ }
+
+ $link = $this->enableDir . DIRECTORY_SEPARATOR . $name;
+ if (! is_link($link)) {
+ throw new ConfigurationError(
+ 'Cannot disable module %s at %s. '
+ . 'It looks like you have installed this module manually and moved it to your module folder. '
+ . 'In order to dynamically enable and disable modules, you have to create a symlink to '
+ . 'the enabledModules folder.',
+ $name,
+ $this->enableDir
+ );
+ }
+
+ if (is_link($link)) {
+ if (! @unlink($link)) {
+ $error = error_get_last();
+ throw new SystemPermissionException(
+ 'Cannot enable module "%s" at %s due to file system errors. '
+ . 'Please check path and mounting points because this is not a permission error. '
+ . 'Primary error was: %s',
+ $name,
+ $this->enableDir,
+ $error['message']
+ );
+ }
+ }
+
+ unset($this->enabledDirs[$name]);
+ return $this;
+ }
+
+ /**
+ * Return the directory of the given module as a string, optionally with a given sub directoy
+ *
+ * @param string $name The module name to return the module directory of
+ * @param string $subdir The sub directory to append to the path
+ *
+ * @return string
+ *
+ * @throws ProgrammingError When the module is not installed or existing
+ */
+ public function getModuleDir($name, $subdir = '')
+ {
+ if ($this->hasLoaded($name)) {
+ return $this->getModule($name)->getBaseDir() . $subdir;
+ }
+
+ if ($this->hasEnabled($name)) {
+ return $this->enabledDirs[$name]. $subdir;
+ }
+
+ if ($this->hasInstalled($name)) {
+ return $this->installedBaseDirs[$name] . $subdir;
+ }
+
+ throw new ProgrammingError(
+ 'Trying to access uninstalled module dir: %s',
+ $name
+ );
+ }
+
+ /**
+ * Return true when the module with the given name is installed, otherwise false
+ *
+ * @param string $name The module to check for being installed
+ *
+ * @return bool
+ */
+ public function hasInstalled($name)
+ {
+ if (!count($this->installedBaseDirs)) {
+ $this->detectInstalledModules();
+ }
+ return array_key_exists($name, $this->installedBaseDirs);
+ }
+
+ /**
+ * Return true when the given module is in enabled state, otherwise false
+ *
+ * @param string $name The module to check for being enabled
+ *
+ * @return bool
+ */
+ public function hasEnabled($name)
+ {
+ return array_key_exists($name, $this->enabledDirs);
+ }
+
+ /**
+ * Return true when the module is in loaded state, otherwise false
+ *
+ * @param string $name The module to check for being loaded
+ *
+ * @return bool
+ */
+ public function hasLoaded($name)
+ {
+ return array_key_exists($name, $this->loadedModules);
+ }
+
+ /**
+ * Check if a module with the given name is enabled
+ *
+ * Passing a version constraint also verifies that the module's version matches.
+ *
+ * @param string $name
+ * @param string $version
+ *
+ * @return bool
+ */
+ public function has($name, $version = null)
+ {
+ if (! $this->hasEnabled($name)) {
+ return false;
+ } elseif ($version === null || $version === true) {
+ return true;
+ }
+
+ $operator = '=';
+ if (preg_match('/^([<>=]{1,2})\s*v?((?:[\d.]+)(?:.+)?)$/', $version, $match)) {
+ $operator = $match[1];
+ $version = $match[2];
+ }
+
+ $modVersion = ltrim($this->getModule($name)->getVersion(), 'v');
+ return version_compare($modVersion, $version, $operator);
+ }
+
+ /**
+ * Get the currently loaded modules
+ *
+ * @return Module[]
+ */
+ public function getLoadedModules()
+ {
+ return $this->loadedModules;
+ }
+
+ /**
+ * Get a module
+ *
+ * @param string $name Name of the module
+ * @param bool $assertLoaded Whether or not to throw an exception if the module hasn't been loaded
+ *
+ * @return Module
+ * @throws ProgrammingError If the module hasn't been loaded
+ */
+ public function getModule($name, $assertLoaded = true)
+ {
+ if ($this->hasLoaded($name)) {
+ return $this->loadedModules[$name];
+ } elseif (! (bool) $assertLoaded) {
+ return new Module($this->app, $name, $this->getModuleDir($name));
+ }
+ throw new ProgrammingError(
+ 'Can\'t access module %s because it hasn\'t been loaded',
+ $name
+ );
+ }
+
+ /**
+ * Return an array containing information objects for each available module
+ *
+ * Each entry has the following fields
+ * * name, name of the module as a string
+ * * path, path where the module is located as a string
+ * * installed, whether the module is installed or not as a boolean
+ * * enabled, whether the module is enabled or not as a boolean
+ * * loaded, whether the module is loaded or not as a boolean
+ *
+ * @return array
+ */
+ public function getModuleInfo()
+ {
+ $info = array();
+
+ $installed = $this->listInstalledModules();
+ foreach ($installed as $name) {
+ $info[$name] = (object) array(
+ 'name' => $name,
+ 'path' => $this->installedBaseDirs[$name],
+ 'installed' => true,
+ 'enabled' => $this->hasEnabled($name),
+ 'loaded' => $this->hasLoaded($name)
+ );
+ }
+
+ $enabled = $this->listEnabledModules();
+ foreach ($enabled as $name) {
+ $info[$name] = (object) array(
+ 'name' => $name,
+ 'path' => $this->enabledDirs[$name],
+ 'installed' => $this->enabledDirs[$name] !== null,
+ 'enabled' => true,
+ 'loaded' => $this->hasLoaded($name)
+ );
+ }
+
+ return $info;
+ }
+
+ /**
+ * Check if the given module has unmet dependencies
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function hasUnmetDependencies($name)
+ {
+ $module = $this->getModule($name, false);
+
+ $requiredMods = $module->getRequiredModules();
+
+ if (isset($requiredMods['monitoring'], $requiredMods['icingadb'])) {
+ if (! $this->has('monitoring', $requiredMods['monitoring'])
+ && ! $this->has('icingadb', $requiredMods['icingadb'])
+ ) {
+ return true;
+ }
+
+ unset($requiredMods['monitoring'], $requiredMods['icingadb']);
+ }
+
+ foreach ($requiredMods as $moduleName => $moduleVersion) {
+ if (! $this->has($moduleName, $moduleVersion)) {
+ return true;
+ }
+ }
+
+ $libraries = Icinga::app()->getLibraries();
+
+ $requiredLibs = $module->getRequiredLibraries();
+ foreach ($requiredLibs as $libraryName => $libraryVersion) {
+ if (! $libraries->has($libraryName, $libraryVersion)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Return an array containing all enabled module names as strings
+ *
+ * @return array
+ */
+ public function listEnabledModules()
+ {
+ if (count($this->enabledDirs) === 0) {
+ $this->detectEnabledModules();
+ }
+
+ return array_keys($this->enabledDirs);
+ }
+
+ /**
+ * Return an array containing all loaded module names as strings
+ *
+ * @return array
+ */
+ public function listLoadedModules()
+ {
+ return array_keys($this->loadedModules);
+ }
+
+ /**
+ * Return an array of module names from installed modules
+ *
+ * Calls detectInstalledModules() if no module discovery has been performed yet
+ *
+ * @return array
+ *
+ * @see detectInstalledModules()
+ */
+ public function listInstalledModules()
+ {
+ if (!count($this->installedBaseDirs)) {
+ $this->detectInstalledModules();
+ }
+
+ if (count($this->installedBaseDirs)) {
+ return array_keys($this->installedBaseDirs);
+ }
+
+ return array();
+ }
+
+ /**
+ * Detect installed modules from every path provided in modulePaths
+ *
+ * @param array $availableDirs Installed modules location
+ *
+ * @return $this
+ */
+ public function detectInstalledModules(array $availableDirs = null)
+ {
+ $modulePaths = $availableDirs !== null ? $availableDirs : $this->modulePaths;
+ foreach ($modulePaths as $basedir) {
+ $canonical = realpath($basedir);
+ if ($canonical === false) {
+ Logger::warning('Module path "%s" does not exist', $basedir);
+ continue;
+ }
+ if (!is_dir($canonical)) {
+ Logger::error('Module path "%s" is not a directory', $canonical);
+ continue;
+ }
+ if (!is_readable($canonical)) {
+ Logger::error('Module path "%s" is not readable', $canonical);
+ continue;
+ }
+ if (($dh = opendir($canonical)) !== false) {
+ while (($file = readdir($dh)) !== false) {
+ if ($file[0] === '.') {
+ continue;
+ }
+ if (is_dir($canonical . '/' . $file)) {
+ if (! array_key_exists($file, $this->installedBaseDirs)) {
+ $this->installedBaseDirs[$file] = $canonical . '/' . $file;
+ } else {
+ Logger::debug(
+ 'Module "%s" already exists in installation path "%s" and is ignored.',
+ $canonical . '/' . $file,
+ $this->installedBaseDirs[$file]
+ );
+ }
+ }
+ }
+ closedir($dh);
+ }
+ }
+ ksort($this->installedBaseDirs);
+ return $this;
+ }
+
+ /**
+ * Get the directories where to look for installed modules
+ *
+ * @return array
+ */
+ public function getModuleDirs()
+ {
+ return $this->modulePaths;
+ }
+}
diff --git a/library/Icinga/Application/Modules/MenuItemContainer.php b/library/Icinga/Application/Modules/MenuItemContainer.php
new file mode 100644
index 0000000..88599e6
--- /dev/null
+++ b/library/Icinga/Application/Modules/MenuItemContainer.php
@@ -0,0 +1,55 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Modules;
+
+/**
+ * Container for module menu items
+ */
+class MenuItemContainer extends NavigationItemContainer
+{
+ /**
+ * This menu item's children
+ *
+ * @var MenuItemContainer[]
+ */
+ protected $children;
+
+ /**
+ * Set this menu item's children
+ *
+ * @param MenuItemContainer[] $children
+ *
+ * @return $this
+ */
+ public function setChildren(array $children)
+ {
+ $this->children = $children;
+ return $this;
+ }
+
+ /**
+ * Return this menu item's children
+ *
+ * @return array
+ */
+ public function getChildren()
+ {
+ return $this->children ?: array();
+ }
+
+ /**
+ * Add a new sub menu
+ *
+ * @param string $name
+ * @param array $properties
+ *
+ * @return MenuItemContainer The newly added sub menu
+ */
+ public function add($name, array $properties = array())
+ {
+ $child = new MenuItemContainer($name, $properties);
+ $this->children[] = $child;
+ return $child;
+ }
+}
diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php
new file mode 100644
index 0000000..6a5afb8
--- /dev/null
+++ b/library/Icinga/Application/Modules/Module.php
@@ -0,0 +1,1451 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Modules;
+
+use Exception;
+use Icinga\Application\ApplicationBootstrap;
+use Icinga\Application\Config;
+use Icinga\Application\Hook;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Setup\SetupWizard;
+use Icinga\Util\File;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Widget;
+use ipl\I18n\GettextTranslator;
+use ipl\I18n\StaticTranslator;
+use ipl\I18n\Translation;
+use Zend_Controller_Router_Route;
+use Zend_Controller_Router_Route_Abstract;
+use Zend_Controller_Router_Route_Regex;
+
+/**
+ * Module handling
+ *
+ * Register modules and initialize it
+ */
+class Module
+{
+ use Translation {
+ translate as protected;
+ translatePlural as protected;
+ }
+
+ /**
+ * Module name
+ *
+ * @var string
+ */
+ private $name;
+
+ /**
+ * Base directory of module
+ *
+ * @var string
+ */
+ private $basedir;
+
+ /**
+ * Directory for styles
+ *
+ * @var string
+ */
+ private $cssdir;
+
+ /**
+ * Directory for Javascript
+ *
+ * @var string
+ */
+ private $jsdir;
+
+ /**
+ * Base application directory
+ *
+ * @var string
+ */
+ private $appdir;
+
+ /**
+ * Library directory
+ *
+ * @var string
+ */
+ private $libdir;
+
+ /**
+ * Config directory
+ *
+ * @var string
+ */
+ private $configdir;
+
+ /**
+ * Directory containing translations
+ *
+ * @var string
+ */
+ private $localedir;
+
+ /**
+ * Directory where controllers reside
+ *
+ * @var string
+ */
+ private $controllerdir;
+
+ /**
+ * Directory containing form implementations
+ *
+ * @var string
+ */
+ private $formdir;
+
+ /**
+ * Module bootstrapping script
+ *
+ * @var string
+ */
+ private $runScript;
+
+ /**
+ * Module configuration script
+ *
+ * @var string
+ */
+ private $configScript;
+
+ /**
+ * Module metadata filename
+ *
+ * @var string
+ */
+ private $metadataFile;
+
+ /**
+ * Module metadata (version...)
+ *
+ * @var object
+ */
+ private $metadata;
+
+ /**
+ * Whether we already tried to include the module configuration script
+ *
+ * @var bool
+ */
+ private $triedToLaunchConfigScript = false;
+
+ /**
+ * Whether the module's namespaces have been registered on our autoloader
+ *
+ * @var bool
+ */
+ protected $registeredAutoloader = false;
+
+ /**
+ * Whether this module has been registered
+ *
+ * @var bool
+ */
+ private $registered = false;
+
+ /**
+ * Provided permissions
+ *
+ * @var array
+ */
+ private $permissionList = array();
+
+ /**
+ * Provided restrictions
+ *
+ * @var array
+ */
+ private $restrictionList = array();
+
+ /**
+ * Provided config tabs
+ *
+ * @var array
+ */
+ private $configTabs = array();
+
+ /**
+ * Provided setup wizard
+ *
+ * @var string
+ */
+ private $setupWizard;
+
+ /**
+ * Icinga application
+ *
+ * @var \Icinga\Application\Web
+ */
+ private $app;
+
+ /**
+ * The CSS/LESS files this module provides
+ *
+ * @var array
+ */
+ protected $cssFiles = array();
+
+ /**
+ * The Javascript files this module provides
+ *
+ * @var array
+ */
+ protected $jsFiles = array();
+
+ /**
+ * Routes to add to the route chain
+ *
+ * @var array Array of name-route pairs
+ *
+ * @see addRoute()
+ */
+ protected $routes = array();
+
+ /**
+ * A set of menu elements
+ *
+ * @var MenuItemContainer[]
+ */
+ protected $menuItems = array();
+
+ /**
+ * A set of Pane elements
+ *
+ * @var array
+ */
+ protected $paneItems = array();
+
+ /**
+ * A set of objects representing a searchUrl configuration
+ *
+ * @var array
+ */
+ protected $searchUrls = array();
+
+ /**
+ * This module's user backends providing several authentication mechanisms
+ *
+ * @var array
+ */
+ protected $userBackends = array();
+
+ /**
+ * This module's user group backends
+ *
+ * @var array
+ */
+ protected $userGroupBackends = array();
+
+ /**
+ * This module's configurable navigation items
+ *
+ * @var array
+ */
+ protected $navigationItems = array();
+
+ /**
+ * Create a new module object
+ *
+ * @param ApplicationBootstrap $app
+ * @param string $name
+ * @param string $basedir
+ */
+ public function __construct(ApplicationBootstrap $app, $name, $basedir)
+ {
+ $this->app = $app;
+ $this->name = $name;
+ $this->basedir = $basedir;
+ $this->cssdir = $basedir . '/public/css';
+ $this->jsdir = $basedir . '/public/js';
+ $this->libdir = $basedir . '/library';
+ $this->configdir = $app->getConfigDir('modules/' . $name);
+ $this->appdir = $basedir . '/application';
+ $this->localedir = $basedir . '/application/locale';
+ $this->formdir = $basedir . '/application/forms';
+ $this->controllerdir = $basedir . '/application/controllers';
+ $this->runScript = $basedir . '/run.php';
+ $this->configScript = $basedir . '/configuration.php';
+ $this->metadataFile = $basedir . '/module.info';
+
+ $this->translationDomain = $name;
+ }
+
+ /**
+ * Provide a search URL
+ *
+ * @param string $title
+ * @param string $url
+ * @param int $priority
+ *
+ * @return $this
+ */
+ public function provideSearchUrl($title, $url, $priority = 0)
+ {
+ $this->searchUrls[] = (object) array(
+ 'title' => (string) $title,
+ 'url' => (string) $url,
+ 'priority' => (int) $priority
+ );
+
+ return $this;
+ }
+
+ /**
+ * Get this module's search urls
+ *
+ * @return array
+ */
+ public function getSearchUrls()
+ {
+ $this->launchConfigScript();
+ return $this->searchUrls;
+ }
+
+ /**
+ * Return this module's dashboard
+ *
+ * @return Navigation
+ */
+ public function getDashboard()
+ {
+ $this->launchConfigScript();
+ return $this->createDashboard($this->paneItems);
+ }
+
+ /**
+ * Create and return a new navigation for the given dashboard panes
+ *
+ * @param DashboardContainer[] $panes
+ *
+ * @return Navigation
+ */
+ public function createDashboard(array $panes)
+ {
+ $navigation = new Navigation();
+ foreach ($panes as $pane) {
+ /** @var DashboardContainer $pane */
+ $dashlets = [];
+ foreach ($pane->getDashlets() as $dashletName => $dashletConfig) {
+ $dashlets[$dashletName] = [
+ 'label' => $this->translate($dashletName),
+ 'url' => $dashletConfig['url'],
+ 'priority' => $dashletConfig['priority']
+ ];
+ }
+
+ $navigation->addItem(
+ $pane->getName(),
+ array_merge(
+ $pane->getProperties(),
+ array(
+ 'label' => $this->translate($pane->getName()),
+ 'type' => 'dashboard-pane',
+ 'children' => $dashlets
+ )
+ )
+ );
+ }
+
+ return $navigation;
+ }
+
+ /**
+ * Add or get a dashboard pane
+ *
+ * @param string $name
+ * @param array $properties
+ *
+ * @return DashboardContainer
+ */
+ protected function dashboard($name, array $properties = array())
+ {
+ if (array_key_exists($name, $this->paneItems)) {
+ $this->paneItems[$name]->setProperties($properties);
+ } else {
+ $this->paneItems[$name] = new DashboardContainer($name, $properties);
+ }
+
+ return $this->paneItems[$name];
+ }
+
+ /**
+ * Return this module's menu
+ *
+ * @return Navigation
+ */
+ public function getMenu()
+ {
+ $this->launchConfigScript();
+ return Navigation::fromArray($this->createMenu($this->menuItems));
+ }
+
+ /**
+ * Create and return an array structure for the given menu items
+ *
+ * @param MenuItemContainer[] $items
+ *
+ * @return array
+ */
+ private function createMenu(array $items)
+ {
+ $navigation = array();
+ foreach ($items as $item) {
+ /** @var MenuItemContainer $item */
+ $properties = $item->getProperties();
+ $properties['children'] = $this->createMenu($item->getChildren());
+ if (! isset($properties['label'])) {
+ $properties['label'] = $this->translate($item->getName());
+ }
+
+ $navigation[$item->getName()] = $properties;
+ }
+
+ return $navigation;
+ }
+
+ /**
+ * Add or get a menu section
+ *
+ * @param string $name
+ * @param array $properties
+ *
+ * @return MenuItemContainer
+ */
+ protected function menuSection($name, array $properties = array())
+ {
+ if (array_key_exists($name, $this->menuItems)) {
+ $this->menuItems[$name]->setProperties($properties);
+ } else {
+ $this->menuItems[$name] = new MenuItemContainer($name, $properties);
+ }
+
+ return $this->menuItems[$name];
+ }
+
+ /**
+ * Register module
+ *
+ * @return bool
+ */
+ public function register()
+ {
+ if ($this->registered) {
+ return true;
+ }
+
+ $this->registerAutoloader();
+ try {
+ $this->launchRunScript();
+ } catch (Exception $e) {
+ Logger::warning(
+ 'Launching the run script %s for module %s failed with the following exception: %s',
+ $this->runScript,
+ $this->name,
+ $e->getMessage()
+ );
+ return false;
+ }
+ $this->registerWebIntegration();
+ $this->registered = true;
+
+ return true;
+ }
+
+ /**
+ * Get whether this module has been registered
+ *
+ * @return bool
+ */
+ public function isRegistered()
+ {
+ return $this->registered;
+ }
+
+ /**
+ * Test for an enabled module by name
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public static function exists($name)
+ {
+ return Icinga::app()->getModuleManager()->hasEnabled($name);
+ }
+
+ /**
+ * Get a module by name
+ *
+ * @param string $name
+ * @param bool $autoload
+ *
+ * @return self
+ *
+ * @throws ProgrammingError When the module is not yet loaded
+ */
+ public static function get($name, $autoload = false)
+ {
+ $manager = Icinga::app()->getModuleManager();
+ if (!$manager->hasLoaded($name)) {
+ if ($autoload === true && $manager->hasEnabled($name)) {
+ $manager->loadModule($name);
+ }
+ }
+ // Throws ProgrammingError when the module is not yet loaded
+ return $manager->getModule($name);
+ }
+
+ /**
+ * Provide an additional CSS/LESS file
+ *
+ * @param string $path The path to the file, relative to self::$cssdir
+ *
+ * @return $this
+ */
+ protected function provideCssFile($path)
+ {
+ $this->cssFiles[] = $this->cssdir . DIRECTORY_SEPARATOR . $path;
+ return $this;
+ }
+
+ /**
+ * Test if module provides css
+ *
+ * @return bool
+ */
+ public function hasCss()
+ {
+ if (file_exists($this->getCssFilename())) {
+ return true;
+ }
+
+ $this->launchConfigScript();
+ return !empty($this->cssFiles);
+ }
+
+ /**
+ * Returns the complete less file name
+ *
+ * @return string
+ */
+ public function getCssFilename()
+ {
+ return $this->cssdir . '/module.less';
+ }
+
+ /**
+ * Return the CSS/LESS files this module provides
+ *
+ * @return array
+ */
+ public function getCssFiles()
+ {
+ $this->launchConfigScript();
+ $files = $this->cssFiles;
+ if (file_exists($this->getCssFilename())) {
+ $files[] = $this->getCssFilename();
+ }
+ return $files;
+ }
+
+ /**
+ * Provide an additional Javascript file
+ *
+ * @param string $path The path to the file, relative to self::$jsdir
+ *
+ * @return $this
+ */
+ protected function provideJsFile($path)
+ {
+ $this->jsFiles[] = $this->jsdir . DIRECTORY_SEPARATOR . $path;
+ return $this;
+ }
+
+ /**
+ * Test if module provides js
+ *
+ * @return bool
+ */
+ public function hasJs()
+ {
+ if (file_exists($this->getJsFilename())) {
+ return true;
+ }
+
+ $this->launchConfigScript();
+ return !empty($this->jsFiles);
+ }
+
+ /**
+ * Returns the complete js file name
+ *
+ * @return string
+ */
+ public function getJsFilename()
+ {
+ return $this->jsdir . '/module.js';
+ }
+
+ /**
+ * Return the Javascript files this module provides
+ *
+ * @return array
+ */
+ public function getJsFiles()
+ {
+ $this->launchConfigScript();
+ $files = $this->jsFiles;
+ $files[] = $this->getJsFilename();
+ return $files;
+ }
+
+ /**
+ * Get the module name
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Get the module namespace
+ *
+ * @return string
+ */
+ public function getNamespace()
+ {
+ return 'Icinga\\Module\\' . ucfirst($this->getName());
+ }
+
+ /**
+ * Get the module version
+ *
+ * @return string
+ */
+ public function getVersion()
+ {
+ return $this->metadata()->version;
+ }
+
+ /**
+ * Get the module description
+ *
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->metadata()->description;
+ }
+
+ /**
+ * Get the module title (short description)
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->metadata()->title;
+ }
+
+ /**
+ * Get the module dependencies
+ *
+ * @return array
+ * @deprecated Use method getRequiredModules() instead
+ */
+ public function getDependencies()
+ {
+ return $this->metadata()->depends;
+ }
+
+ /**
+ * Get required libraries
+ *
+ * @return array
+ */
+ public function getRequiredLibraries()
+ {
+ $requiredLibraries = $this->metadata()->libraries;
+
+ // Register module requirements for ipl and reactbundle as library requirements
+ $requiredModules = $this->metadata()->modules ?: $this->metadata()->depends;
+ if (isset($requiredModules['ipl']) && ! isset($requiredLibraries['icinga-php-library'])) {
+ $requiredLibraries['icinga-php-library'] = $requiredModules['ipl'];
+ }
+
+ if (isset($requiredModules['reactbundle']) && ! isset($requiredLibraries['icinga-php-thirdparty'])) {
+ $requiredLibraries['icinga-php-thirdparty'] = $requiredModules['reactbundle'];
+ }
+
+ return $requiredLibraries;
+ }
+
+ /**
+ * Get required modules
+ *
+ * @return array
+ */
+ public function getRequiredModules()
+ {
+ $requiredModules = $this->metadata()->modules ?: $this->metadata()->depends;
+
+ $hasIcingadb = isset($requiredModules['icingadb']);
+ if (isset($requiredModules['monitoring']) && ($this->isSupportingIcingadb() || $hasIcingadb)) {
+ $requiredMods = [];
+ $icingadbVersion = true;
+ if ($hasIcingadb) {
+ $icingadbVersion = isset($requiredModules['icingadb']) ? $requiredModules['icingadb'] : true;
+ unset($requiredModules['icingadb']);
+ }
+
+ foreach ($requiredModules as $name => $version) {
+ $requiredMods[$name] = $version;
+ if ($name === 'monitoring') {
+ $requiredMods['icingadb'] = $icingadbVersion;
+ }
+ }
+
+ $requiredModules = $requiredMods;
+ }
+
+ // Both modules are deprecated and their successors are now dependencies of web itself
+ unset($requiredModules['ipl'], $requiredModules['reactbundle']);
+
+ return $requiredModules;
+ }
+
+ /**
+ * Check whether module supports icingadb
+ *
+ * @return bool
+ */
+ protected function isSupportingIcingadb()
+ {
+ $icingadbSupportingModules = [
+ 'cube' => '1.2.0',
+ 'jira' => '1.2.0',
+ 'graphite' => '1.2.0',
+ 'director' => '1.9.0',
+ 'toplevelview' => '0.4.0',
+ 'businessprocess' => '2.4.0'
+ ];
+
+ return array_key_exists($this->getName(), $icingadbSupportingModules)
+ && version_compare($this->getVersion(), $icingadbSupportingModules[$this->getName()], '>=');
+ }
+
+ /**
+ * Fetch module metadata
+ *
+ * @return object
+ */
+ protected function metadata()
+ {
+ if ($this->metadata === null) {
+ $metadata = (object) [
+ 'name' => $this->getName(),
+ 'version' => '0.0.0',
+ 'title' => null,
+ 'description' => '',
+ 'depends' => [],
+ 'libraries' => [],
+ 'modules' => []
+ ];
+
+ if (file_exists($this->metadataFile)) {
+ $key = null;
+ $simpleRequires = false;
+ $file = new File($this->metadataFile, 'r');
+ foreach ($file as $lineno => $line) {
+ $line = rtrim($line);
+
+ if ($key === 'description') {
+ if (empty($line)) {
+ $metadata->description .= "\n";
+ continue;
+ } elseif ($line[0] === ' ') {
+ $metadata->description .= $line;
+ continue;
+ }
+ } elseif (empty($line)) {
+ continue;
+ }
+
+ if (strpos($line, ':') === false) {
+ Logger::debug(
+ "Can't process line %d in %s: Line does not specify a key:value pair"
+ . " nor is it part of the description (indented with a single space)",
+ $lineno,
+ $this->metadataFile
+ );
+
+ break;
+ }
+
+ $parts = preg_split('/:\s+/', $line, 2);
+ if (count($parts) === 1) {
+ $parts[] = '';
+ }
+
+ list($key, $val) = $parts;
+
+ $key = strtolower($key);
+ switch ($key) {
+ case 'requires':
+ if ($val) {
+ $simpleRequires = true;
+ $key = 'libraries';
+ } else {
+ break;
+ }
+
+ // Shares the syntax with `Depends`
+ case ' libraries':
+ case ' modules':
+ if ($simpleRequires && $key[0] === ' ') {
+ Logger::debug(
+ 'Can\'t process line %d in %s: Requirements already registered by a previous line',
+ $lineno,
+ $this->metadataFile
+ );
+ break;
+ }
+
+ $key = ltrim($key);
+ // Shares the syntax with `Depends`
+ case 'depends':
+ if (strpos($val, ' ') === false) {
+ $metadata->{$key}[$val] = true;
+ continue 2;
+ }
+
+ $parts = preg_split('/,\s+/', $val);
+ foreach ($parts as $part) {
+ if (preg_match('/^([\w\-\/]+)\s+\((.+)\)$/', $part, $m)) {
+ $metadata->{$key}[$m[1]] = $m[2];
+ } else {
+ $metadata->{$key}[$part] = true;
+ }
+ }
+
+ break;
+ case 'description':
+ if ($metadata->title === null) {
+ $metadata->title = $val;
+ } else {
+ $metadata->description = $val;
+ }
+ break;
+
+ default:
+ $metadata->{$key} = $val;
+ }
+ }
+ }
+
+ if ($metadata->title === null) {
+ $metadata->title = $this->getName();
+ }
+
+ if ($metadata->description === '') {
+ $metadata->description = t(
+ 'This module has no description'
+ );
+ }
+
+ $this->metadata = $metadata;
+ }
+ return $this->metadata;
+ }
+
+ /**
+ * Get the module's CSS directory
+ *
+ * @return string
+ */
+ public function getCssDir()
+ {
+ return $this->cssdir;
+ }
+
+ /**
+ * Get the module's JS directory
+ *
+ * @return string
+ */
+ public function getJsDir()
+ {
+ return $this->jsdir;
+ }
+
+ /**
+ * Get the module's controller directory
+ *
+ * @return string
+ */
+ public function getControllerDir()
+ {
+ return $this->controllerdir;
+ }
+
+ /**
+ * Get the module's base directory
+ *
+ * @return string
+ */
+ public function getBaseDir()
+ {
+ return $this->basedir;
+ }
+
+ /**
+ * Get the module's application directory
+ *
+ * @return string
+ */
+ public function getApplicationDir()
+ {
+ return $this->appdir;
+ }
+
+ /**
+ * Get the module's library directory
+ *
+ * @return string
+ */
+ public function getLibDir()
+ {
+ return $this->libdir;
+ }
+
+ /**
+ * Get the module's configuration directory
+ *
+ * @return string
+ */
+ public function getConfigDir()
+ {
+ return $this->configdir;
+ }
+
+ /**
+ * Get the module's form directory
+ *
+ * @return string
+ */
+ public function getFormDir()
+ {
+ return $this->formdir;
+ }
+
+ /**
+ * Get the module config
+ *
+ * @param string $file
+ *
+ * @return Config
+ */
+ public function getConfig($file = 'config')
+ {
+ return $this->app->getConfig()->module($this->name, $file);
+ }
+
+ /**
+ * Get provided permissions
+ *
+ * @return array
+ */
+ public function getProvidedPermissions()
+ {
+ $this->launchConfigScript();
+ return $this->permissionList;
+ }
+
+ /**
+ * Get provided restrictions
+ *
+ * @return array
+ */
+ public function getProvidedRestrictions()
+ {
+ $this->launchConfigScript();
+ return $this->restrictionList;
+ }
+
+ /**
+ * Whether the module provides the given restriction
+ *
+ * @param string $name Restriction name
+ *
+ * @return bool
+ */
+ public function providesRestriction($name)
+ {
+ $this->launchConfigScript();
+ return array_key_exists($name, $this->restrictionList);
+ }
+
+ /**
+ * Whether the module provides the given permission
+ *
+ * @param string $name Permission name
+ *
+ * @return bool
+ */
+ public function providesPermission($name)
+ {
+ $this->launchConfigScript();
+ return array_key_exists($name, $this->permissionList);
+ }
+
+ /**
+ * Get the module configuration tabs
+ *
+ * @return \Icinga\Web\Widget\Tabs
+ */
+ public function getConfigTabs()
+ {
+ $this->launchConfigScript();
+ $tabs = Widget::create('tabs');
+ /** @var \Icinga\Web\Widget\Tabs $tabs */
+ $tabs->add('info', array(
+ 'url' => 'config/module',
+ 'urlParams' => array('name' => $this->getName()),
+ 'label' => 'Module: ' . $this->getName()
+ ));
+
+ if ($this->app->getModuleManager()->hasEnabled($this->name)) {
+ foreach ($this->configTabs as $name => $config) {
+ $tabs->add($name, $config);
+ }
+ }
+
+ return $tabs;
+ }
+
+ /**
+ * Whether the module provides a setup wizard
+ *
+ * @return bool
+ */
+ public function providesSetupWizard()
+ {
+ $this->launchConfigScript();
+ if ($this->setupWizard && class_exists($this->setupWizard)) {
+ $wizard = new $this->setupWizard;
+ return $wizard instanceof SetupWizard;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the module's setup wizard
+ *
+ * @return SetupWizard
+ */
+ public function getSetupWizard()
+ {
+ return new $this->setupWizard;
+ }
+
+ /**
+ * Get the module's user backends
+ *
+ * @return array
+ */
+ public function getUserBackends()
+ {
+ $this->launchConfigScript();
+ return $this->userBackends;
+ }
+
+ /**
+ * Get the module's user group backends
+ *
+ * @return array
+ */
+ public function getUserGroupBackends()
+ {
+ $this->launchConfigScript();
+ return $this->userGroupBackends;
+ }
+
+ /**
+ * Return this module's configurable navigation items
+ *
+ * @return array
+ */
+ public function getNavigationItems()
+ {
+ $this->launchConfigScript();
+ return $this->navigationItems;
+ }
+
+ /**
+ * Provide a named permission
+ *
+ * @param string $name Unique permission name
+ * @param string $description Permission description
+ *
+ * @throws IcingaException If the permission is already provided
+ */
+ protected function providePermission($name, $description)
+ {
+ if ($this->providesPermission($name)) {
+ throw new IcingaException(
+ 'Cannot provide permission "%s" twice',
+ $name
+ );
+ }
+ $this->permissionList[$name] = (object) array(
+ 'name' => $name,
+ 'description' => $description
+ );
+ }
+
+ /**
+ * Provide a named restriction
+ *
+ * @param string $name Unique restriction name
+ * @param string $description Restriction description
+ *
+ * @throws IcingaException If the restriction is already provided
+ */
+ protected function provideRestriction($name, $description)
+ {
+ if ($this->providesRestriction($name)) {
+ throw new IcingaException(
+ 'Cannot provide restriction "%s" twice',
+ $name
+ );
+ }
+ $this->restrictionList[$name] = (object) array(
+ 'name' => $name,
+ 'description' => $description
+ );
+ }
+
+ /**
+ * Provide a module config tab
+ *
+ * @param string $name Unique tab name
+ * @param array $config Tab config
+ *
+ * @return $this
+ * @throws ProgrammingError If $config lacks the key 'url'
+ */
+ protected function provideConfigTab($name, $config = array())
+ {
+ if (! array_key_exists('url', $config)) {
+ throw new ProgrammingError('A module config tab MUST provide a "url"');
+ }
+ $config['url'] = $this->getName() . '/' . ltrim($config['url'], '/');
+ $this->configTabs[$name] = $config;
+ return $this;
+ }
+
+ /**
+ * Provide a setup wizard
+ *
+ * @param string $className The name of the class
+ *
+ * @return $this
+ */
+ protected function provideSetupWizard($className)
+ {
+ $this->setupWizard = $className;
+ return $this;
+ }
+
+ /**
+ * Provide a user backend capable of authenticating users
+ *
+ * @param string $identifier The identifier of the new backend type
+ * @param string $className The name of the class
+ *
+ * @return $this
+ */
+ protected function provideUserBackend($identifier, $className)
+ {
+ $this->userBackends[strtolower($identifier)] = $className;
+ return $this;
+ }
+
+ /**
+ * Provide a user group backend
+ *
+ * @param string $identifier The identifier of the new backend type
+ * @param string $className The name of the class
+ *
+ * @return $this
+ */
+ protected function provideUserGroupBackend($identifier, $className)
+ {
+ $this->userGroupBackends[strtolower($identifier)] = $className;
+ return $this;
+ }
+
+ /**
+ * Provide a new type of configurable navigation item with a optional label and config filename
+ *
+ * @param string $type
+ * @param string $label
+ * @param string $config
+ *
+ * @return $this
+ */
+ protected function provideNavigationItem($type, $label = null, $config = null)
+ {
+ $this->navigationItems[$type] = array(
+ 'label' => $label,
+ 'config' => $config
+ );
+
+ return $this;
+ }
+
+ /**
+ * Register module namespaces on our class loader
+ *
+ * @return $this
+ */
+ protected function registerAutoloader()
+ {
+ if ($this->registeredAutoloader) {
+ return $this;
+ }
+
+ $moduleName = ucfirst($this->getName());
+
+ $this->app->getLoader()->registerNamespace(
+ 'Icinga\\Module\\' . $moduleName,
+ $this->getLibDir() . '/'. $moduleName,
+ $this->getApplicationDir()
+ );
+
+ $this->registeredAutoloader = true;
+
+ return $this;
+ }
+
+ /**
+ * Bind text domain for i18n
+ *
+ * @return $this
+ */
+ protected function registerLocales()
+ {
+ if ($this->hasLocales() && StaticTranslator::$instance instanceof GettextTranslator) {
+ StaticTranslator::$instance->addTranslationDirectory($this->localedir, $this->name);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get whether the module has translations
+ */
+ public function hasLocales()
+ {
+ return file_exists($this->localedir) && is_dir($this->localedir);
+ }
+
+ /**
+ * List all available locales
+ *
+ * @return array Locale list
+ */
+ public function listLocales()
+ {
+ $locales = array();
+ if (! $this->hasLocales()) {
+ return $locales;
+ }
+
+ $dh = opendir($this->localedir);
+ while (false !== ($file = readdir($dh))) {
+ $filename = $this->localedir . DIRECTORY_SEPARATOR . $file;
+ if (preg_match('/^[a-z]{2}_[A-Z]{2}$/', $file) && is_dir($filename)) {
+ $locales[] = $file;
+ }
+ }
+ closedir($dh);
+ sort($locales);
+ return $locales;
+ }
+
+ /**
+ * Register web integration
+ *
+ * Add controller directory to mvc
+ *
+ * @return $this
+ */
+ protected function registerWebIntegration()
+ {
+ if (! $this->app->isWeb()) {
+ return $this;
+ }
+
+ return $this
+ ->registerLocales()
+ ->registerRoutes();
+ }
+
+ /**
+ * Add routes for static content and any route added via {@link addRoute()} to the route chain
+ *
+ * @return $this
+ */
+ protected function registerRoutes()
+ {
+ $router = $this->app->getFrontController()->getRouter();
+
+ // TODO: We should not be required to do this. Please check dispatch()
+ $this->app->getFrontController()->addControllerDirectory(
+ $this->getControllerDir(),
+ $this->getName()
+ );
+
+ /** @var \Zend_Controller_Router_Rewrite $router */
+ foreach ($this->routes as $name => $route) {
+ $router->addRoute($name, $route);
+ }
+ $router->addRoute(
+ $this->name . '_jsprovider',
+ new Zend_Controller_Router_Route(
+ 'js/' . $this->name . '/:file',
+ array(
+ 'action' => 'javascript',
+ 'controller' => 'static',
+ 'module' => 'default',
+ 'module_name' => $this->name
+ )
+ )
+ );
+ $router->addRoute(
+ $this->name . '_img',
+ new Zend_Controller_Router_Route_Regex(
+ 'img/' . $this->name . '/(.+)',
+ array(
+ 'action' => 'img',
+ 'controller' => 'static',
+ 'module' => 'default',
+ 'module_name' => $this->name
+ ),
+ array(
+ 1 => 'file'
+ )
+ )
+ );
+ return $this;
+ }
+
+ /**
+ * Run module bootstrap script
+ *
+ * @return $this
+ */
+ protected function launchRunScript()
+ {
+ return $this->includeScript($this->runScript);
+ }
+
+ /**
+ * Include a php script if it is readable
+ *
+ * @param string $file File to include
+ *
+ * @return $this
+ */
+ protected function includeScript($file)
+ {
+ if (file_exists($file) && is_readable($file)) {
+ include $file;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Run module config script
+ *
+ * @return $this
+ */
+ protected function launchConfigScript()
+ {
+ if ($this->triedToLaunchConfigScript) {
+ return $this;
+ }
+ $this->triedToLaunchConfigScript = true;
+ $this->registerAutoloader();
+ return $this->includeScript($this->configScript);
+ }
+
+ protected function slashesToNamespace($class)
+ {
+ $list = explode('/', $class);
+ foreach ($list as &$part) {
+ $part = ucfirst($part);
+ }
+
+ return implode('\\', $list);
+ }
+
+ /**
+ * Provide a hook implementation
+ *
+ * @param string $name Name of the hook for which to provide an implementation
+ * @param string $implementation Fully qualified name of the class providing the hook implementation.
+ * Defaults to the module's ProvidedHook namespace plus the hook's name for the
+ * class name
+ * @param bool $alwaysRun To run the hook always (e.g. without permission check)
+ *
+ * @return $this
+ */
+ protected function provideHook($name, $implementation = null, $alwaysRun = false)
+ {
+ if ($implementation === null) {
+ $implementation = $name;
+ }
+
+ if (strpos($implementation, '\\') === false) {
+ $class = $this->getNamespace()
+ . '\\ProvidedHook\\'
+ . $this->slashesToNamespace($implementation);
+ } else {
+ $class = $implementation;
+ }
+
+ Hook::register($name, $class, $class, $alwaysRun);
+ return $this;
+ }
+
+ /**
+ * Add a route which will be added to the route chain
+ *
+ * @param string $name Name of the route
+ * @param Zend_Controller_Router_Route_Abstract $route Instance of the route
+ *
+ * @return $this
+ * @see registerRoutes()
+ */
+ protected function addRoute($name, Zend_Controller_Router_Route_Abstract $route)
+ {
+ $this->routes[$name] = $route;
+ return $this;
+ }
+}
diff --git a/library/Icinga/Application/Modules/NavigationItemContainer.php b/library/Icinga/Application/Modules/NavigationItemContainer.php
new file mode 100644
index 0000000..c906ccb
--- /dev/null
+++ b/library/Icinga/Application/Modules/NavigationItemContainer.php
@@ -0,0 +1,117 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application\Modules;
+
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Container for module navigation items
+ */
+abstract class NavigationItemContainer
+{
+ /**
+ * This navigation item's name
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * This navigation item's properties
+ *
+ * @var array
+ */
+ protected $properties;
+
+ /**
+ * Create a new NavigationItemContainer
+ *
+ * @param string $name
+ * @param array $properties
+ */
+ public function __construct($name, array $properties = array())
+ {
+ $this->name = $name;
+ $this->properties = $properties;
+ }
+
+ /**
+ * Set this menu item's name
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * Return this menu item's name
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set this menu item's properties
+ *
+ * @param array $properties
+ *
+ * @return $this
+ */
+ public function setProperties(array $properties)
+ {
+ $this->properties = $properties;
+ return $this;
+ }
+
+ /**
+ * Return this menu item's properties
+ *
+ * @return array
+ */
+ public function getProperties()
+ {
+ return $this->properties ?: array();
+ }
+
+ /**
+ * Allow dynamic setters and getters for properties
+ *
+ * @param string $name
+ * @param array $arguments
+ *
+ * @return mixed
+ *
+ * @throws ProgrammingError In case the called method is not supported
+ */
+ public function __call($name, $arguments)
+ {
+ if (method_exists($this, $name)) {
+ return call_user_func(array($this, $name), $this, $arguments);
+ }
+
+ $type = substr($name, 0, 3);
+ if ($type !== 'set' && $type !== 'get') {
+ throw new ProgrammingError(
+ 'Dynamic method %s is not supported. Only getters (get*) and setters (set*) are.',
+ $name
+ );
+ }
+
+ $propertyName = strtolower(join('_', preg_split('~(?=[A-Z])~', lcfirst(substr($name, 3)))));
+ if ($type === 'set') {
+ $this->properties[$propertyName] = $arguments[0];
+ return $this;
+ } else { // $type === 'get'
+ return array_key_exists($propertyName, $this->properties) ? $this->properties[$propertyName] : null;
+ }
+ }
+}
diff --git a/library/Icinga/Application/Platform.php b/library/Icinga/Application/Platform.php
new file mode 100644
index 0000000..185a69e
--- /dev/null
+++ b/library/Icinga/Application/Platform.php
@@ -0,0 +1,435 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application;
+
+/**
+ * Platform tests for icingaweb
+ */
+class Platform
+{
+ /**
+ * Domain name
+ *
+ * @var string
+ */
+ protected static $domain;
+
+ /**
+ * Host name
+ *
+ * @var string
+ */
+ protected static $hostname;
+
+ /**
+ * Fully qualified domain name
+ *
+ * @var string
+ */
+ protected static $fqdn;
+
+ /**
+ * Return the operating system's name
+ *
+ * @return string
+ */
+ public static function getOperatingSystemName()
+ {
+ return php_uname('s');
+ }
+
+ /**
+ * Test of windows
+ *
+ * @return bool
+ */
+ public static function isWindows()
+ {
+ return strtoupper(substr(self::getOperatingSystemName(), 0, 3)) === 'WIN';
+ }
+
+ /**
+ * Test of linux
+ *
+ * @return bool
+ */
+ public static function isLinux()
+ {
+ return strtoupper(substr(self::getOperatingSystemName(), 0, 5)) === 'LINUX';
+ }
+
+ /**
+ * Return the Linux distribution's name
+ * or 'linux' if the name could not be found out
+ * or false if the OS isn't Linux or an error occurred
+ *
+ * @param int $reliable
+ * 3: Only parse /etc/os-release (or /usr/lib/os-release).
+ * For the paranoid ones.
+ * 2: If that (3) doesn't help, check /etc/*-release, too.
+ * If something is unclear, return 'linux'.
+ * 1: Almost equal to mode 2. The possible return values also include:
+ * 'redhat' -- unclear whether RHEL/Fedora/...
+ * 'suse' -- unclear whether SLES/openSUSE/...
+ * 0: If even that (1) doesn't help, check /proc/version, too.
+ * This may not work (as expected) on LXC containers!
+ * (No reliability at all!)
+ *
+ * @return string|bool
+ */
+ public static function getLinuxDistro($reliable = 2)
+ {
+ if (! self::isLinux()) {
+ return false;
+ }
+
+ foreach (array('/etc/os-release', '/usr/lib/os-release') as $osReleaseFile) {
+ if (false === ($osRelease = @file(
+ $osReleaseFile,
+ FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES
+ ))) {
+ continue;
+ }
+
+ foreach ($osRelease as $osInfo) {
+ if (false === ($res = @preg_match('/(?<!.)[ \t]*#/ms', $osInfo))) {
+ return false;
+ }
+ if ($res === 1) {
+ continue;
+ }
+
+ $matches = array();
+ if (false === ($res = @preg_match(
+ '/(?<!.)[ \t]*ID[ \t]*=[ \t]*(\'|"|)(.*?)(?:\1)[ \t]*(?!.)/msi',
+ $osInfo,
+ $matches
+ ))) {
+ return false;
+ }
+ if (! ($res === 0 || $matches[2] === '' || $matches[2] === 'linux')) {
+ return $matches[2];
+ }
+ }
+ }
+
+ if ($reliable > 2) {
+ return 'linux';
+ }
+
+ foreach (array(
+ 'fedora' => '/etc/fedora-release',
+ 'centos' => '/etc/centos-release'
+ ) as $distro => $releaseFile) {
+ if (! (false === (
+ $release = @file_get_contents($releaseFile)
+ ) || false === strpos(strtolower($release), $distro))) {
+ return $distro;
+ }
+ }
+
+ if (false !== ($release = @file_get_contents('/etc/redhat-release'))) {
+ $release = strtolower($release);
+ if (false !== strpos($release, 'red hat enterprise linux')) {
+ return 'rhel';
+ }
+ foreach (array('fedora', 'centos') as $distro) {
+ if (false !== strpos($release, $distro)) {
+ return $distro;
+ }
+ }
+ return $reliable < 2 ? 'redhat' : 'linux';
+ }
+
+ if (false !== ($release = @file_get_contents('/etc/SuSE-release'))) {
+ $release = strtolower($release);
+ foreach (array(
+ 'opensuse' => 'opensuse',
+ 'sles' => 'suse linux enterprise server',
+ 'sled' => 'suse linux enterprise desktop'
+ ) as $distro => $name) {
+ if (false !== strpos($release, $name)) {
+ return $distro;
+ }
+ }
+ return $reliable < 2 ? 'suse' : 'linux';
+ }
+
+ if ($reliable < 1) {
+ if (false === ($procVersion = @file_get_contents('/proc/version'))) {
+ return false;
+ }
+ $procVersion = strtolower($procVersion);
+ foreach (array(
+ 'redhat' => 'red hat',
+ 'suse' => 'suse linux',
+ 'ubuntu' => 'ubuntu',
+ 'debian' => 'debian'
+ ) as $distro => $name) {
+ if (false !== strpos($procVersion, $name)) {
+ return $distro;
+ }
+ }
+ }
+
+ return 'linux';
+ }
+
+ /**
+ * Test of CLI environment
+ *
+ * @return bool
+ */
+ public static function isCli()
+ {
+ if (PHP_SAPI == 'cli') {
+ return true;
+ } elseif ((PHP_SAPI == 'cgi' || PHP_SAPI == 'cgi-fcgi')
+ && empty($_SERVER['SERVER_NAME'])) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get the hostname
+ *
+ * @return string
+ */
+ public static function getHostname()
+ {
+ if (self::$hostname === null) {
+ self::discoverHostname();
+ }
+ return self::$hostname;
+ }
+
+ /**
+ * Get the domain name
+ *
+ * @return string
+ */
+ public static function getDomain()
+ {
+ if (self::$domain === null) {
+ self::discoverHostname();
+ }
+ return self::$domain;
+ }
+
+ /**
+ * Get the fully qualified domain name
+ *
+ * @return string
+ */
+ public static function getFqdn()
+ {
+ if (self::$fqdn === null) {
+ self::discoverHostname();
+ }
+ return self::$fqdn;
+ }
+
+ /**
+ * Initialize domain and host strings
+ */
+ protected static function discoverHostname()
+ {
+ self::$hostname = gethostname();
+ self::$fqdn = gethostbyaddr(gethostbyname(self::$hostname));
+
+ if (substr(self::$fqdn, 0, strlen(self::$hostname)) === self::$hostname) {
+ self::$domain = substr(self::$fqdn, strlen(self::$hostname) + 1);
+ } else {
+ $parts = preg_split('~\.~', self::$hostname, 2);
+ self::$domain = array_shift($parts);
+ }
+ }
+
+ /**
+ * Return the version of PHP
+ *
+ * @return string
+ */
+ public static function getPhpVersion()
+ {
+ return phpversion();
+ }
+
+ /**
+ * Return the username PHP is running as
+ *
+ * @return ?string
+ */
+ public static function getPhpUser()
+ {
+ if (static::isWindows()) {
+ return get_current_user(); // http://php.net/manual/en/function.get-current-user.php#75059
+ }
+
+ if (function_exists('posix_geteuid')) {
+ $userInfo = posix_getpwuid(posix_geteuid());
+ return $userInfo['name'];
+ }
+ }
+
+ /**
+ * Test for php extension
+ *
+ * @param string $extensionName E.g. mysql, ldap
+ *
+ * @return bool
+ */
+ public static function extensionLoaded($extensionName)
+ {
+ return extension_loaded($extensionName);
+ }
+
+ /**
+ * Return the value for the given PHP configuration option
+ *
+ * @param string $option The option name for which to return the value
+ *
+ * @return string|false
+ */
+ public static function getPhpConfig($option)
+ {
+ return ini_get($option);
+ }
+
+ /**
+ * Return whether the given class exists
+ *
+ * @param string $name The name of the class to check
+ *
+ * @return bool
+ */
+ public static function classExists($name)
+ {
+ if (@class_exists($name)) {
+ return true;
+ }
+
+ if (strpos($name, '_') !== false) {
+ // Assume it's a Zend-Framework class
+ return (@include str_replace('_', '/', $name) . '.php') !== false;
+ }
+
+ return false;
+ }
+
+ /**
+ * Return whether it's possible to connect to a LDAP server
+ *
+ * Checks whether the ldap extension is loaded
+ *
+ * @return bool
+ */
+ public static function hasLdapSupport()
+ {
+ return static::extensionLoaded('ldap');
+ }
+
+ /**
+ * Return whether it's possible to connect to any of the supported database servers
+ *
+ * @return bool
+ */
+ public static function hasDatabaseSupport()
+ {
+ return static::hasMssqlSupport() || static::hasMysqlSupport() || static::hasOciSupport()
+ || static::hasOracleSupport() || static::hasPostgresqlSupport();
+ }
+
+ /**
+ * Return whether it's possible to connect to a MSSQL database
+ *
+ * Checks whether the mssql/dblib pdo or sqlsrv extension has
+ * been loaded and Zend framework adapter for MSSQL is available
+ *
+ * @return bool
+ */
+ public static function hasMssqlSupport()
+ {
+ if ((static::extensionLoaded('mssql') || static::extensionLoaded('pdo_dblib'))
+ && static::classExists('Zend_Db_Adapter_Pdo_Mssql')
+ ) {
+ return true;
+ }
+
+ return static::extensionLoaded('sqlsrv') && static::classExists('Zend_Db_Adapter_Sqlsrv');
+ }
+
+ /**
+ * Return whether it's possible to connect to a MySQL database
+ *
+ * Checks whether the mysql pdo extension has been loaded and the Zend framework adapter for MySQL is available
+ *
+ * @return bool
+ */
+ public static function hasMysqlSupport()
+ {
+ return static::extensionLoaded('pdo_mysql') && static::classExists('Zend_Db_Adapter_Pdo_Mysql');
+ }
+
+ /**
+ * Return whether it's possible to connect to a IBM DB2 database
+ *
+ * Checks whether the ibm pdo extension has been loaded and the Zend framework adapter for IBM is available
+ *
+ * @return bool
+ */
+ public static function hasIbmSupport()
+ {
+ return static::extensionLoaded('pdo_ibm') && static::classExists('Zend_Db_Adapter_Pdo_Ibm');
+ }
+
+ /**
+ * Return whether it's possible to connect to a Oracle database using OCI8
+ *
+ * Checks whether the OCI8 extension has been loaded and the Zend framework adapter for Oracle is available
+ *
+ * @return bool
+ */
+ public static function hasOciSupport()
+ {
+ return static::extensionLoaded('oci8') && static::classExists('Zend_Db_Adapter_Oracle');
+ }
+
+ /**
+ * Return whether it's possible to connect to a Oracle database using PDO_OCI
+ *
+ * Checks whether the OCI PDO extension has been loaded and the Zend framework adapter for Oci is available
+ *
+ * @return bool
+ */
+ public static function hasOracleSupport()
+ {
+ return static::extensionLoaded('pdo_oci') && static::classExists('Zend_Db_Adapter_Pdo_Oci');
+ }
+
+ /**
+ * Return whether it's possible to connect to a PostgreSQL database
+ *
+ * Checks whether the pgsql pdo extension has been loaded and the Zend framework adapter for PostgreSQL is available
+ *
+ * @return bool
+ */
+ public static function hasPostgresqlSupport()
+ {
+ return static::extensionLoaded('pdo_pgsql') && static::classExists('Zend_Db_Adapter_Pdo_Pgsql');
+ }
+
+ /**
+ * Return whether it's possible to connect to a SQLite database
+ *
+ * Checks whether the sqlite pdo extension has been loaded and the Zend framework adapter for SQLite is available
+ *
+ * @return bool
+ */
+ public static function hasSqliteSupport()
+ {
+ return static::extensionLoaded('pdo_sqlite') && static::classExists('Zend_Db_Adapter_Pdo_Sqlite');
+ }
+}
diff --git a/library/Icinga/Application/ProvidedHook/DbMigration.php b/library/Icinga/Application/ProvidedHook/DbMigration.php
new file mode 100644
index 0000000..899dbf6
--- /dev/null
+++ b/library/Icinga/Application/ProvidedHook/DbMigration.php
@@ -0,0 +1,83 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Application\ProvidedHook;
+
+use Icinga\Application\Hook\DbMigrationHook;
+use Icinga\Common\Database;
+use Icinga\Model\Schema;
+use ipl\Orm\Query;
+use ipl\Sql\Connection;
+
+class DbMigration extends DbMigrationHook
+{
+ use Database {
+ getDb as private getWebDb;
+ }
+
+ public function getDb(): Connection
+ {
+ return $this->getWebDb();
+ }
+
+ public function getName(): string
+ {
+ return $this->translate('Icinga Web');
+ }
+
+ public function providedDescriptions(): array
+ {
+ return [];
+ }
+
+ public function getVersion(): string
+ {
+ if ($this->version === null) {
+ $conn = $this->getDb();
+ $schemaQuery = $this->getSchemaQuery()
+ ->orderBy('id', SORT_DESC)
+ ->limit(2);
+
+ if (static::getColumnType($conn, $schemaQuery->getModel()->getTableName(), 'success')) {
+ /** @var Schema $schema */
+ foreach ($schemaQuery as $schema) {
+ if ($schema->success) {
+ $this->version = $schema->version;
+
+ break;
+ }
+ }
+
+ if (! $this->version) {
+ $this->version = '2.12.0';
+ }
+ } elseif (static::tableExists($conn, $schemaQuery->getModel()->getTableName())
+ || static::getColumnCollation($conn, 'icingaweb_user_preference', 'username') === 'utf8mb4_unicode_ci'
+ ) {
+ $this->version = '2.11.0';
+ } elseif (static::tableExists($conn, 'icingaweb_rememberme')) {
+ $randomIvType = static::getColumnType($conn, 'icingaweb_rememberme', 'random_iv');
+ if ($randomIvType === 'varchar(32)') {
+ $this->version = '2.9.1';
+ } else {
+ $this->version = '2.9.0';
+ }
+ } else {
+ $usernameType = static::getColumnType($conn, 'icingaweb_group_membership', 'username');
+ if ($usernameType === 'varchar(254)') {
+ $this->version = '2.5.0';
+ } else {
+ $this->version = '2.0.0';
+ }
+ }
+ }
+
+ return $this->version;
+ }
+
+ protected function getSchemaQuery(): Query
+ {
+ return Schema::on($this->getDb());
+ }
+}
diff --git a/library/Icinga/Application/StaticWeb.php b/library/Icinga/Application/StaticWeb.php
new file mode 100644
index 0000000..5c64dcb
--- /dev/null
+++ b/library/Icinga/Application/StaticWeb.php
@@ -0,0 +1,21 @@
+<?php
+/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Application;
+
+require_once dirname(__FILE__) . '/EmbeddedWeb.php';
+
+class StaticWeb extends EmbeddedWeb
+{
+ protected function bootstrap()
+ {
+ return $this
+ ->setupErrorHandling()
+ ->loadLibraries()
+ ->loadConfig()
+ ->setupLogging()
+ ->setupLogger()
+ ->setupRequest()
+ ->setupResponse();
+ }
+}
diff --git a/library/Icinga/Application/Test.php b/library/Icinga/Application/Test.php
new file mode 100644
index 0000000..74321ea
--- /dev/null
+++ b/library/Icinga/Application/Test.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Icinga\Application;
+
+use Icinga\Web\Request;
+use Icinga\Web\Response;
+
+require_once __DIR__ . '/Cli.php';
+
+class Test extends Cli
+{
+ protected $isCli = false;
+
+ /** @var Request */
+ private $request;
+
+ /** @var Response */
+ private $response;
+
+ public function setRequest(Request $request): void
+ {
+ $this->request = $request;
+ }
+
+ public function getRequest(): Request
+ {
+ assert(isset($this->request), 'BaseTestCase should have set the request');
+
+ return $this->request;
+ }
+
+ public function setResponse(Response $response): void
+ {
+ $this->response = $response;
+ }
+
+ public function getResponse(): Response
+ {
+ assert(isset($this->request), 'BaseTestCase should have set the response');
+
+ return $this->response;
+ }
+
+ public function getFrontController()
+ {
+ return $this; // Callers are expected to only call getRequest or getResponse, hence the app should suffice
+ }
+
+ protected function bootstrap()
+ {
+ $this->assertRunningOnCli();
+ $this->setupLogging()
+ ->setupErrorHandling()
+ ->loadLibraries()
+ ->setupComposerAutoload()
+ ->loadConfig()
+ ->setupModuleAutoloaders()
+ ->setupTimezone()
+ ->prepareInternationalization()
+ ->setupInternationalization()
+ ->parseBasicParams()
+ ->setupLogger()
+ ->setupModuleManager()
+ ->setupUserBackendFactory()
+ ->setupFakeAuthentication();
+ }
+
+ public function setupAutoloader()
+ {
+ parent::setupAutoloader();
+
+ if (($icingaLibDir = getenv('ICINGAWEB_ICINGA_LIB')) !== false) {
+ $this->getLoader()->registerNamespace('Icinga', $icingaLibDir);
+ }
+
+ // Conflicts with `Tests\Icinga\Module\...\Lib`. But it seems it's not needed anyway...
+ //$this->getLoader()->registerNamespace('Tests', $this->getBaseDir('test/php/library'));
+ $this->getLoader()->registerNamespace('Tests\\Icinga\\Lib', $this->getBaseDir('test/php/Lib'));
+
+ return $this;
+ }
+
+ protected function detectTimezone()
+ {
+ return 'UTC';
+ }
+
+ private function setupModuleAutoloaders(): self
+ {
+ $modulePaths = getenv('ICINGAWEB_MODULE_DIRS');
+
+ if ($modulePaths) {
+ $modulePaths = preg_split('/:/', $modulePaths, -1, PREG_SPLIT_NO_EMPTY);
+ }
+
+ if (! $modulePaths) {
+ $modulePaths = [];
+ foreach ($this->getAvailableModulePaths() as $path) {
+ $candidates = array_flip(scandir($path));
+ unset($candidates['.'], $candidates['..']);
+ foreach ($candidates as $candidate => $_) {
+ $modulePaths[] = "$path/$candidate";
+ }
+ }
+ }
+
+ foreach ($modulePaths as $path) {
+ $module = basename($path);
+
+ $moduleNamespace = 'Icinga\\Module\\' . ucfirst($module);
+ $moduleLibraryPath = "$path/library/" . ucfirst($module);
+
+ if (is_dir($moduleLibraryPath)) {
+ $this->getLoader()->registerNamespace($moduleNamespace, $moduleLibraryPath, "$path/application");
+ }
+
+ $moduleTestPath = "$path/test/php/Lib";
+ if (is_dir($moduleTestPath)) {
+ $this->getLoader()->registerNamespace('Tests\\' . $moduleNamespace . '\\Lib', $moduleTestPath);
+ }
+
+ $composerAutoloader = "$path/vendor/autoload.php";
+ if (file_exists($composerAutoloader)) {
+ require_once $composerAutoloader;
+ }
+ }
+
+ return $this;
+ }
+
+ private function setupComposerAutoload(): self
+ {
+ $vendorAutoload = $this->getBaseDir('/vendor/autoload.php');
+ if (file_exists($vendorAutoload)) {
+ require_once $vendorAutoload;
+ }
+
+ return $this;
+ }
+}
diff --git a/library/Icinga/Application/Version.php b/library/Icinga/Application/Version.php
new file mode 100644
index 0000000..be804f1
--- /dev/null
+++ b/library/Icinga/Application/Version.php
@@ -0,0 +1,65 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application;
+
+/**
+ * Retrieve the version of Icinga Web 2
+ */
+class Version
+{
+ const VERSION = '2.12.1';
+
+ /**
+ * Get the version of this instance of Icinga Web 2
+ *
+ * @return array
+ */
+ public static function get()
+ {
+ $version = array('appVersion' => self::VERSION);
+ preg_match('/2.(\d+)\./', self::VERSION, $matches);
+ $version['docVersion'] = isset($matches[1]) ? '2.' . $matches[1] : null;
+
+ if (false !== ($appVersion = @file_get_contents(Icinga::app()->getApplicationDir('VERSION')))) {
+ $matches = array();
+ if (@preg_match('/^(?P<gitCommitID>\w+) (?P<gitCommitDate>\S+)/', $appVersion, $matches)) {
+ return array_merge($version, $matches);
+ }
+ }
+
+ $gitCommitId = static::getGitHead(Icinga::app()->getBaseDir());
+ if ($gitCommitId !== false) {
+ $version['gitCommitID'] = $gitCommitId;
+ }
+
+ return $version;
+ }
+
+ /**
+ * Get the current commit of the Git repository in the given path
+ *
+ * @param string $repo Path to the Git repository
+ * @param bool $bare Whether the Git repository is bare
+ *
+ * @return string|bool False if not available
+ */
+ public static function getGitHead($repo, $bare = false)
+ {
+ if (! $bare) {
+ $repo .= '/.git';
+ }
+
+ $head = @file_get_contents($repo . '/HEAD');
+
+ if ($head !== false) {
+ if (preg_match('/^ref: (.+)/', $head, $matches)) {
+ return @file_get_contents($repo . '/' . $matches[1]);
+ }
+
+ return $head;
+ }
+
+ return false;
+ }
+}
diff --git a/library/Icinga/Application/Web.php b/library/Icinga/Application/Web.php
new file mode 100644
index 0000000..934af07
--- /dev/null
+++ b/library/Icinga/Application/Web.php
@@ -0,0 +1,509 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application;
+
+require_once __DIR__ . '/EmbeddedWeb.php';
+
+use ErrorException;
+use ipl\I18n\GettextTranslator;
+use ipl\I18n\Locale;
+use ipl\I18n\StaticTranslator;
+use Zend_Controller_Action_HelperBroker;
+use Zend_Controller_Front;
+use Zend_Controller_Router_Route;
+use Zend_Layout;
+use Zend_Paginator;
+use Zend_View_Helper_PaginationControl;
+use Icinga\Authentication\Auth;
+use Icinga\User;
+use Icinga\Util\DirectoryIterator;
+use Icinga\Util\TimezoneDetect;
+use Icinga\Web\Controller\Dispatcher;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Notification;
+use Icinga\Web\Session;
+use Icinga\Web\Session\Session as BaseSession;
+use Icinga\Web\StyleSheet;
+use Icinga\Web\View;
+
+/**
+ * Use this if you want to make use of Icinga functionality in other web projects
+ *
+ * Usage example:
+ * <code>
+ * use Icinga\Application\Web;
+ * Web::start();
+ * </code>
+ */
+class Web extends EmbeddedWeb
+{
+ /**
+ * View object
+ *
+ * @var View
+ */
+ private $viewRenderer;
+
+ /**
+ * Zend front controller instance
+ *
+ * @var Zend_Controller_Front
+ */
+ private $frontController;
+
+ /**
+ * Session object
+ *
+ * @var BaseSession
+ */
+ private $session;
+
+ /**
+ * User object
+ *
+ * @var User
+ */
+ private $user;
+
+ /** @var array */
+ protected $accessibleMenuItems;
+
+ /**
+ * Identify web bootstrap
+ *
+ * @var bool
+ */
+ protected $isWeb = true;
+
+ /**
+ * Initialize all together
+ *
+ * @return $this
+ */
+ protected function bootstrap()
+ {
+ return $this
+ ->setupLogging()
+ ->setupErrorHandling()
+ ->loadLibraries()
+ ->loadConfig()
+ ->setupLogger()
+ ->setupRequest()
+ ->setupSession()
+ ->setupNotifications()
+ ->setupResponse()
+ ->setupZendMvc()
+ ->prepareInternationalization()
+ ->setupModuleManager()
+ ->loadSetupModuleIfNecessary()
+ ->loadEnabledModules()
+ ->setupRoute()
+ ->setupPagination()
+ ->setupUserBackendFactory()
+ ->setupUser()
+ ->setupTimezone()
+ ->setupInternationalization()
+ ->setupFatalErrorHandling()
+ ->registerApplicationHooks();
+ }
+
+ /**
+ * Get themes provided by Web 2 and all enabled modules
+ *
+ * @return string[] Array of theme names as keys and values
+ */
+ public function getThemes()
+ {
+ $themes = array(StyleSheet::DEFAULT_THEME);
+ $applicationThemePath = $this->getBaseDir('public/css/themes');
+ if (DirectoryIterator::isReadable($applicationThemePath)) {
+ foreach (new DirectoryIterator($applicationThemePath, 'less') as $name => $theme) {
+ $themes[] = substr($name, 0, -5);
+ }
+ }
+ $mm = $this->getModuleManager();
+ foreach ($mm->listEnabledModules() as $moduleName) {
+ $moduleThemePath = $mm->getModule($moduleName)->getCssDir() . '/themes';
+ if (! DirectoryIterator::isReadable($moduleThemePath)) {
+ continue;
+ }
+ foreach (new DirectoryIterator($moduleThemePath, 'less') as $name => $theme) {
+ $themes[] = $moduleName . '/' . substr($name, 0, -5);
+ }
+ }
+ return array_combine($themes, $themes);
+ }
+
+ /**
+ * Prepare routing
+ *
+ * @return $this
+ */
+ private function setupRoute()
+ {
+ $this->frontController->getRouter()->addRoute(
+ 'module_javascript',
+ new Zend_Controller_Router_Route(
+ 'js/components/:module_name/:file',
+ array(
+ 'controller' => 'static',
+ 'action' => 'javascript'
+ )
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * Getter for frontController
+ *
+ * @return Zend_Controller_Front
+ */
+ public function getFrontController()
+ {
+ return $this->frontController;
+ }
+
+ /**
+ * Getter for view
+ *
+ * @return View
+ */
+ public function getViewRenderer()
+ {
+ return $this->viewRenderer;
+ }
+
+ private function hasAccessToSharedNavigationItem(&$config, Config $navConfig)
+ {
+ // TODO: Provide a more sophisticated solution
+
+ if (isset($config['owner']) && strtolower($config['owner']) === strtolower($this->user->getUsername())) {
+ unset($config['owner']);
+ unset($config['users']);
+ unset($config['groups']);
+ return true;
+ }
+
+ if (isset($config['parent']) && $navConfig->hasSection($config['parent'])) {
+ unset($config['owner']);
+ if (isset($this->accessibleMenuItems[$config['parent']])) {
+ return $this->accessibleMenuItems[$config['parent']];
+ }
+
+ $parentConfig = $navConfig->getSection($config['parent']);
+ $this->accessibleMenuItems[$config['parent']] = $this->hasAccessToSharedNavigationItem(
+ $parentConfig,
+ $navConfig
+ );
+ return $this->accessibleMenuItems[$config['parent']];
+ }
+
+ if (isset($config['users'])) {
+ $users = array_map('trim', explode(',', strtolower($config['users'])));
+ if (in_array('*', $users, true) || in_array(strtolower($this->user->getUsername()), $users, true)) {
+ unset($config['owner']);
+ unset($config['users']);
+ unset($config['groups']);
+ return true;
+ }
+ }
+
+ if (isset($config['groups'])) {
+ $groups = array_map('trim', explode(',', strtolower($config['groups'])));
+ if (in_array('*', $groups, true)) {
+ unset($config['owner']);
+ unset($config['users']);
+ unset($config['groups']);
+ return true;
+ }
+
+ $userGroups = array_map('strtolower', $this->user->getGroups());
+ $matches = array_intersect($userGroups, $groups);
+ if (! empty($matches)) {
+ unset($config['owner']);
+ unset($config['users']);
+ unset($config['groups']);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Load and return the shared navigation of the given type
+ *
+ * @param string $type
+ *
+ * @return Navigation
+ */
+ public function getSharedNavigation($type)
+ {
+ $config = Config::navigation($type === 'dashboard-pane' ? 'dashlet' : $type);
+
+ if ($type === 'dashboard-pane') {
+ $panes = array();
+ foreach ($config as $dashletName => $dashletConfig) {
+ if ($this->hasAccessToSharedNavigationItem($dashletConfig, $config)) {
+ // TODO: Throw ConfigurationError if pane or url is missing
+ $panes[$dashletConfig->pane][$dashletName] = $dashletConfig->url;
+ }
+ }
+
+ $navigation = new Navigation();
+ foreach ($panes as $paneName => $dashlets) {
+ $navigation->addItem(
+ $paneName,
+ array(
+ 'type' => 'dashboard-pane',
+ 'dashlets' => $dashlets
+ )
+ );
+ }
+ } else {
+ $items = array();
+ foreach ($config as $name => $typeConfig) {
+ if (isset($this->accessibleMenuItems[$name])) {
+ if ($this->accessibleMenuItems[$name]) {
+ $items[$name] = $typeConfig;
+ }
+ } else {
+ if ($this->hasAccessToSharedNavigationItem($typeConfig, $config)) {
+ $this->accessibleMenuItems[$name] = true;
+ $items[$name] = $typeConfig;
+ } else {
+ $this->accessibleMenuItems[$name] = false;
+ }
+ }
+ }
+
+ $navigation = Navigation::fromConfig($items);
+ }
+
+ return $navigation;
+ }
+
+ /**
+ * Dispatch public interface
+ */
+ public function dispatch()
+ {
+ $this->frontController->dispatch($this->getRequest(), $this->getResponse());
+ }
+
+ /**
+ * Prepare Zend MVC Base
+ *
+ * @return $this
+ */
+ private function setupZendMvc()
+ {
+ Zend_Layout::startMvc(
+ array(
+ 'layout' => 'layout',
+ 'layoutPath' => $this->getApplicationDir('/layouts/scripts')
+ )
+ );
+
+ $this->setupFrontController();
+ $this->setupViewRenderer();
+ return $this;
+ }
+
+ /**
+ * Create user object
+ *
+ * @return $this
+ */
+ private function setupUser()
+ {
+ $auth = Auth::getInstance();
+ if (! $this->request->isXmlHttpRequest() && $this->request->isApiRequest() && ! $auth->isAuthenticated()) {
+ $auth->authHttp();
+ }
+ if ($auth->isAuthenticated()) {
+ $user = $auth->getUser();
+ $this->getRequest()->setUser($user);
+ $this->user = $user;
+
+ if ($user->can('user/application/stacktraces')) {
+ $displayExceptions = $this->user->getPreferences()->getValue(
+ 'icingaweb',
+ 'show_stacktraces'
+ );
+
+ if ($displayExceptions !== null) {
+ $this->frontController->setParams(
+ array(
+ 'displayExceptions' => $displayExceptions
+ )
+ );
+ }
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Initialize a session provider
+ *
+ * @return $this
+ */
+ private function setupSession()
+ {
+ $this->session = Session::create();
+ return $this;
+ }
+
+ /**
+ * Initialize notifications to remove them immediately from session
+ *
+ * @return $this
+ */
+ private function setupNotifications()
+ {
+ Notification::getInstance();
+ return $this;
+ }
+
+ /**
+ * Instantiate front controller
+ *
+ * @return $this
+ */
+ private function setupFrontController()
+ {
+ $this->frontController = Zend_Controller_Front::getInstance();
+ $this->frontController->setDispatcher(new Dispatcher());
+ $this->frontController->setRequest($this->getRequest());
+ $this->frontController->setControllerDirectory($this->getApplicationDir('/controllers'));
+
+ $displayExceptions = $this->config->get('global', 'show_stacktraces', true);
+
+ $this->frontController->setParams(
+ array(
+ 'displayExceptions' => $displayExceptions
+ )
+ );
+ return $this;
+ }
+
+ /**
+ * Register helper paths and views for renderer
+ *
+ * @return $this
+ */
+ private function setupViewRenderer()
+ {
+ $view = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');
+ /** @var \Zend_Controller_Action_Helper_ViewRenderer $view */
+ $view->setView(new View());
+ $view->view->addHelperPath($this->getApplicationDir('/views/helpers'));
+ $view->view->setEncoding('UTF-8');
+ $view->view->headTitle()->prepend($this->config->get('global', 'project', 'Icinga'));
+ $view->view->headTitle()->setSeparator(' :: ');
+ $this->viewRenderer = $view;
+ return $this;
+ }
+
+ /**
+ * Configure pagination settings
+ *
+ * @return $this
+ */
+ private function setupPagination()
+ {
+ // TODO: document what we need for whatever reason?!
+ Zend_Paginator::addScrollingStylePrefixPath(
+ 'Icinga_Web_Paginator_ScrollingStyle_',
+ $this->getLibraryDir('Icinga/Web/Paginator/ScrollingStyle')
+ );
+
+ Zend_Paginator::addScrollingStylePrefixPath(
+ 'Icinga_Web_Paginator_ScrollingStyle',
+ 'Icinga/Web/Paginator/ScrollingStyle'
+ );
+
+ Zend_Paginator::setDefaultScrollingStyle('SlidingWithBorder');
+ Zend_View_Helper_PaginationControl::setDefaultViewPartial(
+ array('mixedPagination.phtml', 'default')
+ );
+ return $this;
+ }
+
+ /**
+ * Fatal error handling configuration
+ *
+ * @return $this
+ */
+ protected function setupFatalErrorHandling()
+ {
+ register_shutdown_function(function () {
+ $error = error_get_last();
+
+ if ($error !== null && $error['type'] === E_ERROR) {
+ $frontController = Icinga::app()->getFrontController();
+ $response = $frontController->getResponse();
+
+ $response->setException(new ErrorException(
+ $error['message'],
+ 0,
+ $error['type'],
+ $error['file'],
+ $error['line']
+ ));
+
+ // Clean PHP's fatal error stack trace and replace it with ours
+ ob_end_clean();
+ $frontController->dispatch($frontController->getRequest(), $response);
+ }
+ });
+
+ return $this;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see ApplicationBootstrap::detectTimezone() For the method documentation.
+ */
+ protected function detectTimezone()
+ {
+ $auth = Auth::getInstance();
+ if (! $auth->isAuthenticated()
+ || ($timezone = $auth->getUser()->getPreferences()->getValue('icingaweb', 'timezone')) === null
+ ) {
+ $detect = new TimezoneDetect();
+ $timezone = $detect->getTimezoneName();
+ }
+ return $timezone;
+ }
+
+ /**
+ * Setup internationalization using gettext
+ *
+ * Uses the preferred user language or the browser suggested language or our default.
+ *
+ * @return string Detected locale code
+ */
+ protected function detectLocale()
+ {
+ $auth = Auth::getInstance();
+ if ($auth->isAuthenticated()
+ && ($locale = $auth->getUser()->getPreferences()->getValue('icingaweb', 'language')) !== null
+ ) {
+ return $locale;
+ }
+
+ /** @var GettextTranslator $translator */
+ $translator = StaticTranslator::$instance;
+
+ if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
+ return (new Locale())->getPreferred($_SERVER['HTTP_ACCEPT_LANGUAGE'], $translator->listLocales());
+ }
+
+ return $translator->getDefaultLocale();
+ }
+}
diff --git a/library/Icinga/Application/functions.php b/library/Icinga/Application/functions.php
new file mode 100644
index 0000000..12736fb
--- /dev/null
+++ b/library/Icinga/Application/functions.php
@@ -0,0 +1,110 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+use ipl\Stdlib\Contract\Translator;
+use ipl\I18n\StaticTranslator;
+
+/**
+ * No-op translate
+ *
+ * Supposed to be used for marking a string as available for translation without actually translating it immediately.
+ * The returned string is the one given in the input. This does only work with the standard gettext macros t() and mt().
+ *
+ * @param string $messageId
+ *
+ * @return string
+ */
+function N_(string $messageId): string
+{
+ return $messageId;
+}
+
+// Workaround for test issues, this is required unless our tests are able to
+// accomplish "real" bootstrapping
+if (function_exists('t')) {
+ return;
+}
+
+if (extension_loaded('gettext')) {
+
+ /**
+ * @see Translator::translate() For the function documentation.
+ */
+ function t(string $messageId, ?string $context = null): string
+ {
+ return StaticTranslator::$instance->translate($messageId, $context);
+ }
+
+ /**
+ * @see Translator::translateInDomain() For the function documentation.
+ */
+ function mt(string $domain, string $messageId, ?string $context = null): string
+ {
+ return StaticTranslator::$instance->translateInDomain($domain, $messageId, $context);
+ }
+
+ /**
+ * @see Translator::translatePlural() For the function documentation.
+ */
+ function tp(string $messageId, string $messageId2, ?int $number, ?string $context = null): string
+ {
+ return StaticTranslator::$instance->translatePlural($messageId, $messageId2, $number ?? 0, $context);
+ }
+
+ /**
+ * @see Translator::translatePluralInDomain() For the function documentation.
+ */
+ function mtp(string $domain, string $messageId, string $messageId2, ?int $number, ?string $context = null): string
+ {
+ return StaticTranslator::$instance->translatePluralInDomain(
+ $domain,
+ $messageId,
+ $messageId2,
+ $number ?? 0,
+ $context
+ );
+ }
+
+} else {
+
+ /**
+ * @see Translator::translate() For the function documentation.
+ */
+ function t(string $messageId, ?string $context = null): string
+ {
+ return $messageId;
+ }
+
+ /**
+ * @see Translator::translate() For the function documentation.
+ */
+ function mt(string $domain, string $messageId, ?string $context = null): string
+ {
+ return $messageId;
+ }
+
+ /**
+ * @see Translator::translatePlural() For the function documentation.
+ */
+ function tp(string $messageId, string $messageId2, ?int $number, ?string $context = null): string
+ {
+ if ((int) $number !== 1) {
+ return $messageId2;
+ }
+
+ return $messageId;
+ }
+
+ /**
+ * @see Translator::translatePlural() For the function documentation.
+ */
+ function mtp(string $domain, string $messageId, string $messageId2, ?int $number, ?string $context = null): string
+ {
+ if ((int) $number !== 1) {
+ return $messageId2;
+ }
+
+ return $messageId;
+ }
+
+}
diff --git a/library/Icinga/Application/webrouter.php b/library/Icinga/Application/webrouter.php
new file mode 100644
index 0000000..d9ab30b
--- /dev/null
+++ b/library/Icinga/Application/webrouter.php
@@ -0,0 +1,106 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Application;
+
+use Icinga\Chart\Inline\PieChart;
+use Icinga\Web\Controller\StaticController;
+use Icinga\Web\JavaScript;
+use Icinga\Web\StyleSheet;
+
+error_reporting(E_ALL | E_STRICT);
+
+if (isset($_SERVER['REQUEST_URI'])) {
+ $ruri = $_SERVER['REQUEST_URI'];
+} else {
+ return false;
+}
+
+// Workaround, PHPs internal Webserver seems to mess up SCRIPT_FILENAME
+// as it prefixes it's absolute path with DOCUMENT_ROOT
+if (preg_match('/^PHP .* Development Server/', $_SERVER['SERVER_SOFTWARE'])) {
+ $script = basename($_SERVER['SCRIPT_FILENAME']);
+ $_SERVER['PHP_SELF'] = $_SERVER['SCRIPT_NAME'] = '/' . $script;
+ $_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT']
+ . DIRECTORY_SEPARATOR
+ . $script;
+}
+
+$baseDir = $_SERVER['DOCUMENT_ROOT'];
+$baseDir = dirname($_SERVER['SCRIPT_FILENAME']);
+
+// Fix aliases
+$remove = str_replace('\\', '/', dirname($_SERVER['PHP_SELF']));
+if (substr($ruri, 0, strlen($remove)) !== $remove) {
+ return false;
+}
+$ruri = ltrim(substr($ruri, strlen($remove)), '/');
+
+if (strpos($ruri, '?') === false) {
+ $params = '';
+ $path = $ruri;
+} else {
+ list($path, $params) = preg_split('/\?/', $ruri, 2);
+}
+
+$special = array(
+ 'css/icinga.css',
+ 'css/icinga.min.css',
+ 'js/icinga.dev.js',
+ 'js/icinga.min.js'
+);
+
+if (in_array($path, $special)) {
+ include_once __DIR__ . '/EmbeddedWeb.php';
+ EmbeddedWeb::start();
+
+ switch ($path) {
+ case 'css/icinga.css':
+ Stylesheet::send();
+ exit;
+ case 'css/icinga.min.css':
+ Stylesheet::send(true);
+ exit;
+
+ case 'js/icinga.dev.js':
+ JavaScript::send();
+ exit;
+
+ case 'js/icinga.min.js':
+ JavaScript::sendMinified();
+ break;
+
+ default:
+ return false;
+ }
+} elseif ($path === 'svg/chart.php') {
+ if (!array_key_exists('data', $_GET)) {
+ return false;
+ }
+ include __DIR__ . '/EmbeddedWeb.php';
+ EmbeddedWeb::start();
+ header('Content-Type: image/svg+xml');
+ $pie = new PieChart();
+ $pie->initFromRequest();
+ $pie->toSvg();
+} elseif ($path === 'png/chart.php') {
+ if (!array_key_exists('data', $_GET)) {
+ return false;
+ }
+ include __DIR__ . '/EmbeddedWeb.php';
+ EmbeddedWeb::start();
+ header('Content-Type: image/png');
+ $pie = new PieChart();
+ $pie->initFromRequest();
+ $pie->toPng();
+} elseif (substr($path, 0, 4) === 'lib/') {
+ include_once __DIR__ . '/StaticWeb.php';
+ $app = StaticWeb::start();
+ (new StaticController())->handle($app->getRequest());
+ $app->getResponse()->sendResponse();
+} elseif (file_exists($baseDir . '/' . $path) && is_file($baseDir . '/' . $path)) {
+ return false;
+} else {
+ include __DIR__ . '/Web.php';
+ Web::start()->dispatch();
+}
diff --git a/library/Icinga/Authentication/AdmissionLoader.php b/library/Icinga/Authentication/AdmissionLoader.php
new file mode 100644
index 0000000..0c3fd3f
--- /dev/null
+++ b/library/Icinga/Authentication/AdmissionLoader.php
@@ -0,0 +1,249 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication;
+
+use Generator;
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\NotReadableError;
+use Icinga\Data\ConfigObject;
+use Icinga\User;
+use Icinga\Util\StringHelper;
+
+/**
+ * Retrieve restrictions and permissions for users
+ */
+class AdmissionLoader
+{
+ const LEGACY_PERMISSIONS = [
+ 'admin' => 'application/announcements',
+ 'application/stacktraces' => 'user/application/stacktraces',
+ 'application/share/navigation' => 'user/share/navigation',
+ // Migrating config/application/* would include config/modules, so that's skipped
+ //'config/application/*' => 'config/*',
+ 'config/application/general' => 'config/general',
+ 'config/application/resources' => 'config/resources',
+ 'config/application/navigation' => 'config/navigation',
+ 'config/application/userbackend' => 'config/access-control/users',
+ 'config/application/usergroupbackend' => 'config/access-control/groups',
+ 'config/authentication/*' => 'config/access-control/*',
+ 'config/authentication/users/*' => 'config/access-control/users',
+ 'config/authentication/users/show' => 'config/access-control/users',
+ 'config/authentication/users/add' => 'config/access-control/users',
+ 'config/authentication/users/edit' => 'config/access-control/users',
+ 'config/authentication/users/remove' => 'config/access-control/users',
+ 'config/authentication/groups/*' => 'config/access-control/groups',
+ 'config/authentication/groups/show' => 'config/access-control/groups',
+ 'config/authentication/groups/edit' => 'config/access-control/groups',
+ 'config/authentication/groups/add' => 'config/access-control/groups',
+ 'config/authentication/groups/remove' => 'config/access-control/groups',
+ 'config/authentication/roles/*' => 'config/access-control/roles',
+ 'config/authentication/roles/show' => 'config/access-control/roles',
+ 'config/authentication/roles/add' => 'config/access-control/roles',
+ 'config/authentication/roles/edit' => 'config/access-control/roles',
+ 'config/authentication/roles/remove' => 'config/access-control/roles'
+ ];
+
+ /** @var Role[] */
+ protected $roles;
+
+ /** @var ConfigObject */
+ protected $roleConfig;
+
+ public function __construct()
+ {
+ try {
+ $this->roleConfig = Config::app('roles');
+ } catch (NotReadableError $e) {
+ Logger::error('Can\'t access roles configuration. An exception was thrown:', $e);
+ }
+ }
+
+ /**
+ * Whether the user or groups are a member of the role
+ *
+ * @param string $username
+ * @param array $userGroups
+ * @param ConfigObject $section
+ *
+ * @return bool
+ */
+ protected function match($username, $userGroups, ConfigObject $section)
+ {
+ $username = strtolower($username);
+ if (! empty($section->users)) {
+ $users = array_map('strtolower', StringHelper::trimSplit($section->users));
+ if (in_array('*', $users)) {
+ return true;
+ }
+
+ if (in_array($username, $users)) {
+ return true;
+ }
+ }
+
+ if (! empty($section->groups)) {
+ $groups = array_map('strtolower', StringHelper::trimSplit($section->groups));
+ foreach ($userGroups as $userGroup) {
+ if (in_array(strtolower($userGroup), $groups)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Process role configuration and yield resulting roles
+ *
+ * This will also resolve any parent-child relationships.
+ *
+ * @param string $name
+ * @param ConfigObject $section
+ *
+ * @return Generator
+ * @throws ConfigurationError
+ */
+ protected function loadRole($name, ConfigObject $section)
+ {
+ if (! isset($this->roles[$name])) {
+ $permissions = $section->permissions ? StringHelper::trimSplit($section->permissions) : [];
+ $refusals = $section->refusals ? StringHelper::trimSplit($section->refusals) : [];
+
+ list($permissions, $newRefusals) = self::migrateLegacyPermissions($permissions);
+ if (! empty($newRefusals)) {
+ array_push($refusals, ...$newRefusals);
+ }
+
+ $restrictions = $section->toArray();
+ unset($restrictions['users'], $restrictions['groups']);
+ unset($restrictions['parent'], $restrictions['unrestricted']);
+ unset($restrictions['refusals'], $restrictions['permissions']);
+
+ $role = new Role();
+ $this->roles[$name] = $role
+ ->setName($name)
+ ->setRefusals($refusals)
+ ->setPermissions($permissions)
+ ->setRestrictions($restrictions)
+ ->setIsUnrestricted($section->get('unrestricted', false));
+
+ if (isset($section->parent)) {
+ $parentName = $section->parent;
+ if (! $this->roleConfig->hasSection($parentName)) {
+ Logger::error(
+ 'Failed to parse authentication configuration: Missing parent role "%s" (required by "%s")',
+ $parentName,
+ $name
+ );
+ throw new ConfigurationError(
+ t('Unable to parse authentication configuration. Check the log for more details.')
+ );
+ }
+
+ foreach ($this->loadRole($parentName, $this->roleConfig->getSection($parentName)) as $parent) {
+ if ($parent->getName() === $parentName) {
+ $role->setParent($parent);
+ $parent->addChild($role);
+
+ // Only yield main role once fully assembled
+ yield $role;
+ }
+
+ yield $parent;
+ }
+ } else {
+ yield $role;
+ }
+ } else {
+ yield $this->roles[$name];
+ }
+ }
+
+ /**
+ * Apply permissions, restrictions and roles to the given user
+ *
+ * @param User $user
+ */
+ public function applyRoles(User $user)
+ {
+ if ($this->roleConfig === null) {
+ return;
+ }
+
+ $username = $user->getUsername();
+ $userGroups = $user->getGroups();
+
+ $roles = [];
+ $permissions = [];
+ $restrictions = [];
+ $assignedRoles = [];
+ $isUnrestricted = false;
+ foreach ($this->roleConfig as $roleName => $roleConfig) {
+ $assigned = $this->match($username, $userGroups, $roleConfig);
+ if ($assigned) {
+ $assignedRoles[] = $roleName;
+ }
+
+ if (! isset($roles[$roleName]) && $assigned) {
+ foreach ($this->loadRole($roleName, $roleConfig) as $role) {
+ /** @var Role $role */
+ if (isset($roles[$role->getName()])) {
+ continue;
+ }
+
+ $roles[$role->getName()] = $role;
+
+ $permissions = array_merge(
+ $permissions,
+ array_diff($role->getPermissions(), $permissions)
+ );
+
+ $roleRestrictions = $role->getRestrictions();
+ foreach ($roleRestrictions as $name => & $restriction) {
+ $restriction = str_replace(
+ '$user.local_name$',
+ $user->getLocalUsername(),
+ $restriction
+ );
+ $restrictions[$name][] = $restriction;
+ }
+
+ $role->setRestrictions($roleRestrictions);
+
+ if (! $isUnrestricted) {
+ $isUnrestricted = $role->isUnrestricted();
+ }
+ }
+ }
+ }
+
+ $user->setAdditional('assigned_roles', $assignedRoles);
+
+ $user->setIsUnrestricted($isUnrestricted);
+ $user->setRestrictions($isUnrestricted ? [] : $restrictions);
+ $user->setPermissions($permissions);
+ $user->setRoles(array_values($roles));
+ }
+
+ public static function migrateLegacyPermissions(array $permissions)
+ {
+ $migratedGrants = [];
+ $refusals = [];
+
+ foreach ($permissions as $permission) {
+ if (array_key_exists($permission, self::LEGACY_PERMISSIONS)) {
+ $migratedGrants[] = self::LEGACY_PERMISSIONS[$permission];
+ } elseif ($permission === 'no-user/password-change') {
+ $refusals[] = 'user/password-change';
+ } else {
+ $migratedGrants[] = $permission;
+ }
+ }
+
+ return [$migratedGrants, $refusals];
+ }
+}
diff --git a/library/Icinga/Authentication/Auth.php b/library/Icinga/Authentication/Auth.php
new file mode 100644
index 0000000..f358eac
--- /dev/null
+++ b/library/Icinga/Authentication/Auth.php
@@ -0,0 +1,453 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Application\Hook\AuditHook;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Authentication\User\ExternalBackend;
+use Icinga\Authentication\UserGroup\UserGroupBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotReadableError;
+use Icinga\User;
+use Icinga\User\Preferences;
+use Icinga\User\Preferences\PreferencesStore;
+use Icinga\Web\Session;
+use Icinga\Web\StyleSheet;
+
+class Auth
+{
+ /**
+ * Singleton instance
+ *
+ * @var self
+ */
+ private static $instance;
+
+ /**
+ * Request
+ *
+ * @var \Icinga\Web\Request
+ */
+ protected $request;
+
+ /**
+ * Response
+ *
+ * @var \Icinga\Web\Response
+ */
+ protected $response;
+
+ /**
+ * Authenticated user
+ *
+ * @var User|null
+ */
+ private $user;
+
+
+ /**
+ * @see getInstance()
+ */
+ private function __construct()
+ {
+ }
+
+ /**
+ * Get the authentication manager
+ *
+ * @return self
+ */
+ public static function getInstance()
+ {
+ if (self::$instance === null) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Get the auth chain
+ *
+ * @return AuthChain
+ */
+ public function getAuthChain()
+ {
+ return new AuthChain();
+ }
+
+ /**
+ * Get whether the user is authenticated
+ *
+ * @return bool
+ */
+ public function isAuthenticated()
+ {
+ if ($this->user !== null) {
+ return true;
+ }
+ $this->authenticateFromSession();
+ if ($this->user === null && ! $this->authExternal()) {
+ return false;
+ }
+ return true;
+ }
+
+ public function setAuthenticated(User $user, $persist = true)
+ {
+ $this->setupUser($user);
+
+ // Reload CSS if the theme changed
+ $themingConfig = Icinga::app()->getConfig()->getSection('themes');
+ $userTheme = $user->getPreferences()->getValue('icingaweb', 'theme');
+ if (! (bool) $themingConfig->get('disabled', false) && $userTheme !== null) {
+ $defaultTheme = $themingConfig->get('default', StyleSheet::DEFAULT_THEME);
+ if ($userTheme !== $defaultTheme) {
+ $this->getResponse()->setReloadCss(true);
+ }
+ }
+
+ // Also reload CSS if the theme mode changed
+ $themeMode = $user->getPreferences()->getValue('icingaweb', 'theme_mode');
+ if ($themeMode && $themeMode !== StyleSheet::DEFAULT_MODE) {
+ $this->getResponse()->setReloadCss(true);
+ }
+
+ // Reload entire layout if the locale changed
+ if (($locale = $user->getPreferences()->getValue('icingaweb', 'language')) !== null) {
+ if (setlocale(LC_ALL, 0) !== $locale && $this->getRequest()->isXmlHttpRequest()) {
+ $this->getResponse()->setHeader('X-Icinga-Redirect-Http', 'yes');
+ }
+ }
+
+ $this->user = $user;
+ if ($persist) {
+ $this->persistCurrentUser();
+ }
+
+ AuditHook::logActivity('login', 'User logged in');
+ }
+
+ /**
+ * Getter for groups belonged to authenticated user
+ *
+ * @return array
+ * @see User::getGroups
+ */
+ public function getGroups()
+ {
+ return $this->user->getGroups();
+ }
+
+ /**
+ * Get the request
+ *
+ * @return \Icinga\Web\Request
+ */
+ public function getRequest()
+ {
+ if ($this->request === null) {
+ $this->request = Icinga::app()->getRequest();
+ }
+ return $this->request;
+ }
+
+ /**
+ * Get the response
+ *
+ * @return \Icinga\Web\Response
+ */
+ public function getResponse()
+ {
+ if ($this->response === null) {
+ $this->response = Icinga::app()->getResponse();
+ }
+ return $this->response;
+ }
+
+ /**
+ * Get applied restrictions matching a given restriction name
+ *
+ * Returns a list of applied restrictions, empty if no user is
+ * authenticated
+ *
+ * @param string $restriction Restriction name
+ * @return array
+ */
+ public function getRestrictions($restriction)
+ {
+ if (! $this->isAuthenticated()) {
+ return array();
+ }
+ return $this->user->getRestrictions($restriction);
+ }
+
+ /**
+ * Returns the current user or null if no user is authenticated
+ *
+ * @return User|null
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * Set the authenticated user
+ *
+ * Note that this method just sets the authenticated user and thus bypasses our default authentication process in
+ * {@link setAuthenticated()}.
+ *
+ * @param User $user
+ *
+ * @return $this
+ */
+ public function setUser(User $user)
+ {
+ $this->user = $user;
+
+ return $this;
+ }
+
+ /**
+ * Try to authenticate the user with the current session
+ *
+ * Authentication for externally-authenticated users will be revoked if the username changed or external
+ * authentication is no longer in effect
+ */
+ public function authenticateFromSession()
+ {
+ $this->user = Session::getSession()->get('user');
+ if ($this->user !== null && $this->user->isExternalUser()) {
+ list($originUsername, $field) = $this->user->getExternalUserInformation();
+ $username = ExternalBackend::getRemoteUser($field);
+ if ($username === null || $username !== $originUsername) {
+ $this->removeAuthorization();
+ }
+ }
+ }
+
+ /**
+ * Attempt to authenticate a user from external user backends
+ *
+ * @return bool
+ */
+ protected function authExternal()
+ {
+ $user = new User('');
+ foreach ($this->getAuthChain() as $userBackend) {
+ if ($userBackend instanceof ExternalBackend) {
+ if ($userBackend->authenticate($user)) {
+ if (! $user->hasDomain()) {
+ $user->setDomain(Config::app()->get('authentication', 'default_domain'));
+ }
+ $this->setAuthenticated($user);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Attempt to authenticate a user using HTTP authentication on API requests only
+ *
+ * Supports only the Basic HTTP authentication scheme. XHR will be ignored.
+ *
+ * @return bool
+ */
+ public function authHttp()
+ {
+ $request = $this->getRequest();
+ $header = $request->getHeader('Authorization');
+ if (empty($header)) {
+ return false;
+ }
+ list($scheme) = explode(' ', $header, 2);
+ if ($scheme !== 'Basic') {
+ return false;
+ }
+ $authorization = substr($header, strlen('Basic '));
+ $credentials = base64_decode($authorization);
+ $credentials = array_filter(explode(':', $credentials, 2));
+ if (count($credentials) !== 2) {
+ // Deny empty username and/or password
+ return false;
+ }
+ $user = new User($credentials[0]);
+ if (! $user->hasDomain()) {
+ $user->setDomain(Config::app()->get('authentication', 'default_domain'));
+ }
+ $password = $credentials[1];
+ if ($this->getAuthChain()->setSkipExternalBackends(true)->authenticate($user, $password)) {
+ $this->setAuthenticated($user, false);
+ $user->setIsHttpUser(true);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Challenge client immediately for HTTP authentication
+ *
+ * Sends the response w/ the 401 Unauthorized status code and WWW-Authenticate header.
+ */
+ public function challengeHttp()
+ {
+ $response = $this->getResponse();
+ $response->setHttpResponseCode(401);
+ $response->setHeader('WWW-Authenticate', 'Basic realm="Icinga Web 2"');
+ $response->sendHeaders();
+ exit();
+ }
+
+ /**
+ * Whether an authenticated user has a given permission
+ *
+ * @param string $permission Permission name
+ *
+ * @return bool True if the user owns the given permission, false if not or if not authenticated
+ */
+ public function hasPermission($permission)
+ {
+ if (! $this->isAuthenticated()) {
+ return false;
+ }
+ return $this->user->can($permission);
+ }
+
+ /**
+ * Writes the current user to the session
+ */
+ public function persistCurrentUser()
+ {
+ // @TODO(el): https://dev.icinga.com/issues/10646
+ $params = session_get_cookie_params();
+ setcookie(
+ 'icingaweb2-session',
+ time(),
+ 0,
+ $params['path'],
+ $params['domain'],
+ $params['secure'],
+ $params['httponly']
+ );
+ Session::getSession()->set('user', $this->user)->refreshId();
+ }
+
+ /**
+ * Purges the current authorization information and session
+ */
+ public function removeAuthorization()
+ {
+ AuditHook::logActivity('logout', 'User logged out');
+ $this->user = null;
+ Session::getSession()->purge();
+ }
+
+ /**
+ * Setup the given user
+ *
+ * This loads preferences, groups and roles.
+ *
+ * @param User $user
+ *
+ * @return void
+ */
+ public function setupUser(User $user)
+ {
+ // Load the user's preferences
+
+ try {
+ $config = Config::app();
+ } catch (NotReadableError $e) {
+ Logger::error(
+ new IcingaException(
+ 'Cannot load preferences for user "%s". An exception was thrown: %s',
+ $user->getUsername(),
+ $e
+ )
+ );
+ $config = new Config();
+ }
+
+ $preferencesConfig = new ConfigObject([
+ 'resource' => $config->get('global', 'config_resource')
+ ]);
+
+ try {
+ $preferencesStore = PreferencesStore::create($preferencesConfig, $user);
+ $preferences = new Preferences($preferencesStore->load());
+ } catch (Exception $e) {
+ Logger::error(
+ new IcingaException(
+ 'Cannot load preferences for user "%s". An exception was thrown: %s',
+ $user->getUsername(),
+ $e
+ )
+ );
+ $preferences = new Preferences();
+ }
+
+ $user->setPreferences($preferences);
+
+ // Load the user's groups
+ $groups = $user->getGroups();
+ $userBackendName = $user->getAdditional('backend_name');
+ foreach (Config::app('groups') as $name => $config) {
+ $groupsUserBackend = $config->user_backend;
+ if ($groupsUserBackend
+ && $groupsUserBackend !== 'none'
+ && $userBackendName !== null
+ && $groupsUserBackend !== $userBackendName
+ ) {
+ // Do not ask for Group membership if a specific User Backend
+ // has been assigned to that Group Backend, and the user has
+ // been authenticated by another User Backend
+ continue;
+ }
+
+ try {
+ $groupBackend = UserGroupBackend::create($name, $config);
+ $groupsFromBackend = $groupBackend->getMemberships($user);
+ } catch (Exception $e) {
+ Logger::error(
+ 'Can\'t get group memberships for user \'%s\' from backend \'%s\'. An exception was thrown: %s',
+ $user->getUsername(),
+ $name,
+ $e
+ );
+ continue;
+ }
+
+ if (empty($groupsFromBackend)) {
+ Logger::debug(
+ 'No groups found in backend "%s" which the user "%s" is a member of.',
+ $name,
+ $user->getUsername()
+ );
+ continue;
+ }
+
+ $groupsFromBackend = array_values($groupsFromBackend);
+ Logger::debug(
+ 'Groups found in backend "%s" for user "%s": %s',
+ $name,
+ $user->getUsername(),
+ join(', ', $groupsFromBackend)
+ );
+ $groups = array_merge($groups, array_combine($groupsFromBackend, $groupsFromBackend));
+ }
+
+ $user->setGroups($groups);
+
+ // Load the user's roles
+ $admissionLoader = new AdmissionLoader();
+ $admissionLoader->applyRoles($user);
+ }
+}
diff --git a/library/Icinga/Authentication/AuthChain.php b/library/Icinga/Authentication/AuthChain.php
new file mode 100644
index 0000000..39468e3
--- /dev/null
+++ b/library/Icinga/Authentication/AuthChain.php
@@ -0,0 +1,269 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication;
+
+use Icinga\Application\Hook\AuditHook;
+use Iterator;
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Authentication\User\ExternalBackend;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Authentication\User\UserBackendInterface;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\AuthenticationException;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\NotReadableError;
+use Icinga\User;
+
+/**
+ * Iterate user backends created from config
+ */
+class AuthChain implements Authenticatable, Iterator
+{
+ /**
+ * Authentication config file
+ *
+ * @var string
+ */
+ const AUTHENTICATION_CONFIG = 'authentication';
+
+ /**
+ * Error code if the authentication configuration was not readable
+ *
+ * @var int
+ */
+ const EPERM = 1;
+
+ /**
+ * Error code if the authentication configuration is empty
+ */
+ const EEMPTY = 2;
+
+ /**
+ * Error code if all authentication methods failed
+ *
+ * @var int
+ */
+ const EFAIL = 3;
+
+ /**
+ * Error code if not all authentication methods were available
+ *
+ * @var int
+ */
+ const ENOTALL = 4;
+
+ /**
+ * User backends configuration
+ *
+ * @var Config
+ */
+ protected $config;
+
+ /**
+ * The consecutive user backend while looping
+ *
+ * @var UserBackendInterface
+ */
+ protected $currentBackend;
+
+ /**
+ * Last error code
+ *
+ * @var int|null
+ */
+ protected $error;
+
+ /**
+ * Whether external user backends should be skipped on iteration
+ *
+ * @var bool
+ */
+ protected $skipExternalBackends = false;
+
+ /**
+ * Create a new authentication chain from config
+ *
+ * @param Config $config User backends configuration
+ */
+ public function __construct(Config $config = null)
+ {
+ if ($config === null) {
+ try {
+ $this->config = Config::app(static::AUTHENTICATION_CONFIG);
+ } catch (NotReadableError $e) {
+ $this->config = new Config();
+ $this->error = static::EPERM;
+ }
+ } else {
+ $this->config = $config;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function authenticate(User $user, $password)
+ {
+ $this->error = null;
+ $backendsTried = 0;
+ $backendsWithError = 0;
+ foreach ($this as $backend) {
+ ++$backendsTried;
+ try {
+ $authenticated = $backend->authenticate($user, $password);
+ } catch (AuthenticationException $e) {
+ Logger::error($e);
+ ++$backendsWithError;
+ continue;
+ }
+ if ($authenticated) {
+ $user->setAdditional('backend_name', $backend->getName());
+ $user->setAdditional('backend_type', $this->config->current()->get('backend'));
+ return true;
+ }
+ }
+
+ if ($backendsTried === 0) {
+ $this->error = static::EEMPTY;
+ } elseif ($backendsTried === $backendsWithError) {
+ $this->error = static::EFAIL;
+ } elseif ($backendsWithError) {
+ $this->error = static::ENOTALL;
+ } else {
+ AuditHook::logActivity('login-failed', 'User failed to authenticate', null, $user->getUsername());
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the last error code
+ *
+ * @return int|null
+ */
+ public function getError()
+ {
+ return $this->error;
+ }
+
+ /**
+ * Whether authentication had errors
+ *
+ * @return bool
+ */
+ public function hasError()
+ {
+ return $this->error !== null;
+ }
+
+ /**
+ * Get whether to skip external user backends on iteration
+ *
+ * @return bool
+ */
+ public function getSkipExternalBackends()
+ {
+ return $this->skipExternalBackends;
+ }
+
+ /**
+ * Set whether to skip external user backends on iteration
+ *
+ * @param bool $skipExternalBackends
+ *
+ * @return $this
+ */
+ public function setSkipExternalBackends($skipExternalBackends = true)
+ {
+ $this->skipExternalBackends = (bool) $skipExternalBackends;
+ return $this;
+ }
+
+ /**
+ * Rewind the chain
+ *
+ * @return void
+ */
+ public function rewind(): void
+ {
+ $this->currentBackend = null;
+ $this->config->rewind();
+ }
+
+ /**
+ * Get the current user backend
+ *
+ * @return UserBackendInterface
+ */
+ public function current(): UserBackendInterface
+ {
+ return $this->currentBackend;
+ }
+
+ /**
+ * Get the key of the current user backend config
+ *
+ * @return string
+ */
+ public function key(): string
+ {
+ return $this->config->key();
+ }
+
+ /**
+ * Move forward to the next user backend config
+ *
+ * @return void
+ */
+ public function next(): void
+ {
+ $this->config->next();
+ }
+
+ /**
+ * Check whether the current user backend is valid, i.e. it's enabled, not an external user backend and whether its
+ * config is valid
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ if (! $this->config->valid()) {
+ // Stop when there are no more backends to check
+ return false;
+ }
+
+ $backendConfig = $this->config->current();
+ if ((bool) $backendConfig->get('disabled', false)) {
+ $this->next();
+ return $this->valid();
+ }
+
+ $name = $this->key();
+ try {
+ $backend = UserBackend::create($name, $backendConfig);
+ } catch (ConfigurationError $e) {
+ Logger::error(
+ new ConfigurationError(
+ 'Can\'t create authentication backend "%s". An exception was thrown:',
+ $name,
+ $e
+ )
+ );
+ $this->next();
+ return $this->valid();
+ }
+
+ if ($this->getSkipExternalBackends()
+ && $backend instanceof ExternalBackend
+ ) {
+ $this->next();
+ return $this->valid();
+ }
+
+ $this->currentBackend = $backend;
+ return true;
+ }
+}
diff --git a/library/Icinga/Authentication/Authenticatable.php b/library/Icinga/Authentication/Authenticatable.php
new file mode 100644
index 0000000..c10d6d3
--- /dev/null
+++ b/library/Icinga/Authentication/Authenticatable.php
@@ -0,0 +1,21 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication;
+
+use Icinga\User;
+
+interface Authenticatable
+{
+ /**
+ * Authenticate a user
+ *
+ * @param User $user
+ * @param string $password
+ *
+ * @return bool
+ *
+ * @throws \Icinga\Exception\AuthenticationException If authentication errors
+ */
+ public function authenticate(User $user, $password);
+}
diff --git a/library/Icinga/Authentication/Role.php b/library/Icinga/Authentication/Role.php
new file mode 100644
index 0000000..c409ba4
--- /dev/null
+++ b/library/Icinga/Authentication/Role.php
@@ -0,0 +1,334 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication;
+
+class Role
+{
+ /**
+ * Name of the role
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * The role from which to inherit privileges
+ *
+ * @var Role
+ */
+ protected $parent;
+
+ /**
+ * The roles to which privileges are inherited
+ *
+ * @var Role[]
+ */
+ protected $children;
+
+ /**
+ * Whether restrictions should not apply to owners of the role
+ *
+ * @var bool
+ */
+ protected $unrestricted = false;
+
+ /**
+ * Permissions of the role
+ *
+ * @var string[]
+ */
+ protected $permissions = [];
+
+ /**
+ * Refusals of the role
+ *
+ * @var string[]
+ */
+ protected $refusals = [];
+
+ /**
+ * Restrictions of the role
+ *
+ * @var string[]
+ */
+ protected $restrictions = [];
+
+ /**
+ * Get the name of the role
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set the name of the role
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * Get the role from which privileges are inherited
+ *
+ * @return Role
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Set the role from which to inherit privileges
+ *
+ * @param Role $parent
+ *
+ * @return $this
+ */
+ public function setParent(Role $parent)
+ {
+ $this->parent = $parent;
+
+ return $this;
+ }
+
+ /**
+ * Get the roles to which privileges are inherited
+ *
+ * @return Role[]
+ */
+ public function getChildren()
+ {
+ return $this->children;
+ }
+
+ /**
+ * Set the roles to which inherit privileges
+ *
+ * @param Role[] $children
+ *
+ * @return $this
+ */
+ public function setChildren(array $children)
+ {
+ $this->children = $children;
+
+ return $this;
+ }
+
+ /**
+ * Add a role to which inherit privileges
+ *
+ * @param Role $role
+ *
+ * @return $this
+ */
+ public function addChild(Role $role)
+ {
+ $this->children[] = $role;
+
+ return $this;
+ }
+
+ /**
+ * Get whether restrictions should not apply to owners of the role
+ *
+ * @return bool
+ */
+ public function isUnrestricted()
+ {
+ return $this->unrestricted;
+ }
+
+ /**
+ * Set whether restrictions should not apply to owners of the role
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
+ public function setIsUnrestricted($state)
+ {
+ $this->unrestricted = (bool) $state;
+
+ 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 refusals of the role
+ *
+ * @return string[]
+ */
+ public function getRefusals()
+ {
+ return $this->refusals;
+ }
+
+ /**
+ * Set the refusals of the role
+ *
+ * @param array $refusals
+ *
+ * @return $this
+ */
+ public function setRefusals(array $refusals)
+ {
+ $this->refusals = $refusals;
+
+ 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;
+ }
+
+ /**
+ * Whether this role grants the given permission
+ *
+ * @param string $permission
+ * @param bool $ignoreParent Only evaluate the role's own permissions
+ * @param bool $cascadeUpwards `false` if `foo/bar/*` and `foo/bar/raboof` should not match `foo/*`
+ *
+ * @return bool
+ */
+ public function grants($permission, $ignoreParent = false, $cascadeUpwards = true)
+ {
+ foreach ($this->permissions as $grantedPermission) {
+ if ($this->match($grantedPermission, $permission, $cascadeUpwards)) {
+ return true;
+ }
+ }
+
+ if (! $ignoreParent && $this->getParent() !== null) {
+ return $this->getParent()->grants($permission, false, $cascadeUpwards);
+ }
+
+ return false;
+ }
+
+ /**
+ * Whether this role denies the given permission
+ *
+ * @param string $permission
+ * @param bool $ignoreParent Only evaluate the role's own refusals
+ *
+ * @return bool
+ */
+ public function denies($permission, $ignoreParent = false)
+ {
+ foreach ($this->refusals as $refusedPermission) {
+ if ($this->match($refusedPermission, $permission, false)) {
+ return true;
+ }
+ }
+
+ if (! $ignoreParent && $this->getParent() !== null) {
+ return $this->getParent()->denies($permission);
+ }
+
+ return false;
+ }
+
+ /**
+ * Get whether the role expression matches the required permission
+ *
+ * @param string $roleExpression
+ * @param string $requiredPermission
+ * @param bool $cascadeUpwards `false` if `foo/bar/*` and `foo/bar/raboof` should not match `foo/*`
+ *
+ * @return bool
+ */
+ protected function match($roleExpression, $requiredPermission, $cascadeUpwards = true)
+ {
+ if ($roleExpression === '*' || $roleExpression === $requiredPermission) {
+ return true;
+ }
+
+ $requiredWildcard = strpos($requiredPermission, '*');
+ if ($requiredWildcard !== false) {
+ if (($grantedWildcard = strpos($roleExpression, '*')) !== false) {
+ $wildcard = $cascadeUpwards ? min($requiredWildcard, $grantedWildcard) : $grantedWildcard;
+ } else {
+ $wildcard = $cascadeUpwards ? $requiredWildcard : false;
+ }
+ } else {
+ $wildcard = strpos($roleExpression, '*');
+ }
+
+ if ($wildcard !== false && $wildcard > 0) {
+ if (substr($requiredPermission, 0, $wildcard) === substr($roleExpression, 0, $wildcard)) {
+ return true;
+ }
+ } elseif ($requiredPermission === $roleExpression) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/library/Icinga/Authentication/RolesConfig.php b/library/Icinga/Authentication/RolesConfig.php
new file mode 100644
index 0000000..ac5695f
--- /dev/null
+++ b/library/Icinga/Authentication/RolesConfig.php
@@ -0,0 +1,43 @@
+<?php
+/* Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Authentication;
+
+use Icinga\Application\Icinga;
+use Icinga\Repository\IniRepository;
+
+class RolesConfig extends IniRepository
+{
+ protected $configs = [
+ 'roles' => [
+ 'name' => 'roles',
+ 'keyColumn' => 'name'
+ ]
+ ];
+
+ protected function initializeQueryColumns()
+ {
+ $columns = [
+ 'roles' => [
+ 'parent',
+ 'name',
+ 'users',
+ 'groups',
+ 'refusals',
+ 'permissions',
+ 'unrestricted',
+ 'application/share/users',
+ 'application/share/groups'
+ ]
+ ];
+
+ $moduleManager = Icinga::app()->getModuleManager();
+ foreach ($moduleManager->listInstalledModules() as $moduleName) {
+ foreach ($moduleManager->getModule($moduleName, false)->getProvidedRestrictions() as $restriction) {
+ $columns['roles'][] = $restriction->name;
+ }
+ }
+
+ return $columns;
+ }
+}
diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php
new file mode 100644
index 0000000..0e8cc6a
--- /dev/null
+++ b/library/Icinga/Authentication/User/DbUserBackend.php
@@ -0,0 +1,256 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication\User;
+
+use Exception;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\AuthenticationException;
+use Icinga\Repository\DbRepository;
+use Icinga\User;
+use PDO;
+
+class DbUserBackend extends DbRepository implements UserBackendInterface, Inspectable
+{
+ /**
+ * The query columns being provided
+ *
+ * @var array
+ */
+ protected $queryColumns = array(
+ 'user' => array(
+ 'user' => 'name COLLATE utf8mb4_general_ci',
+ 'user_name' => 'name',
+ 'is_active' => 'active',
+ 'created_at' => 'UNIX_TIMESTAMP(ctime)',
+ 'last_modified' => 'UNIX_TIMESTAMP(mtime)'
+ )
+ );
+
+ /**
+ * The statement columns being provided
+ *
+ * @var array
+ */
+ protected $statementColumns = array(
+ 'user' => array(
+ 'password' => 'password_hash',
+ 'created_at' => 'ctime',
+ 'last_modified' => 'mtime'
+ )
+ );
+
+ /**
+ * The columns which are not permitted to be queried
+ *
+ * @var array
+ */
+ protected $blacklistedQueryColumns = array('user');
+
+ /**
+ * The search columns being provided
+ *
+ * @var array
+ */
+ protected $searchColumns = array('user');
+
+ /**
+ * The default sort rules to be applied on a query
+ *
+ * @var array
+ */
+ protected $sortRules = array(
+ 'user_name' => array(
+ 'columns' => array(
+ 'is_active desc',
+ 'user_name'
+ )
+ )
+ );
+
+ /**
+ * The value conversion rules to apply on a query or statement
+ *
+ * @var array
+ */
+ protected $conversionRules = array(
+ 'user' => array(
+ 'password'
+ )
+ );
+
+ /**
+ * Initialize this database user backend
+ */
+ protected function init()
+ {
+ if (! $this->ds->getTablePrefix()) {
+ $this->ds->setTablePrefix('icingaweb_');
+ }
+ }
+
+ /**
+ * Initialize this repository's filter columns
+ *
+ * @return array
+ */
+ protected function initializeFilterColumns()
+ {
+ $userLabel = t('Username') . ' ' . t('(Case insensitive)');
+ return array(
+ $userLabel => 'user',
+ t('Username') => 'user_name',
+ t('Active') => 'is_active',
+ t('Created at') => 'created_at',
+ t('Last modified') => 'last_modified'
+ );
+ }
+
+ /**
+ * Insert a table row with the given data
+ *
+ * @param string $table
+ * @param array $bind
+ *
+ * @return void
+ */
+ public function insert($table, array $bind, array $types = array())
+ {
+ $this->requireTable($table);
+ $bind['created_at'] = date('Y-m-d H:i:s');
+ $this->ds->insert(
+ $this->prependTablePrefix($table),
+ $this->requireStatementColumns($table, $bind),
+ array(
+ 'active' => PDO::PARAM_INT,
+ 'password_hash' => PDO::PARAM_LOB
+ )
+ );
+ }
+
+ /**
+ * Update table rows with the given data, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param array $bind
+ * @param Filter $filter
+ */
+ public function update($table, array $bind, Filter $filter = null, array $types = array())
+ {
+ $this->requireTable($table);
+ $bind['last_modified'] = date('Y-m-d H:i:s');
+ if ($filter) {
+ $filter = $this->requireFilter($table, $filter);
+ }
+
+ $this->ds->update(
+ $this->prependTablePrefix($table),
+ $this->requireStatementColumns($table, $bind),
+ $filter,
+ array(
+ 'active' => PDO::PARAM_INT,
+ 'password_hash' => PDO::PARAM_LOB
+ )
+ );
+ }
+
+ /**
+ * Hash and return the given password
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ protected function persistPassword($value)
+ {
+ return password_hash($value, PASSWORD_DEFAULT);
+ }
+
+ /**
+ * Fetch the hashed password for the given user
+ *
+ * @param string $username The name of the user
+ *
+ * @return string
+ */
+ protected function getPasswordHash($username)
+ {
+ if ($this->ds->getDbType() === 'pgsql') {
+ // Since PostgreSQL version 9.0 the default value for bytea_output is 'hex' instead of 'escape'
+ $columns = array('password_hash' => 'ENCODE(password_hash, \'escape\')');
+ } else {
+ $columns = array('password_hash');
+ }
+
+ $nameColumn = 'name';
+ if ($this->ds->getDbType() === 'mysql') {
+ $username = strtolower($username);
+ $nameColumn = 'BINARY LOWER(name)';
+ }
+
+ $query = $this->ds->select()
+ ->from($this->prependTablePrefix('user'), $columns)
+ ->where($nameColumn, $username)
+ ->where('active', true);
+
+ $statement = $this->ds->getDbAdapter()->prepare($query->getSelectQuery());
+ $statement->execute();
+ $statement->bindColumn(1, $lob, PDO::PARAM_LOB);
+ $statement->fetch(PDO::FETCH_BOUND);
+ if (is_resource($lob)) {
+ $lob = stream_get_contents($lob);
+ }
+
+ if ($lob === null) {
+ return '';
+ }
+
+ return $this->ds->getDbType() === 'pgsql' ? pg_unescape_bytea($lob) : $lob;
+ }
+
+ /**
+ * Authenticate the given user
+ *
+ * @param User $user
+ * @param string $password
+ *
+ * @return bool True on success, false on failure
+ *
+ * @throws AuthenticationException In case authentication is not possible due to an error
+ */
+ public function authenticate(User $user, $password)
+ {
+ try {
+ return password_verify(
+ $password,
+ $this->getPasswordHash($user->getUsername())
+ );
+ } catch (Exception $e) {
+ throw new AuthenticationException(
+ 'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',
+ $user->getUsername(),
+ $this->getName(),
+ $e
+ );
+ }
+ }
+
+ /**
+ * Inspect this object to gain extended information about its health
+ *
+ * @return Inspection The inspection result
+ */
+ public function inspect()
+ {
+ $insp = new Inspection('Db User Backend');
+ $insp->write($this->ds->inspect());
+ try {
+ $insp->write(sprintf('%s active users', $this->select()->where('is_active', true)->count()));
+ } catch (Exception $e) {
+ $insp->error(sprintf('Query failed: %s', $e->getMessage()));
+ }
+ return $insp;
+ }
+}
diff --git a/library/Icinga/Authentication/User/DomainAwareInterface.php b/library/Icinga/Authentication/User/DomainAwareInterface.php
new file mode 100644
index 0000000..3ff9c31
--- /dev/null
+++ b/library/Icinga/Authentication/User/DomainAwareInterface.php
@@ -0,0 +1,17 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication\User;
+
+/**
+ * Interface for user backends that are responsible for a specific domain
+ */
+interface DomainAwareInterface
+{
+ /**
+ * Get the domain the backend is responsible for
+ *
+ * @return string
+ */
+ public function getDomain();
+}
diff --git a/library/Icinga/Authentication/User/ExternalBackend.php b/library/Icinga/Authentication/User/ExternalBackend.php
new file mode 100644
index 0000000..6e79928
--- /dev/null
+++ b/library/Icinga/Authentication/User/ExternalBackend.php
@@ -0,0 +1,124 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication\User;
+
+use Icinga\Application\Logger;
+use Icinga\Data\ConfigObject;
+use Icinga\User;
+
+/**
+ * Test login with external authentication mechanism, e.g. Apache
+ */
+class ExternalBackend implements UserBackendInterface
+{
+ /**
+ * Possible variables where to read the user from
+ *
+ * @var string[]
+ */
+ public static $remoteUserEnvvars = array('REMOTE_USER', 'REDIRECT_REMOTE_USER');
+
+ /**
+ * The name of this backend
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * Regexp expression to strip values from a username
+ *
+ * @var string
+ */
+ protected $stripUsernameRegexp;
+
+ /**
+ * Create new authentication backend of type "external"
+ *
+ * @param ConfigObject $config
+ */
+ public function __construct(ConfigObject $config)
+ {
+ $this->stripUsernameRegexp = $config->get('strip_username_regexp');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * Get the remote user from environment or $_SERVER, if any
+ *
+ * @param string $variable The name of the variable where to read the user from
+ *
+ * @return string|null
+ */
+ public static function getRemoteUser($variable = 'REMOTE_USER')
+ {
+ $username = getenv($variable);
+ if (! empty($username)) {
+ return $username;
+ }
+
+ if (array_key_exists($variable, $_SERVER) && ! empty($_SERVER[$variable])) {
+ return $_SERVER[$variable];
+ }
+ }
+
+ /**
+ * Get the remote user information from environment or $_SERVER, if any
+ *
+ * @return array Contains always two entries, the username and origin which may both set to null.
+ */
+ public static function getRemoteUserInformation()
+ {
+ foreach (static::$remoteUserEnvvars as $envVar) {
+ $username = static::getRemoteUser($envVar);
+ if ($username !== null) {
+ return array($username, $envVar);
+ }
+ }
+
+ return array(null, null);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function authenticate(User $user, $password = null)
+ {
+ list($username, $field) = static::getRemoteUserInformation();
+ if ($username !== null) {
+ $user->setExternalUserInformation($username, $field);
+
+ if ($this->stripUsernameRegexp) {
+ $stripped = @preg_replace($this->stripUsernameRegexp, '', $username);
+ if ($stripped === false) {
+ Logger::error('Failed to strip external username. The configured regular expression is invalid.');
+ return false;
+ }
+
+ $username = $stripped;
+ }
+
+ $user->setUsername($username);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php
new file mode 100644
index 0000000..6a2cacf
--- /dev/null
+++ b/library/Icinga/Authentication/User/LdapUserBackend.php
@@ -0,0 +1,479 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication\User;
+
+use DateTime;
+use Exception;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Exception\AuthenticationException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\QueryException;
+use Icinga\Repository\LdapRepository;
+use Icinga\Repository\RepositoryQuery;
+use Icinga\Protocol\Ldap\LdapException;
+use Icinga\User;
+
+class LdapUserBackend extends LdapRepository implements UserBackendInterface, DomainAwareInterface, Inspectable
+{
+ /**
+ * The base DN to use for a query
+ *
+ * @var string
+ */
+ protected $baseDn;
+
+ /**
+ * The objectClass where look for users
+ *
+ * @var string
+ */
+ protected $userClass;
+
+ /**
+ * The attribute name where to find a user's name
+ *
+ * @var string
+ */
+ protected $userNameAttribute;
+
+ /**
+ * The custom LDAP filter to apply on search queries
+ *
+ * @var string
+ */
+ protected $filter;
+
+ /**
+ * The domain the backend is responsible for
+ *
+ * @var string
+ */
+ protected $domain;
+
+ /**
+ * The columns which are not permitted to be queried
+ *
+ * @var array
+ */
+ protected $blacklistedQueryColumns = array('user');
+
+ /**
+ * The search columns being provided
+ *
+ * @var array
+ */
+ protected $searchColumns = array('user');
+
+ /**
+ * The default sort rules to be applied on a query
+ *
+ * @var array
+ */
+ protected $sortRules = array(
+ 'user_name' => array(
+ 'columns' => array(
+ 'is_active desc',
+ 'user_name'
+ )
+ )
+ );
+
+ /**
+ * Set the base DN to use for a query
+ *
+ * @param string $baseDn
+ *
+ * @return $this
+ */
+ public function setBaseDn($baseDn)
+ {
+ if ($baseDn && ($baseDn = trim($baseDn))) {
+ $this->baseDn = $baseDn;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the base DN to use for a query
+ *
+ * @return string
+ */
+ public function getBaseDn()
+ {
+ return $this->baseDn;
+ }
+
+ /**
+ * Set the objectClass where to look for users
+ *
+ * @param string $userClass
+ *
+ * @return $this
+ */
+ public function setUserClass($userClass)
+ {
+ $this->userClass = $this->getNormedAttribute($userClass);
+ return $this;
+ }
+
+ /**
+ * Return the objectClass where to look for users
+ *
+ * @return string
+ */
+ public function getUserClass()
+ {
+ return $this->userClass;
+ }
+
+ /**
+ * Set the attribute name where to find a user's name
+ *
+ * @param string $userNameAttribute
+ *
+ * @return $this
+ */
+ public function setUserNameAttribute($userNameAttribute)
+ {
+ $this->userNameAttribute = $this->getNormedAttribute($userNameAttribute);
+ return $this;
+ }
+
+ /**
+ * Return the attribute name where to find a user's name
+ *
+ * @return string
+ */
+ public function getUserNameAttribute()
+ {
+ return $this->userNameAttribute;
+ }
+
+ /**
+ * Set the custom LDAP filter to apply on search queries
+ *
+ * @param string $filter
+ *
+ * @return $this
+ */
+ public function setFilter($filter)
+ {
+ if ($filter && ($filter = trim($filter))) {
+ if ($filter[0] === '(') {
+ $filter = substr($filter, 1, -1);
+ }
+
+ $this->filter = $filter;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the custom LDAP filter to apply on search queries
+ *
+ * @return string
+ */
+ public function getFilter()
+ {
+ return $this->filter;
+ }
+
+ public function getDomain()
+ {
+ return $this->domain;
+ }
+
+ /**
+ * Set the domain the backend is responsible for
+ *
+ * @param string $domain
+ *
+ * @return $this
+ */
+ public function setDomain($domain)
+ {
+ if ($domain && ($domain = trim($domain))) {
+ $this->domain = $domain;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Initialize this repository's virtual tables
+ *
+ * @return array
+ *
+ * @throws ProgrammingError In case $this->userClass has not been set yet
+ */
+ protected function initializeVirtualTables()
+ {
+ if ($this->userClass === null) {
+ throw new ProgrammingError('It is required to set the object class where to find users first');
+ }
+
+ return array(
+ 'user' => $this->userClass
+ );
+ }
+
+ /**
+ * Initialize this repository's query columns
+ *
+ * @return array
+ *
+ * @throws ProgrammingError In case $this->userNameAttribute has not been set yet
+ */
+ protected function initializeQueryColumns()
+ {
+ if ($this->userNameAttribute === null) {
+ throw new ProgrammingError('It is required to set a attribute name where to find a user\'s name first');
+ }
+
+ if ($this->ds->getCapabilities()->isActiveDirectory()) {
+ $isActiveAttribute = 'userAccountControl';
+ $createdAtAttribute = 'whenCreated';
+ $lastModifiedAttribute = 'whenChanged';
+ } else {
+ // TODO(jom): Elaborate whether it is possible to add dynamic support for the ppolicy
+ $isActiveAttribute = 'shadowExpire';
+
+ $createdAtAttribute = 'createTimestamp';
+ $lastModifiedAttribute = 'modifyTimestamp';
+ }
+
+ return array(
+ 'user' => array(
+ 'user' => $this->userNameAttribute,
+ 'user_name' => $this->userNameAttribute,
+ 'is_active' => $isActiveAttribute,
+ 'created_at' => $createdAtAttribute,
+ 'last_modified' => $lastModifiedAttribute
+ )
+ );
+ }
+
+ /**
+ * Initialize this repository's filter columns
+ *
+ * @return array
+ */
+ protected function initializeFilterColumns()
+ {
+ return array(
+ t('Username') => 'user_name',
+ t('Active') => 'is_active',
+ t('Created At') => 'created_at',
+ t('Last modified') => 'last_modified'
+ );
+ }
+
+ /**
+ * Initialize this repository's conversion rules
+ *
+ * @return array
+ */
+ protected function initializeConversionRules()
+ {
+ if ($this->ds->getCapabilities()->isActiveDirectory()) {
+ $stateConverter = 'user_account_control';
+ } else {
+ $stateConverter = 'shadow_expire';
+ }
+
+ return array(
+ 'user' => array(
+ 'is_active' => $stateConverter,
+ 'created_at' => 'generalized_time',
+ 'last_modified' => 'generalized_time'
+ )
+ );
+ }
+
+ /**
+ * Return whether the given userAccountControl value defines that a user is permitted to login
+ *
+ * @param string|null $value
+ *
+ * @return bool
+ */
+ protected function retrieveUserAccountControl($value)
+ {
+ if ($value === null) {
+ return $value;
+ }
+
+ $ADS_UF_ACCOUNTDISABLE = 2;
+ return ((int) $value & $ADS_UF_ACCOUNTDISABLE) === 0;
+ }
+
+ /**
+ * Return whether the given shadowExpire value defines that a user is permitted to login
+ *
+ * @param string|null $value
+ *
+ * @return bool
+ */
+ protected function retrieveShadowExpire($value)
+ {
+ if ($value === null) {
+ return $value;
+ }
+
+ $now = new DateTime();
+ $bigBang = clone $now;
+ $bigBang->setTimestamp(0);
+ return ((int) $value) >= $bigBang->diff($now)->days;
+ }
+
+ /**
+ * Validate that the requested table exists
+ *
+ * @param string $table The table to validate
+ * @param RepositoryQuery $query An optional query to pass as context
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case the given table does not exist
+ */
+ public function requireTable($table, RepositoryQuery $query = null)
+ {
+ if ($query !== null) {
+ $query->getQuery()->setBase($this->baseDn);
+ if ($this->filter) {
+ $query->getQuery()->setNativeFilter($this->filter);
+ }
+ }
+
+ return parent::requireTable($table, $query);
+ }
+
+ /**
+ * Validate that the given column is a valid query target and return it or the actual name if it's an alias
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The name or alias of the column to validate
+ * @param RepositoryQuery $query An optional query to pass as context
+ *
+ * @return string The given column's name
+ *
+ * @throws QueryException In case the given column is not a valid query column
+ */
+ public function requireQueryColumn($table, $name, RepositoryQuery $query = null)
+ {
+ $column = parent::requireQueryColumn($table, $name, $query);
+ if ($name === 'user_name' && $query !== null) {
+ $query->getQuery()->setUnfoldAttribute('user_name');
+ }
+
+ return $column;
+ }
+
+ /**
+ * Authenticate the given user
+ *
+ * @param User $user
+ * @param string $password
+ *
+ * @return bool True on success, false on failure
+ *
+ * @throws AuthenticationException In case authentication is not possible due to an error
+ */
+ public function authenticate(User $user, $password)
+ {
+ if ($this->domain !== null) {
+ if (! $user->hasDomain() || strtolower($user->getDomain()) !== strtolower($this->domain)) {
+ return false;
+ }
+
+ $username = $user->getLocalUsername();
+ } else {
+ $username = $user->getUsername();
+ }
+
+ try {
+ $userDn = $this
+ ->select()
+ ->where('user_name', str_replace('*', '', $username))
+ ->getQuery()
+ ->setUsePagedResults(false)
+ ->fetchDn();
+ if ($userDn === null) {
+ return false;
+ }
+
+ $validCredentials = $this->ds->testCredentials($userDn, $password);
+ if ($validCredentials) {
+ $user->setAdditional('ldap_dn', $userDn);
+ }
+
+ return $validCredentials;
+ } catch (LdapException $e) {
+ throw new AuthenticationException(
+ 'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',
+ $username,
+ $this->getName(),
+ $e
+ );
+ }
+ }
+
+ /**
+ * Inspect if this LDAP User Backend is working as expected by probing the backend
+ * and testing if thea uthentication is possible
+ *
+ * Try to bind to the backend and fetch a single user to check if:
+ * <ul>
+ * <li>Connection credentials are correct and the bind is possible</li>
+ * <li>At least one user exists</li>
+ * <li>The specified userClass has the property specified by userNameAttribute</li>
+ * </ul>
+ *
+ * @return Inspection Inspection result
+ */
+ public function inspect()
+ {
+ $result = new Inspection('Ldap User Backend');
+
+ // inspect the used connection to get more diagnostic info in case the connection is not working
+ $result->write($this->ds->inspect());
+ try {
+ try {
+ $res = $this->select()->fetchRow();
+ } catch (LdapException $e) {
+ throw new AuthenticationException('Connection not possible', $e);
+ }
+ $result->write('Searching for: ' . sprintf(
+ 'objectClass "%s" in DN "%s" (Filter: %s)',
+ $this->userClass,
+ $this->baseDn ?: $this->ds->getDn(),
+ $this->filter ?: 'None'
+ ));
+ if ($res === false) {
+ throw new AuthenticationException('Error, no users found in backend');
+ }
+ $result->write(sprintf('%d users found in backend', $this->select()->count()));
+ if (! isset($res->user_name)) {
+ throw new AuthenticationException(
+ 'UserNameAttribute "%s" not existing in objectClass "%s"',
+ $this->userNameAttribute,
+ $this->userClass
+ );
+ }
+ } catch (AuthenticationException $e) {
+ if (($previous = $e->getPrevious()) !== null) {
+ $result->error($previous->getMessage());
+ } else {
+ $result->error($e->getMessage());
+ }
+ } catch (Exception $e) {
+ $result->error(sprintf('Unable to validate authentication: %s', $e->getMessage()));
+ }
+ return $result;
+ }
+}
diff --git a/library/Icinga/Authentication/User/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php
new file mode 100644
index 0000000..423b278
--- /dev/null
+++ b/library/Icinga/Authentication/User/UserBackend.php
@@ -0,0 +1,259 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication\User;
+
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Application\Icinga;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Util\ConfigAwareFactory;
+
+/**
+ * Factory for user backends
+ */
+class UserBackend implements ConfigAwareFactory
+{
+ /**
+ * The default user backend types provided by Icinga Web 2
+ *
+ * @var array
+ */
+ protected static $defaultBackends = array(
+ 'external',
+ 'db',
+ 'ldap',
+ 'msldap'
+ );
+
+ /**
+ * The registered custom user backends with their identifier as key and class name as value
+ *
+ * @var array
+ */
+ protected static $customBackends;
+
+ /**
+ * User backend configuration
+ *
+ * @var Config
+ */
+ private static $backends;
+
+ /**
+ * Set user backend configuration
+ *
+ * @param Config $config
+ */
+ public static function setConfig($config)
+ {
+ self::$backends = $config;
+ }
+
+ /**
+ * Return the configuration of all existing user backends
+ *
+ * @return Config
+ */
+ public static function getBackendConfigs()
+ {
+ self::assertBackendsExist();
+ return self::$backends;
+ }
+
+ /**
+ * Check if any user backends exist. If not, throw an error.
+ *
+ * @throws ConfigurationError
+ */
+ private static function assertBackendsExist()
+ {
+ if (self::$backends === null) {
+ throw new ConfigurationError(
+ 'User backends not set up. Please contact your Icinga Web administrator'
+ );
+ }
+ }
+
+ /**
+ * Register all custom user backends from all loaded modules
+ */
+ protected static function registerCustomUserBackends()
+ {
+ if (static::$customBackends !== null) {
+ return;
+ }
+
+ static::$customBackends = array();
+ $providedBy = array();
+ foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) {
+ foreach ($module->getUserBackends() as $identifier => $className) {
+ if (array_key_exists($identifier, $providedBy)) {
+ Logger::warning(
+ 'Cannot register user backend of type "%s" provided by module "%s".'
+ . ' The type is already provided by module "%s"',
+ $identifier,
+ $module->getName(),
+ $providedBy[$identifier]
+ );
+ } elseif (in_array($identifier, static::$defaultBackends)) {
+ Logger::warning(
+ 'Cannot register user backend of type "%s" provided by module "%s".'
+ . ' The type is a default type provided by Icinga Web 2',
+ $identifier,
+ $module->getName()
+ );
+ } else {
+ $providedBy[$identifier] = $module->getName();
+ static::$customBackends[$identifier] = $className;
+ }
+ }
+ }
+ }
+
+ /**
+ * Get config forms of all custom user backends
+ */
+ public static function getCustomBackendConfigForms()
+ {
+ $customBackendConfigForms = [];
+ static::registerCustomUserBackends();
+ foreach (self::$customBackends as $customBackendType => $customBackendClass) {
+ if (method_exists($customBackendClass, 'getConfigurationFormClass')) {
+ $customBackendConfigForms[$customBackendType] = $customBackendClass::getConfigurationFormClass();
+ }
+ }
+
+ return $customBackendConfigForms;
+ }
+
+ /**
+ * Return the class for the given custom user backend
+ *
+ * @param string $identifier The identifier of the custom user backend
+ *
+ * @return string|null The name of the class or null in case there was no
+ * backend found with the given identifier
+ *
+ * @throws ConfigurationError In case the class associated to the given identifier does not exist
+ */
+ protected static function getCustomUserBackend($identifier)
+ {
+ static::registerCustomUserBackends();
+ if (array_key_exists($identifier, static::$customBackends)) {
+ $className = static::$customBackends[$identifier];
+ if (! class_exists($className)) {
+ throw new ConfigurationError(
+ 'Cannot utilize user backend of type "%s". Class "%s" does not exist',
+ $identifier,
+ $className
+ );
+ }
+
+ return $className;
+ }
+ }
+
+ /**
+ * Create and return a user backend with the given name and given configuration applied to it
+ *
+ * @param string $name
+ * @param ConfigObject $backendConfig
+ *
+ * @return UserBackendInterface
+ *
+ * @throws ConfigurationError
+ */
+ public static function create($name, ConfigObject $backendConfig = null)
+ {
+ if ($backendConfig === null) {
+ self::assertBackendsExist();
+ if (self::$backends->hasSection($name)) {
+ $backendConfig = self::$backends->getSection($name);
+ } else {
+ throw new ConfigurationError('User backend "%s" does not exist', $name);
+ }
+ }
+
+ if ($backendConfig->name !== null) {
+ $name = $backendConfig->name;
+ }
+
+ if (! ($backendType = strtolower($backendConfig->backend))) {
+ throw new ConfigurationError(
+ 'Authentication configuration for user backend "%s" is missing the \'backend\' directive',
+ $name
+ );
+ }
+
+ if ($backendType === 'external') {
+ $backend = new ExternalBackend($backendConfig);
+ $backend->setName($name);
+ return $backend;
+ }
+ if (in_array($backendType, static::$defaultBackends)) {
+ // The default backend check is the first one because of performance reasons:
+ // Do not attempt to load a custom user backend unless it's actually required
+ } elseif (($customClass = static::getCustomUserBackend($backendType)) !== null) {
+ $backend = new $customClass($backendConfig);
+ if (! is_a($backend, 'Icinga\Authentication\User\UserBackendInterface')) {
+ throw new ConfigurationError(
+ 'Cannot utilize user backend of type "%s". Class "%s" does not implement UserBackendInterface',
+ $backendType,
+ $customClass
+ );
+ }
+
+ $backend->setName($name);
+ return $backend;
+ } else {
+ throw new ConfigurationError(
+ 'Authentication configuration for user backend "%s" defines an invalid backend type.'
+ . ' Backend type "%s" is not supported',
+ $name,
+ $backendType
+ );
+ }
+
+ if ($backendConfig->resource === null) {
+ throw new ConfigurationError(
+ 'Authentication configuration for user backend "%s" is missing the \'resource\' directive',
+ $name
+ );
+ }
+
+ $resourceConfig = ResourceFactory::getResourceConfig($backendConfig->resource);
+ if ($backendType === 'db' && $resourceConfig->db === 'mysql') {
+ $resourceConfig->charset = 'utf8mb4';
+ }
+
+ $resource = ResourceFactory::createResource($resourceConfig);
+ $backend = null;
+ switch ($backendType) {
+ case 'db':
+ $backend = new DbUserBackend($resource);
+ break;
+ case 'msldap':
+ $backend = new LdapUserBackend($resource);
+ $backend->setBaseDn($backendConfig->base_dn);
+ $backend->setUserClass($backendConfig->get('user_class', 'user'));
+ $backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'sAMAccountName'));
+ $backend->setFilter($backendConfig->filter);
+ $backend->setDomain($backendConfig->domain);
+ break;
+ case 'ldap':
+ $backend = new LdapUserBackend($resource);
+ $backend->setBaseDn($backendConfig->base_dn);
+ $backend->setUserClass($backendConfig->get('user_class', 'inetOrgPerson'));
+ $backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'uid'));
+ $backend->setFilter($backendConfig->filter);
+ $backend->setDomain($backendConfig->domain);
+ break;
+ }
+
+ $backend->setName($name);
+ return $backend;
+ }
+}
diff --git a/library/Icinga/Authentication/User/UserBackendInterface.php b/library/Icinga/Authentication/User/UserBackendInterface.php
new file mode 100644
index 0000000..4660eb0
--- /dev/null
+++ b/library/Icinga/Authentication/User/UserBackendInterface.php
@@ -0,0 +1,39 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication\User;
+
+use Icinga\Authentication\Authenticatable;
+use Icinga\User;
+
+/**
+ * Interface for user backends
+ */
+interface UserBackendInterface extends Authenticatable
+{
+ /**
+ * Set this backend's name
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name);
+
+ /**
+ * Return this backend's name
+ *
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Return this backend's configuration form class path
+ *
+ * This is not part of the interface to not break existing implementations.
+ * If you need a custom backend form, implement this method.
+ *
+ * @return string
+ */
+ //public static function getConfigurationFormClass();
+}
diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
new file mode 100644
index 0000000..5299bbb
--- /dev/null
+++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
@@ -0,0 +1,325 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication\UserGroup;
+
+use Exception;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Exception\NotFoundError;
+use Icinga\Repository\DbRepository;
+use Icinga\Repository\RepositoryQuery;
+use Icinga\User;
+
+class DbUserGroupBackend extends DbRepository implements Inspectable, UserGroupBackendInterface
+{
+ /**
+ * The query columns being provided
+ *
+ * @var array
+ */
+ protected $queryColumns = array(
+ 'group' => array(
+ 'group_id' => 'g.id',
+ 'group' => 'g.name COLLATE utf8mb4_general_ci',
+ 'group_name' => 'g.name',
+ 'parent' => 'g.parent',
+ 'created_at' => 'UNIX_TIMESTAMP(g.ctime)',
+ 'last_modified' => 'UNIX_TIMESTAMP(g.mtime)'
+ ),
+ 'group_membership' => array(
+ 'group_id' => 'gm.group_id',
+ 'user' => 'gm.username COLLATE utf8mb4_general_ci',
+ 'user_name' => 'gm.username',
+ 'created_at' => 'UNIX_TIMESTAMP(gm.ctime)',
+ 'last_modified' => 'UNIX_TIMESTAMP(gm.mtime)'
+ )
+ );
+
+ /**
+ * The table aliases being applied
+ *
+ * @var array
+ */
+ protected $tableAliases = array(
+ 'group' => 'g',
+ 'group_membership' => 'gm'
+ );
+
+ /**
+ * The statement columns being provided
+ *
+ * @var array
+ */
+ protected $statementColumns = array(
+ 'group' => array(
+ 'group_id' => 'id',
+ 'group_name' => 'name',
+ 'parent' => 'parent',
+ 'created_at' => 'ctime',
+ 'last_modified' => 'mtime'
+ ),
+ 'group_membership' => array(
+ 'group_id' => 'group_id',
+ 'group_name' => 'group_id',
+ 'user_name' => 'username',
+ 'created_at' => 'ctime',
+ 'last_modified' => 'mtime'
+ )
+ );
+
+ /**
+ * The columns which are not permitted to be queried
+ *
+ * @var array
+ */
+ protected $blacklistedQueryColumns = array('group', 'user');
+
+ /**
+ * The search columns being provided
+ *
+ * @var array
+ */
+ protected $searchColumns = array('group', 'user');
+
+ /**
+ * The value conversion rules to apply on a query or statement
+ *
+ * @var array
+ */
+ protected $conversionRules = array(
+ 'group' => array(
+ 'parent' => 'group_id'
+ ),
+ 'group_membership' => array(
+ 'group_name' => 'group_id'
+ )
+ );
+
+ /**
+ * Initialize this database user group backend
+ */
+ protected function init()
+ {
+ if (! $this->ds->getTablePrefix()) {
+ $this->ds->setTablePrefix('icingaweb_');
+ }
+ }
+
+ /**
+ * Initialize this repository's filter columns
+ *
+ * @return array
+ */
+ protected function initializeFilterColumns()
+ {
+ $userLabel = t('Username') . ' ' . t('(Case insensitive)');
+ $groupLabel = t('User Group') . ' ' . t('(Case insensitive)');
+ return array(
+ $userLabel => 'user',
+ t('Username') => 'user_name',
+ $groupLabel => 'group',
+ t('User Group') => 'group_name',
+ t('Parent') => 'parent',
+ t('Created At') => 'created_at',
+ t('Last modified') => 'last_modified'
+ );
+ }
+
+ /**
+ * Insert a table row with the given data
+ *
+ * @param string $table
+ * @param array $bind
+ */
+ public function insert($table, array $bind, array $types = array())
+ {
+ $bind['created_at'] = date('Y-m-d H:i:s');
+ parent::insert($table, $bind);
+ }
+
+ /**
+ * Update table rows with the given data, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param array $bind
+ * @param Filter $filter
+ */
+ public function update($table, array $bind, Filter $filter = null, array $types = array())
+ {
+ $bind['last_modified'] = date('Y-m-d H:i:s');
+ parent::update($table, $bind, $filter);
+ }
+
+ /**
+ * Delete table rows, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param Filter $filter
+ */
+ public function delete($table, Filter $filter = null)
+ {
+ if ($table === 'group') {
+ parent::delete('group_membership', $filter);
+ $idQuery = $this->select(array('group_id'));
+ if ($filter !== null) {
+ $idQuery->applyFilter($filter);
+ }
+
+ $this->update('group', array('parent' => null), Filter::where('parent', $idQuery->fetchColumn()));
+ }
+
+ parent::delete($table, $filter);
+ }
+
+ /**
+ * Return the groups the given user is a member of
+ *
+ * @param User $user
+ *
+ * @return array
+ */
+ public function getMemberships(User $user)
+ {
+ $groupQuery = $this->ds
+ ->select()
+ ->from(
+ array('g' => $this->prependTablePrefix('group')),
+ array(
+ 'group_name' => 'g.name',
+ 'parent_name' => 'gg.name'
+ )
+ )->joinLeft(
+ array('gg' => $this->prependTablePrefix('group')),
+ 'g.parent = gg.id',
+ array()
+ );
+
+ $groups = array();
+ foreach ($groupQuery as $group) {
+ $groups[$group->group_name] = $group->parent_name;
+ }
+
+ $membershipQuery = $this
+ ->select()
+ ->from('group_membership', array('group_name'))
+ ->where('user_name', $user->getUsername());
+
+ $memberships = array();
+ foreach ($membershipQuery as $membership) {
+ $memberships[] = $membership->group_name;
+ $parent = $groups[$membership->group_name];
+ while ($parent !== null) {
+ $memberships[] = $parent;
+ // Usually a parent is an existing group, but since we do not have a constraint on our table..
+ $parent = isset($groups[$parent]) ? $groups[$parent] : null;
+ }
+ }
+
+ return $memberships;
+ }
+
+ /**
+ * Return the name of the backend that is providing the given user
+ *
+ * @param string $username Currently unused
+ *
+ * @return null|string The name of the backend or null in case this information is not available
+ */
+ public function getUserBackendName($username)
+ {
+ return null; // TODO(10373): Store this to the database when inserting and fetch it here
+ }
+
+ /**
+ * Join group into group_membership
+ *
+ * @param RepositoryQuery $query
+ */
+ protected function joinGroup(RepositoryQuery $query)
+ {
+ $query->getQuery()->join(
+ $this->requireTable('group'),
+ 'gm.group_id = g.id',
+ array()
+ );
+ }
+
+ /**
+ * Join group_membership into group
+ *
+ * @param RepositoryQuery $query
+ */
+ protected function joinGroupMembership(RepositoryQuery $query)
+ {
+ $query->getQuery()->joinLeft(
+ $this->requireTable('group_membership'),
+ 'g.id = gm.group_id',
+ array()
+ )->group('g.id');
+ }
+
+ /**
+ * Fetch and return the corresponding id for the given group's name
+ *
+ * @param string|array $groupName
+ *
+ * @return int
+ *
+ * @throws NotFoundError
+ */
+ protected function persistGroupId($groupName)
+ {
+ if (empty($groupName) || is_numeric($groupName)) {
+ return $groupName;
+ }
+
+ if (is_array($groupName)) {
+ if (is_numeric($groupName[0])) {
+ return $groupName; // In case the array contains mixed types...
+ }
+
+ $groupIds = $this->ds
+ ->select()
+ ->from($this->prependTablePrefix('group'), array('id'))
+ ->where('name', $groupName)
+ ->fetchColumn();
+ if (empty($groupIds)) {
+ throw new NotFoundError('No groups found matching one of: %s', implode(', ', $groupName));
+ }
+
+ return $groupIds;
+ }
+
+ $groupId = $this->ds
+ ->select()
+ ->from($this->prependTablePrefix('group'), array('id'))
+ ->where('name', $groupName)
+ ->fetchOne();
+ if ($groupId === false) {
+ throw new NotFoundError('Group "%s" does not exist', $groupName);
+ }
+
+ return $groupId;
+ }
+
+ /**
+ * Inspect this object to gain extended information about its health
+ *
+ * @return Inspection The inspection result
+ */
+ public function inspect()
+ {
+ $insp = new Inspection('Db User Group Backend');
+ $insp->write($this->ds->inspect());
+
+ try {
+ $insp->write(sprintf('%s group(s)', $this->select()->count()));
+ } catch (Exception $e) {
+ $insp->error(sprintf('Query failed: %s', $e->getMessage()));
+ }
+
+ return $insp;
+ }
+}
diff --git a/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
new file mode 100644
index 0000000..e78242e
--- /dev/null
+++ b/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
@@ -0,0 +1,945 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication\UserGroup;
+
+use Exception;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Authentication\User\LdapUserBackend;
+use Icinga\Application\Logger;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Exception\AuthenticationException;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\QueryException;
+use Icinga\Protocol\Ldap\LdapException;
+use Icinga\Protocol\Ldap\LdapUtils;
+use Icinga\Repository\LdapRepository;
+use Icinga\Repository\RepositoryQuery;
+use Icinga\User;
+
+class LdapUserGroupBackend extends LdapRepository implements Inspectable, UserGroupBackendInterface
+{
+ /**
+ * The user backend being associated with this user group backend
+ *
+ * @var LdapUserBackend
+ */
+ protected $userBackend;
+
+ /**
+ * The base DN to use for a user query
+ *
+ * @var string
+ */
+ protected $userBaseDn;
+
+ /**
+ * The base DN to use for a group query
+ *
+ * @var string
+ */
+ protected $groupBaseDn;
+
+ /**
+ * The objectClass where look for users
+ *
+ * @var string
+ */
+ protected $userClass;
+
+ /**
+ * The objectClass where look for groups
+ *
+ * @var string
+ */
+ protected $groupClass;
+
+ /**
+ * The attribute name where to find a user's name
+ *
+ * @var string
+ */
+ protected $userNameAttribute;
+
+ /**
+ * The attribute name where to find a group's name
+ *
+ * @var string
+ */
+ protected $groupNameAttribute;
+
+ /**
+ * The attribute name where to find a group's member
+ *
+ * @var string
+ */
+ protected $groupMemberAttribute;
+
+ /**
+ * Whether the attribute name where to find a group's member holds ambiguous values
+ *
+ * @var bool
+ */
+ protected $ambiguousMemberAttribute;
+
+ /**
+ * The custom LDAP filter to apply on a user query
+ *
+ * @var string
+ */
+ protected $userFilter;
+
+ /**
+ * The custom LDAP filter to apply on a group query
+ *
+ * @var string
+ */
+ protected $groupFilter;
+
+ /**
+ * ActiveDirectory nested group on the user?
+ *
+ * @var bool
+ */
+ protected $nestedGroupSearch;
+
+ /**
+ * The domain the backend is responsible for
+ *
+ * @var string
+ */
+ protected $domain;
+
+ /**
+ * The columns which are not permitted to be queried
+ *
+ * @var array
+ */
+ protected $blacklistedQueryColumns = array('group', 'user');
+
+ /**
+ * The search columns being provided
+ *
+ * @var array
+ */
+ protected $searchColumns = array('group', 'user');
+
+ /**
+ * The default sort rules to be applied on a query
+ *
+ * @var array
+ */
+ protected $sortRules = array(
+ 'group_name' => array(
+ 'order' => 'asc'
+ )
+ );
+
+ /**
+ * Set the user backend to be associated with this user group backend
+ *
+ * @param LdapUserBackend $backend
+ *
+ * @return $this
+ */
+ public function setUserBackend(LdapUserBackend $backend)
+ {
+ $this->userBackend = $backend;
+ return $this;
+ }
+
+ /**
+ * Return the user backend being associated with this user group backend
+ *
+ * @return LdapUserBackend
+ */
+ public function getUserBackend()
+ {
+ return $this->userBackend;
+ }
+
+ /**
+ * Set the base DN to use for a user query
+ *
+ * @param string $baseDn
+ *
+ * @return $this
+ */
+ public function setUserBaseDn($baseDn)
+ {
+ if ($baseDn && ($baseDn = trim($baseDn))) {
+ $this->userBaseDn = $baseDn;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the base DN to use for a user query
+ *
+ * @return string
+ */
+ public function getUserBaseDn()
+ {
+ return $this->userBaseDn;
+ }
+
+ /**
+ * Set the base DN to use for a group query
+ *
+ * @param string $baseDn
+ *
+ * @return $this
+ */
+ public function setGroupBaseDn($baseDn)
+ {
+ if ($baseDn && ($baseDn = trim($baseDn))) {
+ $this->groupBaseDn = $baseDn;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the base DN to use for a group query
+ *
+ * @return string
+ */
+ public function getGroupBaseDn()
+ {
+ return $this->groupBaseDn;
+ }
+
+ /**
+ * Set the objectClass where to look for users
+ *
+ * @param string $userClass
+ *
+ * @return $this
+ */
+ public function setUserClass($userClass)
+ {
+ $this->userClass = $this->getNormedAttribute($userClass);
+ return $this;
+ }
+
+ /**
+ * Return the objectClass where to look for users
+ *
+ * @return string
+ */
+ public function getUserClass()
+ {
+ return $this->userClass;
+ }
+
+ /**
+ * Set the objectClass where to look for groups
+ *
+ * @param string $groupClass
+ *
+ * @return $this
+ */
+ public function setGroupClass($groupClass)
+ {
+ $this->groupClass = $this->getNormedAttribute($groupClass);
+ return $this;
+ }
+
+ /**
+ * Return the objectClass where to look for groups
+ *
+ * @return string
+ */
+ public function getGroupClass()
+ {
+ return $this->groupClass;
+ }
+
+ /**
+ * Set the attribute name where to find a user's name
+ *
+ * @param string $userNameAttribute
+ *
+ * @return $this
+ */
+ public function setUserNameAttribute($userNameAttribute)
+ {
+ $this->userNameAttribute = $this->getNormedAttribute($userNameAttribute);
+ return $this;
+ }
+
+ /**
+ * Return the attribute name where to find a user's name
+ *
+ * @return string
+ */
+ public function getUserNameAttribute()
+ {
+ return $this->userNameAttribute;
+ }
+
+ /**
+ * Set the attribute name where to find a group's name
+ *
+ * @param string $groupNameAttribute
+ *
+ * @return $this
+ */
+ public function setGroupNameAttribute($groupNameAttribute)
+ {
+ $this->groupNameAttribute = $this->getNormedAttribute($groupNameAttribute);
+ return $this;
+ }
+
+ /**
+ * Return the attribute name where to find a group's name
+ *
+ * @return string
+ */
+ public function getGroupNameAttribute()
+ {
+ return $this->groupNameAttribute;
+ }
+
+ /**
+ * Set the attribute name where to find a group's member
+ *
+ * @param string $groupMemberAttribute
+ *
+ * @return $this
+ */
+ public function setGroupMemberAttribute($groupMemberAttribute)
+ {
+ $this->groupMemberAttribute = $this->getNormedAttribute($groupMemberAttribute);
+ return $this;
+ }
+
+ /**
+ * Return the attribute name where to find a group's member
+ *
+ * @return string
+ */
+ public function getGroupMemberAttribute()
+ {
+ return $this->groupMemberAttribute;
+ }
+
+ /**
+ * Set the custom LDAP filter to apply on a user query
+ *
+ * @param string $filter
+ *
+ * @return $this
+ */
+ public function setUserFilter($filter)
+ {
+ if ($filter && ($filter = trim($filter))) {
+ if ($filter[0] === '(') {
+ $filter = substr($filter, 1, -1);
+ }
+
+ $this->userFilter = $filter;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the custom LDAP filter to apply on a user query
+ *
+ * @return string
+ */
+ public function getUserFilter()
+ {
+ return $this->userFilter;
+ }
+
+ /**
+ * Set the custom LDAP filter to apply on a group query
+ *
+ * @param string $filter
+ *
+ * @return $this
+ */
+ public function setGroupFilter($filter)
+ {
+ if ($filter && ($filter = trim($filter))) {
+ $this->groupFilter = $filter;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the custom LDAP filter to apply on a group query
+ *
+ * @return string
+ */
+ public function getGroupFilter()
+ {
+ return $this->groupFilter;
+ }
+
+ /**
+ * Set nestedGroupSearch for the group query
+ *
+ * @param bool $enable
+ *
+ * @return $this
+ */
+ public function setNestedGroupSearch($enable = true)
+ {
+ $this->nestedGroupSearch = $enable;
+ return $this;
+ }
+
+ /**
+ * Get nestedGroupSearch for the group query
+ *
+ * @return bool
+ */
+ public function getNestedGroupSearch()
+ {
+ return $this->nestedGroupSearch;
+ }
+
+ /**
+ * Get the domain the backend is responsible for
+ *
+ * If the LDAP group backend is linked with a LDAP user backend,
+ * the domain of the user backend will be returned.
+ *
+ * @return string
+ */
+ public function getDomain()
+ {
+ return $this->userBackend !== null ? $this->userBackend->getDomain() : $this->domain;
+ }
+
+ /**
+ * Set the domain the backend is responsible for
+ *
+ * If the LDAP group backend is linked with a LDAP user backend,
+ * the domain of the user backend will be used nonetheless.
+ *
+ * @param string $domain
+ *
+ * @return $this
+ */
+ public function setDomain($domain)
+ {
+ if ($domain && ($domain = trim($domain))) {
+ $this->domain = $domain;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return whether the attribute name where to find a group's member holds ambiguous values
+ *
+ * This tries to detect if the member attribute of groups contain:
+ *
+ * full DN -> distinguished name of another object
+ * other -> ambiguous field referencing the member by userNameAttribute
+ *
+ * @return bool
+ *
+ * @throws ProgrammingError In case either $this->groupClass or $this->groupMemberAttribute
+ * has not been set yet
+ */
+ protected function isMemberAttributeAmbiguous()
+ {
+ if ($this->ambiguousMemberAttribute === null) {
+ if ($this->groupClass === null) {
+ throw new ProgrammingError(
+ 'It is required to set the objectClass where to look for groups first'
+ );
+ } elseif ($this->groupMemberAttribute === null) {
+ throw new ProgrammingError(
+ 'It is required to set a attribute name where to find a group\'s members first'
+ );
+ }
+
+ $sampleValues = $this->ds
+ ->select()
+ ->from($this->groupClass, array($this->groupMemberAttribute))
+ ->where($this->groupMemberAttribute, '*')
+ ->limit(Logger::getInstance()->getLevel() === Logger::DEBUG ? 3 : 1)
+ ->setUnfoldAttribute($this->groupMemberAttribute)
+ ->setBase($this->groupBaseDn)
+ ->fetchAll();
+
+ Logger::debug('Ambiguity query returned %d results', count($sampleValues));
+
+ $i = 0;
+ $sampleValue = null;
+ foreach ($sampleValues as $key => $value) {
+ if ($sampleValue === null) {
+ $sampleValue = $value;
+ }
+
+ Logger::debug('Result %d: %s (%s)', ++$i, $value, $key);
+ }
+
+ if (is_object($sampleValue) && isset($sampleValue->{$this->groupMemberAttribute})) {
+ $this->ambiguousMemberAttribute = ! LdapUtils::isDn($sampleValue->{$this->groupMemberAttribute});
+
+ Logger::debug(
+ 'Ambiguity check came to the conclusion that the member attribute %s ambiguous. Tested sample: %s',
+ $this->ambiguousMemberAttribute ? 'is' : 'is not',
+ $sampleValue->{$this->groupMemberAttribute}
+ );
+ } else {
+ Logger::warning(
+ 'Ambiguity query returned zero or invalid results. Sample value is `%s`',
+ print_r($sampleValue, true)
+ );
+ }
+ }
+
+ return $this->ambiguousMemberAttribute;
+ }
+
+ /**
+ * Initialize this repository's virtual tables
+ *
+ * @return array
+ *
+ * @throws ProgrammingError In case $this->groupClass has not been set yet
+ */
+ protected function initializeVirtualTables()
+ {
+ if ($this->groupClass === null) {
+ throw new ProgrammingError('It is required to set the object class where to find groups first');
+ }
+
+ return array(
+ 'group' => $this->groupClass,
+ 'group_membership' => $this->groupClass
+ );
+ }
+
+ /**
+ * Initialize this repository's query columns
+ *
+ * @return array
+ *
+ * @throws ProgrammingError In case either $this->groupNameAttribute or
+ * $this->groupMemberAttribute has not been set yet
+ */
+ protected function initializeQueryColumns()
+ {
+ if ($this->groupNameAttribute === null) {
+ throw new ProgrammingError('It is required to set a attribute name where to find a group\'s name first');
+ }
+ if ($this->groupMemberAttribute === null) {
+ throw new ProgrammingError('It is required to set a attribute name where to find a group\'s members first');
+ }
+
+ if ($this->ds->getCapabilities()->isActiveDirectory()) {
+ $createdAtAttribute = 'whenCreated';
+ $lastModifiedAttribute = 'whenChanged';
+ } else {
+ $createdAtAttribute = 'createTimestamp';
+ $lastModifiedAttribute = 'modifyTimestamp';
+ }
+
+ $columns = array(
+ 'group' => $this->groupNameAttribute,
+ 'group_name' => $this->groupNameAttribute,
+ 'user' => $this->groupMemberAttribute,
+ 'user_name' => $this->groupMemberAttribute,
+ 'created_at' => $createdAtAttribute,
+ 'last_modified' => $lastModifiedAttribute
+ );
+ return array('group' => $columns, 'group_membership' => $columns);
+ }
+
+ /**
+ * Initialize this repository's filter columns
+ *
+ * @return array
+ */
+ protected function initializeFilterColumns()
+ {
+ return array(
+ t('Username') => 'user_name',
+ t('User Group') => 'group_name',
+ t('Created At') => 'created_at',
+ t('Last modified') => 'last_modified'
+ );
+ }
+
+ /**
+ * Initialize this repository's conversion rules
+ *
+ * @return array
+ */
+ protected function initializeConversionRules()
+ {
+ $rules = array(
+ 'group' => array(
+ 'created_at' => 'generalized_time',
+ 'last_modified' => 'generalized_time'
+ ),
+ 'group_membership' => array(
+ 'created_at' => 'generalized_time',
+ 'last_modified' => 'generalized_time'
+ )
+ );
+ if (! $this->isMemberAttributeAmbiguous()) {
+ $rules['group_membership']['user_name'] = 'user_name';
+ $rules['group_membership']['user'] = 'user_name';
+ $rules['group']['user_name'] = 'user_name';
+ $rules['group']['user'] = 'user_name';
+ }
+
+ return $rules;
+ }
+
+ /**
+ * Return the distinguished name for the given uid or gid
+ *
+ * @param string $name
+ *
+ * @return string
+ */
+ protected function persistUserName($name)
+ {
+ try {
+ $userDn = $this->ds
+ ->select()
+ ->from($this->userClass, array())
+ ->where($this->userNameAttribute, $name)
+ ->setBase($this->userBaseDn)
+ ->setUsePagedResults(false)
+ ->fetchDn();
+ if ($userDn) {
+ return $userDn;
+ }
+
+ $groupDn = $this->ds
+ ->select()
+ ->from($this->groupClass, array())
+ ->where($this->groupNameAttribute, $name)
+ ->setBase($this->groupBaseDn)
+ ->setUsePagedResults(false)
+ ->fetchDn();
+ if ($groupDn) {
+ return $groupDn;
+ }
+ } catch (LdapException $_) {
+ // pass
+ }
+
+ Logger::debug('Unable to persist uid or gid "%s" in repository "%s". No DN found.', $name, $this->getName());
+ return $name;
+ }
+
+ /**
+ * Return the uid for the given distinguished name
+ *
+ * @param string $username
+ *
+ * @return string
+ */
+ protected function retrieveUserName($dn)
+ {
+ return $this->ds
+ ->select()
+ ->from('*', array($this->userNameAttribute))
+ ->setUnfoldAttribute($this->userNameAttribute)
+ ->setBase($dn)
+ ->fetchOne();
+ }
+
+ /**
+ * Validate that the requested table exists
+ *
+ * @param string $table The table to validate
+ * @param RepositoryQuery $query An optional query to pass as context
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case the given table does not exist
+ */
+ public function requireTable($table, RepositoryQuery $query = null)
+ {
+ if ($query !== null) {
+ $query->getQuery()->setBase($this->groupBaseDn);
+ if ($table === 'group' && $this->groupFilter) {
+ $query->getQuery()->setNativeFilter($this->groupFilter);
+ }
+ }
+
+ return parent::requireTable($table, $query);
+ }
+
+ /**
+ * Validate that the given column is a valid query target and return it or the actual name if it's an alias
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The name or alias of the column to validate
+ * @param RepositoryQuery $query An optional query to pass as context
+ *
+ * @return string The given column's name
+ *
+ * @throws QueryException In case the given column is not a valid query column
+ */
+ public function requireQueryColumn($table, $name, RepositoryQuery $query = null)
+ {
+ $column = parent::requireQueryColumn($table, $name, $query);
+ if (($name === 'user_name' || $name === 'group_name') && $query !== null) {
+ $query->getQuery()->setUnfoldAttribute($name);
+ }
+
+ return $column;
+ }
+
+ /**
+ * Return the groups the given user is a member of
+ *
+ * @param User $user
+ *
+ * @return array
+ */
+ public function getMemberships(User $user)
+ {
+ $domain = $this->getDomain();
+
+ if ($domain !== null) {
+ if (! $user->hasDomain() || strtolower($user->getDomain()) !== strtolower($domain)) {
+ return array();
+ }
+
+ $username = $user->getLocalUsername();
+ } else {
+ $username = $user->getUsername();
+ }
+
+ if ($this->isMemberAttributeAmbiguous()) {
+ $queryValue = $username;
+ } elseif (($queryValue = $user->getAdditional('ldap_dn')) === null) {
+ $userQuery = $this->ds
+ ->select()
+ ->from($this->userClass)
+ ->where($this->userNameAttribute, $username)
+ ->setBase($this->userBaseDn)
+ ->setUsePagedResults(false);
+ if ($this->userFilter) {
+ $userQuery->setNativeFilter($this->userFilter);
+ }
+
+ if (($queryValue = $userQuery->fetchDn()) === null) {
+ return array();
+ }
+ }
+
+ if ($this->nestedGroupSearch) {
+ $groupMemberAttribute = $this->groupMemberAttribute . ':1.2.840.113556.1.4.1941:';
+ } else {
+ $groupMemberAttribute = $this->groupMemberAttribute;
+ }
+
+ $groupQuery = $this->ds
+ ->select()
+ ->from($this->groupClass, array($this->groupNameAttribute))
+ ->setUnfoldAttribute($this->groupNameAttribute)
+ ->where($groupMemberAttribute, $queryValue)
+ ->setBase($this->groupBaseDn);
+ if ($this->groupFilter) {
+ $groupQuery->setNativeFilter($this->groupFilter);
+ }
+
+ $groups = array();
+ foreach ($groupQuery as $row) {
+ $groups[] = $row->{$this->groupNameAttribute};
+ if ($domain !== null) {
+ $groups[] = $row->{$this->groupNameAttribute} . "@$domain";
+ }
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Return the name of the backend that is providing the given user
+ *
+ * @param string $username Unused
+ *
+ * @return null|string The name of the backend or null in case this information is not available
+ */
+ public function getUserBackendName($username)
+ {
+ $userBackend = $this->getUserBackend();
+ if ($userBackend !== null) {
+ return $userBackend->getName();
+ }
+ }
+
+ /**
+ * Apply the given configuration on this backend
+ *
+ * @param ConfigObject $config
+ *
+ * @return $this
+ *
+ * @throws ConfigurationError In case a linked user backend does not exist or is invalid
+ */
+ public function setConfig(ConfigObject $config)
+ {
+ if ($config->backend === 'ldap') {
+ $defaults = $this->getOpenLdapDefaults();
+ } elseif ($config->backend === 'msldap') {
+ $defaults = $this->getActiveDirectoryDefaults();
+ } else {
+ $defaults = new ConfigObject();
+ }
+
+ if ($config->user_backend && $config->user_backend !== 'none') {
+ $userBackend = UserBackend::create($config->user_backend);
+ if (! $userBackend instanceof LdapUserBackend) {
+ throw new ConfigurationError('User backend "%s" is not of type LDAP', $config->user_backend);
+ }
+
+ if ($this->ds->getHostname() !== $userBackend->getDataSource()->getHostname()
+ || $this->ds->getPort() !== $userBackend->getDataSource()->getPort()
+ ) {
+ // TODO(jom): Elaborate whether it makes sense to link directories on different hosts
+ throw new ConfigurationError(
+ 'It is required that a linked user backend refers to the '
+ . 'same directory as it\'s user group backend counterpart'
+ );
+ }
+
+ $this->setUserBackend($userBackend);
+ $defaults->merge(array(
+ 'user_base_dn' => $userBackend->getBaseDn(),
+ 'user_class' => $userBackend->getUserClass(),
+ 'user_name_attribute' => $userBackend->getUserNameAttribute(),
+ 'user_filter' => $userBackend->getFilter(),
+ 'domain' => $userBackend->getDomain()
+ ));
+ }
+
+ return $this
+ ->setGroupBaseDn($config->base_dn)
+ ->setUserBaseDn($config->get('user_base_dn', $defaults->get('user_base_dn', $this->getGroupBaseDn())))
+ ->setGroupClass($config->get('group_class', $defaults->group_class))
+ ->setUserClass($config->get('user_class', $defaults->user_class))
+ ->setGroupNameAttribute($config->get('group_name_attribute', $defaults->group_name_attribute))
+ ->setUserNameAttribute($config->get('user_name_attribute', $defaults->user_name_attribute))
+ ->setGroupMemberAttribute($config->get('group_member_attribute', $defaults->group_member_attribute))
+ ->setGroupFilter($config->group_filter)
+ ->setUserFilter($config->user_filter)
+ ->setNestedGroupSearch((bool) $config->get('nested_group_search', $defaults->nested_group_search))
+ ->setDomain($defaults->get('domain', $config->domain));
+ }
+
+ /**
+ * Return the configuration defaults for an OpenLDAP environment
+ *
+ * @return ConfigObject
+ */
+ public function getOpenLdapDefaults()
+ {
+ return new ConfigObject(array(
+ 'group_class' => 'group',
+ 'user_class' => 'inetOrgPerson',
+ 'group_name_attribute' => 'gid',
+ 'user_name_attribute' => 'uid',
+ 'group_member_attribute' => 'member',
+ 'nested_group_search' => '0'
+ ));
+ }
+
+ /**
+ * Return the configuration defaults for an ActiveDirectory environment
+ *
+ * @return ConfigObject
+ */
+ public function getActiveDirectoryDefaults()
+ {
+ return new ConfigObject(array(
+ 'group_class' => 'group',
+ 'user_class' => 'user',
+ 'group_name_attribute' => 'sAMAccountName',
+ 'user_name_attribute' => 'sAMAccountName',
+ 'group_member_attribute' => 'member',
+ 'nested_group_search' => '0'
+ ));
+ }
+
+ /**
+ * Inspect if this LDAP User Group Backend is working as expected by probing the backend
+ *
+ * Try to bind to the backend and fetch a single group to check if:
+ * <ul>
+ * <li>Connection credentials are correct and the bind is possible</li>
+ * <li>At least one group exists</li>
+ * <li>The specified groupClass has the property specified by groupNameAttribute</li>
+ * </ul>
+ *
+ * @return Inspection Inspection result
+ */
+ public function inspect()
+ {
+ $result = new Inspection('Ldap User Group Backend');
+
+ // inspect the used connection to get more diagnostic info in case the connection is not working
+ $result->write($this->ds->inspect());
+
+ try {
+ try {
+ $groupQuery = $this->ds
+ ->select()
+ ->from($this->groupClass, array($this->groupNameAttribute))
+ ->setBase($this->groupBaseDn);
+
+ if ($this->groupFilter) {
+ $groupQuery->setNativeFilter($this->groupFilter);
+ }
+
+ $res = $groupQuery->fetchRow();
+ } catch (LdapException $e) {
+ throw new AuthenticationException('Connection not possible', $e);
+ }
+
+ $result->write('Searching for: ' . sprintf(
+ 'objectClass "%s" in DN "%s" (Filter: %s)',
+ $this->groupClass,
+ $this->groupBaseDn ?: $this->ds->getDn(),
+ $this->groupFilter ?: 'None'
+ ));
+
+ if ($res === false) {
+ throw new AuthenticationException('Error, no groups found in backend');
+ }
+
+ $result->write(sprintf('%d groups found in backend', $groupQuery->count()));
+
+ if (! isset($res->{$this->groupNameAttribute})) {
+ throw new AuthenticationException(
+ 'GroupNameAttribute "%s" not existing in objectClass "%s"',
+ $this->groupNameAttribute,
+ $this->groupClass
+ );
+ }
+ } catch (AuthenticationException $e) {
+ if (($previous = $e->getPrevious()) !== null) {
+ $result->error($previous->getMessage());
+ } else {
+ $result->error($e->getMessage());
+ }
+ } catch (Exception $e) {
+ $result->error(sprintf('Unable to validate backend: %s', $e->getMessage()));
+ }
+
+ return $result;
+ }
+}
diff --git a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php
new file mode 100644
index 0000000..7f0bfcc
--- /dev/null
+++ b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php
@@ -0,0 +1,189 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication\UserGroup;
+
+use Icinga\Application\Logger;
+use Icinga\Application\Icinga;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+
+/**
+ * Factory for user group backends
+ */
+class UserGroupBackend
+{
+ /**
+ * The default user group backend types provided by Icinga Web 2
+ *
+ * @var array
+ */
+ protected static $defaultBackends = array(
+ 'db',
+ 'ldap',
+ 'msldap'
+ );
+
+ /**
+ * The registered custom user group backends with their identifier as key and class name as value
+ *
+ * @var array
+ */
+ protected static $customBackends;
+
+ /**
+ * Register all custom user group backends from all loaded modules
+ */
+ public static function registerCustomUserGroupBackends()
+ {
+ if (static::$customBackends !== null) {
+ return;
+ }
+
+ static::$customBackends = array();
+ $providedBy = array();
+ foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) {
+ foreach ($module->getUserGroupBackends() as $identifier => $className) {
+ if (array_key_exists($identifier, $providedBy)) {
+ Logger::warning(
+ 'Cannot register user group backend of type "%s" provided by module "%s".'
+ . ' The type is already provided by module "%s"',
+ $identifier,
+ $module->getName(),
+ $providedBy[$identifier]
+ );
+ } elseif (in_array($identifier, static::$defaultBackends)) {
+ Logger::warning(
+ 'Cannot register user group backend of type "%s" provided by module "%s".'
+ . ' The type is a default type provided by Icinga Web 2',
+ $identifier,
+ $module->getName()
+ );
+ } else {
+ $providedBy[$identifier] = $module->getName();
+ static::$customBackends[$identifier] = $className;
+ }
+ }
+ }
+ }
+
+ /**
+ * Get config forms of all custom user group backends
+ */
+ public static function getCustomBackendConfigForms()
+ {
+ $customBackendConfigForms = [];
+ static::registerCustomUserGroupBackends();
+ foreach (self::$customBackends as $customBackendType => $customBackendClass) {
+ if (method_exists($customBackendClass, 'getConfigurationFormClass')) {
+ $customBackendConfigForms[$customBackendType] = $customBackendClass::getConfigurationFormClass();
+ }
+ }
+
+ return $customBackendConfigForms;
+ }
+
+ /**
+ * Return the class for the given custom user group backend
+ *
+ * @param string $identifier The identifier of the custom user group backend
+ *
+ * @return string|null The name of the class or null in case there was no
+ * backend found with the given identifier
+ *
+ * @throws ConfigurationError In case the class associated to the given identifier does not exist
+ */
+ protected static function getCustomUserGroupBackend($identifier)
+ {
+ static::registerCustomUserGroupBackends();
+ if (array_key_exists($identifier, static::$customBackends)) {
+ $className = static::$customBackends[$identifier];
+ if (! class_exists($className)) {
+ throw new ConfigurationError(
+ 'Cannot utilize user group backend of type "%s". Class "%s" does not exist',
+ $identifier,
+ $className
+ );
+ }
+
+ return $className;
+ }
+ }
+
+ /**
+ * Create and return a user group backend with the given name and given configuration applied to it
+ *
+ * @param string $name
+ * @param ConfigObject $backendConfig
+ *
+ * @return UserGroupBackendInterface
+ *
+ * @throws ConfigurationError
+ */
+ public static function create($name, ConfigObject $backendConfig)
+ {
+ if ($backendConfig->name !== null) {
+ $name = $backendConfig->name;
+ }
+
+ if (! ($backendType = strtolower($backendConfig->backend))) {
+ throw new ConfigurationError(
+ 'Configuration for user group backend "%s" is missing the \'backend\' directive',
+ $name
+ );
+ }
+ if (in_array($backendType, static::$defaultBackends)) {
+ // The default backend check is the first one because of performance reasons:
+ // Do not attempt to load a custom user group backend unless it's actually required
+ } elseif (($customClass = static::getCustomUserGroupBackend($backendType)) !== null) {
+ $backend = new $customClass($backendConfig);
+ if (! is_a($backend, 'Icinga\Authentication\UserGroup\UserGroupBackendInterface')) {
+ throw new ConfigurationError(
+ 'Cannot utilize user group backend of type "%s".'
+ . ' Class "%s" does not implement UserGroupBackendInterface',
+ $backendType,
+ $customClass
+ );
+ }
+
+ $backend->setName($name);
+ return $backend;
+ } else {
+ throw new ConfigurationError(
+ 'Configuration for user group backend "%s" defines an invalid backend type.'
+ . ' Backend type "%s" is not supported',
+ $name,
+ $backendType
+ );
+ }
+
+ if ($backendConfig->resource === null) {
+ throw new ConfigurationError(
+ 'Configuration for user group backend "%s" is missing the \'resource\' directive',
+ $name
+ );
+ }
+
+ $resourceConfig = ResourceFactory::getResourceConfig($backendConfig->resource);
+ if ($backendType === 'db' && $resourceConfig->db === 'mysql') {
+ $resourceConfig->charset = 'utf8mb4';
+ }
+
+ $backend = null;
+ $resource = ResourceFactory::createResource($resourceConfig);
+ switch ($backendType) {
+ case 'db':
+ $backend = new DbUserGroupBackend($resource);
+ break;
+ case 'ldap':
+ case 'msldap':
+ $backend = new LdapUserGroupBackend($resource);
+ $backend->setConfig($backendConfig);
+ break;
+ }
+
+ $backend->setName($name);
+ return $backend;
+ }
+}
diff --git a/library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php b/library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php
new file mode 100644
index 0000000..cc9438f
--- /dev/null
+++ b/library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php
@@ -0,0 +1,56 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Authentication\UserGroup;
+
+use Icinga\User;
+
+/**
+ * Interface for user group backends
+ */
+interface UserGroupBackendInterface
+{
+ /**
+ * Set this backend's name
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name);
+
+ /**
+ * Return this backend's name
+ *
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Return the groups the given user is a member of
+ *
+ * @param User $user
+ *
+ * @return array
+ */
+ public function getMemberships(User $user);
+
+ /**
+ * Return the name of the backend that is providing the given user
+ *
+ * @param string $username
+ *
+ * @return null|string The name of the backend or null in case this information is not available
+ */
+ public function getUserBackendName($username);
+
+ /**
+ * Return this backend's configuration form class path
+ *
+ * This is not part of the interface to not break existing implementations.
+ * If you need a custom backend form, implement this method.
+ *
+ * @return string
+ */
+ //public static function getConfigurationFormClass();
+}
diff --git a/library/Icinga/Chart/Axis.php b/library/Icinga/Chart/Axis.php
new file mode 100644
index 0000000..1639939
--- /dev/null
+++ b/library/Icinga/Chart/Axis.php
@@ -0,0 +1,485 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+use DOMElement;
+use Icinga\Chart\Primitive\Drawable;
+use Icinga\Chart\Primitive\Line;
+use Icinga\Chart\Primitive\Text;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Render\Rotator;
+use Icinga\Chart\Unit\AxisUnit;
+use Icinga\Chart\Unit\CalendarUnit;
+use Icinga\Chart\Unit\LinearUnit;
+
+/**
+ * Axis class for the GridChart class.
+ *
+ * Implements drawing functions for the axis and its labels but delegates tick and label calculations
+ * to the AxisUnit implementations
+ *
+ * @see GridChart
+ * @see AxisUnit
+ */
+class Axis implements Drawable
+{
+ /**
+ * Draw the label text horizontally
+ */
+ const LABEL_ROTATE_HORIZONTAL = 'normal';
+
+ /**
+ * Draw the label text diagonally
+ */
+ const LABEL_ROTATE_DIAGONAL = 'diagonal';
+
+ /**
+ * Whether to draw the horizontal lines for the background grid
+ *
+ * @var bool
+ */
+ private $drawXGrid = true;
+
+ /**
+ * Whether to draw the vertical lines for the background grid
+ *
+ * @var bool
+ */
+ private $drawYGrid = true;
+
+ /**
+ * The label for the x axis
+ *
+ * @var string
+ */
+ private $xLabel = "";
+
+ /**
+ * The label for the y axis
+ *
+ * @var string
+ */
+ private $yLabel = "";
+
+ /**
+ * The AxisUnit implementation to use for calculating the ticks for the x axis
+ *
+ * @var AxisUnit
+ */
+ private $xUnit = null;
+
+ /**
+ * The AxisUnit implementation to use for calculating the ticks for the y axis
+ *
+ * @var AxisUnit
+ */
+ private $yUnit = null;
+
+ /**
+ * The minimum amount of units each step must take up
+ *
+ * @var int
+ */
+ public $minUnitsPerStep = 80;
+
+ /**
+ * The minimum amount of units each tick must take up
+ *
+ * @var int
+ */
+ public $minUnitsPerTick = 15;
+
+ /**
+ * If the displayed labels should be aligned horizontally or diagonally
+ */
+ protected $labelRotationStyle = self::LABEL_ROTATE_HORIZONTAL;
+
+ /**
+ * Inform the axis about an added dataset
+ *
+ * This is especially needed when one or more AxisUnit implementations dynamically define
+ * their min or max values, as this is the point where they detect the min and max value
+ * from the datasets
+ *
+ * @param array $dataset An dataset to respect on axis generation
+ */
+ public function addDataset(array $dataset)
+ {
+ $this->xUnit->addValues($dataset, 0);
+ $this->yUnit->addValues($dataset, 1);
+ }
+
+ /**
+ * Set the AxisUnit implementation to use for generating the x axis
+ *
+ * @param AxisUnit $unit The AxisUnit implementation to use for the x axis
+ *
+ * @return $this This Axis Object
+ * @see Axis::CalendarUnit
+ * @see Axis::LinearUnit
+ */
+ public function setUnitForXAxis(AxisUnit $unit)
+ {
+ $this->xUnit = $unit;
+ return $this;
+ }
+
+ /**
+ * Set the AxisUnit implementation to use for generating the y axis
+ *
+ * @param AxisUnit $unit The AxisUnit implementation to use for the y axis
+ *
+ * @return $this This Axis Object
+ * @see Axis::CalendarUnit
+ * @see Axis::LinearUnit
+ */
+ public function setUnitForYAxis(AxisUnit $unit)
+ {
+ $this->yUnit = $unit;
+ return $this;
+ }
+
+ /**
+ * Return the padding this axis requires
+ *
+ * @return array An array containing the padding for all sides
+ */
+ public function getRequiredPadding()
+ {
+ return array(10, 5, 15, 10);
+ }
+
+ /**
+ * Render the horizontal axis
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ * @param DOMElement $group The DOMElement this axis will be added to
+ */
+ private function renderHorizontalAxis(RenderContext $ctx, DOMElement $group)
+ {
+ $steps = $this->ticksPerX($this->xUnit->getTicks(), $ctx->getNrOfUnitsX(), $this->minUnitsPerStep);
+ $ticks = $this->ticksPerX($this->xUnit->getTicks(), $ctx->getNrOfUnitsX(), $this->minUnitsPerTick);
+
+ // Steps should always be ticks
+ if ($ticks !== $steps) {
+ $steps = $ticks * 5;
+ }
+
+ // Check whether there is enough room for regular labels
+ $labelRotationStyle = $this->labelRotationStyle;
+ if ($this->labelsOversized($this->xUnit, 6)) {
+ $labelRotationStyle = self::LABEL_ROTATE_DIAGONAL;
+ }
+
+ /*
+ $line = new Line(0, 100, 100, 100);
+ $line->setStrokeWidth(2);
+ $group->appendChild($line->toSvg($ctx));
+ */
+
+ // contains the approximate end position of the last label
+ $lastLabelEnd = -1;
+ $shift = 0;
+
+ $i = 0;
+ foreach ($this->xUnit as $label => $pos) {
+ if ($i % $ticks === 0) {
+ /*
+ $tick = new Line($pos, 100, $pos, 101);
+ $group->appendChild($tick->toSvg($ctx));
+ */
+ }
+
+ if ($i % $steps === 0) {
+ if ($labelRotationStyle === self::LABEL_ROTATE_HORIZONTAL) {
+ // If the last label would overlap this label we shift the y axis a bit
+ if ($lastLabelEnd > $pos) {
+ $shift = ($shift + 5) % 10;
+ } else {
+ $shift = 0;
+ }
+ }
+
+ $labelField = new Text($pos + 0.5, ($this->xLabel ? 107 : 105) + $shift, $label);
+ if ($labelRotationStyle === self::LABEL_ROTATE_HORIZONTAL) {
+ $labelField->setAlignment(Text::ALIGN_MIDDLE)
+ ->setFontSize('2.5em');
+ } else {
+ $labelField->setFontSize('2.5em');
+ }
+
+ if ($labelRotationStyle === self::LABEL_ROTATE_DIAGONAL) {
+ $labelField = new Rotator($labelField, 45);
+ }
+ $labelField = $labelField->toSvg($ctx);
+
+ $group->appendChild($labelField);
+
+ if ($this->drawYGrid) {
+ $bgLine = new Line($pos, 0, $pos, 100);
+ $bgLine->setStrokeWidth(0.5)
+ ->setStrokeColor('#BFBFBF');
+ $group->appendChild($bgLine->toSvg($ctx));
+ }
+ $lastLabelEnd = $pos + strlen($label) * 1.2;
+ }
+ $i++;
+ }
+ }
+
+ /**
+ * Render the vertical axis
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ * @param DOMElement $group The DOMElement this axis will be added to
+ */
+ private function renderVerticalAxis(RenderContext $ctx, DOMElement $group)
+ {
+ $steps = $this->ticksPerX($this->yUnit->getTicks(), $ctx->getNrOfUnitsY(), $this->minUnitsPerStep);
+ $ticks = $this->ticksPerX($this->yUnit->getTicks(), $ctx->getNrOfUnitsY(), $this->minUnitsPerTick);
+
+ // Steps should always be ticks
+ if ($ticks !== $steps) {
+ $steps = $ticks * 5;
+ }
+ /*
+ $line = new Line(0, 0, 0, 100);
+ $line->setStrokeWidth(2);
+ $group->appendChild($line->toSvg($ctx));
+ */
+
+ $i = 0;
+ foreach ($this->yUnit as $label => $pos) {
+ $pos = 100 - $pos;
+
+ if ($i % $ticks === 0) {
+ // draw a tick
+ //$tick = new Line(0, $pos, -1, $pos);
+ //$group->appendChild($tick->toSvg($ctx));
+ }
+
+ if ($i % $steps === 0) {
+ // draw a step
+ $labelField = new Text(-0.5, $pos + 0.5, $label);
+ $labelField->setFontSize('2.5em')
+ ->setAlignment(Text::ALIGN_END);
+
+ $group->appendChild($labelField->toSvg($ctx));
+ if ($this->drawXGrid) {
+ $bgLine = new Line(0, $pos, 100, $pos);
+ $bgLine->setStrokeWidth(0.5)
+ ->setStrokeColor('#BFBFBF');
+ $group->appendChild($bgLine->toSvg($ctx));
+ }
+ }
+ $i++;
+ }
+
+ if ($this->yLabel || $this->xLabel) {
+ if ($this->yLabel && $this->xLabel) {
+ $txt = $this->yLabel . ' / ' . $this->xLabel;
+ } elseif ($this->xLabel) {
+ $txt = $this->xLabel;
+ } else {
+ $txt = $this->yLabel;
+ }
+
+ $axisLabel = new Text(50, -3, $txt);
+ $axisLabel->setFontSize('2em')
+ ->setFontWeight('bold')
+ ->setAlignment(Text::ALIGN_MIDDLE);
+
+ $group->appendChild($axisLabel->toSvg($ctx));
+ }
+ }
+
+ /**
+ * Factory method, create an Axis instance using Linear ticks as the unit
+ *
+ * @return Axis The axis that has been created
+ * @see LinearUnit
+ */
+ public static function createLinearAxis()
+ {
+ $axis = new Axis();
+ $axis->setUnitForXAxis(self::linearUnit());
+ $axis->setUnitForYAxis(self::linearUnit());
+ return $axis;
+ }
+
+ /**
+ * Set the label for the x axis
+ *
+ * An empty string means 'no label'.
+ *
+ * @param string $label The label to use for the x axis
+ *
+ * @return $this Fluid interface
+ */
+ public function setXLabel($label)
+ {
+ $this->xLabel = $label;
+ return $this;
+ }
+
+ /**
+ * Set the label for the y axis
+ *
+ * An empty string means 'no label'.
+ *
+ * @param string $label The label to use for the y axis
+ *
+ * @return $this Fluid interface
+ */
+ public function setYLabel($label)
+ {
+ $this->yLabel = $label;
+ return $this;
+ }
+
+ /**
+ * Set the labels minimum value for the x axis
+ *
+ * Setting the value to null let's the axis unit decide which value to use for the minimum
+ *
+ * @param int $xMin The minimum value to use for the x axis
+ *
+ * @return $this Fluid interface
+ */
+ public function setXMin($xMin)
+ {
+ $this->xUnit->setMin($xMin);
+ return $this;
+ }
+
+ /**
+ * Set the labels minimum value for the y axis
+ *
+ * Setting the value to null let's the axis unit decide which value to use for the minimum
+ *
+ * @param int $yMin The minimum value to use for the x axis
+ *
+ * @return $this Fluid interface
+ */
+ public function setYMin($yMin)
+ {
+ $this->yUnit->setMin($yMin);
+ return $this;
+ }
+
+ /**
+ * Set the labels maximum value for the x axis
+ *
+ * Setting the value to null let's the axis unit decide which value to use for the maximum
+ *
+ * @param int $xMax The minimum value to use for the x axis
+ *
+ * @return $this Fluid interface
+ */
+ public function setXMax($xMax)
+ {
+ $this->xUnit->setMax($xMax);
+ return $this;
+ }
+
+ /**
+ * Set the labels maximum value for the y axis
+ *
+ * Setting the value to null let's the axis unit decide which value to use for the maximum
+ *
+ * @param int $yMax The minimum value to use for the y axis
+ *
+ * @return $this Fluid interface
+ */
+ public function setYMax($yMax)
+ {
+ $this->yUnit->setMax($yMax);
+ return $this;
+ }
+
+ /**
+ * Transform all coordinates of the given dataset to coordinates that fit the graph's coordinate system
+ *
+ * @param array $dataSet The absolute coordinates as provided in the draw call
+ *
+ * @return array A graph relative representation of the given coordinates
+ */
+ public function transform(array &$dataSet)
+ {
+ $result = array();
+ foreach ($dataSet as &$points) {
+ $result[] = array(
+ $this->xUnit->transform($points[0]),
+ 100 - $this->yUnit->transform($points[1])
+ );
+ }
+ return $result;
+ }
+
+ /**
+ * Create an AxisUnit that can be used in the axis to represent timestamps
+ *
+ * @return CalendarUnit
+ */
+ public static function calendarUnit()
+ {
+ return new CalendarUnit();
+ }
+
+ /**
+ * Create an AxisUnit that can be used in the axis to represent a dataset as equally distributed
+ * ticks
+ *
+ * @param int $ticks
+ * @return LinearUnit
+ */
+ public static function linearUnit($ticks = 10)
+ {
+ return new LinearUnit($ticks);
+ }
+
+ /**
+ * Return the SVG representation of this object
+ *
+ * @param RenderContext $ctx The context to use for calculations
+ *
+ * @return DOMElement
+ * @see Drawable::toSvg
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $group = $ctx->getDocument()->createElement('g');
+ $this->renderHorizontalAxis($ctx, $group);
+ $this->renderVerticalAxis($ctx, $group);
+ return $group;
+ }
+
+ protected function ticksPerX($ticks, $units, $min)
+ {
+ $per = 1;
+ while ($per * $units / $ticks < $min) {
+ $per++;
+ }
+ return $per;
+ }
+
+ /**
+ * Returns whether at least one label of the given Axis
+ * is bigger than the given maxLength
+ *
+ * @param AxisUnit $axis The axis that contains the labels that will be checked
+ *
+ * @return boolean Whether at least one label is bigger than maxLength
+ */
+ private function labelsOversized(AxisUnit $axis, $maxLength = 5)
+ {
+ $oversized = false;
+ foreach ($axis as $label => $pos) {
+ if (strlen($label) > $maxLength) {
+ $oversized = true;
+ }
+ }
+ return $oversized;
+ }
+}
diff --git a/library/Icinga/Chart/Chart.php b/library/Icinga/Chart/Chart.php
new file mode 100644
index 0000000..eaf69d1
--- /dev/null
+++ b/library/Icinga/Chart/Chart.php
@@ -0,0 +1,162 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+use Imagick;
+use Icinga\Chart\Legend;
+use Icinga\Chart\Palette;
+use Icinga\Chart\Primitive\Drawable;
+use Icinga\Chart\SVGRenderer;
+use Icinga\Exception\IcingaException;
+
+/**
+ * Base class for charts, extended by all other Chart classes.
+ */
+abstract class Chart implements Drawable
+{
+ protected $align = false;
+
+ /**
+ * SVG renderer that handles
+ *
+ * @var SVGRenderer
+ */
+ protected $renderer;
+
+ /**
+ * Legend to use for this chart
+ *
+ * @var Legend
+ */
+ protected $legend;
+
+ /**
+ * The style-palette for this chart
+ *
+ * @var Palette
+ */
+ protected $palette;
+
+ /**
+ * The title of this chart, used for providing accessibility features
+ *
+ * @var string
+ */
+ public $title;
+
+ /**
+ * The description for this chart, mandatory for providing accessibility features
+ *
+ * @var string
+ */
+ public $description;
+
+ /**
+ * Create a new chart object and create internal objects
+ *
+ * If you want to extend this class use the init() method as an extension point,
+ * as this will be called at the end of the construct call
+ */
+ public function __construct()
+ {
+ $this->legend = new Legend();
+ $this->palette = new Palette();
+ $this->init();
+ }
+
+ /**
+ * Extension point for subclasses, called on __construct
+ */
+ protected function init()
+ {
+ }
+
+ /**
+ * Extension point for implementing rendering logic
+ *
+ * This method is called after data validation, but before toSvg is called
+ */
+ protected function build()
+ {
+ }
+
+ /**
+ * Check if the current dataset has the proper structure for this chart.
+ *
+ * Needs to be overwritten by extending classes. The default implementation returns false.
+ *
+ * @return bool True when the dataset is valid, otherwise false
+ */
+ abstract public function isValidDataFormat();
+
+
+ /**
+ * Disable the legend for this chart
+ */
+ public function disableLegend()
+ {
+ $this->legend = null;
+ }
+
+ /**
+ * Render this graph and return the created SVG
+ *
+ * @return string The SVG created by the SvgRenderer
+ *
+ * @throws IcingaException Thrown wen the dataset is not valid for this graph
+ * @see SVGRenderer::render
+ */
+ public function render()
+ {
+ if (!$this->isValidDataFormat()) {
+ throw new IcingaException('Dataset for graph doesn\'t have the proper structure');
+ }
+ $this->build();
+ if ($this->align) {
+ $this->renderer->preserveAspectRatio();
+ $this->renderer->setXAspectRatioAlignment(SVGRenderer::X_ASPECT_RATIO_MIN);
+ $this->renderer->setYAspectRatioAlignment(SVGRenderer::Y_ASPECT_RATIO_MIN);
+ }
+
+ $this->renderer->setAriaDescription($this->description);
+ $this->renderer->setAriaTitle($this->title);
+ $this->renderer->getCanvas()->setAriaRole('presentation');
+
+ $this->renderer->getCanvas()->addElement($this);
+ return $this->renderer->render();
+ }
+
+ /**
+ * Return this graph rendered as PNG
+ *
+ * @param int $width The width of the PNG in pixel
+ * @param int $height The height of the PNG in pixel
+ *
+ * @return string A PNG binary string
+ *
+ * @throws IcingaException In case ImageMagick is not available
+ */
+ public function toPng($width, $height)
+ {
+ if (! class_exists('Imagick')) {
+ throw new IcingaException('Cannot render PNGs without ImageMagick');
+ }
+
+ $image = new Imagick();
+ $image->readImageBlob($this->render());
+ $image->setImageFormat('png24');
+ $image->resizeImage($width, $height, imagick::FILTER_LANCZOS, 1);
+ return $image;
+ }
+
+ /**
+ * Align the chart to the top left corner instead of centering it
+ *
+ * @param bool $align
+ */
+ public function alignTopLeft($align = true)
+ {
+ $this->align = $align;
+ }
+}
diff --git a/library/Icinga/Chart/Donut.php b/library/Icinga/Chart/Donut.php
new file mode 100644
index 0000000..9d2a2a8
--- /dev/null
+++ b/library/Icinga/Chart/Donut.php
@@ -0,0 +1,465 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+use Icinga\Web\Url;
+
+/** Donut chart implementation */
+class Donut
+{
+ /**
+ * Big label in the middle of the donut, color is critical (red)
+ *
+ * @var string
+ */
+ protected $labelBig;
+
+ /**
+ * Url behind the big label
+ *
+ * @var Url
+ */
+ protected $labelBigUrl;
+
+ /**
+ * The state the big label shall indicate
+ *
+ * @var string|null
+ */
+ protected $labelBigState = 'critical';
+
+ /**
+ * Small label in the lower part of the donuts hole
+ *
+ * @var string
+ */
+ protected $labelSmall;
+
+ /**
+ * Thickness of the donut ring
+ *
+ * @var int
+ */
+ protected $thickness = 6;
+
+ /**
+ * Radius based of 100 to simplify the calculations
+ *
+ * 100 / (2 * M_PI)
+ *
+ * @var float
+ */
+ protected $radius = 15.9154943092;
+
+ /**
+ * Color of the hole in the donut
+ *
+ * Transparent by default so it can be placed anywhere with ease
+ *
+ * @var string
+ */
+ protected $centerColor = 'transparent';
+
+ /**
+ * The different colored parts that represent the data
+ *
+ * @var array
+ */
+ protected $slices = array();
+
+ /**
+ * The total amount of data units
+ *
+ * @var int
+ */
+ protected $count = 0;
+
+ /**
+ * Adds a colored part that represent the data
+ *
+ * @param integer $data Units of data
+ * @param array $attributes HTML attributes for this slice. (For example ['class' => 'slice-state-ok'])
+ *
+ * @return $this
+ */
+ public function addSlice($data, $attributes = array())
+ {
+ $this->slices[] = array($data, $attributes);
+
+ $this->count += $data;
+
+ return $this;
+ }
+
+ /**
+ * Set the thickness for this Donut
+ *
+ * @param integer $thickness
+ *
+ * @return $this
+ */
+ public function setThickness($thickness)
+ {
+ $this->thickness = $thickness;
+
+ return $this;
+ }
+ /**
+ * Get the thickness for this Donut
+ *
+ * @return integer
+ */
+ public function getThickness()
+ {
+ return $this->thickness;
+ }
+
+ /**
+ * Set the center color for this Donut
+ *
+ * @param string $centerColor
+ *
+ * @return $this
+ */
+ public function setCenterColor($centerColor)
+ {
+ $this->centerColor = $centerColor;
+
+ return $this;
+ }
+
+ /**
+ * Get the center color for this Donut
+ *
+ * @return string
+ */
+ public function getCenterColor()
+ {
+ return $this->centerColor;
+ }
+
+ /**
+ * Set the text of the big label
+ *
+ * @param string $labelBig
+ *
+ * @return $this
+ */
+ public function setLabelBig($labelBig)
+ {
+ $this->labelBig = $labelBig;
+
+ return $this;
+ }
+
+ /**
+ * Get the text of the big label
+ *
+ * @return string
+ */
+ public function getLabelBig()
+ {
+ return $this->labelBig;
+ }
+
+ /**
+ * Set the url behind the big label
+ *
+ * @param Url $labelBigUrl
+ *
+ * @return $this
+ */
+ public function setLabelBigUrl($labelBigUrl)
+ {
+ $this->labelBigUrl = $labelBigUrl;
+
+ return $this;
+ }
+
+ /**
+ * Get the url behind the big label
+ *
+ * @return Url
+ */
+ public function getLabelBigUrl()
+ {
+ return $this->labelBigUrl;
+ }
+
+ /**
+ * Get whether the big label shall be eye-catching
+ *
+ * @return bool
+ */
+ public function getLabelBigEyeCatching()
+ {
+ return $this->labelBigState !== null;
+ }
+
+ /**
+ * Set whether the big label shall be eye-catching
+ *
+ * @param bool $labelBigEyeCatching
+ *
+ * @return $this
+ */
+ public function setLabelBigEyeCatching($labelBigEyeCatching = true)
+ {
+ $this->labelBigState = $labelBigEyeCatching ? 'critical' : null;
+
+ return $this;
+ }
+
+ /**
+ * Get the state the big label shall indicate
+ *
+ * @return string|null
+ */
+ public function getLabelBigState()
+ {
+ return $this->labelBigState;
+ }
+
+ /**
+ * Set the state the big label shall indicate
+ *
+ * @param string|null $labelBigState
+ *
+ * @return $this
+ */
+ public function setLabelBigState($labelBigState)
+ {
+ $this->labelBigState = $labelBigState;
+
+ return $this;
+ }
+
+ /**
+ * Set the text of the small label
+ *
+ * @param string $labelSmall
+ *
+ * @return $this
+ */
+ public function setLabelSmall($labelSmall)
+ {
+ $this->labelSmall = $labelSmall;
+
+ return $this;
+ }
+
+ /**
+ * Get the text of the small label
+ *
+ * @return string
+ */
+ public function getLabelSmall()
+ {
+ return $this->labelSmall;
+ }
+
+ /**
+ * Put together all slices of this Donut
+ *
+ * @return array $svg
+ */
+ protected function assemble()
+ {
+ // svg tag containing the ring
+ $svg = array(
+ 'tag' => 'svg',
+ 'attributes' => array(
+ 'xmlns' => 'http://www.w3.org/2000/svg',
+ 'viewbox' => '0 0 40 40',
+ 'class' => 'donut-graph'
+ ),
+ 'content' => array()
+ );
+
+ // Donut hole
+ $svg['content'][] = array(
+ 'tag' => 'circle',
+ 'attributes' => array(
+ 'cx' => 20,
+ 'cy' => 20,
+ 'r' => sprintf('%F', $this->radius),
+ 'fill' => $this->getCenterColor()
+ )
+ );
+
+ // When there is no data show gray circle
+ $svg['content'][] = array(
+ 'tag' => 'circle',
+ 'attributes' => array(
+ 'aria-hidden' => true,
+ 'cx' => 20,
+ 'cy' => 20,
+ 'r' => sprintf('%F', $this->radius),
+ 'fill' => $this->getCenterColor(),
+ 'stroke-width' => $this->getThickness(),
+ 'class' => 'slice-state-not-checked'
+ )
+ );
+
+ $slices = $this->slices;
+
+ if ($this->count !== 0) {
+ array_walk($slices, function (&$slice) {
+ $slice[0] = round(100 / $this->count * $slice[0], 2);
+ });
+ }
+
+ // on 0 the donut would start at "3 o'clock" and the offset shifts counterclockwise
+ $offset = 25;
+
+ foreach ($slices as $slice) {
+ $svg['content'][] = array(
+ 'tag' => 'circle',
+ 'attributes' => $slice[1] + array(
+ 'cx' => 20,
+ 'cy' => 20,
+ 'r' => sprintf('%F', $this->radius),
+ 'fill' => 'transparent',
+ 'stroke-width' => $this->getThickness(),
+ 'stroke-dasharray' => sprintf('%F', $slice[0])
+ . ' '
+ . sprintf('%F', (99.9 - $slice[0])), // 99.9 prevents gaps (slight overlap)
+ 'stroke-dashoffset' => sprintf('%F', $offset)
+ )
+ );
+ // negative values shift in the clockwise direction
+ $offset -= $slice[0];
+ }
+
+ $result = array(
+ 'tag' => 'div',
+ 'content' => array($svg)
+ );
+
+ $labelBig = (string) $this->getLabelBig();
+ $labelSmall = (string) $this->getLabelSmall();
+
+ if ($labelBig !== '' || $labelSmall !== '') {
+ $labels = array(
+ 'tag' => 'div',
+ 'attributes' => array(
+ 'class' => 'donut-label'
+ ),
+ 'content' => array()
+ );
+
+ if ($labelBig !== '') {
+ $labels['content'][] =
+ array(
+ 'tag' => 'a',
+ 'attributes' => array(
+ 'aria-label' => $labelBig . ' ' . $labelSmall,
+ 'href' => $this->getLabelBigUrl() ? $this->getLabelBigUrl()->getAbsoluteUrl() : null,
+ 'class' => $this->labelBigState === null
+ ? 'donut-label-big'
+ : 'donut-label-big state-' . $this->labelBigState
+ ),
+ 'content' => $this->shortenLabel($labelBig)
+ );
+ }
+
+ if ($labelSmall !== '') {
+ $labels['content'][] = array(
+ 'tag' => 'p',
+ 'attributes' => array(
+ 'class' => 'donut-label-small',
+ 'x' => '50%',
+ 'y' => '50%'
+ ),
+ 'content' => $labelSmall
+ );
+ }
+
+ $result['content'][] = $labels;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Shorten the label to 3 digits if it is numeric
+ *
+ * 10 => 10 ... 1111 => ~1k ... 1888 => ~2k
+ *
+ * @param int|string $label
+ *
+ * @return string
+ */
+ protected function shortenLabel($label)
+ {
+ if (is_numeric($label) && strlen($label) > 3) {
+ return round($label, -3)/1000 . 'k';
+ }
+
+ return $label;
+ }
+
+ protected function encode($content)
+ {
+ return htmlspecialchars($content, ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8', true);
+ }
+
+ protected function renderAttributes(array $attributes)
+ {
+ $html = array();
+
+ foreach ($attributes as $name => $value) {
+ if ($value === null) {
+ continue;
+ }
+
+ if (is_bool($value) && $value) {
+ $html[] = $name;
+ continue;
+ }
+
+ if (is_array($value)) {
+ $value = implode(' ', $value);
+ }
+
+ $html[] = "$name=\"" . $this->encode($value) . '"';
+ }
+
+ return implode(' ', $html);
+ }
+
+ protected function renderContent(array $element)
+ {
+ $tag = $element['tag'];
+ $attributes = isset($element['attributes']) ? $element['attributes'] : array();
+ $content = isset($element['content']) ? $element['content'] : null;
+
+ $html = array(
+ // rtrim because attributes may be empty
+ rtrim("<$tag " . $this->renderAttributes($attributes))
+ . ">"
+ );
+
+ if ($content !== null) {
+ if (is_array($content)) {
+ foreach ($content as $child) {
+ $html[] = is_array($child) ? $this->renderContent($child) : $this->encode($child);
+ }
+ } else {
+ $html[] = $this->encode($content);
+ }
+ }
+
+ $html[] = "</$tag>";
+
+ return implode("\n", $html);
+ }
+
+ public function render()
+ {
+ $svg = $this->assemble();
+
+ return $this->renderContent($svg);
+ }
+}
diff --git a/library/Icinga/Chart/Format.php b/library/Icinga/Chart/Format.php
new file mode 100644
index 0000000..9e6c4db
--- /dev/null
+++ b/library/Icinga/Chart/Format.php
@@ -0,0 +1,21 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+class Format
+{
+ /**
+ * Format a number into a number-string as defined by the SVG-Standard
+ *
+ * @see http://www.w3.org/TR/SVG/types.html#DataTypeNumber
+ *
+ * @param $number
+ *
+ * @return string
+ */
+ public static function formatSVGNumber($number)
+ {
+ return number_format($number, 1, '.', '');
+ }
+}
diff --git a/library/Icinga/Chart/Graph/BarGraph.php b/library/Icinga/Chart/Graph/BarGraph.php
new file mode 100644
index 0000000..be142bf
--- /dev/null
+++ b/library/Icinga/Chart/Graph/BarGraph.php
@@ -0,0 +1,163 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Graph;
+
+use DOMElement;
+use Icinga\Chart\Primitive\Animation;
+use Icinga\Chart\Primitive\Drawable;
+use Icinga\Chart\Primitive\Rect;
+use Icinga\Chart\Primitive\Styleable;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Bar graph implementation
+ */
+class BarGraph extends Styleable implements Drawable
+{
+ /**
+ * The dataset order
+ *
+ * @var int
+ */
+ private $order = 0;
+
+ /**
+ * The width of the bars.
+ *
+ * @var int
+ */
+ private $barWidth = 3;
+
+ /**
+ * The dataset to use for this bar graph
+ *
+ * @var array
+ */
+ private $dataSet;
+
+ /**
+ * The tooltips
+ *
+ * @var
+ */
+ private $tooltips;
+
+ /**
+ * All graphs
+ *
+ * @var
+ */
+ private $graphs;
+
+ /**
+ * Create a new BarGraph with the given dataset
+ *
+ * @param array $dataSet An array of data points
+ * @param int $order The graph number displayed by this BarGraph
+ * @param array $tooltips The tooltips to display for each value
+ */
+ public function __construct(
+ array $dataSet,
+ array &$graphs,
+ $order,
+ array $tooltips = null
+ ) {
+ $this->order = $order;
+ $this->dataSet = $dataSet;
+
+ $this->tooltips = $tooltips;
+ $ts = [];
+ foreach ($this->tooltips as $value) {
+ $ts[] = $value;
+ }
+ $this->tooltips = $ts;
+
+ $this->graphs = $graphs;
+ }
+
+ /**
+ * Apply configuration styles from the $cfg
+ *
+ * @param array $cfg The configuration as given in the drawBars call
+ */
+ public function setStyleFromConfig(array $cfg)
+ {
+ foreach ($cfg as $elem => $value) {
+ if ($elem === 'color') {
+ $this->setFill($value);
+ } elseif ($elem === 'width') {
+ $this->setStrokeWidth($value);
+ }
+ }
+ }
+
+ /**
+ * Draw a single rectangle
+ *
+ * @param array $point The
+ * @param string $fill The fill color to use
+ * @param $strokeWidth
+ * @param ?int $index
+ *
+ * @return Rect
+ */
+ private function drawSingleBar($point, $fill, $strokeWidth, $index = null)
+ {
+ $rect = new Rect($point[0] - ($this->barWidth / 2), $point[1], $this->barWidth, 100 - $point[1]);
+ $rect->setFill($fill);
+ $rect->setStrokeWidth($strokeWidth);
+ $rect->setStrokeColor('black');
+ if (isset($index)) {
+ $rect->setAttribute('data-icinga-graph-index', $index);
+ }
+ $rect->setAttribute('data-icinga-graph-type', 'bar');
+ $rect->setAdditionalStyle(['clip-path' => 'url(#clip)']);
+ return $rect;
+ }
+
+ /**
+ * Render this BarChart
+ *
+ * @param RenderContext $ctx The rendering context to use for drawing
+ *
+ * @return DOMElement $dom Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $doc = $ctx->getDocument();
+ $group = $doc->createElement('g');
+ $idx = 0;
+
+ if (count($this->dataSet) > 15) {
+ $this->barWidth = 2;
+ }
+ if (count($this->dataSet) > 25) {
+ $this->barWidth = 1;
+ }
+
+ foreach ($this->dataSet as $x => $point) {
+ // add white background bar, to prevent other bars from altering transparency effects
+ $bar = $this->drawSingleBar($point, 'white', $this->strokeWidth, $idx++)->toSvg($ctx);
+ $group->appendChild($bar);
+
+ // draw actual bar
+ $bar = $this->drawSingleBar($point, $this->fill, $this->strokeWidth)->toSvg($ctx);
+ if (isset($this->tooltips[$x])) {
+ $data = array(
+ 'label' => isset($this->graphs[$this->order]['label']) ?
+ strtolower($this->graphs[$this->order]['label']) : '',
+ 'color' => isset($this->graphs[$this->order]['color']) ?
+ strtolower($this->graphs[$this->order]['color']) : '#fff'
+ );
+ $format = isset($this->graphs[$this->order]['tooltip'])
+ ? $this->graphs[$this->order]['tooltip'] : null;
+ $title = $ctx->getDocument()->createElement('title');
+ $title->textContent = $this->tooltips[$x]->renderNoHtml($this->order, $data, $format);
+ $bar->appendChild($title);
+ }
+ $group->appendChild($bar);
+ }
+ return $group;
+ }
+}
diff --git a/library/Icinga/Chart/Graph/LineGraph.php b/library/Icinga/Chart/Graph/LineGraph.php
new file mode 100644
index 0000000..21f930a
--- /dev/null
+++ b/library/Icinga/Chart/Graph/LineGraph.php
@@ -0,0 +1,202 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Graph;
+
+use DOMElement;
+use Icinga\Chart\Primitive\Drawable;
+use Icinga\Chart\Primitive\Path;
+use Icinga\Chart\Primitive\Circle;
+use Icinga\Chart\Primitive\Styleable;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * LineGraph implementation for drawing a set of datapoints as
+ * a connected path
+ */
+class LineGraph extends Styleable implements Drawable
+{
+ /**
+ * The dataset to use
+ *
+ * @var array
+ */
+ private $dataset;
+
+ /**
+ * True to show dots for each datapoint
+ *
+ * @var bool
+ */
+ private $showDataPoints = false;
+
+ /**
+ * When true, the path will be discrete, i.e. showing hard steps instead of a direct line
+ *
+ * @var bool
+ */
+ private $isDiscrete = false;
+
+ /**
+ * The tooltips
+ *
+ * @var
+ */
+ private $tooltips;
+
+ /** @var array */
+ private $graphs;
+
+ /** @var int */
+ private $order;
+
+ /**
+ * The default stroke width
+ * @var int
+ */
+ public $strokeWidth = 5;
+
+ /**
+ * The size of the displayed dots
+ *
+ * @var int
+ */
+ public $dotWith = 0;
+
+ /**
+ * Create a new LineGraph displaying the given dataset
+ *
+ * @param array $dataset An array of [x, y] arrays to display
+ */
+ public function __construct(
+ array $dataset,
+ array &$graphs,
+ $order,
+ array $tooltips = null
+ ) {
+ usort($dataset, array($this, 'sortByX'));
+ $this->dataset = $dataset;
+ $this->graphs = $graphs;
+
+ $this->tooltips = $tooltips;
+ $ts = [];
+ foreach ($this->tooltips as $value) {
+ $ts[] = $value;
+ }
+ $this->tooltips = $ts;
+ $this->order = $order;
+ }
+
+ /**
+ * Set datapoints to be emphased via dots
+ *
+ * @param bool $bool True to enable datapoints, otherwise false
+ */
+ public function setShowDataPoints($bool)
+ {
+ $this->showDataPoints = $bool;
+ }
+
+ /**
+ * Sort the daset by the xaxis
+ *
+ * @param array $v1
+ * @param array $v2
+ * @return int
+ */
+ private function sortByX(array $v1, array $v2)
+ {
+ if ($v1[0] === $v2[0]) {
+ return 0;
+ }
+ return ($v1[0] < $v2[0]) ? -1 : 1;
+ }
+
+ /**
+ * Configure this style
+ *
+ * @param array $cfg The configuration as given in the drawLine call
+ */
+ public function setStyleFromConfig(array $cfg)
+ {
+ $fill = false;
+ foreach ($cfg as $elem => $value) {
+ if ($elem === 'color') {
+ $this->setStrokeColor($value);
+ } elseif ($elem === 'width') {
+ $this->setStrokeWidth($value);
+ } elseif ($elem === 'showPoints') {
+ $this->setShowDataPoints($value);
+ } elseif ($elem === 'fill') {
+ $fill = $value;
+ } elseif ($elem === 'discrete') {
+ $this->isDiscrete = true;
+ }
+ }
+ if ($fill) {
+ $this->setFill($this->strokeColor);
+ $this->setStrokeColor('black');
+ }
+ }
+
+ /**
+ * Render this BarChart
+ *
+ * @param RenderContext $ctx The rendering context to use for drawing
+ *
+ * @return DOMElement $dom Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $path = new Path($this->dataset);
+ if ($this->isDiscrete) {
+ $path->setDiscrete(true);
+ }
+ $path->setStrokeColor($this->strokeColor);
+ $path->setStrokeWidth($this->strokeWidth);
+
+ $path->setAttribute('data-icinga-graph-type', 'line');
+ if ($this->fill !== 'none') {
+ $firstX = $this->dataset[0][0];
+ $lastX = $this->dataset[count($this->dataset)-1][0];
+ $path->prepend(array($firstX, 100))
+ ->append(array($lastX, 100));
+ $path->setFill($this->fill);
+ }
+
+ $path->setAdditionalStyle(['clip-path' => 'url(#clip)']);
+ $path->setId($this->id ?? uniqid('line-graph-'));
+ $group = $path->toSvg($ctx);
+
+ foreach ($this->dataset as $x => $point) {
+ if ($this->showDataPoints === true) {
+ $dot = new Circle($point[0], $point[1], $this->dotWith);
+ $dot->setFill($this->strokeColor);
+ $group->appendChild($dot->toSvg($ctx));
+ }
+
+ // Draw invisible circle for tooltip hovering
+ if (isset($this->tooltips[$x])) {
+ $invisible = new Circle($point[0], $point[1], 20);
+ $invisible->setFill($this->strokeColor);
+ $invisible->setAdditionalStyle(['opacity' => '0.0']);
+ $data = array(
+ 'label' => isset($this->graphs[$this->order]['label']) ?
+ strtolower($this->graphs[$this->order]['label']) : '',
+ 'color' => isset($this->graphs[$this->order]['color']) ?
+ strtolower($this->graphs[$this->order]['color']) : '#fff'
+ );
+ $format = isset($this->graphs[$this->order]['tooltip'])
+ ? $this->graphs[$this->order]['tooltip'] : null;
+ $title = $ctx->getDocument()->createElement('title');
+ $title->textContent = $this->tooltips[$x]->renderNoHtml($this->order, $data, $format);
+ $invisibleRendered = $invisible->toSvg($ctx);
+ $invisibleRendered->appendChild($title);
+ $group->appendChild($invisibleRendered);
+ }
+ }
+
+ return $group;
+ }
+}
diff --git a/library/Icinga/Chart/Graph/StackedGraph.php b/library/Icinga/Chart/Graph/StackedGraph.php
new file mode 100644
index 0000000..49801a9
--- /dev/null
+++ b/library/Icinga/Chart/Graph/StackedGraph.php
@@ -0,0 +1,88 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Graph;
+
+use DOMElement;
+use Icinga\Chart\Primitive\Drawable;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Graph implementation that stacks several graphs and displays them in a cumulative way
+ */
+class StackedGraph implements Drawable
+{
+ /**
+ * All graphs displayed in this stackedgraph
+ *
+ * @var array
+ */
+ private $stack = array();
+
+ /**
+ * An associative array containing x points as the key and an array of y values as the value
+ *
+ * @var array
+ */
+ private $points = array();
+
+ /**
+ * Add a graph to this stack and aggregate the values on the fly
+ *
+ * This modifies the dataset as a side effect
+ *
+ * @param array $subGraph
+ */
+ public function addGraph(array &$subGraph)
+ {
+ foreach ($subGraph['data'] as &$point) {
+ $x = $point[0];
+ if (!isset($this->points[$x])) {
+ $this->points[$x] = 0;
+ }
+ // store old y-value for displaying the actual (non-aggregated)
+ // value in the tooltip
+ $point[2] = $point[1];
+
+ $this->points[$x] += $point[1];
+ $point[1] = $this->points[$x];
+ }
+ }
+
+ /**
+ * Add a graph to the stack
+ *
+ * @param $graph
+ */
+ public function addToStack($graph)
+ {
+ $this->stack[] = $graph;
+ }
+
+ /**
+ * Empty the stack
+ *
+ * @return bool
+ */
+ public function stackEmpty()
+ {
+ return empty($this->stack);
+ }
+
+ /**
+ * Render this stack in the correct order
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement The SVG representation of this graph
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $group = $ctx->getDocument()->createElement('g');
+ $renderOrder = array_reverse($this->stack);
+ foreach ($renderOrder as $stackElem) {
+ $group->appendChild($stackElem->toSvg($ctx));
+ }
+ return $group;
+ }
+}
diff --git a/library/Icinga/Chart/Graph/Tooltip.php b/library/Icinga/Chart/Graph/Tooltip.php
new file mode 100644
index 0000000..7236685
--- /dev/null
+++ b/library/Icinga/Chart/Graph/Tooltip.php
@@ -0,0 +1,143 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Graph;
+
+/**
+ * A tooltip that stores and aggregates information about displayed data
+ * points of a graph and replaces them in a format string to render the description
+ * for specific data points of the graph.
+ *
+ * When render() is called, placeholders for the keys for each data entry will be replaced by
+ * the current value of this data set and the formatted string will be returned.
+ * The content of the replaced keys can change for each data set and depends on how the data
+ * is passed to this class. There are several types of properties:
+ *
+ * <ul>
+ * <li>Global properties</li>: Key-value pairs that stay the same every time render is called, and are
+ * passed to an instance in the constructor.
+ * <li>Aggregated properties</li>: Global properties that are created automatically from
+ * all attached data points.
+ * <li>Local properties</li>: Key-value pairs that only apply to a single data point and
+ * are passed to the render-function.
+ * </ul>
+ */
+class Tooltip
+{
+ /**
+ * The default format string used
+ * when no other format is specified
+ *
+ * @var string
+ */
+ private $defaultFormat;
+
+ /**
+ * All aggregated points
+ *
+ * @var array
+ */
+ private $points = array();
+
+ /**
+ * Contains all static replacements
+ *
+ * @var array
+ */
+ private $data = array(
+ 'sum' => 0
+ );
+
+ /**
+ * Used to format the displayed tooltip.
+ *
+ * @var string
+ */
+ protected $tooltipFormat;
+
+ /**
+ * Create a new tooltip with the specified default format string
+ *
+ * Allows you to set the global data for this tooltip, that is displayed every
+ * time render is called.
+ *
+ * @param array $data Map of global properties
+ * @param string $format The default format string
+ */
+ public function __construct(
+ $data = array(),
+ $format = '<b>{title}</b>: {value} {label}'
+ ) {
+ $this->data = array_merge($this->data, $data);
+ $this->defaultFormat = $format;
+ }
+
+ /**
+ * Add a single data point to update the aggregated properties for this tooltip
+ *
+ * @param $point array Contains the (x,y) values of the data set
+ */
+ public function addDataPoint($point)
+ {
+ // set x-value
+ if (!isset($this->data['title'])) {
+ $this->data['title'] = $point[0];
+ }
+
+ // aggregate y-values
+ $y = (int)$point[1];
+ if (isset($point[2])) {
+ // load original value in case value already aggregated
+ $y = (int)$point[2];
+ }
+
+ if (!isset($this->data['min']) || $this->data['min'] > $y) {
+ $this->data['min'] = $y;
+ }
+ if (!isset($this->data['max']) || $this->data['max'] < $y) {
+ $this->data['max'] = $y;
+ }
+ $this->data['sum'] += $y;
+ $this->points[] = $y;
+ }
+
+ /**
+ * Format the tooltip for a certain data point
+ *
+ * @param array $order Which data set to render
+ * @param array $data The local data for this tooltip
+ * @param string $format Use a custom format string for this data set
+ *
+ * @return mixed|string The tooltip value
+ */
+ public function render($order, $data = array(), $format = null)
+ {
+ if (isset($format)) {
+ $str = $format;
+ } else {
+ $str = $this->defaultFormat;
+ }
+ $data['value'] = $this->points[$order];
+ foreach (array_merge($this->data, $data) as $key => $value) {
+ $str = str_replace('{' . $key . '}', $value, $str);
+ }
+ return $str;
+ }
+
+ /**
+ * Format the tooltip for a certain data point but remove all
+ * occurring html tags
+ *
+ * This is useful for rendering clean tooltips on client without JavaScript
+ *
+ * @param array $order Which data set to render
+ * @param array $data The local data for this tooltip
+ * @param string $format Use a custom format string for this data set
+ *
+ * @return mixed|string The tooltip value, without any HTML tags
+ */
+ public function renderNoHtml($order, $data, $format)
+ {
+ return strip_tags($this->render($order, $data, $format));
+ }
+}
diff --git a/library/Icinga/Chart/GridChart.php b/library/Icinga/Chart/GridChart.php
new file mode 100644
index 0000000..a8cfca6
--- /dev/null
+++ b/library/Icinga/Chart/GridChart.php
@@ -0,0 +1,446 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+use DOMElement;
+use Icinga\Chart\Chart;
+use Icinga\Chart\Axis;
+use Icinga\Chart\Graph\BarGraph;
+use Icinga\Chart\Graph\LineGraph;
+use Icinga\Chart\Graph\StackedGraph;
+use Icinga\Chart\Graph\Tooltip;
+use Icinga\Chart\Primitive\Canvas;
+use Icinga\Chart\Primitive\Rect;
+use Icinga\Chart\Primitive\Path;
+use Icinga\Chart\Render\LayoutBox;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Unit\AxisUnit;
+
+/**
+ * Base class for grid based charts.
+ *
+ * Allows drawing of Line and Barcharts. See the graphing documentation for further details.
+ *
+ * Example:
+ * <pre>
+ * <code>
+ * $this->chart = new GridChart();
+ * $this->chart->setAxisLabel("X axis label", "Y axis label");
+ * $this->chart->setXAxis(Axis::CalendarUnit());
+ * $this->chart->drawLines(
+ * array(
+ * 'data' => array(
+ * array(time()-7200, 10),array(time()-3620, 30), array(time()-1800, 15), array(time(), 92))
+ * )
+ * );
+ * </code>
+ * </pre>
+ */
+class GridChart extends Chart
+{
+ /**
+ * Internal identifier for Line Chart elements
+ */
+ const TYPE_LINE = "LINE";
+
+ /**
+ * Internal identifier fo Bar Chart elements
+ */
+ const TYPE_BAR = "BAR";
+
+ /**
+ * Internal array containing all elements to be drawn in the order they are drawn
+ *
+ * @var array
+ */
+ private $graphs = array();
+
+ /**
+ * An associative array containing all axis of this Chart in the "name" => Axis() form.
+ *
+ * Currently only the 'default' axis is really supported
+ *
+ * @var array
+ */
+ private $axis = array();
+
+ /**
+ * An associative array containing all StackedGraph objects used for cumulative graphs
+ *
+ * The array key is the 'stack' value given in the graph definitions
+ *
+ * @var array
+ */
+ private $stacks = array();
+
+ /**
+ * An associative array containing all Tooltips used to render the titles
+ *
+ * Each tooltip represents the summary for all y-values of a certain x-value
+ * in the grid chart
+ *
+ * @var Tooltip
+ */
+ private $tooltips = array();
+
+ public function __construct()
+ {
+ $this->title = t('Grid Chart');
+ $this->description = t('Contains data in a bar or line chart.');
+ parent::__construct();
+ }
+
+ /**
+ * Check if the current dataset has the proper structure for this chart.
+ *
+ * Needs to be overwritten by extending classes. The default implementation returns false.
+ *
+ * @return bool True when the dataset is valid, otherwise false
+ */
+ public function isValidDataFormat()
+ {
+ foreach ($this->graphs as $values) {
+ foreach ($values as $value) {
+ if (!isset($value['data']) || !is_array($value['data'])) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Calls Axis::addDataset for every graph added to this GridChart
+ *
+ * @see Axis::addDataset
+ */
+ private function configureAxisFromDatasets()
+ {
+ foreach ($this->graphs as $axis => &$graphs) {
+ $axisObj = $this->axis[$axis];
+ foreach ($graphs as &$graph) {
+ $axisObj->addDataset($graph);
+ }
+ }
+ }
+
+ /**
+ * Add an arbitrary number of lines to be drawn
+ *
+ * Refer to the graphs.md for a detailed list of allowed attributes
+ *
+ * @param array $axis,... The line definitions to draw
+ *
+ * @return $this Fluid interface
+ */
+ public function drawLines(array $axis)
+ {
+ $this->draw(self::TYPE_LINE, func_get_args());
+ return $this;
+ }
+
+ /**
+ * Add arbitrary number of bars to be drawn
+ *
+ * Refer to the graphs.md for a detailed list of allowed attributes
+ *
+ * @param array $axis
+ * @return $this
+ */
+ public function drawBars(array $axis)
+ {
+ $this->draw(self::TYPE_BAR, func_get_args());
+ return $this;
+ }
+
+ /**
+ * Generic method for adding elements to the drawing stack
+ *
+ * @param string $type The type of the element to draw (see TYPE_ constants in this class)
+ * @param array $data The data given to the draw call
+ */
+ private function draw($type, $data)
+ {
+ $axisName = 'default';
+ if (is_string($data[0])) {
+ $axisName = $data[0];
+ array_shift($data);
+ }
+ foreach ($data as &$graph) {
+ $graph['graphType'] = $type;
+ if (isset($graph['stack'])) {
+ if (!isset($this->stacks[$graph['stack']])) {
+ $this->stacks[$graph['stack']] = new StackedGraph();
+ }
+ $this->stacks[$graph['stack']]->addGraph($graph);
+ $graph['stack'] = $this->stacks[$graph['stack']];
+ }
+
+ if (!isset($graph['color'])) {
+ $colorType = isset($graph['palette']) ? $graph['palette'] : Palette::NEUTRAL;
+ $graph['color'] = $this->palette->getNext($colorType);
+ }
+ $this->graphs[$axisName][] = $graph;
+ if ($this->legend) {
+ $this->legend->addDataset($graph);
+ }
+ }
+ $this->initTooltips($data);
+ }
+
+
+ private function initTooltips($data)
+ {
+ foreach ($data as &$graph) {
+ foreach ($graph['data'] as $x => $point) {
+ if (!array_key_exists($x, $this->tooltips)) {
+ $this->tooltips[$x] = new Tooltip(
+ array(
+ 'color' => $graph['color'],
+
+ )
+ );
+ }
+ $this->tooltips[$x]->addDataPoint($point);
+ }
+ }
+ }
+
+ /**
+ * Set the label for the x and y axis
+ *
+ * @param string $xAxisLabel The label to use for the x axis
+ * @param string $yAxisLabel The label to use for the y axis
+ * @param string $axisName The name of the axis, for now 'default'
+ *
+ * @return $this Fluid interface
+ */
+ public function setAxisLabel($xAxisLabel, $yAxisLabel, $axisName = 'default')
+ {
+ $this->axis[$axisName]->setXLabel($xAxisLabel)->setYLabel($yAxisLabel);
+ return $this;
+ }
+
+ /**
+ * Set the AxisUnit to use for calculating the values of the x axis
+ *
+ * @param AxisUnit $unit The unit for the x axis
+ * @param string $axisName The name of the axis to set the label for, currently only 'default'
+ *
+ * @return $this Fluid interface
+ */
+ public function setXAxis(AxisUnit $unit, $axisName = 'default')
+ {
+ $this->axis[$axisName]->setUnitForXAxis($unit);
+ return $this;
+ }
+
+ /**
+ * Set the AxisUnit to use for calculating the values of the y axis
+ *
+ * @param AxisUnit $unit The unit for the y axis
+ * @param string $axisName The name of the axis to set the label for, currently only 'default'
+ *
+ * @return $this Fluid interface
+ */
+ public function setYAxis(AxisUnit $unit, $axisName = 'default')
+ {
+ $this->axis[$axisName]->setUnitForYAxis($unit);
+ return $this;
+ }
+
+ /**
+ * Pre-render setup of the axis
+ *
+ * @see Chart::build
+ */
+ protected function build()
+ {
+ $this->configureAxisFromDatasets();
+ }
+
+ /**
+ * Initialize the renderer and overwrite it with an 2:1 ration renderer
+ */
+ protected function init()
+ {
+ $this->renderer = new SVGRenderer(100, 100);
+ $this->setAxis(Axis::createLinearAxis());
+ }
+
+ /**
+ * Overwrite the axis to use
+ *
+ * @param Axis $axis The new axis to use
+ * @param string $name The name of the axis, currently only 'default'
+ *
+ * @return $this Fluid interface
+ */
+ public function setAxis(Axis $axis, $name = 'default')
+ {
+ $this->axis = array($name => $axis);
+ return $this;
+ }
+
+ /**
+ * Add an axis to this graph (not really supported right now)
+ *
+ * @param Axis $axis The axis object to add
+ * @param string $name The name of the axis
+ *
+ * @return $this Fluid interface
+ */
+ public function addAxis(Axis $axis, $name)
+ {
+ $this->axis[$name] = $axis;
+ return $this;
+ }
+
+ /**
+ * Set minimum values for the x and y axis.
+ *
+ * Setting null to an axis means this will use a value determined by the dataset
+ *
+ * @param int $xMin The minimum value for the x axis or null to use a dynamic value
+ * @param int $yMin The minimum value for the y axis or null to use a dynamic value
+ * @param string $axisName The name of the axis to set the minimum, currently only 'default'
+ *
+ * @return $this Fluid interface
+ */
+ public function setAxisMin($xMin = null, $yMin = null, $axisName = 'default')
+ {
+ $this->axis[$axisName]->setXMin($xMin)->setYMin($yMin);
+ return $this;
+ }
+
+ /**
+ * Set maximum values for the x and y axis.
+ *
+ * Setting null to an axis means this will use a value determined by the dataset
+ *
+ * @param int $xMax The maximum value for the x axis or null to use a dynamic value
+ * @param int $yMax The maximum value for the y axis or null to use a dynamic value
+ * @param string $axisName The name of the axis to set the maximum, currently only 'default'
+ *
+ * @return $this Fluid interface
+ */
+ public function setAxisMax($xMax = null, $yMax = null, $axisName = 'default')
+ {
+ $this->axis[$axisName]->setXMax($xMax)->setYMax($yMax);
+ return $this;
+ }
+
+ /**
+ * Render this GridChart to SVG
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $outerBox = new Canvas('outerGraph', new LayoutBox(0, 0, 100, 100));
+ $innerBox = new Canvas('graph', new LayoutBox(0, 0, 95, 90));
+
+ $maxPadding = array(0,0,0,0);
+ foreach ($this->axis as $axis) {
+ $padding = $axis->getRequiredPadding();
+ for ($i=0; $i < count($padding); $i++) {
+ $maxPadding[$i] = max($maxPadding[$i], $padding[$i]);
+ }
+ $innerBox->addElement($axis);
+ }
+ $this->renderGraphContent($innerBox);
+
+ $innerBox->getLayout()->setPadding($maxPadding[0], $maxPadding[1], $maxPadding[2], $maxPadding[3]);
+ $this->createContentClipBox($innerBox);
+
+ $outerBox->addElement($innerBox);
+ if ($this->legend) {
+ $outerBox->addElement($this->legend);
+ }
+ return $outerBox->toSvg($ctx);
+ }
+
+ /**
+ * Create a clip box that defines which area of the graph is drawable and adds it to the graph.
+ *
+ * The clipbox has the id '#clip' and can be used in the clip-mask element
+ *
+ * @param Canvas $innerBox The inner canvas of the graph to add the clip box to
+ */
+ private function createContentClipBox(Canvas $innerBox)
+ {
+ $clipBox = new Canvas('clip', new LayoutBox(0, 0, 100, 100));
+ $clipBox->toClipPath();
+ $innerBox->addElement($clipBox);
+ $rect = new Rect(0.1, 0, 100, 99.9);
+ $clipBox->addElement($rect);
+ }
+
+ /**
+ * Render the content of the graph, i.e. the draw stack
+ *
+ * @param Canvas $innerBox The inner canvas of the graph to add the content to
+ */
+ private function renderGraphContent(Canvas $innerBox)
+ {
+ foreach ($this->graphs as $axisName => $graphs) {
+ $axis = $this->axis[$axisName];
+ $graphObj = null;
+ foreach ($graphs as $dataset => $graph) {
+ // determine the type and create a graph object for it
+ switch ($graph['graphType']) {
+ case self::TYPE_BAR:
+ $graphObj = new BarGraph(
+ $axis->transform($graph['data']),
+ $graphs,
+ $dataset,
+ $this->tooltips
+ );
+ break;
+ case self::TYPE_LINE:
+ $graphObj = new LineGraph(
+ $axis->transform($graph['data']),
+ $graphs,
+ $dataset,
+ $this->tooltips
+ );
+ break;
+ default:
+ continue 2;
+ }
+ $el = $this->setupGraph($graphObj, $graph);
+ if ($el) {
+ $innerBox->addElement($el);
+ }
+ }
+ }
+ }
+
+ /**
+ * Setup the provided Graph type
+ *
+ * @param mixed $graphObject The graph class, needs the setStyleFromConfig method
+ * @param array $graphConfig The configration array of the graph
+ *
+ * @return mixed Either the graph to be added or null if the graph is not directly added
+ * to the document (e.g. stacked graphs are added by
+ * the StackedGraph Composite object)
+ */
+ private function setupGraph($graphObject, array $graphConfig)
+ {
+ $graphObject->setStyleFromConfig($graphConfig);
+ // When in a stack return the StackedGraph object instead of the graphObject
+ if (isset($graphConfig['stack'])) {
+ $graphConfig['stack']->addToStack($graphObject);
+ if (!$graphConfig['stack']->stackEmpty()) {
+ return $graphConfig['stack'];
+ }
+ // return no object when the graph should not be rendered
+ return null;
+ }
+ return $graphObject;
+ }
+}
diff --git a/library/Icinga/Chart/Inline/Inline.php b/library/Icinga/Chart/Inline/Inline.php
new file mode 100644
index 0000000..3acbd73
--- /dev/null
+++ b/library/Icinga/Chart/Inline/Inline.php
@@ -0,0 +1,96 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Inline;
+
+/**
+ * Class to render and inline chart directly from the request params.
+ *
+ * When rendering huge amounts of inline charts it is too expensive
+ * to bootstrap the complete application for ever single chart and
+ * we need to be able render Charts in a compact environment without
+ * the other Icinga classes.
+ *
+ * Class Inline
+ * @package Icinga\Chart\Inline
+ */
+class Inline
+{
+
+ /**
+ * The data displayed in this chart
+ *
+ * @var array
+ */
+ protected $data;
+
+ /**
+ * The colors used to display this chart
+ *
+ * @var array
+ */
+ protected $colors = array(
+ '#00FF00', // OK
+ '#FFFF00', // Warning
+ '#FF0000', // Critical
+ '#E066FF' // Unreachable
+ );
+
+ /**
+ * The labels displayed on this chart
+ *
+ * @var array
+ */
+ protected $labels = array();
+
+ /**
+ * The height in percent
+ *
+ * @var int
+ */
+ protected $height = 100;
+
+ /**
+ * The width in percent
+ *
+ * @var int
+ */
+ protected $width = 100;
+
+ protected function sanitizeStringArray(array $arr)
+ {
+ $sanitized = array();
+ foreach ($arr as $key => $value) {
+ $sanitized[$key] = htmlspecialchars($value);
+ }
+ return $sanitized;
+ }
+
+ /**
+ * Populate the properties from the current request.
+ */
+ public function initFromRequest()
+ {
+ $this->data = explode(',', $_GET['data']);
+ foreach ($this->data as $key => $value) {
+ $this->data[$key] = (int)$value;
+ }
+ for ($i = 0; $i < count($this->data); $i++) {
+ $this->labels[] = '';
+ }
+
+ if (array_key_exists('colors', $_GET)) {
+ $this->colors = $this->sanitizeStringArray(explode(',', $_GET['colors']));
+ }
+ while (count($this->colors) < count($this->data)) {
+ $this->colors[] = '#FEFEFE';
+ }
+
+ if (array_key_exists('width', $_GET)) {
+ $this->width = (int)$_GET['width'];
+ }
+ if (array_key_exists('height', $_GET)) {
+ $this->height = (int)$_GET['height'];
+ }
+ }
+}
diff --git a/library/Icinga/Chart/Inline/PieChart.php b/library/Icinga/Chart/Inline/PieChart.php
new file mode 100644
index 0000000..de68213
--- /dev/null
+++ b/library/Icinga/Chart/Inline/PieChart.php
@@ -0,0 +1,41 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Inline;
+
+use Icinga\Chart\PieChart as PieChartRenderer;
+
+/**
+ * Draw an inline pie-chart directly from the available request parameters.
+ */
+class PieChart extends Inline
+{
+ protected function getChart()
+ {
+ $pie = new PieChartRenderer();
+ $pie->alignTopLeft();
+ $pie->disableLegend();
+ $pie->drawPie(array(
+ 'data' => $this->data, 'colors' => $this->colors, 'labels' => $this->labels
+ ));
+ return $pie;
+ }
+
+ public function toSvg($output = true)
+ {
+ if ($output) {
+ echo $this->getChart()->render();
+ } else {
+ return $this->getChart()->render();
+ }
+ }
+
+ public function toPng($output = true)
+ {
+ if ($output) {
+ echo $this->getChart()->toPng($this->width, $this->height);
+ } else {
+ return $this->getChart()->toPng($this->width, $this->height);
+ }
+ }
+}
diff --git a/library/Icinga/Chart/Legend.php b/library/Icinga/Chart/Legend.php
new file mode 100644
index 0000000..ab1c9e0
--- /dev/null
+++ b/library/Icinga/Chart/Legend.php
@@ -0,0 +1,102 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+use DOMElement;
+use Icinga\Chart\Palette;
+use Icinga\Chart\Primitive\Canvas;
+use Icinga\Chart\Primitive\Drawable;
+use Icinga\Chart\Primitive\Rect;
+use Icinga\Chart\Primitive\Text;
+use Icinga\Chart\Render\LayoutBox;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Drawable for creating a Graph Legend on the bottom of a graph.
+ *
+ * Usually used by the GridChart class internally.
+ */
+class Legend implements Drawable
+{
+
+ /**
+ * Internal counter for unnamed label identifiers
+ *
+ * @var int
+ */
+ private $internalCtr = 0;
+
+ /**
+ *
+ * Content of this legend
+ *
+ * @var array
+ */
+ private $dataset = array();
+
+
+ /**
+ * Set the content to be displayed by this legend
+ *
+ * @param array $dataset An array of datasets in the form they are provided to the graphing implementation
+ */
+ public function addDataset(array $dataset)
+ {
+ if (!isset($dataset['label'])) {
+ $dataset['label'] = 'Dataset ' . (++$this->internalCtr);
+ }
+ if (!isset($dataset['color'])) {
+ return;
+ }
+ $this->dataset[$dataset['color']] = $dataset['label'];
+ }
+
+ /**
+ * Render the legend to an SVG object
+ *
+ * @param RenderContext $ctx The context to use for rendering this legend
+ *
+ * @return DOMElement The SVG representation of this legend
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $outer = new Canvas('legend', new LayoutBox(0, 0, 100, 100));
+ $outer->getLayout()->setPadding(2, 2, 2, 2);
+ $nrOfColumns = 4;
+
+ $topstep = 10 / $nrOfColumns + 2;
+
+ $top = 0;
+ $left = 0;
+ $lastLabelEndPos = -1;
+ foreach ($this->dataset as $color => $text) {
+ $leftstep = 100 / $nrOfColumns + strlen($text);
+
+ // Make sure labels don't overlap each other
+ while ($lastLabelEndPos >= $left) {
+ $left += $leftstep;
+ }
+ // When a label is longer than the available space, use the next line
+ if ($left + strlen($text) > 100) {
+ $top += $topstep;
+ $left = 0;
+ }
+
+ $colorBox = new Rect($left, $top, 2, 2);
+ $colorBox->setFill($color)->setStrokeWidth(2);
+ $colorBox->keepRatio();
+ $outer->addElement($colorBox);
+
+ $textBox = new Text($left+5, $top+2, $text);
+ $textBox->setFontSize('2em');
+ $outer->addElement($textBox);
+
+ // readjust layout
+ $lastLabelEndPos = $left + strlen($text);
+ $left += $leftstep;
+ }
+ $svg = $outer->toSvg($ctx);
+ return $svg;
+ }
+}
diff --git a/library/Icinga/Chart/Palette.php b/library/Icinga/Chart/Palette.php
new file mode 100644
index 0000000..90ad74b
--- /dev/null
+++ b/library/Icinga/Chart/Palette.php
@@ -0,0 +1,65 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+/**
+ * Provide a set of colors that will be used by the chart as default values
+ */
+class Palette
+{
+ /**
+ * Neutral colors without special meaning
+ */
+ const NEUTRAL = 'neutral';
+
+ /**
+ * A set of problem (i.e. red) colors
+ */
+ const PROBLEM = 'problem';
+
+ /**
+ * A set of ok (i.e. green) colors
+ */
+ const OK = 'ok';
+
+ /**
+ * A set of warning (i.e. yellow) colors
+ */
+ const WARNING = 'warning';
+
+ /**
+ * The colorsets for specific categories
+ *
+ * @var array
+ */
+ public $colorSets = array(
+ self::OK => array('#00FF00'),
+ self::PROBLEM => array('#FF0000'),
+ self::WARNING => array('#FFFF00'),
+ self::NEUTRAL => array('#f3f3f3')
+ );
+
+ /**
+ * Return the next available color as an hex string for the given type
+ *
+ * @param string $type The type to receive a color from
+ *
+ * @return string The color in hex format
+ */
+ public function getNext($type = self::NEUTRAL)
+ {
+ if (!isset($this->colorSets[$type])) {
+ $type = self::NEUTRAL;
+ }
+
+ $color = current($this->colorSets[$type]);
+ if ($color === false) {
+ reset($this->colorSets[$type]);
+
+ $color = current($this->colorSets[$type]);
+ }
+ next($this->colorSets[$type]);
+ return $color;
+ }
+}
diff --git a/library/Icinga/Chart/PieChart.php b/library/Icinga/Chart/PieChart.php
new file mode 100644
index 0000000..1bcf380
--- /dev/null
+++ b/library/Icinga/Chart/PieChart.php
@@ -0,0 +1,306 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+use DOMElement;
+use Icinga\Chart\Chart;
+use Icinga\Chart\Primitive\Canvas;
+use Icinga\Chart\Primitive\PieSlice;
+use Icinga\Chart\Primitive\RawElement;
+use Icinga\Chart\Primitive\Rect;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Render\LayoutBox;
+
+/**
+ * Graphing component for rendering Pie Charts.
+ *
+ * See the graphs.md documentation for further information about how to use this component
+ */
+class PieChart extends Chart
+{
+ /**
+ * Stack multiple pies
+ */
+ const STACKED = "stacked";
+
+ /**
+ * Draw multiple pies beneath each other
+ */
+ const ROW = "row";
+
+ /**
+ * The drawing stack containing all pie definitions in the order they will be drawn
+ *
+ * @var array
+ */
+ private $pies = array();
+
+ /**
+ * The composition type currently used
+ *
+ * @var string
+ */
+ private $type = PieChart::STACKED;
+
+ /**
+ * Disable drawing of captions when set true
+ *
+ * @var bool
+ */
+ private $noCaption = false;
+
+ public function __construct()
+ {
+ $this->title = t('Pie Chart');
+ $this->description = t('Contains data in a pie chart.');
+ parent::__construct();
+ }
+
+ /**
+ * Test if the given pies have the correct format
+ *
+ * @return bool True when the given pies are correct, otherwise false
+ */
+ public function isValidDataFormat()
+ {
+ foreach ($this->pies as $pie) {
+ if (!isset($pie['data']) || !is_array($pie['data'])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Create renderer and normalize the dataset to represent percentage information
+ */
+ protected function build()
+ {
+ $this->renderer = new SVGRenderer(($this->type === self::STACKED) ? 1 : count($this->pies), 1);
+ foreach ($this->pies as &$pie) {
+ $this->normalizeDataSet($pie);
+ }
+ }
+
+ /**
+ * Normalize the given dataset to represent percentage information instead of absolute valuess
+ *
+ * @param array $pie The pie definition given in the drawPie call
+ */
+ private function normalizeDataSet(&$pie)
+ {
+ $total = array_sum($pie['data']);
+ if ($total === 100) {
+ return;
+ }
+ if ($total == 0) {
+ return;
+ }
+ foreach ($pie['data'] as &$slice) {
+ $slice = $slice/$total * 100;
+ }
+ }
+
+ /**
+ * Draw an arbitrary number of pies in this chart
+ *
+ * @param array $dataSet,... The pie definition, see graphs.md for further details concerning the format
+ *
+ * @return $this Fluent interface
+ */
+ public function drawPie(array $dataSet)
+ {
+ $dataSets = func_get_args();
+ $this->pies += $dataSets;
+ foreach ($dataSets as $dataSet) {
+ $this->legend->addDataset($dataSet);
+ }
+ return $this;
+ }
+
+ /**
+ * Return the SVG representation of this graph
+ *
+ * @param RenderContext $ctx The context to use for drawings
+ *
+ * @return DOMElement The SVG representation of this graph
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $labelBox = $ctx->getDocument()->createElement('g');
+ if (!$this->noCaption) {
+ // Scale SVG to make room for captions
+ $outerBox = new Canvas('outerGraph', new LayoutBox(33, -5, 40, 40));
+ $innerBox = new Canvas('graph', new LayoutBox(0, 0, 100, 100));
+ $innerBox->getLayout()->setPadding(10, 10, 10, 10);
+ } else {
+ $outerBox = new Canvas('outerGraph', new LayoutBox(1.5, -10, 124, 124));
+ $innerBox = new Canvas('graph', new LayoutBox(0, 0, 100, 100));
+ $innerBox->getLayout()->setPadding(0, 0, 0, 0);
+ }
+ $this->createContentClipBox($innerBox);
+ $this->renderPies($innerBox, $labelBox);
+ $innerBox->addElement(new RawElement($labelBox));
+ $outerBox->addElement($innerBox);
+
+ return $outerBox->toSvg($ctx);
+ }
+
+ /**
+ * Render the pies in the draw stack using the selected algorithm for composition
+ *
+ * @param Canvas $innerBox The canvas to use for inserting the pies
+ * @param DOMElement $labelBox The DOM element to add the labels to (so they can't be overlapped by pie elements)
+ */
+ private function renderPies(Canvas $innerBox, DOMElement $labelBox)
+ {
+ if ($this->type === self::STACKED) {
+ $this->renderStackedPie($innerBox, $labelBox);
+ } else {
+ $this->renderPieRow($innerBox, $labelBox);
+ }
+ }
+
+ /**
+ * Return the color to be used for the given pie slice
+ *
+ * @param array $pie The pie configuration as provided in the drawPie call
+ * @param int $dataIdx The index of the pie slice in the pie configuration
+ *
+ * @return string The hex color string to use for the pie slice
+ */
+ private function getColorForPieSlice(array $pie, $dataIdx)
+ {
+ if (isset($pie['colors']) && is_array($pie['colors']) && isset($pie['colors'][$dataIdx])) {
+ return $pie['colors'][$dataIdx];
+ }
+ $type = Palette::NEUTRAL;
+ if (isset($pie['palette']) && is_array($pie['palette']) && isset($pie['palette'][$dataIdx])) {
+ $type = $pie['palette'][$dataIdx];
+ }
+ return $this->palette->getNext($type);
+ }
+
+ /**
+ * Render a row of pies
+ *
+ * @param Canvas $innerBox The canvas to insert the pies to
+ * @param DOMElement $labelBox The DOMElement to use for adding label elements
+ */
+ private function renderPieRow(Canvas $innerBox, DOMElement $labelBox)
+ {
+ $radius = 50 / count($this->pies);
+ $x = $radius;
+ foreach ($this->pies as $pie) {
+ $labelPos = 0;
+ $lastRadius = 0;
+
+ foreach ($pie['data'] as $idx => $dataset) {
+ $slice = new PieSlice($radius, $dataset, $lastRadius);
+ $slice->setX($x)
+ ->setStrokeColor('#000')
+ ->setStrokeWidth(1)
+ ->setY(50)
+ ->setFill($this->getColorForPieSlice($pie, $idx));
+ $innerBox->addElement($slice);
+ // add caption if not disabled
+ if (!$this->noCaption && isset($pie['labels'])) {
+ $slice->setCaption($pie['labels'][$labelPos++])
+ ->setLabelGroup($labelBox);
+ }
+ $lastRadius += $dataset;
+ }
+ // shift right for next pie
+ $x += $radius*2;
+ }
+ }
+
+ /**
+ * Render pies in a stacked way so one pie is nested in the previous pie
+ *
+ * @param Canvas $innerBox The canvas to insert the pie to
+ * @param DOMElement $labelBox The DOMElement to use for adding label elements
+ */
+ private function renderStackedPie(Canvas $innerBox, DOMElement $labelBox)
+ {
+ $radius = 40;
+ $minRadius = 20;
+ if (count($this->pies) == 0) {
+ return;
+ }
+ $shrinkStep = ($radius - $minRadius) / count($this->pies);
+ $x = $radius;
+
+ for ($i = 0; $i < count($this->pies); $i++) {
+ $pie = $this->pies[$i];
+ // the offset for the caption path, outer caption indicator shouldn't point
+ // to the middle of the slice as there will be another pie
+ $offset = isset($this->pies[$i+1]) ? $radius - $shrinkStep : 0;
+ $labelPos = 0;
+ $lastRadius = 0;
+ foreach ($pie['data'] as $idx => $dataset) {
+ $color = $this->getColorForPieSlice($pie, $idx);
+ if ($dataset == 0) {
+ $labelPos++;
+ continue;
+ }
+ $slice = new PieSlice($radius, $dataset, $lastRadius);
+ $slice->setY(50)
+ ->setX($x)
+ ->setStrokeColor('#000')
+ ->setStrokeWidth(1)
+ ->setFill($color)
+ ->setLabelGroup($labelBox);
+
+ if (!$this->noCaption && isset($pie['labels'])) {
+ $slice->setCaption($pie['labels'][$labelPos++])
+ ->setCaptionOffset($offset)
+ ->setOuterCaptionBound(50);
+ }
+ $innerBox->addElement($slice);
+ $lastRadius += $dataset;
+ }
+ // shrinken the next pie
+ $radius -= $shrinkStep;
+ }
+ }
+
+ /**
+ * Set the composition type of this PieChart
+ *
+ * @param string $type Either self::STACKED or self::ROW
+ *
+ * @return $this Fluent interface
+ */
+ public function setType($type)
+ {
+ $this->type = $type;
+ return $this;
+ }
+
+ /**
+ * Hide the caption from this PieChart
+ *
+ * @return $this Fluent interface
+ */
+ public function disableLegend()
+ {
+ $this->noCaption = true;
+ return $this;
+ }
+
+ /**
+ * Create the content for this PieChart
+ *
+ * @param Canvas $innerBox The innerbox to add the clip mask to
+ */
+ private function createContentClipBox(Canvas $innerBox)
+ {
+ $clipBox = new Canvas('clip', new LayoutBox(0, 0, 100, 100));
+ $clipBox->toClipPath();
+ $innerBox->addElement($clipBox);
+ $rect = new Rect(0.1, 0, 100, 99.9);
+ $clipBox->addElement($rect);
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Animatable.php b/library/Icinga/Chart/Primitive/Animatable.php
new file mode 100644
index 0000000..69ba0e1
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Animatable.php
@@ -0,0 +1,43 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Base interface for animatable objects
+ */
+abstract class Animatable extends Styleable
+{
+ /**
+ * The animation object set
+ *
+ * @var Animation
+ */
+ public $animation = null;
+
+ /**
+ * Set the animation for this object
+ *
+ * @param Animation $anim The animation to use
+ */
+ public function setAnimation(Animation $anim)
+ {
+ $this->animation = $anim;
+ }
+
+ /**
+ * Append the animation to the given element
+ *
+ * @param DOMElement $dom The element to append the animation to
+ * @param RenderContext $ctx The context to use for rendering the animation object
+ */
+ protected function appendAnimation(DOMElement $dom, RenderContext $ctx)
+ {
+ if ($this->animation) {
+ $dom->appendChild($this->animation->toSvg($ctx));
+ }
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Animation.php b/library/Icinga/Chart/Primitive/Animation.php
new file mode 100644
index 0000000..e620fa7
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Animation.php
@@ -0,0 +1,87 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Drawable for the SVG animate tag
+ */
+class Animation implements Drawable
+{
+ /**
+ * The attribute to animate
+ *
+ * @var string
+ */
+ private $attribute;
+
+ /**
+ * The 'from' value
+ *
+ * @var mixed
+ */
+ private $from;
+
+ /**
+ * The to value
+ *
+ * @var mixed
+ */
+ private $to;
+
+ /**
+ * The begin value (in seconds)
+ *
+ * @var float
+ */
+ private $begin = 0;
+
+ /**
+ * The duration value (in seconds)
+ *
+ * @var float
+ */
+ private $duration = 0.5;
+
+ /**
+ * Create an animation object
+ *
+ * @param string $attribute The attribute to animate
+ * @param string $from The from value for the animation
+ * @param string $to The to value for the animation
+ * @param float $duration The duration of the duration
+ * @param float $begin The begin of the duration
+ */
+ public function __construct($attribute, $from, $to, $duration = 0.5, $begin = 0.0)
+ {
+ $this->attribute = $attribute;
+ $this->from = $from;
+ $this->to = $to;
+ $this->duration = $duration;
+ $this->begin = $begin;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+
+ $animate = $ctx->getDocument()->createElement('animate');
+ $animate->setAttribute('attributeName', $this->attribute);
+ $animate->setAttribute('attributeType', 'XML');
+ $animate->setAttribute('from', $this->from);
+ $animate->setAttribute('to', $this->to);
+ $animate->setAttribute('begin', $this->begin . 's');
+ $animate->setAttribute('dur', $this->duration . 's');
+ $animate->setAttribute('fill', "freeze");
+
+ return $animate;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Canvas.php b/library/Icinga/Chart/Primitive/Canvas.php
new file mode 100644
index 0000000..32f06bf
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Canvas.php
@@ -0,0 +1,140 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Primitive;
+
+use DOMElement;
+use Icinga\Chart\Render\LayoutBox;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Canvas SVG component that encapsulates grouping and padding and allows rendering
+ * multiple elements in a group
+ *
+ */
+class Canvas implements Drawable
+{
+ /**
+ * The name of the canvas, will be used as the id
+ *
+ * @var string
+ */
+ private $name;
+
+ /**
+ * An array of child elements of this Canvas
+ *
+ * @var array
+ */
+ private $children = array();
+
+ /**
+ * When true, this canvas is encapsulated in a clipPath tag and not drawn
+ *
+ * @var bool
+ */
+ private $isClipPath = false;
+
+ /**
+ * The LayoutBox of this Canvas
+ *
+ * @var LayoutBox
+ */
+ private $rect;
+
+ /**
+ * The aria role used to describe this canvas' purpose in the accessibility tree
+ *
+ * @var string
+ */
+ private $ariaRole;
+
+ /**
+ * Create this canvas
+ *
+ * @param String $name The name of this canvas
+ * @param LayoutBox $rect The layout and size of this canvas
+ */
+ public function __construct($name, LayoutBox $rect)
+ {
+ $this->rect = $rect;
+ $this->name = $name;
+ }
+
+ /**
+ * Convert this canvas to a clipPath element
+ */
+ public function toClipPath()
+ {
+ $this->isClipPath = true;
+ }
+
+ /**
+ * Return the layout of this canvas
+ *
+ * @return LayoutBox
+ */
+ public function getLayout()
+ {
+ return $this->rect;
+ }
+
+ /**
+ * Add an element to this canvas
+ *
+ * @param Drawable $child
+ */
+ public function addElement(Drawable $child)
+ {
+ $this->children[] = $child;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $doc = $ctx->getDocument();
+ if ($this->isClipPath) {
+ $outer = $doc->createElement('defs');
+ $innerContainer = $element = $doc->createElement('clipPath');
+ $outer->appendChild($element);
+ } else {
+ $outer = $element = $doc->createElement('g');
+ $innerContainer = $doc->createElement('g');
+ $innerContainer->setAttribute('x', 0);
+ $innerContainer->setAttribute('y', 0);
+ $innerContainer->setAttribute('id', $this->name . '_inner');
+ $innerContainer->setAttribute('transform', $this->rect->getInnerTransform($ctx));
+ $element->appendChild($innerContainer);
+ }
+
+ $element->setAttribute('id', $this->name);
+ foreach ($this->children as $child) {
+ $innerContainer->appendChild($child->toSvg($ctx));
+ }
+
+ if (isset($this->ariaRole)) {
+ $outer->setAttribute('role', $this->ariaRole);
+ }
+ return $outer;
+ }
+
+ /**
+ * Set the aria role used to determine the meaning of this canvas in the accessibility tree
+ *
+ * The role 'presentation' will indicate that the purpose of this canvas is entirely decorative, while the role
+ * 'img' will indicate that the canvas contains an image, with a possible title or a description. For other
+ * possible roles, see http://www.w3.org/TR/wai-aria/roles
+ *
+ * @param $role string The aria role to set
+ */
+ public function setAriaRole($role)
+ {
+ $this->ariaRole = $role;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Circle.php b/library/Icinga/Chart/Primitive/Circle.php
new file mode 100644
index 0000000..f98ffac
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Circle.php
@@ -0,0 +1,84 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Primitive;
+
+use DOMDocument;
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Format;
+
+/**
+ * Drawable for svg circles
+ */
+class Circle extends Styleable implements Drawable
+{
+ /**
+ * The circles x position
+ *
+ * @var int
+ */
+ private $x;
+
+ /**
+ * The circles y position
+ *
+ * @var int
+ */
+ private $y;
+
+ /**
+ * The circles radius
+ *
+ * @var int
+ */
+ private $radius;
+
+ /**
+ * Construct the circle
+ *
+ * @param int $x The x position of the circle
+ * @param int $y The y position of the circle
+ * @param int $radius The radius of the circle
+ */
+ public function __construct($x, $y, $radius)
+ {
+ $this->x = $x;
+ $this->y = $y;
+ $this->radius = $radius;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $coords = $ctx->toAbsolute($this->x, $this->y);
+ $circle = $ctx->getDocument()->createElement('circle');
+ $circle->setAttribute('cx', Format::formatSVGNumber($coords[0]));
+ $circle->setAttribute('cy', Format::formatSVGNumber($coords[1]));
+ $circle->setAttribute('r', $this->radius);
+
+ $id = $this->id ?? uniqid('circle-');
+ $circle->setAttribute('id', $id);
+ $this->setId($id);
+
+ $this->applyAttributes($circle);
+
+ $style = new DOMDocument();
+ $style->loadHTML($this->getStyle());
+
+ $circle->appendChild(
+ $circle->ownerDocument->importNode(
+ $style->getElementsByTagName('style')->item(0),
+ true
+ )
+ );
+
+ return $circle;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Drawable.php b/library/Icinga/Chart/Primitive/Drawable.php
new file mode 100644
index 0000000..5b4355c
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Drawable.php
@@ -0,0 +1,22 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Drawable element for creating svg out of components
+ */
+interface Drawable
+{
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx);
+}
diff --git a/library/Icinga/Chart/Primitive/Line.php b/library/Icinga/Chart/Primitive/Line.php
new file mode 100644
index 0000000..d83cbea
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Line.php
@@ -0,0 +1,103 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMDocument;
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Format;
+
+/**
+ * Drawable for the svg line element
+ */
+class Line extends Styleable implements Drawable
+{
+
+ /**
+ * The default stroke width
+ *
+ * @var int
+ */
+ public $strokeWidth = 1;
+
+ /**
+ * The line's start x coordinate
+ *
+ * @var int
+ */
+ private $xStart = 0;
+
+ /**
+ * The line's end x coordinate
+ *
+ * @var int
+ */
+ private $xEnd = 0;
+
+ /**
+ * The line's start y coordinate
+ *
+ * @var int
+ */
+ private $yStart = 0;
+
+ /**
+ * The line's end y coordinate
+ *
+ * @var int
+ */
+ private $yEnd = 0;
+
+ /**
+ * Create a line object starting at the first coordinate and ending at the second one
+ *
+ * @param int $x1 The line's start x coordinate
+ * @param int $y1 The line's start y coordinate
+ * @param int $x2 The line's end x coordinate
+ * @param int $y2 The line's end y coordinate
+ */
+ public function __construct($x1, $y1, $x2, $y2)
+ {
+ $this->xStart = $x1;
+ $this->xEnd = $x2;
+ $this->yStart = $y1;
+ $this->yEnd = $y2;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $doc = $ctx->getDocument();
+ list($x1, $y1) = $ctx->toAbsolute($this->xStart, $this->yStart);
+ list($x2, $y2) = $ctx->toAbsolute($this->xEnd, $this->yEnd);
+ $line = $doc->createElement('line');
+ $line->setAttribute('x1', Format::formatSVGNumber($x1));
+ $line->setAttribute('x2', Format::formatSVGNumber($x2));
+ $line->setAttribute('y1', Format::formatSVGNumber($y1));
+ $line->setAttribute('y2', Format::formatSVGNumber($y2));
+
+ $id = $this->id ?? uniqid('line-');
+ $line->setAttribute('id', $id);
+ $this->setId($id);
+
+ $this->applyAttributes($line);
+
+ $style = new DOMDocument();
+ $style->loadHTML($this->getStyle());
+
+ $line->appendChild(
+ $line->ownerDocument->importNode(
+ $style->getElementsByTagName('style')->item(0),
+ true
+ )
+ );
+
+ return $line;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Path.php b/library/Icinga/Chart/Primitive/Path.php
new file mode 100644
index 0000000..b9d5f7b
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Path.php
@@ -0,0 +1,187 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMDocument;
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Format;
+
+/**
+ * Drawable for creating a svg path element
+ */
+class Path extends Styleable implements Drawable
+{
+ /**
+ * Syntax template for moving
+ *
+ * @see http://www.w3.org/TR/SVG/paths.html#PathDataMovetoCommands
+ */
+ const TPL_MOVE = 'M %s %s ';
+
+ /**
+ * Syntax template for bezier curve
+ *
+ * @see http://www.w3.org/TR/SVG/paths.html#PathDataCubicBezierCommands
+ */
+ const TPL_BEZIER = 'S %s %s ';
+
+ /**
+ * Syntax template for straight lines
+ *
+ * @see http://www.w3.org/TR/SVG/paths.html#PathDataLinetoCommands
+ */
+ const TPL_STRAIGHT = 'L %s %s ';
+
+ /**
+ * The default stroke width
+ *
+ * @var int
+ */
+ public $strokeWidth = 1;
+
+ /**
+ * True to treat coordinates as absolute values
+ *
+ * @var bool
+ */
+ protected $isAbsolute = false;
+
+ /**
+ * The points to draw, in the order they are drawn
+ *
+ * @var array
+ */
+ protected $points = array();
+
+ /**
+ * True to draw the path discrete, i.e. make hard steps between points
+ *
+ * @var bool
+ */
+ protected $discrete = false;
+
+ /**
+ * Create the path using the given points
+ *
+ * @param array $points Either a single [x, y] point or an array of x, y points
+ */
+ public function __construct(array $points)
+ {
+ $this->append($points);
+ }
+
+ /**
+ * Append a single point or an array of points to this path
+ *
+ * @param array $points Either a single [x, y] point or an array of x, y points
+ *
+ * @return $this Fluid interface
+ */
+ public function append(array $points)
+ {
+ if (count($points) === 0) {
+ return $this;
+ }
+ if (!is_array($points[0])) {
+ $points = array($points);
+ }
+ $this->points = array_merge($this->points, $points);
+ return $this;
+ }
+
+ /**
+ * Prepend a single point or an array of points to this path
+ *
+ * @param array $points Either a single [x, y] point or an array of x, y points
+ *
+ * @return $this Fluid interface
+ */
+ public function prepend(array $points)
+ {
+ if (count($points) === 0) {
+ return $this;
+ }
+ if (!is_array($points[0])) {
+ $points = array($points);
+ }
+ $this->points = array_merge($points, $this->points);
+ return $this;
+ }
+
+ /**
+ * Set this path to be discrete
+ *
+ * @param boolean $bool True to draw discrete or false to draw straight lines between points
+ *
+ * @return $this Fluid interface
+ */
+ public function setDiscrete($bool)
+ {
+ $this->discrete = $bool;
+ return $this;
+ }
+
+ /**
+ * Mark this path as containing absolute coordinates
+ *
+ * @return $this Fluid interface
+ */
+ public function toAbsolute()
+ {
+ $this->isAbsolute = true;
+ return $this;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $doc = $ctx->getDocument();
+ $group = $doc->createElement('g');
+
+ $pathDescription = '';
+ $tpl = self::TPL_MOVE;
+ $lastPoint = null;
+ foreach ($this->points as $point) {
+ if (!$this->isAbsolute) {
+ $point = $ctx->toAbsolute($point[0], $point[1]);
+ }
+ $point[0] = Format::formatSVGNumber($point[0]);
+ $point[1] = Format::formatSVGNumber($point[1]);
+ if ($lastPoint && $this->discrete) {
+ $pathDescription .= sprintf($tpl, $point[0], $lastPoint[1]);
+ }
+ $pathDescription .= vsprintf($tpl, $point);
+ $lastPoint = $point;
+ $tpl = self::TPL_STRAIGHT;
+ }
+
+ $path = $doc->createElement('path');
+
+ $id = $this->id ?? uniqid('path-');
+ $path->setAttribute('id', $id);
+ $this->setId($id);
+
+ $path->setAttribute('d', $pathDescription);
+
+ $this->applyAttributes($path);
+ $style = new DOMDocument();
+ $style->loadHTML($this->getStyle());
+
+ $path->appendChild(
+ $path->ownerDocument->importNode(
+ $style->getElementsByTagName('style')->item(0),
+ true
+ )
+ );
+
+ $group->appendChild($path);
+ return $group;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/PieSlice.php b/library/Icinga/Chart/Primitive/PieSlice.php
new file mode 100644
index 0000000..f898435
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/PieSlice.php
@@ -0,0 +1,307 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMDocument;
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Format;
+
+/**
+ * Component for drawing a pie slice
+ */
+class PieSlice extends Animatable implements Drawable
+{
+ /**
+ * The radius of this pieslice relative to the canvas
+ *
+ * @var int
+ */
+ private $radius = 50;
+
+ /**
+ * The start radian of the pie slice
+ *
+ * @var float
+ */
+ private $startRadian = 0;
+
+ /**
+ * The end radian of the pie slice
+ *
+ * @var float
+ */
+ private $endRadian = 0;
+
+ /**
+ * The x position of the pie slice's center
+ *
+ * @var int
+ */
+ private $x;
+
+ /**
+ * The y position of the pie slice's center
+ *
+ * @var int
+ */
+ private $y;
+
+ /**
+ * The caption of the pie slice, empty string means no caption
+ *
+ * @var string
+ */
+ private $caption = "";
+
+ /**
+ * The offset of the caption, shifting the indicator from the center of the pie slice
+ *
+ * This is required for nested pie slices.
+ *
+ * @var int
+ */
+ private $captionOffset = 0;
+
+ /**
+ * The minimum radius the label must respect
+ *
+ * @var int
+ */
+ private $outerCaptionBound = 0;
+
+ /**
+ * An optional group element to add labels to when rendering
+ *
+ * @var DOMElement
+ */
+ private $labelGroup;
+
+ /**
+ * Create a pie slice
+ *
+ * @param int $radius The radius of the slice
+ * @param int $percent The percentage the slice represents
+ * @param int $percentStart The percentage where this slice starts
+ */
+ public function __construct($radius, $percent, $percentStart = 0)
+ {
+ $this->x = $this->y = $this->radius = $radius;
+
+ $this->startRadian = M_PI * $percentStart/50;
+ $this->endRadian = M_PI * ($percent + $percentStart)/50;
+ }
+
+ /**
+ * Create the path for the pie slice
+ *
+ * @param int $x The x position of the pie slice
+ * @param int $y The y position of the pie slice
+ * @param int $r The absolute radius of the pie slice
+ *
+ * @return string A SVG path string
+ */
+ private function getPieSlicePath($x, $y, $r)
+ {
+ // The coordinate system is mirrored on the Y axis, so we have to flip cos and sin
+ $xStart = $x + ($r * sin($this->startRadian));
+ $yStart = $y - ($r * cos($this->startRadian));
+
+ if ($this->endRadian - $this->startRadian == 2*M_PI) {
+ // To draw a full circle, adjust arc endpoint by a small (unvisible) value
+ $this->endRadian -= 0.001;
+ $pathString = 'M ' . Format::formatSVGNumber($xStart) . ' ' . Format::formatSVGNumber($yStart);
+ } else {
+ // Start at the center of the pieslice
+ $pathString = 'M ' . $x . ' ' . $y;
+ // Draw a straight line to the upper part of the arc
+ $pathString .= ' L ' . Format::formatSVGNumber($xStart) . ' ' . Format::formatSVGNumber($yStart);
+ }
+
+ // Instead of directly connecting the upper part of the arc (leaving a triangle), draw a bow with the radius
+ $pathString .= ' A ' . Format::formatSVGNumber($r) . ' ' . Format::formatSVGNumber($r);
+ // These are the flags for the bow, see the SVG path documentation for details
+ // http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
+ $pathString .= ' 0 ' . (($this->endRadian - $this->startRadian > M_PI) ? '1' : '0 ') . ' 1';
+
+ // xEnd and yEnd are the lower point of the arc
+ $xEnd = $x + ($r * sin($this->endRadian));
+ $yEnd = $y - ($r * cos($this->endRadian));
+ $pathString .= ' ' . Format::formatSVGNumber($xEnd) . ' ' . Format::formatSVGNumber($yEnd);
+
+ return $pathString;
+ }
+
+ /**
+ * Draw the label handler and the text for this pie slice
+ *
+ * @param RenderContext $ctx The rendering context to use for coordinate translation
+ * @param int $r The radius of the pie in absolute coordinates
+ *
+ * @return DOMElement The group DOMElement containing the handle and label
+ */
+ private function drawDescriptionLabel(RenderContext $ctx, $r)
+ {
+ $group = $ctx->getDocument()->createElement('g');
+ $rOuter = ($ctx->xToAbsolute($this->outerCaptionBound) + $ctx->yToAbsolute($this->outerCaptionBound)) / 2;
+ $addOffset = $rOuter - $r ;
+ if ($addOffset < 0) {
+ $addOffset = 0;
+ }
+ list($x, $y) = $ctx->toAbsolute($this->x, $this->y);
+ $midRadius = $this->startRadian + ($this->endRadian - $this->startRadian) / 2;
+ list($offsetX, $offsetY) = $ctx->toAbsolute($this->captionOffset, $this->captionOffset);
+
+ $midX = $x + intval(($offsetX + $r)/2 * sin($midRadius));
+ $midY = $y - intval(($offsetY + $r)/2 * cos($midRadius));
+
+ // Draw the handle
+ $path = new Path(array($midX, $midY));
+
+ $midX += ($addOffset + $r/3) * ($midRadius > M_PI ? -1 : 1);
+ $path->append(array($midX, $midY))->toAbsolute();
+
+ $midX += intval($r/2 * sin(M_PI/9)) * ($midRadius > M_PI ? -1 : 1);
+ $midY -= intval($r/2 * cos(M_PI/3)) * ($midRadius < M_PI*1.4 && $midRadius > M_PI/3 ? -1 : 1);
+
+ if ($ctx->yToRelative($midY) > 100) {
+ $midY = $ctx->yToAbsolute(100);
+ } elseif ($ctx->yToRelative($midY) < 0) {
+ $midY = $ctx->yToAbsolute($ctx->yToRelative(100+$midY));
+ }
+
+ $path->append(array($midX , $midY));
+ $rel = $ctx->toRelative($midX, $midY);
+
+ // Draw the text box
+ $text = new Text($rel[0]+1.5, $rel[1], $this->caption);
+ $text->setFontSize('5em');
+ $text->setAlignment(($midRadius > M_PI ? Text::ALIGN_END : Text::ALIGN_START));
+
+ $group->appendChild($path->toSvg($ctx));
+ $group->appendChild($text->toSvg($ctx));
+
+ return $group;
+ }
+
+ /**
+ * Set the x position of the pie slice
+ *
+ * @param int $x The new x position
+ *
+ * @return $this Fluid interface
+ */
+ public function setX($x)
+ {
+ $this->x = $x;
+ return $this;
+ }
+
+ /**
+ * Set the y position of the pie slice
+ *
+ * @param int $y The new y position
+ *
+ * @return $this Fluid interface
+ */
+ public function setY($y)
+ {
+ $this->y = $y;
+ return $this;
+ }
+
+ /**
+ * Set a root element to be used for drawing labels
+ *
+ * @param DOMElement $group The label group
+ *
+ * @return $this Fluid interface
+ */
+ public function setLabelGroup(DOMElement $group)
+ {
+ $this->labelGroup = $group;
+ return $this;
+ }
+
+ /**
+ * Set the caption for this label
+ *
+ * @param string $caption The caption for this element
+ *
+ * @return $this Fluid interface
+ */
+ public function setCaption($caption)
+ {
+ $this->caption = $caption;
+ return $this;
+ }
+
+ /**
+ * Set the internal offset of the caption handle
+ *
+ * @param int $offset The offset for the caption handle
+ *
+ * @return $this Fluid interface
+ */
+ public function setCaptionOffset($offset)
+ {
+ $this->captionOffset = $offset;
+ return $this;
+ }
+
+ /**
+ * Set the minimum radius to be used for drawing labels
+ *
+ * @param int $bound The offset for the caption text
+ *
+ * @return $this Fluid interface
+ */
+ public function setOuterCaptionBound($bound)
+ {
+ $this->outerCaptionBound = $bound;
+ return $this;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $doc = $ctx->getDocument();
+ $group = $doc->createElement('g');
+ $r = ($ctx->xToAbsolute($this->radius) + $ctx->yToAbsolute($this->radius)) / 2;
+ list($x, $y) = $ctx->toAbsolute($this->x, $this->y);
+
+ $slicePath = $doc->createElement('path');
+
+ $slicePath->setAttribute('d', $this->getPieSlicePath($x, $y, $r));
+ $slicePath->setAttribute('data-icinga-graph-type', 'pieslice');
+
+ $id = $this->id ?? uniqid('slice-');
+ $slicePath->setAttribute('id', $id);
+ $this->setId($id);
+
+ $style = new DOMDocument();
+ $style->loadHTML($this->getStyle());
+
+ $slicePath->appendChild(
+ $slicePath->ownerDocument->importNode(
+ $style->getElementsByTagName('style')->item(0),
+ true
+ )
+ );
+
+ $this->applyAttributes($slicePath);
+ $group->appendChild($slicePath);
+ if ($this->caption != "") {
+ $lblGroup = ($this->labelGroup ? $this->labelGroup : $group);
+ $lblGroup->appendChild($this->drawDescriptionLabel($ctx, $r));
+ }
+ return $group;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/RawElement.php b/library/Icinga/Chart/Primitive/RawElement.php
new file mode 100644
index 0000000..721b6e0
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/RawElement.php
@@ -0,0 +1,43 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMElement;
+use Icinga\Chart\Render\RenderContext;
+
+/**
+ * Wrapper for raw elements to be added as Drawable's
+ */
+class RawElement implements Drawable
+{
+
+ /**
+ * The DOMElement wrapped by this Drawable
+ *
+ * @var DOMElement
+ */
+ private $domEl;
+
+ /**
+ * Create this RawElement
+ *
+ * @param DOMElement $el The element to wrap here
+ */
+ public function __construct(DOMElement $el)
+ {
+ $this->domEl = $el;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ return $this->domEl;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Rect.php b/library/Icinga/Chart/Primitive/Rect.php
new file mode 100644
index 0000000..0c0835f
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Rect.php
@@ -0,0 +1,119 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Primitive;
+
+use DOMElement;
+use DOMDocument;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Format;
+
+/**
+ * Drawable representing the SVG rect element
+ */
+class Rect extends Animatable implements Drawable
+{
+ /**
+ * The x position
+ *
+ * @var int
+ */
+ private $x;
+
+ /**
+ * The y position
+ *
+ * @var int
+ */
+ private $y;
+
+ /**
+ * The width of this rect
+ *
+ * @var int
+ */
+ private $width;
+
+ /**
+ * The height of this rect
+ *
+ * @var int
+ */
+ private $height;
+
+ /**
+ * Whether to keep the ratio
+ *
+ * @var bool
+ */
+ private $keepRatio = false;
+
+ /**
+ * Create this rect
+ *
+ * @param int $x The x position of the rect
+ * @param int $y The y position of the rectangle
+ * @param int $width The width of the rectangle
+ * @param int $height The height of the rectangle
+ */
+ public function __construct($x, $y, $width, $height)
+ {
+ $this->x = $x;
+ $this->y = $y;
+ $this->width = $width;
+ $this->height = $height;
+ }
+
+ /**
+ * Call to let the rectangle keep the ratio
+ */
+ public function keepRatio()
+ {
+ $this->keepRatio = true;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $doc = $ctx->getDocument();
+ $rect = $doc->createElement('rect');
+
+ list($x, $y) = $ctx->toAbsolute($this->x, $this->y);
+ if ($this->keepRatio) {
+ $ctx->keepRatio();
+ }
+ list($width, $height) = $ctx->toAbsolute($this->width, $this->height);
+ if ($this->keepRatio) {
+ $ctx->ignoreRatio();
+ }
+ $rect->setAttribute('x', Format::formatSVGNumber($x));
+ $rect->setAttribute('y', Format::formatSVGNumber($y));
+ $rect->setAttribute('width', Format::formatSVGNumber($width));
+ $rect->setAttribute('height', Format::formatSVGNumber($height));
+
+ $id = $this->id ?? uniqid('rect-');
+ $rect->setAttribute('id', $id);
+ $this->setId($id);
+
+ $this->applyAttributes($rect);
+ $this->appendAnimation($rect, $ctx);
+
+ $style = new DOMDocument();
+ $style->loadHTML($this->getStyle());
+
+ $rect->appendChild(
+ $rect->ownerDocument->importNode(
+ $style->getElementsByTagName('style')->item(0),
+ true
+ )
+ );
+
+ return $rect;
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Styleable.php b/library/Icinga/Chart/Primitive/Styleable.php
new file mode 100644
index 0000000..15025bf
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Styleable.php
@@ -0,0 +1,161 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Primitive;
+
+use DOMElement;
+use Icinga\Util\Csp;
+use ipl\Web\Style;
+
+/**
+ * Base class for stylable drawables
+ */
+class Styleable
+{
+
+ /**
+ * The stroke width to use
+ *
+ * @var int|float
+ */
+ public $strokeWidth = 0;
+
+ /**
+ * The stroke color to use
+ *
+ * @var string
+ */
+ public $strokeColor = '#000';
+
+ /**
+ * The fill color to use
+ *
+ * @var string
+ */
+ public $fill = 'none';
+
+ /**
+ * Additional styles to be appended to the style attribute
+ *
+ * @var array<string, string>
+ */
+ public $additionalStyle = [];
+
+ /**
+ * The id of this element
+ *
+ * @var ?string
+ */
+ public $id = null;
+
+ /**
+ * Additional attributes to be set
+ *
+ * @var array
+ */
+ public $attributes = array();
+
+ /**
+ * Set the stroke width for this drawable
+ *
+ * @param int|float $width The stroke with unit
+ *
+ * @return $this Fluid interface
+ */
+ public function setStrokeWidth($width)
+ {
+ $this->strokeWidth = $width;
+ return $this;
+ }
+
+ /**
+ * Set the color for the stroke or none for no stroke
+ *
+ * @param string $color The color to set for the stroke
+ *
+ * @return $this Fluid interface
+ */
+ public function setStrokeColor($color)
+ {
+ $this->strokeColor = $color ? $color : 'none';
+ return $this;
+ }
+
+ /**
+ * Set additional styles for this drawable
+ *
+ * @param array<string, string> $styles The styles to set additionally
+ *
+ * @return $this Fluid interface
+ */
+ public function setAdditionalStyle($styles)
+ {
+ $this->additionalStyle = $styles;
+ return $this;
+ }
+
+ /**
+ * Set the fill for this styleable
+ *
+ * @param string $color The color to use for filling or null to use no fill
+ *
+ * @return $this Fluid interface
+ */
+ public function setFill($color = null)
+ {
+ $this->fill = $color ? $color : 'none';
+ return $this;
+ }
+
+ /**
+ * Set the id for this element
+ *
+ * @param string $id The id to set for this element
+ *
+ * @return $this Fluid interface
+ */
+ public function setId($id)
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ /**
+ * Return the ruleset used for styling the DOMNode
+ *
+ * @return Style A ruleset containing styles
+ */
+ public function getStyle()
+ {
+ $styles = $this->additionalStyle;
+ $styles['fill'] = $this->fill;
+ $styles['stroke'] = $this->strokeColor;
+ $styles['stroke-width'] = (string) $this->strokeWidth;
+
+ return (new Style())
+ ->setNonce(Csp::getStyleNonce())
+ ->add("#$this->id", $styles);
+ }
+
+ /**
+ * Add an additional attribute to this element
+ */
+ public function setAttribute($key, $value)
+ {
+ $this->attributes[$key] = $value;
+ }
+
+ /**
+ * Apply attribute to a DOMElement
+ *
+ * @param DOMElement $el Element to apply attributes
+ */
+ protected function applyAttributes(DOMElement $el)
+ {
+ foreach ($this->attributes as $name => $value) {
+ $el->setAttribute($name, $value);
+ }
+ }
+}
diff --git a/library/Icinga/Chart/Primitive/Text.php b/library/Icinga/Chart/Primitive/Text.php
new file mode 100644
index 0000000..f6bf365
--- /dev/null
+++ b/library/Icinga/Chart/Primitive/Text.php
@@ -0,0 +1,184 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Primitive;
+
+use DOMDocument;
+use DOMElement;
+use DOMText;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Format;
+use ipl\Html\HtmlDocument;
+
+/**
+ * Wrapper for the SVG text element
+ */
+class Text extends Styleable implements Drawable
+{
+ /**
+ * Align the text to end at the x and y position
+ */
+ const ALIGN_END = 'end';
+
+ /**
+ * Align the text to start at the x and y position
+ */
+ const ALIGN_START = 'start';
+
+ /**
+ * Align the text to be centered at the x and y position
+ */
+ const ALIGN_MIDDLE = 'middle';
+
+ /**
+ * The x position of the Text
+ *
+ * @var int
+ */
+ private $x;
+
+ /**
+ * The y position of the Text
+ *
+ * @var int
+ */
+ private $y;
+
+ /**
+ * The text content
+ *
+ * @var string
+ */
+ private $text;
+
+ /**
+ * The size of the font
+ *
+ * @var string
+ */
+ private $fontSize = '1.5em';
+
+ /**
+ * The weight of the font
+ *
+ * @var string
+ */
+ private $fontWeight = 'normal';
+
+ /**
+ * The default fill color
+ *
+ * @var string
+ */
+ public $fill = '#000';
+
+ /**
+ * The alignment of the text
+ *
+ * @var string
+ */
+ private $alignment = self::ALIGN_START;
+
+ /**
+ * Set the font-stretch property of the text
+ */
+ private $fontStretch = 'semi-condensed';
+
+ /**
+ * Construct a new text drawable
+ *
+ * @param int $x The x position of the text
+ * @param int $y The y position of the text
+ * @param string $text The text this component should contain
+ * @param string $fontSize The font size of the text
+ */
+ public function __construct($x, $y, $text, $fontSize = '1.5em')
+ {
+ $this->x = $x;
+ $this->y = $y;
+ $this->text = $text;
+ $this->fontSize = $fontSize;
+
+ $this->setAdditionalStyle([
+ 'font-size' => $this->fontSize,
+ 'font-family' => 'Ubuntu, Calibri, Trebuchet MS, Helvetica, Verdana, sans-serif',
+ 'font-weight' => $this->fontWeight,
+ 'font-stretch' => $this->fontStretch,
+ 'font-style' => 'normal',
+ 'text-anchor' => $this->alignment
+ ]);
+ }
+
+ /**
+ * Set the font size of the svg text element
+ *
+ * @param string $size The font size including a unit
+ *
+ * @return $this Fluid interface
+ */
+ public function setFontSize($size)
+ {
+ $this->fontSize = $size;
+ return $this;
+ }
+
+ /**
+ * Set the text alignment with one of the ALIGN_* constants
+ *
+ * @param String $align Value how to align
+ *
+ * @return $this Fluid interface
+ */
+ public function setAlignment($align)
+ {
+ $this->alignment = $align;
+ return $this;
+ }
+
+ /**
+ * Set the weight of the current font
+ *
+ * @param string $weight The weight of the string
+ *
+ * @return $this Fluid interface
+ */
+ public function setFontWeight($weight)
+ {
+ $this->fontWeight = $weight;
+ return $this;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ list($x, $y) = $ctx->toAbsolute($this->x, $this->y);
+ $text = $ctx->getDocument()->createElement('text');
+ $text->setAttribute('x', Format::formatSVGNumber($x - 15));
+
+ $id = $this->id ?? uniqid('text-');
+ $text->setAttribute('id', $id);
+ $this->setId($id);
+
+ $text->setAttribute('y', Format::formatSVGNumber($y));
+ $text->appendChild(new DOMText($this->text));
+
+ $style = new DOMDocument();
+ $style->loadHTML($this->getStyle());
+
+ $text->appendChild(
+ $text->ownerDocument->importNode(
+ $style->getElementsByTagName('style')->item(0),
+ true
+ )
+ );
+
+ return $text;
+ }
+}
diff --git a/library/Icinga/Chart/Render/LayoutBox.php b/library/Icinga/Chart/Render/LayoutBox.php
new file mode 100644
index 0000000..fa49461
--- /dev/null
+++ b/library/Icinga/Chart/Render/LayoutBox.php
@@ -0,0 +1,200 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Render;
+
+use Icinga\Chart\Format;
+
+/**
+ * Layout class encapsulating size, padding and margin information
+ */
+class LayoutBox
+{
+ /**
+ * Padding index for top padding
+ */
+ const PADDING_TOP = 0;
+
+ /**
+ * Padding index for right padding
+ */
+ const PADDING_RIGHT = 1;
+
+ /**
+ * Padding index for bottom padding
+ */
+ const PADDING_BOTTOM = 2;
+
+ /**
+ * Padding index for left padding
+ */
+ const PADDING_LEFT = 3;
+
+ /**
+ * The height of this layout element
+ *
+ * @var int
+ */
+ private $height;
+
+ /**
+ * The width of this layout element
+ *
+ * @var int
+ */
+ private $width;
+
+ /**
+ * The x position of this layout
+ *
+ * @var int
+ */
+ private $x;
+
+ /**
+ * The y position of this layout
+ *
+ * @var int
+ */
+ private $y;
+
+ /**
+ * The padding of this layout
+ *
+ * @var array
+ */
+ private $padding = array(0, 0, 0, 0);
+
+ /**
+ * Create this layout box
+ *
+ * Note that x, y, width and height are relative: x with 0 means leftmost, x with 100 means rightmost
+ *
+ * @param int $x The relative x coordinate
+ * @param int $y The relative y coordinate
+ * @param int $width The optional, relative width
+ * @param int $height The optional, relative height
+ */
+ public function __construct($x, $y, $width = null, $height = null)
+ {
+ $this->height = $height ? $height : 100;
+ $this->width = $width ? $width : 100;
+ $this->x = $x;
+ $this->y = $y;
+ }
+
+ /**
+ * Set a padding to all four sides uniformly
+ *
+ * @param int $padding The padding to set for all four sides
+ */
+ public function setUniformPadding($padding)
+ {
+ $this->padding = array($padding, $padding, $padding, $padding);
+ }
+
+ /**
+ * Set the padding for this LayoutBox
+ *
+ * @param int $top The top side padding
+ * @param int $right The right side padding
+ * @param int $bottom The bottom side padding
+ * @param int $left The left side padding
+ */
+ public function setPadding($top, $right, $bottom, $left)
+ {
+ $this->padding = array($top, $right, $bottom, $left);
+ }
+
+ /**
+ * Return a string containing the SVG transform attribute values for the padding
+ *
+ * @param RenderContext $ctx The context to determine the translation coordinates
+ *
+ * @return string The transformation string
+ */
+ public function getInnerTransform(RenderContext $ctx)
+ {
+ list($translateX, $translateY) = $ctx->toAbsolute(
+ $this->padding[self::PADDING_LEFT] + $this->getX(),
+ $this->padding[self::PADDING_TOP] + $this->getY()
+ );
+ list($scaleX, $scaleY) = $ctx->paddingToScaleFactor($this->padding);
+
+ $scaleX *= $this->getWidth()/100;
+ $scaleY *= $this->getHeight()/100;
+ return sprintf(
+ 'translate(%s, %s) scale(%s, %s)',
+ Format::formatSVGNumber($translateX),
+ Format::formatSVGNumber($translateY),
+ Format::formatSVGNumber($scaleX),
+ Format::formatSVGNumber($scaleY)
+ );
+ }
+
+ /**
+ * String representation for this Layout, for debug purposes
+ *
+ * @return string A string containing the bounds of this LayoutBox
+ */
+ public function __toString()
+ {
+ return sprintf(
+ 'Rectangle: x: %s y: %s, height: %s, width: %s',
+ $this->x,
+ $this->y,
+ $this->height,
+ $this->width
+ );
+ }
+
+ /**
+ * Return a four element array with the padding
+ *
+ * @return array The padding of this LayoutBox
+ */
+ public function getPadding()
+ {
+ return $this->padding;
+ }
+
+ /**
+ * Return the height of this LayoutBox
+ *
+ * @return int The height of this box
+ */
+ public function getHeight()
+ {
+ return $this->height;
+ }
+
+ /**
+ * Return the width of this LayoutBox
+ *
+ * @return int The width of this box
+ */
+ public function getWidth()
+ {
+ return $this->width;
+ }
+
+ /**
+ * Return the x position of this LayoutBox
+ *
+ * @return int The x position of this box
+ */
+ public function getX()
+ {
+ return $this->x;
+ }
+
+ /**
+ * Return the y position of this LayoutBox
+ *
+ * @return int The y position of this box
+ */
+ public function getY()
+ {
+ return $this->y;
+ }
+}
diff --git a/library/Icinga/Chart/Render/RenderContext.php b/library/Icinga/Chart/Render/RenderContext.php
new file mode 100644
index 0000000..457fbf3
--- /dev/null
+++ b/library/Icinga/Chart/Render/RenderContext.php
@@ -0,0 +1,225 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Render;
+
+use DOMDocument;
+
+/**
+ * Context for rendering, handles ratio based coordinate calculations.
+ *
+ * The most important functions when rendering are the toAbsolute and roRelative
+ * values, taking world coordinates and translating them into local coordinates.
+ */
+class RenderContext
+{
+
+ /**
+ * The base size of the viewport, i.e. how many units are available on a 1:1 ratio
+ *
+ * @var array
+ */
+ private $viewBoxSize = array(1000, 1000);
+
+
+ /**
+ * The DOMDocument for modifying the elements
+ *
+ * @var DOMDocument
+ */
+ private $document;
+
+ /**
+ * If true no ratio correction will be made
+ *
+ * @var bool
+ */
+ private $respectRatio = false;
+
+ /**
+ * The ratio on the x side. A x ration of 2 means that the width of the SVG is divided in 2000
+ * units (see $viewBox)
+ *
+ * @var int
+ */
+ private $xratio = 1;
+
+ /**
+ * The ratio on the y side. A y ration of 2 means that the height of the SVG is divided in 2000
+ * units (see $viewBox)
+ *
+ * @var int
+ */
+ private $yratio = 1;
+
+ /**
+ * Creates a new context for the given DOM Document
+ *
+ * @param DOMDocument $document The DOM document represented by this context
+ * @param int $width The width (may be approximate) of the document
+ * (only required for ratio calculation)
+ * @param int $height The height (may be approximate) of the document
+ * (only required for ratio calculation)
+ */
+ public function __construct(DOMDocument $document, $width, $height)
+ {
+ $this->document = $document;
+ if ($width > $height) {
+ $this->xratio = $width / $height;
+ } elseif ($height > $width) {
+ $this->yratio = $height / $width;
+ }
+ }
+
+ /**
+ * Return the document represented by this Rendering context
+ *
+ * @return DOMDocument The DOMDocument for creating files
+ */
+ public function getDocument()
+ {
+ return $this->document;
+ }
+
+ /**
+ * Let successive toAbsolute operations ignore ratio correction
+ *
+ * This can be called to avoid distortion on certain elements like rectangles.
+ */
+ public function keepRatio()
+ {
+ $this->respectRatio = true;
+ }
+
+ /**
+ * Let successive toAbsolute operations perform ratio correction
+ *
+ * This will cause distortion on certain elements like rectangles.
+ */
+ public function ignoreRatio()
+ {
+ $this->respectRatio = false;
+ }
+
+ /**
+ * Return how many unit s are available in the Y axis
+ *
+ * @return int The number of units available on the y axis
+ */
+ public function getNrOfUnitsY()
+ {
+ return intval($this->viewBoxSize[1] * $this->yratio);
+ }
+
+ /**
+ * Return how many unit s are available in the X axis
+ *
+ * @return int The number of units available on the x axis
+ */
+ public function getNrOfUnitsX()
+ {
+ return intval($this->viewBoxSize[0] * $this->xratio);
+ }
+
+ /**
+ * Transforms the x,y coordinate from relative coordinates to absolute world coordinates
+ *
+ * (50, 50) would be a point in the middle of the document and map to 500, 1000 on a
+ * 1000 x 1000 viewbox with a 1:2 ratio.
+ *
+ * @param int $x The relative x coordinate
+ * @param int $y The relative y coordinate
+ *
+ * @return array An x,y tuple containing absolute coordinates
+ * @see RenderContext::toRelative
+ */
+ public function toAbsolute($x, $y)
+ {
+ return array($this->xToAbsolute($x), $this->yToAbsolute($y));
+ }
+
+ /**
+ * Transforms the x,y coordinate from absolute coordinates to relative world coordinates
+ *
+ * This is the inverse function of toAbsolute
+ *
+ * @param int $x The absolute x coordinate
+ * @param int $y The absolute y coordinate
+ *
+ * @return array An x,y tupel containing absolute coordinates
+ * @see RenderContext::toAbsolute
+ */
+ public function toRelative($x, $y)
+ {
+ return array($this->xToRelative($x), $this->yToRelative($y));
+ }
+
+ /**
+ * Calculates the scale transformation required to apply the padding on an Canvas
+ *
+ * @param array $padding A 4 element array containing top, right, bottom and left padding
+ *
+ * @return array An array containing the x and y scale
+ */
+ public function paddingToScaleFactor(array $padding)
+ {
+ list($horizontalPadding, $verticalPadding) = $this->toAbsolute(
+ $padding[LayoutBox::PADDING_RIGHT] + $padding[LayoutBox::PADDING_LEFT],
+ $padding[LayoutBox::PADDING_TOP] + $padding[LayoutBox::PADDING_BOTTOM]
+ );
+
+ return array(
+ ($this->getNrOfUnitsX() - $horizontalPadding) / $this->getNrOfUnitsX(),
+ ($this->getNrOfUnitsY() - $verticalPadding) / $this->getNrOfUnitsY()
+ );
+ }
+
+ /**
+ * Transform a relative x coordinate to an absolute one
+ *
+ * @param int $x A relative x coordinate
+ *
+ * @return int An absolute x coordinate
+ **/
+ public function xToAbsolute($x)
+ {
+ return $this->getNrOfUnitsX() / 100 * $x / ($this->respectRatio ? $this->xratio : 1);
+ }
+
+ /**
+ * Transform a relative y coordinate to an absolute one
+ *
+ * @param int $y A relative y coordinate
+ *
+ * @return int An absolute y coordinate
+ */
+ public function yToAbsolute($y)
+ {
+ return $this->getNrOfUnitsY() / 100 * $y / ($this->respectRatio ? $this->yratio : 1);
+ }
+
+ /**
+ * Transform a absolute x coordinate to an relative one
+ *
+ * @param int $x An absolute x coordinate
+ *
+ * @return int A relative x coordinate
+ */
+ public function xToRelative($x)
+ {
+ return $x / $this->getNrOfUnitsX() * 100 * ($this->respectRatio ? $this->xratio : 1);
+ }
+
+ /**
+ * Transform a absolute y coordinate to an relative one
+ *
+ * @param int $y An absolute x coordinate
+ *
+ * @return int A relative x coordinate
+ */
+ public function yToRelative($y)
+ {
+ return $y / $this->getNrOfUnitsY() * 100 * ($this->respectRatio ? $this->yratio : 1);
+ }
+}
diff --git a/library/Icinga/Chart/Render/Rotator.php b/library/Icinga/Chart/Render/Rotator.php
new file mode 100644
index 0000000..3e7071c
--- /dev/null
+++ b/library/Icinga/Chart/Render/Rotator.php
@@ -0,0 +1,80 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Render;
+
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Primitive\Drawable;
+use DOMElement;
+
+/**
+ * Class Rotator
+ * @package Icinga\Chart\Render
+ */
+class Rotator implements Drawable
+{
+ /**
+ * The drawable element to rotate
+ *
+ * @var Drawable
+ */
+ private $element;
+
+ /**
+ * @var int
+ */
+ private $degrees;
+
+ /**
+ * Wrap an element into a new instance of Rotator
+ *
+ * @param Drawable $element The element to rotate
+ * @param int $degrees The amount of degrees
+ */
+ public function __construct(Drawable $element, $degrees)
+ {
+ $this->element = $element;
+ $this->degrees = $degrees;
+ }
+
+ /**
+ * Rotate the given element.
+ *
+ * @param RenderContext $ctx The rendering context
+ * @param DOMElement $el The element to rotate
+ * @param $degrees The amount of degrees
+ *
+ * @return DOMElement The rotated DOMElement
+ */
+ private function rotate(RenderContext $ctx, DOMElement $el, $degrees)
+ {
+ // Create a box containing the rotated element relative to the original element position
+ $container = $ctx->getDocument()->createElement('g');
+ $x = $el->getAttribute('x');
+ $y = $el->getAttribute('y');
+ $container->setAttribute('transform', 'translate(' . $x . ',' . $y . ')');
+ $el->removeAttribute('x');
+ $el->removeAttribute('y');
+
+ // Put the element into a rotated group
+ //$rotate = $ctx->getDocument()->createElement('g');
+ $el->setAttribute('transform', 'rotate(' . $degrees . ')');
+ //$rotate->appendChild($el);
+
+ $container->appendChild($el);
+ return $container;
+ }
+
+ /**
+ * Create the SVG representation from this Drawable
+ *
+ * @param RenderContext $ctx The context to use for rendering
+ *
+ * @return DOMElement The SVG Element
+ */
+ public function toSvg(RenderContext $ctx)
+ {
+ $el = $this->element->toSvg($ctx);
+ return $this->rotate($ctx, $el, $this->degrees);
+ }
+}
diff --git a/library/Icinga/Chart/SVGRenderer.php b/library/Icinga/Chart/SVGRenderer.php
new file mode 100644
index 0000000..d3891f2
--- /dev/null
+++ b/library/Icinga/Chart/SVGRenderer.php
@@ -0,0 +1,331 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart;
+
+use DOMNode;
+use DOMElement;
+use DOMDocument;
+use DOMImplementation;
+use Icinga\Chart\Render\LayoutBox;
+use Icinga\Chart\Render\RenderContext;
+use Icinga\Chart\Primitive\Canvas;
+
+/**
+ * SVG Renderer component.
+ *
+ * Creates the basic DOM tree of the SVG to use
+ */
+class SVGRenderer
+{
+ const X_ASPECT_RATIO_MIN = 'xMin';
+
+ const X_ASPECT_RATIO_MID = 'xMid';
+
+ const X_ASPECT_RATIO_MAX = 'xMax';
+
+ const Y_ASPECT_RATIO_MIN = 'YMin';
+
+ const Y_ASPECT_RATIO_MID = 'YMid';
+
+ const Y_ASPECT_RATIO_MAX = 'YMax';
+
+ const ASPECT_RATIO_PAD = 'meet';
+
+ const ASPECT_RATIO_CUTOFF = 'slice';
+
+ /**
+ * The XML-document
+ *
+ * @var DOMDocument
+ */
+ private $document;
+
+ /**
+ * The SVG-element
+ *
+ * @var DOMNode
+ */
+ private $svg;
+
+ /**
+ * The description of this SVG, useful for screen readers
+ *
+ * @var string
+ */
+ private $ariaDescription;
+
+ /**
+ * The title of this SVG, useful for screen readers
+ *
+ * @var string
+ */
+ private $ariaTitle;
+
+ /**
+ * The aria role used by this svg element
+ *
+ * @var string
+ */
+ private $ariaRole = 'img';
+
+ /**
+ * The root layer for all elements
+ *
+ * @var Canvas
+ */
+ private $rootCanvas;
+
+ /**
+ * The width of this renderer
+ *
+ * @var int
+ */
+ private $width = 100;
+
+ /**
+ * The height of this renderer
+ *
+ * @var int
+ */
+ private $height = 100;
+
+ /**
+ * Whether the aspect ratio is preversed
+ *
+ * @var bool
+ */
+ private $preserveAspectRatio = false;
+
+ /**
+ * Horizontal alignment of SVG element
+ *
+ * @var string
+ */
+ private $xAspectRatio = self::X_ASPECT_RATIO_MID;
+
+ /**
+ * Vertical alignment of SVG element
+ *
+ * @var string
+ */
+ private $yAspectRatio = self::Y_ASPECT_RATIO_MID;
+
+ /**
+ * Define whether aspect differences should be handled using padding (default) or cutoff
+ *
+ * @var string
+ */
+ private $xFillMode = "meet";
+
+
+ /**
+ * Create the root document and the SVG root node
+ */
+ private function createRootDocument()
+ {
+ $implementation = new DOMImplementation();
+ $docType = $implementation->createDocumentType(
+ 'svg',
+ '-//W3C//DTD SVG 1.1//EN',
+ 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'
+ );
+
+ $this->document = $implementation->createDocument(null, '', $docType);
+ $this->svg = $this->createOuterBox();
+ $this->document->appendChild($this->svg);
+ }
+
+ /**
+ * Create the outer SVG box containing the root svg element and namespace and return it
+ *
+ * @return DOMElement The SVG root node
+ */
+ private function createOuterBox()
+ {
+ $ctx = $this->createRenderContext();
+ $svg = $this->document->createElement('svg');
+ $svg->setAttribute('xmlns', 'http://www.w3.org/2000/svg');
+ $svg->setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
+ $svg->setAttribute('role', $this->ariaRole);
+ $svg->setAttribute('width', '100%');
+ $svg->setAttribute('height', '100%');
+ $svg->setAttribute(
+ 'viewBox',
+ sprintf(
+ '0 0 %s %s',
+ $ctx->getNrOfUnitsX(),
+ $ctx->getNrOfUnitsY()
+ )
+ );
+ if ($this->preserveAspectRatio) {
+ $svg->setAttribute(
+ 'preserveAspectRatio',
+ sprintf(
+ '%s%s %s',
+ $this->xAspectRatio,
+ $this->yAspectRatio,
+ $this->xFillMode
+ )
+ );
+ }
+ return $svg;
+ }
+
+ /**
+ * Add aria title and description
+ *
+ * Adds an aria title and desc element to the given SVG node, which are used to describe this SVG by accessibility
+ * tools such as screen readers.
+ *
+ * @param DOMNode $svg The SVG DOMNode to which the aria attributes should be attached
+ * @param $title The title text
+ * @param $description The description text
+ */
+ private function addAriaDescription(DOMNode $svg, $titleText, $descriptionText)
+ {
+ $doc = $svg->ownerDocument;
+
+ $titleId = $descId = '';
+ if (isset($this->ariaTitle)) {
+ $titleId = 'aria-title-' . $this->stripNonAlphanumeric($titleText);
+ $title = $doc->createElement('title');
+ $title->setAttribute('id', $titleId);
+
+ $title->appendChild($doc->createTextNode($titleText));
+ $svg->appendChild($title);
+ }
+
+ if (isset($this->ariaDescription)) {
+ $descId = 'aria-desc-' . $this->stripNonAlphanumeric($descriptionText);
+ $desc = $doc->createElement('desc');
+ $desc->setAttribute('id', $descId);
+
+ $desc->appendChild($doc->createTextNode($descriptionText));
+ $svg->appendChild($desc);
+ }
+
+ $svg->setAttribute('aria-labelledby', join(' ', array($titleId, $descId)));
+ }
+
+ /**
+ * Initialises the XML-document, SVG-element and this figure's root canvas
+ *
+ * @param int $width The width ratio
+ * @param int $height The height ratio
+ */
+ public function __construct($width, $height)
+ {
+ $this->width = $width;
+ $this->height = $height;
+ $this->rootCanvas = new Canvas('root', new LayoutBox(0, 0));
+ }
+
+ /**
+ * Render the SVG-document
+ *
+ * @return string The resulting XML structure
+ */
+ public function render()
+ {
+ $this->createRootDocument();
+ $ctx = $this->createRenderContext();
+ $this->addAriaDescription($this->svg, $this->ariaTitle, $this->ariaDescription);
+ $this->svg->appendChild($this->rootCanvas->toSvg($ctx));
+ $this->document->formatOutput = true;
+ $this->document->encoding = 'UTF-8';
+ return $this->document->saveXML();
+ }
+
+ /**
+ * Create a render context that will be used for rendering elements
+ *
+ * @return RenderContext The created RenderContext instance
+ */
+ public function createRenderContext()
+ {
+ return new RenderContext($this->document, $this->width, $this->height);
+ }
+
+ /**
+ * Return the root canvas of this rendered
+ *
+ * @return Canvas The canvas that will be the uppermost element in this figure
+ */
+ public function getCanvas()
+ {
+ return $this->rootCanvas;
+ }
+
+ /**
+ * Preserve the aspect ratio of the rendered object
+ *
+ * Do not deform the content of the SVG when the aspect ratio of the viewBox
+ * differs from the aspect ratio of the SVG element, but add padding or cutoff
+ * instead
+ *
+ * @param bool $preserve Whether the aspect ratio should be preserved
+ */
+ public function preserveAspectRatio($preserve = true)
+ {
+ $this->preserveAspectRatio = $preserve;
+ }
+
+ /**
+ * Change the horizontal alignment of the SVG element
+ *
+ * Change the horizontal alignment of the svg, when preserveAspectRatio is used and
+ * padding is present. Defaults to
+ */
+ public function setXAspectRatioAlignment($alignment)
+ {
+ $this->xAspectRatio = $alignment;
+ }
+
+ /**
+ * Change the vertical alignment of the SVG element
+ *
+ * Change the vertical alignment of the svg, when preserveAspectRatio is used and
+ * padding is present.
+ */
+ public function setYAspectRatioAlignment($alignment)
+ {
+ $this->yAspectRatio = $alignment;
+ }
+
+ /**
+ * Set the aria description, that is used as a title for this SVG in screen readers
+ *
+ * @param $text
+ */
+ public function setAriaTitle($text)
+ {
+ $this->ariaTitle = $text;
+ }
+
+ /**
+ * Set the aria description, that is used to describe this SVG in screen readers
+ *
+ * @param $text
+ */
+ public function setAriaDescription($text)
+ {
+ $this->ariaDescription = $text;
+ }
+
+ /**
+ * Set the aria role, that is used to describe the purpose of this SVG in screen readers
+ *
+ * @param $text
+ */
+ public function setAriaRole($text)
+ {
+ $this->ariaRole = $text;
+ }
+
+
+ private function stripNonAlphanumeric($str)
+ {
+ return preg_replace('/[^A-Za-z]+/', '', $str);
+ }
+}
diff --git a/library/Icinga/Chart/Unit/AxisUnit.php b/library/Icinga/Chart/Unit/AxisUnit.php
new file mode 100644
index 0000000..251787f
--- /dev/null
+++ b/library/Icinga/Chart/Unit/AxisUnit.php
@@ -0,0 +1,56 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Unit;
+
+use Iterator;
+
+/**
+ * Base class for Axis Units
+ *
+ * An AxisUnit takes a set of values and places them on a given range
+ *
+ * Concrete subclasses must implement the iterator interface, with
+ * getCurrent returning the axis relative position and getValue the label
+ * that will be displayed
+ */
+interface AxisUnit extends Iterator
+{
+ /**
+ * Add a dataset to this AxisUnit, required for dynamic min and max vlaues
+ *
+ * @param array $dataset The dataset that will be shown in the Axis
+ * @param int $id The idx in the dataset (0 for x, 1 for y)
+ */
+ public function addValues(array $dataset, $id = 0);
+
+ /**
+ * Transform the given absolute value in an axis relative value
+ *
+ * @param int $value The absolute, dataset dependent value
+ *
+ * @return int An axis relative value
+ */
+ public function transform($value);
+
+ /**
+ * Set the axis minimum value to a fixed value
+ *
+ * @param int $min The new minimum value
+ */
+ public function setMin($min);
+
+ /**
+ * Set the axis maximum value to a fixed value
+ *
+ * @param int $max The new maximum value
+ */
+ public function setMax($max);
+
+ /**
+ * Get the amount of ticks of this axis
+ *
+ * @return int
+ */
+ public function getTicks();
+}
diff --git a/library/Icinga/Chart/Unit/CalendarUnit.php b/library/Icinga/Chart/Unit/CalendarUnit.php
new file mode 100644
index 0000000..74680c7
--- /dev/null
+++ b/library/Icinga/Chart/Unit/CalendarUnit.php
@@ -0,0 +1,167 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Unit;
+
+use DateTime;
+
+/**
+ * Calendar Axis Unit that transforms timestamps into user-readable values
+ *
+ */
+class CalendarUnit extends LinearUnit
+{
+ /**
+ * Constant for a minute
+ */
+ const MINUTE = 60;
+
+ /**
+ * Constant for an hour
+ */
+ const HOUR = 3600;
+
+ /**
+ * Constant for a day
+ */
+ const DAY = 864000;
+
+ /**
+ * Constant for ~a month
+ * 30 Days, this is sufficient for our needs
+ */
+ const MONTH = 2592000; // x
+
+ /**
+ * An array containing all labels that will be displayed
+ *
+ * @var array
+ */
+ private $labels = array();
+
+ /**
+ * The date format to use
+ *
+ * @var string
+ */
+ private $dateFormat = 'd-m';
+
+ /**
+ * The time format to use
+ *
+ * @var string
+ */
+ private $timeFormat = 'g:i:s';
+
+ /**
+ * Create the labels for the given dataset
+ */
+ private function createLabels()
+ {
+ $this->labels = array();
+ $duration = $this->getMax() - $this->getMin();
+
+ if ($duration <= self::HOUR) {
+ $unit = self::MINUTE;
+ } elseif ($duration <= self::DAY) {
+ $unit = self::HOUR;
+ } elseif ($duration <= self::MONTH) {
+ $unit = self::DAY;
+ } else {
+ $unit = self::MONTH;
+ }
+ $this->calculateLabels($unit);
+ }
+
+ /**
+ * Calculate the labels for this dataset
+ *
+ * @param integer $unit The unit to use as the basis for calculation
+ */
+ private function calculateLabels($unit)
+ {
+ $fac = new DateTime();
+
+ $duration = $this->getMax() - $this->getMin();
+
+ // Calculate number of ticks, but not more than 30
+ $tickCount = ($duration/$unit * 10);
+ if ($tickCount > 30) {
+ $tickCount = 30;
+ }
+
+ $step = $duration / $tickCount;
+ $format = $this->timeFormat;
+ if ($unit === self::DAY) {
+ $format = $this->dateFormat;
+ } elseif ($unit === self::MONTH) {
+ $format = $this->dateFormat;
+ }
+
+ for ($i = 0; $i <= $duration; $i += $step) {
+ $this->labels[] = $fac->setTimestamp($this->getMin() + $i)->format($format);
+ }
+ }
+
+ /**
+ * Add a dataset to this CalendarUnit and update labels
+ *
+ * @param array $dataset The dataset to update
+ * @param int $idx The index to use for determining the data
+ *
+ * @return $this Fluid interface
+ */
+ public function addValues(array $dataset, $idx = 0)
+ {
+ parent::addValues($dataset, $idx);
+ $this->createLabels();
+ return $this;
+ }
+
+ /**
+ * Return the current axis relative position
+ *
+ * @return int The position of the next tick (between 0 and 100)
+ */
+ public function current(): int
+ {
+ return 100 * (key($this->labels) / count($this->labels));
+ }
+
+ /**
+ * Move to next tick
+ */
+ public function next(): void
+ {
+ next($this->labels);
+ }
+
+ /**
+ * Return the current tick caption
+ *
+ * @return string
+ */
+ public function key(): string
+ {
+ return current($this->labels);
+ }
+
+ /**
+ * Return true when the iterator is in a valid range
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ return current($this->labels) !== false;
+ }
+
+ /**
+ * Rewind the internal array
+ */
+ public function rewind(): void
+ {
+ reset($this->labels);
+ }
+}
diff --git a/library/Icinga/Chart/Unit/LinearUnit.php b/library/Icinga/Chart/Unit/LinearUnit.php
new file mode 100644
index 0000000..ea4792b
--- /dev/null
+++ b/library/Icinga/Chart/Unit/LinearUnit.php
@@ -0,0 +1,227 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Unit;
+
+/**
+ * Linear tick distribution over the axis
+ */
+class LinearUnit implements AxisUnit
+{
+ /**
+ * The minimum value to display
+ *
+ * @var int
+ */
+ protected $min;
+
+ /**
+ * The maximum value to display
+ *
+ * @var int
+ */
+ protected $max;
+
+ /**
+ * True when the minimum value is static and isn't affected by the dataset
+ *
+ * @var bool
+ */
+ protected $staticMin = false;
+
+ /**
+ * True when the maximum value is static and isn't affected by the dataset
+ *
+ * @var bool
+ */
+ protected $staticMax = false;
+
+ /**
+ * The number of ticks to use
+ *
+ * @var int
+ */
+ protected $nrOfTicks = 10;
+
+ /**
+ * The currently displayed tick
+ *
+ * @var int
+ */
+ protected $currentTick = 0;
+
+ /**
+ * The currently displayed value
+ * @var int
+ */
+ protected $currentValue = 0;
+
+ /**
+ * Create and initialize this AxisUnit
+ *
+ * @param int $nrOfTicks The number of ticks to use
+ */
+ public function __construct($nrOfTicks = 10)
+ {
+ $this->min = PHP_INT_MAX;
+ $this->max = ~PHP_INT_MAX;
+ $this->nrOfTicks = $nrOfTicks;
+ }
+
+ /**
+ * Add a dataset and calculate the minimum and maximum value for this AxisUnit
+ *
+ * @param array $dataset The dataset to add
+ * @param int $idx The idx (0 for x, 1 for y)
+ *
+ * @return $this Fluent interface
+ */
+ public function addValues(array $dataset, $idx = 0)
+ {
+ $datapoints = array();
+
+ foreach ($dataset['data'] as $points) {
+ $datapoints[] = $points[$idx];
+ }
+ if (empty($datapoints)) {
+ return $this;
+ }
+ sort($datapoints);
+ if (!$this->staticMax) {
+ $this->max = max($this->max, $datapoints[count($datapoints) - 1]);
+ }
+ if (!$this->staticMin) {
+ $this->min = min($this->min, $datapoints[0]);
+ }
+ $this->currentTick = 0;
+ $this->currentValue = $this->min;
+ if ($this->max === $this->min) {
+ $this->max = $this->min + 10;
+ }
+ $this->nrOfTicks = $this->max - $this->min;
+ return $this;
+ }
+
+ /**
+ * Transform the absolute value to an axis relative value
+ *
+ * @param int $value The absolute coordinate from the dataset
+ * @return float|int The axis relative coordinate (between 0 and 100)
+ */
+ public function transform($value)
+ {
+ if ($value < $this->min) {
+ return 0;
+ } elseif ($value > $this->max) {
+ return 100;
+ } else {
+ return 100 * ($value - $this->min) / $this->nrOfTicks;
+ }
+ }
+
+ /**
+ * Return the position of the current tick
+ *
+ * @return int
+ */
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ return $this->currentTick;
+ }
+
+ /**
+ * Calculate the next tick and tick value
+ */
+ public function next(): void
+ {
+ $this->currentTick += (100 / $this->nrOfTicks);
+ $this->currentValue += (($this->max - $this->min) / $this->nrOfTicks);
+ }
+
+ /**
+ * Return the label for the current tick
+ *
+ * @return string The label for the current tick
+ */
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return (string) intval($this->currentValue);
+ }
+
+ /**
+ * True when we're at a valid tick (iterator interface)
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ return $this->currentTick >= 0 && $this->currentTick <= 100;
+ }
+
+ /**
+ * Reset the current tick and label value
+ */
+ public function rewind(): void
+ {
+ $this->currentTick = 0;
+ $this->currentValue = $this->min;
+ }
+
+ /**
+ * Set the axis maximum value to a fixed value
+ *
+ * @param int $max The new maximum value
+ */
+ public function setMax($max)
+ {
+ if ($max !== null) {
+ $this->max = $max;
+ $this->staticMax = true;
+ }
+ }
+
+ /**
+ * Set the axis minimum value to a fixed value
+ *
+ * @param int $min The new minimum value
+ */
+ public function setMin($min)
+ {
+ if ($min !== null) {
+ $this->min = $min;
+ $this->staticMin = true;
+ }
+ }
+
+ /**
+ * Return the current minimum value of the axis
+ *
+ * @return int The minimum set for this axis
+ */
+ public function getMin()
+ {
+ return $this->min;
+ }
+
+ /**
+ * Return the current maximum value of the axis
+ *
+ * @return int The maximum set for this axis
+ */
+ public function getMax()
+ {
+ return $this->max;
+ }
+
+ /**
+ * Get the amount of ticks necessary to display this AxisUnit
+ *
+ * @return int
+ */
+ public function getTicks()
+ {
+ return $this->nrOfTicks;
+ }
+}
diff --git a/library/Icinga/Chart/Unit/LogarithmicUnit.php b/library/Icinga/Chart/Unit/LogarithmicUnit.php
new file mode 100644
index 0000000..70961e2
--- /dev/null
+++ b/library/Icinga/Chart/Unit/LogarithmicUnit.php
@@ -0,0 +1,263 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Chart\Unit;
+
+/**
+ * Logarithmic tick distribution over the axis
+ *
+ * This class does not use the actual logarithm, but a slightly altered version called the
+ * Log-Modulo transformation. This is necessary, since a regular logarithmic scale is not able to display negative
+ * values and zero-points. See <a href="http://blogs.sas.com/content/iml/2014/07/14/log-transformation-of-pos-neg>
+ * this article </a> for a more detailed description.
+ */
+class LogarithmicUnit implements AxisUnit
+{
+ /**
+ * @var int
+ */
+ protected $base;
+
+ /**
+ * @var
+ */
+ protected $currentTick;
+
+ /**
+ * @var
+ */
+ protected $minExp;
+
+ /**
+ * @var
+ */
+ protected $maxExp;
+
+ /**
+ * True when the minimum value is static and isn't affected by the data set
+ *
+ * @var bool
+ */
+ protected $staticMin = false;
+
+ /**
+ * True when the maximum value is static and isn't affected by the data set
+ *
+ * @var bool
+ */
+ protected $staticMax = false;
+
+ /**
+ * Create and initialize this AxisUnit
+ *
+ * @param int $nrOfTicks The number of ticks to use
+ */
+ public function __construct($base = 10)
+ {
+ $this->base = $base;
+ $this->minExp = PHP_INT_MAX;
+ $this->maxExp = ~PHP_INT_MAX;
+ }
+
+ /**
+ * Add a dataset and calculate the minimum and maximum value for this AxisUnit
+ *
+ * @param array $dataset The dataset to add
+ * @param int $idx The idx (0 for x, 1 for y)
+ *
+ * @return $this Fluent interface
+ */
+ public function addValues(array $dataset, $idx = 0)
+ {
+ $datapoints = array();
+
+ foreach ($dataset['data'] as $points) {
+ $datapoints[] = $points[$idx];
+ }
+ if (empty($datapoints)) {
+ return $this;
+ }
+ sort($datapoints);
+ if (!$this->staticMax) {
+ $this->maxExp = max($this->maxExp, $this->logCeil($datapoints[count($datapoints) - 1]));
+ }
+ if (!$this->staticMin) {
+ $this->minExp = min($this->minExp, $this->logFloor($datapoints[0]));
+ }
+ $this->currentTick = 0;
+
+ return $this;
+ }
+
+ /**
+ * Transform the absolute value to an axis relative value
+ *
+ * @param int $value The absolute coordinate from the data set
+ * @return float|int The axis relative coordinate (between 0 and 100)
+ */
+ public function transform($value)
+ {
+ if ($value < $this->pow($this->minExp)) {
+ return 0;
+ } elseif ($value > $this->pow($this->maxExp)) {
+ return 100;
+ } else {
+ return 100 * ($this->log($value) - $this->minExp) / $this->getTicks();
+ }
+ }
+
+ /**
+ * Return the position of the current tick
+ *
+ * @return int
+ */
+ public function current(): int
+ {
+ return $this->currentTick * (100 / $this->getTicks());
+ }
+
+ /**
+ * Calculate the next tick and tick value
+ */
+ public function next(): void
+ {
+ ++ $this->currentTick;
+ }
+
+ /**
+ * Return the label for the current tick
+ *
+ * @return string The label for the current tick
+ */
+ public function key(): string
+ {
+ $currentBase = $this->currentTick + $this->minExp;
+ if (abs($currentBase) > 4) {
+ return $this->base . 'E' . $currentBase;
+ }
+ return (string) intval($this->pow($currentBase));
+ }
+
+ /**
+ * True when we're at a valid tick (iterator interface)
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ return $this->currentTick >= 0 && $this->currentTick < $this->getTicks();
+ }
+
+ /**
+ * Reset the current tick and label value
+ */
+ public function rewind(): void
+ {
+ $this->currentTick = 0;
+ }
+
+ /**
+ * Perform a log-modulo transformation
+ *
+ * @param $value The value to transform
+ *
+ * @return double The transformed value
+ */
+ protected function log($value)
+ {
+ $sign = $value > 0 ? 1 : -1;
+ return $sign * log1p($sign * $value) / log($this->base);
+ }
+
+ /**
+ * Calculate the biggest exponent necessary to display the given data point
+ *
+ * @param $value
+ *
+ * @return float
+ */
+ protected function logCeil($value)
+ {
+ return ceil($this->log($value)) + 1;
+ }
+
+ /**
+ * Calculate the smallest exponent necessary to display the given data point
+ *
+ * @param $value
+ *
+ * @return float
+ */
+ protected function logFloor($value)
+ {
+ return floor($this->log($value));
+ }
+
+ /**
+ * Inverse function to the log-modulo transformation
+ *
+ * @param $value
+ *
+ * @return double
+ */
+ protected function pow($value)
+ {
+ if ($value == 0) {
+ return 0;
+ }
+ $sign = $value > 0 ? 1 : -1;
+ return $sign * (pow($this->base, $sign * $value));
+ }
+
+ /**
+ * Set the axis minimum value to a fixed value
+ *
+ * @param int $min The new minimum value
+ */
+ public function setMin($min)
+ {
+ $this->minExp = $this->logFloor($min);
+ $this->staticMin = true;
+ }
+
+ /**
+ * Set the axis maximum value to a fixed value
+ *
+ * @param int $max The new maximum value
+ */
+ public function setMax($max)
+ {
+ $this->maxExp = $this->logCeil($max);
+ $this->staticMax = true;
+ }
+
+ /**
+ * Return the current minimum value of the axis
+ *
+ * @return int The minimum set for this axis
+ */
+ public function getMin()
+ {
+ return $this->pow($this->minExp);
+ }
+
+ /**
+ * Return the current maximum value of the axis
+ *
+ * @return int The maximum set for this axis
+ */
+ public function getMax()
+ {
+ return $this->pow($this->maxExp);
+ }
+
+ /**
+ * Get the amount of ticks necessary to display this AxisUnit
+ *
+ * @return int
+ */
+ public function getTicks()
+ {
+ return $this->maxExp - $this->minExp;
+ }
+}
diff --git a/library/Icinga/Chart/Unit/StaticAxis.php b/library/Icinga/Chart/Unit/StaticAxis.php
new file mode 100644
index 0000000..6b32aca
--- /dev/null
+++ b/library/Icinga/Chart/Unit/StaticAxis.php
@@ -0,0 +1,130 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Chart\Unit;
+
+class StaticAxis implements AxisUnit
+{
+ private $items = array();
+
+ /**
+ * Add a dataset to this AxisUnit, required for dynamic min and max values
+ *
+ * @param array $dataset The dataset that will be shown in the Axis
+ * @param int $idx The idx in the dataset (0 for x, 1 for y)
+ *
+ * @return $this Fluent interface
+ */
+ public function addValues(array $dataset, $idx = 0)
+ {
+ $datapoints = array();
+ foreach ($dataset['data'] as $points) {
+ $this->items[] = $points[$idx];
+ }
+ $this->items = array_unique($this->items);
+
+ return $this;
+ }
+
+ /**
+ * Transform the given absolute value in an axis relative value
+ *
+ * @param int $value The absolute, dataset dependent value
+ *
+ * @return int An axis relative value
+ */
+ public function transform($value)
+ {
+ $flipped = array_flip($this->items);
+ if (!isset($flipped[$value])) {
+ return 0;
+ }
+ $pos = $flipped[$value];
+ return 1 + (99 / count($this->items) * $pos);
+ }
+ /**
+ * Set the axis minimum value to a fixed value
+ *
+ * @param int $min The new minimum value
+ */
+ public function setMin($min)
+ {
+ }
+
+ /**
+ * Set the axis maximum value to a fixed value
+ *
+ * @param int $max The new maximum value
+ */
+ public function setMax($max)
+ {
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Return the current element
+ * @link http://php.net/manual/en/iterator.current.php
+ * @return int.
+ */
+ public function current(): int
+ {
+ return 1 + (99 / count($this->items) * key($this->items));
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Move forward to next element
+ * @link http://php.net/manual/en/iterator.next.php
+ * @return void Any returned value is ignored.
+ */
+ public function next(): void
+ {
+ next($this->items);
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Return the key of the current element
+ * @link http://php.net/manual/en/iterator.key.php
+ * @return mixed scalar on success, or null on failure.
+ */
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return current($this->items);
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Checks if current position is valid
+ * @link http://php.net/manual/en/iterator.valid.php
+ * @return boolean The return value will be casted to boolean and then evaluated.
+ * Returns true on success or false on failure.
+ */
+ public function valid(): bool
+ {
+ return current($this->items) !== false;
+ }
+
+ /**
+ * (PHP 5 &gt;= 5.0.0)<br/>
+ * Rewind the Iterator to the first element
+ * @link http://php.net/manual/en/iterator.rewind.php
+ * @return void Any returned value is ignored.
+ */
+ public function rewind(): void
+ {
+ reset($this->items);
+ }
+
+ /**
+ * Get the amount of ticks of this axis
+ *
+ * @return int
+ */
+ public function getTicks()
+ {
+ return count($this->items);
+ }
+}
diff --git a/library/Icinga/Cli/AnsiScreen.php b/library/Icinga/Cli/AnsiScreen.php
new file mode 100644
index 0000000..2780f08
--- /dev/null
+++ b/library/Icinga/Cli/AnsiScreen.php
@@ -0,0 +1,122 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Cli;
+
+use Icinga\Cli\Screen;
+use Icinga\Exception\IcingaException;
+
+// @see http://en.wikipedia.org/wiki/ANSI_escape_code
+
+class AnsiScreen extends Screen
+{
+ protected $fgColors = array(
+ 'black' => '30',
+ 'darkgray' => '1;30',
+ 'red' => '31',
+ 'lightred' => '1;31',
+ 'green' => '32',
+ 'lightgreen' => '1;32',
+ 'brown' => '33',
+ 'yellow' => '1;33',
+ 'blue' => '34',
+ 'lightblue' => '1;34',
+ 'purple' => '35',
+ 'lightpurple' => '1;35',
+ 'cyan' => '36',
+ 'lightcyan' => '1;36',
+ 'lightgray' => '37',
+ 'white' => '1;37',
+ );
+
+ protected $bgColors = array(
+ 'black' => '40',
+ 'red' => '41',
+ 'green' => '42',
+ 'brown' => '43',
+ 'blue' => '44',
+ 'purple' => '45',
+ 'cyan' => '46',
+ 'lightgray' => '47',
+ );
+
+ public function strlen($string)
+ {
+ return strlen($this->stripAnsiCodes($string));
+ }
+
+ public function stripAnsiCodes($string)
+ {
+ return preg_replace('/\e\[?.*?[\@-~]/', '', $string);
+ }
+
+ public function clear()
+ {
+ return "\033[2J" // Clear the whole screen
+ . "\033[1;1H" // Move the cursor to row 1, column 1
+ . "\033[1S"; // Scroll whole page up by 1 line (why?)
+ }
+
+ public function underline($text)
+ {
+ return "\033[4m"
+ . $text
+ . "\033[0m"; // Reset color codes
+ }
+
+ public function colorize($text, $fgColor = null, $bgColor = null)
+ {
+ return $this->startColor($fgColor, $bgColor)
+ . $text
+ . "\033[0m"; // Reset color codes
+ }
+
+ protected function fgColor($color)
+ {
+ if (! array_key_exists($color, $this->fgColors)) {
+ throw new IcingaException(
+ 'There is no such foreground color: %s',
+ $color
+ );
+ }
+ return $this->fgColors[$color];
+ }
+
+ protected function bgColor($color)
+ {
+ if (! array_key_exists($color, $this->bgColors)) {
+ throw new IcingaException(
+ 'There is no such background color: %s',
+ $color
+ );
+ }
+ return $this->bgColors[$color];
+ }
+
+ protected function startColor($fgColor = null, $bgColor = null)
+ {
+ $escape = "ESC[";
+ $parts = array();
+ if ($fgColor !== null
+ && $bgColor !== null
+ && ! array_key_exists($bgColor, $this->bgColors)
+ && array_key_exists($bgColor, $this->fgColors)
+ && array_key_exists($fgColor, $this->bgColors)
+ ) {
+ $parts[] = '7'; // reverse video, negative image
+ $parts[] = $this->bgColor($fgColor);
+ $parts[] = $this->fgColor($bgColor);
+ } else {
+ if ($fgColor !== null) {
+ $parts[] = $this->fgColor($fgColor);
+ }
+ if ($bgColor !== null) {
+ $parts[] = $this->bgColor($bgColor);
+ }
+ }
+ if (empty($parts)) {
+ return '';
+ }
+ return "\033[" . implode(';', $parts) . 'm';
+ }
+}
diff --git a/library/Icinga/Cli/Command.php b/library/Icinga/Cli/Command.php
new file mode 100644
index 0000000..7fd5f87
--- /dev/null
+++ b/library/Icinga/Cli/Command.php
@@ -0,0 +1,216 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Cli;
+
+use Icinga\Application\ApplicationBootstrap as App;
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotReadableError;
+use ipl\I18n\Translation;
+
+abstract class Command
+{
+ use Translation;
+
+ protected $app;
+ protected $docs;
+
+ /**
+ * @var Params
+ */
+ protected $params;
+ protected $screen;
+
+ /**
+ * Whether the --verbose switch is given and thus the set log level INFO is
+ *
+ * @var bool
+ */
+ protected $isVerbose;
+
+ /**
+ * Whether the --debug switch is given and thus the set log level DEBUG is
+ *
+ * @var bool
+ */
+ protected $isDebugging;
+
+ protected $moduleName;
+ protected $commandName;
+ protected $actionName;
+
+ protected $config;
+
+ protected $configs;
+
+ protected $defaultActionName = 'default';
+
+ /** @var bool Whether to automatically load enabled modules */
+ protected $loadEnabledModules = true;
+
+ /** @var bool Whether to enable trace for the CLI commands */
+ protected $trace = false;
+
+ public function __construct(App $app, $moduleName, $commandName, $actionName, $initialize = true)
+ {
+ $this->app = $app;
+ $this->moduleName = $moduleName;
+ $this->commandName = $commandName;
+ $this->actionName = $actionName;
+ $this->params = $app->getParams();
+ $this->screen = Screen::instance();
+ $this->trace = $this->params->shift('trace', false);
+ $this->isVerbose = $this->params->shift('verbose', false);
+ $this->isDebugging = $this->params->shift('debug', false);
+ $this->configs = [];
+
+ $this->translationDomain = $moduleName ?: 'icinga';
+
+ if ($this->loadEnabledModules) {
+ try {
+ $app->getModuleManager()->loadEnabledModules();
+ } catch (NotReadableError $e) {
+ Logger::error(new IcingaException('Cannot load enabled modules. An exception was thrown:', $e));
+ }
+ }
+
+ if ($initialize) {
+ $this->init();
+ }
+ }
+
+ public function Config($file = null)
+ {
+ if ($this->isModule()) {
+ return $this->getModuleConfig($file);
+ } else {
+ return $this->getMainConfig($file);
+ }
+ }
+
+ private function getModuleConfig($file = null)
+ {
+ if ($file === null) {
+ if ($this->config === null) {
+ $this->config = Config::module($this->moduleName);
+ }
+ return $this->config;
+ } else {
+ if (! array_key_exists($file, $this->configs)) {
+ $this->configs[$file] = Config::module($this->moduleName, $file);
+ }
+ return $this->configs[$file];
+ }
+ }
+
+ private function getMainConfig($file = null)
+ {
+ if ($file === null) {
+ if ($this->config === null) {
+ $this->config = Config::app();
+ }
+ return $this->config;
+ } else {
+ if (! array_key_exists($file, $this->configs)) {
+ $this->configs[$file] = Config::app($file);
+ }
+ return $this->configs[$file];
+ }
+ return $this->config;
+ }
+
+ public function isModule()
+ {
+ return substr(get_class($this), 0, 14) === 'Icinga\\Module\\';
+ }
+
+ public function setParams(Params $params)
+ {
+ $this->params = $params;
+ }
+
+ public function hasRemainingParams()
+ {
+ return $this->params->count() > 0;
+ }
+
+ public function showTrace()
+ {
+ return $this->trace;
+ }
+
+ /**
+ * @param $msg
+ *
+ * @throws IcingaException
+ */
+ public function fail($msg)
+ {
+ throw new IcingaException('%s', $msg);
+ }
+
+ public function getDefaultActionName()
+ {
+ return $this->defaultActionName;
+ }
+
+ /**
+ * Get {@link moduleName}
+ *
+ * @return string
+ */
+ public function getModuleName()
+ {
+ return $this->moduleName;
+ }
+
+ public function hasDefaultActionName()
+ {
+ return $this->hasActionName($this->defaultActionName);
+ }
+
+ public function hasActionName($name)
+ {
+ $actions = $this->listActions();
+ return in_array($name, $actions);
+ }
+
+ public function listActions()
+ {
+ $actions = array();
+ foreach (get_class_methods($this) as $method) {
+ if (preg_match('~^([A-Za-z0-9]+)Action$~', $method, $m)) {
+ $actions[] = $m[1];
+ }
+ }
+ sort($actions);
+ return $actions;
+ }
+
+ public function docs()
+ {
+ if ($this->docs === null) {
+ $this->docs = new Documentation($this->app);
+ }
+ return $this->docs;
+ }
+
+ public function showUsage($action = null)
+ {
+ if ($action === null) {
+ $action = $this->actionName;
+ }
+ echo $this->docs()->usage(
+ $this->moduleName,
+ $this->commandName,
+ $action
+ );
+ return false;
+ }
+
+ public function init()
+ {
+ }
+}
diff --git a/library/Icinga/Cli/Documentation.php b/library/Icinga/Cli/Documentation.php
new file mode 100644
index 0000000..6881467
--- /dev/null
+++ b/library/Icinga/Cli/Documentation.php
@@ -0,0 +1,167 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Cli;
+
+use Icinga\Application\ApplicationBootstrap as App;
+use Icinga\Cli\Documentation\CommentParser;
+use ReflectionClass;
+use ReflectionMethod;
+
+class Documentation
+{
+ protected $icinga;
+
+ protected $app;
+
+ protected $loader;
+
+ public function __construct(App $app)
+ {
+ $this->app = $app;
+ $this->loader = $app->cliLoader();
+ }
+
+ public function usage($module = null, $command = null, $action = null)
+ {
+ if ($module) {
+ $module = $this->loader->resolveModuleName($module);
+ return $this->moduleUsage($module, $command, $action);
+ }
+ if ($command) {
+ $command = $this->loader->resolveCommandName($command);
+ return $this->commandUsage($command, $action);
+ }
+ return $this->globalUsage();
+ }
+
+ public function globalUsage()
+ {
+ $d = "USAGE: icingacli [module] <command> [action] [options]\n\n"
+ . "Available commands:\n\n";
+ foreach ($this->loader->listCommands() as $command) {
+ if ($command !== 'autocomplete') {
+ $obj = $this->loader->getCommandInstance($command);
+ $d .= sprintf(
+ " %-14s %s\n",
+ $command,
+ $this->getClassTitle($obj)
+ );
+ }
+ }
+ $d .= "\nAvailable modules:\n\n";
+ foreach ($this->loader->listModules() as $module) {
+ $d .= ' ' . $module . "\n";
+ }
+ $d .= "\nGlobal options:\n\n"
+ . " --log [t] Log to <t>, either stderr, file or syslog (default: stderr)\n"
+ . " --log-path <f> Which file to log into in case of --log file\n"
+ . " --verbose Be verbose\n"
+ . " --debug Show debug output\n"
+ . " --help Show help\n"
+ . " --benchmark Show benchmark summary\n"
+ . " --watch [s] Refresh output every <s> seconds (default: 5)\n"
+ . " --version Shows version of Icinga Web 2, loaded modules and PHP\n"
+ ;
+ $d .= "\nShow help on a specific command : icingacli help <command>"
+ . "\nShow help on a specific module : icingacli help <module>"
+ . "\n";
+ return $d;
+ }
+
+ public function moduleUsage($module, $command = null, $action = null)
+ {
+ $commands = $this->loader->listModuleCommands($module);
+
+ if (empty($commands)) {
+ return "The '$module' module does not provide any CLI commands\n";
+ }
+ $d = '';
+ $obj = null;
+ if ($command) {
+ $obj = $this->loader->getModuleCommandInstance($module, $command);
+ }
+ if ($command === null) {
+ $d = "USAGE: icingacli $module <command> [<action>] [options]\n\n"
+ . "Available commands:\n\n";
+ foreach ($commands as $command) {
+ $d .= ' ' . $command . "\n";
+ }
+ $d .= "\nShow help on a specific command: icingacli help $module <command>\n";
+ } elseif ($action === null) {
+ $d .= $this->showCommandActions($obj, $command);
+ } else {
+ $action = $this->loader->resolveObjectActionName($obj, $action);
+ $d .= $this->getMethodDocumentation($obj, $action);
+ }
+ return $d;
+ }
+
+ /**
+ * @param Command $command
+ * @param string $name
+ *
+ * @return string
+ */
+ protected function showCommandActions($command, $name)
+ {
+ $actions = $command->listActions();
+ $d = $this->getClassDocumentation($command)
+ . "Available actions:\n\n";
+ foreach ($actions as $action) {
+ $d .= sprintf(
+ " %-14s %s\n",
+ $action,
+ $this->getMethodTitle($command, $action)
+ );
+ }
+ $d .= "\nShow help on a specific action: icingacli help ";
+ if ($command->isModule()) {
+ $d .= $command->getModuleName() . ' ';
+ }
+ $d .= "$name <action>\n";
+ return $d;
+ }
+
+ public function commandUsage($command, $action = null)
+ {
+ $obj = $this->loader->getCommandInstance($command);
+ $action = $this->loader->resolveObjectActionName($obj, $action);
+
+ $d = "\n";
+ if ($action) {
+ $d .= $this->getMethodDocumentation($obj, $action);
+ } else {
+ $d .= $this->showCommandActions($obj, $command);
+ }
+ return $d;
+ }
+
+ protected function getClassTitle($class)
+ {
+ $ref = new ReflectionClass($class);
+ $comment = new CommentParser($ref->getDocComment());
+ return $comment->getTitle();
+ }
+
+ protected function getClassDocumentation($class)
+ {
+ $ref = new ReflectionClass($class);
+ $comment = new CommentParser($ref->getDocComment());
+ return $comment->dump();
+ }
+
+ protected function getMethodTitle($class, $method)
+ {
+ $ref = new ReflectionMethod($class, $method . 'Action');
+ $comment = new CommentParser($ref->getDocComment());
+ return $comment->getTitle();
+ }
+
+ protected function getMethodDocumentation($class, $method)
+ {
+ $ref = new ReflectionMethod($class, $method . 'Action');
+ $comment = new CommentParser($ref->getDocComment());
+ return $comment->dump();
+ }
+}
diff --git a/library/Icinga/Cli/Documentation/CommentParser.php b/library/Icinga/Cli/Documentation/CommentParser.php
new file mode 100644
index 0000000..4104848
--- /dev/null
+++ b/library/Icinga/Cli/Documentation/CommentParser.php
@@ -0,0 +1,85 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Cli\Documentation;
+
+use Icinga\Cli\Screen;
+
+class CommentParser
+{
+ protected $raw;
+ protected $plain;
+ protected $title;
+ protected $paragraphs = array();
+
+ public function __construct($raw)
+ {
+ $this->raw = $raw;
+ if ($raw) {
+ $this->parse();
+ }
+ }
+
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ protected function parse()
+ {
+ $plain = $this->raw;
+
+ // Strip comment start /**
+ $plain = preg_replace('~^/\s*\*\*\n~s', '', $plain);
+
+ // Strip comment end */
+ $plain = preg_replace('~\n\s*\*/\s*~s', "\n", $plain);
+ $p = null;
+ foreach (preg_split('~\n~', $plain) as $line) {
+ // Strip * at line start
+ $line = preg_replace('~^\s*\*\s?~', '', $line);
+ $line = rtrim($line);
+ if ($this->title === null) {
+ $this->title = $line;
+ continue;
+ }
+ if ($p === null && empty($this->paragraphs)) {
+ $p = & $this->paragraphs[];
+ }
+
+ if ($line === '') {
+ if ($p !== null) {
+ $p = & $this->paragraphs[];
+ }
+ continue;
+ }
+ if ($p === null) {
+ $p = $line;
+ } else {
+ if (substr($line, 0, 2) === ' ') {
+ $p .= "\n" . $line;
+ } else {
+ $p .= ' ' . $line;
+ }
+ }
+ }
+ if ($p === null) {
+ array_pop($this->paragraphs);
+ }
+ }
+
+ public function dump()
+ {
+ if ($this->title) {
+ $res = $this->title . "\n" . str_repeat('=', strlen($this->title)) . "\n\n";
+ } else {
+ $res = '';
+ }
+
+ foreach ($this->paragraphs as $p) {
+ $res .= wordwrap($p, Screen::instance()->getColumns()) . "\n\n";
+ }
+
+ return $res;
+ }
+}
diff --git a/library/Icinga/Cli/Loader.php b/library/Icinga/Cli/Loader.php
new file mode 100644
index 0000000..5e63f3f
--- /dev/null
+++ b/library/Icinga/Cli/Loader.php
@@ -0,0 +1,501 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Cli;
+
+use Icinga\Application\ApplicationBootstrap as App;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotReadableError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Cli\Params;
+use Icinga\Cli\Screen;
+use Icinga\Cli\Command;
+use Icinga\Cli\Documentation;
+use Exception;
+
+/**
+ *
+ */
+class Loader
+{
+ protected $app;
+
+ protected $docs;
+
+ protected $commands;
+
+ protected $modules;
+
+ protected $moduleCommands = array();
+
+ protected $coreAppDir;
+
+ protected $screen;
+
+ protected $moduleName;
+
+ protected $commandName;
+
+ protected $actionName; // Should this better be moved to the Command?
+
+ /**
+ * [$command] = $class;
+ */
+ protected $commandClassMap = array();
+
+ /**
+ * [$command] = $file;
+ */
+ protected $commandFileMap = array();
+
+ /**
+ * [$module][$command] = $class;
+ */
+ protected $moduleClassMap = array();
+
+ /**
+ * [$module][$command] = $file;
+ */
+ protected $moduleFileMap = array();
+
+ protected $commandInstances = array();
+
+ protected $moduleInstances = array();
+
+ protected $lastSuggestions = array();
+
+ public function __construct(App $app)
+ {
+ $this->app = $app;
+ $this->coreAppDir = $app->getApplicationDir('clicommands');
+ }
+
+ /**
+ * Screen shortcut
+ *
+ * @return Screen
+ */
+ protected function screen()
+ {
+ if ($this->screen === null) {
+ $this->screen = Screen::instance(STDERR);
+ }
+
+ return $this->screen;
+ }
+
+ /**
+ * Documentation shortcut
+ *
+ * @return Documentation
+ */
+ protected function docs()
+ {
+ if ($this->docs === null) {
+ $this->docs = new Documentation($this->app);
+ }
+ return $this->docs;
+ }
+
+ /**
+ * Show given message and exit
+ *
+ * @param string $msg message to show
+ */
+ public function fail($msg)
+ {
+ fprintf(STDERR, "%s: %s\n", $this->screen()->colorize('ERROR', 'red'), $msg);
+ exit(1);
+ }
+
+ public function getModuleName()
+ {
+ return $this->moduleName;
+ }
+
+ public function setModuleName($name)
+ {
+ $this->moduleName = $name;
+ return $this;
+ }
+
+ public function getCommandName()
+ {
+ return $this->commandName;
+ }
+
+ public function getActionName()
+ {
+ return $this->actionName;
+ }
+
+ public function getCommandInstance($command)
+ {
+ if (! array_key_exists($command, $this->commandInstances)) {
+ $this->assertCommandExists($command);
+ require_once $this->commandFileMap[$command];
+ $className = $this->commandClassMap[$command];
+ $this->commandInstances[$command] = new $className(
+ $this->app,
+ null,
+ $command,
+ null,
+ false
+ );
+ }
+ return $this->commandInstances[$command];
+ }
+
+ public function getModuleCommandInstance($module, $command)
+ {
+ if (! array_key_exists($command, $this->moduleInstances[$module])) {
+ $this->assertModuleCommandExists($module, $command);
+ require_once $this->moduleFileMap[$module][$command];
+ $className = $this->moduleClassMap[$module][$command];
+ $this->moduleInstances[$module][$command] = new $className(
+ $this->app,
+ $module,
+ $command,
+ null,
+ false
+ );
+ }
+ return $this->moduleInstances[$module][$command];
+ }
+
+ public function getLastSuggestions()
+ {
+ return $this->lastSuggestions;
+ }
+
+ public function showLastSuggestions()
+ {
+ if (! empty($this->lastSuggestions)) {
+ foreach ($this->lastSuggestions as & $s) {
+ $s = $this->screen()->colorize($s, 'lightblue');
+ }
+ fprintf(
+ STDERR,
+ "Did you mean %s?\n",
+ implode(" or ", $this->lastSuggestions)
+ );
+ }
+ }
+
+ public function parseParams(Params $params = null)
+ {
+ if ($params === null) {
+ $params = $this->app->getParams();
+ }
+
+ $first = null;
+ if ($this->moduleName === null) {
+ $first = $params->shift();
+ if (! $first) {
+ return;
+ }
+ $found = $this->resolveName($first);
+ } else {
+ $found = $this->moduleName;
+ }
+ if (! $found) {
+ $msg = "There is no such module or command: '$first'";
+ fprintf(STDERR, "%s: %s\n", $this->screen()->colorize('ERROR', 'red'), $msg);
+ $this->showLastSuggestions();
+ fwrite(STDERR, "\n");
+ }
+
+ $obj = null;
+ if ($this->hasCommand($found)) {
+ $this->commandName = $found;
+ $obj = $this->getCommandInstance($this->commandName);
+ } elseif ($this->hasModule($found)) {
+ $this->moduleName = $found;
+ $command = $this->resolveModuleCommandName($found, $params->shift());
+ if ($command) {
+ $this->commandName = $command;
+ $obj = $this->getModuleCommandInstance(
+ $this->moduleName,
+ $this->commandName
+ );
+ }
+ }
+ if ($obj !== null) {
+ $action = $this->resolveObjectActionName(
+ $obj,
+ $params->getStandalone()
+ );
+ if ($obj->hasActionName($action)) {
+ $this->actionName = $action;
+ $params->shift();
+ } elseif ($obj->hasDefaultActionName()) {
+ $this->actionName = $obj->getDefaultActionName();
+ }
+ }
+ return $this;
+ }
+
+ public function handleParams(Params $params = null)
+ {
+ $this->parseParams($params);
+ $this->dispatch();
+ }
+
+ public function dispatch(Params $overrideParams = null)
+ {
+ if ($this->commandName === null) {
+ fwrite(STDERR, $this->docs()->usage($this->moduleName));
+ return false;
+ } elseif ($this->actionName === null) {
+ fwrite(STDERR, $this->docs()->usage($this->moduleName, $this->commandName));
+ return false;
+ }
+
+ $obj = null;
+ try {
+ if ($this->moduleName) {
+ $this->app->getModuleManager()->loadModule($this->moduleName);
+ $obj = $this->getModuleCommandInstance(
+ $this->moduleName,
+ $this->commandName
+ );
+ } else {
+ $obj = $this->getCommandInstance($this->commandName);
+ }
+ if ($overrideParams !== null) {
+ $obj->setParams($overrideParams);
+ }
+ $obj->init();
+ return $obj->{$this->actionName . 'Action'}();
+ } catch (Exception $e) {
+ if ($obj instanceof Command && $obj->showTrace()) {
+ fwrite(STDERR, $this->formatTrace($e->getTrace()));
+ }
+
+ $this->fail(IcingaException::describe($e));
+ }
+ }
+
+ protected function searchMatch($needle, $haystack)
+ {
+ if ($needle === null) {
+ $needle = '';
+ }
+
+ $this->lastSuggestions = preg_grep(sprintf('/^%s.*$/', preg_quote($needle, '/')), $haystack);
+ $match = array_search($needle, $haystack, true);
+ if (false !== $match) {
+ return $haystack[$match];
+ }
+ if (count($this->lastSuggestions) === 1) {
+ $lastSuggestions = array_values($this->lastSuggestions);
+ return $lastSuggestions[0];
+ }
+ return false;
+ }
+
+ public function resolveName($name)
+ {
+ return $this->searchMatch(
+ $name,
+ array_merge($this->listCommands(), $this->listModules())
+ );
+ }
+
+ public function resolveCommandName($name)
+ {
+ return $this->searchMatch($name, $this->listCommands());
+ }
+
+ public function resolveModuleName($name)
+ {
+ return $this->searchMatch($name, $this->listModules());
+ }
+
+ public function resolveModuleCommandName($module, $name)
+ {
+ return $this->searchMatch($name, $this->listModuleCommands($module));
+ }
+
+ public function resolveObjectActionName($obj, $name)
+ {
+ return $this->searchMatch($name, $obj->listActions());
+ }
+
+ protected function assertModuleExists($module)
+ {
+ if (! $this->hasModule($module)) {
+ throw new ProgrammingError(
+ 'There is no such module: %s',
+ $module
+ );
+ }
+ }
+
+ protected function assertCommandExists($command)
+ {
+ if (! $this->hasCommand($command)) {
+ throw new ProgrammingError(
+ 'There is no such command: %s',
+ $command
+ );
+ }
+ }
+
+ protected function assertModuleCommandExists($module, $command)
+ {
+ $this->assertModuleExists($module);
+ if (! $this->hasModuleCommand($module, $command)) {
+ throw new ProgrammingError(
+ 'The module \'%s\' has no such command: %s',
+ $module,
+ $command
+ );
+ }
+ }
+
+ protected function formatTrace($trace)
+ {
+ $output = array();
+ foreach ($trace as $i => $step) {
+ $object = '';
+ if (isset($step['object']) && is_object($step['object'])) {
+ $object = sprintf('[%s]', get_class($step['object'])) . $step['type'];
+ } elseif (! empty($step['object'])) {
+ $object = (string) $step['object'] . $step['type'];
+ }
+ if (isset($step['args']) && is_array($step['args'])) {
+ foreach ($step['args'] as & $arg) {
+ if (is_object($arg)) {
+ $arg = sprintf('[%s]', get_class($arg));
+ }
+ if (is_string($arg)) {
+ $arg = preg_replace('~\n~', '\n', $arg);
+ if (strlen($arg) > 50) {
+ $arg = substr($arg, 0, 47) . '...';
+ }
+ $arg = "'" . $arg . "'";
+ }
+ if ($arg === null) {
+ $arg = 'NULL';
+ }
+ if (is_bool($arg)) {
+ $arg = $arg ? 'TRUE' : 'FALSE';
+ }
+ }
+ } else {
+ $step['args'] = array();
+ }
+ $args = $step['args'];
+ foreach ($args as & $v) {
+ if (is_array($v)) {
+ $v = var_export($v, 1);
+ } else {
+ $v = (string) $v;
+ }
+ }
+ $output[$i] = sprintf(
+ '#%d %s:%d %s%s(%s)',
+ $i,
+ isset($step['file']) ? preg_replace(
+ '~.+/library/~',
+ 'library/',
+ $step['file']
+ ) : '[unknown file]',
+ isset($step['line']) ? $step['line'] : '0',
+ $object,
+ $step['function'],
+ implode(', ', $args)
+ );
+ }
+ return implode(PHP_EOL, $output) . PHP_EOL;
+ }
+
+ public function hasCommand($name)
+ {
+ return in_array($name, $this->listCommands());
+ }
+
+ public function hasModule($name)
+ {
+ return in_array($name, $this->listModules());
+ }
+
+ public function hasModuleCommand($module, $name)
+ {
+ return in_array($name, $this->listModuleCommands($module));
+ }
+
+ public function listModules()
+ {
+ if ($this->modules === null) {
+ $this->modules = array();
+ try {
+ $this->modules = array_unique(array_merge(
+ $this->app->getModuleManager()->listEnabledModules(),
+ $this->app->getModuleManager()->listLoadedModules()
+ ));
+ } catch (NotReadableError $e) {
+ $this->fail($e->getMessage());
+ }
+ }
+ return $this->modules;
+ }
+
+ protected function retrieveCommandsFromDir($dirname)
+ {
+ $commands = array();
+ if (! @file_exists($dirname) || ! is_readable($dirname)) {
+ return $commands;
+ }
+
+ $base = opendir($dirname);
+ if ($base === false) {
+ return $commands;
+ }
+ while (false !== ($dir = readdir($base))) {
+ if ($dir[0] === '.') {
+ continue;
+ }
+ if (preg_match('~^([A-Za-z0-9]+)Command\.php$~', $dir, $m)) {
+ $cmd = strtolower($m[1]);
+ $commands[] = $cmd;
+ }
+ }
+ closedir($base);
+ sort($commands);
+ return $commands;
+ }
+
+ public function listCommands()
+ {
+ if ($this->commands === null) {
+ $this->commands = array();
+ $ns = 'Icinga\\Clicommands\\';
+ $this->commands = $this->retrieveCommandsFromDir($this->coreAppDir);
+ foreach ($this->commands as $cmd) {
+ $this->commandClassMap[$cmd] = $ns . ucfirst($cmd) . 'Command';
+ $this->commandFileMap[$cmd] = $this->coreAppDir . '/' . ucfirst($cmd) . 'Command.php';
+ }
+ }
+ return $this->commands;
+ }
+
+ public function listModuleCommands($module)
+ {
+ if (! array_key_exists($module, $this->moduleCommands)) {
+ $ns = 'Icinga\\Module\\' . ucfirst($module) . '\\Clicommands\\';
+ $this->assertModuleExists($module);
+ $manager = $this->app->getModuleManager();
+ $manager->loadModule($module);
+ $dir = $manager->getModuleDir($module) . '/application/clicommands';
+ $this->moduleCommands[$module] = $this->retrieveCommandsFromDir($dir);
+ $this->moduleInstances[$module] = array();
+ foreach ($this->moduleCommands[$module] as $cmd) {
+ $this->moduleClassMap[$module][$cmd] = $ns . ucfirst($cmd) . 'Command';
+ $this->moduleFileMap[$module][$cmd] = $dir . '/' . ucfirst($cmd) . 'Command.php';
+ }
+ }
+ return $this->moduleCommands[$module];
+ }
+}
diff --git a/library/Icinga/Cli/Params.php b/library/Icinga/Cli/Params.php
new file mode 100644
index 0000000..463d4ae
--- /dev/null
+++ b/library/Icinga/Cli/Params.php
@@ -0,0 +1,320 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Cli;
+
+use Icinga\Exception\MissingParameterException;
+
+/**
+ * Params
+ *
+ * A class to ease commandline-option and -argument handling.
+ */
+class Params
+{
+ /**
+ * The name and path of the executable
+ *
+ * @var string
+ */
+ protected $program;
+
+ /**
+ * The arguments
+ *
+ * @var array
+ */
+ protected $standalone = array();
+
+ /**
+ * The options
+ *
+ * @var array
+ */
+ protected $params = array();
+
+ /**
+ * Parse the given commandline and create a new Params object
+ *
+ * @param array $argv The commandline
+ */
+ public function __construct($argv)
+ {
+ $noOptionFlag = false;
+ $this->program = array_shift($argv);
+ for ($i = 0; $i < count($argv); $i++) {
+ if ($argv[$i] === '--') {
+ $noOptionFlag = true;
+ } elseif (!$noOptionFlag && substr($argv[$i], 0, 2) === '--') {
+ $key = substr($argv[$i], 2);
+ $matches = array();
+ if (1 === preg_match(
+ '/(?<!.)([^=]+)=(.*)(?!.)/ms',
+ $key,
+ $matches
+ )) {
+ $this->params[$matches[1]] = $matches[2];
+ } elseif (! isset($argv[$i + 1]) || substr($argv[$i + 1], 0, 2) === '--') {
+ $this->params[$key] = true;
+ } elseif (array_key_exists($key, $this->params)) {
+ if (!is_array($this->params[$key])) {
+ $this->params[$key] = array($this->params[$key]);
+ }
+ $this->params[$key][] = $argv[++$i];
+ } else {
+ $this->params[$key] = $argv[++$i];
+ }
+ } else {
+ $this->standalone[] = $argv[$i];
+ }
+ }
+ }
+
+ /**
+ * Return the value for an argument by position
+ *
+ * @param int $pos The position of the argument
+ * @param mixed $default The default value to return
+ *
+ * @return mixed
+ */
+ public function getStandalone($pos = 0, $default = null)
+ {
+ if (isset($this->standalone[$pos])) {
+ return $this->standalone[$pos];
+ }
+ return $default;
+ }
+
+ /**
+ * Count and return the number of arguments and options
+ *
+ * @return int
+ */
+ public function count()
+ {
+ return count($this->standalone) + count($this->params);
+ }
+
+ /**
+ * Return the options
+ *
+ * @return array
+ */
+ public function getParams()
+ {
+ return $this->params;
+ }
+
+ /**
+ * Return the arguments
+ *
+ * @return array
+ */
+ public function getAllStandalone()
+ {
+ return $this->standalone;
+ }
+
+ /**
+ * Support isset() and empty() checks on options
+ *
+ * @param $name
+ *
+ * @return bool
+ */
+ public function __isset($name)
+ {
+ return isset($this->params[$name]);
+ }
+
+ /**
+ * @see Params::get()
+ */
+ public function __get($key)
+ {
+ return $this->get($key);
+ }
+
+ /**
+ * Return whether the given option exists
+ *
+ * @param string $key The option name to check
+ *
+ * @return bool
+ */
+ public function has($key)
+ {
+ return array_key_exists($key, $this->params);
+ }
+
+ /**
+ * Return the value of the given option
+ *
+ * @param string $key The option name
+ * @param mixed $default The default value to return
+ *
+ * @return mixed
+ */
+ public function get($key, $default = null)
+ {
+ if ($this->has($key)) {
+ return $this->params[$key];
+ }
+ return $default;
+ }
+
+ /**
+ * Require a parameter
+ *
+ * @param string $name Name of the parameter
+ * @param bool $strict Whether the parameter's value must not be the empty string
+ *
+ * @return mixed
+ *
+ * @throws MissingParameterException If the parameter was not given
+ */
+ public function getRequired($name, $strict = true)
+ {
+ if ($this->has($name)) {
+ $value = $this->get($name);
+ if (! $strict || strlen($value) > 0) {
+ return $value;
+ }
+ }
+ $e = new MissingParameterException(t('Required parameter \'%s\' missing'), $name);
+ $e->setParameter($name);
+ throw $e;
+ }
+
+ /**
+ * Set a value for the given option
+ *
+ * @param string $key The option name
+ * @param mixed $value The value to set
+ *
+ * @return $this
+ */
+ public function set($key, $value)
+ {
+ $this->params[$key] = $value;
+ return $this;
+ }
+
+ /**
+ * Remove a single option or multiple options
+ *
+ * @param string|array $keys The option or options to remove
+ *
+ * @return $this
+ */
+ public function remove($keys = array())
+ {
+ if (! is_array($keys)) {
+ $keys = array($keys);
+ }
+ foreach ($keys as $key) {
+ if (array_key_exists($key, $this->params)) {
+ unset($this->params[$key]);
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Return a copy of this object with the given options being removed
+ *
+ * @param string|array $keys The option or options to remove
+ *
+ * @return Params
+ */
+ public function without($keys = array())
+ {
+ $params = clone($this);
+ return $params->remove($keys);
+ }
+
+ /**
+ * Remove and return the value of the given option
+ *
+ * Called multiple times for an option with multiple values returns
+ * them one by one in case the default is not an array.
+ *
+ * @param string $key The option name
+ * @param mixed $default The default value to return
+ *
+ * @return mixed
+ */
+ public function shift($key = null, $default = null)
+ {
+ if ($key === null) {
+ if (count($this->standalone) > 0) {
+ return array_shift($this->standalone);
+ }
+ return $default;
+ }
+ $result = $this->get($key, $default);
+ if (is_array($result) && !is_array($default)) {
+ $result = array_shift($result) || $default;
+ if ($result === $default) {
+ $this->remove($key);
+ }
+ } else {
+ $this->remove($key);
+ }
+ return $result;
+ }
+
+ /**
+ * Require and remove a parameter
+ *
+ * @param string $name Name of the parameter
+ * @param bool $strict Whether the parameter's value must not be the empty string
+ *
+ * @return mixed
+ *
+ * @throws MissingParameterException If the parameter was not given
+ */
+ public function shiftRequired($name, $strict = true)
+ {
+ if ($this->has($name)) {
+ $value = $this->get($name);
+ if (! $strict || strlen($value) > 0) {
+ $this->shift($name);
+ return $value;
+ }
+ }
+ $e = new MissingParameterException(t('Required parameter \'%s\' missing'), $name);
+ $e->setParameter($name);
+ throw $e;
+ }
+
+ /**
+ * Put the given value onto the argument stack
+ *
+ * @param mixed $key The argument
+ *
+ * @return $this
+ */
+ public function unshift($key)
+ {
+ array_unshift($this->standalone, $key);
+ return $this;
+ }
+
+ /**
+ * Parse the given commandline
+ *
+ * @param array $argv The commandline to parse
+ *
+ * @return Params
+ */
+ public static function parse($argv = null)
+ {
+ if ($argv === null) {
+ $argv = $GLOBALS['argv'];
+ }
+ $params = new self($argv);
+ return $params;
+ }
+}
diff --git a/library/Icinga/Cli/Screen.php b/library/Icinga/Cli/Screen.php
new file mode 100644
index 0000000..4ffad72
--- /dev/null
+++ b/library/Icinga/Cli/Screen.php
@@ -0,0 +1,106 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Cli;
+
+use Icinga\Cli\AnsiScreen;
+
+class Screen
+{
+ protected static $instances = [];
+
+ protected $isUtf8;
+
+ public function getColumns()
+ {
+ $cols = (int) getenv('COLUMNS');
+ if (! $cols) {
+ // stty -a ?
+ $cols = (int) exec('tput cols');
+ }
+ if (! $cols) {
+ $cols = 80;
+ }
+ return $cols;
+ }
+
+ public function getRows()
+ {
+ $rows = (int) getenv('ROWS');
+ if (! $rows) {
+ // stty -a ?
+ $rows = (int) exec('tput lines');
+ }
+ if (! $rows) {
+ $rows = 25;
+ }
+ return $rows;
+ }
+
+ public function strlen($string)
+ {
+ return strlen($string);
+ }
+
+ public function newlines($count = 1)
+ {
+ return str_repeat("\n", $count);
+ }
+
+ public function center($txt)
+ {
+ $len = $this->strlen($txt);
+ $width = floor(($this->getColumns() + $len) / 2) - $len;
+ return str_repeat(' ', $width) . $txt;
+ }
+
+ public function hasUtf8()
+ {
+ if ($this->isUtf8 === null) {
+ // null should equal 0 here, however seems to equal '' on some systems:
+ $current = setlocale(LC_ALL, 0);
+
+ $parts = preg_split('/;/', $current);
+ $lc_parts = array();
+ foreach ($parts as $part) {
+ if (strpos($part, '=') === false) {
+ continue;
+ }
+ list($key, $val) = preg_split('/=/', $part, 2);
+ $lc_parts[$key] = $val;
+ }
+
+ $this->isUtf8 = array_key_exists('LC_CTYPE', $lc_parts)
+ && preg_match('~\.UTF-8$~i', $lc_parts['LC_CTYPE']);
+ }
+ return $this->isUtf8;
+ }
+
+ public function clear()
+ {
+ return "\n";
+ }
+
+ public function underline($text)
+ {
+ return $text;
+ }
+
+ public function colorize($text, $fgColor = null, $bgColor = null)
+ {
+ return $text;
+ }
+
+ public static function instance($output = STDOUT)
+ {
+ if (! isset(self::$instances[(int) $output])) {
+ if (function_exists('posix_isatty') && posix_isatty($output)) {
+ self::$instances[(int) $output] = new AnsiScreen();
+ } else {
+ self::$instances[(int) $output] = new Screen();
+ }
+ }
+
+ return self::$instances[(int) $output];
+ }
+}
diff --git a/library/Icinga/Common/Database.php b/library/Icinga/Common/Database.php
new file mode 100644
index 0000000..d54eb25
--- /dev/null
+++ b/library/Icinga/Common/Database.php
@@ -0,0 +1,56 @@
+<?php
+/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Common;
+
+use Icinga\Application\Config as IcingaConfig;
+use Icinga\Data\ResourceFactory;
+use ipl\Sql\Config as SqlConfig;
+use ipl\Sql\Connection;
+use LogicException;
+use PDO;
+
+/**
+ * Trait for accessing the Icinga Web database
+ */
+trait Database
+{
+ /**
+ * Get a connection to the Icinga Web database
+ *
+ * @return Connection
+ *
+ * @throws \Icinga\Exception\ConfigurationError
+ */
+ protected function getDb(): Connection
+ {
+ if (! $this->hasDb()) {
+ throw new LogicException('Please check if a db instance exists at all');
+ }
+
+ $config = new SqlConfig(ResourceFactory::getResourceConfig(
+ IcingaConfig::app()->get('global', 'config_resource')
+ ));
+ if ($config->db === 'mysql') {
+ $config->charset = 'utf8mb4';
+ }
+
+ $config->options = [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ];
+ if ($config->db === 'mysql') {
+ $config->options[PDO::MYSQL_ATTR_INIT_COMMAND] = "SET SESSION SQL_MODE='STRICT_TRANS_TABLES"
+ . ",NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'";
+ }
+
+ return new Connection($config);
+ }
+
+ /**
+ * Check if db exists
+ *
+ * @return bool true if a database was found otherwise false
+ */
+ protected function hasDb()
+ {
+ return (bool) IcingaConfig::app()->get('global', 'config_resource');
+ }
+}
diff --git a/library/Icinga/Common/PdfExport.php b/library/Icinga/Common/PdfExport.php
new file mode 100644
index 0000000..afea9bf
--- /dev/null
+++ b/library/Icinga/Common/PdfExport.php
@@ -0,0 +1,105 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Common;
+
+use Icinga\Application\Icinga;
+use Icinga\Date\DateFormatter;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Pdfexport\PrintableHtmlDocument;
+use Icinga\Util\Environment;
+use Icinga\Web\Controller;
+use ipl\Html\Html;
+use ipl\Html\HtmlString;
+use ipl\Html\ValidHtml;
+use ipl\Web\Compat\CompatController;
+use ipl\Web\Url;
+
+trait PdfExport
+{
+ /** @var string The image to show in a pdf exports page header */
+ private $pdfHeaderImage = 'img/icinga-logo-big-dark.png';
+
+ /**
+ * Export the requested action to PDF and send it
+ *
+ * @return never
+ * @throws ConfigurationError If the pdfexport module is not available
+ */
+ protected function sendAsPdf()
+ {
+ if (! Icinga::app()->getModuleManager()->has('pdfexport')) {
+ throw new ConfigurationError('The pdfexport module is required for exports to PDF');
+ }
+
+ putenv('ICINGAWEB_EXPORT_FORMAT=pdf');
+ Environment::raiseMemoryLimit('512M');
+ Environment::raiseExecutionTime(300);
+
+ $time = DateFormatter::formatDateTime(time());
+ $iconPath = is_readable($this->pdfHeaderImage)
+ ? $this->pdfHeaderImage
+ : Icinga::app()->getBootstrapDirectory() . '/' . $this->pdfHeaderImage;
+ $encodedIcon = is_readable($iconPath) ? base64_encode(file_get_contents($iconPath)) : null;
+ $html = $this instanceof CompatController && ! $this->content->isEmpty()
+ ? $this->content
+ : $this->renderControllerAction();
+
+ $doc = (new PrintableHtmlDocument())
+ ->setTitle($this->view->title)
+ ->setHeader(Html::wantHtml([
+ Html::tag('span', ['class' => 'title']),
+ $encodedIcon
+ ? Html::tag('img', ['height' => 13, 'src' => 'data:image/png;base64,' . $encodedIcon])
+ : null,
+ Html::tag('time', null, $time)
+ ]))
+ ->setFooter(Html::wantHtml([
+ Html::tag('span', null, [
+ t('Page') . ' ',
+ Html::tag('span', ['class' => 'pageNumber']),
+ ' / ',
+ Html::tag('span', ['class' => 'totalPages'])
+ ]),
+ Html::tag('p', null, rawurldecode(Url::fromRequest()->setParams($this->params)))
+ ]))
+ ->addHtml($html);
+
+ if (($moduleName = $this->getRequest()->getModuleName()) !== 'default') {
+ $doc->getAttributes()->add('class', 'icinga-module module-' . $moduleName);
+ }
+
+ \Icinga\Module\Pdfexport\ProvidedHook\Pdfexport::first()->streamPdfFromHtml($doc, sprintf(
+ '%s-%s',
+ $this->view->title ?: $this->getRequest()->getActionName(),
+ $time
+ ));
+ }
+
+ /**
+ * Render the requested action
+ *
+ * @return ValidHtml
+ */
+ protected function renderControllerAction()
+ {
+ /** @var Controller $this */
+ $this->view->compact = true;
+
+ $viewRenderer = $this->getHelper('viewRenderer');
+ $viewRenderer->postDispatch();
+
+ $layoutHelper = $this->getHelper('layout');
+ $oldLayout = $layoutHelper->getLayout();
+ $layout = $layoutHelper->setLayout('inline');
+
+ $layout->content = $this->getResponse();
+ $html = $layout->render();
+
+ // Restore previous layout and reset content, to properly show errors
+ $this->getResponse()->clearBody($viewRenderer->getResponseSegment());
+ $layoutHelper->setLayout($oldLayout);
+
+ return HtmlString::create($html);
+ }
+}
diff --git a/library/Icinga/Crypt/AesCrypt.php b/library/Icinga/Crypt/AesCrypt.php
new file mode 100644
index 0000000..8e9d453
--- /dev/null
+++ b/library/Icinga/Crypt/AesCrypt.php
@@ -0,0 +1,337 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Crypt;
+
+use UnexpectedValueException;
+use RuntimeException;
+
+/**
+ * Data encryption and decryption using symmetric algorithm
+ *
+ * # Example Usage
+ *
+ * ```php
+ *
+ * // Encryption
+ * $encryptedData = (new AesCrypt())->encrypt($data); // Accepts a string
+ *
+ *
+ * // Encrypt and encode to Base64
+ * $encryptedData = (new AesCrypt())->encryptToBase64($data); // Accepts a string
+ *
+ *
+ * // Decryption
+ * $aesCrypt = (new AesCrypt())
+ * ->setTag($tag) // if exists
+ * ->setIV($iv)
+ * ->setKey($key);
+ *
+ * $decryptedData = $aesCrypt->decrypt($data);
+ *
+ * // Decode from Base64 and decrypt
+ * $aesCrypt = (new AesCrypt())
+ * ->setTag($tag)
+ * ->setIV($iv)
+ * ->setKey($key);
+ *
+ * $decryptedData = $aesCrypt->decryptFromBase64($data);
+ * ```
+ *
+ */
+class AesCrypt
+{
+ /** @var array The list of cipher methods */
+ const METHODS = [
+ 'aes-256-gcm',
+ 'aes-256-cbc',
+ 'aes-256-ctr'
+ ];
+
+ /** @var string The encryption key */
+ private $key;
+
+ /** @var int The length of the key */
+ private $keyLength;
+
+ /** @var string The initialization vector which is not NULL */
+ private $iv;
+
+ /** @var string The authentication tag which is passed by reference when using AEAD cipher mode */
+ private $tag;
+
+ /** @var string The cipher method */
+ private $method;
+
+ public function __construct($keyLength = 128)
+ {
+ $this->keyLength = $keyLength;
+ }
+
+ /**
+ * Set the method
+ *
+ * @return $this
+ */
+ public function setMethod($method)
+ {
+ $this->method = $method;
+
+ return $this;
+ }
+
+ /**
+ * Get the method
+ *
+ * @return string
+ */
+ public function getMethod()
+ {
+ if ($this->method === null) {
+ $this->method = $this->getSupportedMethod();
+ }
+
+ return $this->method;
+ }
+
+ /**
+ * Get supported method
+ *
+ * @return string
+ *
+ * @throws RuntimeException If none of the methods listed in the METHODS array is available
+ */
+ protected function getSupportedMethod()
+ {
+ $availableMethods = openssl_get_cipher_methods();
+ $methods = self::METHODS;
+
+ if (! $this->isAuthenticatedEncryptionSupported()) {
+ unset($methods[0]);
+ }
+
+ foreach ($methods as $method) {
+ if (in_array($method, $availableMethods)) {
+ return $method;
+ }
+ }
+
+ throw new RuntimeException('No supported method found');
+ }
+
+ /**
+ * Set the key
+ *
+ * @return $this
+ */
+ public function setKey($key)
+ {
+ $this->key = $key;
+
+ return $this;
+ }
+
+ /**
+ * Get the key
+ *
+ * @return string
+ *
+ */
+ public function getKey()
+ {
+ if (empty($this->key)) {
+ $this->key = random_bytes($this->keyLength);
+ }
+
+ return $this->key;
+ }
+
+ /**
+ * Set the IV
+ *
+ * @return $this
+ */
+ public function setIV($iv)
+ {
+ $this->iv = $iv;
+
+ return $this;
+ }
+
+ /**
+ * Get the IV
+ *
+ * @return string
+ *
+ */
+ public function getIV()
+ {
+ if (empty($this->iv)) {
+ $len = openssl_cipher_iv_length($this->getMethod());
+ $this->iv = random_bytes($len);
+ }
+
+ return $this->iv;
+ }
+
+ /**
+ * Set the Tag
+ *
+ * @return $this
+ *
+ * @throws RuntimeException If a tag is available but authenticated encryption (AE) is not supported.
+ *
+ * @throws UnexpectedValueException If tag length is less then 16
+ */
+ public function setTag($tag)
+ {
+ if (! $this->isAuthenticatedEncryptionSupported()) {
+ throw new RuntimeException(sprintf(
+ "The given decryption method is not supported in php version '%s'",
+ PHP_VERSION
+ ));
+ }
+
+ if (strlen($tag) !== 16) {
+ throw new UnexpectedValueException(sprintf(
+ 'expects tag length to be 16, got instead %s',
+ strlen($tag)
+ ));
+ }
+
+ $this->tag = $tag;
+
+ return $this;
+ }
+
+ /**
+ * Get the Tag
+ *
+ * @return string
+ *
+ * @throws RuntimeException If the Tag is not set
+ */
+ public function getTag()
+ {
+ if (empty($this->tag)) {
+ throw new RuntimeException('No tag set');
+ }
+
+ return $this->tag;
+ }
+
+ /**
+ * Decrypt the given string
+ *
+ * @param string $data
+ *
+ * @return string
+ *
+ * @throws RuntimeException If decryption fails
+ */
+ public function decrypt($data)
+ {
+ if (! $this->isAuthenticatedEncryptionRequired()) {
+ return $this->nonAEDecrypt($data);
+ }
+
+ $decrypt = openssl_decrypt($data, $this->getMethod(), $this->getKey(), 0, $this->getIV(), $this->getTag());
+
+ if ($decrypt === false) {
+ throw new RuntimeException('Decryption failed');
+ }
+
+ return $decrypt;
+ }
+
+ /**
+ * Encrypt the given string
+ *
+ * @param string $data
+ *
+ * @return string encrypted string
+ *
+ * @throws RuntimeException If decryption fails
+ */
+ public function encrypt($data)
+ {
+ if (! $this->isAuthenticatedEncryptionRequired()) {
+ return $this->nonAEEncrypt($data);
+ }
+
+ $encrypt = openssl_encrypt($data, $this->getMethod(), $this->getKey(), 0, $this->getIV(), $this->tag);
+
+ if ($encrypt === false) {
+ throw new RuntimeException('Encryption failed');
+ }
+
+ return $encrypt;
+ }
+
+ /**
+ * Decrypt the given string with non Authenticated encryption (AE) cipher method
+ *
+ * @param string $data
+ *
+ * @return string decrypted string
+ *
+ * @throws RuntimeException If decryption fails
+ */
+ private function nonAEDecrypt($data)
+ {
+ $c = base64_decode($data);
+ $hmac = substr($c, 0, 32);
+ $data = substr($c, 32);
+
+ $decrypt = openssl_decrypt($data, $this->getMethod(), $this->getKey(), 0, $this->getIV());
+ $calcHmac = hash_hmac('sha256', $this->getIV() . $data, $this->getKey(), true);
+
+ if ($decrypt === false || ! hash_equals($hmac, $calcHmac)) {
+ throw new RuntimeException('Decryption failed');
+ }
+
+ return $decrypt;
+ }
+
+ /**
+ * Encrypt the given string with non Authenticated encryption (AE) cipher method
+ *
+ * @param string $data
+ *
+ * @return string encrypted string
+ *
+ * @throws RuntimeException If encryption fails
+ */
+ private function nonAEEncrypt($data)
+ {
+ $encrypt = openssl_encrypt($data, $this->getMethod(), $this->getKey(), 0, $this->getIV());
+
+ if ($encrypt === false) {
+ throw new RuntimeException('Encryption failed');
+ }
+
+ $hmac = hash_hmac('sha256', $this->getIV() . $encrypt, $this->getKey(), true);
+
+ return base64_encode($hmac . $encrypt);
+ }
+
+ /**
+ * Whether the Authenticated encryption (a tag) is required
+ *
+ * @return bool True if required false otherwise
+ */
+ public function isAuthenticatedEncryptionRequired()
+ {
+ return $this->getMethod() === 'aes-256-gcm';
+ }
+
+ /**
+ * Whether the php version supports Authenticated encryption (AE) or not
+ *
+ * @return bool True if supported false otherwise
+ */
+ public function isAuthenticatedEncryptionSupported()
+ {
+ return PHP_VERSION_ID >= 70100;
+ }
+}
diff --git a/library/Icinga/Data/ConfigObject.php b/library/Icinga/Data/ConfigObject.php
new file mode 100644
index 0000000..c9a3134
--- /dev/null
+++ b/library/Icinga/Data/ConfigObject.php
@@ -0,0 +1,289 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+use Iterator;
+use ArrayAccess;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Container for configuration values
+ */
+class ConfigObject extends ArrayDatasource implements Iterator, ArrayAccess
+{
+ /**
+ * Create a new config
+ *
+ * @param array $data The data to initialize the new config with
+ */
+ public function __construct(array $data = array())
+ {
+ // Convert all embedded arrays to ConfigObjects as well
+ foreach ($data as & $value) {
+ if (is_array($value)) {
+ $value = new static($value);
+ }
+ }
+
+ parent::__construct($data);
+ }
+
+ /**
+ * Deep clone this config
+ */
+ public function __clone()
+ {
+ $array = array();
+ foreach ($this->data as $key => $value) {
+ if ($value instanceof self) {
+ $array[$key] = clone $value;
+ } else {
+ $array[$key] = $value;
+ }
+ }
+
+ $this->data = $array;
+ }
+
+ /**
+ * Reset the current position of $this->data
+ *
+ * @return void
+ */
+ public function rewind(): void
+ {
+ reset($this->data);
+ }
+
+ /**
+ * Return the section's or property's value of the current iteration
+ *
+ * @return mixed
+ */
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ return current($this->data);
+ }
+
+ /**
+ * Return whether the position of the current iteration is valid
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ return key($this->data) !== null;
+ }
+
+ /**
+ * Return the section's or property's name of the current iteration
+ *
+ * @return string
+ */
+ public function key(): string
+ {
+ return key($this->data);
+ }
+
+ /**
+ * Advance the position of the current iteration and return the new section's or property's value
+ *
+ * @return void
+ */
+ public function next(): void
+ {
+ next($this->data);
+ }
+
+ /**
+ * Return whether the given section or property is set
+ *
+ * @param string $key The name of the section or property
+ *
+ * @return bool
+ */
+ public function __isset($key)
+ {
+ return isset($this->data[$key]);
+ }
+
+ /**
+ * Return the value for the given property or the config for the given section
+ *
+ * @param string $key The name of the property or section
+ *
+ * @return mixed|NULL The value or NULL in case $key does not exist
+ */
+ public function __get($key)
+ {
+ return $this->get($key);
+ }
+
+ /**
+ * Add a new property or section
+ *
+ * @param string $key The name of the new property or section
+ * @param mixed $value The value to set for the new property or section
+ */
+ public function __set($key, $value)
+ {
+ if (is_array($value)) {
+ $this->data[$key] = new static($value);
+ } else {
+ $this->data[$key] = $value;
+ }
+ }
+
+ /**
+ * Remove the given property or section
+ *
+ * @param string $key The property or section to remove
+ */
+ public function __unset($key)
+ {
+ unset($this->data[$key]);
+ }
+
+ /**
+ * Return whether the given section or property is set
+ *
+ * @param string $key The name of the section or property
+ *
+ * @return bool
+ */
+ public function offsetExists($key): bool
+ {
+ return isset($this->$key);
+ }
+
+ /**
+ * Return the value for the given property or the config for the given section
+ *
+ * @param string $key The name of the property or section
+ *
+ * @return ?mixed The value or NULL in case $key does not exist
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($key)
+ {
+ return $this->get($key);
+ }
+
+ /**
+ * Add a new property or section
+ *
+ * @param string $key The name of the new property or section
+ * @param mixed $value The value to set for the new property or section
+ *
+ * @throws ProgrammingError If the key is null
+ */
+ public function offsetSet($key, $value): void
+ {
+ if ($key === null) {
+ throw new ProgrammingError('Appending values without an explicit key is not supported');
+ }
+
+ $this->$key = $value;
+ }
+
+ /**
+ * Remove the given property or section
+ *
+ * @param string $key The property or section to remove
+ */
+ public function offsetUnset($key): void
+ {
+ unset($this->$key);
+ }
+
+ /**
+ * Return whether this config has any data
+ *
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ return empty($this->data);
+ }
+
+ /**
+ * Return the value for the given property or the config for the given section
+ *
+ * @param string $key The name of the property or section
+ * @param mixed $default The value to return in case the property or section is missing
+ *
+ * @return mixed
+ */
+ public function get($key, $default = null)
+ {
+ if (array_key_exists($key, $this->data)) {
+ return $this->data[$key];
+ }
+
+ return $default;
+ }
+
+ /**
+ * Return all section and property names
+ *
+ * @return array
+ */
+ public function keys()
+ {
+ return array_keys($this->data);
+ }
+
+ /**
+ * Return this config's data as associative array
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ $array = array();
+ foreach ($this->data as $key => $value) {
+ if ($value instanceof self) {
+ $array[$key] = $value->toArray();
+ } else {
+ $array[$key] = $value;
+ }
+ }
+
+ return $array;
+ }
+
+ /**
+ * Merge the given data with this config
+ *
+ * @param array|ConfigObject $data An array or a config
+ *
+ * @return $this
+ */
+ public function merge($data)
+ {
+ if ($data instanceof self) {
+ $data = $data->toArray();
+ }
+
+ foreach ($data as $key => $value) {
+ if (array_key_exists($key, $this->data)) {
+ if (is_array($value)) {
+ if ($this->data[$key] instanceof self) {
+ $this->data[$key]->merge($value);
+ } else {
+ $this->data[$key] = new static($value);
+ }
+ } else {
+ $this->data[$key] = $value;
+ }
+ } else {
+ $this->data[$key] = is_array($value) ? new static($value) : $value;
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/library/Icinga/Data/ConnectionInterface.php b/library/Icinga/Data/ConnectionInterface.php
new file mode 100644
index 0000000..bd7d026
--- /dev/null
+++ b/library/Icinga/Data/ConnectionInterface.php
@@ -0,0 +1,8 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+interface ConnectionInterface extends Selectable, Queryable
+{
+}
diff --git a/library/Icinga/Data/DataArray/ArrayDatasource.php b/library/Icinga/Data/DataArray/ArrayDatasource.php
new file mode 100644
index 0000000..e300616
--- /dev/null
+++ b/library/Icinga/Data/DataArray/ArrayDatasource.php
@@ -0,0 +1,292 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\DataArray;
+
+use ArrayIterator;
+use Icinga\Data\Selectable;
+use Icinga\Data\SimpleQuery;
+
+class ArrayDatasource implements Selectable
+{
+ /**
+ * The array being used as data source
+ *
+ * @var array
+ */
+ protected $data;
+
+ /**
+ * The current result
+ *
+ * @var array
+ */
+ protected $result;
+
+ /**
+ * The result of a counted query
+ *
+ * @var int
+ */
+ protected $count;
+
+ /**
+ * The name of the column to map array keys on
+ *
+ * In case the array being used as data source provides keys of type string,this name
+ * will be used to set such as column on each row, if the column is not set already.
+ *
+ * @var string
+ */
+ protected $keyColumn;
+
+ /**
+ * Create a new data source for the given array
+ *
+ * @param array $data The array you're going to use as a data source
+ */
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ }
+
+ /**
+ * Set the name of the column to map array keys on
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setKeyColumn($name)
+ {
+ $this->keyColumn = $name;
+ return $this;
+ }
+
+ /**
+ * Return the name of the column to map array keys on
+ *
+ * @return string
+ */
+ public function getKeyColumn()
+ {
+ return $this->keyColumn;
+ }
+
+ /**
+ * Provide a query for this data source
+ *
+ * @return SimpleQuery
+ */
+ public function select()
+ {
+ return new SimpleQuery(clone $this);
+ }
+
+ /**
+ * Fetch and return all rows of the given query's result set using an iterator
+ *
+ * @param SimpleQuery $query
+ *
+ * @return ArrayIterator
+ */
+ public function query(SimpleQuery $query)
+ {
+ return new ArrayIterator($this->fetchAll($query));
+ }
+
+ /**
+ * Fetch and return a column of all rows of the result set as an array
+ *
+ * @param SimpleQuery $query
+ *
+ * @return array
+ */
+ public function fetchColumn(SimpleQuery $query)
+ {
+ $result = array();
+ foreach ($this->getResult($query) as $row) {
+ $arr = (array) $row;
+ $result[] = array_shift($arr);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Fetch and return all rows of the given query's result as a flattened key/value based array
+ *
+ * @param SimpleQuery $query
+ *
+ * @return array
+ */
+ public function fetchPairs(SimpleQuery $query)
+ {
+ $result = array();
+ $keys = null;
+ foreach ($this->getResult($query) as $row) {
+ if ($keys === null) {
+ $keys = array_keys((array) $row);
+ if (count($keys) < 2) {
+ $keys[1] = $keys[0];
+ }
+ }
+
+ $result[$row->{$keys[0]}] = $row->{$keys[1]};
+ }
+
+ return $result;
+ }
+
+ /**
+ * Fetch and return the first row of the given query's result
+ *
+ * @param SimpleQuery $query
+ *
+ * @return object|false The row or false in case the result is empty
+ */
+ public function fetchRow(SimpleQuery $query)
+ {
+ $result = $this->getResult($query);
+ if (empty($result)) {
+ return false;
+ }
+
+ return array_shift($result);
+ }
+
+ /**
+ * Fetch and return all rows of the given query's result as an array
+ *
+ * @param SimpleQuery $query
+ *
+ * @return array
+ */
+ public function fetchAll(SimpleQuery $query)
+ {
+ return $this->getResult($query);
+ }
+
+ /**
+ * Count all rows of the given query's result
+ *
+ * @param SimpleQuery $query
+ *
+ * @return int
+ */
+ public function count(SimpleQuery $query)
+ {
+ if ($this->count === null) {
+ $this->count = count($this->createResult($query));
+ }
+
+ return $this->count;
+ }
+
+ /**
+ * Create and return the result for the given query
+ *
+ * @param SimpleQuery $query
+ *
+ * @return array
+ */
+ protected function createResult(SimpleQuery $query)
+ {
+ $columns = $query->getColumns();
+ $filter = $query->getFilter();
+ $offset = $query->hasOffset() ? $query->getOffset() : 0;
+ $limit = $query->hasLimit() ? $query->getLimit() : 0;
+ $data = $this->data;
+
+ if ($query->hasOrder()) {
+ uasort($data, [$query, 'compare']);
+ }
+
+ $foundStringKey = false;
+ $result = [];
+ $skipped = 0;
+ foreach ($data as $key => $row) {
+ if ($this->keyColumn !== null && !isset($row->{$this->keyColumn})) {
+ $row = clone $row; // Make sure that this won't affect the actual data
+ $row->{$this->keyColumn} = $key;
+ }
+
+ if (! $filter->matches($row)) {
+ continue;
+ } elseif ($skipped < $offset) {
+ $skipped++;
+ continue;
+ }
+
+ // Get only desired columns if asked so
+ if (! empty($columns)) {
+ $filteredRow = (object) array();
+ foreach ($columns as $alias => $name) {
+ if (! is_string($alias)) {
+ $alias = $name;
+ }
+
+ if (isset($row->$name)) {
+ $filteredRow->$alias = $row->$name;
+ } else {
+ $filteredRow->$alias = null;
+ }
+ }
+ } else {
+ $filteredRow = $row;
+ }
+
+ $foundStringKey |= is_string($key);
+ $result[$key] = $filteredRow;
+
+ if (count($result) === $limit) {
+ break;
+ }
+ }
+
+ if (! $foundStringKey) {
+ $result = array_values($result);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Return whether a query result exists
+ *
+ * @return bool
+ */
+ protected function hasResult()
+ {
+ return $this->result !== null;
+ }
+
+ /**
+ * Set the current result
+ *
+ * @param array $result
+ *
+ * @return $this
+ */
+ protected function setResult(array $result)
+ {
+ $this->result = $result;
+ return $this;
+ }
+
+ /**
+ * Return the result for the given query
+ *
+ * @param SimpleQuery $query
+ *
+ * @return array
+ */
+ protected function getResult(SimpleQuery $query)
+ {
+ if (! $this->hasResult()) {
+ $this->setResult($this->createResult($query));
+ }
+
+ return $this->result;
+ }
+}
diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php
new file mode 100644
index 0000000..fc6814d
--- /dev/null
+++ b/library/Icinga/Data/Db/DbConnection.php
@@ -0,0 +1,655 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Db;
+
+use DateTime;
+use DateTimeZone;
+use Exception;
+use Icinga\Data\Filter\FilterEqual;
+use Icinga\Data\Filter\FilterNotEqual;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use PDO;
+use Iterator;
+use Zend_Db;
+use Zend_Db_Expr;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Extensible;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Data\Filter\FilterNot;
+use Icinga\Data\Filter\FilterOr;
+use Icinga\Data\Reducible;
+use Icinga\Data\ResourceFactory;
+use Icinga\Data\Selectable;
+use Icinga\Data\Updatable;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Encapsulate database connections and query creation
+ */
+class DbConnection implements Selectable, Extensible, Updatable, Reducible, Inspectable
+{
+ /**
+ * Connection config
+ *
+ * @var ConfigObject
+ */
+ private $config;
+
+ /**
+ * Database type
+ *
+ * @var string
+ */
+ private $dbType;
+
+ /**
+ * @var \Zend_Db_Adapter_Abstract
+ */
+ private $dbAdapter;
+
+ /**
+ * Table prefix
+ *
+ * @var string
+ */
+ private $tablePrefix = '';
+
+ private static $genericAdapterOptions = array(
+ Zend_Db::AUTO_QUOTE_IDENTIFIERS => false,
+ Zend_Db::CASE_FOLDING => Zend_Db::CASE_LOWER
+ );
+
+ private static $driverOptions = array(
+ PDO::ATTR_TIMEOUT => 10,
+ PDO::ATTR_CASE => PDO::CASE_LOWER,
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
+ );
+
+ /**
+ * Create a new connection object
+ *
+ * @param ConfigObject $config
+ */
+ public function __construct(ConfigObject $config = null)
+ {
+ $this->config = $config;
+ $this->connect();
+ }
+
+ /**
+ * Provide a query on this connection
+ *
+ * @return DbQuery
+ */
+ public function select()
+ {
+ return new DbQuery($this);
+ }
+
+ /**
+ * Fetch and return all rows of the given query's result set using an iterator
+ *
+ * @param DbQuery $query
+ *
+ * @return Iterator
+ */
+ public function query(DbQuery $query)
+ {
+ return $query->getSelectQuery()->query();
+ }
+
+ /**
+ * Get the connection configuration
+ *
+ * @return ConfigObject
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Getter for database type
+ *
+ * @return string
+ */
+ public function getDbType()
+ {
+ return $this->dbType;
+ }
+
+ /**
+ * Getter for the Zend_Db_Adapter
+ *
+ * @return \Zend_Db_Adapter_Abstract
+ */
+ public function getDbAdapter()
+ {
+ return $this->dbAdapter;
+ }
+
+ /**
+ * Create a new connection
+ */
+ private function connect()
+ {
+ $genericAdapterOptions = self::$genericAdapterOptions;
+ $driverOptions = self::$driverOptions;
+ $adapterParamaters = array(
+ 'host' => $this->config->host,
+ 'username' => $this->config->username,
+ 'password' => $this->config->password,
+ 'dbname' => $this->config->dbname,
+ 'charset' => $this->config->charset ?: null,
+ 'options' => & $genericAdapterOptions,
+ 'driver_options' => & $driverOptions
+ );
+ $this->dbType = strtolower($this->config->get('db', 'mysql'));
+ switch ($this->dbType) {
+ case 'mssql':
+ $adapter = 'Pdo_Mssql';
+ $pdoType = $this->config->get('pdoType');
+ if (empty($pdoType)) {
+ if (extension_loaded('sqlsrv')) {
+ $adapter = 'Sqlsrv';
+ } else {
+ $pdoType = 'dblib';
+ }
+ }
+ if ($pdoType === 'dblib') {
+ // Driver does not support setting attributes
+ unset($adapterParamaters['options']);
+ unset($adapterParamaters['driver_options']);
+ }
+ if (! empty($pdoType)) {
+ $adapterParamaters['pdoType'] = $pdoType;
+ }
+ $defaultPort = 1433;
+ break;
+ case 'mysql':
+ $adapter = 'Pdo_Mysql';
+ if ($this->config->use_ssl) {
+ # The presence of these keys as empty strings or null cause non-ssl connections to fail
+ if ($this->config->ssl_key) {
+ $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_KEY] = $this->config->ssl_key;
+ }
+ if ($this->config->ssl_cert) {
+ $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CERT] = $this->config->ssl_cert;
+ }
+ if ($this->config->ssl_ca) {
+ $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CA] = $this->config->ssl_ca;
+ }
+ if ($this->config->ssl_capath) {
+ $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CAPATH] = $this->config->ssl_capath;
+ }
+ if ($this->config->ssl_cipher) {
+ $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CIPHER] = $this->config->ssl_cipher;
+ }
+ if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')
+ && $this->config->ssl_do_not_verify_server_cert
+ ) {
+ $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false;
+ }
+ }
+ /*
+ * Set MySQL server SQL modes to behave as closely as possible to Oracle and PostgreSQL. Note that the
+ * ONLY_FULL_GROUP_BY mode is left on purpose because MySQL requires you to specify all non-aggregate
+ * columns in the group by list even if the query is grouped by the master table's primary key which is
+ * valid ANSI SQL though. Further in that case the query plan would suffer if you add more columns to
+ * the group by list.
+ */
+ $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,'
+ . 'ANSI_QUOTES,PIPES_AS_CONCAT,NO_ENGINE_SUBSTITUTION\'';
+ if (isset($adapterParamaters['charset'])) {
+ $driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .= ', NAMES ' . $adapterParamaters['charset'];
+ if (trim($adapterParamaters['charset']) === 'latin1') {
+ // Required for MySQL 8+ because we need PIPES_AS_CONCAT and
+ // have several columns with explicit COLLATE instructions
+ $driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .= ' COLLATE latin1_general_ci';
+ }
+
+ unset($adapterParamaters['charset']);
+ }
+
+ $driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .= ", time_zone='" . $this->defaultTimezoneOffset() . "'";
+ $driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .=';';
+ $defaultPort = 3306;
+ break;
+ case 'oci':
+ $adapter = 'Oracle';
+ unset($adapterParamaters['options']);
+ unset($adapterParamaters['driver_options']);
+ $adapterParamaters['driver_options'] = array(
+ 'lob_as_string' => true
+ );
+ $defaultPort = 1521;
+ break;
+ case 'oracle':
+ $adapter = 'Pdo_Oci';
+ $defaultPort = 1521;
+
+ // remove host parameter when not configured
+ if (empty($this->config->host)) {
+ unset($adapterParamaters['host']);
+ }
+ break;
+ case 'pgsql':
+ $adapter = 'Pdo_Pgsql';
+ $defaultPort = 5432;
+ break;
+ case 'ibm':
+ $adapter = 'Pdo_Ibm';
+ $defaultPort = 50000;
+ break;
+ case 'sqlite':
+ $adapter = 'Pdo_Sqlite';
+ $defaultPort = 0; // Dummy port because a value is required
+ break;
+ default:
+ throw new ConfigurationError(
+ 'Backend "%s" is not supported',
+ $this->dbType
+ );
+ }
+ $adapterParamaters['port'] = $this->config->get('port', $defaultPort);
+ $this->dbAdapter = Zend_Db::factory($adapter, $adapterParamaters);
+ $this->dbAdapter->setFetchMode(Zend_Db::FETCH_OBJ);
+ // TODO(el/tg): The profiler is disabled per default, why do we disable the profiler explicitly?
+ $this->dbAdapter->getProfiler()->setEnabled(false);
+ }
+
+ public static function fromResourceName($name)
+ {
+ return new static(ResourceFactory::getResourceConfig($name));
+ }
+
+ /**
+ * Getter for the table prefix
+ *
+ * @return string
+ */
+ public function getTablePrefix()
+ {
+ return $this->tablePrefix;
+ }
+
+ /**
+ * Setter for the table prefix
+ *
+ * @param string $prefix
+ *
+ * @return $this
+ */
+ public function setTablePrefix($prefix)
+ {
+ $this->tablePrefix = $prefix;
+ return $this;
+ }
+
+ /**
+ * Get offset from the current default timezone to GMT
+ *
+ * @return string
+ */
+ protected function defaultTimezoneOffset()
+ {
+ $tz = new DateTimeZone(date_default_timezone_get());
+ $offset = $tz->getOffset(new DateTime());
+ $prefix = $offset >= 0 ? '+' : '-';
+ $offset = abs($offset);
+ $hours = (int) floor($offset / 3600);
+ $minutes = (int) floor(($offset % 3600) / 60);
+ return sprintf('%s%d:%02d', $prefix, $hours, $minutes);
+ }
+
+ /**
+ * Count all rows of the result set
+ *
+ * @param DbQuery $query
+ *
+ * @return int
+ */
+ public function count(DbQuery $query)
+ {
+ return (int) $this->dbAdapter->fetchOne($query->getCountQuery());
+ }
+
+ /**
+ * Retrieve an array containing all rows of the result set
+ *
+ * @param DbQuery $query
+ *
+ * @return array
+ */
+ public function fetchAll(DbQuery $query)
+ {
+ return $this->dbAdapter->fetchAll($query->getSelectQuery());
+ }
+
+ /**
+ * Fetch the first row of the result set
+ *
+ * @param DbQuery $query
+ *
+ * @return mixed
+ */
+ public function fetchRow(DbQuery $query)
+ {
+ return $this->dbAdapter->fetchRow($query->getSelectQuery());
+ }
+
+ /**
+ * Fetch the first column of all rows of the result set as an array
+ *
+ * @param DbQuery $query
+ *
+ * @return array
+ */
+ public function fetchColumn(DbQuery $query)
+ {
+ return $this->dbAdapter->fetchCol($query->getSelectQuery());
+ }
+
+ /**
+ * Fetch the first column of the first row of the result set
+ *
+ * @param DbQuery $query
+ *
+ * @return string
+ */
+ public function fetchOne(DbQuery $query)
+ {
+ return $this->dbAdapter->fetchOne($query->getSelectQuery());
+ }
+
+ /**
+ * Fetch all rows of the result set as an array of key-value pairs
+ *
+ * The first column is the key, the second column is the value.
+ *
+ * @param DbQuery $query
+ *
+ * @return array
+ */
+ public function fetchPairs(DbQuery $query)
+ {
+ return $this->dbAdapter->fetchPairs($query->getSelectQuery());
+ }
+
+ /**
+ * Insert a table row with the given data
+ *
+ * Note that the base implementation does not perform any quoting on the $table argument.
+ * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
+ * as third parameter $types to define a different type than string for a particular column.
+ *
+ * @param string $table
+ * @param array $bind
+ * @param array $types
+ *
+ * @return int The number of affected rows
+ */
+ public function insert($table, array $bind, array $types = array())
+ {
+ $columns = $values = array();
+ foreach ($bind as $column => $value) {
+ $columns[] = $column;
+ if ($value instanceof Zend_Db_Expr) {
+ $values[] = (string) $value;
+ unset($bind[$column]);
+ } else {
+ $values[] = ':' . $column;
+ }
+ }
+
+ $sql = 'INSERT INTO ' . $table
+ . ' (' . join(', ', $columns) . ') '
+ . 'VALUES (' . join(', ', $values) . ')';
+ $statement = $this->dbAdapter->prepare($sql);
+
+ foreach ($bind as $column => $value) {
+ $type = isset($types[$column]) ? $types[$column] : PDO::PARAM_STR;
+ $statement->bindValue(':' . $column, $value, $type);
+ }
+
+ $statement->execute();
+ return $statement->rowCount();
+ }
+
+ /**
+ * Update table rows with the given data, optionally limited by using a filter
+ *
+ * Note that the base implementation does not perform any quoting on the $table argument.
+ * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
+ * as fourth parameter $types to define a different type than string for a particular column.
+ *
+ * @param string $table
+ * @param array $bind
+ * @param Filter $filter
+ * @param array $types
+ *
+ * @return int The number of affected rows
+ */
+ public function update($table, array $bind, Filter $filter = null, array $types = array())
+ {
+ $set = array();
+ foreach ($bind as $column => $value) {
+ if ($value instanceof Zend_Db_Expr) {
+ $set[] = $column . ' = ' . $value;
+ unset($bind[$column]);
+ } else {
+ $set[] = $column . ' = :' . $column;
+ }
+ }
+
+ $sql = 'UPDATE ' . $table
+ . ' SET ' . join(', ', $set)
+ . ($filter ? ' WHERE ' . $this->renderFilter($filter) : '');
+ $statement = $this->dbAdapter->prepare($sql);
+
+ foreach ($bind as $column => $value) {
+ $type = isset($types[$column]) ? $types[$column] : PDO::PARAM_STR;
+ $statement->bindValue(':' . $column, $value, $type);
+ }
+
+ $statement->execute();
+ return $statement->rowCount();
+ }
+
+ /**
+ * Delete table rows, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param Filter $filter
+ *
+ * @return int The number of affected rows
+ */
+ public function delete($table, Filter $filter = null)
+ {
+ return $this->dbAdapter->delete($table, $filter ? $this->renderFilter($filter) : '');
+ }
+
+ /**
+ * Render and return the given filter as SQL-WHERE clause
+ *
+ * @param Filter $filter
+ *
+ * @return string
+ */
+ public function renderFilter(Filter $filter, $level = 0)
+ {
+ // TODO: This is supposed to supersede DbQuery::renderFilter()
+ $where = '';
+ if ($filter->isChain()) {
+ if ($filter instanceof FilterAnd) {
+ $operator = ' AND ';
+ } elseif ($filter instanceof FilterOr) {
+ $operator = ' OR ';
+ } elseif ($filter instanceof FilterNot) {
+ $operator = ' AND ';
+ $where .= ' NOT ';
+ } else {
+ throw new ProgrammingError('Cannot render filter: %s', get_class($filter));
+ }
+
+ if (! $filter->isEmpty()) {
+ $parts = array();
+ foreach ($filter->filters() as $filterPart) {
+ $part = $this->renderFilter($filterPart, $level + 1);
+ if ($part) {
+ $parts[] = $part;
+ }
+ }
+
+ if (! empty($parts)) {
+ if ($level > 0) {
+ $where .= ' (' . implode($operator, $parts) . ') ';
+ } else {
+ $where .= implode($operator, $parts);
+ }
+ }
+ } else {
+ return ''; // Explicitly return the empty string due to the FilterNot case
+ }
+ } else {
+ $where .= $this->renderFilterExpression($filter);
+ }
+
+ return $where;
+ }
+
+ /**
+ * Render and return the given filter expression
+ *
+ * @param Filter $filter
+ *
+ * @return string
+ */
+ protected function renderFilterExpression(Filter $filter)
+ {
+ $column = $filter->getColumn();
+ $sign = $filter->getSign();
+ $value = $filter->getExpression();
+
+ if (is_array($value)) {
+ $comp = [];
+ $pattern = [];
+ foreach ($value as $val) {
+ if (strpos($val, '*') === false) {
+ $comp[] = $val;
+ } else {
+ $pattern[] = $this->renderFilterExpression(Filter::expression($column, $sign, $val));
+ }
+ }
+
+ $sql = $pattern;
+ if ($sign === '=') {
+ if (! empty($comp)) {
+ $sql[] = $column . ' IN (' . $this->dbAdapter->quote($comp) . ')';
+ }
+
+ $operator = 'OR';
+ } elseif ($sign === '!=') {
+ if (! empty($comp)) {
+ $sql[] = sprintf(
+ '(%1$s NOT IN (%2$s) OR %1$s IS NULL)',
+ $column,
+ $this->dbAdapter->quote($comp)
+ );
+ }
+
+ $operator = 'AND';
+ } else {
+ throw new ProgrammingError(
+ 'Unable to render array expressions with operators other than equal or not equal'
+ );
+ }
+
+ return count($sql) === 1 ? $sql[0] : '(' . implode(" $operator ", $sql) . ')';
+ } elseif ($sign === '='
+ && ! $filter instanceof FilterEqual
+ && $value !== null
+ && strpos($value, '*') !== false
+ ) {
+ if ($value === '*') {
+ return $column . ' IS NOT NULL';
+ }
+
+ return $column . ' LIKE ' . $this->dbAdapter->quote(preg_replace('~\*~', '%', $value));
+ } elseif ($sign === '!='
+ && ! $filter instanceof FilterNotEqual
+ && $value !== null
+ && strpos($value, '*') !== false
+ ) {
+ if ($value === '*') {
+ return $column . ' IS NULL';
+ }
+
+ return sprintf(
+ '(%1$s NOT LIKE %2$s OR %1$s IS NULL)',
+ $column,
+ $this->dbAdapter->quote(preg_replace('~\*~', '%', $value))
+ );
+ } elseif ($sign === '!=') {
+ return sprintf('(%1$s != %2$s OR %1$s IS NULL)', $column, $this->dbAdapter->quote($value));
+ } else {
+ return sprintf('%s %s %s', $column, $sign, $this->dbAdapter->quote($value));
+ }
+ }
+
+ public function inspect()
+ {
+ $insp = new Inspection('Db Connection');
+ try {
+ $this->getDbAdapter()->getConnection();
+ $config = $this->dbAdapter->getConfig();
+ $insp->write(sprintf(
+ 'Connection to %s as %s on %s:%s successful',
+ $config['dbname'],
+ $config['username'],
+ array_key_exists('host', $config) ? $config['host'] : '(none)',
+ $config['port']
+ ));
+ switch ($this->dbType) {
+ case 'mysql':
+ $rows = $this->dbAdapter->query(
+ 'SHOW VARIABLES WHERE variable_name ' .
+ 'IN (\'version\', \'protocol_version\', \'version_compile_os\', \'have_ssl\');'
+ )->fetchAll();
+ $sqlinsp = new Inspection('MySQL');
+ $hasSsl = false;
+ foreach ($rows as $row) {
+ $sqlinsp->write($row->variable_name . ': ' . $row->value);
+ if ($row->variable_name === 'have_ssl' && $row->value === 'YES') {
+ $hasSsl = true;
+ }
+ }
+ if ($hasSsl) {
+ $ssl_rows = $this->dbAdapter->query(
+ 'SHOW STATUS WHERE variable_name ' .
+ 'IN (\'Ssl_Cipher\');'
+ )->fetchAll();
+ foreach ($ssl_rows as $ssl_row) {
+ $sqlinsp->write($ssl_row->variable_name . ': ' . $ssl_row->value);
+ }
+ }
+ $insp->write($sqlinsp);
+ break;
+ case 'pgsql':
+ $row = $this->dbAdapter->query('SELECT version();')->fetchAll();
+ $sqlinsp = new Inspection('PostgreSQL');
+ $sqlinsp->write($row[0]->version);
+ $insp->write($sqlinsp);
+ break;
+ }
+ } catch (Exception $e) {
+ return $insp->error(sprintf('Connection failed %s', $e->getMessage()));
+ }
+ return $insp;
+ }
+}
diff --git a/library/Icinga/Data/Db/DbQuery.php b/library/Icinga/Data/Db/DbQuery.php
new file mode 100644
index 0000000..ff1d131
--- /dev/null
+++ b/library/Icinga/Data/Db/DbQuery.php
@@ -0,0 +1,565 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Db;
+
+use DateInterval;
+use DateTime;
+use DateTimeZone;
+use Exception;
+use Icinga\Data\Filter\Filter;
+use Zend_Db_Adapter_Abstract;
+use Zend_Db_Expr;
+use Zend_Db_Select;
+use Icinga\Application\Logger;
+use Icinga\Data\SimpleQuery;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\QueryException;
+
+/**
+ * Database query class
+ */
+class DbQuery extends SimpleQuery
+{
+ /**
+ * @var Zend_Db_Adapter_Abstract
+ */
+ protected $db;
+
+ /**
+ * Whether or not the query is a sub query
+ *
+ * Sub queries are automatically wrapped in parentheses
+ *
+ * @var bool
+ */
+ protected $isSubQuery = false;
+
+ /**
+ * Select query
+ *
+ * @var Zend_Db_Select
+ */
+ protected $select;
+
+ /**
+ * Whether to use a subquery for counting
+ *
+ * When the query is distinct or has a HAVING or GROUP BY clause this must be set to true
+ *
+ * @var bool
+ */
+ protected $useSubqueryCount = false;
+
+ /**
+ * Count query result
+ *
+ * Count queries are only executed once
+ *
+ * @var int
+ */
+ protected $count;
+
+ /**
+ * GROUP BY clauses
+ *
+ * @var string|array
+ */
+ protected $group;
+
+ protected function init()
+ {
+ $this->db = $this->ds->getDbAdapter();
+ $this->select = $this->db->select();
+ parent::init();
+ }
+
+ /**
+ * Get whether or not the query is a sub query
+ */
+ public function getIsSubQuery()
+ {
+ return $this->isSubQuery;
+ }
+
+ /**
+ * Set whether or not the query is a sub query
+ *
+ * @param bool $isSubQuery
+ *
+ * @return $this
+ */
+ public function setIsSubQuery($isSubQuery = true)
+ {
+ $this->isSubQuery = (bool) $isSubQuery;
+ return $this;
+ }
+
+ public function setUseSubqueryCount($useSubqueryCount = true)
+ {
+ $this->useSubqueryCount = $useSubqueryCount;
+ return $this;
+ }
+
+ public function from($target, array $fields = null)
+ {
+ parent::from($target, $fields);
+ $this->select->from($this->target, array());
+ return $this;
+ }
+
+ public function where($condition, $value = null)
+ {
+ // $this->count = $this->select = null;
+ return parent::where($condition, $value);
+ }
+
+ public function addFilter(Filter $filter)
+ {
+ $this->expressionsToTimestamp($filter);
+ return parent::addFilter($filter);
+ }
+
+ private function expressionsToTimestamp(Filter $filter)
+ {
+ if ($filter->isChain()) {
+ foreach ($filter->filters() as $child) {
+ $this->expressionsToTimestamp($child);
+ }
+ } elseif ($this->isTimestamp($filter->getColumn())) {
+ $filter->setExpression($this->valueToTimestamp($filter->getExpression()));
+ }
+ }
+
+ protected function dbSelect()
+ {
+ return clone $this->select;
+ }
+
+ /**
+ * Return the underlying select
+ *
+ * @return Zend_Db_Select
+ */
+ public function select()
+ {
+ return $this->select;
+ }
+
+ /**
+ * Get the select query
+ *
+ * Applies order and limit if any
+ *
+ * @return Zend_Db_Select
+ */
+ public function getSelectQuery()
+ {
+ $select = $this->dbSelect();
+ // Add order fields to select for postgres distinct queries (#6351)
+ if ($this->hasOrder()
+ && $this->getDatasource()->getDbType() === 'pgsql'
+ && $select->getPart(Zend_Db_Select::DISTINCT) === true) {
+ foreach ($this->getOrder() as $fieldAndDirection) {
+ if (array_search($fieldAndDirection[0], $this->columns, true) === false) {
+ $this->columns[] = $fieldAndDirection[0];
+ }
+ }
+ }
+
+ $group = $this->getGroup();
+ if ($group) {
+ $select->group($group);
+ }
+
+ if (! empty($this->columns)) {
+ $select->columns($this->columns);
+ }
+
+ $this->applyFilterSql($select);
+
+ if ($this->hasLimit() || $this->hasOffset()) {
+ $select->limit($this->getLimit(), $this->getOffset());
+ }
+ if ($this->hasOrder()) {
+ foreach ($this->getOrder() as $fieldAndDirection) {
+ $select->order(
+ $fieldAndDirection[0] . ' ' . $fieldAndDirection[1]
+ );
+ }
+ }
+
+ return $select;
+ }
+
+ protected function applyFilterSql($select)
+ {
+ $where = $this->getDatasource()->renderFilter($this->filter);
+ if ($where !== '') {
+ $select->where($where);
+ }
+ }
+
+ protected function escapeForSql($value)
+ {
+ // bindParam? bindValue?
+ if (is_array($value)) {
+ $ret = array();
+ foreach ($value as $val) {
+ $ret[] = $this->escapeForSql($val);
+ }
+ return implode(', ', $ret);
+ } else {
+ //if (preg_match('/^\d+$/', $value)) {
+ // return $value;
+ //} else {
+ return $this->db->quote($value);
+ //}
+ }
+ }
+
+ protected function escapeWildcards($value)
+ {
+ return preg_replace('/\*/', '%', $value);
+ }
+
+ protected function valueToTimestamp($value)
+ {
+ if (is_string($value)) {
+ if (ctype_digit($value)) {
+ $value = (int) $value;
+ } else {
+ $value = strtotime($value);
+ }
+ } elseif (! is_int($value)) {
+ $value = (int) $value;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Render the given timestamp based on the local timezone
+ *
+ * Since {@see DbConnection::defaultTimezoneOffset()} tells the database the timezone with just an offset,
+ * this will prepare the rendered value in a way that it plays fine with daylight savings.
+ *
+ * @param int $value
+ * @return string
+ */
+ protected function timestampForSql($value)
+ {
+ if ($this->getDatasource()->getDbType() === 'pgsql') {
+ // We don't tell PostgreSQL the user's timezone
+ $dateTime = (new DateTime())
+ ->setTimezone(new DateTimeZone('UTC'))
+ ->setTimestamp($value);
+ } else {
+ $dateTime = new DateTime();
+ // Get "current" offset the database will use
+ $offsetToUTC = $dateTime->getOffset();
+ // Set timezone to UTC and initialize it with the timestamp
+ $dateTime->setTimezone(new DateTimeZone('UTC'))->setTimestamp($value);
+ // Normalize every datetime based on the only offset the database knows about
+ if ($offsetToUTC >= 0) {
+ $dateTime->add(new DateInterval("PT{$offsetToUTC}S"));
+ } else {
+ $offsetToUTC = abs($offsetToUTC);
+ $dateTime->sub(new DateInterval("PT{$offsetToUTC}S"));
+ }
+ }
+
+ return $dateTime->format('Y-m-d H:i:s');
+ }
+
+ /**
+ * Check for timestamp fields
+ *
+ * TODO: This is not here to do automagic timestamp stuff. One may
+ * override this function for custom voodoo, IdoQuery right now
+ * does. IMO we need to split whereToSql functionality, however
+ * I'd prefer to wait with this unless we understood how other
+ * backends will work. We probably should also rename this
+ * function to isTimestampColumn().
+ *
+ * @param string $field Field Field name to checked
+ * @return bool Whether this field expects timestamps
+ */
+ public function isTimestamp($field)
+ {
+ return false;
+ }
+
+ /**
+ * Get the count query
+ *
+ * @return Zend_Db_Select
+ */
+ public function getCountQuery()
+ {
+ // TODO: there may be situations where we should clone the "select"
+ $count = $this->dbSelect();
+ $this->applyFilterSql($count);
+ $group = $this->getGroup();
+ if ($this->useSubqueryCount || $group) {
+ if (! empty($this->columns)) {
+ $count->columns($this->columns);
+ }
+ if ($group) {
+ $count->group($group);
+ }
+ $columns = array('cnt' => 'COUNT(*)');
+ return $this->db->select()->from($count, $columns);
+ }
+
+ $count->columns(array('cnt' => 'COUNT(*)'));
+ return $count;
+ }
+
+ /**
+ * Count all rows of the result set
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ if ($this->count === null) {
+ $this->count = parent::count();
+ }
+
+ return $this->count;
+ }
+
+ /**
+ * Return the select and count query as a textual representation
+ *
+ * @return string A string containing the select and count query, using unix style newlines as linebreaks
+ */
+ public function dump()
+ {
+ return "QUERY\n=====\n"
+ . $this->getSelectQuery()
+ . "\n\nCOUNT\n=====\n"
+ . $this->getCountQuery()
+ . "\n\n";
+ }
+
+ public function __clone()
+ {
+ parent::__clone();
+ $this->select = clone $this->select;
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString()
+ {
+ try {
+ $select = (string) $this->getSelectQuery();
+ return $this->getIsSubQuery() ? ('(' . $select . ')') : $select;
+ } catch (Exception $e) {
+ Logger::debug('Failed to render DbQuery. An error occured: %s', $e);
+ return '';
+ }
+ }
+
+ /**
+ * Add a GROUP BY clause
+ *
+ * @param string|array $group
+ *
+ * @return $this
+ */
+ public function group($group)
+ {
+ $this->group = $group;
+ return $this;
+ }
+
+ /**
+ * Return the GROUP BY clause
+ *
+ * @return string|array
+ */
+ public function getGroup()
+ {
+ return $this->group;
+ }
+
+ /**
+ * Return whether the given table has been joined
+ *
+ * @param string $table
+ *
+ * @return bool
+ */
+ public function hasJoinedTable($table)
+ {
+ $fromPart = $this->select->getPart(Zend_Db_Select::FROM);
+ if (isset($fromPart[$table])) {
+ return true;
+ }
+
+ foreach ($fromPart as $options) {
+ if ($options['tableName'] === $table && $options['joinType'] !== Zend_Db_Select::FROM) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Return the alias used for joining the given table
+ *
+ * @param string $table
+ *
+ * @return string|null null in case no alias is being used
+ *
+ * @throws ProgrammingError In case the given table has not been joined
+ */
+ public function getJoinedTableAlias($table)
+ {
+ $fromPart = $this->select->getPart(Zend_Db_Select::FROM);
+ if (isset($fromPart[$table])) {
+ if ($fromPart[$table]['joinType'] === Zend_Db_Select::FROM) {
+ throw new ProgrammingError('Table "%s" has not been joined', $table);
+ }
+
+ return; // No alias in use
+ }
+
+ foreach ($fromPart as $alias => $options) {
+ if ($options['tableName'] === $table && $options['joinType'] !== Zend_Db_Select::FROM) {
+ return $alias;
+ }
+ }
+
+ throw new ProgrammingError('Table "%s" has not been joined', $table);
+ }
+
+ /**
+ * Add an INNER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function join($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinInner($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add an INNER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinInner($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinInner($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a LEFT OUTER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinLeft($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinLeft($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a RIGHT OUTER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinRight($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinRight($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a FULL OUTER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinFull($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinFull($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a CROSS JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinCross($name, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinCross($name, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a NATURAL JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinNatural($name, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinNatural($name, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a UNION clause to the query
+ *
+ * @param array $select Select clauses for the union
+ * @param string $type Type of UNION to use
+ *
+ * @return $this
+ */
+ public function union($select = array(), $type = Zend_Db_Select::SQL_UNION)
+ {
+ $this->select->union($select, $type);
+ return $this;
+ }
+}
diff --git a/library/Icinga/Data/Extensible.php b/library/Icinga/Data/Extensible.php
new file mode 100644
index 0000000..ad690d8
--- /dev/null
+++ b/library/Icinga/Data/Extensible.php
@@ -0,0 +1,22 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+use Icinga\Exception\StatementException;
+
+/**
+ * Interface for data insertion
+ */
+interface Extensible
+{
+ /**
+ * Insert the given data for the given target
+ *
+ * @param string $target
+ * @param array $data
+ *
+ * @throws StatementException
+ */
+ public function insert($target, array $data);
+}
diff --git a/library/Icinga/Data/Fetchable.php b/library/Icinga/Data/Fetchable.php
new file mode 100644
index 0000000..342740a
--- /dev/null
+++ b/library/Icinga/Data/Fetchable.php
@@ -0,0 +1,47 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+/**
+ * Interface for retrieving data
+ */
+interface Fetchable
+{
+ /**
+ * Retrieve an array containing all rows of the result set
+ *
+ * @return array
+ */
+ public function fetchAll();
+
+ /**
+ * Fetch the first row of the result set
+ *
+ * @return mixed
+ */
+ public function fetchRow();
+
+ /**
+ * Fetch the first column of all rows of the result set as an array
+ *
+ * @return array
+ */
+ public function fetchColumn();
+
+ /**
+ * Fetch the first column of the first row of the result set
+ *
+ * @return string
+ */
+ public function fetchOne();
+
+ /**
+ * Fetch all rows of the result set as an array of key-value pairs
+ *
+ * The first column is the key, the second column is the value.
+ *
+ * @return array
+ */
+ public function fetchPairs();
+}
diff --git a/library/Icinga/Data/Filter/Filter.php b/library/Icinga/Data/Filter/Filter.php
new file mode 100644
index 0000000..f5d8bdf
--- /dev/null
+++ b/library/Icinga/Data/Filter/Filter.php
@@ -0,0 +1,255 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+use Icinga\Web\UrlParams;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Filter
+ *
+ * Base class for filters (why?) and factory for the different FilterOperators
+ */
+abstract class Filter
+{
+ protected $id = '1';
+
+ public function setId($id)
+ {
+ $this->id = (string) $id;
+ return $this;
+ }
+
+ abstract public function isExpression();
+
+ abstract public function isChain();
+
+ abstract public function isEmpty();
+
+ abstract public function toQueryString();
+
+ abstract public function andFilter(Filter $filter);
+
+ abstract public function orFilter(Filter $filter);
+
+ /**
+ * Whether the give row matches this Filter
+ *
+ * @param mixed $row Preferrably an stdClass instance
+ * @return bool
+ */
+ abstract public function matches($row);
+
+ public function getUrlParams()
+ {
+ return UrlParams::fromQueryString($this->toQueryString());
+ }
+
+ public function getById($id)
+ {
+ if ((string) $id === $this->getId()) {
+ return $this;
+ }
+ throw new ProgrammingError(
+ 'Trying to get invalid filter index "%s" from "%s" ("%s")',
+ $id,
+ $this,
+ $this->id
+ );
+ }
+
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ public function isRootNode()
+ {
+ return false === strpos($this->id, '-');
+ }
+
+ abstract public function listFilteredColumns();
+
+ public function applyChanges($changes)
+ {
+ $filter = $this;
+ $pairs = array();
+ foreach ($changes as $k => $v) {
+ if (preg_match('/^(column|value|sign|operator)_([\d-]+)$/', $k, $m)) {
+ $pairs[$m[2]][$m[1]] = $v;
+ }
+ }
+ $operators = array();
+ foreach ($pairs as $id => $fs) {
+ if (array_key_exists('operator', $fs)) {
+ $operators[$id] = $fs['operator'];
+ } else {
+ $f = $filter->getById($id);
+ $f->setColumn($fs['column']);
+ if ($f->getSign() !== $fs['sign']) {
+ if ($f->isRootNode()) {
+ $filter = $f->setSign($fs['sign']);
+ } else {
+ $filter->replaceById($id, $f->setSign($fs['sign']));
+ }
+ }
+ $f->setExpression($fs['value']);
+ }
+ }
+
+ krsort($operators, SORT_NATURAL);
+ foreach ($operators as $id => $operator) {
+ $f = $filter->getById($id);
+ if ($f->getOperatorName() !== $operator) {
+ if ($f->isRootNode()) {
+ $filter = $f->setOperatorName($operator);
+ } else {
+ $filter->replaceById($id, $f->setOperatorName($operator));
+ }
+ }
+ }
+
+ return $filter;
+ }
+
+ public function getParentId()
+ {
+ if ($this->isRootNode()) {
+ throw new ProgrammingError('Filter root nodes have no parent');
+ }
+ return substr($this->id, 0, strrpos($this->id, '-'));
+ }
+
+ public function getParent()
+ {
+ return $this->getById($this->getParentId());
+ }
+
+ public function hasId($id)
+ {
+ if ($id === $this->getId()) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Where Filter factory
+ *
+ * @param string $col Column to be filtered
+ * @param string $filter Filter expression
+ *
+ * @throws FilterException
+ * @return FilterExpression
+ */
+ public static function where($col, $filter)
+ {
+ return new FilterExpression($col, '=', $filter);
+ }
+
+ public static function expression($col, $op, $expression)
+ {
+ switch ($op) {
+ case '=':
+ return new FilterMatch($col, $op, $expression);
+ case '<':
+ return new FilterLessThan($col, $op, $expression);
+ case '>':
+ return new FilterGreaterThan($col, $op, $expression);
+ case '>=':
+ return new FilterEqualOrGreaterThan($col, $op, $expression);
+ case '<=':
+ return new FilterEqualOrLessThan($col, $op, $expression);
+ case '!=':
+ return new FilterMatchNot($col, $op, $expression);
+ default:
+ throw new ProgrammingError(
+ 'There is no such filter sign: %s',
+ $op
+ );
+ }
+ }
+
+ /**
+ * Or FilterOperator factory
+ *
+ * @param Filter $filter,... Unlimited optional list of Filters
+ *
+ * @return FilterOr
+ */
+ public static function matchAny()
+ {
+ $args = func_get_args();
+ if (count($args) === 1 && is_array($args[0])) {
+ $args = $args[0];
+ }
+ return new FilterOr($args);
+ }
+
+ /**
+ * Or FilterOperator factory
+ *
+ * @param Filter $filter,... Unlimited optional list of Filters
+ *
+ * @return FilterAnd
+ */
+ public static function matchAll()
+ {
+ $args = func_get_args();
+ if (count($args) === 1 && is_array($args[0])) {
+ $args = $args[0];
+ }
+ return new FilterAnd($args);
+ }
+
+ /**
+ * FilterNot factory, negates the given filter
+ *
+ * @param Filter $filter Filter to be negated
+ *
+ * @return FilterNot
+ */
+ public static function not()
+ {
+ $args = func_get_args();
+ if (count($args) === 1) {
+ if (is_array($args[0])) {
+ $args = $args[0];
+ }
+ }
+ if (count($args) > 1) {
+ return new FilterNot(array(new FilterAnd($args)));
+ } else {
+ return new FilterNot($args);
+ }
+ }
+
+ public static function chain($operator, $filters = array())
+ {
+ switch ($operator) {
+ case 'AND':
+ return self::matchAll($filters);
+ case 'OR':
+ return self::matchAny($filters);
+ case 'NOT':
+ return self::not($filters);
+ }
+ throw new ProgrammingError(
+ '"%s" is not a valid filter chain operator',
+ $operator
+ );
+ }
+
+ /**
+ * Create filter from queryString
+ *
+ * This is still pretty basic, need improvement
+ *
+ * @return static
+ */
+ public static function fromQueryString($query)
+ {
+ return FilterQueryString::parse($query);
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterAnd.php b/library/Icinga/Data/Filter/FilterAnd.php
new file mode 100644
index 0000000..96b68cc
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterAnd.php
@@ -0,0 +1,42 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+/**
+ * Filter list AND
+ *
+ * Binary AND, all contained filters must succeed
+ */
+class FilterAnd extends FilterChain
+{
+ protected $operatorName = 'AND';
+
+ protected $operatorSymbol = '&';
+
+ /**
+ * Whether the given row object matches this filter
+ *
+ * @object $row
+ * @return boolean
+ */
+ public function matches($row)
+ {
+ foreach ($this->filters as $filter) {
+ if (! $filter->matches($row)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public function andFilter(Filter $filter)
+ {
+ return $this->addFilter($filter);
+ }
+
+ public function orFilter(Filter $filter)
+ {
+ return Filter::matchAny($this, $filter);
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterChain.php b/library/Icinga/Data/Filter/FilterChain.php
new file mode 100644
index 0000000..0f1e071
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterChain.php
@@ -0,0 +1,286 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\QueryException;
+
+/**
+ * FilterChain
+ *
+ * A FilterChain contains a list ...
+ */
+abstract class FilterChain extends Filter
+{
+ protected $filters = array();
+
+ protected $operatorName;
+
+ protected $operatorSymbol;
+
+ protected $allowedColumns;
+
+ /**
+ * Set the filters
+ *
+ * @param array $filters
+ *
+ * @return $this
+ */
+ public function setFilters(array $filters)
+ {
+ $this->filters = $filters;
+
+ $this->refreshChildIds();
+
+ return $this;
+ }
+
+ public function hasId($id)
+ {
+ foreach ($this->filters() as $filter) {
+ if ($filter->hasId($id)) {
+ return true;
+ }
+ }
+ return parent::hasId($id);
+ }
+
+ public function getById($id)
+ {
+ foreach ($this->filters() as $filter) {
+ if ($filter->hasId($id)) {
+ return $filter->getById($id);
+ }
+ }
+ return parent::getById($id);
+ }
+
+ public function removeId($id)
+ {
+ if ($id === $this->getId()) {
+ $this->filters = array();
+ return $this;
+ }
+ $remove = null;
+ foreach ($this->filters as $key => $filter) {
+ if ($filter->getId() === $id) {
+ $remove = $key;
+ } elseif ($filter instanceof FilterChain) {
+ $filter->removeId($id);
+ }
+ }
+ if ($remove !== null) {
+ unset($this->filters[$remove]);
+ $this->filters = array_values($this->filters);
+ }
+ $this->refreshChildIds();
+ return $this;
+ }
+
+ public function replaceById($id, $filter)
+ {
+ $found = false;
+ foreach ($this->filters as $k => $child) {
+ if ($child->getId() == $id) {
+ $this->filters[$k] = $filter;
+ $found = true;
+ break;
+ }
+ if ($child->hasId($id)) {
+ $child->replaceById($id, $filter);
+ $found = true;
+ break;
+ }
+ }
+ if (! $found) {
+ throw new ProgrammingError('You tried to replace an unexistant child filter');
+ }
+ $this->refreshChildIds();
+ return $this;
+ }
+
+ protected function refreshChildIds()
+ {
+ $i = 0;
+ $id = $this->getId();
+ foreach ($this->filters as $filter) {
+ $i++;
+ $filter->setId($id . '-' . $i);
+ }
+ return $this;
+ }
+
+ public function setId($id)
+ {
+ return parent::setId($id)->refreshChildIds();
+ }
+
+ public function getOperatorName()
+ {
+ return $this->operatorName;
+ }
+
+ public function setOperatorName($name)
+ {
+ if ($name !== $this->operatorName) {
+ return Filter::chain($name, $this->filters);
+ }
+ return $this;
+ }
+
+ public function getOperatorSymbol()
+ {
+ return $this->operatorSymbol;
+ }
+
+ public function setAllowedFilterColumns(array $columns)
+ {
+ $this->allowedColumns = $columns;
+ return $this;
+ }
+
+ /**
+ * List and return all column names referenced in this filter
+ *
+ * @param array $columns The columns listed so far
+ *
+ * @return array
+ */
+ public function listFilteredColumns(array $columns = array())
+ {
+ foreach ($this->filters as $filter) {
+ if ($filter instanceof FilterExpression) {
+ $column= $filter->getColumn();
+ if (! in_array($column, $columns, true)) {
+ $columns[] = $column;
+ }
+ } else {
+ $columns = $filter->listFilteredColumns($columns);
+ }
+ }
+
+ return $columns;
+ }
+
+ public function toQueryString()
+ {
+ $parts = array();
+ if (empty($this->filters)) {
+ return '';
+ }
+ foreach ($this->filters() as $filter) {
+ if (! $filter->isEmpty()) {
+ $parts[] = $filter->toQueryString();
+ }
+ }
+
+ // TODO: getLevel??
+ if (strpos($this->getId(), '-')) {
+ return '(' . implode($this->getOperatorSymbol(), $parts) . ')';
+ } else {
+ return implode($this->getOperatorSymbol(), $parts);
+ }
+ }
+
+ /**
+ * Get simple string representation
+ *
+ * Useful for debugging only
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ if (empty($this->filters)) {
+ return '';
+ }
+ $parts = array();
+ foreach ($this->filters as $filter) {
+ if ($filter instanceof FilterChain) {
+ $parts[] = '(' . $filter . ')';
+ } else {
+ $parts[] = (string) $filter;
+ }
+ }
+ $op = ' ' . $this->getOperatorSymbol() . ' ';
+ return implode($op, $parts);
+ }
+
+ public function __construct($filters = array())
+ {
+ foreach ($filters as $filter) {
+ $this->addFilter($filter);
+ }
+ }
+
+ public function isExpression()
+ {
+ return false;
+ }
+
+ public function isChain()
+ {
+ return true;
+ }
+
+ public function isEmpty()
+ {
+ return empty($this->filters);
+ }
+
+ public function addFilter(Filter $filter)
+ {
+ if (! empty($this->allowedColumns)) {
+ $this->validateFilterColumns($filter);
+ }
+
+ $this->filters[] = $filter;
+ $filter->setId($this->getId() . '-' . $this->count());
+ return $this;
+ }
+
+ protected function validateFilterColumns(Filter $filter)
+ {
+ if ($filter->isExpression()) {
+ $valid = false;
+ foreach ($this->allowedColumns as $column) {
+ if (is_callable($column)) {
+ if (call_user_func($column, $filter->getColumn())) {
+ $valid = true;
+ break;
+ }
+ } elseif ($filter->getColumn() === $column) {
+ $valid = true;
+ break;
+ }
+ }
+
+ if (! $valid) {
+ throw new QueryException('Invalid filter column provided: %s', $filter->getColumn());
+ }
+ } else {
+ foreach ($filter->filters() as $subFilter) {
+ $this->validateFilterColumns($subFilter);
+ }
+ }
+ }
+
+ public function &filters()
+ {
+ return $this->filters;
+ }
+
+ public function count()
+ {
+ return count($this->filters);
+ }
+
+ public function __clone()
+ {
+ foreach ($this->filters as & $filter) {
+ $filter = clone $filter;
+ }
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterEqual.php b/library/Icinga/Data/Filter/FilterEqual.php
new file mode 100644
index 0000000..da53d3f
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterEqual.php
@@ -0,0 +1,16 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+class FilterEqual extends FilterExpression
+{
+ public function matches($row)
+ {
+ if (! isset($row->{$this->column})) {
+ return false;
+ }
+
+ return (string) $row->{$this->column} === (string) $this->expression;
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php b/library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php
new file mode 100644
index 0000000..d7bd5b8
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php
@@ -0,0 +1,16 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+class FilterEqualOrGreaterThan extends FilterExpression
+{
+ public function matches($row)
+ {
+ if (! isset($row->{$this->column})) {
+ return false;
+ }
+
+ return (string) $row->{$this->column} >= (string) $this->expression;
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterEqualOrLessThan.php b/library/Icinga/Data/Filter/FilterEqualOrLessThan.php
new file mode 100644
index 0000000..8016fc4
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterEqualOrLessThan.php
@@ -0,0 +1,26 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+class FilterEqualOrLessThan extends FilterExpression
+{
+ public function __toString()
+ {
+ return $this->column . ' <= ' . $this->expression;
+ }
+
+ public function toQueryString()
+ {
+ return $this->column . '<=' . $this->expression;
+ }
+
+ public function matches($row)
+ {
+ if (! isset($row->{$this->column})) {
+ return false;
+ }
+
+ return (string) $row->{$this->column} <= (string) $this->expression;
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterException.php b/library/Icinga/Data/Filter/FilterException.php
new file mode 100644
index 0000000..842d7ab
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterException.php
@@ -0,0 +1,15 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+use Icinga\Exception\IcingaException;
+
+/**
+ * Filter Exception Class
+ *
+ * Filter Exceptions should be thrown on filter parse errors or similar
+ */
+class FilterException extends IcingaException
+{
+}
diff --git a/library/Icinga/Data/Filter/FilterExpression.php b/library/Icinga/Data/Filter/FilterExpression.php
new file mode 100644
index 0000000..73fb625
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterExpression.php
@@ -0,0 +1,224 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+use Exception;
+
+class FilterExpression extends Filter
+{
+ protected $column;
+ protected $sign;
+ protected $expression;
+
+ /**
+ * Does this filter compare case sensitive?
+ *
+ * @var bool
+ */
+ protected $caseSensitive;
+
+ public function __construct($column, $sign, $expression)
+ {
+ $column = trim($column);
+ $this->column = $column;
+ $this->sign = $sign;
+ $this->expression = $expression;
+ $this->caseSensitive = true;
+ }
+
+ public function isExpression()
+ {
+ return true;
+ }
+
+ public function isChain()
+ {
+ return false;
+ }
+
+ public function isEmpty()
+ {
+ return false;
+ }
+
+ public function getColumn()
+ {
+ return $this->column;
+ }
+
+ public function getSign()
+ {
+ return $this->sign;
+ }
+
+ public function setColumn($column)
+ {
+ $this->column = $column;
+ return $this;
+ }
+
+ public function getExpression()
+ {
+ return $this->expression;
+ }
+
+ /**
+ * Return whether this filter compares case sensitive
+ *
+ * @return bool
+ */
+ public function getCaseSensitive()
+ {
+ return $this->caseSensitive;
+ }
+
+ public function setExpression($expression)
+ {
+ $this->expression = $expression;
+ return $this;
+ }
+
+ public function setSign($sign)
+ {
+ if ($sign !== $this->sign) {
+ return Filter::expression($this->column, $sign, $this->expression);
+ }
+ return $this;
+ }
+
+ /**
+ * Set this filter's case sensitivity
+ *
+ * @param bool $caseSensitive
+ *
+ * @return $this
+ */
+ public function setCaseSensitive($caseSensitive = true)
+ {
+ $this->caseSensitive = $caseSensitive;
+ return $this;
+ }
+
+ public function listFilteredColumns()
+ {
+ return array($this->getColumn());
+ }
+
+ public function __toString()
+ {
+ if ($this->isBooleanTrue()) {
+ return $this->column;
+ }
+
+ $expression = is_array($this->expression) ?
+ '( ' . implode(' | ', $this->expression) . ' )' :
+ $this->expression;
+
+ return sprintf(
+ '%s %s %s',
+ $this->column,
+ $this->sign,
+ $expression
+ );
+ }
+
+ public function toQueryString()
+ {
+ if ($this->isBooleanTrue()) {
+ return $this->column;
+ }
+
+ $expression = is_array($this->expression) ?
+ '(' . implode('|', array_map('rawurlencode', $this->expression)) . ')' :
+ rawurlencode($this->expression);
+
+ return $this->column . $this->sign . $expression;
+ }
+
+ protected function isBooleanTrue()
+ {
+ return $this->sign === '=' && $this->expression === true;
+ }
+
+ /**
+ * If $var is a scalar, do the same as strtolower() would do.
+ * If $var is an array, map $this->strtolowerRecursive() to its elements.
+ * Otherwise, return $var unchanged.
+ *
+ * @param mixed $var
+ *
+ * @return mixed
+ */
+ protected function strtolowerRecursive($var)
+ {
+ if ($var === null) {
+ return '';
+ }
+ if (is_scalar($var)) {
+ return strtolower($var);
+ }
+ if (is_array($var)) {
+ return array_map(array($this, 'strtolowerRecursive'), $var);
+ }
+ return $var;
+ }
+
+ public function matches($row)
+ {
+ try {
+ $rowValue = $row->{$this->column};
+ } catch (Exception $e) {
+ // TODO: REALLY? Exception?
+ return false;
+ }
+
+ if ($this->caseSensitive) {
+ $expression = $this->expression;
+ } else {
+ $rowValue = $this->strtolowerRecursive($rowValue);
+ $expression = $this->strtolowerRecursive($this->expression);
+ }
+
+ if (is_array($expression)) {
+ return in_array($rowValue, $expression);
+ }
+
+ $expression = (string) $expression;
+ if (strpos($expression, '*') === false) {
+ if (is_array($rowValue)) {
+ return in_array($expression, $rowValue);
+ }
+
+ return (string) $rowValue === $expression;
+ }
+
+ $parts = array();
+ foreach (preg_split('~\*~', $expression) as $part) {
+ $parts[] = preg_quote($part, '/');
+ }
+ $pattern = '/^' . implode('.*', $parts) . '$/';
+
+ if (is_array($rowValue)) {
+ foreach ($rowValue as $candidate) {
+ if (preg_match($pattern, $candidate)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ return $rowValue !== null && preg_match($pattern, $rowValue);
+ }
+
+ public function andFilter(Filter $filter)
+ {
+ return Filter::matchAll($this, $filter);
+ }
+
+ public function orFilter(Filter $filter)
+ {
+ return Filter::matchAny($this, $filter);
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterGreaterThan.php b/library/Icinga/Data/Filter/FilterGreaterThan.php
new file mode 100644
index 0000000..92a0e62
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterGreaterThan.php
@@ -0,0 +1,16 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+class FilterGreaterThan extends FilterExpression
+{
+ public function matches($row)
+ {
+ if (! isset($row->{$this->column})) {
+ // TODO: REALLY? Exception?
+ return false;
+ }
+ return (string) $row->{$this->column} > (string) $this->expression;
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterLessThan.php b/library/Icinga/Data/Filter/FilterLessThan.php
new file mode 100644
index 0000000..c13a1ce
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterLessThan.php
@@ -0,0 +1,26 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+class FilterLessThan extends FilterExpression
+{
+ public function __toString()
+ {
+ return $this->column . ' < ' . $this->expression;
+ }
+
+ public function toQueryString()
+ {
+ return $this->column . '<' . $this->expression;
+ }
+
+ public function matches($row)
+ {
+ if (! isset($row->{$this->column})) {
+ return false;
+ }
+
+ return (string) $row->{$this->column} < (string) $this->expression;
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterMatch.php b/library/Icinga/Data/Filter/FilterMatch.php
new file mode 100644
index 0000000..a3befad
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterMatch.php
@@ -0,0 +1,8 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+class FilterMatch extends FilterExpression
+{
+}
diff --git a/library/Icinga/Data/Filter/FilterMatchCaseInsensitive.php b/library/Icinga/Data/Filter/FilterMatchCaseInsensitive.php
new file mode 100644
index 0000000..9eca173
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterMatchCaseInsensitive.php
@@ -0,0 +1,13 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+class FilterMatchCaseInsensitive extends FilterMatch
+{
+ public function __construct($column, $sign, $expression)
+ {
+ parent::__construct($column, $sign, $expression);
+ $this->caseSensitive = false;
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterMatchNot.php b/library/Icinga/Data/Filter/FilterMatchNot.php
new file mode 100644
index 0000000..1e5050e
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterMatchNot.php
@@ -0,0 +1,12 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+class FilterMatchNot extends FilterExpression
+{
+ public function matches($row)
+ {
+ return !parent::matches($row);
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterMatchNotCaseInsensitive.php b/library/Icinga/Data/Filter/FilterMatchNotCaseInsensitive.php
new file mode 100644
index 0000000..3838fa2
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterMatchNotCaseInsensitive.php
@@ -0,0 +1,13 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+class FilterMatchNotCaseInsensitive extends FilterMatchNot
+{
+ public function __construct($column, $sign, $expression)
+ {
+ parent::__construct($column, $sign, $expression);
+ $this->caseSensitive = false;
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterNot.php b/library/Icinga/Data/Filter/FilterNot.php
new file mode 100644
index 0000000..b61f497
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterNot.php
@@ -0,0 +1,58 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+class FilterNot extends FilterChain
+{
+ protected $operatorName = 'NOT';
+
+ protected $operatorSymbol = '!'; // BULLSHIT
+
+// TODO: Max count 1 or autocreate sub-and?
+
+ public function matches($row)
+ {
+ foreach ($this->filters() as $filter) {
+ if ($filter->matches($row)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public function andFilter(Filter $filter)
+ {
+ return Filter::matchAll($this, $filter);
+ }
+
+ public function orFilter(Filter $filter)
+ {
+ return Filter::matchAny($filter);
+ }
+
+ public function toQueryString()
+ {
+ $parts = array();
+ if (empty($this->filters)) {
+ return '';
+ }
+
+ foreach ($this->filters() as $filter) {
+ $parts[] = $filter->toQueryString();
+ }
+ if (count($parts) === 1) {
+ return '!' . $parts[0];
+ } else {
+ return '!(' . implode('&', $parts) . ')';
+ }
+ }
+
+ public function __toString()
+ {
+ if (count($this->filters) === 1) {
+ return '! ' . $this->filters[0];
+ }
+ return '! (' . implode('&', $this->filters) . ')';
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterNotEqual.php b/library/Icinga/Data/Filter/FilterNotEqual.php
new file mode 100644
index 0000000..8915a3d
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterNotEqual.php
@@ -0,0 +1,12 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+class FilterNotEqual extends FilterExpression
+{
+ public function matches($row)
+ {
+ return (string) $row->{$this->column} !== (string) $this->expression;
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterOr.php b/library/Icinga/Data/Filter/FilterOr.php
new file mode 100644
index 0000000..aca91f3
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterOr.php
@@ -0,0 +1,39 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+class FilterOr extends FilterChain
+{
+ protected $operatorName = 'OR';
+
+ protected $operatorSymbol = '|';
+
+ public function matches($row)
+ {
+ foreach ($this->filters as $filter) {
+ if ($filter->matches($row)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public function setOperatorName($name)
+ {
+ if ($this->count() > 1 && $name === 'NOT') {
+ return Filter::not(clone $this);
+ }
+ return parent::setOperatorName($name);
+ }
+
+ public function andFilter(Filter $filter)
+ {
+ return Filter::matchAll($this, $filter);
+ }
+
+ public function orFilter(Filter $filter)
+ {
+ return $this->addFilter($filter);
+ }
+}
diff --git a/library/Icinga/Data/Filter/FilterParseException.php b/library/Icinga/Data/Filter/FilterParseException.php
new file mode 100644
index 0000000..f2b732b
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterParseException.php
@@ -0,0 +1,10 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+use Icinga\Exception\IcingaException;
+
+class FilterParseException extends IcingaException
+{
+}
diff --git a/library/Icinga/Data/Filter/FilterQueryString.php b/library/Icinga/Data/Filter/FilterQueryString.php
new file mode 100644
index 0000000..8535df5
--- /dev/null
+++ b/library/Icinga/Data/Filter/FilterQueryString.php
@@ -0,0 +1,320 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Filter;
+
+class FilterQueryString
+{
+ protected $string;
+
+ protected $pos;
+
+ protected $debug = array();
+
+ protected $reportDebug = false;
+
+ protected $length;
+
+ protected function __construct()
+ {
+ }
+
+ protected function debug($msg, $level = 0, $op = null)
+ {
+ if ($op === null) {
+ $op = 'NULL';
+ }
+ $this->debug[] = sprintf(
+ '%s[%d=%s] (%s): %s',
+ str_repeat('* ', $level),
+ $this->pos,
+ $this->string[$this->pos - 1],
+ $op,
+ $msg
+ );
+ }
+
+ public static function parse($string)
+ {
+ $parser = new static();
+ return $parser->parseQueryString($string);
+ }
+
+ protected function readNextKey()
+ {
+ $str = $this->readUnlessSpecialChar();
+
+ if ($str === false) {
+ return $str;
+ }
+ return rawurldecode($str);
+ }
+
+ protected function readNextValue()
+ {
+ if ($this->nextChar() === '(') {
+ $this->readChar();
+ $var = preg_split('~\|~', $this->readUnless(')'));
+ if ($this->readChar() !== ')') {
+ $this->parseError(null, 'Expected ")"');
+ }
+ } else {
+ $var = rawurldecode($this->readUnless(array(')', '&', '|', '>', '<')));
+ }
+ return $var;
+ }
+
+ protected function readNextExpression()
+ {
+ if ('' === ($key = $this->readNextKey())) {
+ return false;
+ }
+
+ foreach (array('<', '>') as $sign) {
+ if (false !== ($pos = strpos($key, $sign))) {
+ if ($this->nextChar() === '=') {
+ break;
+ }
+ $var = substr($key, $pos + 1);
+ $key = substr($key, 0, $pos);
+
+ if (ctype_digit($var)) {
+ $var = (float) $var;
+ }
+
+ return Filter::expression($key, $sign, $var);
+ }
+ }
+ if (in_array($this->nextChar(), array('=', '>', '<', '!'))) {
+ $sign = $this->readChar();
+ } else {
+ $sign = false;
+ }
+ if ($sign === false) {
+ return Filter::expression($key, '=', true);
+ }
+
+ $toFloat = false;
+ if ($sign === '=') {
+ $last = substr($key, -1);
+ if ($last === '>' || $last === '<') {
+ $sign = $last . $sign;
+ $key = substr($key, 0, -1);
+ $toFloat = true;
+ }
+ // TODO: Same as above for unescaped <> - do we really need this?
+ } elseif ($sign === '>' || $sign === '<' || $sign === '!') {
+ $toFloat = $sign === '>' || $sign === '<';
+ if ($this->nextChar() === '=') {
+ $sign .= $this->readChar();
+ }
+ }
+
+ $var = $this->readNextValue();
+ if ($toFloat && ctype_digit($var)) {
+ $var = (float) $var;
+ }
+
+ return Filter::expression($key, $sign, $var);
+ }
+
+ protected function parseError($char = null, $extraMsg = null)
+ {
+ if ($extraMsg === null) {
+ $extra = '';
+ } else {
+ $extra = ': ' . $extraMsg;
+ }
+ if ($char === null) {
+ $char = $this->string[$this->pos];
+ }
+ if ($this->reportDebug) {
+ $extra .= "\n" . implode("\n", $this->debug);
+ }
+
+ throw new FilterParseException(
+ 'Invalid filter "%s", unexpected %s at pos %d%s',
+ $this->string,
+ $char,
+ $this->pos,
+ $extra
+ );
+ }
+
+ protected function readFilters($nestingLevel = 0, $op = null)
+ {
+ $filters = array();
+ while ($this->pos < $this->length) {
+ if ($op === '!' && count($filters) === 1) {
+ break;
+ }
+ $filter = $this->readNextExpression();
+ $next = $this->readChar();
+
+
+ if ($filter === false) {
+ $this->debug('Got no next expression, next is ' . $next, $nestingLevel, $op);
+ if ($next === '!') {
+ $not = $this->readFilters($nestingLevel + 1, '!');
+ $filters[] = $not;
+ if (in_array($this->nextChar(), array('|', '&', ')'))) {
+ $next = $this->readChar();
+ $this->debug('Got NOT, next is now: ' . $next, $nestingLevel, $op);
+ } else {
+ $this->debug('Breaking after NOT: ' . $not, $nestingLevel, $op);
+ break;
+ }
+ }
+
+ if ($op === null && count($filters) > 0 && ($next === '&' || $next === '|')) {
+ $op = $next;
+ continue;
+ }
+
+ if ($next === false) {
+ // Nothing more to read
+ break;
+ }
+
+ if ($next === ')') {
+ if ($nestingLevel > 0) {
+ $this->debug('Closing without filter: ' . $next, $nestingLevel, $op);
+ break;
+ }
+ $this->parseError($next);
+ }
+ if ($next === '(') {
+ $filters[] = $this->readFilters($nestingLevel + 1, null);
+ continue;
+ }
+ if ($next === $op) {
+ continue;
+ }
+ $this->parseError($next, "$op level $nestingLevel");
+ } else {
+ $this->debug('Got new expression: ' . $filter, $nestingLevel, $op);
+
+ $filters[] = $filter;
+
+ if ($next === false) {
+ $this->debug('Next is false, nothing to read but got filter', $nestingLevel, $op);
+ // Got filter, nothing more to read
+ break;
+ }
+
+ if ($op === '!') {
+ $this->pos--;
+ break;
+ }
+ if ($next === $op) {
+ $this->debug('Next matches operator', $nestingLevel, $op);
+ continue; // Break??
+ }
+
+ if ($next === ')') {
+ if ($nestingLevel > 0) {
+ $this->debug('Closing with filter: ' . $next, $nestingLevel, $op);
+ break;
+ }
+ $this->parseError($next);
+ }
+ if ($op === null && in_array($next, array('&', '|'))) {
+ $this->debug('Setting op to ' . $next, $nestingLevel, $op);
+ $op = $next;
+ continue;
+ }
+ $this->parseError($next);
+ }
+ }
+
+ if ($nestingLevel === 0 && $this->pos < $this->length) {
+ $this->parseError($op, 'Did not read full filter');
+ }
+
+ if ($nestingLevel === 0 && count($filters) === 1 && $op !== '!') {
+ // There is only one filter expression, no chain
+ $this->debug('Returning first filter only: ' . $filters[0], $nestingLevel, $op);
+ return $filters[0];
+ }
+
+ if ($op === null && count($filters) === 1) {
+ $this->debug('No op, single filter, setting AND', $nestingLevel, $op);
+ $op = '&';
+ }
+ $this->debug(sprintf('Got %d filters, returning', count($filters)), $nestingLevel, $op);
+
+ switch ($op) {
+ case '&':
+ return Filter::matchAll($filters);
+ case '|':
+ return Filter::matchAny($filters);
+ case '!':
+ return Filter::not($filters);
+ case null:
+ return Filter::matchAll();
+ default:
+ $this->parseError($op);
+ }
+ }
+
+ protected function parseQueryString($string)
+ {
+ $this->pos = 0;
+
+ $this->string = $string;
+
+ $this->length = $string ? strlen($string) : 0;
+
+ if ($this->length === 0) {
+ return Filter::matchAll();
+ }
+ return $this->readFilters();
+ }
+
+ protected function readUnless($char)
+ {
+ $buffer = '';
+ while (false !== ($c = $this->readChar())) {
+ if (is_array($char)) {
+ if (in_array($c, $char)) {
+ $this->pos--;
+ break;
+ }
+ } else {
+ if ($c === $char) {
+ $this->pos--;
+ break;
+ }
+ }
+ $buffer .= $c;
+ }
+
+ return $buffer;
+ }
+
+ protected function readUnlessSpecialChar()
+ {
+ return $this->readUnless(array('=', '(', ')', '&', '|', '>', '<', '!'));
+ }
+
+ protected function readExpressionOperator()
+ {
+ return $this->readUnless(array('=', '>', '<', '!'));
+ }
+
+ protected function readChar()
+ {
+ if ($this->length > $this->pos) {
+ return $this->string[$this->pos++];
+ }
+ return false;
+ }
+
+ protected function nextChar()
+ {
+ if ($this->length > $this->pos) {
+ return $this->string[$this->pos];
+ }
+ return false;
+ }
+}
diff --git a/library/Icinga/Data/FilterColumns.php b/library/Icinga/Data/FilterColumns.php
new file mode 100644
index 0000000..7eaacea
--- /dev/null
+++ b/library/Icinga/Data/FilterColumns.php
@@ -0,0 +1,21 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+interface FilterColumns
+{
+ /**
+ * Return a filterable's filter columns with their optional label as key
+ *
+ * @return array
+ */
+ public function getFilterColumns();
+
+ /**
+ * Return a filterable's search columns
+ *
+ * @return array
+ */
+ public function getSearchColumns();
+}
diff --git a/library/Icinga/Data/Filterable.php b/library/Icinga/Data/Filterable.php
new file mode 100644
index 0000000..ceca22f
--- /dev/null
+++ b/library/Icinga/Data/Filterable.php
@@ -0,0 +1,27 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Interface for filtering a result set
+ *
+ * @deprecated(EL): addFilter and applyFilter do the same in all usages.
+ * addFilter could be replaced w/ getFilter()->add(). We must no require classes implementing this interface to
+ * implement redundant methods over and over again. This interface must be moved to the namespace Icinga\Data\Filter.
+ * It lacks documentation.
+ */
+interface Filterable
+{
+ public function applyFilter(Filter $filter);
+
+ public function setFilter(Filter $filter);
+
+ public function getFilter();
+
+ public function addFilter(Filter $filter);
+
+ public function where($condition, $value = null);
+}
diff --git a/library/Icinga/Data/Identifiable.php b/library/Icinga/Data/Identifiable.php
new file mode 100644
index 0000000..7435026
--- /dev/null
+++ b/library/Icinga/Data/Identifiable.php
@@ -0,0 +1,17 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+/**
+ * Interface for objects that are identifiable by an ID of any type
+ */
+interface Identifiable
+{
+ /**
+ * Get the ID associated with this Identifiable object
+ *
+ * @return mixed
+ */
+ public function getId();
+}
diff --git a/library/Icinga/Data/Inspectable.php b/library/Icinga/Data/Inspectable.php
new file mode 100644
index 0000000..d40ce57
--- /dev/null
+++ b/library/Icinga/Data/Inspectable.php
@@ -0,0 +1,20 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+/**
+ * An object for which the user can retrieve status information
+ *
+ * This interface is useful for providing summaries or diagnostic information about objects
+ * to users.
+ */
+interface Inspectable
+{
+ /**
+ * Inspect this object to gain extended information about its health
+ *
+ * @return Inspection The inspection result
+ */
+ public function inspect();
+}
diff --git a/library/Icinga/Data/Inspection.php b/library/Icinga/Data/Inspection.php
new file mode 100644
index 0000000..b0dd298
--- /dev/null
+++ b/library/Icinga/Data/Inspection.php
@@ -0,0 +1,129 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+use Icinga\Application\Logger;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Contains information about an object in the form of human-readable log entries and indicates if the object has errors
+ */
+class Inspection
+{
+ /**
+ * @var array
+ */
+ protected $log = array();
+
+ /**
+ * @var string
+ */
+ protected $description;
+
+ /**
+ * @var string|Inspection
+ */
+ protected $error;
+
+ /**
+ * @param $description Describes the object that is being inspected
+ */
+ public function __construct($description)
+ {
+ $this->description = $description;
+ }
+
+ /**
+ * Get the name of this Inspection
+ *
+ * @return mixed
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * Append the given log entry or nested inspection
+ *
+ * @throws ProgrammingError When called after erroring
+ *
+ * @param $entry string|Inspection A log entry or nested inspection
+ */
+ public function write($entry)
+ {
+ if (isset($this->error)) {
+ throw new ProgrammingError('Inspection object used after error');
+ }
+ if ($entry instanceof Inspection) {
+ $this->log[$entry->description] = $entry->toArray();
+ } else {
+ Logger::debug($entry);
+ $this->log[] = $entry;
+ }
+ }
+
+ /**
+ * Append the given log entry and fail this inspection with the given error
+ *
+ * @param $entry string|Inspection A log entry or nested inspection
+ *
+ * @throws ProgrammingError When called multiple times
+ *
+ * @return $this fluent interface
+ */
+ public function error($entry)
+ {
+ if (isset($this->error)) {
+ throw new ProgrammingError('Inspection object used after error');
+ }
+ Logger::error($entry);
+ $this->log[] = $entry;
+ $this->error = $entry;
+ return $this;
+ }
+
+ /**
+ * If the inspection resulted in an error
+ *
+ * @return bool
+ */
+ public function hasError()
+ {
+ return isset($this->error);
+ }
+
+ /**
+ * The error that caused the inspection to fail
+ *
+ * @return Inspection|string
+ */
+ public function getError()
+ {
+ return $this->error;
+ }
+
+ /**
+ * Convert the inspection to an array
+ *
+ * @return array An array of strings that describe the state in a human-readable form, each array element
+ * represents one log entry about this object.
+ */
+ public function toArray()
+ {
+ return $this->log;
+ }
+
+ /**
+ * Return a text representation of the inspection log entries
+ */
+ public function __toString()
+ {
+ return sprintf(
+ 'Inspection: description: "%s" error: "%s"',
+ $this->description,
+ $this->error
+ );
+ }
+}
diff --git a/library/Icinga/Data/Limitable.php b/library/Icinga/Data/Limitable.php
new file mode 100644
index 0000000..8591a79
--- /dev/null
+++ b/library/Icinga/Data/Limitable.php
@@ -0,0 +1,48 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+/**
+ * Interface for retrieving just a portion of a result set
+ */
+interface Limitable
+{
+ /**
+ * Set a limit count and offset
+ *
+ * @param int $count Number of rows to return
+ * @param int $offset Start returning after this many rows
+ *
+ * @return self
+ */
+ public function limit($count = null, $offset = null);
+
+ /**
+ * Whether a limit is set
+ *
+ * @return bool
+ */
+ public function hasLimit();
+
+ /**
+ * Get the limit if any
+ *
+ * @return int|null
+ */
+ public function getLimit();
+
+ /**
+ * Whether an offset is set
+ *
+ * @return bool
+ */
+ public function hasOffset();
+
+ /**
+ * Get the offset if any
+ *
+ * @return int|null
+ */
+ public function getOffset();
+}
diff --git a/library/Icinga/Data/Paginatable.php b/library/Icinga/Data/Paginatable.php
new file mode 100644
index 0000000..468cca2
--- /dev/null
+++ b/library/Icinga/Data/Paginatable.php
@@ -0,0 +1,10 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+use Countable;
+
+interface Paginatable extends Limitable, Countable
+{
+}
diff --git a/library/Icinga/Data/PivotTable.php b/library/Icinga/Data/PivotTable.php
new file mode 100644
index 0000000..6c7f806
--- /dev/null
+++ b/library/Icinga/Data/PivotTable.php
@@ -0,0 +1,396 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Application\Icinga;
+use Icinga\Web\Paginator\Adapter\QueryAdapter;
+use Zend_Paginator;
+
+class PivotTable implements Sortable
+{
+ /**
+ * The query to fetch as pivot table
+ *
+ * @var SimpleQuery
+ */
+ protected $baseQuery;
+
+ /**
+ * X-axis pivot column
+ *
+ * @var string
+ */
+ protected $xAxisColumn;
+
+ /**
+ * Y-axis pivot column
+ *
+ * @var string
+ */
+ protected $yAxisColumn;
+
+ /**
+ * Column for sorting the result set
+ *
+ * @var array
+ */
+ protected $order = array();
+
+ /**
+ * The filter being applied on the query for the x-axis
+ *
+ * @var Filter
+ */
+ protected $xAxisFilter;
+
+ /**
+ * The filter being applied on the query for the y-axis
+ *
+ * @var Filter
+ */
+ protected $yAxisFilter;
+
+ /**
+ * The query to fetch the leading x-axis rows and their headers
+ *
+ * @var SimpleQuery
+ */
+ protected $xAxisQuery;
+
+ /**
+ * The query to fetch the leading y-axis rows and their headers
+ *
+ * @var SimpleQuery
+ */
+ protected $yAxisQuery;
+
+ /**
+ * X-axis header column
+ *
+ * @var string|null
+ */
+ protected $xAxisHeader;
+
+ /**
+ * Y-axis header column
+ *
+ * @var string|null
+ */
+ protected $yAxisHeader;
+
+ /**
+ * Create a new pivot table
+ *
+ * @param SimpleQuery $query The query to fetch as pivot table
+ * @param string $xAxisColumn X-axis pivot column
+ * @param string $yAxisColumn Y-axis pivot column
+ */
+ public function __construct(SimpleQuery $query, $xAxisColumn, $yAxisColumn)
+ {
+ $this->baseQuery = $query;
+ $this->xAxisColumn = $xAxisColumn;
+ $this->yAxisColumn = $yAxisColumn;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOrder()
+ {
+ return $this->order;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasOrder()
+ {
+ return ! empty($this->order);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order($field, $direction = null)
+ {
+ $this->order[$field] = $direction;
+ return $this;
+ }
+
+ /**
+ * Set the filter to apply on the query for the x-axis
+ *
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function setXAxisFilter(Filter $filter = null)
+ {
+ $this->xAxisFilter = $filter;
+ return $this;
+ }
+
+ /**
+ * Set the filter to apply on the query for the y-axis
+ *
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function setYAxisFilter(Filter $filter = null)
+ {
+ $this->yAxisFilter = $filter;
+ return $this;
+ }
+
+ /**
+ * Get the x-axis header
+ *
+ * Defaults to {@link $xAxisColumn} in case no x-axis header has been set using {@link setXAxisHeader()}
+ *
+ * @return string
+ */
+ public function getXAxisHeader()
+ {
+ return $this->xAxisHeader !== null ? $this->xAxisHeader : $this->xAxisColumn;
+ }
+
+ /**
+ * Set the x-axis header
+ *
+ * @param string $xAxisHeader
+ *
+ * @return $this
+ */
+ public function setXAxisHeader($xAxisHeader)
+ {
+ $this->xAxisHeader = (string) $xAxisHeader;
+ return $this;
+ }
+
+ /**
+ * Get the y-axis header
+ *
+ * Defaults to {@link $yAxisColumn} in case no x-axis header has been set using {@link setYAxisHeader()}
+ *
+ * @return string
+ */
+ public function getYAxisHeader()
+ {
+ return $this->yAxisHeader !== null ? $this->yAxisHeader : $this->yAxisColumn;
+ }
+
+ /**
+ * Set the y-axis header
+ *
+ * @param string $yAxisHeader
+ *
+ * @return $this
+ */
+ public function setYAxisHeader($yAxisHeader)
+ {
+ $this->yAxisHeader = (string) $yAxisHeader;
+ return $this;
+ }
+
+ /**
+ * Return the value for the given request parameter
+ *
+ * @param string $axis The axis for which to return the parameter ('x' or 'y')
+ * @param string $param The parameter name to return
+ * @param int $default The default value to return
+ *
+ * @return int
+ */
+ protected function getPaginationParameter($axis, $param, $default = null)
+ {
+ $request = Icinga::app()->getRequest();
+
+ $value = $request->getParam($param, '');
+ if (strpos($value, ',') > 0) {
+ $parts = explode(',', $value, 2);
+ return intval($parts[$axis === 'x' ? 0 : 1]);
+ }
+
+ return $default !== null ? $default : 0;
+ }
+
+ /**
+ * Query horizontal (x) axis
+ *
+ * @return SimpleQuery
+ */
+ protected function queryXAxis()
+ {
+ if ($this->xAxisQuery === null) {
+ $this->xAxisQuery = clone $this->baseQuery;
+ $this->xAxisQuery->clearGroupingRules();
+ $xAxisHeader = $this->getXAxisHeader();
+ $columns = array($this->xAxisColumn, $xAxisHeader);
+ $this->xAxisQuery->group(array_unique($columns)); // xAxisColumn and header may be the same column
+ $this->xAxisQuery->columns($columns);
+
+ if ($this->xAxisFilter !== null) {
+ $this->xAxisQuery->addFilter($this->xAxisFilter);
+ }
+
+ $this->xAxisQuery->order(
+ $xAxisHeader,
+ isset($this->order[$xAxisHeader]) ? $this->order[$xAxisHeader] : self::SORT_ASC
+ );
+ }
+
+ return $this->xAxisQuery;
+ }
+
+ /**
+ * Query vertical (y) axis
+ *
+ * @return SimpleQuery
+ */
+ protected function queryYAxis()
+ {
+ if ($this->yAxisQuery === null) {
+ $this->yAxisQuery = clone $this->baseQuery;
+ $this->yAxisQuery->clearGroupingRules();
+ $yAxisHeader = $this->getYAxisHeader();
+ $columns = array($this->yAxisColumn, $yAxisHeader);
+ $this->yAxisQuery->group(array_unique($columns)); // yAxisColumn and header may be the same column
+ $this->yAxisQuery->columns($columns);
+
+ if ($this->yAxisFilter !== null) {
+ $this->yAxisQuery->addFilter($this->yAxisFilter);
+ }
+
+ $this->yAxisQuery->order(
+ $yAxisHeader,
+ isset($this->order[$yAxisHeader]) ? $this->order[$yAxisHeader] : self::SORT_ASC
+ );
+ }
+ return $this->yAxisQuery;
+ }
+
+ /**
+ * Return a pagination adapter for the x-axis query
+ *
+ * $limit and $page are taken from the current request if not given.
+ *
+ * @param int $limit The maximum amount of entries to fetch
+ * @param int $page The page to set as current one
+ *
+ * @return Zend_Paginator
+ */
+ public function paginateXAxis($limit = null, $page = null)
+ {
+ if ($limit === null || $page === null) {
+ if ($limit === null) {
+ $limit = $this->getPaginationParameter('x', 'limit', 20);
+ }
+
+ if ($page === null) {
+ $page = $this->getPaginationParameter('x', 'page', 1);
+ }
+ }
+
+ $query = $this->queryXAxis();
+ $query->limit($limit, $page > 0 ? ($page - 1) * $limit : 0);
+
+ $paginator = new Zend_Paginator(new QueryAdapter($query));
+ $paginator->setItemCountPerPage($limit);
+ $paginator->setCurrentPageNumber($page);
+ return $paginator;
+ }
+
+ /**
+ * Return a pagination adapter for the y-axis query
+ *
+ * $limit and $page are taken from the current request if not given.
+ *
+ * @param int $limit The maximum amount of entries to fetch
+ * @param int $page The page to set as current one
+ *
+ * @return Zend_Paginator
+ */
+ public function paginateYAxis($limit = null, $page = null)
+ {
+ if ($limit === null || $page === null) {
+ if ($limit === null) {
+ $limit = $this->getPaginationParameter('y', 'limit', 20);
+ }
+
+ if ($page === null) {
+ $page = $this->getPaginationParameter('y', 'page', 1);
+ }
+ }
+
+ $query = $this->queryYAxis();
+ $query->limit($limit, $page > 0 ? ($page - 1) * $limit : 0);
+
+ $paginator = new Zend_Paginator(new QueryAdapter($query));
+ $paginator->setItemCountPerPage($limit);
+ $paginator->setCurrentPageNumber($page);
+ return $paginator;
+ }
+
+ /**
+ * Return the pivot table as an array of pivot data and pivot header
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ if (($this->xAxisFilter === null && $this->yAxisFilter === null)
+ || ($this->xAxisFilter !== null && $this->yAxisFilter !== null)
+ ) {
+ $xAxis = $this->queryXAxis()->fetchPairs();
+ $yAxis = $this->queryYAxis()->fetchPairs();
+ $xAxisKeys = array_keys($xAxis);
+ $yAxisKeys = array_keys($yAxis);
+ } else {
+ if ($this->xAxisFilter !== null) {
+ $xAxis = $this->queryXAxis()->fetchPairs();
+ $xAxisKeys = array_keys($xAxis);
+ $yAxis = $this->queryYAxis()->where($this->xAxisColumn, $xAxisKeys)->fetchPairs();
+ $yAxisKeys = array_keys($yAxis);
+ } else { // $this->yAxisFilter !== null
+ $yAxis = $this->queryYAxis()->fetchPairs();
+ $yAxisKeys = array_keys($yAxis);
+ $xAxis = $this->queryXAxis()->where($this->yAxisColumn, $yAxisKeys)->fetchPairs();
+ $xAxisKeys = array_keys($xAxis);
+ }
+ }
+ $pivotData = array();
+ $pivotHeader = array(
+ 'cols' => $xAxis,
+ 'rows' => $yAxis
+ );
+ if (! empty($xAxis) && ! empty($yAxis)) {
+ $this->baseQuery
+ ->where($this->xAxisColumn, array_map(
+ function ($key) {
+ return (string) $key;
+ },
+ $xAxisKeys
+ ))
+ ->where($this->yAxisColumn, array_map(
+ function ($key) {
+ return (string) $key;
+ },
+ $yAxisKeys
+ ));
+
+ foreach ($yAxisKeys as $yAxisKey) {
+ foreach ($xAxisKeys as $xAxisKey) {
+ $pivotData[$yAxisKey][$xAxisKey] = null;
+ }
+ }
+
+ foreach ($this->baseQuery as $row) {
+ $pivotData[$row->{$this->yAxisColumn}][$row->{$this->xAxisColumn}] = $row;
+ }
+ }
+ return array($pivotData, $pivotHeader);
+ }
+}
diff --git a/library/Icinga/Data/QueryInterface.php b/library/Icinga/Data/QueryInterface.php
new file mode 100644
index 0000000..e723857
--- /dev/null
+++ b/library/Icinga/Data/QueryInterface.php
@@ -0,0 +1,8 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+interface QueryInterface extends Fetchable, Filterable, Paginatable, Sortable
+{
+}
diff --git a/library/Icinga/Data/Queryable.php b/library/Icinga/Data/Queryable.php
new file mode 100644
index 0000000..75cdc98
--- /dev/null
+++ b/library/Icinga/Data/Queryable.php
@@ -0,0 +1,20 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+/**
+ * Interface for specifying data sources
+ */
+interface Queryable
+{
+ /**
+ * Set the target and fields to query
+ *
+ * @param string $target
+ * @param array $fields
+ *
+ * @return Fetchable
+ */
+ public function from($target, array $fields = null);
+}
diff --git a/library/Icinga/Data/Reducible.php b/library/Icinga/Data/Reducible.php
new file mode 100644
index 0000000..6ece17e
--- /dev/null
+++ b/library/Icinga/Data/Reducible.php
@@ -0,0 +1,23 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\StatementException;
+
+/**
+ * Interface for data deletion
+ */
+interface Reducible
+{
+ /**
+ * Delete entries in the given target, optionally limiting the affected entries by using a filter
+ *
+ * @param string $target
+ * @param Filter $filter
+ *
+ * @throws StatementException
+ */
+ public function delete($target, Filter $filter = null);
+}
diff --git a/library/Icinga/Data/ResourceFactory.php b/library/Icinga/Data/ResourceFactory.php
new file mode 100644
index 0000000..5b477c7
--- /dev/null
+++ b/library/Icinga/Data/ResourceFactory.php
@@ -0,0 +1,138 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+use Icinga\Application\Config;
+use Icinga\Util\ConfigAwareFactory;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Data\Db\DbConnection;
+use Icinga\Protocol\Ldap\LdapConnection;
+use Icinga\Protocol\File\FileReader;
+
+/**
+ * Create resources from names or resource configuration
+ */
+class ResourceFactory implements ConfigAwareFactory
+{
+ /**
+ * Resource configuration
+ *
+ * @var Config
+ */
+ private static $resources;
+
+ /**
+ * Set resource configurations
+ *
+ * @param Config $config
+ */
+ public static function setConfig($config)
+ {
+ self::$resources = $config;
+ }
+
+ /**
+ * Get the configuration for a specific resource
+ *
+ * @param $resourceName String The resource's name
+ *
+ * @return ConfigObject The configuration of the resource
+ *
+ * @throws ConfigurationError
+ */
+ public static function getResourceConfig($resourceName)
+ {
+ self::assertResourcesExist();
+ $resourceConfig = self::$resources->getSection($resourceName);
+ if ($resourceConfig->isEmpty()) {
+ throw new ConfigurationError(
+ 'Cannot load resource config "%s". Resource does not exist',
+ $resourceName
+ );
+ }
+ return $resourceConfig;
+ }
+
+ /**
+ * Get the configuration of all existing resources, or all resources of the given type
+ *
+ * @param string $type Filter for resource type
+ *
+ * @return Config The resources configuration
+ */
+ public static function getResourceConfigs($type = null)
+ {
+ self::assertResourcesExist();
+ if ($type === null) {
+ return self::$resources;
+ }
+ $resources = array();
+ foreach (self::$resources as $name => $resource) {
+ if ($resource->get('type') === $type) {
+ $resources[$name] = $resource;
+ }
+ }
+ return Config::fromArray($resources);
+ }
+
+ /**
+ * Check if the existing resources are set. If not, load them from resources.ini
+ *
+ * @throws ConfigurationError
+ */
+ private static function assertResourcesExist()
+ {
+ if (self::$resources === null) {
+ self::$resources = Config::app('resources');
+ }
+ }
+
+ /**
+ * Create and return a resource based on the given configuration
+ *
+ * @param ConfigObject $config The configuration of the resource to create
+ *
+ * @return Selectable The resource
+ * @throws ConfigurationError In case of an unsupported type or invalid configuration
+ */
+ public static function createResource(ConfigObject $config)
+ {
+ switch (strtolower($config->type)) {
+ case 'db':
+ $resource = new DbConnection($config);
+ break;
+ case 'ldap':
+ if (empty($config->root_dn)) {
+ throw new ConfigurationError('LDAP root DN missing');
+ }
+
+ $resource = new LdapConnection($config);
+ break;
+ case 'file':
+ $resource = new FileReader($config);
+ break;
+ case 'ini':
+ $resource = Config::fromIni($config->ini);
+ break;
+ default:
+ throw new ConfigurationError(
+ 'Unsupported resource type "%s"',
+ $config->type
+ );
+ }
+
+ return $resource;
+ }
+
+ /**
+ * Create a resource from name
+ *
+ * @param string $resourceName
+ * @return DbConnection|LdapConnection
+ */
+ public static function create($resourceName)
+ {
+ return self::createResource(self::getResourceConfig($resourceName));
+ }
+}
diff --git a/library/Icinga/Data/Selectable.php b/library/Icinga/Data/Selectable.php
new file mode 100644
index 0000000..ace4e79
--- /dev/null
+++ b/library/Icinga/Data/Selectable.php
@@ -0,0 +1,17 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+/**
+ * Interface for classes providing a data source to fetch data from
+ */
+interface Selectable
+{
+ /**
+ * Provide a data source to fetch data from
+ *
+ * @return Queryable
+ */
+ public function select();
+}
diff --git a/library/Icinga/Data/SimpleQuery.php b/library/Icinga/Data/SimpleQuery.php
new file mode 100644
index 0000000..1ef0c27
--- /dev/null
+++ b/library/Icinga/Data/SimpleQuery.php
@@ -0,0 +1,650 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+use Iterator;
+use IteratorAggregate;
+use Icinga\Application\Benchmark;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\ProgrammingError;
+
+class SimpleQuery implements QueryInterface, Queryable, Iterator
+{
+ /**
+ * Query data source
+ *
+ * @var mixed
+ */
+ protected $ds;
+
+ /**
+ * This query's iterator
+ *
+ * @var Iterator
+ */
+ protected $iterator;
+
+ /**
+ * The current position of this query's iterator
+ *
+ * @var int
+ */
+ protected $iteratorPosition;
+
+ /**
+ * The amount of rows previously calculated
+ *
+ * @var int
+ */
+ protected $cachedCount;
+
+ /**
+ * The target you are going to query
+ *
+ * @var mixed
+ */
+ protected $target;
+
+ /**
+ * The columns you asked for
+ *
+ * All columns if null, no column if empty??? Alias handling goes here!
+ *
+ * @var array
+ */
+ protected $desiredColumns = array();
+
+ /**
+ * The columns you are interested in
+ *
+ * All columns if null, no column if empty??? Alias handling goes here!
+ *
+ * @var array
+ */
+ protected $columns = array();
+
+ /**
+ * The columns and their aliases flipped in order to handle aliased sort columns
+ *
+ * Supposed to be used and populated by $this->compare *only*.
+ *
+ * @var array
+ */
+ protected $flippedColumns;
+
+ /**
+ * The columns you're using to sort the query result
+ *
+ * @var array
+ */
+ protected $order = array();
+
+ /**
+ * Number of rows to return
+ *
+ * @var int
+ */
+ protected $limitCount;
+
+ /**
+ * Result starts with this row
+ *
+ * @var int
+ */
+ protected $limitOffset;
+
+ /**
+ * Whether to peek ahead for more results
+ *
+ * @var bool
+ */
+ protected $peekAhead;
+
+ /**
+ * Whether the query did not yield all available results
+ *
+ * @var bool
+ */
+ protected $hasMore;
+
+ protected $filter;
+
+ /**
+ * Constructor
+ *
+ * @param mixed $ds
+ */
+ public function __construct($ds, $columns = null)
+ {
+ $this->ds = $ds;
+ $this->filter = Filter::matchAll();
+ if ($columns !== null) {
+ $this->desiredColumns = $columns;
+ }
+ $this->init();
+ if ($this->desiredColumns !== null) {
+ $this->columns($this->desiredColumns);
+ }
+ }
+
+ /**
+ * Initialize query
+ *
+ * Overwrite this instead of __construct (it's called at the end of the construct) to
+ * implement custom initialization logic on construction time
+ */
+ protected function init()
+ {
+ }
+
+ /**
+ * Get the data source
+ *
+ * @return mixed
+ */
+ public function getDatasource()
+ {
+ return $this->ds;
+ }
+
+ /**
+ * Return the current position of this query's iterator
+ *
+ * @return int
+ */
+ public function getIteratorPosition()
+ {
+ return $this->iteratorPosition;
+ }
+
+ /**
+ * Start or rewind the iteration
+ */
+ public function rewind(): void
+ {
+ if ($this->iterator === null) {
+ $iterator = $this->ds->query($this);
+ if ($iterator instanceof IteratorAggregate) {
+ $this->iterator = $iterator->getIterator();
+ } else {
+ $this->iterator = $iterator;
+ }
+ }
+
+ $this->iterator->rewind();
+ $this->iteratorPosition = null;
+ Benchmark::measure('Query result iteration started');
+ }
+
+ /**
+ * Fetch and return the current row of this query's result
+ *
+ * @return mixed
+ */
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ return $this->iterator->current();
+ }
+
+ /**
+ * Return whether the current row of this query's result is valid
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ $valid = $this->iterator->valid();
+ if ($valid && $this->peekAhead && $this->hasLimit() && $this->iteratorPosition + 1 === $this->getLimit()) {
+ $this->hasMore = true;
+ $valid = false; // We arrived at the last result, which is the requested extra row, so stop the iteration
+ } elseif (! $valid) {
+ $this->hasMore = false;
+ }
+
+ if (! $valid) {
+ Benchmark::measure('Query result iteration finished');
+ return false;
+ } elseif ($this->iteratorPosition === null) {
+ $this->iteratorPosition = 0;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return the key for the current row of this query's result
+ *
+ * @return mixed
+ */
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->iterator->key();
+ }
+
+ /**
+ * Advance to the next row of this query's result
+ */
+ public function next(): void
+ {
+ $this->iterator->next();
+ $this->iteratorPosition += 1;
+ }
+
+ /**
+ * Choose a table and the columns you are interested in
+ *
+ * Query will return all available columns if none are given here.
+ *
+ * @param mixed $target
+ * @param array $fields
+ *
+ * @return $this
+ */
+ public function from($target, array $fields = null)
+ {
+ $this->target = $target;
+ if ($fields !== null) {
+ $this->columns($fields);
+ }
+ return $this;
+ }
+
+ /**
+ * Add a where condition to the query by and
+ *
+ * The syntax of the condition and valid values are defined by the concrete backend-specific query implementation.
+ *
+ * @param string $condition
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function where($condition, $value = null)
+ {
+ // TODO: more intelligence please
+ $this->filter->addFilter(Filter::expression($condition, '=', $value));
+ return $this;
+ }
+
+ public function getFilter()
+ {
+ return $this->filter;
+ }
+
+ public function applyFilter(Filter $filter)
+ {
+ return $this->addFilter($filter);
+ }
+
+ public function addFilter(Filter $filter)
+ {
+ $this->filter->addFilter($filter);
+ return $this;
+ }
+
+ public function setFilter(Filter $filter)
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ public function setOrderColumns(array $orderColumns)
+ {
+ throw new IcingaException('This function does nothing and will be removed');
+ }
+
+ /**
+ * Split order field into its field and sort direction
+ *
+ * @param string $field
+ *
+ * @return array
+ */
+ public function splitOrder($field)
+ {
+ $fieldAndDirection = explode(' ', $field, 2);
+ if (count($fieldAndDirection) === 1) {
+ $direction = null;
+ } else {
+ $field = $fieldAndDirection[0];
+ $direction = (strtoupper(trim($fieldAndDirection[1])) === 'DESC') ?
+ Sortable::SORT_DESC : Sortable::SORT_ASC;
+ }
+ return array($field, $direction);
+ }
+
+ /**
+ * Sort result set by the given field (and direction)
+ *
+ * Preferred usage:
+ * <code>
+ * $query->order('field, 'ASC')
+ * </code>
+ *
+ * @param string $field
+ * @param string $direction
+ *
+ * @return $this
+ */
+ public function order($field, $direction = null)
+ {
+ if ($direction === null) {
+ list($field, $direction) = $this->splitOrder($field);
+ if ($direction === null) {
+ $direction = Sortable::SORT_ASC;
+ }
+ } else {
+ switch (($direction = strtoupper($direction))) {
+ case Sortable::SORT_ASC:
+ case Sortable::SORT_DESC:
+ break;
+ default:
+ $direction = Sortable::SORT_ASC;
+ break;
+ }
+ }
+ $this->order[] = array($field, $direction);
+ return $this;
+ }
+
+ /**
+ * Compare $a with $b based on this query's sort rules and column aliases
+ *
+ * @param object $a
+ * @param object $b
+ * @param int $orderIndex
+ *
+ * @return int
+ */
+ public function compare($a, $b, $orderIndex = 0)
+ {
+ if (! array_key_exists($orderIndex, $this->order)) {
+ return 0; // Last column to sort reached, rows are considered being equal
+ }
+
+ if ($this->flippedColumns === null) {
+ $this->flippedColumns = array_flip($this->columns);
+ }
+
+ $column = $this->order[$orderIndex][0];
+ if (array_key_exists($column, $this->flippedColumns) && is_string($this->flippedColumns[$column])) {
+ $column = $this->flippedColumns[$column];
+ }
+
+ $result = strcmp(strtolower($a->$column ?? ''), strtolower($b->$column ?? ''));
+ if ($result === 0) {
+ return $this->compare($a, $b, ++$orderIndex);
+ }
+
+ $direction = $this->order[$orderIndex][1];
+ if ($direction === self::SORT_ASC) {
+ return $result;
+ } else {
+ return $result * -1;
+ }
+ }
+
+ /**
+ * Clear the order if any
+ *
+ * @return $this
+ */
+ public function clearOrder()
+ {
+ $this->order = array();
+ return $this;
+ }
+
+ /**
+ * Whether an order is set
+ *
+ * @return bool
+ */
+ public function hasOrder()
+ {
+ return ! empty($this->order);
+ }
+
+ /**
+ * Get the order
+ *
+ * @return array
+ */
+ public function getOrder()
+ {
+ return $this->order;
+ }
+
+ /**
+ * Set whether this query should peek ahead for more results
+ *
+ * Enabling this causes the current query limit to be increased by one. The potential extra row being yielded will
+ * be removed from the result set. Note that this only applies when fetching multiple results of limited queries.
+ *
+ * @return $this
+ */
+ public function peekAhead($state = true)
+ {
+ $this->peekAhead = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether this query did not yield all available results
+ *
+ * @return bool
+ *
+ * @throws ProgrammingError In case the query did not run yet
+ */
+ public function hasMore()
+ {
+ if ($this->hasMore === null) {
+ throw new ProgrammingError('Query did not run. Cannot determine whether there are more results.');
+ }
+
+ return $this->hasMore;
+ }
+
+ /**
+ * Return whether this query will or has yielded any result
+ *
+ * @return bool
+ */
+ public function hasResult()
+ {
+ return $this->cachedCount > 0 || $this->iteratorPosition !== null || $this->fetchRow() !== false;
+ }
+
+ /**
+ * Set a limit count and offset to the query
+ *
+ * @param int $count Number of rows to return
+ * @param int $offset Start returning after this many rows
+ *
+ * @return $this
+ */
+ public function limit($count = null, $offset = null)
+ {
+ $this->limitCount = $count !== null ? (int) $count : null;
+ $this->limitOffset = (int) $offset;
+ return $this;
+ }
+
+ /**
+ * Whether a limit is set
+ *
+ * @return bool
+ */
+ public function hasLimit()
+ {
+ return $this->limitCount !== null && $this->limitCount > 0;
+ }
+
+ /**
+ * Get the limit if any
+ *
+ * @return int|null
+ */
+ public function getLimit()
+ {
+ return $this->peekAhead && $this->hasLimit() ? $this->limitCount + 1 : $this->limitCount;
+ }
+
+ /**
+ * Whether an offset is set
+ *
+ * @return bool
+ */
+ public function hasOffset()
+ {
+ return $this->limitOffset > 0;
+ }
+
+ /**
+ * Get the offset if any
+ *
+ * @return int|null
+ */
+ public function getOffset()
+ {
+ return $this->limitOffset;
+ }
+
+ /**
+ * Retrieve an array containing all rows of the result set
+ *
+ * @return array
+ */
+ public function fetchAll()
+ {
+ Benchmark::measure('Fetching all results started');
+ $results = $this->ds->fetchAll($this);
+ Benchmark::measure('Fetching all results finished');
+
+ if ($this->peekAhead && $this->hasLimit() && count($results) === $this->getLimit()) {
+ $this->hasMore = true;
+ array_pop($results);
+ } else {
+ $this->hasMore = false;
+ }
+
+ return $results;
+ }
+
+ /**
+ * Fetch the first row of the result set
+ *
+ * @return mixed
+ */
+ public function fetchRow()
+ {
+ Benchmark::measure('Fetching one row started');
+ $row = $this->ds->fetchRow($this);
+ Benchmark::measure('Fetching one row finished');
+ return $row;
+ }
+
+ /**
+ * Fetch the first column of all rows of the result set as an array
+ *
+ * @return array
+ */
+ public function fetchColumn()
+ {
+ Benchmark::measure('Fetching one column started');
+ $values = $this->ds->fetchColumn($this);
+ Benchmark::measure('Fetching one column finished');
+
+ if ($this->peekAhead && $this->hasLimit() && count($values) === $this->getLimit()) {
+ $this->hasMore = true;
+ array_pop($values);
+ } else {
+ $this->hasMore = false;
+ }
+
+ return $values;
+ }
+
+ /**
+ * Fetch the first column of the first row of the result set
+ *
+ * @return string
+ */
+ public function fetchOne()
+ {
+ Benchmark::measure('Fetching one value started');
+ $value = $this->ds->fetchOne($this);
+ Benchmark::measure('Fetching one value finished');
+ return $value;
+ }
+
+ /**
+ * Fetch all rows of the result set as an array of key-value pairs
+ *
+ * The first column is the key, the second column is the value.
+ *
+ * @return array
+ */
+ public function fetchPairs()
+ {
+ Benchmark::measure('Fetching pairs started');
+ $pairs = $this->ds->fetchPairs($this);
+ Benchmark::measure('Fetching pairs finished');
+
+ if ($this->peekAhead && $this->hasLimit() && count($pairs) === $this->getLimit()) {
+ $this->hasMore = true;
+ array_pop($pairs);
+ } else {
+ $this->hasMore = false;
+ }
+
+ return $pairs;
+ }
+
+ /**
+ * Count all rows of the result set, ignoring limit and offset
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ $query = clone $this;
+ $query->limit(0, 0);
+ Benchmark::measure('Counting all results started');
+ $count = $this->ds->count($query);
+ $this->cachedCount = $count;
+ Benchmark::measure('Counting all results finished');
+ return $count;
+ }
+
+ /**
+ * Set columns
+ *
+ * @param array $columns
+ *
+ * @return $this
+ */
+ public function columns(array $columns)
+ {
+ $this->columns = $columns;
+ $this->flippedColumns = null; // Reset, due to updated columns
+ return $this;
+ }
+
+ public function getColumns()
+ {
+ return $this->columns;
+ }
+
+ /**
+ * Deep clone self::$filter
+ */
+ public function __clone()
+ {
+ $this->filter = clone $this->filter;
+ }
+}
diff --git a/library/Icinga/Data/SortRules.php b/library/Icinga/Data/SortRules.php
new file mode 100644
index 0000000..c93bdda
--- /dev/null
+++ b/library/Icinga/Data/SortRules.php
@@ -0,0 +1,14 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+interface SortRules
+{
+ /**
+ * Return some sort rules
+ *
+ * @return array
+ */
+ public function getSortRules();
+}
diff --git a/library/Icinga/Data/Sortable.php b/library/Icinga/Data/Sortable.php
new file mode 100644
index 0000000..11d38c3
--- /dev/null
+++ b/library/Icinga/Data/Sortable.php
@@ -0,0 +1,49 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+/**
+ * Interface for sorting a result set
+ */
+interface Sortable
+{
+ /**
+ * Sort ascending
+ */
+ const SORT_ASC = 'ASC';
+
+ /**
+ * Sort descending
+ */
+ const SORT_DESC = 'DESC';
+
+ /**
+ * Sort result set by the given field (and direction)
+ *
+ * Preferred usage:
+ * <code>
+ * $query->order('field, 'ASC')
+ * </code>
+ *
+ * @param string $field
+ * @param string $direction
+ *
+ * @return self
+ */
+ public function order($field, $direction = null);
+
+ /**
+ * Whether an order is set
+ *
+ * @return bool
+ */
+ public function hasOrder();
+
+ /**
+ * Get the order if any
+ *
+ * @return array|null
+ */
+ public function getOrder();
+}
diff --git a/library/Icinga/Data/Tree/SimpleTree.php b/library/Icinga/Data/Tree/SimpleTree.php
new file mode 100644
index 0000000..e89f589
--- /dev/null
+++ b/library/Icinga/Data/Tree/SimpleTree.php
@@ -0,0 +1,90 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Tree;
+
+use IteratorAggregate;
+use LogicException;
+use Traversable;
+
+/**
+ * A simple tree
+ */
+class SimpleTree implements IteratorAggregate
+{
+ /**
+ * Root node
+ *
+ * @var TreeNode
+ */
+ protected $sentinel;
+
+ /**
+ * Nodes
+ *
+ * @var array
+ */
+ protected $nodes = array();
+
+ /**
+ * Create a new simple tree
+ */
+ public function __construct()
+ {
+ $this->sentinel = new TreeNode();
+ }
+
+ /**
+ * Add a child node
+ *
+ * @param TreeNode $child
+ * @param TreeNode $parent
+ *
+ * @return $this
+ */
+ public function addChild(TreeNode $child, TreeNode $parent = null)
+ {
+ if ($parent === null) {
+ $parent = $this->sentinel;
+ } elseif (! isset($this->nodes[$parent->getId()])) {
+ throw new LogicException(sprintf(
+ 'Can\'t append child node %s to parent node %s: Parent node does not exist',
+ $child->getId(),
+ $parent->getId()
+ ));
+ }
+ if (isset($this->nodes[$child->getId()])) {
+ throw new LogicException(sprintf(
+ 'Can\'t append child node %s to parent node %s: Child node does already exist',
+ $child->getId(),
+ $parent->getId()
+ ));
+ }
+ $this->nodes[$child->getId()] = $child;
+ $parent->appendChild($child);
+ return $this;
+ }
+
+ /**
+ * Get a node by its ID
+ *
+ * @param mixed $id
+ *
+ * @return TreeNode|null
+ */
+ public function getNode($id)
+ {
+ if (! isset($this->nodes[$id])) {
+ return null;
+ }
+ return $this->nodes[$id];
+ }
+
+ /**
+ * @return TreeNodeIterator
+ */
+ public function getIterator(): Traversable
+ {
+ return new TreeNodeIterator($this->sentinel);
+ }
+}
diff --git a/library/Icinga/Data/Tree/TreeNode.php b/library/Icinga/Data/Tree/TreeNode.php
new file mode 100644
index 0000000..66bce79
--- /dev/null
+++ b/library/Icinga/Data/Tree/TreeNode.php
@@ -0,0 +1,109 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Tree;
+
+use Icinga\Data\Identifiable;
+
+class TreeNode implements Identifiable
+{
+ /**
+ * The node's ID
+ *
+ * @var mixed
+ */
+ protected $id;
+
+ /**
+ * The node's value
+ *
+ * @var mixed
+ */
+ protected $value;
+
+ /**
+ * The node's children
+ *
+ * @var array
+ */
+ protected $children = array();
+
+ /**
+ * Set the node's ID
+ *
+ * @param mixed $id ID of the node
+ *
+ * @return $this
+ */
+ public function setId($id)
+ {
+ $this->id = $id;
+ return $this;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see Identifiable::getId() For the method documentation.
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Set the node's value
+ *
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function setValue($value)
+ {
+ $this->value = $value;
+ return $this;
+ }
+
+ /**
+ * Get the node's value
+ *
+ * @return mixed
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Append a child node as the last child of this node
+ *
+ * @param TreeNode $child The child to append
+ *
+ * @return $this
+ */
+ public function appendChild(TreeNode $child)
+ {
+ $this->children[] = $child;
+ return $this;
+ }
+
+
+ /**
+ * Get whether the node has children
+ *
+ * @return bool
+ */
+ public function hasChildren()
+ {
+ return ! empty($this->children);
+ }
+
+ /**
+ * Get the node's children
+ *
+ * @return array
+ */
+ public function getChildren()
+ {
+ return $this->children;
+ }
+}
diff --git a/library/Icinga/Data/Tree/TreeNodeIterator.php b/library/Icinga/Data/Tree/TreeNodeIterator.php
new file mode 100644
index 0000000..1c71787
--- /dev/null
+++ b/library/Icinga/Data/Tree/TreeNodeIterator.php
@@ -0,0 +1,75 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Tree;
+
+use ArrayIterator;
+use RecursiveIterator;
+
+/**
+ * Iterator over a tree node's children
+ */
+class TreeNodeIterator implements RecursiveIterator
+{
+ /**
+ * The node's children
+ *
+ * @var ArrayIterator
+ */
+ protected $children;
+
+ /**
+ * Create a new iterator over a tree node's children
+ *
+ * @param TreeNode $node
+ */
+ public function __construct(TreeNode $node)
+ {
+ $this->children = new ArrayIterator($node->getChildren());
+ }
+
+ public function current(): TreeNode
+ {
+ return $this->children->current();
+ }
+
+ public function key(): int
+ {
+ return $this->children->key();
+ }
+
+ public function next(): void
+ {
+ $this->children->next();
+ }
+
+ public function rewind(): void
+ {
+ $this->children->rewind();
+ }
+
+ public function valid(): bool
+ {
+ return $this->children->valid();
+ }
+
+ public function hasChildren(): bool
+ {
+ return $this->current()->hasChildren();
+ }
+
+ public function getChildren(): TreeNodeIterator
+ {
+ return new static($this->current());
+ }
+
+ /**
+ * Get whether the iterator is empty
+ *
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ return ! $this->children->count();
+ }
+}
diff --git a/library/Icinga/Data/Updatable.php b/library/Icinga/Data/Updatable.php
new file mode 100644
index 0000000..ff70b99
--- /dev/null
+++ b/library/Icinga/Data/Updatable.php
@@ -0,0 +1,24 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\StatementException;
+
+/**
+ * Interface for data updating
+ */
+interface Updatable
+{
+ /**
+ * Update the target with the given data and optionally limit the affected entries by using a filter
+ *
+ * @param string $target
+ * @param array $data
+ * @param Filter $filter
+ *
+ * @throws StatementException
+ */
+ public function update($target, array $data, Filter $filter = null);
+}
diff --git a/library/Icinga/Date/DateFormatter.php b/library/Icinga/Date/DateFormatter.php
new file mode 100644
index 0000000..867462a
--- /dev/null
+++ b/library/Icinga/Date/DateFormatter.php
@@ -0,0 +1,265 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Date;
+
+/**
+ * Date formatting
+ */
+class DateFormatter
+{
+ /**
+ * Format relative
+ *
+ * @var int
+ */
+ const RELATIVE = 0;
+
+ /**
+ * Format time
+ *
+ * @var int
+ */
+ const TIME = 1;
+
+ /**
+ * Format date
+ *
+ * @var int
+ */
+ const DATE = 2;
+
+ /**
+ * Format date and time
+ *
+ * @var int
+ */
+ const DATETIME = 4;
+
+ /**
+ * Get the diff between the given time and the current time
+ *
+ * @param int|float $time
+ * @param bool $requireTime
+ *
+ * @return array
+ */
+ protected static function diff($time, $requireTime = false)
+ {
+ $invert = false;
+ $now = time();
+ $time = (int) $time;
+ $diff = $time - $now;
+ if ($diff < 0) {
+ $diff = abs($diff);
+ $invert = true;
+ }
+ if ($diff > 3600 * 24 * 3) {
+ $type = static::DATE;
+ if (date('Y') === date('Y', $time)) {
+ $formatted = date($requireTime ? 'M j H:i' : 'M j', $time);
+ } else {
+ $formatted = date($requireTime ? 'Y-m-d H:i' : 'Y-m', $time);
+ }
+ } else {
+ $minutes = floor($diff / 60);
+ if ($minutes < 60) {
+ $type = static::RELATIVE;
+ $formatted = sprintf('%dm %ds', $minutes, $diff % 60);
+ } else {
+ $hours = floor($minutes / 60);
+ if ($hours < 24) {
+ if (date('d') === date('d', $time)) {
+ $type = static::TIME;
+ $formatted = date('H:i', $time);
+ } else {
+ $type = static::DATE;
+ $formatted = date('M j H:i', $time);
+ }
+ } else {
+ $type = static::RELATIVE;
+ $formatted = sprintf('%dd %dh', floor($hours / 24), $hours % 24);
+ }
+ }
+ }
+ return array($type, $formatted, $invert);
+ }
+
+ /**
+ * Format date
+ *
+ * @param int|float $date
+ *
+ * @return string
+ */
+ public static function formatDate($date)
+ {
+ return date('Y-m-d', (int) $date);
+ }
+
+ /**
+ * Format date and time
+ *
+ * @param int|float $dateTime
+ *
+ * @return string
+ */
+ public static function formatDateTime($dateTime)
+ {
+ return date('Y-m-d H:i:s', (int) $dateTime);
+ }
+
+ /**
+ * Format a duration
+ *
+ * @param int|float $seconds Duration in seconds
+ *
+ * @return string
+ */
+ public static function formatDuration($seconds)
+ {
+ $minutes = floor((float) $seconds / 60);
+ if ($minutes < 60) {
+ $formatted = sprintf('%dm %ds', $minutes, $seconds % 60);
+ } else {
+ $hours = floor($minutes / 60);
+ if ($hours < 24) {
+ $formatted = sprintf('%dh %dm', $hours, $minutes % 60);
+ } else {
+ $formatted = sprintf('%dd %dh', floor($hours / 24), $hours % 24);
+ }
+ }
+ return $formatted;
+ }
+
+ /**
+ * Format time
+ *
+ * @param int|float $time
+ *
+ * @return string
+ */
+ public static function formatTime($time)
+ {
+ return date('H:i:s', (int) $time);
+ }
+
+ /**
+ * Format time as time ago
+ *
+ * @param int|float $time
+ * @param bool $timeOnly
+ * @param bool $requireTime
+ *
+ * @return ?string
+ */
+ public static function timeAgo($time, $timeOnly = false, $requireTime = false)
+ {
+ list($type, $ago, $invert) = static::diff($time, $requireTime);
+ if ($timeOnly) {
+ return $ago;
+ }
+
+ $formatted = null;
+ switch ($type) {
+ case static::DATE:
+ // Move to next case
+ case static::DATETIME:
+ $formatted = sprintf(
+ t('on %s', 'An event happened on the given date or date and time'),
+ $ago
+ );
+ break;
+ case static::RELATIVE:
+ $formatted = sprintf(
+ t('%s ago', 'An event that happened the given time interval ago'),
+ $ago
+ );
+ break;
+ case static::TIME:
+ $formatted = sprintf(t('at %s', 'An event happened at the given time'), $ago);
+ break;
+ }
+ return $formatted;
+ }
+
+ /**
+ * Format time as time since
+ *
+ * @param int|float $time
+ * @param bool $timeOnly
+ * @param bool $requireTime
+ *
+ * @return ?string
+ */
+ public static function timeSince($time, $timeOnly = false, $requireTime = false)
+ {
+ list($type, $since, $invert) = static::diff($time, $requireTime);
+ if ($timeOnly) {
+ return $since;
+ }
+
+ $formatted = null;
+ switch ($type) {
+ case static::RELATIVE:
+ $formatted = sprintf(
+ t('for %s', 'A status is lasting for the given time interval'),
+ $since
+ );
+ break;
+ case static::DATE:
+ // Move to next case
+ case static::DATETIME:
+ // Move to next case
+ case static::TIME:
+ $formatted = sprintf(
+ t('since %s', 'A status is lasting since the given time, date or date and time'),
+ $since
+ );
+ break;
+ }
+ return $formatted;
+ }
+
+ /**
+ * Format time as time until
+ *
+ * @param int|float $time
+ * @param bool $timeOnly
+ * @param bool $requireTime
+ *
+ * @return ?string
+ */
+ public static function timeUntil($time, $timeOnly = false, $requireTime = false)
+ {
+ list($type, $until, $invert) = static::diff($time, $requireTime);
+ if ($invert && $type === static::RELATIVE) {
+ $until = '-' . $until;
+ }
+ if ($timeOnly) {
+ return $until;
+ }
+
+ $formatted = null;
+ switch ($type) {
+ case static::DATE:
+ // Move to next case
+ case static::DATETIME:
+ $formatted = sprintf(
+ t('on %s', 'An event will happen on the given date or date and time'),
+ $until
+ );
+ break;
+ case static::RELATIVE:
+ $formatted = sprintf(
+ t('in %s', 'An event will happen after the given time interval has elapsed'),
+ $until
+ );
+ break;
+ case static::TIME:
+ $formatted = sprintf(t('at %s', 'An event will happen at the given time'), $until);
+ break;
+ }
+ return $formatted;
+ }
+}
diff --git a/library/Icinga/Exception/AlreadyExistsException.php b/library/Icinga/Exception/AlreadyExistsException.php
new file mode 100644
index 0000000..d70c58f
--- /dev/null
+++ b/library/Icinga/Exception/AlreadyExistsException.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception;
+
+/**
+ * Exception thrown if something to add already exists
+ */
+class AlreadyExistsException extends IcingaException
+{
+}
diff --git a/library/Icinga/Exception/AuthenticationException.php b/library/Icinga/Exception/AuthenticationException.php
new file mode 100644
index 0000000..50910b8
--- /dev/null
+++ b/library/Icinga/Exception/AuthenticationException.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception;
+
+/**
+ * Exception thrown if an error occurs during authentication
+ */
+class AuthenticationException extends IcingaException
+{
+}
diff --git a/library/Icinga/Exception/ConfigurationError.php b/library/Icinga/Exception/ConfigurationError.php
new file mode 100644
index 0000000..e66ec46
--- /dev/null
+++ b/library/Icinga/Exception/ConfigurationError.php
@@ -0,0 +1,12 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception;
+
+/**
+ * Class ConfigurationError
+ * @package Icinga\Exception
+ */
+class ConfigurationError extends IcingaException
+{
+}
diff --git a/library/Icinga/Exception/Http/BaseHttpException.php b/library/Icinga/Exception/Http/BaseHttpException.php
new file mode 100644
index 0000000..cad41c6
--- /dev/null
+++ b/library/Icinga/Exception/Http/BaseHttpException.php
@@ -0,0 +1,73 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception\Http;
+
+use Icinga\Exception\IcingaException;
+
+/**
+ * Base class for HTTP exceptions
+ */
+class BaseHttpException extends IcingaException implements HttpExceptionInterface
+{
+ /**
+ * This exception's HTTP status code
+ *
+ * @var int
+ */
+ protected $statusCode;
+
+ /**
+ * This exception's HTTP response headers
+ *
+ * @var array
+ */
+ protected $headers;
+
+ /**
+ * Return this exception's HTTP status code
+ *
+ * @return int
+ */
+ public function getStatusCode()
+ {
+ return $this->statusCode;
+ }
+
+ /**
+ * Set this exception's HTTP response headers
+ *
+ * @param array $headers
+ *
+ * @return $this
+ */
+ public function setHeaders(array $headers)
+ {
+ $this->headers = $headers;
+ return $this;
+ }
+
+ /**
+ * Set/Add a HTTP response header
+ *
+ * @param string $name
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function setHeader($name, $value)
+ {
+ $this->headers[$name] = $value;
+ return $this;
+ }
+
+ /**
+ * Return this exception's HTTP response headers
+ *
+ * @return array An array where each key is a header name and the value its value
+ */
+ public function getHeaders()
+ {
+ return $this->headers ?: array();
+ }
+}
diff --git a/library/Icinga/Exception/Http/HttpBadRequestException.php b/library/Icinga/Exception/Http/HttpBadRequestException.php
new file mode 100644
index 0000000..004eabd
--- /dev/null
+++ b/library/Icinga/Exception/Http/HttpBadRequestException.php
@@ -0,0 +1,12 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception\Http;
+
+/**
+ * Exception thrown for sending a HTTP 400 response w/ a custom message
+ */
+class HttpBadRequestException extends BaseHttpException
+{
+ protected $statusCode = 400;
+}
diff --git a/library/Icinga/Exception/Http/HttpException.php b/library/Icinga/Exception/Http/HttpException.php
new file mode 100644
index 0000000..cd6b543
--- /dev/null
+++ b/library/Icinga/Exception/Http/HttpException.php
@@ -0,0 +1,25 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception\Http;
+
+class HttpException extends BaseHttpException
+{
+ /**
+ * Create a new HttpException
+ *
+ * @param int $statusCode HTTP status code
+ * @param string $message Exception message or exception format string
+ * @param mixed ...$arg Format string argument
+ *
+ * If there is at least one exception, the last one will be used for exception chaining.
+ */
+ public function __construct($statusCode, $message)
+ {
+ $this->statusCode = (int) $statusCode;
+
+ $args = func_get_args();
+ array_shift($args);
+ call_user_func_array('parent::__construct', $args);
+ }
+}
diff --git a/library/Icinga/Exception/Http/HttpExceptionInterface.php b/library/Icinga/Exception/Http/HttpExceptionInterface.php
new file mode 100644
index 0000000..c5e0cc7
--- /dev/null
+++ b/library/Icinga/Exception/Http/HttpExceptionInterface.php
@@ -0,0 +1,21 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception\Http;
+
+interface HttpExceptionInterface
+{
+ /**
+ * Return this exception's HTTP status code
+ *
+ * @return int
+ */
+ public function getStatusCode();
+
+ /**
+ * Return this exception's HTTP response headers
+ *
+ * @return array An array where each key is a header name and the value its value
+ */
+ public function getHeaders();
+}
diff --git a/library/Icinga/Exception/Http/HttpMethodNotAllowedException.php b/library/Icinga/Exception/Http/HttpMethodNotAllowedException.php
new file mode 100644
index 0000000..4e40b6a
--- /dev/null
+++ b/library/Icinga/Exception/Http/HttpMethodNotAllowedException.php
@@ -0,0 +1,36 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception\Http;
+
+/**
+ * Exception thrown if the HTTP method is not allowed
+ */
+class HttpMethodNotAllowedException extends BaseHttpException
+{
+ protected $statusCode = 405;
+
+ /**
+ * Get the allowed HTTP methods
+ *
+ * @return string
+ */
+ public function getAllowedMethods()
+ {
+ $headers = $this->getHeaders();
+ return isset($headers['Allow']) ? $headers['Allow'] : null;
+ }
+
+ /**
+ * Set the allowed HTTP methods
+ *
+ * @param string $allowedMethods
+ *
+ * @return $this
+ */
+ public function setAllowedMethods($allowedMethods)
+ {
+ $this->setHeader('Allow', (string) $allowedMethods);
+ return $this;
+ }
+}
diff --git a/library/Icinga/Exception/Http/HttpNotFoundException.php b/library/Icinga/Exception/Http/HttpNotFoundException.php
new file mode 100644
index 0000000..eb91d63
--- /dev/null
+++ b/library/Icinga/Exception/Http/HttpNotFoundException.php
@@ -0,0 +1,12 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception\Http;
+
+/**
+ * Exception thrown for sending a HTTP 404 response w/ a custom message
+ */
+class HttpNotFoundException extends BaseHttpException
+{
+ protected $statusCode = 404;
+}
diff --git a/library/Icinga/Exception/IcingaException.php b/library/Icinga/Exception/IcingaException.php
new file mode 100644
index 0000000..f3d06d1
--- /dev/null
+++ b/library/Icinga/Exception/IcingaException.php
@@ -0,0 +1,114 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception;
+
+use Exception;
+use ReflectionClass;
+use Throwable;
+
+class IcingaException extends Exception
+{
+ /**
+ * Create a new exception
+ *
+ * @param string $message Exception message or exception format string
+ * @param mixed ...$arg Format string argument
+ *
+ * If there is at least one exception, the last one will be used for exception chaining.
+ */
+ public function __construct($message)
+ {
+ $args = array_slice(func_get_args(), 1);
+ $exc = null;
+ foreach ($args as &$arg) {
+ if ($arg instanceof Throwable) {
+ $exc = $arg;
+ }
+ }
+
+ if (! empty($args)) {
+ $message = vsprintf($message, $args);
+ }
+
+ parent::__construct($message, 0, $exc);
+ }
+
+ /**
+ * Create the exception from an array of arguments
+ *
+ * @param array $args
+ *
+ * @return static
+ */
+ public static function create(array $args)
+ {
+ $e = new ReflectionClass(get_called_class());
+ return $e->newInstanceArgs($args);
+ }
+
+ /**
+ * Return the given exception formatted as one-liner
+ *
+ * The format used is: %class% in %path%:%line% with message: %message%
+ *
+ * @param Throwable $exception
+ *
+ * @return string
+ */
+ public static function describe(Throwable $exception)
+ {
+ return sprintf(
+ '%s in %s:%d with message: %s',
+ get_class($exception),
+ $exception->getFile(),
+ $exception->getLine(),
+ $exception->getMessage()
+ );
+ }
+
+ /**
+ * Return the same as {@link Exception::getTraceAsString()} for the given exception,
+ * but show only the types of scalar arguments
+ *
+ * @param Throwable $exception
+ *
+ * @return string
+ */
+ public static function getConfidentialTraceAsString(Throwable $exception)
+ {
+ $trace = array();
+
+ $index = 0;
+ foreach ($exception->getTrace() as $index => $frame) {
+ $trace[] = isset($frame['file'])
+ ? "#{$index} {$frame['file']}({$frame['line']}): "
+ : "#{$index} [internal function]: ";
+
+ if (isset($frame['class'])) {
+ $trace[] = $frame['class'];
+ }
+
+ if (isset($frame['type'])) {
+ $trace[] = $frame['type'];
+ }
+
+ $trace[] = "{$frame['function']}(";
+
+ if (isset($frame['args'])) {
+ $args = array();
+ foreach ($frame['args'] as $arg) {
+ $type = gettype($arg);
+ $args[] = $type === 'object' ? 'Object(' . get_class($arg) . ')' : ucfirst($type);
+ }
+
+ $trace[] = implode(', ', $args);
+ }
+ $trace[] = ")\n";
+ }
+
+ $trace[] = '#' . ($index + 1) . ' {main}';
+
+ return implode($trace);
+ }
+}
diff --git a/library/Icinga/Exception/InvalidPropertyException.php b/library/Icinga/Exception/InvalidPropertyException.php
new file mode 100644
index 0000000..e7bcf32
--- /dev/null
+++ b/library/Icinga/Exception/InvalidPropertyException.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception;
+
+/**
+ * Exception thrown if a property does not exist
+ */
+class InvalidPropertyException extends IcingaException
+{
+}
diff --git a/library/Icinga/Exception/Json/JsonDecodeException.php b/library/Icinga/Exception/Json/JsonDecodeException.php
new file mode 100644
index 0000000..978eb30
--- /dev/null
+++ b/library/Icinga/Exception/Json/JsonDecodeException.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception\Json;
+
+/**
+ * Exception thrown by {@link \Icinga\Util\Json::decode()} on failure
+ */
+class JsonDecodeException extends JsonException
+{
+}
diff --git a/library/Icinga/Exception/Json/JsonEncodeException.php b/library/Icinga/Exception/Json/JsonEncodeException.php
new file mode 100644
index 0000000..0bcc6c0
--- /dev/null
+++ b/library/Icinga/Exception/Json/JsonEncodeException.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception\Json;
+
+/**
+ * Exception thrown by {@link \Icinga\Util\Json::encode()} on failure
+ */
+class JsonEncodeException extends JsonException
+{
+}
diff --git a/library/Icinga/Exception/Json/JsonException.php b/library/Icinga/Exception/Json/JsonException.php
new file mode 100644
index 0000000..2ca3605
--- /dev/null
+++ b/library/Icinga/Exception/Json/JsonException.php
@@ -0,0 +1,13 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception\Json;
+
+use Icinga\Exception\IcingaException;
+
+/**
+ * Exception thrown by {@link \Icinga\Util\Json} on failure
+ */
+abstract class JsonException extends IcingaException
+{
+}
diff --git a/library/Icinga/Exception/MissingParameterException.php b/library/Icinga/Exception/MissingParameterException.php
new file mode 100644
index 0000000..a8bd78d
--- /dev/null
+++ b/library/Icinga/Exception/MissingParameterException.php
@@ -0,0 +1,40 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception;
+
+/**
+ * Exception thrown if a mandatory parameter was not given
+ */
+class MissingParameterException extends IcingaException
+{
+ /**
+ * Name of the missing parameter
+ *
+ * @var string
+ */
+ protected $parameter;
+
+ /**
+ * Get the name of the missing parameter
+ *
+ * @return string
+ */
+ public function getParameter()
+ {
+ return $this->parameter;
+ }
+
+ /**
+ * Set the name of the missing parameter
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setParameter($name)
+ {
+ $this->parameter = (string) $name;
+ return $this;
+ }
+}
diff --git a/library/Icinga/Exception/NotFoundError.php b/library/Icinga/Exception/NotFoundError.php
new file mode 100644
index 0000000..74e6941
--- /dev/null
+++ b/library/Icinga/Exception/NotFoundError.php
@@ -0,0 +1,8 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception;
+
+class NotFoundError extends IcingaException
+{
+}
diff --git a/library/Icinga/Exception/NotImplementedError.php b/library/Icinga/Exception/NotImplementedError.php
new file mode 100644
index 0000000..395b4b2
--- /dev/null
+++ b/library/Icinga/Exception/NotImplementedError.php
@@ -0,0 +1,12 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception;
+
+/**
+ * Class NotImplementedError
+ * @package Icinga\Exception
+ */
+class NotImplementedError extends IcingaException
+{
+}
diff --git a/library/Icinga/Exception/NotReadableError.php b/library/Icinga/Exception/NotReadableError.php
new file mode 100644
index 0000000..6bf2b3c
--- /dev/null
+++ b/library/Icinga/Exception/NotReadableError.php
@@ -0,0 +1,8 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception;
+
+class NotReadableError extends IcingaException
+{
+}
diff --git a/library/Icinga/Exception/NotWritableError.php b/library/Icinga/Exception/NotWritableError.php
new file mode 100644
index 0000000..efe1fbb
--- /dev/null
+++ b/library/Icinga/Exception/NotWritableError.php
@@ -0,0 +1,8 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception;
+
+class NotWritableError extends IcingaException
+{
+}
diff --git a/library/Icinga/Exception/ProgrammingError.php b/library/Icinga/Exception/ProgrammingError.php
new file mode 100644
index 0000000..02d4b47
--- /dev/null
+++ b/library/Icinga/Exception/ProgrammingError.php
@@ -0,0 +1,12 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception;
+
+/**
+ * Class ProgrammingError
+ * @package Icinga\Exception
+ */
+class ProgrammingError extends IcingaException
+{
+}
diff --git a/library/Icinga/Exception/QueryException.php b/library/Icinga/Exception/QueryException.php
new file mode 100644
index 0000000..9344b86
--- /dev/null
+++ b/library/Icinga/Exception/QueryException.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception;
+
+/**
+ * Exception thrown if a query encountered an error
+ */
+class QueryException extends IcingaException
+{
+}
diff --git a/library/Icinga/Exception/StatementException.php b/library/Icinga/Exception/StatementException.php
new file mode 100644
index 0000000..7501c86
--- /dev/null
+++ b/library/Icinga/Exception/StatementException.php
@@ -0,0 +1,8 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception;
+
+class StatementException extends IcingaException
+{
+}
diff --git a/library/Icinga/Exception/SystemPermissionException.php b/library/Icinga/Exception/SystemPermissionException.php
new file mode 100644
index 0000000..5651169
--- /dev/null
+++ b/library/Icinga/Exception/SystemPermissionException.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Exception;
+
+/**
+ * Handle problems according to file system permissions
+ */
+class SystemPermissionException extends IcingaException
+{
+}
diff --git a/library/Icinga/File/Csv.php b/library/Icinga/File/Csv.php
new file mode 100644
index 0000000..56ee233
--- /dev/null
+++ b/library/Icinga/File/Csv.php
@@ -0,0 +1,47 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File;
+
+use Traversable;
+
+class Csv
+{
+ protected $query;
+
+ protected function __construct()
+ {
+ }
+
+ public static function fromQuery(Traversable $query)
+ {
+ $csv = new static();
+ $csv->query = $query;
+ return $csv;
+ }
+
+ public function dump()
+ {
+ header('Content-type: text/csv');
+ echo (string) $this;
+ }
+
+ public function __toString()
+ {
+ $first = true;
+ $csv = '';
+ foreach ($this->query as $row) {
+ if ($first) {
+ $csv .= implode(',', array_keys((array) $row)) . "\r\n";
+ $first = false;
+ }
+ $out = array();
+ foreach ($row as & $val) {
+ $out[] = '"' . str_replace('"', '""', $val) . '"';
+ }
+ $csv .= implode(',', $out) . "\r\n";
+ }
+
+ return $csv;
+ }
+}
diff --git a/library/Icinga/File/Ini/Dom/Comment.php b/library/Icinga/File/Ini/Dom/Comment.php
new file mode 100644
index 0000000..c202d0f
--- /dev/null
+++ b/library/Icinga/File/Ini/Dom/Comment.php
@@ -0,0 +1,37 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File\Ini\Dom;
+
+/**
+ * A single comment-line in an INI file
+ */
+class Comment
+{
+ /**
+ * The comment text
+ *
+ * @var string
+ */
+ protected $content;
+
+ /**
+ * Set the text content of this comment
+ *
+ * @param $content
+ */
+ public function setContent($content)
+ {
+ $this->content = $content;
+ }
+
+ /**
+ * Render this comment into INI markup
+ *
+ * @return string
+ */
+ public function render()
+ {
+ return ';' . $this->content;
+ }
+}
diff --git a/library/Icinga/File/Ini/Dom/Directive.php b/library/Icinga/File/Ini/Dom/Directive.php
new file mode 100644
index 0000000..4279a5f
--- /dev/null
+++ b/library/Icinga/File/Ini/Dom/Directive.php
@@ -0,0 +1,166 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File\Ini\Dom;
+
+use Icinga\Exception\ConfigurationError;
+
+/**
+ * A key value pair in a Section
+ */
+class Directive
+{
+ /**
+ * The value of this configuration directive
+ *
+ * @var string
+ */
+ protected $key;
+
+ /**
+ * The immutable name of this configuration directive
+ *
+ * @var string
+ */
+ protected $value;
+
+ /**
+ * Comments added one line before this directive
+ *
+ * @var Comment[] The comment lines
+ */
+ protected $commentsPre = null;
+
+ /**
+ * Comment added at the end of the same line
+ *
+ * @var Comment
+ */
+ protected $commentPost = null;
+
+ /**
+ * @param string $key The name of this configuration directive
+ *
+ * @throws ConfigurationError
+ */
+ public function __construct($key)
+ {
+ $this->key = trim($key);
+ if (strlen($this->key) < 1) {
+ throw new ConfigurationError(sprintf('Ini error: empty directive key.'));
+ }
+ }
+
+ /**
+ * Return the name of this directive
+ *
+ * @return string
+ */
+ public function getKey()
+ {
+ return $this->key;
+ }
+
+ /**
+ * Return the value of this configuration directive
+ *
+ * @return string
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Set the value of this configuration directive
+ *
+ * @param string $value
+ */
+ public function setValue($value)
+ {
+ $this->value = trim($value);
+ }
+
+ /**
+ * Set the comments to be rendered on the line before this directive
+ *
+ * @param Comment[] $comments
+ */
+ public function setCommentsPre(array $comments)
+ {
+ $this->commentsPre = $comments;
+ }
+
+ /**
+ * Return the comments to be rendered on the line before this directive
+ *
+ * @return Comment[]
+ */
+ public function getCommentsPre()
+ {
+ return $this->commentsPre;
+ }
+
+ /**
+ * Set the comment rendered on the same line of this directive
+ *
+ * @param Comment $comment
+ */
+ public function setCommentPost(Comment $comment)
+ {
+ $this->commentPost = $comment;
+ }
+
+ /**
+ * Render this configuration directive into INI markup
+ *
+ * @return string
+ */
+ public function render()
+ {
+ $str = '';
+ if (! empty($this->commentsPre)) {
+ $comments = array();
+ foreach ($this->commentsPre as $comment) {
+ $comments[] = $comment->render();
+ }
+ $str = implode(PHP_EOL, $comments) . PHP_EOL;
+ }
+ $str .= sprintf('%s = "%s"', $this->sanitizeKey($this->key), $this->sanitizeValue($this->value));
+ if (isset($this->commentPost)) {
+ $str .= ' ' . $this->commentPost->render();
+ }
+ return $str;
+ }
+
+ /**
+ * Assure that the given identifier contains no newlines and pending or trailing whitespaces
+ *
+ * @param $str The string to sanitize
+ *
+ * @return string
+ */
+ protected function sanitizeKey($str)
+ {
+ return trim(str_replace(PHP_EOL, ' ', $str));
+ }
+
+ /**
+ * Escape the significant characters in directive values, normalize line breaks and assure that
+ * the character contains no linebreaks
+ *
+ * @param $str The string to sanitize
+ *
+ * @return mixed|string
+ */
+ protected function sanitizeValue($str)
+ {
+ $str = trim($str);
+ $str = str_replace('\\', '\\\\', $str);
+ $str = str_replace('"', '\"', $str);
+ $str = str_replace("\r", '\r', $str);
+ $str = str_replace("\n", '\n', $str);
+
+ return $str;
+ }
+}
diff --git a/library/Icinga/File/Ini/Dom/Document.php b/library/Icinga/File/Ini/Dom/Document.php
new file mode 100644
index 0000000..f38f33e
--- /dev/null
+++ b/library/Icinga/File/Ini/Dom/Document.php
@@ -0,0 +1,132 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File\Ini\Dom;
+
+class Document
+{
+ /**
+ * The sections of this INI file
+ *
+ * @var Section[]
+ */
+ protected $sections = array();
+
+ /**
+ * The comemnts at file end that belong to no particular section
+ *
+ * @var Comment[]
+ */
+ protected $commentsDangling;
+
+ /**
+ * Append a section to the end of this INI file
+ *
+ * @param Section $section
+ */
+ public function addSection(Section $section)
+ {
+ $this->sections[$section->getName()] = $section;
+ }
+
+ /**
+ * Return whether this INI file has the section with the given key
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function hasSection($name)
+ {
+ return isset($this->sections[trim($name)]);
+ }
+
+ /**
+ * Return the section with the given name
+ *
+ * @param string $name
+ *
+ * @return Section
+ */
+ public function getSection($name)
+ {
+ return $this->sections[trim($name)];
+ }
+
+ /**
+ * Set the section with the given name
+ *
+ * @param string $name
+ * @param Section $section
+ *
+ * @return Section
+ */
+ public function setSection($name, Section $section)
+ {
+ return $this->sections[trim($name)] = $section;
+ }
+
+ /**
+ * Remove the section with the given name
+ *
+ * @param string $name
+ */
+ public function removeSection($name)
+ {
+ unset($this->sections[trim($name)]);
+ }
+
+ /**
+ * Set the dangling comments at file end that belong to no particular directive
+ *
+ * @param Comment[] $comments
+ */
+ public function setCommentsDangling(array $comments)
+ {
+ $this->commentsDangling = $comments;
+ }
+
+ /**
+ * Get the dangling comments at file end that belong to no particular directive
+ *
+ * @return array
+ */
+ public function getCommentsDangling()
+ {
+ return $this->commentsDangling;
+ }
+
+ /**
+ * Render this document into the corresponding INI markup
+ *
+ * @return string
+ */
+ public function render()
+ {
+ $sections = array();
+ foreach ($this->sections as $section) {
+ $sections []= $section->render();
+ }
+ $str = implode(PHP_EOL, $sections);
+ if (! empty($this->commentsDangling)) {
+ foreach ($this->commentsDangling as $comment) {
+ $str .= PHP_EOL . $comment->render();
+ }
+ }
+ return $str;
+ }
+
+ /**
+ * Convert $this to an array
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ $a = array();
+ foreach ($this->sections as $section) {
+ $a[$section->getName()] = $section->toArray();
+ }
+ return $a;
+ }
+}
diff --git a/library/Icinga/File/Ini/Dom/Section.php b/library/Icinga/File/Ini/Dom/Section.php
new file mode 100644
index 0000000..5fac5ea
--- /dev/null
+++ b/library/Icinga/File/Ini/Dom/Section.php
@@ -0,0 +1,190 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File\Ini\Dom;
+
+use Icinga\Exception\ConfigurationError;
+
+/**
+ * A section in an INI file
+ */
+class Section
+{
+ /**
+ * The immutable name of this section
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * All configuration directives of this section
+ *
+ * @var Directive[]
+ */
+ protected $directives = array();
+
+ /**
+ * Comments added one line before this section
+ *
+ * @var Comment[]
+ */
+ protected $commentsPre;
+
+ /**
+ * Comment added at the end of the same line
+ *
+ * @var Comment
+ */
+ protected $commentPost;
+
+ /**
+ * @param string $name The immutable name of this section
+ *
+ * @throws ConfigurationError When the section name is empty or contains brackets
+ */
+ public function __construct($name)
+ {
+ $this->name = trim($name);
+ if (strlen($this->name) < 1) {
+ throw new ConfigurationError('Ini file error: empty section identifier');
+ } elseif (strpos($name, '[') !== false || strpos($name, ']') !== false) {
+ throw new ConfigurationError(
+ 'Ini file error: Section name "%s" must not contain any brackets ([, ])',
+ $name
+ );
+ }
+ }
+
+ /**
+ * Append a directive to the end of this section
+ *
+ * @param Directive $directive The directive to append
+ */
+ public function addDirective(Directive $directive)
+ {
+ $this->directives[$directive->getKey()] = $directive;
+ }
+
+ /**
+ * Remove the directive with the given name
+ *
+ * @param string $key They name of the directive to remove
+ */
+ public function removeDirective($key)
+ {
+ unset($this->directives[$key]);
+ }
+
+ /**
+ * Return whether this section has a directive with the given key
+ *
+ * @param string $key The name of the directive
+ *
+ * @return bool
+ */
+ public function hasDirective($key)
+ {
+ return isset($this->directives[$key]);
+ }
+
+ /**
+ * Get the directive with the given key
+ *
+ * @param $key string
+ *
+ * @return Directive
+ */
+ public function getDirective($key)
+ {
+ return $this->directives[$key];
+ }
+
+ /**
+ * Return the name of this section
+ *
+ * @return string The name
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set the comments to be rendered on the line before this section
+ *
+ * @param Comment[] $comments
+ */
+ public function setCommentsPre(array $comments)
+ {
+ $this->commentsPre = $comments;
+ }
+
+ /**
+ * Set the comment rendered on the same line of this section
+ *
+ * @param Comment $comment
+ */
+ public function setCommentPost(Comment $comment)
+ {
+ $this->commentPost = $comment;
+ }
+
+ /**
+ * Render this section into INI markup
+ *
+ * @return string
+ */
+ public function render()
+ {
+ $dirs = '';
+ $i = 0;
+ foreach ($this->directives as $directive) {
+ $comments = $directive->getCommentsPre();
+ $dirs .= (($i++ > 0 && ! empty($comments)) ? PHP_EOL : '')
+ . $directive->render() . PHP_EOL;
+ }
+ $cms = '';
+ if (! empty($this->commentsPre)) {
+ foreach ($this->commentsPre as $comment) {
+ $comments[] = $comment->render();
+ }
+ $cms = implode(PHP_EOL, $comments) . PHP_EOL;
+ }
+ $post = '';
+ if (isset($this->commentPost)) {
+ $post = ' ' . $this->commentPost->render();
+ }
+ return $cms . sprintf('[%s]', $this->sanitize($this->name)) . $post . PHP_EOL . $dirs;
+ }
+
+ /**
+ * Escape the significant characters in sections and normalize line breaks
+ *
+ * @param $str The string to sanitize
+ *
+ * @return mixed
+ */
+ protected function sanitize($str)
+ {
+ $str = trim($str);
+ $str = str_replace('\\', '\\\\', $str);
+ $str = str_replace('"', '\\"', $str);
+ $str = str_replace(';', '\\;', $str);
+ return str_replace(PHP_EOL, ' ', $str);
+ }
+
+ /**
+ * Convert $this to an array
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ $a = array();
+ foreach ($this->directives as $directive) {
+ $a[$directive->getKey()] = $directive->getValue();
+ }
+ return $a;
+ }
+}
diff --git a/library/Icinga/File/Ini/IniParser.php b/library/Icinga/File/Ini/IniParser.php
new file mode 100644
index 0000000..279aa45
--- /dev/null
+++ b/library/Icinga/File/Ini/IniParser.php
@@ -0,0 +1,310 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File\Ini;
+
+use ErrorException;
+use Icinga\File\Ini\Dom\Section;
+use Icinga\File\Ini\Dom\Comment;
+use Icinga\File\Ini\Dom\Document;
+use Icinga\File\Ini\Dom\Directive;
+use Icinga\Application\Logger;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\NotReadableError;
+use Icinga\Application\Config;
+
+class IniParser
+{
+ const LINE_START = 0;
+ const SECTION = 1;
+ const ESCAPE = 2;
+ const DIRECTIVE_KEY = 4;
+ const DIRECTIVE_VALUE_START = 5;
+ const DIRECTIVE_VALUE = 6;
+ const DIRECTIVE_VALUE_QUOTED = 7;
+ const COMMENT = 8;
+ const COMMENT_END = 9;
+ const LINE_END = 10;
+
+ /**
+ * Cancel the parsing with an error
+ *
+ * @param $message The error description
+ * @param $line The line in which the error occured
+ *
+ * @throws ConfigurationError
+ */
+ private static function throwParseError($message, $line)
+ {
+ throw new ConfigurationError(sprintf('Ini parser error: %s. (l. %d)', $message, $line));
+ }
+
+ /**
+ * Read the ini file contained in a string and return a mutable DOM that can be used
+ * to change the content of an INI file.
+ *
+ * @param $str A string containing the whole ini file
+ *
+ * @return Document The mutable DOM object.
+ * @throws ConfigurationError In case the file is not parseable
+ */
+ public static function parseIni($str)
+ {
+ $doc = new Document();
+ $sec = null;
+ $dir = null;
+ $coms = array();
+ $state = self::LINE_START;
+ $escaping = null;
+ $token = '';
+ $line = 0;
+
+ for ($i = 0; $i < strlen($str); $i++) {
+ $s = $str[$i];
+ switch ($state) {
+ case self::LINE_START:
+ if (ctype_space($s)) {
+ continue 2;
+ }
+ switch ($s) {
+ case '[':
+ $state = self::SECTION;
+ break;
+ case ';':
+ $state = self::COMMENT;
+ break;
+ default:
+ $state = self::DIRECTIVE_KEY;
+ $token = $s;
+ break;
+ }
+ break;
+
+ case self::ESCAPE:
+ $token .= $s;
+ $state = $escaping;
+ $escaping = null;
+ break;
+
+ case self::SECTION:
+ if ($s === "\n") {
+ self::throwParseError('Unterminated SECTION', $line);
+ } elseif ($s === '\\') {
+ $state = self::ESCAPE;
+ $escaping = self::SECTION;
+ } elseif ($s !== ']') {
+ $token .= $s;
+ } else {
+ $sec = new Section($token);
+ $sec->setCommentsPre($coms);
+ $doc->addSection($sec);
+ $dir = null;
+ $coms = array();
+
+ $state = self::LINE_END;
+ $token = '';
+ }
+ break;
+
+ case self::DIRECTIVE_KEY:
+ if ($s !== '=') {
+ $token .= $s;
+ } else {
+ $dir = new Directive($token);
+ $dir->setCommentsPre($coms);
+ if (isset($sec)) {
+ $sec->addDirective($dir);
+ } else {
+ Logger::warning(sprintf(
+ 'Ini parser warning: section-less directive "%s" ignored. (l. %d)',
+ $token,
+ $line
+ ));
+ }
+
+ $coms = array();
+ $state = self::DIRECTIVE_VALUE_START;
+ $token = '';
+ }
+ break;
+
+ case self::DIRECTIVE_VALUE_START:
+ if (ctype_space($s)) {
+ continue 2;
+ } elseif ($s === '"') {
+ $state = self::DIRECTIVE_VALUE_QUOTED;
+ } else {
+ $state = self::DIRECTIVE_VALUE;
+ $token = $s;
+ }
+ break;
+
+ case self::DIRECTIVE_VALUE:
+ /*
+ Escaping non-quoted values is not supported by php_parse_ini, it might
+ be reasonable to include in case we are switching completely our own
+ parser implementation
+ */
+ if ($s === "\n" || $s === ";") {
+ $dir->setValue($token);
+ $token = '';
+
+ if ($s === "\n") {
+ $state = self::LINE_START;
+ $line ++;
+ } elseif ($s === ';') {
+ $state = self::COMMENT;
+ }
+ } else {
+ $token .= $s;
+ }
+ break;
+
+ case self::DIRECTIVE_VALUE_QUOTED:
+ if ($s === '\\') {
+ $state = self::ESCAPE;
+ $escaping = self::DIRECTIVE_VALUE_QUOTED;
+ } elseif ($s !== '"') {
+ $token .= $s;
+ } else {
+ $dir->setValue($token);
+ $token = '';
+ $state = self::LINE_END;
+ }
+ break;
+
+ case self::COMMENT:
+ case self::COMMENT_END:
+ if ($s !== "\n") {
+ $token .= $s;
+ } else {
+ $com = new Comment();
+ $com->setContent($token);
+ $token = '';
+
+ // Comments at the line end belong to the current line's directive or section. Comments
+ // on empty lines belong to the next directive that shows up.
+ if ($state === self::COMMENT_END) {
+ if (isset($dir)) {
+ $dir->setCommentPost($com);
+ } else {
+ $sec->setCommentPost($com);
+ }
+ } else {
+ $coms[] = $com;
+ }
+ $state = self::LINE_START;
+ $line ++;
+ }
+ break;
+
+ case self::LINE_END:
+ if ($s === "\n") {
+ $state = self::LINE_START;
+ $line ++;
+ } elseif ($s === ';') {
+ $state = self::COMMENT_END;
+ }
+ break;
+ }
+ }
+
+ // process the last token
+ switch ($state) {
+ case self::COMMENT:
+ case self::COMMENT_END:
+ $com = new Comment();
+ $com->setContent($token);
+ if ($state === self::COMMENT_END) {
+ if (isset($dir)) {
+ $dir->setCommentPost($com);
+ } else {
+ $sec->setCommentPost($com);
+ }
+ } else {
+ $coms[] = $com;
+ }
+ break;
+
+ case self::DIRECTIVE_VALUE:
+ $dir->setValue($token);
+ $sec->addDirective($dir);
+ break;
+
+ case self::ESCAPE:
+ case self::DIRECTIVE_VALUE_QUOTED:
+ case self::DIRECTIVE_KEY:
+ case self::SECTION:
+ self::throwParseError('File ended in unterminated state ' . $state, $line);
+ }
+ if (! empty($coms)) {
+ $doc->setCommentsDangling($coms);
+ }
+ return $doc;
+ }
+
+ /**
+ * Read the ini file and parse it with ::parseIni()
+ *
+ * @param string $file The ini file to read
+ *
+ * @return Config
+ * @throws NotReadableError When the file cannot be read
+ */
+ public static function parseIniFile($file)
+ {
+ if (($path = realpath($file)) === false) {
+ throw new NotReadableError('Couldn\'t compute the absolute path of `%s\'', $file);
+ }
+
+ if (($content = file_get_contents($path)) === false) {
+ throw new NotReadableError('Couldn\'t read the file `%s\'', $path);
+ }
+
+ try {
+ $configArray = parse_ini_string($content, true, INI_SCANNER_RAW);
+ } catch (ErrorException $e) {
+ throw new ConfigurationError('Couldn\'t parse the INI file `%s\'', $path, $e);
+ }
+
+ $unescaped = array();
+ foreach ($configArray as $section => $options) {
+ $unescaped[self::unescapeSectionName($section)] = array_map([__CLASS__, 'unescapeOptionValue'], $options);
+ }
+
+ return Config::fromArray($unescaped)->setConfigFile($file);
+ }
+
+ /**
+ * Unescape significant characters in the given section name
+ *
+ * @param string $str
+ *
+ * @return string
+ */
+ protected static function unescapeSectionName($str)
+ {
+ $str = str_replace('\"', '"', $str);
+ $str = str_replace('\;', ';', $str);
+
+ return str_replace('\\\\', '\\', $str);
+ }
+
+ /**
+ * Unescape significant characters in the given option value
+ *
+ * @param string $str
+ *
+ * @return string
+ */
+ protected static function unescapeOptionValue($str)
+ {
+ $str = str_replace('\n', "\n", $str);
+ $str = str_replace('\r', "\r", $str);
+ $str = str_replace('\"', '"', $str);
+ $str = str_replace('\\\\', '\\', $str);
+
+ // This replacement is a work-around for PHP bug #76965. Fixed with versions 7.1.24, 7.2.12 and 7.3.0.
+ return preg_replace('~^([\'"])(.*?)\1\s+$~', '$2', $str);
+ }
+}
diff --git a/library/Icinga/File/Ini/IniWriter.php b/library/Icinga/File/Ini/IniWriter.php
new file mode 100644
index 0000000..1f470b0
--- /dev/null
+++ b/library/Icinga/File/Ini/IniWriter.php
@@ -0,0 +1,205 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File\Ini;
+
+use Icinga\Application\Logger;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\ProgrammingError;
+use Icinga\File\Ini\Dom\Directive;
+use Icinga\File\Ini\Dom\Document;
+use Icinga\File\Ini\Dom\Section;
+use Zend_Config_Exception;
+use Icinga\Application\Config;
+
+/**
+ * A INI file adapter that respects the file structure and the comments of already existing ini files
+ */
+class IniWriter
+{
+ /**
+ * Stores the options
+ *
+ * @var array
+ */
+ protected $options;
+
+ /**
+ * The configuration object to write
+ *
+ * @var Config
+ */
+ protected $config;
+
+ /**
+ * The mode to set on new files
+ *
+ * @var int
+ */
+ protected $fileMode;
+
+ /**
+ * The path to write to
+ *
+ * @var string
+ */
+ protected $filename;
+
+ /**
+ * Create a new INI writer
+ *
+ * @param Config $config The configuration to write
+ * @param string $filename The file name to write to
+ * @param int $filemode Octal file persmissions
+ *
+ * @link http://framework.zend.com/apidoc/1.12/files/Config.Writer.html#\Zend_Config_Writer
+ */
+ public function __construct(Config $config, $filename, $filemode = 0660, $options = array())
+ {
+ $this->config = $config;
+ $this->filename = $filename;
+ $this->fileMode = $filemode;
+ $this->options = $options;
+ }
+
+ /**
+ * Render the Zend_Config into a config filestring
+ *
+ * @return string
+ */
+ public function render()
+ {
+ if (file_exists($this->filename)) {
+ $oldconfig = Config::fromIni($this->filename);
+ $content = trim(file_get_contents($this->filename));
+ } else {
+ $oldconfig = Config::fromArray(array());
+ $content = '';
+ }
+ $doc = IniParser::parseIni($content);
+ $this->diffPropertyUpdates($this->config, $doc);
+ $this->diffPropertyDeletions($oldconfig, $this->config, $doc);
+ $doc = $this->updateSectionOrder($this->config, $doc);
+ return $doc->render();
+ }
+
+ /**
+ * Write configuration to file and set file mode in case it does not exist yet
+ *
+ * @param string $filename
+ * @param bool $exclusiveLock
+ *
+ * @throws Zend_Config_Exception
+ */
+ public function write($filename = null, $exclusiveLock = false)
+ {
+ $filePath = isset($filename) ? $filename : $this->filename;
+ $setMode = false === file_exists($filePath);
+
+ if (file_put_contents($filePath, $this->render(), $exclusiveLock ? LOCK_EX : 0) === false) {
+ throw new Zend_Config_Exception('Could not write to file "' . $filePath . '"');
+ }
+
+ if ($setMode) {
+ // file was newly created
+ $mode = $this->fileMode;
+ if (is_int($this->fileMode) && false === @chmod($filePath, $this->fileMode)) {
+ throw new Zend_Config_Exception(sprintf('Failed to set file mode "%o" on file "%s"', $mode, $filePath));
+ }
+ }
+ }
+
+ /**
+ * Update the order of the sections in the ini file to match the order of the new config
+ *
+ * @return Document A new document with the changed section order applied
+ */
+ protected function updateSectionOrder(Config $newconfig, Document $oldDoc)
+ {
+ $doc = new Document();
+ $dangling = $oldDoc->getCommentsDangling();
+ if (! empty($dangling)) {
+ $doc->setCommentsDangling($dangling);
+ }
+ foreach ($newconfig->toArray() as $section => $directives) {
+ $doc->addSection($oldDoc->getSection($section));
+ }
+ return $doc;
+ }
+
+ /**
+ * Search for created and updated properties and use the editor to create or update these entries
+ *
+ * @param Config $newconfig The config representing the state after the change
+ * @param Document $doc
+ *
+ * @throws ProgrammingError
+ */
+ protected function diffPropertyUpdates(Config $newconfig, Document $doc)
+ {
+ foreach ($newconfig->toArray() as $section => $directives) {
+ if (! is_array($directives)) {
+ Logger::warning('Section-less property ' . (string)$directives . ' was ignored.');
+ continue;
+ }
+ if (!$doc->hasSection($section)) {
+ $domSection = new Section($section);
+ $doc->addSection($domSection);
+ } else {
+ $domSection = $doc->getSection($section);
+ }
+ foreach ($directives as $key => $value) {
+ if ($value === null) {
+ continue;
+ }
+
+ if ($value instanceof ConfigObject) {
+ throw new ProgrammingError('Cannot diff recursive configs');
+ }
+ if ($domSection->hasDirective($key)) {
+ $domSection->getDirective($key)->setValue($value);
+ } else {
+ $dir = new Directive($key);
+ $dir->setValue($value);
+ $domSection->addDirective($dir);
+ }
+ }
+ }
+ }
+
+ /**
+ * Search for deleted properties and use the editor to delete these entries
+ *
+ * @param Config $oldconfig The config representing the state before the change
+ * @param Config $newconfig The config representing the state after the change
+ * @param Document $doc
+ *
+ * @throws ProgrammingError
+ */
+ protected function diffPropertyDeletions(Config $oldconfig, Config $newconfig, Document $doc)
+ {
+ // Iterate over all properties in the old configuration file and remove those that don't
+ // exist in the new config
+ foreach ($oldconfig->toArray() as $section => $directives) {
+ if (! is_array($directives)) {
+ Logger::warning('Section-less property ' . (string)$directives . ' was ignored.');
+ continue;
+ }
+
+ if ($newconfig->hasSection($section)) {
+ $newSection = $newconfig->getSection($section);
+ $oldDomSection = $doc->getSection($section);
+ foreach ($directives as $key => $value) {
+ if ($value instanceof ConfigObject) {
+ throw new ProgrammingError('Cannot diff recursive configs');
+ }
+ if (null === $newSection->get($key) && $oldDomSection->hasDirective($key)) {
+ $oldDomSection->removeDirective($key);
+ }
+ }
+ } else {
+ $doc->removeSection($section);
+ }
+ }
+ }
+}
diff --git a/library/Icinga/File/Pdf.php b/library/Icinga/File/Pdf.php
new file mode 100644
index 0000000..1b78424
--- /dev/null
+++ b/library/Icinga/File/Pdf.php
@@ -0,0 +1,81 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File;
+
+use Dompdf\Dompdf;
+use Dompdf\Options;
+use Icinga\Application\Icinga;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Util\Environment;
+use Icinga\Web\Hook;
+use Icinga\Web\Url;
+
+class Pdf
+{
+ protected function assertNoHeadersSent()
+ {
+ if (headers_sent()) {
+ throw new ProgrammingError(
+ 'Could not send pdf-response, content already written to output.'
+ );
+ }
+ }
+
+ public function renderControllerAction($controller)
+ {
+ $this->assertNoHeadersSent();
+
+ Environment::raiseMemoryLimit('512M');
+ Environment::raiseExecutionTime(300);
+
+ $viewRenderer = $controller->getHelper('viewRenderer');
+ $viewRenderer->postDispatch();
+
+ $layoutHelper = $controller->getHelper('layout');
+ $oldLayout = $layoutHelper->getLayout();
+ $layout = $layoutHelper->setLayout('pdf');
+
+ $layout->content = $controller->getResponse();
+ $html = $layout->render();
+
+ // Restore previous layout and reset content, to properly show errors
+ $controller->getResponse()->clearBody($viewRenderer->getResponseSegment());
+ $layoutHelper->setLayout($oldLayout);
+
+ $imgDir = Url::fromPath('img');
+ $html = preg_replace(
+ '~src="' . $imgDir . '/~',
+ 'src="' . Icinga::app()->getBootstrapDirectory() . '/img/',
+ $html
+ );
+
+ $request = $controller->getRequest();
+
+ if (Hook::has('Pdfexport')) {
+ $pdfexport = Hook::first('Pdfexport');
+ $pdfexport->streamPdfFromHtml($html, sprintf(
+ '%s-%s-%d',
+ $request->getControllerName(),
+ $request->getActionName(),
+ time()
+ ));
+
+ return;
+ }
+
+ $options = new Options();
+ $options->set('defaultPaperSize', 'A4');
+ $dompdf = new Dompdf($options);
+ $dompdf->loadHtml($html);
+ $dompdf->render();
+ $dompdf->stream(
+ sprintf(
+ '%s-%s-%d',
+ $request->getControllerName(),
+ $request->getActionName(),
+ time()
+ )
+ );
+ }
+}
diff --git a/library/Icinga/File/Storage/LocalFileStorage.php b/library/Icinga/File/Storage/LocalFileStorage.php
new file mode 100644
index 0000000..e1ed641
--- /dev/null
+++ b/library/Icinga/File/Storage/LocalFileStorage.php
@@ -0,0 +1,164 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File\Storage;
+
+use ErrorException;
+use Icinga\Exception\AlreadyExistsException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\NotReadableError;
+use Icinga\Exception\NotWritableError;
+use InvalidArgumentException;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use Traversable;
+use UnexpectedValueException;
+
+/**
+ * Stores files in the local file system
+ */
+class LocalFileStorage implements StorageInterface
+{
+ /**
+ * The root directory of this storage
+ *
+ * @var string
+ */
+ protected $baseDir;
+
+ /**
+ * Constructor
+ *
+ * @param string $baseDir The root directory of this storage
+ */
+ public function __construct($baseDir)
+ {
+ $this->baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR);
+ }
+
+ public function getIterator(): Traversable
+ {
+ try {
+ return new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator(
+ $this->baseDir,
+ RecursiveDirectoryIterator::CURRENT_AS_FILEINFO
+ | RecursiveDirectoryIterator::KEY_AS_PATHNAME
+ | RecursiveDirectoryIterator::SKIP_DOTS
+ )
+ );
+ } catch (UnexpectedValueException $e) {
+ throw new NotReadableError('Couldn\'t read the directory "%s": %s', $this->baseDir, $e);
+ }
+ }
+
+ public function has($path)
+ {
+ return is_file($this->resolvePath($path));
+ }
+
+ public function create($path, $content)
+ {
+ $resolvedPath = $this->resolvePath($path);
+
+ $this->ensureDir(dirname($resolvedPath));
+
+ try {
+ $stream = fopen($resolvedPath, 'x');
+ } catch (ErrorException $e) {
+ throw new AlreadyExistsException('Couldn\'t create the file "%s": %s', $path, $e);
+ }
+
+ try {
+ fclose($stream);
+ chmod($resolvedPath, 0664);
+ file_put_contents($resolvedPath, $content);
+ } catch (ErrorException $e) {
+ throw new NotWritableError('Couldn\'t create the file "%s": %s', $path, $e);
+ }
+
+ return $this;
+ }
+
+ public function read($path)
+ {
+ $resolvedPath = $this->resolvePath($path, true);
+
+ try {
+ return file_get_contents($resolvedPath);
+ } catch (ErrorException $e) {
+ throw new NotReadableError('Couldn\'t read the file "%s": %s', $path, $e);
+ }
+ }
+
+ public function update($path, $content)
+ {
+ $resolvedPath = $this->resolvePath($path, true);
+
+ try {
+ file_put_contents($resolvedPath, $content);
+ } catch (ErrorException $e) {
+ throw new NotWritableError('Couldn\'t update the file "%s": %s', $path, $e);
+ }
+
+ return $this;
+ }
+
+ public function delete($path)
+ {
+ $resolvedPath = $this->resolvePath($path, true);
+
+ try {
+ unlink($resolvedPath);
+ } catch (ErrorException $e) {
+ throw new NotWritableError('Couldn\'t delete the file "%s": %s', $path, $e);
+ }
+
+ return $this;
+ }
+
+ public function resolvePath($path, $assertExistence = false)
+ {
+ if ($assertExistence && ! $this->has($path)) {
+ throw new NotFoundError('No such file: "%s"', $path);
+ }
+
+ $steps = preg_split('~/~', $path, -1, PREG_SPLIT_NO_EMPTY);
+ for ($i = 0; $i < count($steps);) {
+ if ($steps[$i] === '.') {
+ array_splice($steps, $i, 1);
+ } elseif ($steps[$i] === '..' && $i > 0 && $steps[$i - 1] !== '..') {
+ array_splice($steps, $i - 1, 2);
+ --$i;
+ } else {
+ ++$i;
+ }
+ }
+
+ if ($steps[0] === '..') {
+ throw new InvalidArgumentException('Paths above the base directory are not allowed');
+ }
+
+ return $this->baseDir . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $steps);
+ }
+
+ /**
+ * Ensure that the given directory exists
+ *
+ * @param string $dir
+ *
+ * @throws NotWritableError
+ */
+ protected function ensureDir($dir)
+ {
+ if (! is_dir($dir)) {
+ $this->ensureDir(dirname($dir));
+
+ try {
+ mkdir($dir, 02770);
+ } catch (ErrorException $e) {
+ throw new NotWritableError('Couldn\'t create the directory "%s": %s', $dir, $e);
+ }
+ }
+ }
+}
diff --git a/library/Icinga/File/Storage/StorageInterface.php b/library/Icinga/File/Storage/StorageInterface.php
new file mode 100644
index 0000000..f416b00
--- /dev/null
+++ b/library/Icinga/File/Storage/StorageInterface.php
@@ -0,0 +1,94 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File\Storage;
+
+use Icinga\Exception\AlreadyExistsException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Exception\NotReadableError;
+use Icinga\Exception\NotWritableError;
+use IteratorAggregate;
+use Traversable;
+
+interface StorageInterface extends IteratorAggregate
+{
+ /**
+ * Iterate over all existing files' paths
+ *
+ * @return Traversable
+ *
+ * @throws NotReadableError If the file list can't be read
+ */
+ public function getIterator(): Traversable;
+
+ /**
+ * Return whether the given file exists
+ *
+ * @param string $path
+ *
+ * @return bool
+ */
+ public function has($path);
+
+ /**
+ * Create the given file with the given content
+ *
+ * @param string $path
+ * @param mixed $content
+ *
+ * @return $this
+ *
+ * @throws AlreadyExistsException If the file already exists
+ * @throws NotWritableError If the file can't be written to
+ */
+ public function create($path, $content);
+
+ /**
+ * Load the content of the given file
+ *
+ * @param string $path
+ *
+ * @return mixed
+ *
+ * @throws NotFoundError If the file can't be found
+ * @throws NotReadableError If the file can't be read
+ */
+ public function read($path);
+
+ /**
+ * Overwrite the given file with the given content
+ *
+ * @param string $path
+ * @param mixed $content
+ *
+ * @return $this
+ *
+ * @throws NotFoundError If the file can't be found
+ * @throws NotWritableError If the file can't be written to
+ */
+ public function update($path, $content);
+
+ /**
+ * Delete the given file
+ *
+ * @param string $path
+ *
+ * @return $this
+ *
+ * @throws NotFoundError If the file can't be found
+ * @throws NotWritableError If the file can't be deleted
+ */
+ public function delete($path);
+
+ /**
+ * Get the absolute path to the given file
+ *
+ * @param string $path
+ * @param bool $assertExistence Whether to require that the given file exists
+ *
+ * @return string
+ *
+ * @throws NotFoundError If the file has to exist, but can't be found
+ */
+ public function resolvePath($path, $assertExistence = false);
+}
diff --git a/library/Icinga/File/Storage/TemporaryLocalFileStorage.php b/library/Icinga/File/Storage/TemporaryLocalFileStorage.php
new file mode 100644
index 0000000..faf91f5
--- /dev/null
+++ b/library/Icinga/File/Storage/TemporaryLocalFileStorage.php
@@ -0,0 +1,59 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\File\Storage;
+
+use ErrorException;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+
+/**
+ * Stores files in a temporary directory
+ */
+class TemporaryLocalFileStorage extends LocalFileStorage
+{
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid();
+ mkdir($path, 0700);
+
+ parent::__construct($path);
+ }
+
+ /**
+ * Destructor
+ */
+ public function __destruct()
+ {
+ // Some classes may have cleaned up the tmp file, so we need to check this
+ // beforehand to prevent an unexpected crash.
+ if (! @realpath($this->baseDir)) {
+ return;
+ }
+
+ $directoryIterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator(
+ $this->baseDir,
+ RecursiveDirectoryIterator::CURRENT_AS_FILEINFO
+ | RecursiveDirectoryIterator::KEY_AS_PATHNAME
+ | RecursiveDirectoryIterator::SKIP_DOTS
+ ),
+ RecursiveIteratorIterator::CHILD_FIRST
+ );
+
+ foreach ($directoryIterator as $path => $entry) {
+ /** @var \SplFileInfo $entry */
+
+ if ($entry->isDir() && ! $entry->isLink()) {
+ rmdir($path);
+ } else {
+ unlink($path);
+ }
+ }
+
+ rmdir($this->baseDir);
+ }
+}
diff --git a/library/Icinga/Legacy/DashboardConfig.php b/library/Icinga/Legacy/DashboardConfig.php
new file mode 100644
index 0000000..3fb5c2f
--- /dev/null
+++ b/library/Icinga/Legacy/DashboardConfig.php
@@ -0,0 +1,137 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Legacy;
+
+use Icinga\Application\Config;
+use Icinga\User;
+use Icinga\Web\Navigation\DashboardPane;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Navigation\NavigationItem;
+
+/**
+ * Legacy dashboard config class for case insensitive interpretation of dashboard config files
+ *
+ * Before 2.2, the username part in dashboard config files was not lowered.
+ *
+ * @deprecated(el): Remove. TBD.
+ */
+class DashboardConfig extends Config
+{
+ /**
+ * User
+ *
+ * @var User
+ */
+ protected $user;
+
+ /**
+ * Get the user
+ *
+ * @return User
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * Set the user
+ *
+ * @param User $user
+ *
+ * @return $this
+ */
+ public function setUser($user)
+ {
+ $this->user = $user;
+ return $this;
+ }
+
+
+ /**
+ * List all dashboard configuration files that match the given user
+ *
+ * @param User $user
+ *
+ * @return string[]
+ */
+ public static function listConfigFilesForUser(User $user)
+ {
+ $files = array();
+ $dashboards = static::resolvePath('dashboards');
+ if ($handle = @opendir($dashboards)) {
+ while (false !== ($entry = readdir($handle))) {
+ if ($entry[0] === '.' || ! is_dir($dashboards . '/' . $entry)) {
+ continue;
+ }
+ if (strtolower($entry) === strtolower($user->getUsername())) {
+ $files[] = $dashboards . '/' . $entry . '/dashboard.ini';
+ }
+ }
+ closedir($handle);
+ }
+ return $files;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function saveIni($filePath = null, $fileMode = 0660)
+ {
+ // Preprocessing start, ensures that the non-translated names are used to save module dashboard changes
+ // TODO: This MUST NOT survive the new dashboard implementation (yes, it's still a thing..)
+ $dashboardNavigation = new Navigation();
+ $dashboardNavigation->load('dashboard-pane');
+ $getDashboardPane = function ($label) use ($dashboardNavigation) {
+ foreach ($dashboardNavigation as $dashboardPane) {
+ /** @var DashboardPane $dashboardPane */
+ if ($dashboardPane->getLabel() === $label) {
+ return $dashboardPane;
+ }
+
+ foreach ($dashboardPane->getChildren() as $dashlet) {
+ /** @var NavigationItem $dashlet */
+ if ($dashlet->getLabel() === $label) {
+ return $dashlet;
+ }
+ }
+ }
+ };
+
+ foreach (clone $this->config as $name => $options) {
+ if (strpos($name, '.') !== false) {
+ list($dashboardLabel, $dashletLabel) = explode('.', $name, 2);
+ } else {
+ $dashboardLabel = $name;
+ $dashletLabel = null;
+ }
+
+ $dashboardPane = $getDashboardPane($dashboardLabel);
+ if ($dashboardPane !== null) {
+ $dashboardLabel = $dashboardPane->getName();
+ }
+
+ if ($dashletLabel !== null) {
+ $dashletItem = $getDashboardPane($dashletLabel);
+ if ($dashletItem !== null) {
+ $dashletLabel = $dashletItem->getName();
+ }
+ }
+
+ unset($this->config[$name]);
+ $this->config[$dashboardLabel . ($dashletLabel ? '.' . $dashletLabel : '')] = $options;
+ }
+ // Preprocessing end
+
+ parent::saveIni($filePath, $fileMode);
+ if ($filePath === null) {
+ $filePath = $this->configFile;
+ }
+ foreach (static::listConfigFilesForUser($this->user) as $file) {
+ if ($file !== $filePath) {
+ @unlink($file);
+ }
+ }
+ }
+}
diff --git a/library/Icinga/Less/Call.php b/library/Icinga/Less/Call.php
new file mode 100644
index 0000000..0a78cb5
--- /dev/null
+++ b/library/Icinga/Less/Call.php
@@ -0,0 +1,77 @@
+<?php
+
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Less;
+
+use Less_Tree_Call;
+use Less_Tree_Color;
+use Less_Tree_Value;
+use Less_Tree_Variable;
+
+class Call extends Less_Tree_Call
+{
+ public static function fromCall(Less_Tree_Call $call)
+ {
+ return new static($call->name, $call->args, $call->index, $call->currentFileInfo);
+ }
+
+ public function compile($env = null)
+ {
+ if (! $env) {
+ // Not sure how to trigger this, but if there is no $env, there is nothing we can do
+ return parent::compile($env);
+ }
+
+ foreach ($this->args as $arg) {
+ if (! is_array($arg->value)) {
+ continue;
+ }
+
+ $name = null;
+ if ($arg->value[0] instanceof Less_Tree_Variable) {
+ // This is the case when defining a variable with a callable LESS rules such as fade, fadeout..
+ // Example: `@foo: #fff; @foo-bar: fade(@foo, 10);`
+ $name = $arg->value[0]->name;
+ } elseif ($arg->value[0] instanceof ColorPropOrVariable) {
+ // This is the case when defining a CSS rule using the LESS functions and passing
+ // a variable as an argument to them. Example: `... { color: fade(@foo, 10%); }`
+ $name = $arg->value[0]->getVariable()->name;
+ }
+
+ if ($name) {
+ foreach ($env->frames as $frame) {
+ if (($v = $frame->variable($name))) {
+ // Variables from the frame stack are always of type LESS Tree Rule
+ $vr = $v->value;
+ if ($vr instanceof Less_Tree_Value) {
+ // Get the actual color prop, otherwise this may cause an invalid argument error
+ $vr = $vr->compile($env);
+ }
+
+ if ($vr instanceof DeferredColorProp) {
+ if (! $vr->hasReference()) {
+ // Should never happen, though just for safety's sake
+ $vr->compile($env);
+ }
+
+ // Get the uppermost variable of the variable references
+ while (! $vr instanceof ColorProp) {
+ $vr = $vr->getRef();
+ }
+ } elseif ($vr instanceof Less_Tree_Color) {
+ $vr = ColorProp::fromColor($vr);
+ $vr->setName($name);
+ }
+
+ $arg->value[0] = $vr;
+
+ break;
+ }
+ }
+ }
+ }
+
+ return parent::compile($env);
+ }
+}
diff --git a/library/Icinga/Less/ColorProp.php b/library/Icinga/Less/ColorProp.php
new file mode 100644
index 0000000..3f83c5e
--- /dev/null
+++ b/library/Icinga/Less/ColorProp.php
@@ -0,0 +1,109 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Less;
+
+use Less_Tree_Call;
+use Less_Tree_Color;
+use Less_Tree_Keyword;
+
+/**
+ * ColorProp renders Less colors as CSS var() function calls
+ *
+ * It extends {@link Less_Tree_Color} so that Less functions that take a Less_Tree_Color as an argument do not fail.
+ */
+class ColorProp extends Less_Tree_Color
+{
+ /** @var Less_Tree_Color Color with which we created the ColorProp */
+ protected $color;
+
+ /** @var int */
+ protected $index;
+
+ /** @var string Color variable name */
+ protected $name;
+
+ public function __construct()
+ {
+ }
+
+ /**
+ * @param Less_Tree_Color $color
+ *
+ * @return static
+ */
+ public static function fromColor(Less_Tree_Color $color)
+ {
+ $self = new static();
+ $self->color = $color;
+
+ foreach ($color as $k => $v) {
+ if ($k === 'name') {
+ $self->setName($v); // Removes the @ char from the name
+ } else {
+ $self->$k = $v;
+ }
+ }
+
+ return $self;
+ }
+
+ /**
+ * @return int
+ */
+ public function getIndex()
+ {
+ return $this->index;
+ }
+
+ /**
+ * @param int $index
+ *
+ * @return $this
+ */
+ public function setIndex($index)
+ {
+ $this->index = $index;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ if ($name[0] === '@') {
+ $name = substr($name, 1);
+ }
+
+ $this->name = $name;
+
+ return $this;
+ }
+
+ public function genCSS($output)
+ {
+ $css = (new Less_Tree_Call(
+ 'var',
+ [
+ new Less_Tree_Keyword('--' . $this->getName()),
+ // Use the Less_Tree_Color with which we created the ColorProp so that we don't get into genCSS() loops.
+ $this->color
+ ],
+ $this->getIndex()
+ ))->toCSS();
+
+ $output->add($css);
+ }
+}
diff --git a/library/Icinga/Less/ColorPropOrVariable.php b/library/Icinga/Less/ColorPropOrVariable.php
new file mode 100644
index 0000000..7918674
--- /dev/null
+++ b/library/Icinga/Less/ColorPropOrVariable.php
@@ -0,0 +1,71 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Less;
+
+use Less_Tree;
+use Less_Tree_Color;
+use Less_Tree_Variable;
+
+/**
+ * Compile a Less variable to {@link ColorProp} if it is a color
+ */
+class ColorPropOrVariable extends Less_Tree
+{
+ public $type = 'Variable';
+
+ /** @var Less_Tree_Variable */
+ protected $variable;
+
+ /**
+ * @return Less_Tree_Variable
+ */
+ public function getVariable()
+ {
+ return $this->variable;
+ }
+
+ /**
+ * @param Less_Tree_Variable $variable
+ *
+ * @return $this
+ */
+ public function setVariable(Less_Tree_Variable $variable)
+ {
+ $this->variable = $variable;
+
+ return $this;
+ }
+
+ public function compile($env)
+ {
+ $v = $this->getVariable();
+
+ if ($v->name[1] === '@') {
+ // Evaluate variable variable as in Less_Tree_Variable:28.
+ $vv = new Less_Tree_Variable(substr($v->name, 1), $v->index + 1, $v->currentFileInfo);
+ // Overwrite the name so that the variable variable is not evaluated again.
+ $result = $vv->compile($env);
+ if ($result instanceof DeferredColorProp) {
+ $v->name = $result->name;
+ } else {
+ $v->name = '@' . $result->value;
+ }
+ }
+
+ $compiled = $v->compile($env);
+
+ if ($compiled instanceof ColorProp) {
+ // We may already have a ColorProp, which is the case with mixin calls.
+ return $compiled;
+ }
+
+ if ($compiled instanceof Less_Tree_Color) {
+ return ColorProp::fromColor($compiled)
+ ->setIndex($v->index)
+ ->setName($v->name);
+ }
+
+ return $compiled;
+ }
+}
diff --git a/library/Icinga/Less/DeferredColorProp.php b/library/Icinga/Less/DeferredColorProp.php
new file mode 100644
index 0000000..c9c39ad
--- /dev/null
+++ b/library/Icinga/Less/DeferredColorProp.php
@@ -0,0 +1,136 @@
+<?php
+
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Less;
+
+use Less_Exception_Compiler;
+use Less_Tree_Call;
+use Less_Tree_Color;
+use Less_Tree_Keyword;
+use Less_Tree_Value;
+use Less_Tree_Variable;
+
+class DeferredColorProp extends Less_Tree_Variable
+{
+ /** @var DeferredColorProp|ColorProp */
+ protected $reference;
+
+ protected $resolved = false;
+
+ public function __construct($name, $variable, $index = null, $currentFileInfo = null)
+ {
+ parent::__construct($name, $index, $currentFileInfo);
+
+ if ($variable instanceof Less_Tree_Variable) {
+ $this->reference = self::fromVariable($variable);
+ }
+ }
+
+ public function isResolved()
+ {
+ return $this->resolved;
+ }
+
+ public function getName()
+ {
+ $name = $this->name;
+ if ($this->name[0] === '@') {
+ $name = substr($this->name, 1);
+ }
+
+ return $name;
+ }
+
+ public function hasReference()
+ {
+ return $this->reference !== null;
+ }
+
+ public function getRef()
+ {
+ return $this->reference;
+ }
+
+ public function setReference($ref)
+ {
+ $this->reference = $ref;
+
+ return $this;
+ }
+
+ public static function fromVariable(Less_Tree_Variable $variable)
+ {
+ $static = new static($variable->name, $variable->index, $variable->currentFileInfo);
+ $static->evaluating = $variable->evaluating;
+ $static->type = $variable->type;
+
+ return $static;
+ }
+
+ public function compile($env)
+ {
+ if (! $this->hasReference()) {
+ // This is never supposed to happen, however, we might have a deferred color prop
+ // without a reference. In this case we can simply use the parent method.
+ return parent::compile($env);
+ }
+
+ if ($this->isResolved()) {
+ // The dependencies are already resolved, no need to traverse the frame stack over again!
+ return $this;
+ }
+
+ if ($this->evaluating) { // Just like the parent method
+ throw new Less_Exception_Compiler(
+ "Recursive variable definition for " . $this->name,
+ null,
+ $this->index,
+ $this->currentFileInfo
+ );
+ }
+
+ $this->evaluating = true;
+
+ foreach ($env->frames as $frame) {
+ if (($v = $frame->variable($this->getRef()->name))) {
+ $rv = $v->value;
+ if ($rv instanceof Less_Tree_Value) {
+ $rv = $rv->compile($env);
+ }
+
+ // As we are at it anyway, let's cast the tree color to our color prop as well!
+ if ($rv instanceof Less_Tree_Color) {
+ $rv = ColorProp::fromColor($rv);
+ $rv->setName($this->getRef()->getName());
+ }
+
+ $this->evaluating = false;
+ $this->resolved = true;
+ $this->setReference($rv);
+
+ break;
+ }
+ }
+
+ return $this;
+ }
+
+ public function genCSS($output)
+ {
+ if (! $this->hasReference()) {
+ return; // Nothing to generate
+ }
+
+ $css = (new Less_Tree_Call(
+ 'var',
+ [
+ new Less_Tree_Keyword('--' . $this->getName()),
+ $this->getRef() // Each of the references will be generated recursively
+ ],
+ $this->index
+ ))->toCSS();
+
+ $output->add($css);
+ }
+}
diff --git a/library/Icinga/Less/LightMode.php b/library/Icinga/Less/LightMode.php
new file mode 100644
index 0000000..b4b72a0
--- /dev/null
+++ b/library/Icinga/Less/LightMode.php
@@ -0,0 +1,128 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Less;
+
+use ArrayIterator;
+use InvalidArgumentException;
+use IteratorAggregate;
+use Less_Environment;
+use Traversable;
+
+/**
+ * Registry for light modes and the environments in which they are defined
+ */
+class LightMode implements IteratorAggregate
+{
+ /** @var array Mode environments as mode-environment pairs */
+ protected $envs = [];
+
+ /** @var array Assoc list of modes */
+ protected $modes = [];
+
+ /** @var array Mode selectors as mode-selector pairs */
+ protected $selectors = [];
+
+ /**
+ * @param string $mode
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException If the mode already exists
+ */
+ public function add($mode)
+ {
+ if (array_key_exists($mode, $this->modes)) {
+ throw new InvalidArgumentException("$mode already exists");
+ }
+
+ $this->modes[$mode] = true;
+
+ return $this;
+ }
+
+ /**
+ * @param string $mode
+ *
+ * @return Less_Environment
+ *
+ * @throws InvalidArgumentException If there is no environment for the given mode
+ */
+ public function getEnv($mode)
+ {
+ if (! isset($this->envs[$mode])) {
+ throw new InvalidArgumentException("$mode does not exist");
+ }
+
+ return $this->envs[$mode];
+ }
+
+ /**
+ * @param string $mode
+ * @param Less_Environment $env
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException If an environment for given the mode already exists
+ */
+ public function setEnv($mode, Less_Environment $env)
+ {
+ if (array_key_exists($mode, $this->envs)) {
+ throw new InvalidArgumentException("$mode already exists");
+ }
+
+ $this->envs[$mode] = $env;
+
+ return $this;
+ }
+
+ /**
+ * @param string $mode
+ *
+ * @return bool
+ */
+ public function hasSelector($mode)
+ {
+ return isset($this->selectors[$mode]);
+ }
+
+ /**
+ * @param string $mode
+ *
+ * @return string
+ *
+ * @throws InvalidArgumentException If there is no selector for the given mode
+ */
+ public function getSelector($mode)
+ {
+ if (! isset($this->selectors[$mode])) {
+ throw new InvalidArgumentException("$mode does not exist");
+ }
+
+ return $this->selectors[$mode];
+ }
+
+ /**
+ * @param string $mode
+ * @param string $selector
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException If a selector for given the mode already exists
+ */
+ public function setSelector($mode, $selector)
+ {
+ if (array_key_exists($mode, $this->selectors)) {
+ throw new InvalidArgumentException("$mode already exists");
+ }
+
+ $this->selectors[$mode] = $selector;
+
+ return $this;
+ }
+
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator(array_keys($this->modes));
+ }
+}
diff --git a/library/Icinga/Less/LightModeCall.php b/library/Icinga/Less/LightModeCall.php
new file mode 100644
index 0000000..d899e3c
--- /dev/null
+++ b/library/Icinga/Less/LightModeCall.php
@@ -0,0 +1,38 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Less;
+
+use Less_Environment;
+use Less_Tree_Ruleset;
+use Less_Tree_RulesetCall;
+
+/**
+ * Use the environment where the light mode was defined to evaluate the call
+ */
+class LightModeCall extends Less_Tree_RulesetCall
+{
+ use LightModeTrait;
+
+ /**
+ * @param Less_Tree_RulesetCall $c
+ *
+ * @return static
+ */
+ public static function fromRulesetCall(Less_Tree_RulesetCall $c)
+ {
+ return new static($c->variable);
+ }
+
+ /**
+ * @param Less_Environment $env
+ *
+ * @return Less_Tree_Ruleset
+ */
+ public function compile($env)
+ {
+ return parent::compile(
+ $env->copyEvalEnv(array_merge($env->frames, $this->getLightMode()->getEnv($this->variable)->frames))
+ );
+ }
+}
diff --git a/library/Icinga/Less/LightModeDefinition.php b/library/Icinga/Less/LightModeDefinition.php
new file mode 100644
index 0000000..929e95c
--- /dev/null
+++ b/library/Icinga/Less/LightModeDefinition.php
@@ -0,0 +1,75 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Less;
+
+use Less_Environment;
+use Less_Exception_Compiler;
+use Less_Tree_DetachedRuleset;
+use Less_Tree_Ruleset;
+
+/**
+ * Register the environment in which the light mode is defined
+ */
+class LightModeDefinition extends Less_Tree_DetachedRuleset
+{
+ use LightModeTrait;
+
+ /** @var string */
+ protected $name;
+
+ /**
+ * @param Less_Tree_DetachedRuleset $drs
+ *
+ * @return static
+ */
+ public static function fromDetachedRuleset(Less_Tree_DetachedRuleset $drs)
+ {
+ return new static($drs->ruleset, $drs->frames);
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * @param Less_Environment $env
+ *
+ * @return Less_Tree_DetachedRuleset
+ */
+ public function compile($env)
+ {
+ $drs = parent::compile($env);
+
+ /** @var $frame Less_Tree_Ruleset */
+ foreach ($env->frames as $frame) {
+ if ($frame->variable($this->getName())) {
+ if (! empty($frame->first_oelements) && ! isset($frame->first_oelements['.icinga-module'])) {
+ throw new Less_Exception_Compiler('Light mode definition not allowed in selectors');
+ }
+
+ break;
+ }
+ }
+
+ $this->getLightMode()->setEnv($this->getName(), $env->copyEvalEnv($env->frames));
+
+ return $drs;
+ }
+}
diff --git a/library/Icinga/Less/LightModeTrait.php b/library/Icinga/Less/LightModeTrait.php
new file mode 100644
index 0000000..d328265
--- /dev/null
+++ b/library/Icinga/Less/LightModeTrait.php
@@ -0,0 +1,30 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Less;
+
+trait LightModeTrait
+{
+ /** @var LightMode */
+ private $lightMode;
+
+ /**
+ * @return LightMode
+ */
+ public function getLightMode()
+ {
+ return $this->lightMode;
+ }
+
+ /**
+ * @param LightMode $lightMode
+ *
+ * @return $this
+ */
+ public function setLightMode(LightMode $lightMode)
+ {
+ $this->lightMode = $lightMode;
+
+ return $this;
+ }
+}
diff --git a/library/Icinga/Less/LightModeVisitor.php b/library/Icinga/Less/LightModeVisitor.php
new file mode 100644
index 0000000..35758b4
--- /dev/null
+++ b/library/Icinga/Less/LightModeVisitor.php
@@ -0,0 +1,26 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Less;
+
+use Less_VisitorReplacing;
+
+/**
+ * Ensure that light mode calls have access to the environment in which the mode was defined
+ */
+class LightModeVisitor extends Less_VisitorReplacing
+{
+ use LightModeTrait;
+
+ public $isPreVisitor = true;
+
+ public function visitRulesetCall($c)
+ {
+ return LightModeCall::fromRulesetCall($c)->setLightMode($this->getLightMode());
+ }
+
+ public function run($node)
+ {
+ return $this->visitObj($node);
+ }
+}
diff --git a/library/Icinga/Less/Visitor.php b/library/Icinga/Less/Visitor.php
new file mode 100644
index 0000000..c04a0eb
--- /dev/null
+++ b/library/Icinga/Less/Visitor.php
@@ -0,0 +1,233 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Less;
+
+use Less_Parser;
+use Less_Tree_Expression;
+use Less_Tree_Rule;
+use Less_Tree_Value;
+use Less_Tree_Variable;
+use Less_VisitorReplacing;
+use LogicException;
+use ReflectionProperty;
+
+/**
+ * Replace compiled Less colors with CSS var() function calls and inject light mode calls
+ *
+ * Color replacing basically works by replacing every visited Less variable with {@link ColorPropOrVariable},
+ * which is later compiled to {@link ColorProp} if it is a color.
+ *
+ * Light mode calls are generated from light mode definitions.
+ */
+class Visitor extends Less_VisitorReplacing
+{
+ const LIGHT_MODE_CSS = <<<'CSS'
+@media (min-height: @prefer-light-color-scheme), print,
+(prefers-color-scheme: light) and (min-height: @enable-color-preference) {
+ %s
+}
+CSS;
+
+ const LIGHT_MODE_NAME = 'light-mode';
+
+ public $isPreEvalVisitor = true;
+
+ /**
+ * Whether calling var() CSS function
+ *
+ * If that's the case, don't try to replace compiled Less colors with CSS var() function calls.
+ *
+ * @var bool|string
+ */
+ protected $callingVar = false;
+
+ /**
+ * Whether defining a variable
+ *
+ * If that's the case, don't try to replace compiled Less colors with CSS var() function calls.
+ *
+ * @var false|string
+ */
+ protected $definingVariable = false;
+
+ /** @var Less_Tree_Rule If defining a variable, determines the origin rule of the variable */
+ protected $variableOrigin;
+
+ /** @var LightMode Light mode registry */
+ protected $lightMode;
+
+ /** @var false|string Whether parsing module Less */
+ protected $moduleScope = false;
+
+ /** @var null|string CSS module selector if any */
+ protected $moduleSelector;
+
+ public function visitCall($c)
+ {
+ if ($c->name !== 'var') {
+ // We need to use our own tree call class , so that we can precompile the arguments before making
+ // the actual LESS function calls. Otherwise, it will produce lots of invalid argument exceptions!
+ $c = Call::fromCall($c);
+ }
+
+ return $c;
+ }
+
+ public function visitDetachedRuleset($drs)
+ {
+ if ($this->variableOrigin->name === '@' . static::LIGHT_MODE_NAME) {
+ $this->variableOrigin->name .= '-' . substr(sha1(uniqid(mt_rand(), true)), 0, 7);
+
+ $this->lightMode->add($this->variableOrigin->name);
+
+ if ($this->moduleSelector !== false) {
+ $this->lightMode->setSelector($this->variableOrigin->name, $this->moduleSelector);
+ }
+
+ $drs = LightModeDefinition::fromDetachedRuleset($drs)
+ ->setLightMode($this->lightMode)
+ ->setName($this->variableOrigin->name);
+ }
+
+ // Since a detached ruleset is a variable definition in the first place,
+ // just reset that we define a variable.
+ $this->definingVariable = false;
+
+ return $drs;
+ }
+
+ public function visitMixinCall($c)
+ {
+ // Less_Tree_Mixin_Call::accept() does not visit arguments, but we have to replace them if necessary.
+ foreach ($c->arguments as $a) {
+ $a['value'] = $this->visitObj($a['value']);
+ }
+
+ return $c;
+ }
+
+ public function visitMixinDefinition($m)
+ {
+ // Less_Tree_Mixin_Definition::accept() does not visit params, but we have to replace them if necessary.
+ foreach ($m->params as $p) {
+ if (! isset($p['value'])) {
+ continue;
+ }
+
+ $p['value'] = $this->visitObj($p['value']);
+ }
+
+ return $m;
+ }
+
+ public function visitRule($r)
+ {
+ if ($r->name[0] === '@' && $r->variable) {
+ if ($this->definingVariable !== false) {
+ throw new LogicException('Already defining a variable');
+ }
+
+ $this->definingVariable = spl_object_hash($r);
+ $this->variableOrigin = $r;
+
+ if ($r->value instanceof Less_Tree_Value) {
+ if ($r->value->value[0] instanceof Less_Tree_Expression) {
+ if ($r->value->value[0]->value[0] instanceof Less_Tree_Variable) {
+ // Transform the variable definition rule into our own class
+ $r->value->value[0]->value[0] = new DeferredColorProp($r->name, $r->value->value[0]->value[0]);
+ }
+ }
+ }
+ }
+
+ return $r;
+ }
+
+ public function visitRuleOut($r)
+ {
+ if ($this->definingVariable !== false && $this->definingVariable === spl_object_hash($r)) {
+ $this->definingVariable = false;
+ $this->variableOrigin = null;
+ }
+ }
+
+ public function visitRuleset($rs)
+ {
+ // Method is required, otherwise visitRulesetOut will not be called.
+ return $rs;
+ }
+
+ public function visitRulesetOut($rs)
+ {
+ if ($this->moduleScope !== false
+ && isset($rs->selectors)
+ && spl_object_hash($rs->selectors[0]) === $this->moduleScope
+ ) {
+ $this->moduleSelector = null;
+ $this->moduleScope = false;
+ }
+ }
+
+ public function visitSelector($s)
+ {
+ if ($s->_oelements_len === 2 && $s->_oelements[0] === '.icinga-module') {
+ $this->moduleSelector = implode('', $s->_oelements);
+ $this->moduleScope = spl_object_hash($s);
+ }
+
+ return $s;
+ }
+
+ public function visitVariable($v)
+ {
+ if ($this->definingVariable !== false) {
+ return $v;
+ }
+
+ return (new ColorPropOrVariable())
+ ->setVariable($v);
+ }
+
+ public function run($node)
+ {
+ $this->lightMode = new LightMode();
+
+ $evald = $this->visitObj($node);
+
+ // The visitor has registered all light modes in visitDetachedRuleset, but has not called them yet.
+ // Now the light mode calls are prepared with the appropriate CSS selectors.
+ $calls = [];
+ foreach ($this->lightMode as $mode) {
+ if ($this->lightMode->hasSelector($mode)) {
+ $calls[] = "{$this->lightMode->getSelector($mode)} {\n$mode();\n}";
+ } else {
+ $calls[] = "$mode();";
+ }
+ }
+
+ if (! empty($calls)) {
+ // Place and parse light mode calls into a new anonymous file,
+ // leaving the original Less in which the light modes were defined untouched.
+ $parser = (new Less_Parser())
+ ->parse(sprintf(static::LIGHT_MODE_CSS, implode("\n", $calls)));
+
+ // Because Less variables are block scoped,
+ // we can't just access the light mode definitions in the calls above.
+ // The LightModeVisitor ensures that all calls have access to the environment in which the mode was defined.
+ // Finally, the rules are merged so that the light mode calls are also rendered to CSS.
+ $rules = new ReflectionProperty(get_class($parser), 'rules');
+ $rules->setAccessible(true);
+ $evald->rules = array_merge(
+ $evald->rules,
+ (new LightModeVisitor())
+ ->setLightMode($this->lightMode)
+ ->visitArray($rules->getValue($parser))
+ );
+ // The LightModeVisitor is used explicitly here instead of using it as a plugin
+ // since we only need to process the newly created rules for the light mode calls.
+ }
+
+ return $evald;
+ }
+}
diff --git a/library/Icinga/Model/Schema.php b/library/Icinga/Model/Schema.php
new file mode 100644
index 0000000..465cce0
--- /dev/null
+++ b/library/Icinga/Model/Schema.php
@@ -0,0 +1,49 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Model;
+
+use DateTime;
+use ipl\Orm\Behavior\BoolCast;
+use ipl\Orm\Behavior\MillisecondTimestamp;
+use ipl\Orm\Behaviors;
+use ipl\Orm\Model;
+
+/**
+ * A database model for Icinga Web schema version table
+ *
+ * @property int $id Unique identifier of the database schema entries
+ * @property string $version The current schema version of Icinga Web
+ * @property DateTime $timestamp The insert/modify time of the schema entry
+ * @property bool $success Whether the database migration of the current version was successful
+ * @property ?string $reason The reason why the database migration has failed
+ */
+class Schema extends Model
+{
+ public function getTableName(): string
+ {
+ return 'icingaweb_schema';
+ }
+
+ public function getKeyName()
+ {
+ return 'id';
+ }
+
+ public function getColumns(): array
+ {
+ return [
+ 'version',
+ 'timestamp',
+ 'success',
+ 'reason'
+ ];
+ }
+
+ public function createBehaviors(Behaviors $behaviors): void
+ {
+ $behaviors->add(new BoolCast(['success']));
+ $behaviors->add(new MillisecondTimestamp(['timestamp']));
+ }
+}
diff --git a/library/Icinga/Protocol/Dns.php b/library/Icinga/Protocol/Dns.php
new file mode 100644
index 0000000..3d422d7
--- /dev/null
+++ b/library/Icinga/Protocol/Dns.php
@@ -0,0 +1,89 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol;
+
+/**
+ * Discover dns records using regular or reverse lookup
+ */
+class Dns
+{
+ /**
+ * Discover all service records on a given domain
+ *
+ * @param string $domain The domain to search
+ * @param string $service The type of the service, like for example 'ldaps' or 'ldap'
+ * @param string $protocol The transport protocol used by the service, defaults to 'tcp'
+ *
+ * @return array An array of all found service records
+ */
+ public static function getSrvRecords($domain, $service, $protocol = 'tcp')
+ {
+ $records = dns_get_record('_' . $service . '._' . $protocol . '.' . $domain, DNS_SRV);
+ return $records === false ? array() : $records;
+ }
+
+ /**
+ * Get all ldap records for the given domain
+ *
+ * @param string $query The domain to query
+ * @param int $type The type of DNS-entry to fetch, see
+ * http://www.php.net/manual/de/function.dns-get-record.php for available types
+ *
+ * @return array|null An array of record entries
+ */
+ public static function records($query, $type = DNS_ANY)
+ {
+ return dns_get_record($query, $type);
+ }
+
+ /**
+ * Reverse lookup all host names available on the given ip address
+ *
+ * @param string $ipAddress
+ * @param int $type
+ *
+ * @return array|null
+ */
+ public static function ptr($ipAddress, $type = DNS_ANY)
+ {
+ $host = gethostbyaddr($ipAddress);
+ if ($host === false || $host === $ipAddress) {
+ // malformed input or no host found
+ return null;
+ }
+ return self::records($host, $type);
+ }
+
+ /**
+ * Get the IPv4 address of the given hostname.
+ *
+ * @param $hostname The hostname to resolve
+ *
+ * @return string|null The IPv4 address of the given hostname or null, when no entry exists.
+ */
+ public static function ipv4($hostname)
+ {
+ $records = dns_get_record($hostname, DNS_A);
+ if ($records !== false && count($records) > 0) {
+ return $records[0]['ip'];
+ }
+ return null;
+ }
+
+ /**
+ * Get the IPv6 address of the given hostname.
+ *
+ * @param $hostname The hostname to resolve
+ *
+ * @return string|null The IPv6 address of the given hostname or null, when no entry exists.
+ */
+ public static function ipv6($hostname)
+ {
+ $records = dns_get_record($hostname, DNS_AAAA);
+ if ($records !== false && count($records) > 0) {
+ return $records[0]['ip'];
+ }
+ return null;
+ }
+}
diff --git a/library/Icinga/Protocol/File/Exception/FileReaderException.php b/library/Icinga/Protocol/File/Exception/FileReaderException.php
new file mode 100644
index 0000000..237352c
--- /dev/null
+++ b/library/Icinga/Protocol/File/Exception/FileReaderException.php
@@ -0,0 +1,12 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+namespace Icinga\Protocol\File;
+
+use Icinga\Exception\IcingaException;
+
+/**
+ * Exception thrown if a file reader specific error occurs
+ */
+class FileReaderException extends IcingaException
+{
+}
diff --git a/library/Icinga/Protocol/File/FileIterator.php b/library/Icinga/Protocol/File/FileIterator.php
new file mode 100644
index 0000000..64b6600
--- /dev/null
+++ b/library/Icinga/Protocol/File/FileIterator.php
@@ -0,0 +1,81 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol\File;
+
+use Icinga\Util\EnumeratingFilterIterator;
+use Icinga\Util\File;
+
+/**
+ * Class FileIterator
+ *
+ * Iterate over a file, yielding only fields of non-empty lines which match a PCRE expression
+ */
+class FileIterator extends EnumeratingFilterIterator
+{
+ /**
+ * A PCRE string with the fields to extract from the file's lines as named subpatterns
+ *
+ * @var string
+ */
+ protected $fields;
+
+ /**
+ * An associative array of the current line's fields ($field => $value)
+ *
+ * @var array
+ */
+ protected $currentData;
+
+ public function __construct($filename, $fields)
+ {
+ $this->fields = $fields;
+ $f = new File($filename);
+ $f->setFlags(
+ File::DROP_NEW_LINE |
+ File::READ_AHEAD |
+ File::SKIP_EMPTY
+ );
+ parent::__construct($f);
+ }
+
+ /**
+ * Return the current data
+ *
+ * @return array
+ */
+ public function current(): array
+ {
+ return $this->currentData;
+ }
+
+ /**
+ * Accept lines matching the given PCRE pattern
+ *
+ * @return bool
+ *
+ * @throws FileReaderException If PHP failed parsing the PCRE pattern
+ */
+ public function accept(): bool
+ {
+ $data = array();
+ $matched = preg_match(
+ $this->fields,
+ $this->getInnerIterator()->current(),
+ $data
+ );
+
+ if ($matched === false) {
+ throw new FileReaderException('Failed parsing regular expression!');
+ } elseif ($matched === 1) {
+ foreach ($data as $key => $value) {
+ if (is_int($key)) {
+ unset($data[$key]);
+ }
+ }
+ $this->currentData = $data;
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/library/Icinga/Protocol/File/FileQuery.php b/library/Icinga/Protocol/File/FileQuery.php
new file mode 100644
index 0000000..504de2e
--- /dev/null
+++ b/library/Icinga/Protocol/File/FileQuery.php
@@ -0,0 +1,86 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol\File;
+
+use Icinga\Data\SimpleQuery;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Class FileQuery
+ *
+ * Query for Datasource Icinga\Protocol\File\FileReader
+ *
+ * @package Icinga\Protocol\File
+ */
+class FileQuery extends SimpleQuery
+{
+ /**
+ * Sort direction
+ *
+ * @var int
+ */
+ private $sortDir;
+
+ /**
+ * Filters to apply on result
+ *
+ * @var array
+ */
+ private $filters = array();
+
+ /**
+ * Nothing to do here
+ */
+ public function applyFilter(Filter $filter)
+ {
+ }
+
+ /**
+ * Sort query result chronological
+ *
+ * @param string $dir Sort direction, 'ASC' or 'DESC' (default)
+ *
+ * @return FileQuery
+ */
+ public function order($field, $direction = null)
+ {
+ $this->sortDir = (
+ $direction === null || strtoupper(trim($direction)) === 'DESC'
+ ) ? self::SORT_DESC : self::SORT_ASC;
+ return $this;
+ }
+
+ /**
+ * Return true if sorting descending, false otherwise
+ *
+ * @return bool
+ */
+ public function sortDesc()
+ {
+ return $this->sortDir === self::SORT_DESC;
+ }
+
+ /**
+ * Add an mandatory filter expression to be applied on this query
+ *
+ * @param string $expression the filter expression to be applied
+ *
+ * @return FileQuery
+ */
+ public function andWhere($expression)
+ {
+ $this->filters[] = $expression;
+ return $this;
+ }
+
+ /**
+ * Get filters currently applied on this query
+ *
+ * @return array
+ */
+ public function getFilters()
+ {
+ return $this->filters;
+ }
+}
diff --git a/library/Icinga/Protocol/File/FileReader.php b/library/Icinga/Protocol/File/FileReader.php
new file mode 100644
index 0000000..a06494c
--- /dev/null
+++ b/library/Icinga/Protocol/File/FileReader.php
@@ -0,0 +1,208 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol\File;
+
+use Countable;
+use ArrayIterator;
+use Icinga\Data\Selectable;
+use Icinga\Data\ConfigObject;
+
+/**
+ * Read file line by line
+ */
+class FileReader implements Selectable, Countable
+{
+ /**
+ * A PCRE string with the fields to extract from the file's lines as named subpatterns
+ *
+ * @var string
+ */
+ protected $fields;
+
+ /**
+ * Name of the target file
+ *
+ * @var string
+ */
+ protected $filename;
+
+ /**
+ * Cache for static::count()
+ *
+ * @var int
+ */
+ protected $count = null;
+
+ /**
+ * Create a new reader
+ *
+ * @param ConfigObject $config
+ *
+ * @throws FileReaderException If a required $config directive (filename or fields) is missing
+ */
+ public function __construct(ConfigObject $config)
+ {
+ foreach (array('filename', 'fields') as $key) {
+ if (isset($config->{$key})) {
+ $this->{$key} = $config->{$key};
+ } else {
+ throw new FileReaderException('The directive `%s\' is required', $key);
+ }
+ }
+ }
+
+ /**
+ * Instantiate a FileIterator object with the target file
+ *
+ * @return FileIterator
+ */
+ public function iterate()
+ {
+ return new LogFileIterator($this->filename, $this->fields);
+ }
+
+ /**
+ * Instantiate a FileQuery object
+ *
+ * @return FileQuery
+ */
+ public function select()
+ {
+ return new FileQuery($this);
+ }
+
+ /**
+ * Fetch and return all rows of the given query's result set using an iterator
+ *
+ * @param FileQuery $query
+ *
+ * @return ArrayIterator
+ */
+ public function query(FileQuery $query)
+ {
+ return new ArrayIterator($this->fetchAll($query));
+ }
+
+ /**
+ * Return the number of available valid lines.
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ if ($this->count === null) {
+ $this->count = iterator_count($this->iterate());
+ }
+ return $this->count;
+ }
+
+ /**
+ * Fetch result as an array of objects
+ *
+ * @param FileQuery $query
+ *
+ * @return array
+ */
+ public function fetchAll(FileQuery $query)
+ {
+ $all = array();
+ foreach ($this->fetchPairs($query) as $index => $value) {
+ $all[$index] = (object) $value;
+ }
+ return $all;
+ }
+
+ /**
+ * Fetch result as a key/value pair array
+ *
+ * @param FileQuery $query
+ *
+ * @return array
+ */
+ public function fetchPairs(FileQuery $query)
+ {
+ $skip = $query->getOffset();
+ $read = $query->getLimit();
+ if ($skip === null) {
+ $skip = 0;
+ }
+ $lines = array();
+ if ($query->sortDesc()) {
+ $count = $this->count();
+ if ($count <= $skip) {
+ return $lines;
+ } elseif ($count < ($skip + $read)) {
+ $read = $count - $skip;
+ $skip = 0;
+ } else {
+ $skip = $count - ($skip + $read);
+ }
+ }
+ foreach ($this->iterate() as $index => $line) {
+ if ($index >= $skip) {
+ if ($index >= $skip + $read) {
+ break;
+ }
+ $lines[] = $line;
+ }
+ }
+ if ($query->sortDesc()) {
+ $lines = array_reverse($lines);
+ }
+ return $lines;
+ }
+
+ /**
+ * Fetch first result row
+ *
+ * @param FileQuery $query
+ *
+ * @return object
+ */
+ public function fetchRow(FileQuery $query)
+ {
+ $all = $this->fetchAll($query);
+ if (isset($all[0])) {
+ return $all[0];
+ }
+ return null;
+ }
+
+ /**
+ * Fetch first result column
+ *
+ * @param FileQuery $query
+ *
+ * @return array
+ */
+ public function fetchColumn(FileQuery $query)
+ {
+ $column = array();
+ foreach ($this->fetchPairs($query) as $pair) {
+ foreach ($pair as $value) {
+ $column[] = $value;
+ break;
+ }
+ }
+ return $column;
+ }
+
+ /**
+ * Fetch first column value from first result row
+ *
+ * @param FileQuery $query
+ *
+ * @return mixed
+ */
+ public function fetchOne(FileQuery $query)
+ {
+ $pairs = $this->fetchPairs($query);
+ if (isset($pairs[0])) {
+ foreach ($pairs[0] as $value) {
+ return $value;
+ }
+ }
+ return null;
+ }
+}
diff --git a/library/Icinga/Protocol/File/LogFileIterator.php b/library/Icinga/Protocol/File/LogFileIterator.php
new file mode 100644
index 0000000..67a4d99
--- /dev/null
+++ b/library/Icinga/Protocol/File/LogFileIterator.php
@@ -0,0 +1,149 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol\File;
+
+use Icinga\Exception\IcingaException;
+use SplFileObject;
+use Iterator;
+
+/**
+ * Iterate over a log file, yielding the regex fields of the log messages
+ */
+class LogFileIterator implements Iterator
+{
+ /**
+ * Log file
+ *
+ * @var SplFileObject
+ */
+ protected $file;
+
+ /**
+ * A PCRE string with the fields to extract
+ * from the log messages as named subpatterns
+ *
+ * @var string
+ */
+ protected $fields;
+
+ /**
+ * Value for static::current()
+ *
+ * @var array
+ */
+ protected $current;
+
+ /**
+ * Index for static::key()
+ *
+ * @var int
+ */
+ protected $index;
+
+ /**
+ * Value for static::valid()
+ *
+ * @var boolean
+ */
+ protected $valid;
+
+ /**
+ * @var string
+ */
+ protected $next = null;
+
+ /**
+ * @param string $filename The log file's name
+ * @param string $fields A PCRE string with the fields to extract
+ * from the log messages as named subpatterns
+ */
+ public function __construct($filename, $fields)
+ {
+ $this->file = new SplFileObject($filename);
+ $this->file->setFlags(
+ SplFileObject::DROP_NEW_LINE |
+ SplFileObject::READ_AHEAD
+ );
+ $this->fields = $fields;
+ }
+
+ public function rewind(): void
+ {
+ $this->file->rewind();
+ $this->index = 0;
+ $this->nextMessage();
+ }
+
+ public function next(): void
+ {
+ $this->file->next();
+ ++$this->index;
+ $this->nextMessage();
+ }
+
+ public function current(): array
+ {
+ return $this->current;
+ }
+
+ public function key(): int
+ {
+ return $this->index;
+ }
+
+ public function valid(): bool
+ {
+ return $this->valid;
+ }
+
+ protected function nextMessage()
+ {
+ $message = $this->next === null ? array() : array($this->next);
+ $this->valid = null;
+ while ($this->file->valid()) {
+ if (false === ($res = preg_match(
+ $this->fields,
+ $current = $this->file->current()
+ ))) {
+ throw new IcingaException('Failed at preg_match()');
+ }
+ if (empty($message)) {
+ if ($res === 1) {
+ $message[] = $current;
+ }
+ } elseif ($res === 1) {
+ $this->next = $current;
+ $this->valid = true;
+ break;
+ } else {
+ $message[] = $current;
+ }
+
+ $this->file->next();
+ }
+ if ($this->valid === null) {
+ $this->next = null;
+ $this->valid = ! empty($message);
+ }
+
+ if ($this->valid) {
+ while (! empty($message)) {
+ $matches = array();
+ if (false === ($res = preg_match(
+ $this->fields,
+ implode(PHP_EOL, $message),
+ $matches
+ ))) {
+ throw new IcingaException('Failed at preg_match()');
+ }
+ if ($res === 1) {
+ $this->current = $matches;
+ return;
+ }
+ array_pop($message);
+ }
+ $this->valid = false;
+ }
+ }
+}
diff --git a/library/Icinga/Protocol/Ldap/Discovery.php b/library/Icinga/Protocol/Ldap/Discovery.php
new file mode 100644
index 0000000..9c7990a
--- /dev/null
+++ b/library/Icinga/Protocol/Ldap/Discovery.php
@@ -0,0 +1,143 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol\Ldap;
+
+use Icinga\Data\ConfigObject;
+use Icinga\Protocol\Dns;
+
+class Discovery
+{
+ /**
+ * @var LdapConnection
+ */
+ private $connection;
+
+ /**
+ * @param LdapConnection $conn The ldap connection to use for the discovery
+ */
+ public function __construct(LdapConnection $conn)
+ {
+ $this->connection = $conn;
+ }
+
+ /**
+ * Suggests a resource configuration of hostname, port and root_dn
+ * based on the discovery
+ *
+ * @return array The suggested configuration as an array
+ */
+ public function suggestResourceSettings()
+ {
+ return array(
+ 'hostname' => $this->connection->getHostname(),
+ 'port' => $this->connection->getPort(),
+ 'root_dn' => $this->connection->getCapabilities()->getDefaultNamingContext()
+ );
+ }
+
+ /**
+ * Suggests a backend configuration of base_dn, user_class and user_name_attribute
+ * based on the discovery
+ *
+ * @return array The suggested configuration as an array
+ */
+ public function suggestBackendSettings()
+ {
+ if ($this->isAd()) {
+ return array(
+ 'backend' => 'msldap',
+ 'base_dn' => $this->connection->getCapabilities()->getDefaultNamingContext(),
+ 'user_class' => 'user',
+ 'user_name_attribute' => 'sAMAccountName'
+ );
+ } else {
+ return array(
+ 'backend' => 'ldap',
+ 'base_dn' => $this->connection->getCapabilities()->getDefaultNamingContext(),
+ 'user_class' => 'inetOrgPerson',
+ 'user_name_attribute' => 'uid'
+ );
+ }
+ }
+
+ /**
+ * Whether the suggested ldap server is an ActiveDirectory
+ *
+ * @return boolean
+ */
+ public function isAd()
+ {
+ return $this->connection->getCapabilities()->isActiveDirectory();
+ }
+
+ /**
+ * Whether the discovery was successful
+ *
+ * @return bool False when the suggestions are guessed
+ */
+ public function isSuccess()
+ {
+ return $this->connection->discoverySuccessful();
+ }
+
+ /**
+ * Why the discovery failed
+ *
+ * @return \Exception|null
+ */
+ public function getError()
+ {
+ return $this->connection->getDiscoveryError();
+ }
+
+ /**
+ * Discover LDAP servers on the given domain
+ *
+ * @param ?string $domain The object containing the form elements
+ *
+ * @return Discovery True when the discovery was successful, false when the configuration was guessed
+ */
+ public static function discoverDomain($domain)
+ {
+ if (! isset($domain)) {
+ return false;
+ }
+
+ // Attempt 1: Connect to the domain directly
+ $disc = Discovery::discover($domain, 389);
+ if ($disc->isSuccess()) {
+ return $disc;
+ }
+
+ // Attempt 2: Discover all available ldap dns records and connect to the first one
+ $records = array_merge(Dns::getSrvRecords($domain, 'ldap'), Dns::getSrvRecords($domain, 'ldaps'));
+ if (isset($records[0])) {
+ $record = $records[0];
+ return Discovery::discover(
+ isset($record['target']) ? $record['target'] : $domain,
+ isset($record['port']) ? $record['port'] : $domain
+ );
+ }
+
+ // Return the first failed discovery, which will suggest properties based on guesses
+ return $disc;
+ }
+
+ /**
+ * Convenience method to instantiate a new Discovery
+ *
+ * @param $host The host on which to execute the discovery
+ * @param $port The port on which to execute the discovery
+ *
+ * @return Discovery The resulting Discovery
+ */
+ public static function discover($host, $port)
+ {
+ $conn = new LdapConnection(new ConfigObject(array(
+ 'hostname' => $host,
+ 'port' => $port
+ )));
+ return new Discovery($conn);
+ }
+}
diff --git a/library/Icinga/Protocol/Ldap/LdapCapabilities.php b/library/Icinga/Protocol/Ldap/LdapCapabilities.php
new file mode 100644
index 0000000..721655a
--- /dev/null
+++ b/library/Icinga/Protocol/Ldap/LdapCapabilities.php
@@ -0,0 +1,440 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol\Ldap;
+
+use Icinga\Application\Logger;
+use stdClass;
+
+/**
+ * The properties and capabilities of an LDAP server
+ *
+ * Provides information about the available encryption mechanisms (StartTLS), the supported
+ * LDAP protocol (v2/v3), vendor-specific extensions or protocols controls and extensions.
+ */
+class LdapCapabilities
+{
+ const LDAP_SERVER_START_TLS_OID = '1.3.6.1.4.1.1466.20037';
+
+ const LDAP_PAGED_RESULT_OID_STRING = '1.2.840.113556.1.4.319';
+
+ const LDAP_SERVER_SHOW_DELETED_OID = '1.2.840.113556.1.4.417';
+
+ const LDAP_SERVER_SORT_OID = '1.2.840.113556.1.4.473';
+
+ const LDAP_SERVER_CROSSDOM_MOVE_TARGET_OID = '1.2.840.113556.1.4.521';
+
+ const LDAP_SERVER_NOTIFICATION_OID = '1.2.840.113556.1.4.528';
+
+ const LDAP_SERVER_EXTENDED_DN_OID = '1.2.840.113556.1.4.529';
+
+ const LDAP_SERVER_LAZY_COMMIT_OID = '1.2.840.113556.1.4.619';
+
+ const LDAP_SERVER_SD_FLAGS_OID = '1.2.840.113556.1.4.801';
+
+ const LDAP_SERVER_TREE_DELETE_OID = '1.2.840.113556.1.4.805';
+
+ const LDAP_SERVER_DIRSYNC_OID = '1.2.840.113556.1.4.841';
+
+ const LDAP_SERVER_VERIFY_NAME_OID = '1.2.840.113556.1.4.1338';
+
+ const LDAP_SERVER_DOMAIN_SCOPE_OID = '1.2.840.113556.1.4.1339';
+
+ const LDAP_SERVER_SEARCH_OPTIONS_OID = '1.2.840.113556.1.4.1340';
+
+ const LDAP_SERVER_PERMISSIVE_MODIFY_OID = '1.2.840.113556.1.4.1413';
+
+ const LDAP_SERVER_ASQ_OID = '1.2.840.113556.1.4.1504';
+
+ const LDAP_SERVER_FAST_BIND_OID = '1.2.840.113556.1.4.1781';
+
+ const LDAP_CONTROL_VLVREQUEST = '2.16.840.1.113730.3.4.9';
+
+
+ // MS Capabilities, Source: http://msdn.microsoft.com/en-us/library/cc223359.aspx
+
+ // Running Active Directory as AD DS
+ const LDAP_CAP_ACTIVE_DIRECTORY_OID = '1.2.840.113556.1.4.800';
+
+ // Capable of signing and sealing on an NTLM authenticated connection
+ // and of performing subsequent binds on a signed or sealed connection
+ const LDAP_CAP_ACTIVE_DIRECTORY_LDAP_INTEG_OID = '1.2.840.113556.1.4.1791';
+
+ // If AD DS: running at least W2K3, if AD LDS running at least W2K8
+ const LDAP_CAP_ACTIVE_DIRECTORY_V51_OID = '1.2.840.113556.1.4.1670';
+
+ // If AD LDS: accepts DIGEST-MD5 binds for AD LDSsecurity principals
+ const LDAP_CAP_ACTIVE_DIRECTORY_ADAM_DIGEST = '1.2.840.113556.1.4.1880';
+
+ // Running Active Directory as AD LDS
+ const LDAP_CAP_ACTIVE_DIRECTORY_ADAM_OID = '1.2.840.113556.1.4.1851';
+
+ // If AD DS: it's a Read Only DC (RODC)
+ const LDAP_CAP_ACTIVE_DIRECTORY_PARTIAL_SECRETS_OID = '1.2.840.113556.1.4.1920';
+
+ // Running at least W2K8
+ const LDAP_CAP_ACTIVE_DIRECTORY_V60_OID = '1.2.840.113556.1.4.1935';
+
+ // Running at least W2K8r2
+ const LDAP_CAP_ACTIVE_DIRECTORY_V61_R2_OID = '1.2.840.113556.1.4.2080';
+
+ // Running at least W2K12
+ const LDAP_CAP_ACTIVE_DIRECTORY_W8_OID = '1.2.840.113556.1.4.2237';
+
+ /**
+ * Attributes of the LDAP Server returned by the discovery query
+ *
+ * @var stdClass
+ */
+ private $attributes;
+
+ /**
+ * Map of supported available OIDS
+ *
+ * @var array
+ */
+ private $oids;
+
+ /**
+ * Construct a new capability
+ *
+ * @param $attributes stdClass The attributes returned, may be null for guessing default capabilities
+ */
+ public function __construct($attributes = null)
+ {
+ $this->setAttributes($attributes);
+ }
+
+ /**
+ * Set the attributes and (re)build the OIDs
+ *
+ * @param $attributes stdClass The attributes returned, may be null for guessing default capabilities
+ */
+ protected function setAttributes($attributes)
+ {
+ $this->attributes = $attributes;
+ $this->oids = array();
+
+ $keys = array('supportedControl', 'supportedExtension', 'supportedFeatures', 'supportedCapabilities');
+ foreach ($keys as $key) {
+ if (isset($attributes->$key)) {
+ if (is_array($attributes->$key)) {
+ foreach ($attributes->$key as $oid) {
+ $this->oids[$oid] = true;
+ }
+ } else {
+ $this->oids[$attributes->$key] = true;
+ }
+ }
+ }
+ }
+
+ /**
+ * Return if the capability object contains support for StartTLS
+ *
+ * @return bool Whether StartTLS is supported
+ */
+ public function hasStartTls()
+ {
+ return isset($this->oids[self::LDAP_SERVER_START_TLS_OID]);
+ }
+
+ /**
+ * Return if the capability object contains support for paged results
+ *
+ * @return bool Whether StartTLS is supported
+ */
+ public function hasPagedResult()
+ {
+ return isset($this->oids[self::LDAP_PAGED_RESULT_OID_STRING]);
+ }
+
+ /**
+ * Whether the ldap server is an ActiveDirectory server
+ *
+ * @return boolean
+ */
+ public function isActiveDirectory()
+ {
+ return isset($this->oids[self::LDAP_CAP_ACTIVE_DIRECTORY_OID]);
+ }
+
+ /**
+ * Whether the ldap server is an OpenLDAP server
+ *
+ * @return bool
+ */
+ public function isOpenLdap()
+ {
+ return isset($this->attributes->structuralObjectClass) &&
+ $this->attributes->structuralObjectClass === 'OpenLDAProotDSE';
+ }
+
+ /**
+ * Return if the capability objects contains support for LdapV3, defaults to true if discovery failed
+ *
+ * @return bool
+ */
+ public function hasLdapV3()
+ {
+ if (! isset($this->attributes) || ! isset($this->attributes->supportedLDAPVersion)) {
+ // Default to true, if unknown
+ return true;
+ }
+
+ return (is_string($this->attributes->supportedLDAPVersion)
+ && (int) $this->attributes->supportedLDAPVersion === 3)
+ || (is_array($this->attributes->supportedLDAPVersion)
+ && in_array(3, $this->attributes->supportedLDAPVersion));
+ }
+
+ /**
+ * Whether the capability with the given OID is supported
+ *
+ * @param $oid string The OID of the capability
+ *
+ * @return bool
+ */
+ public function hasOid($oid)
+ {
+ return isset($this->oids[$oid]);
+ }
+
+ /**
+ * Get the default naming context
+ *
+ * @return string|null the default naming context, or null when no contexts are available
+ */
+ public function getDefaultNamingContext()
+ {
+ // defaultNamingContext entry has higher priority
+ if (isset($this->attributes->defaultNamingContext)) {
+ return $this->attributes->defaultNamingContext;
+ }
+
+ // if its missing use namingContext
+ $namingContexts = $this->namingContexts();
+ return empty($namingContexts) ? null : $namingContexts[0];
+ }
+
+ /**
+ * Get the configuration naming context
+ *
+ * @return string|null
+ */
+ public function getConfigurationNamingContext()
+ {
+ if (isset($this->attributes->configurationNamingContext)) {
+ return $this->attributes->configurationNamingContext;
+ }
+ }
+
+ /**
+ * Get the NetBIOS name
+ *
+ * @return string|null
+ */
+ public function getNetBiosName()
+ {
+ if (isset($this->attributes->nETBIOSName)) {
+ return $this->attributes->nETBIOSName;
+ }
+ }
+
+ /**
+ * Fetch the namingContexts
+ *
+ * @return array the available naming contexts
+ */
+ public function namingContexts()
+ {
+ if (!isset($this->attributes->namingContexts)) {
+ return array();
+ }
+ if (!is_array($this->attributes->namingContexts)) {
+ return array($this->attributes->namingContexts);
+ }
+ return$this->attributes->namingContexts;
+ }
+
+ public function getVendor()
+ {
+ /*
+ rfc #3045 specifies that the name of the server MAY be included in the attribute 'verndorName',
+ AD and OpenLDAP don't do this, but for all all other vendors we follow the standard and
+ just hope for the best.
+ */
+
+ if ($this->isActiveDirectory()) {
+ return 'Microsoft Active Directory';
+ }
+
+ if ($this->isOpenLdap()) {
+ return 'OpenLDAP';
+ }
+
+ if (! isset($this->attributes->vendorName)) {
+ return null;
+ }
+ return $this->attributes->vendorName;
+ }
+
+ public function getVersion()
+ {
+ /*
+ rfc #3045 specifies that the version of the server MAY be included in the attribute 'vendorVersion',
+ but AD and OpenLDAP don't do this. For OpenLDAP there is no way to query the server versions, but for all
+ all other vendors we follow the standard and just hope for the best.
+ */
+
+ if ($this->isActiveDirectory()) {
+ return $this->getAdObjectVersionName();
+ }
+
+ if (! isset($this->attributes->vendorVersion)) {
+ return null;
+ }
+ return $this->attributes->vendorVersion;
+ }
+
+ /**
+ * Discover the capabilities of the given LDAP server
+ *
+ * @param LdapConnection $connection The ldap connection to use
+ *
+ * @return LdapCapabilities
+ *
+ * @throws LdapException In case the capability query has failed
+ */
+ public static function discoverCapabilities(LdapConnection $connection)
+ {
+ $ds = $connection->getConnection();
+
+ $fields = array(
+ 'configurationNamingContext',
+ 'defaultNamingContext',
+ 'namingContexts',
+ 'vendorName',
+ 'vendorVersion',
+ 'supportedSaslMechanisms',
+ 'dnsHostName',
+ 'schemaNamingContext',
+ 'supportedLDAPVersion', // => array(3, 2)
+ 'supportedCapabilities',
+ 'supportedControl',
+ 'supportedExtension',
+ 'objectVersion',
+ '+'
+ );
+
+ $result = @ldap_read($ds, '', (string) $connection->select()->from('*', $fields), $fields);
+ if (! $result) {
+ throw new LdapException(
+ 'Capability query failed (%s; Default port: %d): %s. Check if hostname and port'
+ . ' of the ldap resource are correct and if anonymous access is permitted.',
+ $connection->getHostname(),
+ $connection->getPort(),
+ ldap_error($ds)
+ );
+ }
+
+ $entry = ldap_first_entry($ds, $result);
+ if ($entry === false) {
+ throw new LdapException(
+ 'Capabilities not available (%s; Default port: %d): %s. Discovery of root DSE probably not permitted.',
+ $connection->getHostname(),
+ $connection->getPort(),
+ ldap_error($ds)
+ );
+ }
+
+ $cap = new LdapCapabilities($connection->cleanupAttributes(ldap_get_attributes($ds, $entry), $fields));
+ $cap->discoverAdConfigOptions($connection);
+
+ if (isset($cap->attributes) && Logger::getInstance()->getLevel() === Logger::DEBUG) {
+ Logger::debug('Capability query discovered the following attributes:');
+ foreach ($cap->attributes as $name => $value) {
+ if ($value !== null) {
+ Logger::debug(' %s = %s', $name, $value);
+ }
+ }
+ Logger::debug('Capability query attribute listing ended.');
+ }
+
+ return $cap;
+ }
+
+ /**
+ * Discover the AD-specific configuration options of the given LDAP server
+ *
+ * @param LdapConnection $connection The ldap connection to use
+ *
+ * @throws LdapException In case the configuration options query has failed
+ */
+ protected function discoverAdConfigOptions(LdapConnection $connection)
+ {
+ if ($this->isActiveDirectory()) {
+ $configurationNamingContext = $this->getConfigurationNamingContext();
+ $defaultNamingContext = $this->getDefaultNamingContext();
+ if (!($configurationNamingContext === null || $defaultNamingContext === null)) {
+ $ds = $connection->bind()->getConnection();
+ $adFields = array('nETBIOSName');
+ $partitions = 'CN=Partitions,' . $configurationNamingContext;
+
+ $result = @ldap_list(
+ $ds,
+ $partitions,
+ (string) $connection->select()->from('*', $adFields)->where('nCName', $defaultNamingContext),
+ $adFields
+ );
+ if ($result) {
+ $entry = ldap_first_entry($ds, $result);
+ if ($entry === false) {
+ throw new LdapException(
+ 'Configuration options not available (%s:%d). Discovery of "%s" probably not permitted.',
+ $connection->getHostname(),
+ $connection->getPort(),
+ $partitions
+ );
+ }
+
+ $this->setAttributes((object) array_merge(
+ (array) $this->attributes,
+ (array) $connection->cleanupAttributes(ldap_get_attributes($ds, $entry), $adFields)
+ ));
+ } else {
+ if (ldap_errno($ds) !== 1) {
+ // One stands for "operations error" which occurs if not bound non-anonymously.
+
+ throw new LdapException(
+ 'Configuration options query failed (%s:%d): %s. Check if hostname and port of the'
+ . ' ldap resource are correct and if anonymous access is permitted.',
+ $connection->getHostname(),
+ $connection->getPort(),
+ ldap_error($ds)
+ );
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Determine the active directory version using the available capabillities
+ *
+ * @return null|string The server version description or null when unknown
+ */
+ protected function getAdObjectVersionName()
+ {
+ if (isset($this->oids[self::LDAP_CAP_ACTIVE_DIRECTORY_W8_OID])) {
+ return 'Windows Server 2012 (or newer)';
+ }
+ if (isset($this->oids[self::LDAP_CAP_ACTIVE_DIRECTORY_V61_R2_OID])) {
+ return 'Windows Server 2008 R2 (or newer)';
+ }
+ if (isset($this->oids[self::LDAP_CAP_ACTIVE_DIRECTORY_V60_OID])) {
+ return 'Windows Server 2008 (or newer)';
+ }
+ return null;
+ }
+}
diff --git a/library/Icinga/Protocol/Ldap/LdapConnection.php b/library/Icinga/Protocol/Ldap/LdapConnection.php
new file mode 100644
index 0000000..a620e6d
--- /dev/null
+++ b/library/Icinga/Protocol/Ldap/LdapConnection.php
@@ -0,0 +1,1584 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol\Ldap;
+
+use ArrayIterator;
+use Exception;
+use Icinga\Data\Filter\FilterNot;
+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\Exception\ProgrammingError;
+use Icinga\Web\Url;
+
+/**
+ * Encapsulate LDAP connections and query creation
+ */
+class LdapConnection implements Selectable, Inspectable
+{
+ /**
+ * Indicates that the target object cannot be found
+ *
+ * @var int
+ */
+ const LDAP_NO_SUCH_OBJECT = 32;
+
+ /**
+ * Indicates that in a search operation, the size limit specified by the client or the server has been exceeded
+ *
+ * @var int
+ */
+ const LDAP_SIZELIMIT_EXCEEDED = 4;
+
+ /**
+ * Indicates that an LDAP server limit set by an administrative authority has been exceeded
+ *
+ * @var int
+ */
+ const LDAP_ADMINLIMIT_EXCEEDED = 11;
+
+ /**
+ * Indicates that during a bind operation one of the following occurred: The client passed either an incorrect DN
+ * or password, or the password is incorrect because it has expired, intruder detection has locked the account, or
+ * another similar reason.
+ *
+ * @var int
+ */
+ const LDAP_INVALID_CREDENTIALS = 49;
+
+ /**
+ * The default page size to use for paged queries
+ *
+ * @var int
+ */
+ const PAGE_SIZE = 1000;
+
+ /**
+ * Encrypt connection using STARTTLS (upgrading a plain text connection)
+ *
+ * @var string
+ */
+ const STARTTLS = 'starttls';
+
+ /**
+ * Encrypt connection using LDAP over SSL (using a separate port)
+ *
+ * @var string
+ */
+ const LDAPS = 'ldaps';
+
+ /** @var ConfigObject Connection configuration */
+ protected $config;
+
+ /**
+ * Encryption for the connection if any
+ *
+ * @var string
+ */
+ protected $encryption;
+
+ /**
+ * The LDAP link identifier being used
+ *
+ * @var resource
+ */
+ protected $ds;
+
+ /**
+ * The ip address, hostname or ldap URI being used to connect with the LDAP server
+ *
+ * @var string
+ */
+ protected $hostname;
+
+ /**
+ * The port being used to connect with the LDAP server
+ *
+ * @var int
+ */
+ protected $port;
+
+ /**
+ * The distinguished name being used to bind to the LDAP server
+ *
+ * @var string
+ */
+ protected $bindDn;
+
+ /**
+ * The password being used to bind to the LDAP server
+ *
+ * @var string
+ */
+ protected $bindPw;
+
+ /**
+ * The distinguished name being used as the base path for queries which do not provide one theirselves
+ *
+ * @var string
+ */
+ protected $rootDn;
+
+ /**
+ * Whether the bind on this connection has already been performed
+ *
+ * @var bool
+ */
+ protected $bound;
+
+ /**
+ * The current connection's root node
+ *
+ * @var Root
+ */
+ protected $root;
+
+ /**
+ * LDAP_OPT_NETWORK_TIMEOUT for the LDAP connection
+ *
+ * @var int
+ */
+ protected $timeout;
+
+ /**
+ * The properties and capabilities of the LDAP server
+ *
+ * @var LdapCapabilities
+ */
+ protected $capabilities;
+
+ /**
+ * Whether discovery was successful
+ *
+ * @var bool
+ */
+ protected $discoverySuccess;
+
+ /**
+ * The cause of the discovery's failure
+ *
+ * @var Exception|null
+ */
+ private $discoveryError;
+
+ /**
+ * Whether the current connection is encrypted
+ *
+ * @var bool
+ */
+ protected $encrypted = null;
+
+ /**
+ * Create a new connection object
+ *
+ * @param ConfigObject $config
+ */
+ public function __construct(ConfigObject $config)
+ {
+ $this->config = $config;
+ $this->hostname = $config->hostname;
+ $this->bindDn = $config->bind_dn;
+ $this->bindPw = $config->bind_pw;
+ $this->rootDn = $config->root_dn;
+ $this->port = (int) $config->get('port', 389);
+ $this->timeout = (int) $config->get('timeout', 5);
+
+ $this->encryption = $config->encryption;
+ if ($this->encryption !== null) {
+ $this->encryption = strtolower($this->encryption);
+ }
+ }
+
+ /**
+ * Return the ip address, hostname or ldap URI being used to connect with the LDAP server
+ *
+ * @return string
+ */
+ public function getHostname()
+ {
+ return $this->hostname;
+ }
+
+ /**
+ * Return the port being used to connect with the LDAP server
+ *
+ * @return int
+ */
+ public function getPort()
+ {
+ return $this->port;
+ }
+
+ /**
+ * Return the distinguished name being used as the base path for queries which do not provide one theirselves
+ *
+ * @return string
+ */
+ public function getDn()
+ {
+ return $this->rootDn;
+ }
+
+ /**
+ * Return the root node for this connection
+ *
+ * @return Root
+ */
+ public function root()
+ {
+ if ($this->root === null) {
+ $this->root = Root::forConnection($this);
+ }
+
+ return $this->root;
+ }
+
+ /**
+ * Return the LDAP link identifier being used
+ *
+ * Establishes a connection if necessary.
+ *
+ * @return resource
+ */
+ public function getConnection()
+ {
+ if ($this->ds === null) {
+ $this->ds = $this->prepareNewConnection();
+ }
+
+ return $this->ds;
+ }
+
+ /**
+ * Return the capabilities of the current connection
+ *
+ * @return LdapCapabilities
+ */
+ public function getCapabilities()
+ {
+ if ($this->capabilities === null) {
+ try {
+ $this->capabilities = LdapCapabilities::discoverCapabilities($this);
+ $this->discoverySuccess = true;
+ $this->discoveryError = null;
+ } catch (LdapException $e) {
+ Logger::debug($e);
+ Logger::warning('LADP discovery failed, assuming default LDAP capabilities.');
+ $this->capabilities = new LdapCapabilities(); // create empty default capabilities
+ $this->discoverySuccess = false;
+ $this->discoveryError = $e;
+ }
+ }
+
+ return $this->capabilities;
+ }
+
+ /**
+ * Return whether discovery was successful
+ *
+ * @return bool true if the capabilities were successfully determined, false if the capabilities were guessed
+ */
+ public function discoverySuccessful()
+ {
+ if ($this->discoverySuccess === null) {
+ $this->getCapabilities(); // Initializes self::$discoverySuccess
+ }
+
+ return $this->discoverySuccess;
+ }
+
+ /**
+ * Get discovery error if any
+ *
+ * @return Exception|null
+ */
+ public function getDiscoveryError()
+ {
+ return $this->discoveryError;
+ }
+
+ /**
+ * Return whether the current connection is encrypted
+ *
+ * @return bool
+ */
+ public function isEncrypted()
+ {
+ if ($this->encrypted === null) {
+ return false;
+ }
+
+ return $this->encrypted;
+ }
+
+ /**
+ * Perform a LDAP bind on the current connection
+ *
+ * @throws LdapException In case the LDAP bind was unsuccessful or insecure
+ */
+ public function bind()
+ {
+ if ($this->bound) {
+ return $this;
+ }
+
+ $ds = $this->getConnection();
+
+ $success = @ldap_bind($ds, $this->bindDn, $this->bindPw);
+ if (! $success) {
+ throw new LdapException(
+ 'LDAP bind (%s / %s) to %s failed: %s',
+ $this->bindDn,
+ '***' /* $this->bindPw */,
+ $this->normalizeHostname($this->hostname),
+ ldap_error($ds)
+ );
+ }
+
+ $this->bound = true;
+ return $this;
+ }
+
+ /**
+ * Provide a query on this connection
+ *
+ * @return LdapQuery
+ */
+ public function select()
+ {
+ return new LdapQuery($this);
+ }
+
+ /**
+ * Fetch and return all rows of the given query's result set using an iterator
+ *
+ * @param LdapQuery $query The query returning the result set
+ *
+ * @return ArrayIterator
+ */
+ public function query(LdapQuery $query)
+ {
+ return new ArrayIterator($this->fetchAll($query));
+ }
+
+ /**
+ * Count all rows of the given query's result set
+ *
+ * @param LdapQuery $query The query returning the result set
+ *
+ * @return int
+ */
+ public function count(LdapQuery $query)
+ {
+ $this->bind();
+
+ if (($unfoldAttribute = $query->getUnfoldAttribute()) !== null) {
+ $desiredColumns = $query->getColumns();
+ if (isset($desiredColumns[$unfoldAttribute])) {
+ $fields = array($unfoldAttribute => $desiredColumns[$unfoldAttribute]);
+ } elseif (in_array($unfoldAttribute, $desiredColumns, true)) {
+ $fields = array($unfoldAttribute);
+ } else {
+ throw new ProgrammingError(
+ 'The attribute used to unfold a query\'s result must be selected'
+ );
+ }
+
+ $res = $this->runQuery($query, $fields);
+ return count($res);
+ }
+
+ $ds = $this->getConnection();
+ $results = $this->ldapSearch($query, array('dn'));
+
+ if ($results === false) {
+ if (ldap_errno($ds) !== self::LDAP_NO_SUCH_OBJECT) {
+ throw new LdapException(
+ 'LDAP count query "%s" (base %s) failed: %s',
+ (string) $query,
+ $query->getBase() ?: $this->getDn(),
+ ldap_error($ds)
+ );
+ }
+ }
+
+ return ldap_count_entries($ds, $results);
+ }
+
+ /**
+ * Retrieve an array containing all rows of the result set
+ *
+ * @param LdapQuery $query The query returning the result set
+ * @param array $fields Request these attributes instead of the ones registered in the given query
+ *
+ * @return array
+ */
+ public function fetchAll(LdapQuery $query, array $fields = null)
+ {
+ $this->bind();
+
+ if ($query->getUsePagedResults() && $this->getCapabilities()->hasPagedResult()) {
+ return $this->runPagedQuery($query, $fields);
+ } else {
+ return $this->runQuery($query, $fields);
+ }
+ }
+
+ /**
+ * Fetch the first row of the result set
+ *
+ * @param LdapQuery $query The query returning the result set
+ * @param array $fields Request these attributes instead of the ones registered in the given query
+ *
+ * @return mixed
+ */
+ public function fetchRow(LdapQuery $query, array $fields = null)
+ {
+ $clonedQuery = clone $query;
+ $clonedQuery->limit(1);
+ $clonedQuery->setUsePagedResults(false);
+ $results = $this->fetchAll($clonedQuery, $fields);
+ return array_shift($results) ?: false;
+ }
+
+ /**
+ * Fetch the first column of all rows of the result set as an array
+ *
+ * @param LdapQuery $query The query returning the result set
+ * @param array $fields Request these attributes instead of the ones registered in the given query
+ *
+ * @return array
+ *
+ * @throws ProgrammingError In case no attribute is being requested
+ */
+ public function fetchColumn(LdapQuery $query, array $fields = null)
+ {
+ if ($fields === null) {
+ $fields = $query->getColumns();
+ }
+
+ if (empty($fields)) {
+ throw new ProgrammingError('You must request at least one attribute when fetching a single column');
+ }
+
+ $alias = key($fields);
+ $results = $this->fetchAll($query, array($alias => current($fields)));
+ $column = is_int($alias) ? current($fields) : $alias;
+ $values = array();
+ foreach ($results as $row) {
+ if (isset($row->$column)) {
+ $values[] = $row->$column;
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Fetch the first column of the first row of the result set
+ *
+ * @param LdapQuery $query The query returning the result set
+ * @param array $fields Request these attributes instead of the ones registered in the given query
+ *
+ * @return string
+ */
+ public function fetchOne(LdapQuery $query, array $fields = null)
+ {
+ $row = $this->fetchRow($query, $fields);
+ if ($row === false) {
+ return false;
+ }
+
+ $values = get_object_vars($row);
+ if (empty($values)) {
+ return false;
+ }
+
+ if ($fields === null) {
+ // Fetch the desired columns from the query if not explicitly overriden in the method's parameter
+ $fields = $query->getColumns();
+ }
+
+ if (empty($fields)) {
+ // The desired columns may be empty independently whether provided by the query or the method's parameter
+ return array_shift($values);
+ }
+
+ $alias = key($fields);
+ return $values[is_string($alias) ? $alias : $fields[$alias]];
+ }
+
+ /**
+ * Fetch all rows of the result set as an array of key-value pairs
+ *
+ * The first column is the key, the second column is the value.
+ *
+ * @param LdapQuery $query The query returning the result set
+ * @param array $fields Request these attributes instead of the ones registered in the given query
+ *
+ * @return array
+ *
+ * @throws ProgrammingError In case there are less than two attributes being requested
+ */
+ public function fetchPairs(LdapQuery $query, array $fields = null)
+ {
+ if ($fields === null) {
+ $fields = $query->getColumns();
+ }
+
+ if (count($fields) < 2) {
+ throw new ProgrammingError('You are required to request at least two attributes');
+ }
+
+ $columns = $desiredColumnNames = array();
+ foreach ($fields as $alias => $column) {
+ if (is_int($alias)) {
+ $columns[] = $column;
+ $desiredColumnNames[] = $column;
+ } else {
+ $columns[$alias] = $column;
+ $desiredColumnNames[] = $alias;
+ }
+
+ if (count($desiredColumnNames) === 2) {
+ break;
+ }
+ }
+
+ $results = $this->fetchAll($query, $columns);
+ $pairs = array();
+ foreach ($results as $row) {
+ $colOne = $desiredColumnNames[0];
+ $colTwo = $desiredColumnNames[1];
+ $pairs[$row->$colOne] = $row->$colTwo;
+ }
+
+ 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
+ *
+ * @param string $bindDn
+ * @param string $bindPw
+ *
+ * @return bool Whether the given credentials are valid
+ *
+ * @throws LdapException In case an error occured while establishing the connection or attempting the bind
+ */
+ public function testCredentials($bindDn, $bindPw)
+ {
+ $ds = $this->getConnection();
+ $success = @ldap_bind($ds, $bindDn, $bindPw);
+ if (! $success) {
+ if (ldap_errno($ds) === self::LDAP_INVALID_CREDENTIALS) {
+ Logger::debug(
+ 'Testing LDAP credentials (%s / %s) failed: %s',
+ $bindDn,
+ '***',
+ ldap_error($ds)
+ );
+ return false;
+ }
+
+ throw new LdapException(ldap_error($ds));
+ }
+
+ return true;
+ }
+
+ /**
+ * Return whether an entry identified by the given distinguished name exists
+ *
+ * @param string $dn
+ *
+ * @return bool
+ */
+ public function hasDn($dn)
+ {
+ $ds = $this->getConnection();
+ $this->bind();
+
+ $result = ldap_read($ds, $dn, '(objectClass=*)', array('objectClass'));
+ return ldap_count_entries($ds, $result) > 0;
+ }
+
+ /**
+ * Delete a root entry and all of its children identified by the given distinguished name
+ *
+ * @param string $dn
+ *
+ * @return bool
+ *
+ * @throws LdapException In case an error occured while deleting an entry
+ */
+ public function deleteRecursively($dn)
+ {
+ $ds = $this->getConnection();
+ $this->bind();
+
+ $result = @ldap_list($ds, $dn, '(objectClass=*)', array('objectClass'));
+ if ($result === false) {
+ if (ldap_errno($ds) === self::LDAP_NO_SUCH_OBJECT) {
+ return false;
+ }
+
+ throw new LdapException('LDAP list for "%s" failed: %s', $dn, ldap_error($ds));
+ }
+
+ $children = ldap_get_entries($ds, $result);
+ for ($i = 0; $i < $children['count']; $i++) {
+ $result = $this->deleteRecursively($children[$i]['dn']);
+ if (! $result) {
+ // TODO: return result code, if delete fails
+ throw new LdapException('Recursively deleting "%s" failed', $dn);
+ }
+ }
+
+ return $this->deleteDn($dn);
+ }
+
+ /**
+ * Delete a single entry identified by the given distinguished name
+ *
+ * @param string $dn
+ *
+ * @return bool
+ *
+ * @throws LdapException In case an error occured while deleting the entry
+ */
+ public function deleteDn($dn)
+ {
+ $ds = $this->getConnection();
+ $this->bind();
+
+ $result = @ldap_delete($ds, $dn);
+ if ($result === false) {
+ if (ldap_errno($ds) === self::LDAP_NO_SUCH_OBJECT) {
+ return false; // TODO: Isn't it a success if something i'd like to remove is not existing at all???
+ }
+
+ throw new LdapException('LDAP delete for "%s" failed: %s', $dn, ldap_error($ds));
+ }
+
+ return true;
+ }
+
+ /**
+ * Fetch the distinguished name of the result of the given query
+ *
+ * @param LdapQuery $query The query returning the result set
+ *
+ * @return string The distinguished name, or false when the given query yields no results
+ *
+ * @throws LdapException In case the query yields multiple results
+ */
+ public function fetchDn(LdapQuery $query)
+ {
+ $rows = $this->fetchAll($query, array());
+ if (count($rows) > 1) {
+ throw new LdapException('Cannot fetch single DN for %s', $query);
+ }
+
+ return key($rows);
+ }
+
+ /**
+ * Run the given LDAP query and return the resulting entries
+ *
+ * @param LdapQuery $query The query to fetch results with
+ * @param array $fields Request these attributes instead of the ones registered in the given query
+ *
+ * @return array
+ *
+ * @throws LdapException In case an error occured while fetching the results
+ */
+ protected function runQuery(LdapQuery $query, array $fields = null)
+ {
+ $limit = $query->getLimit();
+ $offset = $query->hasOffset() ? $query->getOffset() : 0;
+
+ if ($fields === null) {
+ $fields = $query->getColumns();
+ }
+
+ $ds = $this->getConnection();
+
+ $serverSorting = ! $this->config->disable_server_side_sort
+ && $this->getCapabilities()->hasOid(LdapCapabilities::LDAP_SERVER_SORT_OID);
+
+ if ($query->hasOrder()) {
+ if ($serverSorting) {
+ ldap_set_option($ds, LDAP_OPT_SERVER_CONTROLS, array(
+ array(
+ 'oid' => LdapCapabilities::LDAP_SERVER_SORT_OID,
+ 'value' => $this->encodeSortRules($query->getOrder())
+ )
+ ));
+ } elseif (! empty($fields)) {
+ foreach ($query->getOrder() as $rule) {
+ if (! in_array($rule[0], $fields, true)) {
+ $fields[] = $rule[0];
+ }
+ }
+ }
+ }
+
+ $unfoldAttribute = $query->getUnfoldAttribute();
+ if ($unfoldAttribute) {
+ foreach ($query->getFilter()->listFilteredColumns() as $filterColumn) {
+ $fieldKey = array_search($filterColumn, $fields, true);
+ if ($fieldKey === false || is_string($fieldKey)) {
+ $fields[] = $filterColumn;
+ }
+ }
+ }
+
+ $results = $this->ldapSearch(
+ $query,
+ array_values($fields),
+ 0,
+ ($serverSorting || ! $query->hasOrder()) && $limit ? $offset + $limit : 0
+ );
+ if ($results === false) {
+ if (ldap_errno($ds) === self::LDAP_NO_SUCH_OBJECT) {
+ return array();
+ }
+
+ throw new LdapException(
+ 'LDAP query "%s" (base %s) failed. Error: %s',
+ $query,
+ $query->getBase() ?: $this->rootDn,
+ ldap_error($ds)
+ );
+ } elseif (ldap_count_entries($ds, $results) === 0) {
+ return array();
+ }
+
+ $count = 0;
+ $entries = array();
+ $entry = ldap_first_entry($ds, $results);
+ do {
+ if ($unfoldAttribute) {
+ $rows = $this->cleanupAttributes(ldap_get_attributes($ds, $entry), $fields, $unfoldAttribute);
+ if (is_array($rows)) {
+ // TODO: Register the DN the same way as a section name in the ArrayDatasource!
+ foreach ($rows as $row) {
+ if ($query->getFilter()->matches($row)) {
+ $count += 1;
+ if (! $serverSorting || $offset === 0 || $offset < $count) {
+ $entries[] = $row;
+ }
+
+ if ($serverSorting && $limit > 0 && $limit === count($entries)) {
+ break;
+ }
+ }
+ }
+ } else {
+ $count += 1;
+ if (! $serverSorting || $offset === 0 || $offset < $count) {
+ $entries[ldap_get_dn($ds, $entry)] = $rows;
+ }
+ }
+ } else {
+ $count += 1;
+ if (! $serverSorting || $offset === 0 || $offset < $count) {
+ $entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes(
+ ldap_get_attributes($ds, $entry),
+ $fields
+ );
+ }
+ }
+ } while ((! $serverSorting || $limit === 0 || $limit !== count($entries))
+ && ($entry = ldap_next_entry($ds, $entry))
+ );
+
+ if (! $serverSorting) {
+ if ($query->hasOrder()) {
+ uasort($entries, array($query, 'compare'));
+ }
+
+ if ($limit && $count > $limit) {
+ $entries = array_splice($entries, $query->hasOffset() ? $query->getOffset() : 0, $limit);
+ }
+ }
+
+ ldap_free_result($results);
+ return $entries;
+ }
+
+ /**
+ * Run the given LDAP query and return the resulting entries
+ *
+ * This utilizes paged search requests as defined in RFC 2696.
+ *
+ * @param LdapQuery $query The query to fetch results with
+ * @param array $fields Request these attributes instead of the ones registered in the given query
+ * @param int $pageSize The maximum page size, defaults to self::PAGE_SIZE
+ *
+ * @return array
+ *
+ * @throws LdapException In case an error occured while fetching the results
+ */
+ protected function runPagedQuery(LdapQuery $query, array $fields = null, $pageSize = null)
+ {
+ if ($pageSize === null) {
+ $pageSize = static::PAGE_SIZE;
+ }
+
+ $limit = $query->getLimit();
+ $offset = $query->hasOffset() ? $query->getOffset() : 0;
+
+ if ($fields === null) {
+ $fields = $query->getColumns();
+ }
+
+ $ds = $this->getConnection();
+
+ $serverSorting = false;//$this->getCapabilities()->hasOid(LdapCapabilities::LDAP_SERVER_SORT_OID);
+ if (! $serverSorting && $query->hasOrder() && ! empty($fields)) {
+ foreach ($query->getOrder() as $rule) {
+ if (! in_array($rule[0], $fields, true)) {
+ $fields[] = $rule[0];
+ }
+ }
+ }
+
+ $unfoldAttribute = $query->getUnfoldAttribute();
+ if ($unfoldAttribute) {
+ foreach ($query->getFilter()->listFilteredColumns() as $filterColumn) {
+ $fieldKey = array_search($filterColumn, $fields, true);
+ if ($fieldKey === false || is_string($fieldKey)) {
+ $fields[] = $filterColumn;
+ }
+ }
+ }
+
+ $controls = [];
+ $legacyControlHandling = version_compare(PHP_VERSION, '7.3.0') < 0;
+ if ($serverSorting && $query->hasOrder()) {
+ $control = [
+ 'oid' => LDAP_CONTROL_SORTREQUEST,
+ 'value' => $this->encodeSortRules($query->getOrder())
+ ];
+ if ($legacyControlHandling) {
+ ldap_set_option($ds, LDAP_OPT_SERVER_CONTROLS, [$control]);
+ } else {
+ $controls[LDAP_CONTROL_SORTREQUEST] = $control;
+ }
+ }
+
+ $count = 0;
+ $cookie = '';
+ $entries = array();
+ do {
+ if ($legacyControlHandling) {
+ // Do not request the pagination control as a critical extension, as we want the
+ // server to return results even if the paged search request cannot be satisfied
+ ldap_control_paged_result($ds, $pageSize, false, $cookie);
+ } else {
+ $controls[LDAP_CONTROL_PAGEDRESULTS] = [
+ 'oid' => LDAP_CONTROL_PAGEDRESULTS,
+ 'iscritical' => false, // See above
+ 'value' => [
+ 'size' => $pageSize,
+ 'cookie' => $cookie
+ ]
+ ];
+ }
+
+ $results = $this->ldapSearch(
+ $query,
+ array_values($fields),
+ 0,
+ ($serverSorting || ! $query->hasOrder()) && $limit ? $offset + $limit : 0,
+ 0,
+ LDAP_DEREF_NEVER,
+ empty($controls) ? null : $controls
+ );
+ if ($results === false) {
+ if (ldap_errno($ds) === self::LDAP_NO_SUCH_OBJECT) {
+ break;
+ }
+
+ throw new LdapException(
+ 'LDAP query "%s" (base %s) failed. Error: %s',
+ (string) $query,
+ $query->getBase() ?: $this->getDn(),
+ ldap_error($ds)
+ );
+ } elseif (ldap_count_entries($ds, $results) === 0) {
+ if (in_array(
+ ldap_errno($ds),
+ array(static::LDAP_SIZELIMIT_EXCEEDED, static::LDAP_ADMINLIMIT_EXCEEDED),
+ true
+ )) {
+ Logger::warning(
+ 'Unable to request more than %u results. Does the server allow paged search requests? (%s)',
+ $count,
+ ldap_error($ds)
+ );
+ }
+
+ break;
+ }
+
+ $entry = ldap_first_entry($ds, $results);
+ do {
+ if ($unfoldAttribute) {
+ $rows = $this->cleanupAttributes(ldap_get_attributes($ds, $entry), $fields, $unfoldAttribute);
+ if (is_array($rows)) {
+ // TODO: Register the DN the same way as a section name in the ArrayDatasource!
+ foreach ($rows as $row) {
+ if ($query->getFilter()->matches($row)) {
+ $count += 1;
+ if (! $serverSorting || $offset === 0 || $offset < $count) {
+ $entries[] = $row;
+ }
+
+ if ($serverSorting && $limit > 0 && $limit === count($entries)) {
+ break;
+ }
+ }
+ }
+ } else {
+ $count += 1;
+ if (! $serverSorting || $offset === 0 || $offset < $count) {
+ $entries[ldap_get_dn($ds, $entry)] = $rows;
+ }
+ }
+ } else {
+ $count += 1;
+ if (! $serverSorting || $offset === 0 || $offset < $count) {
+ $entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes(
+ ldap_get_attributes($ds, $entry),
+ $fields
+ );
+ }
+ }
+ } while ((! $serverSorting || $limit === 0 || $limit !== count($entries))
+ && ($entry = ldap_next_entry($ds, $entry))
+ );
+
+ if ($legacyControlHandling) {
+ if (false === @ldap_control_paged_result_response($ds, $results, $cookie)) {
+ // If the page size is greater than or equal to the sizeLimit value, the server should ignore the
+ // control as the request can be satisfied in a single page: https://www.ietf.org/rfc/rfc2696.txt
+ // This applies no matter whether paged search requests are permitted or not. You're done once you
+ // got everything you were out for.
+ if ($serverSorting && count($entries) !== $limit) {
+ // The server does not support pagination, but still returned a response by ignoring the
+ // pagedResultsControl. We output a warning to indicate that the pagination control was ignored.
+ Logger::warning(
+ 'Unable to request paged LDAP results. Does the server allow paged search requests?'
+ );
+ }
+ }
+ } else {
+ ldap_parse_result($ds, $results, $errno, $dn, $errmsg, $refs, $controlsReturned);
+ $cookie = $controlsReturned[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
+ }
+
+ ldap_free_result($results);
+ } while ($cookie && (! $serverSorting || $limit === 0 || count($entries) < $limit));
+
+ if ($legacyControlHandling && $cookie) {
+ // A sequence of paged search requests is abandoned by the client sending a search request containing a
+ // pagedResultsControl with the size set to zero (0) and the cookie set to the last cookie returned by
+ // the server: https://www.ietf.org/rfc/rfc2696.txt
+ ldap_control_paged_result($ds, 0, false, $cookie);
+ // Returns no entries, due to the page size
+ ldap_search($ds, $query->getBase() ?: $this->getDn(), (string) $query);
+ }
+
+ if (! $serverSorting) {
+ if ($query->hasOrder()) {
+ uasort($entries, array($query, 'compare'));
+ }
+
+ if ($limit && $count > $limit) {
+ $entries = array_splice($entries, $query->hasOffset() ? $query->getOffset() : 0, $limit);
+ }
+ }
+
+ return $entries;
+ }
+
+ /**
+ * Clean up the given attributes and return them as simple object
+ *
+ * Applies column aliases, aggregates/unfolds multi-value attributes
+ * as array and sets null for each missing attribute.
+ *
+ * @param array $attributes
+ * @param array $requestedFields
+ * @param string $unfoldAttribute
+ *
+ * @return object|array An array in case the object has been unfolded
+ */
+ public function cleanupAttributes($attributes, array $requestedFields, $unfoldAttribute = null)
+ {
+ // In case the result contains attributes with a differing case than the requested fields, it is
+ // necessary to create another array to map attributes case insensitively to their requested counterparts.
+ // This does also apply the virtual alias handling. (Since an LDAP server does not handle such)
+ $loweredFieldMap = array();
+ foreach ($requestedFields as $alias => $name) {
+ $loweredName = strtolower($name);
+ if (isset($loweredFieldMap[$loweredName])) {
+ if (! is_array($loweredFieldMap[$loweredName])) {
+ $loweredFieldMap[$loweredName] = array($loweredFieldMap[$loweredName]);
+ }
+
+ $loweredFieldMap[$loweredName][] = is_string($alias) ? $alias : $name;
+ } else {
+ $loweredFieldMap[$loweredName] = is_string($alias) ? $alias : $name;
+ }
+ }
+
+ $cleanedAttributes = array();
+ for ($i = 0; $i < $attributes['count']; $i++) {
+ $attribute_name = $attributes[$i];
+ if ($attributes[$attribute_name]['count'] === 1) {
+ $attribute_value = $attributes[$attribute_name][0];
+ } else {
+ $attribute_value = array();
+ for ($j = 0; $j < $attributes[$attribute_name]['count']; $j++) {
+ $attribute_value[] = $attributes[$attribute_name][$j];
+ }
+ }
+
+ $requestedAttributeName = isset($loweredFieldMap[strtolower($attribute_name)])
+ ? $loweredFieldMap[strtolower($attribute_name)]
+ : $attribute_name;
+ if (is_array($requestedAttributeName)) {
+ foreach ($requestedAttributeName as $requestedName) {
+ $cleanedAttributes[$requestedName] = $attribute_value;
+ }
+ } else {
+ $cleanedAttributes[$requestedAttributeName] = $attribute_value;
+ }
+ }
+
+ // The result may not contain all requested fields, so populate the cleaned
+ // result with the missing fields and their value being set to null
+ foreach ($requestedFields as $alias => $name) {
+ if (! is_string($alias)) {
+ $alias = $name;
+ }
+
+ if (! array_key_exists($alias, $cleanedAttributes)) {
+ $cleanedAttributes[$alias] = null;
+ Logger::debug('LDAP query result does not provide the requested field "%s"', $name);
+ }
+ }
+
+ if ($unfoldAttribute !== null
+ && isset($cleanedAttributes[$unfoldAttribute])
+ && is_array($cleanedAttributes[$unfoldAttribute])
+ ) {
+ $siblings = array();
+ foreach ($loweredFieldMap as $loweredName => $requestedNames) {
+ if (is_array($requestedNames) && in_array($unfoldAttribute, $requestedNames, true)) {
+ $siblings = array_diff($requestedNames, array($unfoldAttribute));
+ break;
+ }
+ }
+
+ $values = $cleanedAttributes[$unfoldAttribute];
+ unset($cleanedAttributes[$unfoldAttribute]);
+ $baseRow = (object) $cleanedAttributes;
+ $rows = array();
+ foreach ($values as $value) {
+ $row = clone $baseRow;
+ $row->{$unfoldAttribute} = $value;
+ foreach ($siblings as $sibling) {
+ $row->{$sibling} = $value;
+ }
+
+ $rows[] = $row;
+ }
+
+ return $rows;
+ }
+
+ return (object) $cleanedAttributes;
+ }
+
+ /**
+ * Encode the given array of sort rules as ASN.1 octet stream according to RFC 2891
+ *
+ * @param array $sortRules
+ *
+ * @return string Binary representation of the octet stream
+ */
+ protected function encodeSortRules(array $sortRules)
+ {
+ $sequenceOf = '';
+
+ foreach ($sortRules as $rule) {
+ if ($rule[1] === Sortable::SORT_DESC) {
+ $reversed = '8101ff';
+ } else {
+ $reversed = '';
+ }
+
+ $attributeType = unpack('H*', $rule[0]);
+ $attributeType = $attributeType[1];
+ $attributeOctets = strlen($attributeType) / 2;
+ if ($attributeOctets >= 127) {
+ // Use the indefinite form of the length octets (the long form would be another option)
+ $attributeType = '0440' . $attributeType . '0000';
+ } else {
+ $attributeType = '04' . str_pad(dechex($attributeOctets), 2, '0', STR_PAD_LEFT) . $attributeType;
+ }
+
+ $sequence = $attributeType . $reversed;
+ $sequenceOctects = strlen($sequence) / 2;
+ if ($sequenceOctects >= 127) {
+ $sequence = '3040' . $sequence . '0000';
+ } else {
+ $sequence = '30' . str_pad(dechex($sequenceOctects), 2, '0', STR_PAD_LEFT) . $sequence;
+ }
+
+ $sequenceOf .= $sequence;
+ }
+
+ $sequenceOfOctets = strlen($sequenceOf) / 2;
+ if ($sequenceOfOctets >= 127) {
+ $sequenceOf = '3040' . $sequenceOf . '0000';
+ } else {
+ $sequenceOf = '30' . str_pad(dechex($sequenceOfOctets), 2, '0', STR_PAD_LEFT) . $sequenceOf;
+ }
+
+ return hex2bin($sequenceOf);
+ }
+
+ /**
+ * Prepare and establish a connection with the LDAP server
+ *
+ * @param Inspection $info Optional inspection to fill with diagnostic info
+ *
+ * @return resource A LDAP link identifier
+ *
+ * @throws LdapException In case the connection is not possible
+ */
+ protected function prepareNewConnection(Inspection $info = null)
+ {
+ if (! isset($info)) {
+ $info = new Inspection('');
+ }
+
+ $hostname = $this->normalizeHostname($this->hostname);
+
+ $ds = ldap_connect($hostname);
+
+ // Set a proper timeout for each connection
+ ldap_set_option($ds, LDAP_OPT_NETWORK_TIMEOUT, $this->timeout);
+
+ // Usage of ldap_rename, setting LDAP_OPT_REFERRALS to 0 or using STARTTLS requires LDAPv3.
+ // If this does not work we're probably not in a PHP 5.3+ environment as it is VERY
+ // unlikely that the server complains about it by itself prior to a bind request
+ ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);
+
+ // Not setting this results in "Operations error" on AD when using the whole domain as search base
+ ldap_set_option($ds, LDAP_OPT_REFERRALS, 0);
+
+ if ($this->encryption === static::LDAPS) {
+ $info->write('Connect using LDAPS');
+ } elseif ($this->encryption === static::STARTTLS) {
+ $this->encrypted = true;
+ $info->write('Connect using STARTTLS');
+ if (! ldap_start_tls($ds)) {
+ throw new LdapException('LDAP STARTTLS failed: %s', ldap_error($ds));
+ }
+ } elseif ($this->encryption !== static::LDAPS) {
+ $this->encrypted = false;
+ $info->write('Connect without encryption');
+ }
+
+ return $ds;
+ }
+
+ /**
+ * Perform a LDAP search and return the result
+ *
+ * @param LdapQuery $query
+ * @param array $attributes An array of the required attributes
+ * @param int $attrsonly Should be set to 1 if only attribute types are wanted
+ * @param int $sizelimit Enables you to limit the count of entries fetched
+ * @param int $timelimit Sets the number of seconds how long is spend on the search
+ * @param int $deref
+ * @param array $controls LDAP Controls to send with the request (Only supported with PHP v7.3+)
+ *
+ * @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,
+ array $attributes = null,
+ $attrsonly = 0,
+ $sizelimit = 0,
+ $timelimit = 0,
+ $deref = LDAP_DEREF_NEVER,
+ $controls = null
+ ) {
+ $queryString = (string) $query;
+ $baseDn = $query->getBase() ?: $this->getDn();
+ $scope = $query->getScope();
+
+ if (Logger::getInstance()->getLevel() === Logger::DEBUG) {
+ // We're checking the level by ourselves to avoid rendering the ldapsearch commandline for nothing
+ $starttlsParam = $this->encryption === static::STARTTLS ? ' -ZZ' : '';
+
+ $bindParams = '';
+ if ($this->bound) {
+ $bindParams = ' -D "' . $this->bindDn . '"' . ($this->bindPw ? ' -W' : '');
+ }
+
+ if ($deref === LDAP_DEREF_NEVER) {
+ $derefName = 'never';
+ } elseif ($deref === LDAP_DEREF_ALWAYS) {
+ $derefName = 'always';
+ } elseif ($deref === LDAP_DEREF_SEARCHING) {
+ $derefName = 'search';
+ } else { // $deref === LDAP_DEREF_FINDING
+ $derefName = 'find';
+ }
+
+ Logger::debug("Issuing LDAP search. Use '%s' to reproduce.", sprintf(
+ 'ldapsearch -P 3%s -H "%s"%s -b "%s" -s "%s" -z %u -l %u -a "%s"%s%s%s',
+ $starttlsParam,
+ $this->normalizeHostname($this->hostname),
+ $bindParams,
+ $baseDn,
+ $scope,
+ $sizelimit,
+ $timelimit,
+ $derefName,
+ $attrsonly ? ' -A' : '',
+ $queryString ? ' "' . $queryString . '"' : '',
+ $attributes ? ' "' . join('" "', $attributes) . '"' : ''
+ ));
+ }
+
+ 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);
+ }
+
+ // Explicit calls with and without controls,
+ // because the parameter is only supported since PHP 7.3.
+ // Since it is a public method,
+ // providing controls will naturally fail if the parameter is not supported by PHP.
+ if ($controls !== null) {
+ return @$function(
+ $this->getConnection(),
+ $baseDn,
+ $queryString,
+ $attributes,
+ $attrsonly,
+ $sizelimit,
+ $timelimit,
+ $deref,
+ $controls
+ );
+ } else {
+ return @$function(
+ $this->getConnection(),
+ $baseDn,
+ $queryString,
+ $attributes,
+ $attrsonly,
+ $sizelimit,
+ $timelimit,
+ $deref
+ );
+ }
+ }
+
+ /**
+ * Create an LDAP entry
+ *
+ * @param string $dn The distinguished name to use
+ * @param array $attributes The entry's attributes
+ *
+ * @return bool Whether the operation was successful
+ */
+ public function addEntry($dn, array $attributes)
+ {
+ return ldap_add($this->getConnection(), $dn, $attributes);
+ }
+
+ /**
+ * Modify an LDAP entry
+ *
+ * @param string $dn The distinguished name to use
+ * @param array $attributes The attributes to update the entry with
+ *
+ * @return bool Whether the operation was successful
+ */
+ public function modifyEntry($dn, array $attributes)
+ {
+ return ldap_modify($this->getConnection(), $dn, $attributes);
+ }
+
+ /**
+ * Change the distinguished name of an LDAP entry
+ *
+ * @param string $dn The entry's current distinguished name
+ * @param string $newRdn The new relative distinguished name
+ * @param string $newParentDn The new parent or superior entry's distinguished name
+ *
+ * @return resource The resulting search result identifier
+ *
+ * @throws LdapException In case an error occured
+ */
+ public function moveEntry($dn, $newRdn, $newParentDn)
+ {
+ $ds = $this->getConnection();
+ $result = ldap_rename($ds, $dn, $newRdn, $newParentDn, false);
+ if ($result === false) {
+ throw new LdapException('Could not move entry "%s" to "%s": %s', $dn, $newRdn, ldap_error($ds));
+ }
+
+ return $result;
+ }
+
+ /**
+ * Return the LDAP specific configuration directory with the given relative path being appended
+ *
+ * @param string $sub
+ *
+ * @return string
+ */
+ protected function getConfigDir($sub = null)
+ {
+ $dir = Config::$configDir . '/ldap';
+ if ($sub !== null) {
+ $dir .= '/' . $sub;
+ }
+
+ return $dir;
+ }
+
+ /**
+ * Render and return a valid LDAP filter representation of the given filter
+ *
+ * @param Filter $filter
+ * @param int $level
+ *
+ * @return string
+ */
+ public function renderFilter(Filter $filter, $level = 0)
+ {
+ if ($filter->isExpression()) {
+ /** @var $filter FilterExpression */
+ return $this->renderFilterExpression($filter);
+ }
+
+ /** @var $filter FilterChain */
+ $parts = array();
+ foreach ($filter->filters() as $filterPart) {
+ $part = $this->renderFilter($filterPart, $level + 1);
+ if ($part) {
+ $parts[] = $part;
+ }
+ }
+
+ if (empty($parts)) {
+ return '';
+ }
+
+ $format = '%1$s(%2$s)';
+ if (count($parts) === 1 && ! $filter instanceof FilterNot) {
+ $format = '%2$s';
+ }
+ if ($level === 0) {
+ $format = '(' . $format . ')';
+ }
+
+ return sprintf($format, $filter->getOperatorSymbol(), implode(')(', $parts));
+ }
+
+ /**
+ * Render and return a valid LDAP filter expression of the given filter
+ *
+ * @param FilterExpression $filter
+ *
+ * @return string
+ */
+ protected function renderFilterExpression(FilterExpression $filter)
+ {
+ $column = $filter->getColumn();
+ $sign = $filter->getSign();
+ $expression = $filter->getExpression();
+ $format = '%1$s%2$s%3$s';
+
+ if ($expression === null || $expression === true) {
+ $expression = '*';
+ } elseif (is_array($expression)) {
+ $seqFormat = '|(%s)';
+ if ($sign === '!=') {
+ $seqFormat = '!(' . $seqFormat . ')';
+ $sign = '=';
+ }
+
+ $seqParts = array();
+ foreach ($expression as $expressionValue) {
+ $seqParts[] = sprintf(
+ $format,
+ LdapUtils::quoteForSearch($column),
+ $sign,
+ LdapUtils::quoteForSearch($expressionValue, true)
+ );
+ }
+
+ return sprintf($seqFormat, implode(')(', $seqParts));
+ }
+
+ if ($sign === '!=') {
+ $format = '!(%1$s=%3$s)';
+ }
+
+ return sprintf(
+ $format,
+ LdapUtils::quoteForSearch($column),
+ $sign,
+ LdapUtils::quoteForSearch($expression, true)
+ );
+ }
+
+ /**
+ * Inspect if this LDAP Connection is working as expected
+ *
+ * Check if connection, bind and encryption is working as expected and get additional
+ * information about the used
+ *
+ * @return Inspection Inspection result
+ */
+ public function inspect()
+ {
+ $insp = new Inspection('Ldap Connection');
+
+ // Try to connect to the server with the given connection parameters
+ try {
+ $ds = $this->prepareNewConnection($insp);
+ } catch (Exception $e) {
+ if ($this->encryption === 'starttls') {
+ // The Exception does not return any proper error messages in case of certificate errors. Connecting
+ // by STARTTLS will usually fail at this point when the certificate is unknown,
+ // so at least try to give some hints.
+ $insp->write('NOTE: There might be an issue with the chosen encryption. Ensure that the LDAP-Server ' .
+ 'supports STARTTLS and that the LDAP-Client is configured to accept its certificate.');
+ }
+ return $insp->error($e->getMessage());
+ }
+
+ // Try a bind-command with the given user credentials, this must not fail
+ $success = @ldap_bind($ds, $this->bindDn, $this->bindPw);
+ $msg = sprintf(
+ 'LDAP bind (%s / %s) to %s',
+ $this->bindDn,
+ '***' /* $this->bindPw */,
+ $this->normalizeHostname($this->hostname)
+ );
+ if (! $success) {
+ // ldap_error does not return any proper error messages in case of certificate errors. Connecting
+ // by LDAPS will usually fail at this point when the certificate is unknown, so at least try to give
+ // some hints.
+ if ($this->encryption === 'ldaps') {
+ $insp->write('NOTE: There might be an issue with the chosen encryption. Ensure that the LDAP-Server ' .
+ ' supports LDAPS and that the LDAP-Client is configured to accept its certificate.');
+ }
+ return $insp->error(sprintf('%s failed: %s', $msg, ldap_error($ds)));
+ }
+ $insp->write(sprintf($msg . ' successful'));
+
+ // Try to execute a schema discovery this may fail if schema discovery is not supported
+ try {
+ $cap = LdapCapabilities::discoverCapabilities($this);
+ $discovery = new Inspection('Discovery Results');
+ $vendor = $cap->getVendor();
+ if (isset($vendor)) {
+ $discovery->write($vendor);
+ }
+ $version = $cap->getVersion();
+ if (isset($version)) {
+ $discovery->write($version);
+ }
+ $discovery->write('Supports STARTTLS: ' . ($cap->hasStartTls() ? 'True' : 'False'));
+ $discovery->write('Default naming context: ' . $cap->getDefaultNamingContext());
+ $insp->write($discovery);
+ } catch (Exception $e) {
+ $insp->write('Schema discovery not possible: ' . $e->getMessage());
+ }
+ return $insp;
+ }
+
+ protected function normalizeHostname($hostname)
+ {
+ $scheme = $this->encryption === static::LDAPS ? 'ldaps://' : 'ldap://';
+ $normalizeHostname = function ($hostname) use ($scheme) {
+ if (strpos($hostname, $scheme) === false) {
+ $hostname = $scheme . $hostname;
+ }
+
+ if (! preg_match('/:\d+$/', $hostname)) {
+ $hostname .= ':' . $this->port;
+ }
+
+ return $hostname;
+ };
+
+ $ldapUrls = explode(' ', $hostname);
+ if (count($ldapUrls) > 1) {
+ foreach ($ldapUrls as & $uri) {
+ $uri = $normalizeHostname($uri);
+ }
+
+ $hostname = implode(' ', $ldapUrls);
+ } else {
+ $hostname = $normalizeHostname($hostname);
+ }
+
+ return $hostname;
+ }
+}
diff --git a/library/Icinga/Protocol/Ldap/LdapException.php b/library/Icinga/Protocol/Ldap/LdapException.php
new file mode 100644
index 0000000..740ee29
--- /dev/null
+++ b/library/Icinga/Protocol/Ldap/LdapException.php
@@ -0,0 +1,14 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol\Ldap;
+
+use Icinga\Exception\IcingaException;
+
+/**
+ * Class LdapException
+ * @package Icinga\Protocol\Ldap
+ */
+class LdapException extends IcingaException
+{
+}
diff --git a/library/Icinga/Protocol/Ldap/LdapQuery.php b/library/Icinga/Protocol/Ldap/LdapQuery.php
new file mode 100644
index 0000000..f4e1986
--- /dev/null
+++ b/library/Icinga/Protocol/Ldap/LdapQuery.php
@@ -0,0 +1,361 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol\Ldap;
+
+use Icinga\Data\Filter\Filter;
+use LogicException;
+use Icinga\Data\SimpleQuery;
+
+/**
+ * LDAP query class
+ */
+class LdapQuery extends SimpleQuery
+{
+ /**
+ * The base dn being used for this query
+ *
+ * @var string
+ */
+ protected $base;
+
+ /**
+ * Whether this query is permitted to utilize paged results
+ *
+ * @var bool
+ */
+ protected $usePagedResults;
+
+ /**
+ * The name of the attribute used to unfold the result
+ *
+ * @var string
+ */
+ protected $unfoldAttribute;
+
+ /**
+ * This query's native LDAP filter
+ *
+ * @var string
+ */
+ 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
+ */
+ protected function init()
+ {
+ $this->usePagedResults = false;
+ }
+
+ /**
+ * Set the base dn to be used for this query
+ *
+ * @param string $base
+ *
+ * @return $this
+ */
+ public function setBase($base)
+ {
+ $this->base = $base;
+ return $this;
+ }
+
+ /**
+ * Return the base dn being used for this query
+ *
+ * @return string
+ */
+ public function getBase()
+ {
+ return $this->base;
+ }
+
+ /**
+ * Set whether this query is permitted to utilize paged results
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
+ public function setUsePagedResults($state = true)
+ {
+ $this->usePagedResults = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether this query is permitted to utilize paged results
+ *
+ * @return bool
+ */
+ public function getUsePagedResults()
+ {
+ return $this->usePagedResults;
+ }
+
+ /**
+ * Set the attribute to be used to unfold the result
+ *
+ * @param string $attributeName
+ *
+ * @return $this
+ */
+ public function setUnfoldAttribute($attributeName)
+ {
+ $this->unfoldAttribute = $attributeName;
+ return $this;
+ }
+
+ /**
+ * Return the attribute to use to unfold the result
+ *
+ * @return string
+ */
+ public function getUnfoldAttribute()
+ {
+ return $this->unfoldAttribute;
+ }
+
+ /**
+ * Set this query's native LDAP filter
+ *
+ * @param string $filter
+ *
+ * @return $this
+ */
+ public function setNativeFilter($filter)
+ {
+ $this->nativeFilter = $filter;
+ return $this;
+ }
+
+ /**
+ * Return this query's native LDAP filter
+ *
+ * @return string
+ */
+ public function getNativeFilter()
+ {
+ return $this->nativeFilter;
+ }
+
+ /**
+ * Choose an objectClass and the columns you are interested in
+ *
+ * {@inheritdoc} This creates an objectClass filter.
+ */
+ public function from($target, array $fields = null)
+ {
+ $this->where('objectClass', $target);
+ return parent::from($target, $fields);
+ }
+
+ public function where($condition, $value = null)
+ {
+ $this->addFilter(Filter::expression($condition, '=', $value));
+ return $this;
+ }
+
+ public function addFilter(Filter $filter)
+ {
+ $this->makeCaseInsensitive($filter);
+ return parent::addFilter($filter);
+ }
+
+ public function setFilter(Filter $filter)
+ {
+ $this->makeCaseInsensitive($filter);
+ return parent::setFilter($filter);
+ }
+
+ protected function makeCaseInsensitive(Filter $filter)
+ {
+ if ($filter->isExpression()) {
+ /** @var \Icinga\Data\Filter\FilterExpression $filter */
+ $filter->setCaseSensitive(false);
+ } else {
+ /** @var \Icinga\Data\Filter\FilterChain $filter */
+ foreach ($filter->filters() as $subFilter) {
+ $this->makeCaseInsensitive($subFilter);
+ }
+ }
+ }
+
+ public function compare($a, $b, $orderIndex = 0)
+ {
+ if (array_key_exists($orderIndex, $this->order)) {
+ $column = $this->order[$orderIndex][0];
+ $direction = $this->order[$orderIndex][1];
+
+ $flippedColumns = $this->flippedColumns ?: array_flip($this->columns);
+ if (array_key_exists($column, $flippedColumns) && is_string($flippedColumns[$column])) {
+ $column = $flippedColumns[$column];
+ }
+
+ if (is_array($a->$column)) {
+ // rfc2891 states: If a sort key is a multi-valued attribute, and an entry happens to
+ // have multiple values for that attribute and no other controls are
+ // present that affect the sorting order, then the server SHOULD use the
+ // least value (according to the ORDERING rule for that attribute).
+ $a = clone $a;
+ $a->$column = array_reduce($a->$column, function ($carry, $item) use ($direction) {
+ $result = $carry === null ? 0 : strcmp($item, $carry);
+ return ($direction === self::SORT_ASC ? $result : $result * -1) < 1 ? $item : $carry;
+ });
+ }
+
+ if (is_array($b->$column)) {
+ $b = clone $b;
+ $b->$column = array_reduce($b->$column, function ($carry, $item) use ($direction) {
+ $result = $carry === null ? 0 : strcmp($item, $carry);
+ return ($direction === self::SORT_ASC ? $result : $result * -1) < 1 ? $item : $carry;
+ });
+ }
+ }
+
+ return parent::compare($a, $b, $orderIndex);
+ }
+
+ /**
+ * Fetch result as tree
+ *
+ * @return Root
+ *
+ * @todo This is untested waste, not being used anywhere and ignores the query's order and base dn.
+ * Evaluate whether it's reasonable to properly implement and test it.
+ */
+ public function fetchTree()
+ {
+ $result = $this->fetchAll();
+ $sorted = array();
+ $quotedDn = preg_quote($this->ds->getDn(), '/');
+ foreach ($result as $key => & $item) {
+ $new_key = LdapUtils::implodeDN(
+ array_reverse(
+ LdapUtils::explodeDN(
+ preg_replace('/,' . $quotedDn . '$/', '', $key)
+ )
+ )
+ );
+ $sorted[$new_key] = $key;
+ }
+
+ ksort($sorted);
+
+ $tree = Root::forConnection($this->ds);
+ $root_dn = $tree->getDN();
+ foreach ($sorted as $sort_key => & $key) {
+ if ($key === $root_dn) {
+ continue;
+ }
+ $tree->createChildByDN($key, $result[$key]);
+ }
+ return $tree;
+ }
+
+ /**
+ * Fetch the distinguished name of the first result
+ *
+ * @return string|false The distinguished name or false in case it's not possible to fetch a result
+ *
+ * @throws LdapException In case the query returns multiple results
+ * (i.e. it's not possible to fetch a unique DN)
+ */
+ public function fetchDn()
+ {
+ return $this->ds->fetchDn($this);
+ }
+
+ /**
+ * Render and return this query's filter
+ *
+ * @return string
+ */
+ public function renderFilter()
+ {
+ $filter = $this->ds->renderFilter($this->filter);
+ if ($this->nativeFilter) {
+ $filter = '(&(' . $this->nativeFilter . ')' . $filter . ')';
+ }
+
+ return $filter;
+ }
+
+ /**
+ * Return the LDAP filter to be applied on this query
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ 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
new file mode 100644
index 0000000..9c9ae10
--- /dev/null
+++ b/library/Icinga/Protocol/Ldap/LdapUtils.php
@@ -0,0 +1,148 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol\Ldap;
+
+/**
+ * This class provides useful LDAP-related functions
+ *
+ * @copyright Copyright (c) 2013 Icinga-Web Team <info@icinga.com>
+ * @author Icinga-Web Team <info@icinga.com>
+ * @package Icinga\Protocol\Ldap
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
+ */
+class LdapUtils
+{
+ /**
+ * Extends PHPs ldap_explode_dn() function
+ *
+ * UTF-8 chars like German umlauts would otherwise be escaped and shown
+ * as backslash-prefixed hexcode-sequenzes.
+ *
+ * @param string $dn DN
+ * @param boolean $with_type Returns 'type=value' when true and 'value' when false
+ *
+ * @return array
+ */
+ public static function explodeDN($dn, $with_type = true)
+ {
+ $res = ldap_explode_dn($dn, $with_type ? 0 : 1);
+
+ foreach ($res as $k => $v) {
+ $res[$k] = preg_replace_callback(
+ '/\\\([0-9a-f]{2})/i',
+ function ($m) {
+ return chr(hexdec($m[1]));
+ },
+ $v
+ );
+ }
+ unset($res['count']);
+ return $res;
+ }
+
+ /**
+ * Implode unquoted RDNs to a DN
+ *
+ * TODO: throw away, this is not how it shall be done
+ *
+ * @param array $parts DN-component
+ *
+ * @return string
+ */
+ public static function implodeDN($parts)
+ {
+ $str = '';
+ foreach ($parts as $part) {
+ if ($str !== '') {
+ $str .= ',';
+ }
+ list($key, $val) = preg_split('~=~', $part, 2);
+ $str .= $key . '=' . self::quoteForDN($val);
+ }
+ return $str;
+ }
+
+ /**
+ * Test if supplied value looks like a DN
+ *
+ * @param mixed $value
+ *
+ * @return bool
+ */
+ public static function isDn($value)
+ {
+ if (is_string($value)) {
+ return ldap_dn2ufn($value) !== false;
+ }
+ return false;
+ }
+
+ /**
+ * Quote a string that should be used in a DN
+ *
+ * Special characters will be escaped
+ *
+ * @param string $str DN-component
+ *
+ * @return string
+ */
+ public static function quoteForDN($str)
+ {
+ return self::quoteChars(
+ $str,
+ array(
+ ',',
+ '=',
+ '+',
+ '<',
+ '>',
+ ';',
+ '\\',
+ '"',
+ '#'
+ )
+ );
+ }
+
+ /**
+ * Quote a string that should be used in an LDAP search
+ *
+ * Special characters will be escaped
+ *
+ * @param string String to be escaped
+ * @param bool $allow_wildcard
+ * @return string
+ */
+ public static function quoteForSearch($str, $allow_wildcard = false)
+ {
+ if ($allow_wildcard) {
+ return self::quoteChars($str, array('(', ')', '\\', chr(0)));
+ }
+ return self::quoteChars($str, array('*', '(', ')', '\\', chr(0)));
+ }
+
+ /**
+ * Escape given characters in the given string
+ *
+ * Special characters will be escaped
+ *
+ * @param $str
+ * @param $chars
+ * @internal param String $string to be escaped
+ * @return string
+ */
+ protected static function quoteChars($str, $chars)
+ {
+ $quotedChars = array();
+ foreach ($chars as $k => $v) {
+ // Temporarily prefixing with illegal '('
+ $quotedChars[$k] = '(' . str_pad(dechex(ord($v)), 2, '0');
+ }
+ $str = str_replace($chars, $quotedChars, $str);
+ // Replacing temporary '(' with '\\'. This is a workaround, as
+ // str_replace behaves pretty strange with leading a backslash:
+ $str = preg_replace('~\(~', '\\', $str);
+ return $str;
+ }
+}
diff --git a/library/Icinga/Protocol/Ldap/Node.php b/library/Icinga/Protocol/Ldap/Node.php
new file mode 100644
index 0000000..176f962
--- /dev/null
+++ b/library/Icinga/Protocol/Ldap/Node.php
@@ -0,0 +1,69 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol\Ldap;
+
+/**
+ * This class represents an LDAP node object
+ *
+ * @copyright Copyright (c) 2013 Icinga-Web Team <info@icinga.com>
+ * @author Icinga-Web Team <info@icinga.com>
+ * @package Icinga\Protocol\Ldap
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
+ */
+class Node extends Root
+{
+ /**
+ * @var LdapConnection
+ */
+ protected $connection;
+
+ /**
+ * @var
+ */
+ protected $rdn;
+
+ /**
+ * @var Root
+ */
+ protected $parent;
+
+ /**
+ * @param Root $parent
+ */
+ protected function __construct(Root $parent)
+ {
+ $this->connection = $parent->getConnection();
+ $this->parent = $parent;
+ }
+
+ /**
+ * @param $parent
+ * @param $rdn
+ * @param array $props
+ * @return Node
+ */
+ public static function createWithRDN($parent, $rdn, $props = array())
+ {
+ $node = new Node($parent);
+ $node->rdn = $rdn;
+ $node->props = $props;
+ return $node;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getRDN()
+ {
+ return $this->rdn;
+ }
+
+ /**
+ * @return mixed|string
+ */
+ public function getDN()
+ {
+ return $this->getRDN() . ',' . $this->parent->getDN();
+ }
+}
diff --git a/library/Icinga/Protocol/Ldap/Root.php b/library/Icinga/Protocol/Ldap/Root.php
new file mode 100644
index 0000000..48d8719
--- /dev/null
+++ b/library/Icinga/Protocol/Ldap/Root.php
@@ -0,0 +1,242 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol\Ldap;
+
+use Icinga\Exception\IcingaException;
+
+/**
+ * This class is a special node object, representing your connections root node
+ *
+ * @copyright Copyright (c) 2013 Icinga-Web Team <info@icinga.com>
+ * @author Icinga-Web Team <info@icinga.com>
+ * @package Icinga\Protocol\Ldap
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
+ * @package Icinga\Protocol\Ldap
+ */
+class Root
+{
+ /**
+ * @var string
+ */
+ protected $rdn;
+
+ /**
+ * @var LdapConnection
+ */
+ protected $connection;
+
+ /**
+ * @var array
+ */
+ protected $children = array();
+
+ /**
+ * @var array
+ */
+ protected $props = array();
+
+ /**
+ * @param LdapConnection $connection
+ */
+ protected function __construct(LdapConnection $connection)
+ {
+ $this->connection = $connection;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasParent()
+ {
+ return false;
+ }
+
+ /**
+ * @param LdapConnection $connection
+ * @return Root
+ */
+ public static function forConnection(LdapConnection $connection)
+ {
+ $root = new Root($connection);
+ return $root;
+ }
+
+ /**
+ * @param $dn
+ * @param array $props
+ * @return Node
+ */
+ public function createChildByDN($dn, $props = array())
+ {
+ $dn = $this->stripMyDN($dn);
+ $parts = array_reverse(LdapUtils::explodeDN($dn));
+ $parent = $this;
+ $child = null;
+ while ($rdn = array_shift($parts)) {
+ if ($parent->hasChildRDN($rdn)) {
+ $child = $parent->getChildByRDN($rdn);
+ } else {
+ $child = Node::createWithRDN($parent, $rdn, (array)$props);
+ $parent->addChild($child);
+ }
+ $parent = $child;
+ }
+ return $child;
+ }
+
+ /**
+ * @param $rdn
+ * @return bool
+ */
+ public function hasChildRDN($rdn)
+ {
+ return array_key_exists(strtolower($rdn), $this->children);
+ }
+
+ /**
+ * @param $rdn
+ * @return mixed
+ * @throws IcingaException
+ */
+ public function getChildByRDN($rdn)
+ {
+ if (!$this->hasChildRDN($rdn)) {
+ throw new IcingaException(
+ 'The child RDN "%s" is not available',
+ $rdn
+ );
+ }
+ return $this->children[strtolower($rdn)];
+ }
+
+ /**
+ * @return array
+ */
+ public function children()
+ {
+ return $this->children;
+ }
+
+ public function countChildren()
+ {
+ return count($this->children);
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasChildren()
+ {
+ return !empty($this->children);
+ }
+
+ /**
+ * @param Node $child
+ * @return $this
+ */
+ public function addChild(Node $child)
+ {
+ $this->children[strtolower($child->getRDN())] = $child;
+ return $this;
+ }
+
+ /**
+ * @param $dn
+ * @return string
+ */
+ protected function stripMyDN($dn)
+ {
+ $this->assertSubDN($dn);
+ return substr($dn, 0, strlen($dn) - strlen($this->getDN()) - 1);
+ }
+
+ /**
+ * @param $dn
+ * @return $this
+ * @throws IcingaException
+ */
+ protected function assertSubDN($dn)
+ {
+ $mydn = $this->getDN();
+ $end = substr($dn, -1 * strlen($mydn));
+ if (strtolower($end) !== strtolower($mydn)) {
+ throw new IcingaException(
+ '"%s" is not a child of "%s"',
+ $dn,
+ $mydn
+ );
+ }
+ if (strlen($dn) === strlen($mydn)) {
+ throw new IcingaException(
+ '"%s" is not a child of "%s", they are equal',
+ $dn,
+ $mydn
+ );
+ }
+ return $this;
+ }
+
+ /**
+ * @param LdapConnection $connection
+ * @return $this
+ */
+ public function setConnection(LdapConnection $connection)
+ {
+ $this->connection = $connection;
+ return $this;
+ }
+
+ /**
+ * @return LdapConnection
+ */
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasBeenChanged()
+ {
+ return false;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getRDN()
+ {
+ return $this->getDN();
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getDN()
+ {
+ return $this->connection->getDn();
+ }
+
+ /**
+ * @param $key
+ * @return null
+ */
+ public function __get($key)
+ {
+ if (!array_key_exists($key, $this->props)) {
+ return null;
+ }
+ return $this->props[$key];
+ }
+
+ /**
+ * @param $key
+ * @return bool
+ */
+ public function __isset($key)
+ {
+ return array_key_exists($key, $this->props);
+ }
+}
diff --git a/library/Icinga/Protocol/Nrpe/Connection.php b/library/Icinga/Protocol/Nrpe/Connection.php
new file mode 100644
index 0000000..491a965
--- /dev/null
+++ b/library/Icinga/Protocol/Nrpe/Connection.php
@@ -0,0 +1,111 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol\Nrpe;
+
+use Icinga\Exception\IcingaException;
+
+class Connection
+{
+ protected $host;
+ protected $port;
+ protected $connection;
+ protected $use_ssl = false;
+ protected $lastReturnCode = null;
+
+ public function __construct($host, $port = 5666)
+ {
+ $this->host = $host;
+ $this->port = $port;
+ }
+
+ public function useSsl($use_ssl = true)
+ {
+ $this->use_ssl = $use_ssl;
+ return $this;
+ }
+
+ public function sendCommand($command, $args = null)
+ {
+ if (! empty($args)) {
+ $command .= '!' . implode('!', $args);
+ }
+
+ $packet = Packet::createQuery($command);
+ return $this->send($packet);
+ }
+
+ public function getLastReturnCode()
+ {
+ return $this->lastReturnCode;
+ }
+
+ public function send(Packet $packet)
+ {
+ $conn = $this->connection();
+ $bytes = $packet->getBinary();
+ fputs($conn, $bytes, strlen($bytes));
+ // TODO: Check result checksum!
+ $result = fread($conn, 8192);
+ if ($result === false) {
+ throw new IcingaException('CHECK_NRPE: Error receiving data from daemon.');
+ } elseif (strlen($result) === 0) {
+ throw new IcingaException(
+ 'CHECK_NRPE: Received 0 bytes from daemon. Check the remote server logs for error messages'
+ );
+ }
+ // TODO: CHECK_NRPE: Receive underflow - only %d bytes received (%d expected)
+ $code = unpack('n', substr($result, 8, 2));
+ $this->lastReturnCode = $code[1];
+ $this->disconnect();
+ return rtrim(substr($result, 10, -2));
+ }
+
+ protected function connect()
+ {
+ $ctx = stream_context_create();
+ if ($this->use_ssl) {
+ // TODO: fail if not ok:
+ $res = stream_context_set_option($ctx, 'ssl', 'ciphers', 'ADH');
+ $uri = sprintf('ssl://%s:%d', $this->host, $this->port);
+ } else {
+ $uri = sprintf('tcp://%s:%d', $this->host, $this->port);
+ }
+ $this->connection = @stream_socket_client(
+ $uri,
+ $errno,
+ $errstr,
+ 10,
+ STREAM_CLIENT_CONNECT,
+ $ctx
+ );
+ if (! $this->connection) {
+ throw new IcingaException(
+ 'NRPE Connection failed: %s',
+ $errstr
+ );
+ }
+ }
+
+ protected function connection()
+ {
+ if ($this->connection === null) {
+ $this->connect();
+ }
+ return $this->connection;
+ }
+
+ protected function disconnect()
+ {
+ if (is_resource($this->connection)) {
+ fclose($this->connection);
+ $this->connection = null;
+ }
+ return $this;
+ }
+
+ public function __destruct()
+ {
+ $this->disconnect();
+ }
+}
diff --git a/library/Icinga/Protocol/Nrpe/Packet.php b/library/Icinga/Protocol/Nrpe/Packet.php
new file mode 100644
index 0000000..54c8526
--- /dev/null
+++ b/library/Icinga/Protocol/Nrpe/Packet.php
@@ -0,0 +1,69 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Protocol\Nrpe;
+
+class Packet
+{
+ const QUERY = 0x01;
+ const RESPONSE = 0x02;
+
+ protected $version = 0x02;
+ protected $type;
+ protected $body;
+ protected static $randomBytes;
+
+ public function __construct($type, $body)
+ {
+ $this->type = $type;
+ $this->body = $body;
+ $this->regenerateRandomBytes();
+ }
+
+ // TODO: renew "from time to time" to allow long-running daemons
+ protected function regenerateRandomBytes()
+ {
+ self::$randomBytes = '';
+ for ($i = 0; $i < 4096; $i++) {
+ self::$randomBytes .= pack('N', mt_rand());
+ }
+ }
+
+ public static function createQuery($body)
+ {
+ $packet = new Packet(self::QUERY, $body);
+ return $packet;
+ }
+
+ protected function getFillString($length)
+ {
+ $max = strlen(self::$randomBytes) - $length;
+ return substr(self::$randomBytes, rand(0, $max), $length);
+ }
+
+ // TODO: WTF is SR? And 2324?
+ public function getBinary()
+ {
+ $version = pack('n', $this->version);
+ $type = pack('n', $this->type);
+ $dummycrc = "\x00\x00\x00\x00";
+ $result = "\x00\x00";
+ $result = pack('n', 2324);
+ $body = $this->body
+ . "\x00"
+ . $this->getFillString(1023 - strlen($this->body))
+ . 'SR';
+
+ $crc = pack(
+ 'N',
+ crc32($version . $type . $dummycrc . $result . $body)
+ );
+ $bytes = $version . $type . $crc . $result . $body;
+ return $bytes;
+ }
+
+ public function __toString()
+ {
+ return $this->body;
+ }
+}
diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php
new file mode 100644
index 0000000..3f8b604
--- /dev/null
+++ b/library/Icinga/Repository/DbRepository.php
@@ -0,0 +1,1078 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Repository;
+
+use Icinga\Exception\QueryException;
+use Zend_Db_Expr;
+use Icinga\Data\Db\DbConnection;
+use Icinga\Data\Extensible;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\Reducible;
+use Icinga\Data\Updatable;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\StatementException;
+use Icinga\Util\StringHelper;
+
+/**
+ * Abstract base class for concrete database repository implementations
+ *
+ * Additionally provided features:
+ * <ul>
+ * <li>Support for table aliases</li>
+ * <li>Automatic table prefix handling</li>
+ * <li>Insert, update and delete capabilities</li>
+ * <li>Differentiation between statement and query columns</li>
+ * <li>Capability to join additional tables depending on the columns being selected or used in a filter</li>
+ * </ul>
+ *
+ * @method DbConnection getDataSource($table = null)
+ */
+abstract class DbRepository extends Repository implements Extensible, Updatable, Reducible
+{
+ /**
+ * The datasource being used
+ *
+ * @var DbConnection
+ */
+ protected $ds;
+
+ /**
+ * The table aliases being applied
+ *
+ * This must be initialized by repositories which are going to make use of table aliases. Every table for which
+ * aliased columns are provided must be defined in this array using its name as key and the alias being used as
+ * value. Failure to do so will result in invalid queries.
+ *
+ * @var array
+ */
+ protected $tableAliases;
+
+ /**
+ * The join probability rules
+ *
+ * This may be initialized by repositories which make use of the table join capability. It allows to define
+ * probability rules to enhance control how ambiguous column aliases are associated with the correct table.
+ * To define a rule use the name of a base table as key and another array of table names as probable join
+ * targets ordered by priority. (Ascending: Lower means higher priority)
+ * <code>
+ * array(
+ * 'table_name' => array('target1', 'target2', 'target3')
+ * )
+ * </code>
+ *
+ * @todo Support for tree-ish rules
+ *
+ * @var array
+ */
+ protected $joinProbabilities;
+
+ /**
+ * The statement columns being provided
+ *
+ * This may be initialized by repositories which are going to make use of table aliases. It allows to provide
+ * alias-less column names to be used for a statement. The array needs to be in the following format:
+ * <code>
+ * array(
+ * 'table_name' => array(
+ * 'column1',
+ * 'alias1' => 'column2',
+ * 'alias2' => 'column3'
+ * )
+ * )
+ * </code>
+ *
+ * @var array
+ */
+ protected $statementColumns;
+
+ /**
+ * An array to map table names to statement columns/aliases
+ *
+ * @var array
+ */
+ protected $statementAliasTableMap;
+
+ /**
+ * A flattened array to map statement columns to aliases
+ *
+ * @var array
+ */
+ protected $statementAliasColumnMap;
+
+ /**
+ * An array to map table names to statement columns
+ *
+ * @var array
+ */
+ protected $statementColumnTableMap;
+
+ /**
+ * A flattened array to map aliases to statement columns
+ *
+ * @var array
+ */
+ protected $statementColumnAliasMap;
+
+ /**
+ * List of column names or aliases mapped to their table where the COLLATE SQL-instruction has been removed
+ *
+ * This list is being populated in case of a PostgreSQL backend only,
+ * to ensure case-insensitive string comparison in WHERE clauses.
+ *
+ * @var array
+ */
+ protected $caseInsensitiveColumns;
+
+ /**
+ * Create a new DB repository object
+ *
+ * In case $this->queryColumns has already been initialized, this initializes
+ * $this->caseInsensitiveColumns in case of a PostgreSQL connection.
+ *
+ * @param DbConnection $ds The datasource to use
+ */
+ public function __construct(DbConnection $ds)
+ {
+ parent::__construct($ds);
+
+ if ($ds->getDbType() === 'pgsql' && $this->queryColumns !== null) {
+ $this->queryColumns = $this->removeCollateInstruction($this->queryColumns);
+ }
+ }
+
+ /**
+ * Return the query columns being provided
+ *
+ * Initializes $this->caseInsensitiveColumns in case of a PostgreSQL connection.
+ *
+ * @return array
+ */
+ public function getQueryColumns()
+ {
+ if ($this->queryColumns === null) {
+ $this->queryColumns = parent::getQueryColumns();
+ if ($this->ds->getDbType() === 'pgsql') {
+ $this->queryColumns = $this->removeCollateInstruction($this->queryColumns);
+ }
+ }
+
+ return $this->queryColumns;
+ }
+
+ /**
+ * Return the table aliases to be applied
+ *
+ * Calls $this->initializeTableAliases() in case $this->tableAliases is null.
+ *
+ * @return array
+ */
+ public function getTableAliases()
+ {
+ if ($this->tableAliases === null) {
+ $this->tableAliases = $this->initializeTableAliases();
+ }
+
+ return $this->tableAliases;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the table aliases lazily
+ *
+ * @return array
+ */
+ protected function initializeTableAliases()
+ {
+ return array();
+ }
+
+ /**
+ * Return the join probability rules
+ *
+ * Calls $this->initializeJoinProbabilities() in case $this->joinProbabilities is null.
+ *
+ * @return array
+ */
+ public function getJoinProbabilities()
+ {
+ if ($this->joinProbabilities === null) {
+ $this->joinProbabilities = $this->initializeJoinProbabilities();
+ }
+
+ return $this->joinProbabilities;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the join probabilities lazily
+ *
+ * @return array
+ */
+ protected function initializeJoinProbabilities()
+ {
+ return array();
+ }
+
+ /**
+ * Remove each COLLATE SQL-instruction from all given query columns
+ *
+ * @param array $queryColumns
+ *
+ * @return array $queryColumns, the updated version
+ */
+ protected function removeCollateInstruction($queryColumns)
+ {
+ foreach ($queryColumns as $table => & $columns) {
+ foreach ($columns as $alias => & $column) {
+ // Using a regex here because COLLATE may occur anywhere in the string
+ $column = preg_replace('/ COLLATE .+$/', '', $column, -1, $count);
+ if ($count > 0) {
+ $this->caseInsensitiveColumns[$table][is_string($alias) ? $alias : $column] = true;
+ }
+ }
+ }
+
+ return $queryColumns;
+ }
+
+ /**
+ * Initialize table, column and alias maps
+ *
+ * @throws ProgrammingError In case $this->queryColumns does not provide any column information
+ */
+ protected function initializeAliasMaps()
+ {
+ parent::initializeAliasMaps();
+
+ foreach ($this->aliasTableMap as $alias => $table) {
+ if ($table !== null) {
+ if (strpos($alias, '.') !== false) {
+ $prefixedAlias = str_replace('.', '_', $alias);
+ } else {
+ $prefixedAlias = $table . '_' . $alias;
+ }
+
+ if (array_key_exists($prefixedAlias, $this->aliasTableMap)) {
+ if ($this->aliasTableMap[$prefixedAlias] !== null) {
+ $existingTable = $this->aliasTableMap[$prefixedAlias];
+ $existingColumn = $this->aliasColumnMap[$prefixedAlias];
+ $this->aliasTableMap[$existingTable . '.' . $prefixedAlias] = $existingTable;
+ $this->aliasColumnMap[$existingTable . '.' . $prefixedAlias] = $existingColumn;
+ $this->aliasTableMap[$prefixedAlias] = null;
+ $this->aliasColumnMap[$prefixedAlias] = null;
+ }
+
+ $this->aliasTableMap[$table . '.' . $prefixedAlias] = $table;
+ $this->aliasColumnMap[$table . '.' . $prefixedAlias] = $this->aliasColumnMap[$alias];
+ } else {
+ $this->aliasTableMap[$prefixedAlias] = $table;
+ $this->aliasColumnMap[$prefixedAlias] = $this->aliasColumnMap[$alias];
+ }
+ }
+ }
+ }
+
+ /**
+ * Return the given table with the datasource's prefix being prepended
+ *
+ * @param array|string $table
+ *
+ * @return array|string
+ *
+ * @throws IcingaException In case $table is not of a supported type
+ */
+ protected function prependTablePrefix($table)
+ {
+ $prefix = $this->ds->getTablePrefix();
+ if (! $prefix) {
+ return $table;
+ }
+
+ if (is_array($table)) {
+ foreach ($table as & $tableName) {
+ if (strpos($tableName, $prefix) === false) {
+ $tableName = $prefix . $tableName;
+ }
+ }
+ } elseif (is_string($table)) {
+ $table = (strpos($table, $prefix) === false ? $prefix : '') . $table;
+ } else {
+ throw new IcingaException('Table prefix handling for type "%s" is not supported', gettype($table));
+ }
+
+ return $table;
+ }
+
+ /**
+ * Remove the datasource's prefix from the given table name and return the remaining part
+ *
+ * @param array|string $table
+ *
+ * @return array|string
+ *
+ * @throws IcingaException In case $table is not of a supported type
+ */
+ protected function removeTablePrefix($table)
+ {
+ $prefix = $this->ds->getTablePrefix();
+ if (! $prefix) {
+ return $table;
+ }
+
+ if (is_array($table)) {
+ foreach ($table as & $tableName) {
+ if (strpos($tableName, $prefix) === 0) {
+ $tableName = str_replace($prefix, '', $tableName);
+ }
+ }
+ } elseif (is_string($table)) {
+ if (strpos($table, $prefix) === 0) {
+ $table = str_replace($prefix, '', $table);
+ }
+ } else {
+ throw new IcingaException('Table prefix handling for type "%s" is not supported', gettype($table));
+ }
+
+ return $table;
+ }
+
+ /**
+ * Return the given table with its alias being applied
+ *
+ * @param array|string $table
+ * @param string $virtualTable
+ *
+ * @return array|string
+ */
+ protected function applyTableAlias($table, $virtualTable = null)
+ {
+ if (! is_array($table)) {
+ $tableAliases = $this->getTableAliases();
+ if ($virtualTable !== null && isset($tableAliases[$virtualTable])) {
+ return array($tableAliases[$virtualTable] => $table);
+ }
+
+ if (isset($tableAliases[($nonPrefixedTable = $this->removeTablePrefix($table))])) {
+ return array($tableAliases[$nonPrefixedTable] => $table);
+ }
+ }
+
+ return $table;
+ }
+
+ /**
+ * Return the given table with its alias being cleared
+ *
+ * @param array|string $table
+ *
+ * @return string
+ *
+ * @throws IcingaException In case $table is not of a supported type
+ */
+ protected function clearTableAlias($table)
+ {
+ if (is_string($table)) {
+ return $table;
+ }
+
+ if (is_array($table)) {
+ return reset($table);
+ }
+
+ throw new IcingaException('Table alias handling for type "%s" is not supported', type($table));
+ }
+
+ /**
+ * Insert a table row with the given data
+ *
+ * Note that the base implementation does not perform any quoting on the $table argument.
+ * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
+ * as third parameter $types to define a different type than string for a particular column.
+ *
+ * @param string $table
+ * @param array $bind
+ * @param array $types
+ *
+ * @return int The number of affected rows
+ */
+ public function insert($table, array $bind, array $types = array())
+ {
+ $realTable = $this->clearTableAlias($this->requireTable($table));
+
+ foreach ($types as $alias => $type) {
+ unset($types[$alias]);
+ $types[$this->requireStatementColumn($table, $alias)] = $type;
+ }
+
+ return $this->ds->insert($realTable, $this->requireStatementColumns($table, $bind), $types);
+ }
+
+ /**
+ * Update table rows with the given data, optionally limited by using a filter
+ *
+ * Note that the base implementation does not perform any quoting on the $table argument.
+ * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
+ * as fourth parameter $types to define a different type than string for a particular column.
+ *
+ * @param string $table
+ * @param array $bind
+ * @param Filter $filter
+ * @param array $types
+ *
+ * @return int The number of affected rows
+ */
+ public function update($table, array $bind, Filter $filter = null, array $types = array())
+ {
+ $realTable = $this->clearTableAlias($this->requireTable($table));
+
+ if ($filter) {
+ $filter = $this->requireFilter($table, $filter);
+ }
+
+ foreach ($types as $alias => $type) {
+ unset($types[$alias]);
+ $types[$this->requireStatementColumn($table, $alias)] = $type;
+ }
+
+ return $this->ds->update($realTable, $this->requireStatementColumns($table, $bind), $filter, $types);
+ }
+
+ /**
+ * Delete table rows, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param Filter $filter
+ *
+ * @return int The number of affected rows
+ */
+ public function delete($table, Filter $filter = null)
+ {
+ $realTable = $this->clearTableAlias($this->requireTable($table));
+
+ if ($filter) {
+ $filter = $this->requireFilter($table, $filter);
+ }
+
+ return $this->ds->delete($realTable, $filter);
+ }
+
+ /**
+ * Return the statement columns being provided
+ *
+ * Calls $this->initializeStatementColumns() in case $this->statementColumns is null.
+ *
+ * @return array
+ */
+ public function getStatementColumns()
+ {
+ if ($this->statementColumns === null) {
+ $this->statementColumns = $this->initializeStatementColumns();
+ }
+
+ return $this->statementColumns;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the statement columns lazily
+ *
+ * @return array
+ */
+ protected function initializeStatementColumns()
+ {
+ return array();
+ }
+
+ /**
+ * Return an array to map table names to statement columns/aliases
+ *
+ * @return array
+ */
+ protected function getStatementAliasTableMap()
+ {
+ if ($this->statementAliasTableMap === null) {
+ $this->initializeStatementMaps();
+ }
+
+ return $this->statementAliasTableMap;
+ }
+
+ /**
+ * Return a flattened array to map statement columns to aliases
+ *
+ * @return array
+ */
+ protected function getStatementAliasColumnMap()
+ {
+ if ($this->statementAliasColumnMap === null) {
+ $this->initializeStatementMaps();
+ }
+
+ return $this->statementAliasColumnMap;
+ }
+
+ /**
+ * Return an array to map table names to statement columns
+ *
+ * @return array
+ */
+ protected function getStatementColumnTableMap()
+ {
+ if ($this->statementColumnTableMap === null) {
+ $this->initializeStatementMaps();
+ }
+
+ return $this->statementColumnTableMap;
+ }
+
+ /**
+ * Return a flattened array to map aliases to statement columns
+ *
+ * @return array
+ */
+ protected function getStatementColumnAliasMap()
+ {
+ if ($this->statementColumnAliasMap === null) {
+ $this->initializeStatementMaps();
+ }
+
+ return $this->statementColumnAliasMap;
+ }
+
+ /**
+ * Initialize $this->statementAliasTableMap and $this->statementAliasColumnMap
+ */
+ protected function initializeStatementMaps()
+ {
+ $this->statementAliasTableMap = array();
+ $this->statementAliasColumnMap = array();
+ $this->statementColumnTableMap = array();
+ $this->statementColumnAliasMap = array();
+ foreach ($this->getStatementColumns() as $table => $columns) {
+ foreach ($columns as $alias => $column) {
+ $key = is_string($alias) ? $alias : $column;
+ if (array_key_exists($key, $this->statementAliasTableMap)) {
+ if ($this->statementAliasTableMap[$key] !== null) {
+ $existingTable = $this->statementAliasTableMap[$key];
+ $existingColumn = $this->statementAliasColumnMap[$key];
+ $this->statementAliasTableMap[$existingTable . '.' . $key] = $existingTable;
+ $this->statementAliasColumnMap[$existingTable . '.' . $key] = $existingColumn;
+ $this->statementAliasTableMap[$key] = null;
+ $this->statementAliasColumnMap[$key] = null;
+ }
+
+ $this->statementAliasTableMap[$table . '.' . $key] = $table;
+ $this->statementAliasColumnMap[$table . '.' . $key] = $column;
+ } else {
+ $this->statementAliasTableMap[$key] = $table;
+ $this->statementAliasColumnMap[$key] = $column;
+ }
+
+ if (array_key_exists($column, $this->statementColumnTableMap)) {
+ if ($this->statementColumnTableMap[$column] !== null) {
+ $existingTable = $this->statementColumnTableMap[$column];
+ $existingAlias = $this->statementColumnAliasMap[$column];
+ $this->statementColumnTableMap[$existingTable . '.' . $column] = $existingTable;
+ $this->statementColumnAliasMap[$existingTable . '.' . $column] = $existingAlias;
+ $this->statementColumnTableMap[$column] = null;
+ $this->statementColumnAliasMap[$column] = null;
+ }
+
+ $this->statementColumnTableMap[$table . '.' . $column] = $table;
+ $this->statementColumnAliasMap[$table . '.' . $column] = $key;
+ } else {
+ $this->statementColumnTableMap[$column] = $table;
+ $this->statementColumnAliasMap[$column] = $key;
+ }
+ }
+ }
+ }
+
+ /**
+ * Return whether this repository is capable of converting values for the given table and optional column
+ *
+ * This does not check whether any conversion for the given table is available if $column is not given, as it
+ * may be possible that columns from another table where joined in which would otherwise not being converted.
+ *
+ * @param string $table
+ * @param string $column
+ *
+ * @return bool
+ */
+ public function providesValueConversion($table, $column = null)
+ {
+ if ($column !== null) {
+ if ($column instanceof Zend_Db_Expr) {
+ return false;
+ }
+
+ if ($this->validateQueryColumnAssociation($table, $column)) {
+ return parent::providesValueConversion($table, $column);
+ }
+
+ if (($tableName = $this->findTableName($column, $table))) {
+ return parent::providesValueConversion($tableName, $column);
+ }
+
+ return false;
+ }
+
+ $conversionRules = $this->getConversionRules();
+ return !empty($conversionRules);
+ }
+
+ /**
+ * Return the name of the conversion method for the given alias or column name and context
+ *
+ * If a query column or a filter column, which is part of a query filter, needs to be converted,
+ * you'll need to pass $query, otherwise the column is considered a statement column.
+ *
+ * @param string $table The datasource's table
+ * @param string $name The alias or column name for which to return a conversion method
+ * @param string $context The context of the conversion: persist or retrieve
+ * @param RepositoryQuery $query If given the column is considered a query column,
+ * statement column otherwise
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case a conversion rule is found but not any conversion method
+ */
+ protected function getConverter($table, $name, $context, RepositoryQuery $query = null)
+ {
+ if ($name instanceof Zend_Db_Expr) {
+ return;
+ }
+
+ if (! ($query !== null && $this->validateQueryColumnAssociation($table, $name))
+ && !($query === null && $this->validateStatementColumnAssociation($table, $name))
+ ) {
+ $table = $this->findTableName($name, $table);
+ if (! $table) {
+ if ($query !== null) {
+ // It may be an aliased Zend_Db_Expr
+ $desiredColumns = $query->getColumns();
+ if (isset($desiredColumns[$name]) && $desiredColumns[$name] instanceof Zend_Db_Expr) {
+ return;
+ }
+ }
+
+ throw new ProgrammingError('Column name validation seems to have failed. Did you require the column?');
+ }
+ }
+
+ return parent::getConverter($table, $name, $context, $query);
+ }
+
+ /**
+ * Validate that the requested table exists
+ *
+ * This will prepend the datasource's table prefix and will apply the table's alias, if any.
+ *
+ * @param string $table The table to validate
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (unused by the base implementation)
+ *
+ * @return array|string
+ *
+ * @throws ProgrammingError In case the given table does not exist
+ */
+ public function requireTable($table, RepositoryQuery $query = null)
+ {
+ $virtualTable = null;
+ $statementColumns = $this->getStatementColumns();
+ if (! isset($statementColumns[$table])) {
+ $newTable = parent::requireTable($table);
+ if ($newTable !== $table) {
+ $virtualTable = $table;
+ }
+
+ $table = $newTable;
+ } else {
+ $virtualTables = $this->getVirtualTables();
+ if (isset($virtualTables[$table])) {
+ $virtualTable = $table;
+ $table = $virtualTables[$table];
+ }
+ }
+
+ return $this->prependTablePrefix($this->applyTableAlias($table, $virtualTable));
+ }
+
+ /**
+ * Return the alias for the given table or null if none has been defined
+ *
+ * @param string $table
+ *
+ * @return string|null
+ */
+ public function resolveTableAlias($table)
+ {
+ $tableAliases = $this->getTableAliases();
+ if (isset($tableAliases[$table])) {
+ return $tableAliases[$table];
+ }
+ }
+
+ /**
+ * Return the alias for the given query column name or null in case the query column name does not exist
+ *
+ * @param string $table
+ * @param string $column
+ *
+ * @return string|null
+ */
+ public function reassembleQueryColumnAlias($table, $column)
+ {
+ $alias = parent::reassembleQueryColumnAlias($table, $column);
+ if ($alias === null
+ && !$this->validateQueryColumnAssociation($table, $column)
+ && ($tableName = $this->findTableName($column, $table))
+ ) {
+ return parent::reassembleQueryColumnAlias($tableName, $column);
+ }
+
+ return $alias;
+ }
+
+ /**
+ * Validate that the given column is a valid query target and return it or the actual name if it's an alias
+ *
+ * Attempts to join the given column from a different table if its association to the given table cannot be
+ * verified.
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The name or alias of the column to validate
+ * @param RepositoryQuery $query An optional query to pass as context,
+ * if not given no join will be attempted
+ *
+ * @return string The given column's name
+ *
+ * @throws QueryException In case the given column is not a valid query column
+ * @throws ProgrammingError In case the given column is not found in $table and cannot be joined in
+ */
+ public function requireQueryColumn($table, $name, RepositoryQuery $query = null)
+ {
+ if ($name instanceof Zend_Db_Expr) {
+ return $name;
+ }
+
+ if ($query === null || $this->validateQueryColumnAssociation($table, $name)) {
+ return parent::requireQueryColumn($table, $name, $query);
+ }
+
+ $column = $this->joinColumn($name, $table, $query);
+ if ($column === null) {
+ if ($query !== null) {
+ // It may be an aliased Zend_Db_Expr
+ $desiredColumns = $query->getColumns();
+ if (isset($desiredColumns[$name]) && $desiredColumns[$name] instanceof Zend_Db_Expr) {
+ $column = $desiredColumns[$name];
+ }
+ }
+
+ if ($column === null) {
+ throw new ProgrammingError(
+ 'Unable to find a valid table for column "%s" to join into "%s"',
+ $name,
+ $table
+ );
+ }
+ }
+
+ return $column;
+ }
+
+ /**
+ * Validate that the given column is a valid filter target and return it or the actual name if it's an alias
+ *
+ * Attempts to join the given column from a different table if its association to the given table cannot be
+ * verified. In case of a PostgreSQL connection and if a COLLATE SQL-instruction is part of the resolved column,
+ * this applies LOWER() on the column and, if given, strtolower() on the filter's expression.
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The name or alias of the column to validate
+ * @param RepositoryQuery $query An optional query to pass as context,
+ * if not given the column is considered being used for a statement filter
+ * @param FilterExpression $filter An optional filter to pass as context
+ *
+ * @return string The given column's name
+ *
+ * @throws QueryException In case the given column is not a valid filter column
+ * @throws ProgrammingError In case the given column is not found in $table and cannot be joined in
+ */
+ public function requireFilterColumn($table, $name, RepositoryQuery $query = null, FilterExpression $filter = null)
+ {
+ if ($name instanceof Zend_Db_Expr) {
+ return $name;
+ }
+
+ $joined = false;
+ if ($query === null) {
+ $column = $this->requireStatementColumn($table, $name);
+ } elseif ($this->validateQueryColumnAssociation($table, $name)) {
+ $column = parent::requireFilterColumn($table, $name, $query, $filter);
+ } else {
+ $column = $this->joinColumn($name, $table, $query);
+ if ($column === null) {
+ if ($query !== null) {
+ // It may be an aliased Zend_Db_Expr
+ $desiredColumns = $query->getColumns();
+ if (isset($desiredColumns[$name]) && $desiredColumns[$name] instanceof Zend_Db_Expr) {
+ $column = $desiredColumns[$name];
+ }
+ }
+
+ if ($column === null) {
+ throw new ProgrammingError(
+ 'Unable to find a valid table for column "%s" to join into "%s"',
+ $name,
+ $table
+ );
+ }
+ } else {
+ $joined = true;
+ }
+ }
+
+ if (! empty($this->caseInsensitiveColumns)) {
+ if ($joined) {
+ $table = $this->findTableName($name, $table);
+ }
+
+ if ($column === $name) {
+ if ($query === null) {
+ $name = $this->reassembleStatementColumnAlias($table, $name);
+ } else {
+ $name = $this->reassembleQueryColumnAlias($table, $name);
+ }
+ }
+
+ if (isset($this->caseInsensitiveColumns[$table][$name])) {
+ $column = 'LOWER(' . $column . ')';
+ if ($filter !== null) {
+ $expression = $filter->getExpression();
+ if (is_array($expression)) {
+ $filter->setExpression(array_map('strtolower', $expression));
+ } else {
+ $filter->setExpression(strtolower($expression));
+ }
+ }
+ }
+ }
+
+ return $column;
+ }
+
+ /**
+ * Return the statement column name for the given alias or null in case the alias does not exist
+ *
+ * @param string $table
+ * @param string $alias
+ *
+ * @return string|null
+ */
+ public function resolveStatementColumnAlias($table, $alias)
+ {
+ $statementAliasColumnMap = $this->getStatementAliasColumnMap();
+ if (isset($statementAliasColumnMap[$alias])) {
+ return $statementAliasColumnMap[$alias];
+ }
+
+ $prefixedAlias = $table . '.' . $alias;
+ if (isset($statementAliasColumnMap[$prefixedAlias])) {
+ return $statementAliasColumnMap[$prefixedAlias];
+ }
+ }
+
+ /**
+ * Return the alias for the given statement column name or null in case the statement column does not exist
+ *
+ * @param string $table
+ * @param string $column
+ *
+ * @return string|null
+ */
+ public function reassembleStatementColumnAlias($table, $column)
+ {
+ $statementColumnAliasMap = $this->getStatementColumnAliasMap();
+ if (isset($statementColumnAliasMap[$column])) {
+ return $statementColumnAliasMap[$column];
+ }
+
+ $prefixedColumn = $table . '.' . $column;
+ if (isset($statementColumnAliasMap[$prefixedColumn])) {
+ return $statementColumnAliasMap[$prefixedColumn];
+ }
+ }
+
+ /**
+ * Return whether the given alias or statement column name is available in the given table
+ *
+ * @param string $table
+ * @param string $alias
+ *
+ * @return bool
+ */
+ public function validateStatementColumnAssociation($table, $alias)
+ {
+ $statementAliasTableMap = $this->getStatementAliasTableMap();
+ if (isset($statementAliasTableMap[$alias])) {
+ return $statementAliasTableMap[$alias] === $table;
+ }
+
+ $prefixedAlias = $table . '.' . $alias;
+ if (isset($statementAliasTableMap[$prefixedAlias])) {
+ return true;
+ }
+
+ $statementColumnTableMap = $this->getStatementColumnTableMap();
+ if (isset($statementColumnTableMap[$alias])) {
+ return $statementColumnTableMap[$alias] === $table;
+ }
+
+ return isset($statementColumnTableMap[$prefixedAlias]);
+ }
+
+ /**
+ * Return whether the given column name or alias of the given table is a valid statement column
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The column name or alias to check
+ *
+ * @return bool
+ */
+ public function hasStatementColumn($table, $name)
+ {
+ if (($this->resolveStatementColumnAlias($table, $name) === null
+ && $this->reassembleStatementColumnAlias($table, $name) === null)
+ || !$this->validateStatementColumnAssociation($table, $name)
+ ) {
+ return parent::hasStatementColumn($table, $name);
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate that the given column is a valid statement column and return it or the actual name if it's an alias
+ *
+ * @param string $table The table for which to require the column
+ * @param string $name The name or alias of the column to validate
+ *
+ * @return string The given column's name
+ *
+ * @throws StatementException In case the given column is not a statement column
+ */
+ public function requireStatementColumn($table, $name)
+ {
+ if (($column = $this->resolveStatementColumnAlias($table, $name)) !== null) {
+ $alias = $name;
+ } elseif (($alias = $this->reassembleStatementColumnAlias($table, $name)) !== null) {
+ $column = $name;
+ } else {
+ return parent::requireStatementColumn($table, $name);
+ }
+
+ if (! $this->validateStatementColumnAssociation($table, $alias)) {
+ throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
+ }
+
+ return $column;
+ }
+
+ /**
+ * Join alias or column $name into $table using $query
+ *
+ * Attempts to find a valid table for the given alias or column name and a method labelled join<TableName>
+ * to process the actual join logic. If neither of those is found, null is returned.
+ * The method is called with the same parameters but in reversed order.
+ *
+ * @param string $name The alias or column name to join into $target
+ * @param string $target The table to join $name into
+ * @param RepositoryQuery $query The query to apply the JOIN-clause on
+ *
+ * @return string|null The resolved alias or $name, null if no join logic is found
+ */
+ public function joinColumn($name, $target, RepositoryQuery $query)
+ {
+ if (! ($tableName = $this->findTableName($name, $target))) {
+ return;
+ }
+
+ if (($column = $this->resolveQueryColumnAlias($tableName, $name)) === null) {
+ $column = $name;
+ }
+
+ if (($joinIdentifier = $this->resolveTableAlias($tableName)) === null) {
+ $joinIdentifier = $this->prependTablePrefix($tableName);
+ }
+ if ($query->getQuery()->hasJoinedTable($joinIdentifier)) {
+ return $column;
+ }
+
+ $joinMethod = 'join' . StringHelper::cname($tableName);
+ if (! method_exists($this, $joinMethod)) {
+ throw new ProgrammingError(
+ 'Unable to join table "%s" into "%s". Method "%s" not found',
+ $tableName,
+ $target,
+ $joinMethod
+ );
+ }
+
+ $this->$joinMethod($query, $target, $name);
+ return $column;
+ }
+
+ /**
+ * Return the table name for the given alias or column name
+ *
+ * @param string $column The alias or column name
+ * @param string $origin The base table of a SELECT query
+ *
+ * @return string|null null in case no table is found
+ */
+ protected function findTableName($column, $origin)
+ {
+ // First, try to produce an exact match since it's faster and cheaper
+ $aliasTableMap = $this->getAliasTableMap();
+ if (isset($aliasTableMap[$column])) {
+ $table = $aliasTableMap[$column];
+ } else {
+ $columnTableMap = $this->getColumnTableMap();
+ if (isset($columnTableMap[$column])) {
+ $table = $columnTableMap[$column];
+ }
+ }
+
+ // But only return it if it's a probable join...
+ $joinProbabilities = $this->getJoinProbabilities();
+ if (isset($joinProbabilities[$origin])) {
+ $probableJoins = $joinProbabilities[$origin];
+ }
+
+ // ...if probability can be determined
+ if (isset($table) && (empty($probableJoins) || in_array($table, $probableJoins, true))) {
+ return $table;
+ }
+
+ // Without a proper exact match, there is only one fast and cheap way to find a suitable table..
+ if (! empty($probableJoins)) {
+ foreach ($probableJoins as $table) {
+ if (isset($aliasTableMap[$table . '.' . $column])) {
+ return $table;
+ }
+ }
+ }
+
+ // Last chance to find a table. Though, this usually ends up with a QueryException..
+ foreach ($aliasTableMap as $prefixedAlias => $table) {
+ if (strpos($prefixedAlias, '.') !== false) {
+ list($_, $alias) = explode('.', $prefixedAlias, 2);
+ if ($alias === $column) {
+ return $table;
+ }
+ }
+ }
+ }
+}
diff --git a/library/Icinga/Repository/IniRepository.php b/library/Icinga/Repository/IniRepository.php
new file mode 100644
index 0000000..2519d03
--- /dev/null
+++ b/library/Icinga/Repository/IniRepository.php
@@ -0,0 +1,418 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Repository;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Extensible;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Updatable;
+use Icinga\Data\Reducible;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\StatementException;
+
+/**
+ * Abstract base class for concrete INI repository implementations
+ *
+ * Additionally provided features:
+ * <ul>
+ * <li>Insert, update and delete capabilities</li>
+ * <li>Triggers for inserts, updates and deletions</li>
+ * <li>Lazy initialization of table specific configs</li>
+ * </ul>
+ */
+abstract class IniRepository extends Repository implements Extensible, Updatable, Reducible
+{
+ /**
+ * The configuration files used as table specific datasources
+ *
+ * This must be initialized by concrete repository implementations, in the following format
+ * <code>
+ * array(
+ * 'table_name' => array(
+ * 'name' => 'name_of_the_ini_file_without_extension',
+ * 'keyColumn' => 'the_name_of_the_column_to_use_as_key_column',
+ * ['module' => 'the_name_of_the_module_if_any']
+ * )
+ * )
+ * </code>
+ *
+ * @var array
+ */
+ protected $configs;
+
+ /**
+ * The tables for which triggers are available when inserting, updating or deleting rows
+ *
+ * This may be initialized by concrete repository implementations and describes for which table names triggers
+ * are available. The repository attempts to find a method depending on the type of event and table for which
+ * to run the trigger. The name of such a method is expected to be declared using lowerCamelCase.
+ * (e.g. group_membership will be translated to onUpdateGroupMembership and groupmembership will be translated
+ * to onUpdateGroupmembership) The available events are onInsert, onUpdate and onDelete.
+ *
+ * @var array
+ */
+ protected $triggers;
+
+ /**
+ * Create a new INI repository object
+ *
+ * @param Config|null $ds The data source to use
+ *
+ * @throws ProgrammingError In case the given data source does not provide a valid key column
+ */
+ public function __construct(Config $ds = null)
+ {
+ parent::__construct($ds); // First! Due to init().
+
+ if ($ds !== null && !$ds->getConfigObject()->getKeyColumn()) {
+ throw new ProgrammingError('INI repositories require their data source to provide a valid key column');
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return Config
+ */
+ public function getDataSource($table = null)
+ {
+ if ($this->ds !== null) {
+ return parent::getDataSource($table);
+ }
+
+ $table = $table ?: $this->getBaseTable();
+ $configs = $this->getConfigs();
+ if (! isset($configs[$table])) {
+ throw new ProgrammingError('Config for table "%s" missing', $table);
+ } elseif (! $configs[$table] instanceof Config) {
+ $configs[$table] = $this->createConfig($configs[$table], $table);
+ }
+
+ if (! $configs[$table]->getConfigObject()->getKeyColumn()) {
+ throw new ProgrammingError(
+ 'INI repositories require their data source to provide a valid key column'
+ );
+ }
+
+ return $configs[$table];
+ }
+
+ /**
+ * Return the configuration files used as table specific datasources
+ *
+ * Calls $this->initializeConfigs() in case $this->configs is null.
+ *
+ * @return array
+ */
+ public function getConfigs()
+ {
+ if ($this->configs === null) {
+ $this->configs = $this->initializeConfigs();
+ }
+
+ return $this->configs;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the configs lazily
+ *
+ * @return array
+ */
+ protected function initializeConfigs()
+ {
+ return array();
+ }
+
+ /**
+ * Return the tables for which triggers are available when inserting, updating or deleting rows
+ *
+ * Calls $this->initializeTriggers() in case $this->triggers is null.
+ *
+ * @return array
+ */
+ public function getTriggers()
+ {
+ if ($this->triggers === null) {
+ $this->triggers = $this->initializeTriggers();
+ }
+
+ return $this->triggers;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the triggers lazily
+ *
+ * @return array
+ */
+ protected function initializeTriggers()
+ {
+ return array();
+ }
+
+ /**
+ * Run a trigger for the given table and row which is about to be inserted
+ *
+ * @param string $table
+ * @param ConfigObject $new
+ *
+ * @return ConfigObject
+ */
+ public function onInsert($table, ConfigObject $new)
+ {
+ $trigger = $this->getTrigger($table, 'onInsert');
+ if ($trigger !== null) {
+ $row = $this->$trigger($new);
+ if ($row !== null) {
+ $new = $row;
+ }
+ }
+
+ return $new;
+ }
+
+ /**
+ * Run a trigger for the given table and row which is about to be updated
+ *
+ * @param string $table
+ * @param ConfigObject $old
+ * @param ConfigObject $new
+ *
+ * @return ConfigObject
+ */
+ public function onUpdate($table, ConfigObject $old, ConfigObject $new)
+ {
+ $trigger = $this->getTrigger($table, 'onUpdate');
+ if ($trigger !== null) {
+ $row = $this->$trigger($old, $new);
+ if ($row !== null) {
+ $new = $row;
+ }
+ }
+
+ return $new;
+ }
+
+ /**
+ * Run a trigger for the given table and row which has been deleted
+ *
+ * @param string $table
+ * @param ConfigObject $old
+ */
+ public function onDelete($table, ConfigObject $old)
+ {
+ $trigger = $this->getTrigger($table, 'onDelete');
+ if ($trigger !== null) {
+ $this->$trigger($old);
+ }
+ }
+
+ /**
+ * Return the name of the trigger method for the given table and event-type
+ *
+ * @param string $table The table name for which to return a trigger method
+ * @param string $event The name of the event type
+ *
+ * @return ?string
+ */
+ protected function getTrigger($table, $event)
+ {
+ if (! in_array($table, $this->getTriggers())) {
+ return;
+ }
+
+ $identifier = join('', array_map('ucfirst', explode('_', $table)));
+ if (method_exists($this, $event . $identifier)) {
+ return $event . $identifier;
+ }
+ }
+
+ /**
+ * Insert the given data for the given target
+ *
+ * $data must provide a proper value for the data source's key column.
+ *
+ * @param string $target
+ * @param array $data
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function insert($target, array $data)
+ {
+ $ds = $this->getDataSource($target);
+ $newData = $this->requireStatementColumns($target, $data);
+
+ $config = $this->onInsert($target, new ConfigObject($newData));
+ $section = $this->extractSectionName($config, $ds->getConfigObject()->getKeyColumn());
+
+ if ($ds->hasSection($section)) {
+ throw new StatementException(t('Cannot insert. Section "%s" does already exist'), $section);
+ }
+
+ $ds->setSection($section, $config);
+
+ try {
+ $ds->saveIni();
+ } catch (Exception $e) {
+ throw new StatementException(t('Failed to insert. An error occurred: %s'), $e->getMessage());
+ }
+ }
+
+ /**
+ * Update the target with the given data and optionally limit the affected entries by using a filter
+ *
+ * @param string $target
+ * @param array $data
+ * @param Filter $filter
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function update($target, array $data, Filter $filter = null)
+ {
+ $ds = $this->getDataSource($target);
+ $newData = $this->requireStatementColumns($target, $data);
+
+ $keyColumn = $ds->getConfigObject()->getKeyColumn();
+ if ($filter === null && isset($newData[$keyColumn])) {
+ throw new StatementException(
+ t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
+ $keyColumn
+ );
+ }
+
+ $query = $ds->select();
+ if ($filter !== null) {
+ $query->addFilter($this->requireFilter($target, $filter));
+ }
+
+ /** @var ConfigObject $config */
+ $newSection = null;
+ foreach ($query as $section => $config) {
+ if ($newSection !== null) {
+ throw new StatementException(
+ t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
+ $keyColumn
+ );
+ }
+
+ $newConfig = clone $config;
+ foreach ($newData as $column => $value) {
+ if ($column === $keyColumn) {
+ if ($value !== $config->get($keyColumn)) {
+ $newSection = $value;
+ }
+ } else {
+ $newConfig->$column = $value;
+ }
+ }
+
+ // This is necessary as the query result set contains the key column.
+ unset($newConfig->$keyColumn);
+
+ if ($newSection) {
+ if ($ds->hasSection($newSection)) {
+ throw new StatementException(t('Cannot update. Section "%s" does already exist'), $newSection);
+ }
+
+ $ds->removeSection($section)->setSection(
+ $newSection,
+ $this->onUpdate($target, $config, $newConfig)
+ );
+ } else {
+ $ds->setSection(
+ $section,
+ $this->onUpdate($target, $config, $newConfig)
+ );
+ }
+ }
+
+ try {
+ $ds->saveIni();
+ } catch (Exception $e) {
+ throw new StatementException(t('Failed to update. An error occurred: %s'), $e->getMessage());
+ }
+ }
+
+ /**
+ * Delete entries in the given target, optionally limiting the affected entries by using a filter
+ *
+ * @param string $target
+ * @param Filter $filter
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function delete($target, Filter $filter = null)
+ {
+ $ds = $this->getDataSource($target);
+
+ $query = $ds->select();
+ if ($filter !== null) {
+ $query->addFilter($this->requireFilter($target, $filter));
+ }
+
+ /** @var ConfigObject $config */
+ foreach ($query as $section => $config) {
+ $ds->removeSection($section);
+ $this->onDelete($target, $config);
+ }
+
+ try {
+ $ds->saveIni();
+ } catch (Exception $e) {
+ throw new StatementException(t('Failed to delete. An error occurred: %s'), $e->getMessage());
+ }
+ }
+
+ /**
+ * Create and return a Config for the given meta and table
+ *
+ * @param array $meta
+ * @param string $table
+ *
+ * @return Config
+ *
+ * @throws ProgrammingError In case the given meta is invalid
+ */
+ protected function createConfig(array $meta, $table)
+ {
+ if (! isset($meta['name'])) {
+ throw new ProgrammingError('Config file name missing for table "%s"', $table);
+ } elseif (! isset($meta['keyColumn'])) {
+ throw new ProgrammingError('Config key column name missing for table "%s"', $table);
+ }
+
+ if (isset($meta['module'])) {
+ $config = Config::module($meta['module'], $meta['name']);
+ } else {
+ $config = Config::app($meta['name']);
+ }
+
+ $config->getConfigObject()->setKeyColumn($meta['keyColumn']);
+ return $config;
+ }
+
+ /**
+ * Extract and return the section name off of the given $config
+ *
+ * @param array|ConfigObject $config
+ * @param string $keyColumn
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case no valid section name is available
+ */
+ protected function extractSectionName(&$config, $keyColumn)
+ {
+ if (! is_array($config) && !$config instanceof ConfigObject) {
+ throw new ProgrammingError('$config is neither an array nor a ConfigObject');
+ } elseif (! isset($config[$keyColumn])) {
+ throw new ProgrammingError('$config does not provide a value for key column "%s"', $keyColumn);
+ }
+
+ $section = $config[$keyColumn];
+ unset($config[$keyColumn]);
+ return $section;
+ }
+}
diff --git a/library/Icinga/Repository/LdapRepository.php b/library/Icinga/Repository/LdapRepository.php
new file mode 100644
index 0000000..af3cf00
--- /dev/null
+++ b/library/Icinga/Repository/LdapRepository.php
@@ -0,0 +1,71 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Repository;
+
+use Icinga\Protocol\Ldap\LdapConnection;
+
+/**
+ * Abstract base class for concrete LDAP repository implementations
+ *
+ * Additionally provided features:
+ * <ul>
+ * <li>Attribute name normalization</li>
+ * </ul>
+ */
+abstract class LdapRepository extends Repository
+{
+ /**
+ * The datasource being used
+ *
+ * @var LdapConnection
+ */
+ protected $ds;
+
+ /**
+ * Normed attribute names based on known LDAP environments
+ *
+ * @var array
+ */
+ protected $normedAttributes = array(
+ 'uid' => 'uid',
+ 'gid' => 'gid',
+ 'user' => 'user',
+ 'group' => 'group',
+ 'member' => 'member',
+ 'memberuid' => 'memberUid',
+ 'posixgroup' => 'posixGroup',
+ 'uniquemember' => 'uniqueMember',
+ 'groupofnames' => 'groupOfNames',
+ 'inetorgperson' => 'inetOrgPerson',
+ 'samaccountname' => 'sAMAccountName',
+ 'groupofuniquenames' => 'groupOfUniqueNames'
+ );
+
+ /**
+ * Create a new LDAP repository object
+ *
+ * @param LdapConnection $ds The data source to use
+ */
+ public function __construct(LdapConnection $ds)
+ {
+ parent::__construct($ds);
+ }
+
+ /**
+ * Return the given attribute name normed to known LDAP enviroments, if possible
+ *
+ * @param ?string $name
+ *
+ * @return string
+ */
+ protected function getNormedAttribute($name)
+ {
+ $loweredName = strtolower($name ?? '');
+ if (array_key_exists($loweredName, $this->normedAttributes)) {
+ return $this->normedAttributes[$loweredName];
+ }
+
+ return $name;
+ }
+}
diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php
new file mode 100644
index 0000000..404f1f6
--- /dev/null
+++ b/library/Icinga/Repository/Repository.php
@@ -0,0 +1,1261 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Repository;
+
+use DateTime;
+use Icinga\Application\Logger;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\Selectable;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\QueryException;
+use Icinga\Exception\StatementException;
+use Icinga\Util\ASN1;
+use Icinga\Util\StringHelper;
+use InvalidArgumentException;
+
+/**
+ * Abstract base class for concrete repository implementations
+ *
+ * To utilize this class and its features, the following is required:
+ * <ul>
+ * <li>Concrete implementations need to initialize Repository::$queryColumns</li>
+ * <li>The datasource passed to a repository must implement the Selectable interface</li>
+ * <li>The datasource must yield an instance of Queryable when its select() method is called</li>
+ * </ul>
+ */
+abstract class Repository implements Selectable
+{
+ /**
+ * The format to use when converting values of type date_time
+ */
+ const DATETIME_FORMAT = 'd/m/Y g:i A';
+
+ /**
+ * The name of this repository
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * The datasource being used
+ *
+ * @var Selectable
+ */
+ protected $ds;
+
+ /**
+ * The base table name this repository is responsible for
+ *
+ * This will be automatically set to the first key of $queryColumns if not explicitly set.
+ *
+ * @var string
+ */
+ protected $baseTable;
+
+ /**
+ * The virtual tables being provided
+ *
+ * This may be initialized by concrete repository implementations with an array
+ * where a key is the name of a virtual table and its value the real table name.
+ *
+ * @var array
+ */
+ protected $virtualTables;
+
+ /**
+ * The query columns being provided
+ *
+ * This must be initialized by concrete repository implementations, in the following format
+ * <code>
+ * array(
+ * 'baseTable' => array(
+ * 'column1',
+ * 'alias1' => 'column2',
+ * 'alias2' => 'column3'
+ * )
+ * )
+ * </code>
+ *
+ * @var array
+ */
+ protected $queryColumns;
+
+ /**
+ * The columns (or aliases) which are not permitted to be queried
+ *
+ * Blacklisted query columns can still occur in a filter expression or sort rule.
+ *
+ * @var array An array of strings
+ */
+ protected $blacklistedQueryColumns;
+
+ /**
+ * Whether the blacklisted query columns are in the legacy format
+ *
+ * @var bool
+ */
+ protected $legacyBlacklistedQueryColumns;
+
+ /**
+ * The filter columns being provided
+ *
+ * This may be intialized by concrete repository implementations, in the following format
+ * <code>
+ * array(
+ * 'alias_or_column_name',
+ * 'label_to_show_in_the_filter_editor' => 'alias_or_column_name'
+ * )
+ * </code>
+ *
+ * @var array
+ */
+ protected $filterColumns;
+
+ /**
+ * Whether the provided filter columns are in the legacy format
+ *
+ * @var bool
+ */
+ protected $legacyFilterColumns;
+
+ /**
+ * The search columns (or aliases) being provided
+ *
+ * @var array An array of strings
+ */
+ protected $searchColumns;
+
+ /**
+ * Whether the provided search columns are in the legacy format
+ *
+ * @var bool
+ */
+ protected $legacySearchColumns;
+
+ /**
+ * The sort rules to be applied on a query
+ *
+ * This may be initialized by concrete repository implementations, in the following format
+ * <code>
+ * array(
+ * 'alias_or_column_name' => array(
+ * 'order' => 'asc'
+ * ),
+ * 'alias_or_column_name' => array(
+ * 'columns' => array(
+ * 'once_more_the_alias_or_column_name_as_in_the_parent_key',
+ * 'an_additional_alias_or_column_name_with_a_specific_direction asc'
+ * ),
+ * 'order' => 'desc'
+ * ),
+ * 'alias_or_column_name' => array(
+ * 'columns' => array('a_different_alias_or_column_name_designated_to_act_as_the_only_sort_column')
+ * // Ascendant sort by default
+ * )
+ * )
+ * </code>
+ * Note that it's mandatory to supply the alias name in case there is one.
+ *
+ * @var array
+ */
+ protected $sortRules;
+
+ /**
+ * Whether the provided sort rules are in the legacy format
+ *
+ * @var bool
+ */
+ protected $legacySortRules;
+
+ /**
+ * The value conversion rules to apply on a query or statement
+ *
+ * This may be initialized by concrete repository implementations and describes for which aliases or column
+ * names what type of conversion is available. For entries, where the key is the alias/column and the value
+ * is the type identifier, the repository attempts to find a conversion method for the alias/column first and,
+ * if none is found, then for the type. If an entry only provides a value, which is the alias/column, the
+ * repository only attempts to find a conversion method for the alias/column. The name of a conversion method
+ * is expected to be declared using lowerCamelCase. (e.g. user_name will be translated to persistUserName and
+ * groupname will be translated to retrieveGroupname)
+ *
+ * @var array
+ */
+ protected $conversionRules;
+
+ /**
+ * An array to map table names to aliases
+ *
+ * @var array
+ */
+ protected $aliasTableMap;
+
+ /**
+ * A flattened array to map query columns to aliases
+ *
+ * @var array
+ */
+ protected $aliasColumnMap;
+
+ /**
+ * An array to map table names to query columns
+ *
+ * @var array
+ */
+ protected $columnTableMap;
+
+ /**
+ * A flattened array to map aliases to query columns
+ *
+ * @var array
+ */
+ protected $columnAliasMap;
+
+ /**
+ * Create a new repository object
+ *
+ * @param Selectable|null $ds The datasource to use.
+ * Only pass null if you have overridden {@link getDataSource()}!
+ */
+ public function __construct(Selectable $ds = null)
+ {
+ $this->ds = $ds;
+ $this->aliasTableMap = array();
+ $this->aliasColumnMap = array();
+ $this->columnTableMap = array();
+ $this->columnAliasMap = array();
+
+ $this->init();
+ }
+
+ /**
+ * Initialize this repository
+ *
+ * Supposed to be overwritten by concrete repository implementations.
+ */
+ protected function init()
+ {
+ }
+
+ /**
+ * Set this repository's name
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * Return this repository's name
+ *
+ * In case no name has been explicitly set yet, the class name is returned.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name ?: __CLASS__;
+ }
+
+ /**
+ * Return the datasource being used for the given table
+ *
+ * @param string $table
+ *
+ * @return Selectable
+ *
+ * @throws ProgrammingError In case no datasource is available
+ */
+ public function getDataSource($table = null)
+ {
+ if ($this->ds === null) {
+ throw new ProgrammingError(
+ 'No data source available. It is required to either pass it'
+ . ' at initialization time or by overriding this method.'
+ );
+ }
+
+ return $this->ds;
+ }
+
+ /**
+ * Return the base table name this repository is responsible for
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case no base table name has been set and
+ * $this->queryColumns does not provide one either
+ */
+ public function getBaseTable()
+ {
+ if ($this->baseTable === null) {
+ $queryColumns = $this->getQueryColumns();
+ reset($queryColumns);
+ $this->baseTable = key($queryColumns);
+ if (is_int($this->baseTable) || !is_array($queryColumns[$this->baseTable])) {
+ throw new ProgrammingError('"%s" is not a valid base table', $this->baseTable);
+ }
+ }
+
+ return $this->baseTable;
+ }
+
+ /**
+ * Return the virtual tables being provided
+ *
+ * Calls $this->initializeVirtualTables() in case $this->virtualTables is null.
+ *
+ * @return array
+ */
+ public function getVirtualTables()
+ {
+ if ($this->virtualTables === null) {
+ $this->virtualTables = $this->initializeVirtualTables();
+ }
+
+ return $this->virtualTables;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the virtual tables lazily
+ *
+ * @return array
+ */
+ protected function initializeVirtualTables()
+ {
+ return array();
+ }
+
+ /**
+ * Return the query columns being provided
+ *
+ * Calls $this->initializeQueryColumns() in case $this->queryColumns is null.
+ *
+ * @return array
+ */
+ public function getQueryColumns()
+ {
+ if ($this->queryColumns === null) {
+ $this->queryColumns = $this->initializeQueryColumns();
+ }
+
+ return $this->queryColumns;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the query columns lazily
+ *
+ * @return array
+ */
+ protected function initializeQueryColumns()
+ {
+ return array();
+ }
+
+ /**
+ * Return the columns (or aliases) which are not permitted to be queried
+ *
+ * Calls $this->initializeBlacklistedQueryColumns() in case $this->blacklistedQueryColumns is null.
+ *
+ * @param string $table
+ *
+ * @return array
+ */
+ public function getBlacklistedQueryColumns($table = null)
+ {
+ if ($this->blacklistedQueryColumns === null) {
+ $this->legacyBlacklistedQueryColumns = false;
+
+ $blacklistedQueryColumns = $this->initializeBlacklistedQueryColumns($table);
+ if (is_int(key($blacklistedQueryColumns))) {
+ $this->blacklistedQueryColumns[$table] = $blacklistedQueryColumns;
+ } else {
+ $this->blacklistedQueryColumns = $blacklistedQueryColumns;
+ }
+ } elseif ($this->legacyBlacklistedQueryColumns === null) {
+ $this->legacyBlacklistedQueryColumns = is_int(key($this->blacklistedQueryColumns));
+ }
+
+ if ($this->legacyBlacklistedQueryColumns) {
+ return $this->blacklistedQueryColumns;
+ } elseif (! isset($this->blacklistedQueryColumns[$table])) {
+ $this->blacklistedQueryColumns[$table] = $this->initializeBlacklistedQueryColumns($table);
+ }
+
+ return $this->blacklistedQueryColumns[$table];
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the
+ * blacklisted query columns lazily or dependent on a query's current base table
+ *
+ * @param string $table
+ *
+ * @return array
+ */
+ protected function initializeBlacklistedQueryColumns()
+ {
+ // $table is not part of the signature due to PHP strict standards
+ return array();
+ }
+
+ /**
+ * Return the filter columns being provided
+ *
+ * Calls $this->initializeFilterColumns() in case $this->filterColumns is null.
+ *
+ * @param string $table
+ *
+ * @return array
+ */
+ public function getFilterColumns($table = null)
+ {
+ if ($this->filterColumns === null) {
+ $this->legacyFilterColumns = false;
+
+ $filterColumns = $this->initializeFilterColumns($table);
+ $foundTables = array_intersect_key($this->getQueryColumns(), $filterColumns);
+ if (empty($foundTables)) {
+ $this->filterColumns[$table] = $filterColumns;
+ } else {
+ $this->filterColumns = $filterColumns;
+ }
+ } elseif ($this->legacyFilterColumns === null) {
+ $foundTables = array_intersect_key($this->getQueryColumns(), $this->filterColumns);
+ $this->legacyFilterColumns = empty($foundTables);
+ }
+
+ if ($this->legacyFilterColumns) {
+ return $this->filterColumns;
+ } elseif (! isset($this->filterColumns[$table])) {
+ $this->filterColumns[$table] = $this->initializeFilterColumns($table);
+ }
+
+ return $this->filterColumns[$table];
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize
+ * the filter columns lazily or dependent on a query's current base table
+ *
+ * @param string $table
+ *
+ * @return array
+ */
+ protected function initializeFilterColumns()
+ {
+ // $table is not part of the signature due to PHP strict standards
+ return array();
+ }
+
+ /**
+ * Return the search columns being provided
+ *
+ * Calls $this->initializeSearchColumns() in case $this->searchColumns is null.
+ *
+ * @param string $table
+ *
+ * @return array
+ */
+ public function getSearchColumns($table = null)
+ {
+ if ($this->searchColumns === null) {
+ $this->legacySearchColumns = false;
+
+ $searchColumns = $this->initializeSearchColumns($table);
+ if (is_int(key($searchColumns))) {
+ $this->searchColumns[$table] = $searchColumns;
+ } else {
+ $this->searchColumns = $searchColumns;
+ }
+ } elseif ($this->legacySearchColumns === null) {
+ $this->legacySearchColumns = is_int(key($this->searchColumns));
+ }
+
+ if ($this->legacySearchColumns) {
+ return $this->searchColumns;
+ } elseif (! isset($this->searchColumns[$table])) {
+ $this->searchColumns[$table] = $this->initializeSearchColumns($table);
+ }
+
+ return $this->searchColumns[$table];
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize
+ * the search columns lazily or dependent on a query's current base table
+ *
+ * @param string $table
+ *
+ * @return array
+ */
+ protected function initializeSearchColumns()
+ {
+ // $table is not part of the signature due to PHP strict standards
+ return array();
+ }
+
+ /**
+ * Return the sort rules to be applied on a query
+ *
+ * Calls $this->initializeSortRules() in case $this->sortRules is null.
+ *
+ * @param string $table
+ *
+ * @return array
+ */
+ public function getSortRules($table = null)
+ {
+ if ($this->sortRules === null) {
+ $this->legacySortRules = false;
+
+ $sortRules = $this->initializeSortRules($table);
+ $foundTables = array_intersect_key($this->getQueryColumns(), $sortRules);
+ if (empty($foundTables)) {
+ $this->sortRules[$table] = $sortRules;
+ } else {
+ $this->sortRules = $sortRules;
+ }
+ } elseif ($this->legacySortRules === null) {
+ $foundTables = array_intersect_key($this->getQueryColumns(), $this->sortRules);
+ $this->legacySortRules = empty($foundTables);
+ }
+
+ if ($this->legacySortRules) {
+ return $this->sortRules;
+ } elseif (! isset($this->sortRules[$table])) {
+ $this->sortRules[$table] = $this->initializeSortRules($table);
+ }
+
+ return $this->sortRules[$table];
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize
+ * the sort rules lazily or dependent on a query's current base table
+ *
+ * @param string $table
+ *
+ * @return array
+ */
+ protected function initializeSortRules()
+ {
+ // $table is not part of the signature due to PHP strict standards
+ return array();
+ }
+
+ /**
+ * Return the value conversion rules to apply on a query
+ *
+ * Calls $this->initializeConversionRules() in case $this->conversionRules is null.
+ *
+ * @return array
+ */
+ public function getConversionRules()
+ {
+ if ($this->conversionRules === null) {
+ $this->conversionRules = $this->initializeConversionRules();
+ }
+
+ return $this->conversionRules;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the conversion rules lazily
+ *
+ * @return array
+ */
+ protected function initializeConversionRules()
+ {
+ return array();
+ }
+
+ /**
+ * Return an array to map table names to aliases
+ *
+ * @return array
+ */
+ protected function getAliasTableMap()
+ {
+ if (empty($this->aliasTableMap)) {
+ $this->initializeAliasMaps();
+ }
+
+ return $this->aliasTableMap;
+ }
+
+ /**
+ * Return a flattened array to map query columns to aliases
+ *
+ * @return array
+ */
+ protected function getAliasColumnMap()
+ {
+ if (empty($this->aliasColumnMap)) {
+ $this->initializeAliasMaps();
+ }
+
+ return $this->aliasColumnMap;
+ }
+
+ /**
+ * Return an array to map table names to query columns
+ *
+ * @return array
+ */
+ protected function getColumnTableMap()
+ {
+ if (empty($this->columnTableMap)) {
+ $this->initializeAliasMaps();
+ }
+
+ return $this->columnTableMap;
+ }
+
+ /**
+ * Return a flattened array to map aliases to query columns
+ *
+ * @return array
+ */
+ protected function getColumnAliasMap()
+ {
+ if (empty($this->columnAliasMap)) {
+ $this->initializeAliasMaps();
+ }
+
+ return $this->columnAliasMap;
+ }
+
+ /**
+ * Initialize $this->aliasTableMap and $this->aliasColumnMap
+ *
+ * @throws ProgrammingError In case $this->queryColumns does not provide any column information
+ */
+ protected function initializeAliasMaps()
+ {
+ $queryColumns = $this->getQueryColumns();
+ if (empty($queryColumns)) {
+ throw new ProgrammingError('Repositories are required to initialize $this->queryColumns first');
+ }
+
+ foreach ($queryColumns as $table => $columns) {
+ foreach ($columns as $alias => $column) {
+ if (! is_string($alias)) {
+ $key = $column;
+ } else {
+ $key = $alias;
+ $column = preg_replace('~\n\s*~', ' ', $column);
+ }
+
+ if (array_key_exists($key, $this->aliasTableMap)) {
+ if ($this->aliasTableMap[$key] !== null) {
+ $existingTable = $this->aliasTableMap[$key];
+ $existingColumn = $this->aliasColumnMap[$key];
+ $this->aliasTableMap[$existingTable . '.' . $key] = $existingTable;
+ $this->aliasColumnMap[$existingTable . '.' . $key] = $existingColumn;
+ $this->aliasTableMap[$key] = null;
+ $this->aliasColumnMap[$key] = null;
+ }
+
+ $this->aliasTableMap[$table . '.' . $key] = $table;
+ $this->aliasColumnMap[$table . '.' . $key] = $column;
+ } else {
+ $this->aliasTableMap[$key] = $table;
+ $this->aliasColumnMap[$key] = $column;
+ }
+
+ if (array_key_exists($column, $this->columnTableMap)) {
+ if ($this->columnTableMap[$column] !== null) {
+ $existingTable = $this->columnTableMap[$column];
+ $existingAlias = $this->columnAliasMap[$column];
+ $this->columnTableMap[$existingTable . '.' . $column] = $existingTable;
+ $this->columnAliasMap[$existingTable . '.' . $column] = $existingAlias;
+ $this->columnTableMap[$column] = null;
+ $this->columnAliasMap[$column] = null;
+ }
+
+ $this->columnTableMap[$table . '.' . $column] = $table;
+ $this->columnAliasMap[$table . '.' . $column] = $key;
+ } else {
+ $this->columnTableMap[$column] = $table;
+ $this->columnAliasMap[$column] = $key;
+ }
+ }
+ }
+ }
+
+ /**
+ * Return a new query for the given columns
+ *
+ * @param array $columns The desired columns, if null all columns will be queried
+ *
+ * @return RepositoryQuery
+ */
+ public function select(array $columns = null)
+ {
+ $query = new RepositoryQuery($this);
+ $query->from($this->getBaseTable(), $columns);
+ return $query;
+ }
+
+ /**
+ * Return whether this repository is capable of converting values for the given table and optional column
+ *
+ * @param string $table
+ * @param string $column
+ *
+ * @return bool
+ */
+ public function providesValueConversion($table, $column = null)
+ {
+ $conversionRules = $this->getConversionRules();
+ if (empty($conversionRules)) {
+ return false;
+ }
+
+ if (! isset($conversionRules[$table])) {
+ return false;
+ } elseif ($column === null) {
+ return true;
+ }
+
+ $alias = $this->reassembleQueryColumnAlias($table, $column) ?: $column;
+ return array_key_exists($alias, $conversionRules[$table]) || in_array($alias, $conversionRules[$table]);
+ }
+
+ /**
+ * Convert a value supposed to be transmitted to the data source
+ *
+ * @param string $table The table where to persist the value
+ * @param string $name The alias or column name
+ * @param mixed $value The value to convert
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (Directly passed through to $this->getConverter)
+ *
+ * @return mixed If conversion was possible, the converted value,
+ * otherwise the unchanged value
+ */
+ public function persistColumn($table, $name, $value, RepositoryQuery $query = null)
+ {
+ $converter = $this->getConverter($table, $name, 'persist', $query);
+ if ($converter !== null) {
+ $value = $this->$converter($value, $name, $table, $query);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Convert a value which was fetched from the data source
+ *
+ * @param string $table The table the value has been fetched from
+ * @param string $name The alias or column name
+ * @param mixed $value The value to convert
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (Directly passed through to $this->getConverter)
+ *
+ * @return mixed If conversion was possible, the converted value,
+ * otherwise the unchanged value
+ */
+ public function retrieveColumn($table, $name, $value, RepositoryQuery $query = null)
+ {
+ $converter = $this->getConverter($table, $name, 'retrieve', $query);
+ if ($converter !== null) {
+ $value = $this->$converter($value, $name, $table, $query);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Return the name of the conversion method for the given alias or column name and context
+ *
+ * @param string $table The datasource's table
+ * @param string $name The alias or column name for which to return a conversion method
+ * @param string $context The context of the conversion: persist or retrieve
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (unused by the base implementation)
+ *
+ * @return ?string
+ *
+ * @throws ProgrammingError In case a conversion rule is found but not any conversion method
+ */
+ protected function getConverter($table, $name, $context, RepositoryQuery $query = null)
+ {
+ $conversionRules = $this->getConversionRules();
+ if (! isset($conversionRules[$table])) {
+ return null;
+ }
+
+ $tableRules = $conversionRules[$table];
+ if (($alias = $this->reassembleQueryColumnAlias($table, $name)) === null) {
+ $alias = $name;
+ }
+
+ // Check for a conversion method for the alias/column first
+ if (array_key_exists($alias, $tableRules) || in_array($alias, $tableRules)) {
+ $methodName = $context . join('', array_map('ucfirst', explode('_', $alias)));
+ if (method_exists($this, $methodName)) {
+ return $methodName;
+ }
+ }
+
+ // The conversion method for the type is just a fallback, but it is required to exist if defined
+ if (isset($tableRules[$alias])) {
+ $identifier = join('', array_map('ucfirst', explode('_', $tableRules[$alias])));
+ if (! method_exists($this, $context . $identifier)) {
+ // Do not throw an error in case at least one conversion method exists
+ if (! method_exists($this, ($context === 'persist' ? 'retrieve' : 'persist') . $identifier)) {
+ throw new ProgrammingError(
+ 'Cannot find any conversion method for type "%s"'
+ . '. Add a proper conversion method or remove the type definition',
+ $tableRules[$alias]
+ );
+ }
+
+ Logger::debug(
+ 'Conversion method "%s" for type definition "%s" does not exist in repository "%s".',
+ $context . $identifier,
+ $tableRules[$alias],
+ $this->getName()
+ );
+ } else {
+ return $context . $identifier;
+ }
+ }
+ }
+
+ /**
+ * Convert a timestamp or DateTime object to a string formatted using static::DATETIME_FORMAT
+ *
+ * @param mixed $value
+ *
+ * @return string
+ */
+ protected function persistDateTime($value)
+ {
+ if (is_numeric($value)) {
+ $value = date(static::DATETIME_FORMAT, $value);
+ } elseif ($value instanceof DateTime) {
+ $value = date(static::DATETIME_FORMAT, $value->getTimestamp()); // Using date here, to ignore any timezone
+ } elseif ($value !== null) {
+ throw new ProgrammingError(
+ 'Cannot persist value "%s" as type date_time. It\'s not a timestamp or DateTime object',
+ $value
+ );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Convert a string formatted using static::DATETIME_FORMAT to a unix timestamp
+ *
+ * @param string $value
+ *
+ * @return int
+ */
+ protected function retrieveDateTime($value)
+ {
+ if (is_numeric($value)) {
+ $value = (int) $value;
+ } elseif (is_string($value)) {
+ $dateTime = DateTime::createFromFormat(static::DATETIME_FORMAT, $value);
+ if ($dateTime === false) {
+ Logger::debug(
+ 'Unable to parse string "%s" as type date_time with format "%s" in repository "%s"',
+ $value,
+ static::DATETIME_FORMAT,
+ $this->getName()
+ );
+ $value = null;
+ } else {
+ $value = $dateTime->getTimestamp();
+ }
+ } elseif ($value !== null) {
+ throw new ProgrammingError(
+ 'Cannot retrieve value "%s" as type date_time. It\'s not a integer or (numeric) string',
+ $value
+ );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Convert the given array to an comma separated string
+ *
+ * @param array|string $value
+ *
+ * @return string
+ */
+ protected function persistCommaSeparatedString($value)
+ {
+ if (is_array($value)) {
+ $value = join(',', array_map('trim', $value));
+ } elseif ($value !== null && !is_string($value)) {
+ throw new ProgrammingError('Cannot persist value "%s" as comma separated string', $value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Convert the given comma separated string to an array
+ *
+ * @param string $value
+ *
+ * @return array
+ */
+ protected function retrieveCommaSeparatedString($value)
+ {
+ if ($value && is_string($value)) {
+ $value = StringHelper::trimSplit($value);
+ } elseif ($value !== null) {
+ throw new ProgrammingError('Cannot retrieve value "%s" as array. It\'s not a string', $value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation
+ *
+ * @param string|null $value
+ *
+ * @return ?int
+ *
+ * @see https://tools.ietf.org/html/rfc4517#section-3.3.13
+ */
+ protected function retrieveGeneralizedTime($value)
+ {
+ if ($value === null) {
+ return $value;
+ }
+
+ try {
+ return ASN1::parseGeneralizedTime($value)->getTimestamp();
+ } catch (InvalidArgumentException $e) {
+ Logger::debug(sprintf('Repository "%s": %s', $this->getName(), $e->getMessage()));
+ }
+ }
+
+ /**
+ * Validate that the requested table exists and resolve it's real name if necessary
+ *
+ * @param string $table The table to validate
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (unused by the base implementation)
+ *
+ * @return string The table's name, may differ from the given one
+ *
+ * @throws ProgrammingError In case the given table does not exist
+ */
+ public function requireTable($table, RepositoryQuery $query = null)
+ {
+ $queryColumns = $this->getQueryColumns();
+ if (! isset($queryColumns[$table])) {
+ throw new ProgrammingError('Table "%s" not found', $table);
+ }
+
+ $virtualTables = $this->getVirtualTables();
+ if (isset($virtualTables[$table])) {
+ $table = $virtualTables[$table];
+ }
+
+ return $table;
+ }
+
+ /**
+ * Recurse the given filter, require each column for the given table and convert all values
+ *
+ * @param string $table The table being filtered
+ * @param Filter $filter The filter to recurse
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (Directly passed through to $this->requireFilterColumn)
+ * @param bool $clone Whether to clone $filter first
+ *
+ * @return Filter The udpated filter
+ */
+ public function requireFilter($table, Filter $filter, RepositoryQuery $query = null, $clone = true)
+ {
+ if ($clone) {
+ $filter = clone $filter;
+ }
+
+ if ($filter->isExpression()) {
+ $column = $filter->getColumn();
+ $filter->setColumn($this->requireFilterColumn($table, $column, $query, $filter));
+ $filter->setExpression($this->persistColumn($table, $column, $filter->getExpression(), $query));
+ } elseif ($filter->isChain()) {
+ foreach ($filter->filters() as $chainOrExpression) {
+ $this->requireFilter($table, $chainOrExpression, $query, false);
+ }
+ }
+
+ return $filter;
+ }
+
+ /**
+ * Return this repository's query columns of the given table mapped to their respective aliases
+ *
+ * @param string $table
+ *
+ * @return array
+ *
+ * @throws ProgrammingError In case $table does not exist
+ */
+ public function requireAllQueryColumns($table)
+ {
+ $queryColumns = $this->getQueryColumns();
+ if (! array_key_exists($table, $queryColumns)) {
+ throw new ProgrammingError('Table name "%s" not found', $table);
+ }
+
+ $blacklist = $this->getBlacklistedQueryColumns($table);
+ $columns = array();
+ foreach ($queryColumns[$table] as $alias => $column) {
+ $name = is_string($alias) ? $alias : $column;
+ if (! in_array($name, $blacklist)) {
+ $columns[$alias] = $this->resolveQueryColumnAlias($table, $name);
+ }
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Return the query column name for the given alias or null in case the alias does not exist
+ *
+ * @param string $table
+ * @param string $alias
+ *
+ * @return string|null
+ */
+ public function resolveQueryColumnAlias($table, $alias)
+ {
+ $aliasColumnMap = $this->getAliasColumnMap();
+ if (isset($aliasColumnMap[$alias])) {
+ return $aliasColumnMap[$alias];
+ }
+
+ $prefixedAlias = $table . '.' . $alias;
+ if (isset($aliasColumnMap[$prefixedAlias])) {
+ return $aliasColumnMap[$prefixedAlias];
+ }
+ }
+
+ /**
+ * Return the alias for the given query column name or null in case the query column name does not exist
+ *
+ * @param string $table
+ * @param string $column
+ *
+ * @return string|null
+ */
+ public function reassembleQueryColumnAlias($table, $column)
+ {
+ $columnAliasMap = $this->getColumnAliasMap();
+ if (isset($columnAliasMap[$column])) {
+ return $columnAliasMap[$column];
+ }
+
+ $prefixedColumn = $table . '.' . $column;
+ if (isset($columnAliasMap[$prefixedColumn])) {
+ return $columnAliasMap[$prefixedColumn];
+ }
+ }
+
+ /**
+ * Return whether the given alias or query column name is available in the given table
+ *
+ * @param string $table
+ * @param string $alias
+ *
+ * @return bool
+ */
+ public function validateQueryColumnAssociation($table, $alias)
+ {
+ $aliasTableMap = $this->getAliasTableMap();
+ if (isset($aliasTableMap[$alias])) {
+ return $aliasTableMap[$alias] === $table;
+ }
+
+ $prefixedAlias = $table . '.' . $alias;
+ if (isset($aliasTableMap[$prefixedAlias])) {
+ return true;
+ }
+
+ $columnTableMap = $this->getColumnTableMap();
+ if (isset($columnTableMap[$alias])) {
+ return $columnTableMap[$alias] === $table;
+ }
+
+ return isset($columnTableMap[$prefixedAlias]);
+ }
+
+ /**
+ * Return whether the given column name or alias is a valid query column
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The column name or alias to check
+ *
+ * @return bool
+ */
+ public function hasQueryColumn($table, $name)
+ {
+ if ($this->resolveQueryColumnAlias($table, $name) !== null) {
+ $alias = $name;
+ } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) === null) {
+ return false;
+ }
+
+ return !in_array($alias, $this->getBlacklistedQueryColumns($table))
+ && $this->validateQueryColumnAssociation($table, $name);
+ }
+
+ /**
+ * Validate that the given column is a valid query target and return it or the actual name if it's an alias
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The name or alias of the column to validate
+ * @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation)
+ *
+ * @return string The given column's name
+ *
+ * @throws QueryException In case the given column is not a valid query column
+ */
+ public function requireQueryColumn($table, $name, RepositoryQuery $query = null)
+ {
+ if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) {
+ $alias = $name;
+ } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) {
+ $column = $name;
+ } else {
+ throw new QueryException(t('Query column "%s" not found'), $name);
+ }
+
+ if (in_array($alias, $this->getBlacklistedQueryColumns($table))) {
+ throw new QueryException(t('Column "%s" cannot be queried'), $name);
+ }
+
+ if (! $this->validateQueryColumnAssociation($table, $alias)) {
+ throw new QueryException(t('Query column "%s" not found in table "%s"'), $name, $table);
+ }
+
+ return $column;
+ }
+
+ /**
+ * Return whether the given column name or alias is a valid filter column
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The column name or alias to check
+ *
+ * @return bool
+ */
+ public function hasFilterColumn($table, $name)
+ {
+ return ($this->resolveQueryColumnAlias($table, $name) !== null
+ || $this->reassembleQueryColumnAlias($table, $name) !== null)
+ && $this->validateQueryColumnAssociation($table, $name);
+ }
+
+ /**
+ * Validate that the given column is a valid filter target and return it or the actual name if it's an alias
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The name or alias of the column to validate
+ * @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation)
+ * @param FilterExpression $filter An optional filter to pass as context (unused by the base implementation)
+ *
+ * @return string The given column's name
+ *
+ * @throws QueryException In case the given column is not a valid filter column
+ */
+ public function requireFilterColumn($table, $name, RepositoryQuery $query = null, FilterExpression $filter = null)
+ {
+ if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) {
+ $alias = $name;
+ } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) {
+ $column = $name;
+ } else {
+ throw new QueryException(t('Filter column "%s" not found'), $name);
+ }
+
+ if (! $this->validateQueryColumnAssociation($table, $alias)) {
+ throw new QueryException(t('Filter column "%s" not found in table "%s"'), $name, $table);
+ }
+
+ return $column;
+ }
+
+ /**
+ * Return whether the given column name or alias of the given table is a valid statement column
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The column name or alias to check
+ *
+ * @return bool
+ */
+ public function hasStatementColumn($table, $name)
+ {
+ return $this->hasQueryColumn($table, $name);
+ }
+
+ /**
+ * Validate that the given column is a valid statement column and return it or the actual name if it's an alias
+ *
+ * @param string $table The table for which to require the column
+ * @param string $name The name or alias of the column to validate
+ *
+ * @return string The given column's name
+ *
+ * @throws StatementException In case the given column is not a statement column
+ */
+ public function requireStatementColumn($table, $name)
+ {
+ if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) {
+ $alias = $name;
+ } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) {
+ $column = $name;
+ } else {
+ throw new StatementException('Statement column "%s" not found', $name);
+ }
+
+ if (in_array($alias, $this->getBlacklistedQueryColumns($table))) {
+ throw new StatementException('Column "%s" cannot be referenced in a statement', $name);
+ }
+
+ if (! $this->validateQueryColumnAssociation($table, $alias)) {
+ throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
+ }
+
+ return $column;
+ }
+
+ /**
+ * Resolve the given aliases or column names of the given table supposed to be persisted and convert their values
+ *
+ * @param string $table
+ * @param array $data
+ *
+ * @return array
+ */
+ public function requireStatementColumns($table, array $data)
+ {
+ $resolved = array();
+ foreach ($data as $alias => $value) {
+ $resolved[$this->requireStatementColumn($table, $alias)] = $this->persistColumn($table, $alias, $value);
+ }
+
+ return $resolved;
+ }
+}
diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php
new file mode 100644
index 0000000..84f7c6e
--- /dev/null
+++ b/library/Icinga/Repository/RepositoryQuery.php
@@ -0,0 +1,797 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Repository;
+
+use Iterator;
+use IteratorAggregate;
+use Traversable;
+use Icinga\Application\Benchmark;
+use Icinga\Application\Logger;
+use Icinga\Data\QueryInterface;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\FilterColumns;
+use Icinga\Data\SortRules;
+use Icinga\Exception\QueryException;
+
+/**
+ * Query class supposed to mediate between a repository and its datasource's query
+ */
+class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Iterator
+{
+ /**
+ * The repository being used
+ *
+ * @var Repository
+ */
+ protected $repository;
+
+ /**
+ * The real query being used
+ *
+ * @var QueryInterface
+ */
+ protected $query;
+
+ /**
+ * The current target to be queried
+ *
+ * @var mixed
+ */
+ protected $target;
+
+ /**
+ * The real query's iterator
+ *
+ * @var Iterator
+ */
+ protected $iterator;
+
+ /**
+ * This query's custom aliases
+ *
+ * @var array
+ */
+ protected $customAliases;
+
+ /**
+ * Create a new repository query
+ *
+ * @param Repository $repository The repository to use
+ */
+ public function __construct(Repository $repository)
+ {
+ $this->repository = $repository;
+ }
+
+ /**
+ * Clone all state relevant properties of this query
+ */
+ public function __clone()
+ {
+ if ($this->query !== null) {
+ $this->query = clone $this->query;
+ }
+ if ($this->iterator !== null) {
+ $this->iterator = clone $this->iterator;
+ }
+ }
+
+ /**
+ * Return a string representation of this query
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return (string) $this->query;
+ }
+
+ /**
+ * Return the real query being used
+ *
+ * @return QueryInterface
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Set where to fetch which columns
+ *
+ * This notifies the repository about each desired query column.
+ *
+ * @param mixed $target The target from which to fetch the columns
+ * @param array $columns If null or an empty array, all columns will be fetched
+ *
+ * @return $this
+ */
+ public function from($target, array $columns = null)
+ {
+ $this->query = $this->repository->getDataSource($target)->select();
+ $this->query->from($this->repository->requireTable($target, $this));
+ $this->query->columns($this->prepareQueryColumns($target, $columns));
+ $this->target = $target;
+ return $this;
+ }
+
+ /**
+ * Return the columns to fetch
+ *
+ * @return array
+ */
+ public function getColumns()
+ {
+ return $this->query->getColumns();
+ }
+
+ /**
+ * Set which columns to fetch
+ *
+ * This notifies the repository about each desired query column.
+ *
+ * @param array $columns If null or an empty array, all columns will be fetched
+ *
+ * @return $this
+ */
+ public function columns(array $columns)
+ {
+ $this->query->columns($this->prepareQueryColumns($this->target, $columns));
+ return $this;
+ }
+
+ /**
+ * Resolve the given columns supposed to be fetched
+ *
+ * This notifies the repository about each desired query column.
+ *
+ * @param mixed $target The target where to look for each column
+ * @param array $desiredColumns Pass null or an empty array to require all query columns
+ *
+ * @return array The desired columns indexed by their respective alias
+ */
+ protected function prepareQueryColumns($target, array $desiredColumns = null)
+ {
+ $this->customAliases = array();
+ if (empty($desiredColumns)) {
+ $columns = $this->repository->requireAllQueryColumns($target);
+ } else {
+ $columns = array();
+ foreach ($desiredColumns as $customAlias => $columnAlias) {
+ $resolvedColumn = $this->repository->requireQueryColumn($target, $columnAlias, $this);
+ if ($resolvedColumn !== $columnAlias) {
+ if (is_string($customAlias)) {
+ $columns[$customAlias] = $resolvedColumn;
+ $this->customAliases[$customAlias] = $columnAlias;
+ } else {
+ $columns[$columnAlias] = $resolvedColumn;
+ }
+ } elseif (is_string($customAlias)) {
+ $columns[$customAlias] = $columnAlias;
+ $this->customAliases[$customAlias] = $columnAlias;
+ } else {
+ $columns[] = $columnAlias;
+ }
+ }
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Return the native column alias for the given custom alias
+ *
+ * If no custom alias is found with the given name, it is returned unchanged.
+ *
+ * @param string $customAlias
+ *
+ * @return string
+ */
+ protected function getNativeAlias($customAlias)
+ {
+ if (isset($this->customAliases[$customAlias])) {
+ return $this->customAliases[$customAlias];
+ }
+
+ return $customAlias;
+ }
+
+ /**
+ * Return this query's available filter columns with their optional label as key
+ *
+ * @return array
+ */
+ public function getFilterColumns()
+ {
+ return $this->repository->getFilterColumns($this->target);
+ }
+
+ /**
+ * Return this query's available search columns
+ *
+ * @return array
+ */
+ public function getSearchColumns()
+ {
+ return $this->repository->getSearchColumns($this->target);
+ }
+
+ /**
+ * Filter this query using the given column and value
+ *
+ * This notifies the repository about the required filter column.
+ *
+ * @param string $column
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function where($column, $value = null)
+ {
+ $this->addFilter(Filter::where($column, $value));
+ return $this;
+ }
+
+ /**
+ * Add an additional filter expression to this query
+ *
+ * This notifies the repository about each required filter column.
+ *
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function applyFilter(Filter $filter)
+ {
+ return $this->addFilter($filter);
+ }
+
+ /**
+ * Set a filter for this query
+ *
+ * This notifies the repository about each required filter column.
+ *
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function setFilter(Filter $filter)
+ {
+ $this->query->setFilter($this->repository->requireFilter($this->target, $filter, $this));
+ return $this;
+ }
+
+ /**
+ * Add an additional filter expression to this query
+ *
+ * This notifies the repository about each required filter column.
+ *
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function addFilter(Filter $filter)
+ {
+ $this->query->addFilter($this->repository->requireFilter($this->target, $filter, $this));
+ return $this;
+ }
+
+ /**
+ * Return the current filter
+ *
+ * @return Filter
+ */
+ public function getFilter()
+ {
+ return $this->query->getFilter();
+ }
+
+ /**
+ * Return the sort rules being applied on this query
+ *
+ * @return array
+ */
+ public function getSortRules()
+ {
+ return $this->repository->getSortRules($this->target);
+ }
+
+ /**
+ * Add a sort rule for this query
+ *
+ * If called without a specific column, the repository's defaul sort rules will be applied.
+ * This notifies the repository about each column being required as filter column.
+ *
+ * @param string $field The name of the column by which to sort the query's result
+ * @param string $direction The direction to use when sorting (asc or desc, default is asc)
+ * @param bool $ignoreDefault Whether to ignore any default sort rules if $field is given
+ *
+ * @return $this
+ */
+ public function order($field = null, $direction = null, $ignoreDefault = false)
+ {
+ $sortRules = $this->getSortRules();
+ if ($field === null) {
+ // Use first available sort rule as default
+ if (empty($sortRules)) {
+ // Return early in case of no sort defaults and no given $field
+ return $this;
+ }
+
+ $sortColumns = reset($sortRules);
+ if (! array_key_exists('columns', $sortColumns)) {
+ $sortColumns['columns'] = array(key($sortRules));
+ }
+ if ($direction !== null || !array_key_exists('order', $sortColumns)) {
+ $sortColumns['order'] = $direction ?: static::SORT_ASC;
+ }
+ } else {
+ $alias = $this->repository->reassembleQueryColumnAlias($this->target, $field) ?: $field;
+ if (! $ignoreDefault && array_key_exists($alias, $sortRules)) {
+ $sortColumns = $sortRules[$alias];
+ if (! array_key_exists('columns', $sortColumns)) {
+ $sortColumns['columns'] = array($alias);
+ }
+ if ($direction !== null || !array_key_exists('order', $sortColumns)) {
+ $sortColumns['order'] = $direction ?: static::SORT_ASC;
+ }
+ } else {
+ $sortColumns = array(
+ 'columns' => array($alias),
+ 'order' => $direction
+ );
+ }
+ }
+
+ $baseDirection = isset($sortColumns['order']) && strtoupper($sortColumns['order']) === static::SORT_DESC
+ ? static::SORT_DESC
+ : static::SORT_ASC;
+
+ foreach ($sortColumns['columns'] as $column) {
+ list($column, $specificDirection) = $this->splitOrder($column);
+
+ if ($this->hasLimit() && $this->repository->providesValueConversion($this->target, $column)) {
+ Logger::debug(
+ 'Cannot order by column "%s" in repository "%s". The query is'
+ . ' limited and applies value conversion rules on the column',
+ $column,
+ $this->repository->getName()
+ );
+ continue;
+ }
+
+ try {
+ $this->query->order(
+ $this->repository->requireFilterColumn($this->target, $column, $this),
+ $specificDirection ?: $baseDirection
+ // I would have liked the following solution, but hey, a coder should be allowed to produce crap...
+ // $specificDirection && (! $direction || $column !== $field) ? $specificDirection : $baseDirection
+ );
+ } catch (QueryException $_) {
+ Logger::info('Cannot order by column "%s" in repository "%s"', $column, $this->repository->getName());
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Extract and return the name and direction of the given sort column definition
+ *
+ * @param string $field
+ *
+ * @return array An array of two items: $columnName, $direction
+ */
+ protected function splitOrder($field)
+ {
+ $columnAndDirection = explode(' ', $field, 2);
+ if (count($columnAndDirection) === 1) {
+ $column = $field;
+ $direction = null;
+ } else {
+ $column = $columnAndDirection[0];
+ $direction = strtoupper($columnAndDirection[1]) === static::SORT_DESC
+ ? static::SORT_DESC
+ : static::SORT_ASC;
+ }
+
+ return array($column, $direction);
+ }
+
+ /**
+ * Return whether any sort rules were applied to this query
+ *
+ * @return bool
+ */
+ public function hasOrder()
+ {
+ return $this->query->hasOrder();
+ }
+
+ /**
+ * Return the sort rules applied to this query
+ *
+ * @return array
+ */
+ public function getOrder()
+ {
+ return $this->query->getOrder();
+ }
+
+ /**
+ * Set whether this query should peek ahead for more results
+ *
+ * Enabling this causes the current query limit to be increased by one. The potential extra row being yielded will
+ * be removed from the result set. Note that this only applies when fetching multiple results of limited queries.
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
+ public function peekAhead($state = true)
+ {
+ $this->query->peekAhead($state);
+ return $this;
+ }
+
+ /**
+ * Return whether this query did not yield all available results
+ *
+ * @return bool
+ */
+ public function hasMore()
+ {
+ return $this->query->hasMore();
+ }
+
+ /**
+ * Return whether this query will or has yielded any result
+ *
+ * @return bool
+ */
+ public function hasResult()
+ {
+ return $this->query->hasResult();
+ }
+
+ /**
+ * Limit this query's results
+ *
+ * @param int $count When to stop returning results
+ * @param int $offset When to start returning results
+ *
+ * @return $this
+ */
+ public function limit($count = null, $offset = null)
+ {
+ $this->query->limit($count, $offset);
+ return $this;
+ }
+
+ /**
+ * Return whether this query does not return all available entries from its result
+ *
+ * @return bool
+ */
+ public function hasLimit()
+ {
+ return $this->query->hasLimit();
+ }
+
+ /**
+ * Return the limit when to stop returning results
+ *
+ * @return int
+ */
+ public function getLimit()
+ {
+ return $this->query->getLimit();
+ }
+
+ /**
+ * Return whether this query does not start returning results at the very first entry
+ *
+ * @return bool
+ */
+ public function hasOffset()
+ {
+ return $this->query->hasOffset();
+ }
+
+ /**
+ * Return the offset when to start returning results
+ *
+ * @return int
+ */
+ public function getOffset()
+ {
+ return $this->query->getOffset();
+ }
+
+ /**
+ * Fetch and return the first column of this query's first row
+ *
+ * @return mixed|false False in case of no result
+ */
+ public function fetchOne()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $result = $this->query->fetchOne();
+ if ($result !== false && $this->repository->providesValueConversion($this->target)) {
+ $columns = $this->getColumns();
+ $column = isset($columns[0]) ? $columns[0] : $this->getNativeAlias(key($columns));
+ return $this->repository->retrieveColumn($this->target, $column, $result, $this);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Fetch and return the first row of this query's result
+ *
+ * @return object|false False in case of no result
+ */
+ public function fetchRow()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $result = $this->query->fetchRow();
+ if ($result !== false && $this->repository->providesValueConversion($this->target)) {
+ foreach ($this->getColumns() as $alias => $column) {
+ if (! is_string($alias)) {
+ $alias = $column;
+ }
+
+ $result->$alias = $this->repository->retrieveColumn(
+ $this->target,
+ $this->getNativeAlias($alias),
+ $result->$alias,
+ $this
+ );
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Fetch and return the first column of all rows of the result set as an array
+ *
+ * @return array
+ */
+ public function fetchColumn()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $results = $this->query->fetchColumn();
+ if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
+ $columns = $this->getColumns();
+ $aliases = array_keys($columns);
+ $column = is_int($aliases[0]) ? $columns[0] : $this->getNativeAlias($aliases[0]);
+ if ($this->repository->providesValueConversion($this->target, $column)) {
+ foreach ($results as & $value) {
+ $value = $this->repository->retrieveColumn($this->target, $column, $value, $this);
+ }
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Fetch and return all rows of this query's result set as an array of key-value pairs
+ *
+ * The first column is the key, the second column is the value.
+ *
+ * @return array
+ */
+ public function fetchPairs()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $results = $this->query->fetchPairs();
+ if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
+ $columns = $this->getColumns();
+ $aliases = array_keys($columns);
+ $colOne = $aliases[0] !== 0 ? $this->getNativeAlias($aliases[0]) : $columns[0];
+ $colTwo = count($aliases) < 2 ? $colOne : (
+ $aliases[1] !== 1 ? $this->getNativeAlias($aliases[1]) : $columns[1]
+ );
+
+ if ($this->repository->providesValueConversion($this->target, $colOne)
+ || $this->repository->providesValueConversion($this->target, $colTwo)
+ ) {
+ $newResults = array();
+ foreach ($results as $colOneValue => $colTwoValue) {
+ $colOneValue = $this->repository->retrieveColumn($this->target, $colOne, $colOneValue, $this);
+ $newResults[$colOneValue] = $this->repository->retrieveColumn(
+ $this->target,
+ $colTwo,
+ $colTwoValue,
+ $this
+ );
+ }
+
+ $results = $newResults;
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Fetch and return all results of this query
+ *
+ * @return array
+ */
+ public function fetchAll()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $results = $this->query->fetchAll();
+ if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
+ $updateOrder = false;
+ $columns = $this->getColumns();
+ $flippedColumns = array_flip($columns);
+ foreach ($results as $row) {
+ foreach ($columns as $alias => $column) {
+ if (! is_string($alias)) {
+ $alias = $column;
+ }
+
+ $row->$alias = $this->repository->retrieveColumn(
+ $this->target,
+ $this->getNativeAlias($alias),
+ $row->$alias,
+ $this
+ );
+ }
+
+ foreach (($this->getOrder() ?: array()) as $rule) {
+ $nativeAlias = $this->getNativeAlias($rule[0]);
+ if (! array_key_exists($rule[0], $flippedColumns) && property_exists($row, $rule[0])) {
+ if ($this->repository->providesValueConversion($this->target, $nativeAlias)) {
+ $updateOrder = true;
+ $row->{$rule[0]} = $this->repository->retrieveColumn(
+ $this->target,
+ $nativeAlias,
+ $row->{$rule[0]},
+ $this
+ );
+ }
+ } elseif (array_key_exists($rule[0], $flippedColumns)) {
+ if ($this->repository->providesValueConversion($this->target, $nativeAlias)) {
+ $updateOrder = true;
+ }
+ }
+ }
+ }
+
+ if ($updateOrder) {
+ uasort($results, array($this->query, 'compare'));
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Count all results of this query
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return $this->query->count();
+ }
+
+ /**
+ * Return the current position of this query's iterator
+ *
+ * @return int
+ */
+ public function getIteratorPosition()
+ {
+ return $this->query->getIteratorPosition();
+ }
+
+ /**
+ * Start or rewind the iteration
+ */
+ public function rewind(): void
+ {
+ if ($this->iterator === null) {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ if ($this->query instanceof Traversable) {
+ $iterator = $this->query;
+ } else {
+ $iterator = $this->repository->getDataSource($this->target)->query($this->query);
+ }
+
+ if ($iterator instanceof IteratorAggregate) {
+ $this->iterator = $iterator->getIterator();
+ } else {
+ $this->iterator = $iterator;
+ }
+ }
+
+ $this->iterator->rewind();
+ Benchmark::measure('Query result iteration started');
+ }
+
+ /**
+ * Fetch and return the current row of this query's result
+ *
+ * @return mixed
+ */
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ $row = $this->iterator->current();
+ if ($this->repository->providesValueConversion($this->target)) {
+ foreach ($this->getColumns() as $alias => $column) {
+ if (! is_string($alias)) {
+ $alias = $column;
+ }
+
+ $row->$alias = $this->repository->retrieveColumn(
+ $this->target,
+ $this->getNativeAlias($alias),
+ $row->$alias,
+ $this
+ );
+ }
+ }
+
+ return $row;
+ }
+
+ /**
+ * Return whether the current row of this query's result is valid
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ if (! $this->iterator->valid()) {
+ Benchmark::measure('Query result iteration finished');
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return the key for the current row of this query's result
+ *
+ * @return mixed
+ */
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->iterator->key();
+ }
+
+ /**
+ * Advance to the next row of this query's result
+ */
+ public function next(): void
+ {
+ $this->iterator->next();
+ }
+}
diff --git a/library/Icinga/Security/SecurityException.php b/library/Icinga/Security/SecurityException.php
new file mode 100644
index 0000000..861dcf1
--- /dev/null
+++ b/library/Icinga/Security/SecurityException.php
@@ -0,0 +1,13 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Security;
+
+use Icinga\Exception\IcingaException;
+
+/**
+ * Exception thrown when a caller does not have the permissions required to access a resource
+ */
+class SecurityException extends IcingaException
+{
+}
diff --git a/library/Icinga/Test/BaseTestCase.php b/library/Icinga/Test/BaseTestCase.php
new file mode 100644
index 0000000..1283013
--- /dev/null
+++ b/library/Icinga/Test/BaseTestCase.php
@@ -0,0 +1,313 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Test {
+
+ use Exception;
+ use Icinga\Util\Csp;
+ use Icinga\Web\Request;
+ use Icinga\Web\Response;
+ use Icinga\Web\Session;
+ use ipl\I18n\NoopTranslator;
+ use ipl\I18n\StaticTranslator;
+ use RuntimeException;
+ use Mockery;
+ use Icinga\Application\Icinga;
+ use Icinga\Data\ConfigObject;
+ use Icinga\Data\ResourceFactory;
+ use Icinga\Data\Db\DbConnection;
+ use Tests\Icinga\Lib\FakeSession;
+
+ /**
+ * Class BaseTestCase
+ */
+ abstract class BaseTestCase extends Mockery\Adapter\Phpunit\MockeryTestCase implements DbTest
+ {
+ /**
+ * Path to application/
+ *
+ * @var string
+ * @deprecated Use Icinga::app()->getApplicationDir() instead
+ */
+ public static $appDir;
+
+ /**
+ * Path to library/Icinga
+ *
+ * @var string
+ * @deprecated Use Icinga::app()->getLibraryDir('Icinga') instead
+ */
+ public static $libDir;
+
+ /**
+ * Path to etc/
+ *
+ * @var string
+ * @deprecated Use Icinga::app()->getBaseDir('etc') instead
+ */
+ public static $etcDir;
+
+ /**
+ * Path to test/php/
+ *
+ * @var string
+ * @deprecated Use Icinga::app()->getBaseDir('test/php') instead
+ */
+ public static $testDir;
+
+ /**
+ * Path to share/icinga2-web
+ *
+ * @var string
+ * @deprecated Unused
+ */
+ public static $shareDir;
+
+ /**
+ * Path to modules/
+ *
+ * @var string
+ * @deprecated Use Icinga::app()->getModuleManager()->getModuleDirs() instead
+ */
+ public static $moduleDir;
+
+ /**
+ * Resource configuration for different database types
+ *
+ * @var array
+ */
+ protected static $dbConfiguration = array(
+ 'mysql' => array(
+ 'type' => 'db',
+ 'db' => 'mysql',
+ 'host' => '127.0.0.1',
+ 'port' => 3306,
+ 'dbname' => 'icinga_unittest',
+ 'username' => 'icinga_unittest',
+ 'password' => 'icinga_unittest'
+ ),
+ 'pgsql' => array(
+ 'type' => 'db',
+ 'db' => 'pgsql',
+ 'host' => '127.0.0.1',
+ 'port' => 5432,
+ 'dbname' => 'icinga_unittest',
+ 'username' => 'icinga_unittest',
+ 'password' => 'icinga_unittest'
+ ),
+ );
+
+ /** @var Request */
+ private $requestMock;
+
+ /** @var Response */
+ private $responseMock;
+
+ /**
+ * Setup MVC bootstrapping and ensure that the Icinga-Mock gets reinitialized
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->setupRequestMock();
+ $this->setupResponseMock();
+ Session::create(new FakeSession());
+ Csp::createNonce();
+
+ StaticTranslator::$instance = new NoopTranslator();
+ }
+
+ private function setupRequestMock()
+ {
+ $this->requestMock = Mockery::mock('Icinga\Web\Request')->shouldDeferMissing();
+ $this->requestMock->shouldReceive('getPathInfo')->andReturn('')->byDefault()
+ ->shouldReceive('getBaseUrl')->andReturn('/')->byDefault()
+ ->shouldReceive('getQuery')->andReturn(array())->byDefault()
+ ->shouldReceive('getParam')->with(Mockery::type('string'), Mockery::type('string'))
+ ->andReturnUsing(function ($name, $default) {
+ return $default;
+ })->byDefault();
+
+ Icinga::app()->setRequest($this->requestMock);
+ }
+
+ private function setupResponseMock()
+ {
+ $this->responseMock = Mockery::mock('Icinga\Web\Response')->shouldDeferMissing();
+ Icinga::app()->setResponse($this->responseMock);
+ }
+
+ /**
+ * Return the currently active request mock object
+ *
+ * @return Request
+ */
+ public function getRequestMock()
+ {
+ return $this->requestMock;
+ }
+
+ /**
+ * Return the currently active response mock object
+ *
+ * @return Response
+ */
+ public function getResponseMock()
+ {
+ return $this->responseMock;
+ }
+
+ /**
+ * Create Config for database configuration
+ *
+ * @param string $name
+ *
+ * @return ConfigObject
+ * @throws RuntimeException
+ */
+ protected function createDbConfigFor($name)
+ {
+ if (array_key_exists($name, self::$dbConfiguration)) {
+ $config = new ConfigObject(self::$dbConfiguration[$name]);
+
+ $host = getenv(sprintf('ICINGAWEB_TEST_%s_HOST', strtoupper($name)));
+ if ($host) {
+ $config['host'] = $host;
+ }
+
+ $port = getenv(sprintf('ICINGAWEB_TEST_%s_PORT', strtoupper($name)));
+ if ($port) {
+ $config['port'] = $port;
+ }
+
+ return $config;
+ }
+
+ throw new RuntimeException('Configuration for database type not available: ' . $name);
+ }
+
+ /**
+ * Creates an array of Icinga\Data\Db\DbConnection
+ *
+ * @param string $name
+ *
+ * @return array
+ */
+ protected function createDbConnectionFor($name)
+ {
+ try {
+ $conn = ResourceFactory::createResource($this->createDbConfigFor($name));
+ } catch (Exception $e) {
+ $conn = $e->getMessage();
+ }
+
+ return array(
+ array($conn)
+ );
+ }
+
+ /**
+ * PHPUnit provider for mysql
+ *
+ * @return DbConnection
+ */
+ public function mysqlDb()
+ {
+ return $this->createDbConnectionFor('mysql');
+ }
+
+ /**
+ * PHPUnit provider for pgsql
+ *
+ * @return DbConnection
+ */
+ public function pgsqlDb()
+ {
+ return $this->createDbConnectionFor('pgsql');
+ }
+
+ /**
+ * PHPUnit provider for oracle
+ *
+ * @return DbConnection
+ */
+ public function oracleDb()
+ {
+ return $this->createDbConnectionFor('oracle');
+ }
+
+ /**
+ * Executes sql file by using the database connection
+ *
+ * @param DbConnection $resource
+ * @param string $filename
+ *
+ * @throws RuntimeException
+ */
+ public function loadSql(DbConnection $resource, $filename)
+ {
+ if (!is_file($filename)) {
+ throw new RuntimeException(
+ 'Sql file not found: ' . $filename . ' (test=' . $this->getName() . ')'
+ );
+ }
+
+ $sqlData = file_get_contents($filename);
+
+ if (!$sqlData) {
+ throw new RuntimeException(
+ 'Sql file is empty: ' . $filename . ' (test=' . $this->getName() . ')'
+ );
+ }
+
+ $resource->getDbAdapter()->exec($sqlData);
+ }
+
+ /**
+ * Setup provider for testcase
+ *
+ * @param string|DbConnection|null $resource
+ */
+ public function setupDbProvider($resource)
+ {
+ if (!$resource instanceof DbConnection) {
+ if (is_string($resource)) {
+ $this->markTestSkipped('Could not initialize provider: ' . $resource);
+ } else {
+ $this->markTestSkipped('Could not initialize provider');
+ }
+ return;
+ }
+
+ $adapter = $resource->getDbAdapter();
+
+ try {
+ $adapter->getConnection();
+ } catch (Exception $e) {
+ $this->markTestSkipped('Could not connect to provider: '. $e->getMessage());
+ }
+
+ $tables = $adapter->listTables();
+ foreach ($tables as $table) {
+ $adapter->exec('DROP TABLE ' . $table . ';');
+ }
+ }
+
+ /**
+ * Add assertMatchesRegularExpression() method for phpunit >= 8.0 < 9.0 for compatibility with PHP 7.2.
+ *
+ * @TODO Remove once PHP 7.2 support is not needed for testing anymore.
+ */
+ public static function assertMatchesRegularExpression(
+ string $pattern,
+ string $string,
+ string $message = ''
+ ): void {
+ if (method_exists(parent::class, 'assertMatchesRegularExpression')) {
+ parent::assertMatchesRegularExpression($pattern, $string, $message);
+ } else {
+ static::assertRegExp($pattern, $string, $message);
+ }
+ }
+ }
+}
diff --git a/library/Icinga/Test/ClassLoader.php b/library/Icinga/Test/ClassLoader.php
new file mode 100644
index 0000000..af90a7e
--- /dev/null
+++ b/library/Icinga/Test/ClassLoader.php
@@ -0,0 +1,113 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Test;
+
+/**
+ * PSR-4 class loader
+ */
+class ClassLoader
+{
+ /**
+ * Namespace separator
+ */
+ const NAMESPACE_SEPARATOR = '\\';
+
+ /**
+ * Namespaces
+ *
+ * @var array
+ */
+ private $namespaces = array();
+
+ /**
+ * Register a base directory for a namespace prefix
+ *
+ * @param string $namespace
+ * @param string $directory
+ *
+ * @return $this
+ */
+ public function registerNamespace($namespace, $directory)
+ {
+ $this->namespaces[$namespace] = $directory;
+
+ return $this;
+ }
+
+ /**
+ * Test whether a namespace exists
+ *
+ * @param string $namespace
+ *
+ * @return bool
+ */
+ public function hasNamespace($namespace)
+ {
+ return array_key_exists($namespace, $this->namespaces);
+ }
+
+ /**
+ * Get the source file of the given class or interface
+ *
+ * @param string $class Name of the class or interface
+ *
+ * @return string|null
+ */
+ public function getSourceFile($class)
+ {
+ foreach ($this->namespaces as $namespace => $dir) {
+ if ($class === strstr($class, $namespace)) {
+ $classPath = str_replace(
+ self::NAMESPACE_SEPARATOR,
+ DIRECTORY_SEPARATOR,
+ substr($class, strlen($namespace))
+ ) . '.php';
+ if (file_exists($file = $dir . $classPath)) {
+ return $file;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Load the given class or interface
+ *
+ * @param string $class Name of the class or interface
+ *
+ * @return bool Whether the class or interface has been loaded
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->getSourceFile($class)) {
+ require $file;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Register {@link loadClass()} as an autoloader
+ */
+ public function register()
+ {
+ spl_autoload_register(array($this, 'loadClass'));
+ }
+
+ /**
+ * Unregister {@link loadClass()} as an autoloader
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+ }
+
+ /**
+ * Unregister this as an autoloader
+ */
+ public function __destruct()
+ {
+ $this->unregister();
+ }
+}
diff --git a/library/Icinga/Test/DbTest.php b/library/Icinga/Test/DbTest.php
new file mode 100644
index 0000000..d1b1ff0
--- /dev/null
+++ b/library/Icinga/Test/DbTest.php
@@ -0,0 +1,47 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Test;
+
+use Icinga\Data\Db\DbConnection;
+
+interface DbTest
+{
+ /**
+ * PHPUnit provider for mysql
+ *
+ * @return DbConnection
+ */
+ public function mysqlDb();
+
+ /**
+ * PHPUnit provider for pgsql
+ *
+ * @return DbConnection
+ */
+ public function pgsqlDb();
+
+ /**
+ * PHPUnit provider for oracle
+ *
+ * @return DbConnection
+ */
+ public function oracleDb();
+
+ /**
+ * Executes sql file on PDO object
+ *
+ * @param DbConnection $resource
+ * @param string $filename
+ *
+ * @return boolean Operational success flag
+ */
+ public function loadSql(DbConnection $resource, $filename);
+
+ /**
+ * Setup provider for testcase
+ *
+ * @param string|DbConnection|null $resource
+ */
+ public function setupDbProvider($resource);
+}
diff --git a/library/Icinga/User.php b/library/Icinga/User.php
new file mode 100644
index 0000000..8610dd0
--- /dev/null
+++ b/library/Icinga/User.php
@@ -0,0 +1,649 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga;
+
+use DateTimeZone;
+use Icinga\Authentication\AdmissionLoader;
+use InvalidArgumentException;
+use Icinga\Application\Config;
+use Icinga\Authentication\Role;
+use Icinga\Exception\ProgrammingError;
+use Icinga\User\Preferences;
+use Icinga\Web\Navigation\Navigation;
+
+/**
+ * This class represents an authorized user
+ *
+ * You can retrieve authorization information (@TODO: Not implemented yet) or user information
+ */
+class User
+{
+ /**
+ * Firstname
+ *
+ * @var string
+ */
+ protected $firstname;
+
+ /**
+ * Lastname
+ *
+ * @var string
+ */
+ protected $lastname;
+
+ /**
+ * Users email address
+ *
+ * @var string
+ */
+ protected $email;
+
+ /**
+ * {@link username} without {@link domain}
+ *
+ * @var string
+ */
+ protected $localUsername;
+
+ /**
+ * Domain
+ *
+ * @var string
+ */
+ protected $domain;
+
+ /**
+ * More information about this user
+ *
+ * @var array
+ */
+ protected $additionalInformation = array();
+
+ /**
+ * Information if the user is externally authenticated
+ *
+ * Keys:
+ *
+ * 0: origin username
+ * 1: origin field name
+ *
+ * @var array
+ */
+ protected $externalUserInformation = array();
+
+ /**
+ * Whether restrictions should not apply to this user
+ *
+ * @var bool
+ */
+ protected $unrestricted = false;
+
+ /**
+ * Set of permissions
+ *
+ * @var array
+ */
+ protected $permissions = array();
+
+ /**
+ * Set of restrictions
+ *
+ * @var array
+ */
+ protected $restrictions = array();
+
+ /**
+ * Groups for this user
+ *
+ * @var array
+ */
+ protected $groups = array();
+
+ /**
+ * Roles of this user
+ *
+ * @var Role[]
+ */
+ protected $roles = array();
+
+ /**
+ * Preferences object
+ *
+ * @var Preferences
+ */
+ protected $preferences;
+
+ /**
+ * Whether the user is authenticated using a HTTP authentication mechanism
+ *
+ * @var bool
+ */
+ protected $isHttpUser = false;
+
+ /**
+ * Creates a user object given the provided information
+ *
+ * @param string $username
+ * @param string $firstname
+ * @param string $lastname
+ * @param string $email
+ */
+ public function __construct($username, $firstname = null, $lastname = null, $email = null)
+ {
+ $this->setUsername($username);
+
+ if ($firstname !== null) {
+ $this->setFirstname($firstname);
+ }
+
+ if ($lastname !== null) {
+ $this->setLastname($lastname);
+ }
+
+ if ($email !== null) {
+ $this->setEmail($email);
+ }
+ }
+
+ /**
+ * Setter for preferences
+ *
+ * @param Preferences $preferences
+ *
+ * @return $this
+ */
+ public function setPreferences(Preferences $preferences)
+ {
+ $this->preferences = $preferences;
+ return $this;
+ }
+
+ /**
+ * Getter for preferences
+ *
+ * @return Preferences
+ */
+ public function getPreferences()
+ {
+ if ($this->preferences === null) {
+ $this->preferences = new Preferences();
+ }
+
+ return $this->preferences;
+ }
+
+ /**
+ * Return all groups this user belongs to
+ *
+ * @return array
+ */
+ public function getGroups()
+ {
+ return $this->groups;
+ }
+
+ /**
+ * Set the groups this user belongs to
+ *
+ * @param array $groups
+ *
+ * @return $this
+ */
+ public function setGroups(array $groups)
+ {
+ $this->groups = $groups;
+ return $this;
+ }
+
+ /**
+ * Return true if the user is a member of this group
+ *
+ * @param string $group
+ *
+ * @return boolean
+ */
+ public function isMemberOf($group)
+ {
+ return in_array($group, $this->groups);
+ }
+
+ /**
+ * Get whether restrictions should not apply to this user
+ *
+ * @return bool
+ */
+ public function isUnrestricted()
+ {
+ return $this->unrestricted;
+ }
+
+ /**
+ * Set whether restrictions should not apply to this user
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
+ public function setIsUnrestricted($state)
+ {
+ $this->unrestricted = (bool) $state;
+
+ return $this;
+ }
+
+ /**
+ * Get the user's permissions
+ *
+ * @return array
+ */
+ public function getPermissions()
+ {
+ return $this->permissions;
+ }
+
+ /**
+ * Set the user's permissions
+ *
+ * @param array $permissions
+ *
+ * @return $this
+ */
+ public function setPermissions(array $permissions)
+ {
+ if (! empty($permissions)) {
+ natcasesort($permissions);
+ $this->permissions = array_combine($permissions, $permissions);
+ }
+ return $this;
+ }
+
+ /**
+ * Return restriction information for this user
+ *
+ * @param string $name
+ *
+ * @return array
+ */
+ public function getRestrictions($name)
+ {
+ if (array_key_exists($name, $this->restrictions)) {
+ return $this->restrictions[$name];
+ }
+
+ return array();
+ }
+
+ /**
+ * Set the user's 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;
+ }
+
+ /**
+ * Getter for username
+ *
+ * @return string
+ */
+ public function getUsername()
+ {
+ return $this->domain === null ? $this->localUsername : $this->localUsername . '@' . $this->domain;
+ }
+
+ /**
+ * Setter for username
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setUsername($name)
+ {
+ $parts = explode('\\', $name, 2);
+ if (count($parts) === 2) {
+ list($this->domain, $this->localUsername) = $parts;
+ } else {
+ $parts = explode('@', $name, 2);
+ if (count($parts) === 2) {
+ list($this->localUsername, $this->domain) = $parts;
+ } else {
+ $this->localUsername = $name;
+ $this->domain = null;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Getter for firstname
+ *
+ * @return string
+ */
+ public function getFirstname()
+ {
+ return $this->firstname;
+ }
+
+ /**
+ * Setter for firstname
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setFirstname($name)
+ {
+ $this->firstname = $name;
+ return $this;
+ }
+
+ /**
+ * Getter for lastname
+ *
+ * @return string
+ */
+ public function getLastname()
+ {
+ return $this->lastname;
+ }
+
+ /**
+ * Setter for lastname
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setLastname($name)
+ {
+ $this->lastname = $name;
+ return $this;
+ }
+
+ /**
+ * Getter for email
+ *
+ * @return string
+ */
+ public function getEmail()
+ {
+ return $this->email;
+ }
+
+ /**
+ * Setter for mail
+ *
+ * @param string $mail
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException When an invalid mail is provided
+ */
+ public function setEmail($mail)
+ {
+ if ($mail !== null && !filter_var($mail, FILTER_VALIDATE_EMAIL)) {
+ throw new InvalidArgumentException(
+ sprintf('Invalid mail given for user %s: %s', $this->getUsername(), $mail)
+ );
+ }
+
+ $this->email = $mail;
+ return $this;
+ }
+
+ /**
+ * Set the domain
+ *
+ * @param string $domain
+ *
+ * @return $this
+ */
+ public function setDomain($domain)
+ {
+ if ($domain && ($domain = trim($domain))) {
+ $this->domain = $domain;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get whether the user has a domain
+ *
+ * @return bool
+ */
+ public function hasDomain()
+ {
+ return $this->domain !== null;
+ }
+
+ /**
+ * Get the domain
+ *
+ * @return string
+ *
+ * @throws ProgrammingError If the user does not have a domain
+ */
+ public function getDomain()
+ {
+ if ($this->domain === null) {
+ throw new ProgrammingError(
+ 'User does not have a domain.'
+ . ' Use User::hasDomain() to check whether the user has a domain beforehand.'
+ );
+ }
+ return $this->domain;
+ }
+
+ /**
+ * Get the local username, ie. the username without its domain
+ *
+ * @return string
+ */
+ public function getLocalUsername()
+ {
+ return $this->localUsername;
+ }
+
+ /**
+ * Set additional information about user
+ *
+ * @param string $key
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function setAdditional($key, $value)
+ {
+ $this->additionalInformation[$key] = $value;
+ return $this;
+ }
+
+ /**
+ * Getter for additional information
+ *
+ * @param string $key
+ * @return mixed|null
+ */
+ public function getAdditional($key)
+ {
+ if (isset($this->additionalInformation[$key])) {
+ return $this->additionalInformation[$key];
+ }
+
+ return null;
+ }
+
+ /**
+ * Retrieve the user's timezone
+ *
+ * If the user did not set a timezone, the default timezone set via config.ini is returned
+ *
+ * @return DateTimeZone
+ */
+ public function getTimeZone()
+ {
+ $tz = $this->preferences->get('timezone');
+ if ($tz === null) {
+ $tz = date_default_timezone_get();
+ }
+
+ return new DateTimeZone($tz);
+ }
+
+ /**
+ * Set additional external user information
+ *
+ * @param string $username
+ * @param string $field
+ *
+ * @return $this
+ */
+ public function setExternalUserInformation($username, $field)
+ {
+ $this->externalUserInformation = array($username, $field);
+ return $this;
+ }
+
+ /**
+ * Get additional external user information
+ *
+ * @return array
+ */
+ public function getExternalUserInformation()
+ {
+ return $this->externalUserInformation;
+ }
+
+ /**
+ * Return true if user has external user information set
+ *
+ * @return bool
+ */
+ public function isExternalUser()
+ {
+ return ! empty($this->externalUserInformation);
+ }
+
+ /**
+ * Get whether the user is authenticated using a HTTP authentication mechanism
+ *
+ * @return bool
+ */
+ public function getIsHttpUser()
+ {
+ return $this->isHttpUser;
+ }
+
+ /**
+ * Set whether the user is authenticated using a HTTP authentication mechanism
+ *
+ * @param bool $isHttpUser
+ *
+ * @return $this
+ */
+ public function setIsHttpUser($isHttpUser = true)
+ {
+ $this->isHttpUser = (bool) $isHttpUser;
+ return $this;
+ }
+
+ /**
+ * Whether the user has a given permission
+ *
+ * @param string $requiredPermission
+ *
+ * @return bool
+ */
+ public function can($requiredPermission)
+ {
+ list($permissions, $refusals) = AdmissionLoader::migrateLegacyPermissions([$requiredPermission]);
+ if (! empty($permissions)) {
+ $requiredPermission = array_pop($permissions);
+ } elseif (! empty($refusals)) {
+ throw new InvalidArgumentException(
+ 'Refusals are not supported anymore. Check for a grant instead!'
+ );
+ }
+
+ $granted = false;
+ foreach ($this->getRoles() as $role) {
+ if ($role->denies($requiredPermission)) {
+ return false;
+ }
+
+ if (! $granted && $role->grants($requiredPermission)) {
+ $granted = true;
+ }
+ }
+
+ return $granted;
+ }
+
+ /**
+ * Load and return this user's configured navigation of the given type
+ *
+ * @param string $type
+ *
+ * @return Navigation
+ */
+ public function getNavigation($type)
+ {
+ $config = Config::navigation($type === 'dashboard-pane' ? 'dashlet' : $type, $this->getUsername());
+
+ if ($type === 'dashboard-pane') {
+ $panes = array();
+ foreach ($config as $dashletName => $dashletConfig) {
+ // TODO: Throw ConfigurationError if pane or url is missing
+ $panes[$dashletConfig->pane][$dashletName] = $dashletConfig->url;
+ }
+
+ $navigation = new Navigation();
+ foreach ($panes as $paneName => $dashlets) {
+ $navigation->addItem(
+ $paneName,
+ array(
+ 'type' => 'dashboard-pane',
+ 'dashlets' => $dashlets
+ )
+ );
+ }
+ } else {
+ $navigation = Navigation::fromConfig($config);
+ }
+
+ return $navigation;
+ }
+}
diff --git a/library/Icinga/User/Preferences.php b/library/Icinga/User/Preferences.php
new file mode 100644
index 0000000..b09462b
--- /dev/null
+++ b/library/Icinga/User/Preferences.php
@@ -0,0 +1,169 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\User;
+
+use Countable;
+
+/**
+ * User preferences container
+ *
+ * Usage example:
+ * <code>
+ * <?php
+ *
+ * use Icinga\User\Preferences;
+ *
+ * $preferences = new Preferences(); // Start with empty preferences
+ *
+ * $preferences = new Preferences(array('aPreference' => 'value')); // Start with initial preferences
+ *
+ * $preferences->aNewPreference = 'value'; // Set a preference
+ *
+ * unset($preferences->aPreference); // Unset a preference
+ *
+ * // Retrieve a preference and return a default value if the preference does not exist
+ * $anotherPreference = $preferences->get('anotherPreference', 'defaultValue');
+ */
+class Preferences implements Countable
+{
+ /**
+ * Preferences key-value array
+ *
+ * @var array
+ */
+ protected $preferences = array();
+
+ /**
+ * Constructor
+ *
+ * @param array $preferences Preferences key-value array
+ */
+ public function __construct(array $preferences = array())
+ {
+ $this->preferences = $preferences;
+ }
+
+ /**
+ * Count all preferences
+ *
+ * @return int The number of preferences
+ */
+ public function count(): int
+ {
+ return count($this->preferences);
+ }
+
+ /**
+ * Determine whether a preference exists
+ *
+ * @param string $name
+ *
+ * @return bool
+ */
+ public function has($name)
+ {
+ return array_key_exists($name, $this->preferences);
+ }
+
+ /**
+ * Write data to a preference
+ *
+ * @param string $name
+ * @param mixed $value
+ */
+ public function __set($name, $value)
+ {
+ $this->preferences[$name] = $value;
+ }
+
+ /**
+ * Retrieve a preference section
+ *
+ * @param string $name
+ *
+ * @return array|null
+ */
+ public function get($name)
+ {
+ if (array_key_exists($name, $this->preferences)) {
+ return $this->preferences[$name];
+ }
+
+ return null;
+ }
+
+ /**
+ * Retrieve a value from a specific section
+ *
+ * @param string $section
+ * @param string $name
+ * @param null $default
+ *
+ * @return array|null
+ */
+ public function getValue($section, $name, $default = null)
+ {
+ if (array_key_exists($section, $this->preferences)
+ && array_key_exists($name, $this->preferences[$section])
+ ) {
+ return $this->preferences[$section][$name];
+ }
+
+ return $default;
+ }
+
+ /**
+ * Magic method so that $obj->value will work.
+ *
+ * @param string $name
+ *
+ * @return mixed
+ */
+ public function __get($name)
+ {
+ return $this->get($name);
+ }
+
+ /**
+ * Remove a given preference
+ *
+ * @param string $name Preference name
+ */
+ public function remove($name)
+ {
+ unset($this->preferences[$name]);
+ }
+
+ /**
+ * Determine if a preference is set and is not NULL
+ *
+ * @param string $name Preference name
+ *
+ * @return bool
+ */
+ public function __isset($name)
+ {
+ return isset($this->preferences[$name]);
+ }
+
+ /**
+ * Unset a given preference
+ *
+ * @param string $name Preference name
+ */
+ public function __unset($name)
+ {
+ $this->remove($name);
+ }
+
+ /**
+ * Get preferences as array
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ return $this->preferences;
+ }
+}
diff --git a/library/Icinga/User/Preferences/PreferencesStore.php b/library/Icinga/User/Preferences/PreferencesStore.php
new file mode 100644
index 0000000..8ecc677
--- /dev/null
+++ b/library/Icinga/User/Preferences/PreferencesStore.php
@@ -0,0 +1,344 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\User\Preferences;
+
+use Exception;
+use Icinga\Exception\NotReadableError;
+use Icinga\Exception\NotWritableError;
+use Icinga\User;
+use Icinga\User\Preferences;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+use Zend_Db_Expr;
+
+/**
+ * Preferences store factory
+ *
+ * Load and save user preferences by using a database
+ *
+ * Usage example:
+ * <code>
+ * <?php
+ *
+ * use Icinga\Data\ConfigObject;
+ * use Icinga\User\Preferences;
+ * use Icinga\User\Preferences\PreferencesStore;
+ *
+ * // Create a db store
+ * $store = PreferencesStore::create(
+ * new ConfigObject(
+ * 'resource' => 'resource name'
+ * ),
+ * $user // Instance of \Icinga\User
+ * );
+ *
+ * $preferences = new Preferences($store->load());
+ * $preferences->aPreference = 'value';
+ * $store->save($preferences);
+ * </code>
+ */
+class PreferencesStore
+{
+ /**
+ * Column name for username
+ */
+ const COLUMN_USERNAME = 'username';
+
+ /**
+ * Column name for section
+ */
+ const COLUMN_SECTION = 'section';
+
+ /**
+ * Column name for preference
+ */
+ const COLUMN_PREFERENCE = 'name';
+
+ /**
+ * Column name for value
+ */
+ const COLUMN_VALUE = 'value';
+
+ /**
+ * Column name for created time
+ */
+ const COLUMN_CREATED_TIME = 'ctime';
+
+ /**
+ * Column name for modified time
+ */
+ const COLUMN_MODIFIED_TIME = 'mtime';
+
+ /**
+ * Table name
+ *
+ * @var string
+ */
+ protected $table = 'icingaweb_user_preference';
+
+ /**
+ * Stored preferences
+ *
+ * @var array
+ */
+ protected $preferences = [];
+
+ /**
+ * Store config
+ *
+ * @var ConfigObject
+ */
+ protected $config;
+
+ /**
+ * Given user
+ *
+ * @var User
+ */
+ protected $user;
+
+ /**
+ * Create a new store
+ *
+ * @param ConfigObject $config The config for this adapter
+ * @param User $user The user to which these preferences belong
+ */
+ public function __construct(ConfigObject $config, User $user)
+ {
+ $this->config = $config;
+ $this->user = $user;
+ $this->init();
+ }
+
+ /**
+ * Getter for the store config
+ *
+ * @return ConfigObject
+ */
+ public function getStoreConfig(): ConfigObject
+ {
+ return $this->config;
+ }
+
+ /**
+ * Getter for the user
+ *
+ * @return User
+ */
+ public function getUser(): User
+ {
+ return $this->user;
+ }
+
+ /**
+ * Initialize the store
+ */
+ protected function init(): void
+ {
+ }
+
+ /**
+ * Load preferences from the database
+ *
+ * @return array
+ *
+ * @throws NotReadableError In case the database operation failed
+ */
+ public function load(): array
+ {
+ try {
+ $select = $this->getStoreConfig()->connection->getDbAdapter()->select();
+ $result = $select
+ ->from($this->table, [self::COLUMN_SECTION, self::COLUMN_PREFERENCE, self::COLUMN_VALUE])
+ ->where(self::COLUMN_USERNAME . ' = ?', $this->getUser()->getUsername())
+ ->query()
+ ->fetchAll();
+ } catch (Exception $e) {
+ throw new NotReadableError(
+ 'Cannot fetch preferences for user %s from database',
+ $this->getUser()->getUsername(),
+ $e
+ );
+ }
+
+ if ($result !== false) {
+ $values = [];
+ foreach ($result as $row) {
+ $values[$row->{self::COLUMN_SECTION}][$row->{self::COLUMN_PREFERENCE}] = $row->{self::COLUMN_VALUE};
+ }
+
+ $this->preferences = $values;
+ }
+
+ return $this->preferences;
+ }
+
+ /**
+ * Save the given preferences in the database
+ *
+ * @param Preferences $preferences The preferences to save
+ */
+ public function save(Preferences $preferences): void
+ {
+ $preferences = $preferences->toArray();
+
+ $sections = array_keys($preferences);
+
+ foreach ($sections as $section) {
+ if (! array_key_exists($section, $this->preferences)) {
+ $this->preferences[$section] = [];
+ }
+
+ if (! array_key_exists($section, $preferences)) {
+ $preferences[$section] = [];
+ }
+
+ $toBeInserted = array_diff_key($preferences[$section], $this->preferences[$section]);
+ if (!empty($toBeInserted)) {
+ $this->insert($toBeInserted, $section);
+ }
+
+ $toBeUpdated = array_intersect_key(
+ array_diff_assoc($preferences[$section], $this->preferences[$section]),
+ array_diff_assoc($this->preferences[$section], $preferences[$section])
+ );
+
+ if (!empty($toBeUpdated)) {
+ $this->update($toBeUpdated, $section);
+ }
+
+ $toBeDeleted = array_keys(array_diff_key($this->preferences[$section], $preferences[$section]));
+ if (!empty($toBeDeleted)) {
+ $this->delete($toBeDeleted, $section);
+ }
+ }
+ }
+
+ /**
+ * Insert the given preferences into the database
+ *
+ * @param array $preferences The preferences to insert
+ * @param string $section The preferences in section to update
+ *
+ * @throws NotWritableError In case the database operation failed
+ */
+ protected function insert(array $preferences, string $section): void
+ {
+ /** @var \Zend_Db_Adapter_Abstract $db */
+ $db = $this->getStoreConfig()->connection->getDbAdapter();
+
+ try {
+ foreach ($preferences as $key => $value) {
+ $db->insert(
+ $this->table,
+ [
+ self::COLUMN_USERNAME => $this->getUser()->getUsername(),
+ $db->quoteIdentifier(self::COLUMN_SECTION) => $section,
+ $db->quoteIdentifier(self::COLUMN_PREFERENCE) => $key,
+ self::COLUMN_VALUE => $value,
+ self::COLUMN_CREATED_TIME => new Zend_Db_Expr('NOW()'),
+ self::COLUMN_MODIFIED_TIME => new Zend_Db_Expr('NOW()')
+ ]
+ );
+ }
+ } catch (Exception $e) {
+ throw new NotWritableError(
+ 'Cannot insert preferences for user %s into database',
+ $this->getUser()->getUsername(),
+ $e
+ );
+ }
+ }
+
+ /**
+ * Update the given preferences in the database
+ *
+ * @param array $preferences The preferences to update
+ * @param string $section The preferences in section to update
+ *
+ * @throws NotWritableError In case the database operation failed
+ */
+ protected function update(array $preferences, string $section): void
+ {
+ /** @var \Zend_Db_Adapter_Abstract $db */
+ $db = $this->getStoreConfig()->connection->getDbAdapter();
+
+ try {
+ foreach ($preferences as $key => $value) {
+ $db->update(
+ $this->table,
+ [
+ self::COLUMN_VALUE => $value,
+ self::COLUMN_MODIFIED_TIME => new Zend_Db_Expr('NOW()')
+ ],
+ [
+ self::COLUMN_USERNAME . '=?' => $this->getUser()->getUsername(),
+ $db->quoteIdentifier(self::COLUMN_SECTION) . '=?' => $section,
+ $db->quoteIdentifier(self::COLUMN_PREFERENCE) . '=?' => $key
+ ]
+ );
+ }
+ } catch (Exception $e) {
+ throw new NotWritableError(
+ 'Cannot update preferences for user %s in database',
+ $this->getUser()->getUsername(),
+ $e
+ );
+ }
+ }
+
+ /**
+ * Delete the given preference names from the database
+ *
+ * @param array $preferenceKeys The preference names to delete
+ * @param string $section The preferences in section to update
+ *
+ * @throws NotWritableError In case the database operation failed
+ */
+ protected function delete(array $preferenceKeys, string $section): void
+ {
+ /** @var \Zend_Db_Adapter_Abstract $db */
+ $db = $this->getStoreConfig()->connection->getDbAdapter();
+
+ try {
+ $db->delete(
+ $this->table,
+ [
+ self::COLUMN_USERNAME . '=?' => $this->getUser()->getUsername(),
+ $db->quoteIdentifier(self::COLUMN_SECTION) . '=?' => $section,
+ $db->quoteIdentifier(self::COLUMN_PREFERENCE) . ' IN (?)' => $preferenceKeys
+ ]
+ );
+ } catch (Exception $e) {
+ throw new NotWritableError(
+ 'Cannot delete preferences for user %s from database',
+ $this->getUser()->getUsername(),
+ $e
+ );
+ }
+ }
+
+ /**
+ * Create preferences storage adapter from config
+ *
+ * @param ConfigObject $config The config for the adapter
+ * @param User $user The user to which these preferences belong
+ *
+ * @return self
+ *
+ * @throws ConfigurationError When the configuration defines an invalid storage type
+ */
+ public static function create(ConfigObject $config, User $user): self
+ {
+ $resourceConfig = ResourceFactory::getResourceConfig($config->resource);
+ if ($resourceConfig->db === 'mysql') {
+ $resourceConfig->charset = 'utf8mb4';
+ }
+
+ $config->connection = ResourceFactory::createResource($resourceConfig);
+
+ return new self($config, $user);
+ }
+}
diff --git a/library/Icinga/Util/ASN1.php b/library/Icinga/Util/ASN1.php
new file mode 100644
index 0000000..9e00258
--- /dev/null
+++ b/library/Icinga/Util/ASN1.php
@@ -0,0 +1,102 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Util;
+
+use DateInterval;
+use DateTime;
+use InvalidArgumentException;
+
+/**
+ * Parsers for ASN.1 types
+ */
+class ASN1
+{
+ /**
+ * Parse the given value based on the "3.3.13. Generalized Time" syntax as specified by IETF RFC 4517
+ *
+ * @param string $value
+ *
+ * @return DateTime
+ *
+ * @throws InvalidArgumentException
+ *
+ * @see https://tools.ietf.org/html/rfc4517#section-3.3.13
+ */
+ public static function parseGeneralizedTime($value)
+ {
+ $generalizedTimePattern = <<<EOD
+/\A
+ (?P<YmdH>
+ [0-9]{4} # century year
+ (?:0[1-9]|1[0-2]) # month
+ (?:0[1-9]|[12][0-9]|3[0-1]) # day
+ (?:[01][0-9]|2[0-3]) # hour
+ )
+ (?:
+ (?P<i>[0-5][0-9]) # minute
+ (?P<s>[0-5][0-9]|60)? # second or leap-second
+ )?
+ (?:[.,](?P<frac>[0-9]+))? # fraction
+ (?P<tz> # g-time-zone
+ Z
+ |
+ [-+]
+ (?:[01][0-9]|2[0-3]) # hour
+ (?:[0-5][0-9])? # minute
+ )
+\z/x
+EOD;
+
+ $matches = array();
+
+ if (preg_match($generalizedTimePattern, $value, $matches)) {
+ $dateTimeRaw = $matches['YmdH'];
+ $dateTimeFormat = 'YmdH';
+
+ if ($matches['i'] !== '') {
+ $dateTimeRaw .= $matches['i'];
+ $dateTimeFormat .= 'i';
+
+ if ($matches['s'] !== '') {
+ $dateTimeRaw .= $matches['s'];
+ $dateTimeFormat .= 's';
+ $fractionOfSeconds = 1;
+ } else {
+ $fractionOfSeconds = 60;
+ }
+ } else {
+ $fractionOfSeconds = 3600;
+ }
+
+ $dateTimeFormat .= 'O';
+
+ if ($matches['tz'] === 'Z') {
+ $dateTimeRaw .= '+0000';
+ } else {
+ $dateTimeRaw .= $matches['tz'];
+
+ if (strlen($matches['tz']) === 3) {
+ $dateTimeRaw .= '00';
+ }
+ }
+
+ $dateTime = DateTime::createFromFormat($dateTimeFormat, $dateTimeRaw);
+
+ if ($dateTime !== false) {
+ if (isset($matches['frac'])) {
+ $dateTime->add(new DateInterval(
+ 'PT' . round((float) ('0.' . $matches['frac']) * $fractionOfSeconds) . 'S'
+ ));
+ }
+
+ return $dateTime;
+ }
+ }
+
+ throw new InvalidArgumentException(sprintf(
+ 'Failed to parse %s based on the ASN.1 standard (GeneralizedTime)',
+ var_export($value, true)
+ ));
+ }
+}
diff --git a/library/Icinga/Util/Color.php b/library/Icinga/Util/Color.php
new file mode 100644
index 0000000..cf88f41
--- /dev/null
+++ b/library/Icinga/Util/Color.php
@@ -0,0 +1,121 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Util;
+
+/**
+ * Provide functions to change and convert colors.
+ */
+class Color
+{
+ /**
+ * Convert a given color string to an rgb-array containing
+ * each color as a decimal value.
+ *
+ * @param $color The color-string #RRGGBB
+ *
+ * @return array The converted rgb-array.
+ */
+ public static function rgbAsArray($color)
+ {
+ if (substr($color, 0, 1) !== '#') {
+ $color = '#' . $color;
+ }
+ if (strlen($color) !== 7) {
+ return;
+ }
+ $r = (float)intval(substr($color, 1, 2), 16);
+ $g = (float)intval(substr($color, 3, 2), 16);
+ $b = (float)intval(substr($color, 5, 2), 16);
+ return array($r, $g, $b);
+ }
+
+ /**
+ * Convert a rgb array to a color-string
+ *
+ * @param array $rgb The rgb-array
+ *
+ * @return string The color string #RRGGBB
+ */
+ public static function arrayToRgb(array $rgb)
+ {
+ $r = (string)dechex($rgb[0]);
+ $g = (string)dechex($rgb[1]);
+ $b = (string)dechex($rgb[2]);
+ return '#'
+ . (strlen($r) > 1 ? $r : '0' . $r)
+ . (strlen($g) > 1 ? $g : '0' . $g)
+ . (strlen($b) > 1 ? $b : '0' . $b);
+ }
+
+ /**
+ * Change the saturation for a given color.
+ *
+ * @param $color string The color to change
+ * @param $change float The change.
+ * 0.0 creates a black-and-white image.
+ * 0.5 reduces the color saturation by half.
+ * 1.0 causes no change.
+ * 2.0 doubles the color saturation.
+ * @return string
+ */
+ public static function changeSaturation($color, $change)
+ {
+ return self::arrayToRgb(self::changeRgbSaturation(self::rgbAsArray($color), $change));
+ }
+
+ /**
+ * Change the brightness for a given color
+ *
+ * @param $color string The color to change
+ * @param $change float The change in percent
+ *
+ * @return string
+ */
+ public static function changeBrightness($color, $change)
+ {
+ return self::arrayToRgb(self::changeRgbBrightness(self::rgbAsArray($color), $change));
+ }
+
+ /**
+ * @param $rgb array The rgb-array to change
+ * @param $change float The factor
+ *
+ * @return array The updated rgb-array
+ */
+ private static function changeRgbSaturation(array $rgb, $change)
+ {
+ $pr = 0.499; // 0.299
+ $pg = 0.387; // 0.587
+ $pb = 0.114; // 0.114
+ $r = $rgb[0];
+ $g = $rgb[1];
+ $b = $rgb[2];
+ $p = sqrt(
+ $r * $r * $pr +
+ $g * $g * $pg +
+ $b * $b * $pb
+ );
+ $rgb[0] = (int)($p + ($r - $p) * $change);
+ $rgb[1] = (int)($p + ($g - $p) * $change);
+ $rgb[2] = (int)($p + ($b - $p) * $change);
+ return $rgb;
+ }
+
+ /**
+ * @param $rgb array The rgb-array to change
+ * @param $change float The factor
+ *
+ * @return array The updated rgb-array
+ */
+ private static function changeRgbBrightness(array $rgb, $change)
+ {
+ $red = $rgb[0] + ($rgb[0] * $change);
+ $green = $rgb[1] + ($rgb[1] * $change);
+ $blue = $rgb[2] + ($rgb[2] * $change);
+ $rgb[0] = $red < 255 ? (int) $red : 255;
+ $rgb[1] = $green < 255 ? (int) $green : 255;
+ $rgb[2] = $blue < 255 ? (int) $blue : 255;
+ return $rgb;
+ }
+}
diff --git a/library/Icinga/Util/ConfigAwareFactory.php b/library/Icinga/Util/ConfigAwareFactory.php
new file mode 100644
index 0000000..133887a
--- /dev/null
+++ b/library/Icinga/Util/ConfigAwareFactory.php
@@ -0,0 +1,18 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Util;
+
+/**
+ * Interface defining a factory which is configured at runtime
+ */
+interface ConfigAwareFactory
+{
+ /**
+ * Set the factory's config
+ *
+ * @param mixed $config
+ * @throws \Icinga\Exception\ConfigurationError if the given config is not valid
+ */
+ public static function setConfig($config);
+}
diff --git a/library/Icinga/Util/Csp.php b/library/Icinga/Util/Csp.php
new file mode 100644
index 0000000..bd275c6
--- /dev/null
+++ b/library/Icinga/Util/Csp.php
@@ -0,0 +1,107 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Util;
+
+use Icinga\Web\Response;
+use Icinga\Web\Window;
+use RuntimeException;
+
+use function ipl\Stdlib\get_php_type;
+
+/**
+ * Helper to enable strict content security policy (CSP)
+ *
+ * {@see static::addHeader()} adds a strict Content-Security-Policy header with a nonce to still support dynamic CSS
+ * securely.
+ * Note that {@see static::createNonce()} must be called first.
+ * Use {@see static::getStyleNonce()} to access the nonce for dynamic CSS.
+ *
+ * A nonce is not created for dynamic JS,
+ * and it is questionable whether this will ever be supported.
+ */
+class Csp
+{
+ /** @var static */
+ protected static $instance;
+
+ /** @var ?string */
+ protected $styleNonce;
+
+ /** Singleton */
+ private function __construct()
+ {
+ }
+
+ /**
+ * Add Content-Security-Policy header with a nonce for dynamic CSS
+ *
+ * Note that {@see static::createNonce()} must be called beforehand.
+ *
+ * @param Response $response
+ *
+ * @throws RuntimeException If no nonce set for CSS
+ */
+ public static function addHeader(Response $response): void
+ {
+ $csp = static::getInstance();
+
+ if (empty($csp->styleNonce)) {
+ throw new RuntimeException('No nonce set for CSS');
+ }
+
+ $response->setHeader('Content-Security-Policy', "style-src 'self' 'nonce-$csp->styleNonce';", true);
+ }
+
+ /**
+ * Set/recreate nonce for dynamic CSS
+ *
+ * Should always be called upon initial page loads or page reloads,
+ * as it sets/recreates a nonce for CSS and writes it to a window-aware session.
+ */
+ public static function createNonce(): void
+ {
+ $csp = static::getInstance();
+ $csp->styleNonce = base64_encode(random_bytes(16));
+
+ Window::getInstance()->getSessionNamespace('csp')->set('style_nonce', $csp->styleNonce);
+ }
+
+ /**
+ * Get nonce for dynamic CSS
+ *
+ * @return ?string
+ */
+ public static function getStyleNonce(): ?string
+ {
+ return static::getInstance()->styleNonce;
+ }
+
+ /**
+ * Get the CSP instance
+ *
+ * @return self
+ */
+ protected static function getInstance(): self
+ {
+ if (static::$instance === null) {
+ $csp = new static();
+ $nonce = Window::getInstance()->getSessionNamespace('csp')->get('style_nonce');
+ if ($nonce !== null && ! is_string($nonce)) {
+ throw new RuntimeException(
+ sprintf(
+ 'Nonce value is expected to be string, got %s instead',
+ get_php_type($nonce)
+ )
+ );
+ }
+
+ $csp->styleNonce = $nonce;
+
+ static::$instance = $csp;
+ }
+
+ return static::$instance;
+ }
+}
diff --git a/library/Icinga/Util/Dimension.php b/library/Icinga/Util/Dimension.php
new file mode 100644
index 0000000..6860fd8
--- /dev/null
+++ b/library/Icinga/Util/Dimension.php
@@ -0,0 +1,123 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Util;
+
+class Dimension
+{
+ /**
+ * Defines this dimension as nr of pixels
+ */
+ const UNIT_PX = "px";
+
+ /**
+ * Defines this dimension as width of 'M' in current font
+ */
+ const UNIT_EM = "em";
+
+ /**
+ * Defines this dimension as a percentage value
+ */
+ const UNIT_PERCENT = "%";
+
+ /**
+ * Defines this dimension in points
+ */
+ const UNIT_PT = "pt";
+
+ /**
+ * The current set value for this dimension
+ *
+ * @var int
+ */
+ private $value = 0;
+
+ /**
+ * The unit to interpret the value with
+ *
+ * @var string
+ */
+ private $unit = self::UNIT_PX;
+
+ /**
+ * Create a new Dimension object with the given size and unit
+ *
+ * @param int $value The new value
+ * @param string $unit The unit to use (default: px)
+ */
+ public function __construct($value, $unit = self::UNIT_PX)
+ {
+ $this->setValue($value, $unit);
+ }
+
+ /**
+ * Change the value and unit of this dimension
+ *
+ * @param int $value The new value
+ * @param string $unit The unit to use (default: px)
+ */
+ public function setValue($value, $unit = self::UNIT_PX)
+ {
+ $this->value = intval($value);
+ $this->unit = $unit;
+ }
+
+ /**
+ * Return true when the value is > 0
+ *
+ * @return bool
+ */
+ public function isDefined()
+ {
+ return $this->value > 0;
+ }
+
+ /**
+ * Return the underlying value without unit information
+ *
+ * @return int
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Return the unit used for the value
+ *
+ * @return string
+ */
+ public function getUnit()
+ {
+ return $this->unit;
+ }
+
+ /**
+ * Return this value with it's according unit as a string
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ if (!$this->isDefined()) {
+ return "";
+ }
+ return $this->value.$this->unit;
+ }
+
+ /**
+ * Create a new Dimension object from a string containing the numeric value and the dimension (e.g. 200px, 20%)
+ *
+ * @param $string The string to parse
+ *
+ * @return Dimension
+ */
+ public static function fromString($string)
+ {
+ $matches = array();
+ if (!preg_match_all('/^ *([0-9]+)(px|pt|em|\%) */i', $string, $matches)) {
+ return new Dimension(0);
+ }
+ return new Dimension(intval($matches[1][0]), $matches[2][0]);
+ }
+}
diff --git a/library/Icinga/Util/DirectoryIterator.php b/library/Icinga/Util/DirectoryIterator.php
new file mode 100644
index 0000000..cee37b6
--- /dev/null
+++ b/library/Icinga/Util/DirectoryIterator.php
@@ -0,0 +1,214 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Util;
+
+use ArrayIterator;
+use InvalidArgumentException;
+use RecursiveIterator;
+
+/**
+ * Iterator for traversing a directory
+ */
+class DirectoryIterator implements RecursiveIterator
+{
+ /**
+ * Iterate files first
+ *
+ * @var int
+ */
+ const FILES_FIRST = 1;
+
+ /**
+ * Current directory item
+ *
+ * @var string|false
+ */
+ private $current;
+
+ /**
+ * The file extension to filter for
+ *
+ * @var string
+ */
+ protected $extension;
+
+ /**
+ * Scanned files
+ *
+ * @var ArrayIterator
+ */
+ private $files;
+
+ /**
+ * Iterator flags
+ *
+ * @var int
+ */
+ protected $flags;
+
+ /**
+ * Current key
+ *
+ * @var string|false
+ */
+ private $key;
+
+ /**
+ * The path of the directory to traverse
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * Directory queue if FILES_FIRST flag is set
+ *
+ * @var array
+ */
+ private $queue;
+
+ /**
+ * Whether to skip empty files
+ *
+ * Defaults to true.
+ *
+ * @var bool
+ */
+ protected $skipEmpty = true;
+
+ /**
+ * Whether to skip hidden files
+ *
+ * Defaults to true.
+ *
+ * @var bool
+ */
+ protected $skipHidden = true;
+
+ /**
+ * Create a new directory iterator from path
+ *
+ * The given path will not be validated whether it is readable. Use {@link isReadable()} before creating a new
+ * directory iterator instance.
+ *
+ * @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, $flags = null)
+ {
+ if (empty($path)) {
+ throw new InvalidArgumentException('The path can\'t be empty');
+ }
+ $this->path = $path;
+ if (! empty($extension)) {
+ $this->extension = '.' . ltrim($extension, '.');
+ }
+ if ($flags !== null) {
+ $this->flags = $flags;
+ }
+ }
+
+ /**
+ * Check whether the given path is a directory and is readable
+ *
+ * @param string $path The path of the directory
+ *
+ * @return bool
+ */
+ public static function isReadable($path)
+ {
+ return is_dir($path) && is_readable($path);
+ }
+
+ public function hasChildren(): bool
+ {
+ return static::isReadable($this->current);
+ }
+
+ public function getChildren(): DirectoryIterator
+ {
+ return new static($this->current, $this->extension, $this->flags);
+ }
+
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ return $this->current;
+ }
+
+ public function next(): void
+ {
+ $path = null;
+ do {
+ $this->files->next();
+ $skip = false;
+ if (! $this->files->valid()) {
+ $file = false;
+ $path = false;
+ break;
+ } else {
+ $file = $this->files->current();
+ do {
+ if ($this->skipHidden && $file[0] === '.') {
+ $skip = true;
+ break;
+ }
+
+ $path = $this->path . '/' . $file;
+
+ if (is_dir($path)) {
+ if ($this->flags & static::FILES_FIRST === static::FILES_FIRST) {
+ $this->queue[] = array($path, $file);
+ $skip = true;
+ }
+ break;
+ }
+
+ if ($this->skipEmpty && ! filesize($path)) {
+ $skip = true;
+ break;
+ }
+
+ if ($this->extension && ! StringHelper::endsWith($file, $this->extension)) {
+ $skip = true;
+ break;
+ }
+ } while (0);
+ }
+ } while ($skip);
+
+ /** @noinspection PhpUndefinedVariableInspection */
+
+ if ($path === false && ! empty($this->queue)) {
+ list($path, $file) = array_shift($this->queue);
+ }
+
+ $this->current = $path;
+ $this->key = $file;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->key;
+ }
+
+ public function valid(): bool
+ {
+ return $this->current !== false;
+ }
+
+ public function rewind(): void
+ {
+ if ($this->files === null) {
+ $files = scandir($this->path);
+ natcasesort($files);
+ $this->files = new ArrayIterator($files);
+ }
+ $this->files->rewind();
+ $this->queue = array();
+ $this->next();
+ }
+}
diff --git a/library/Icinga/Util/EnumeratingFilterIterator.php b/library/Icinga/Util/EnumeratingFilterIterator.php
new file mode 100644
index 0000000..0659961
--- /dev/null
+++ b/library/Icinga/Util/EnumeratingFilterIterator.php
@@ -0,0 +1,30 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Util;
+
+use FilterIterator;
+
+/**
+ * Class EnumeratingFilterIterator
+ *
+ * FilterIterator with continuous numeric key (index)
+ */
+abstract class EnumeratingFilterIterator extends FilterIterator
+{
+ /**
+ * @var int
+ */
+ private $index;
+
+ public function rewind(): void
+ {
+ parent::rewind();
+ $this->index = 0;
+ }
+
+ public function key(): int
+ {
+ return $this->index++;
+ }
+}
diff --git a/library/Icinga/Util/Environment.php b/library/Icinga/Util/Environment.php
new file mode 100644
index 0000000..8d47b84
--- /dev/null
+++ b/library/Icinga/Util/Environment.php
@@ -0,0 +1,42 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Util;
+
+/**
+ * Helper for configuring the PHP environment
+ */
+class Environment
+{
+ /**
+ * Raise the PHP memory_limit
+ *
+ * Unless it is not already set to a higher limit
+ *
+ * @param string|int $minimum
+ */
+ public static function raiseMemoryLimit($minimum = '512M')
+ {
+ if (is_string($minimum)) {
+ $minimum = Format::unpackShorthandBytes($minimum);
+ }
+
+ if (Format::unpackShorthandBytes(ini_get('memory_limit')) < $minimum) {
+ ini_set('memory_limit', $minimum);
+ }
+ }
+
+ /**
+ * Raise the PHP max_execution_time
+ *
+ * Unless it is not already configured to a higher value.
+ *
+ * @param int $minimum
+ */
+ public static function raiseExecutionTime($minimum = 300)
+ {
+ if ((int) ini_get('max_execution_time') < $minimum) {
+ ini_set('max_execution_time', $minimum);
+ }
+ }
+}
diff --git a/library/Icinga/Util/File.php b/library/Icinga/Util/File.php
new file mode 100644
index 0000000..dad332a
--- /dev/null
+++ b/library/Icinga/Util/File.php
@@ -0,0 +1,195 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Util;
+
+use SplFileObject;
+use ErrorException;
+use RuntimeException;
+use Icinga\Exception\NotWritableError;
+
+/**
+ * File
+ *
+ * A class to ease opening files and reading/writing to them.
+ */
+class File extends SplFileObject
+{
+ /**
+ * The mode used to open the file
+ *
+ * @var string
+ */
+ protected $openMode;
+
+ /**
+ * The access mode to use when creating directories
+ *
+ * @var int
+ */
+ public static $dirMode = 1528; // 2770
+
+ /**
+ * @see SplFileObject::__construct()
+ */
+ public function __construct($filename, $openMode = 'r', $useIncludePath = false, $context = null)
+ {
+ $this->openMode = $openMode;
+ if ($context === null) {
+ parent::__construct($filename, $openMode, $useIncludePath);
+ } else {
+ parent::__construct($filename, $openMode, $useIncludePath, $context);
+ }
+ }
+
+ /**
+ * Create a file using the given access mode and return a instance of File open for writing
+ *
+ * @param string $path The path to the file
+ * @param int $accessMode The access mode to set
+ * @param bool $recursive Whether missing nested directories of the given path should be created
+ *
+ * @return File
+ *
+ * @throws RuntimeException In case the file cannot be created or the access mode cannot be set
+ * @throws NotWritableError In case the path's (existing) parent is not writable
+ */
+ public static function create($path, $accessMode, $recursive = true)
+ {
+ $dirPath = dirname($path);
+ if ($recursive && !is_dir($dirPath)) {
+ static::createDirectories($dirPath);
+ } elseif (! is_writable($dirPath)) {
+ throw new NotWritableError(sprintf('Path "%s" is not writable', $dirPath));
+ }
+
+ $file = new static($path, 'x+');
+
+ if (! @chmod($path, $accessMode)) {
+ $error = error_get_last();
+ throw new RuntimeException(sprintf(
+ 'Cannot set access mode "%s" on file "%s" (%s)',
+ decoct($accessMode),
+ $path,
+ $error['message']
+ ));
+ }
+
+ return $file;
+ }
+
+ /**
+ * Create missing directories
+ *
+ * @param string $path
+ *
+ * @throws RuntimeException In case a directory cannot be created or the access mode cannot be set
+ */
+ protected static function createDirectories($path)
+ {
+ $part = strpos($path, DIRECTORY_SEPARATOR) === 0 ? DIRECTORY_SEPARATOR : '';
+ foreach (explode(DIRECTORY_SEPARATOR, ltrim($path, DIRECTORY_SEPARATOR)) as $dir) {
+ $part .= $dir . DIRECTORY_SEPARATOR;
+
+ if (! is_dir($part)) {
+ if (! @mkdir($part, static::$dirMode)) {
+ $error = error_get_last();
+ throw new RuntimeException(sprintf(
+ 'Failed to create missing directory "%s" (%s)',
+ $part,
+ $error['message']
+ ));
+ }
+
+ if (! @chmod($part, static::$dirMode)) {
+ $error = error_get_last();
+ throw new RuntimeException(sprintf(
+ 'Failed to set access mode "%s" for directory "%s" (%s)',
+ decoct(static::$dirMode),
+ $part,
+ $error['message']
+ ));
+ }
+ }
+ }
+ }
+
+ #[\ReturnTypeWillChange]
+ public function fwrite($str, $length = null)
+ {
+ $this->assertOpenForWriting();
+ $this->setupErrorHandler();
+ $retVal = $length === null ? parent::fwrite($str) : parent::fwrite($str, $length);
+ restore_error_handler();
+ return $retVal;
+ }
+
+ public function ftruncate($size): bool
+ {
+ $this->assertOpenForWriting();
+ $this->setupErrorHandler();
+ $retVal = parent::ftruncate($size);
+ restore_error_handler();
+ return $retVal;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function ftell()
+ {
+ $this->setupErrorHandler();
+ $retVal = parent::ftell();
+ restore_error_handler();
+ return $retVal;
+ }
+
+ public function flock($operation, &$wouldblock = null): bool
+ {
+ $this->setupErrorHandler();
+ $retVal = parent::flock($operation, $wouldblock);
+ restore_error_handler();
+ return $retVal;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function fgetc()
+ {
+ $this->setupErrorHandler();
+ $retVal = parent::fgetc();
+ restore_error_handler();
+ return $retVal;
+ }
+
+ public function fflush(): bool
+ {
+ $this->setupErrorHandler();
+ $retVal = parent::fflush();
+ restore_error_handler();
+ return $retVal;
+ }
+
+ /**
+ * Setup an error handler that throws a RuntimeException for every emitted E_WARNING
+ */
+ protected function setupErrorHandler()
+ {
+ set_error_handler(
+ function ($errno, $errstr, $errfile, $errline) {
+ restore_error_handler();
+ throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
+ },
+ E_WARNING
+ );
+ }
+
+ /**
+ * Assert that the file was opened for writing and throw an exception otherwise
+ *
+ * @throws NotWritableError In case the file was not opened for writing
+ */
+ protected function assertOpenForWriting()
+ {
+ if (!preg_match('@w|a|\+@', $this->openMode)) {
+ throw new NotWritableError('File not open for writing');
+ }
+ }
+}
diff --git a/library/Icinga/Util/Format.php b/library/Icinga/Util/Format.php
new file mode 100644
index 0000000..1158208
--- /dev/null
+++ b/library/Icinga/Util/Format.php
@@ -0,0 +1,197 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Util;
+
+use DateTime;
+
+class Format
+{
+ const STANDARD_IEC = 0;
+ const STANDARD_SI = 1;
+ protected static $instance;
+
+ protected static $bitPrefix = array(
+ array('bit', 'Kibit', 'Mibit', 'Gibit', 'Tibit', 'Pibit', 'Eibit', 'Zibit', 'Yibit'),
+ array('bit', 'kbit', 'Mbit', 'Gbit', 'Tbit', 'Pbit', 'Ebit', 'Zbit', 'Ybit'),
+ );
+ protected static $bitBase = array(1024, 1000);
+
+ protected static $bytePrefix = array(
+ array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'),
+ array('B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'),
+ );
+ protected static $byteBase = array(1024, 1000);
+
+ protected static $secondPrefix = array('s', 'ms', 'µs', 'ns', 'ps', 'fs', 'as');
+ protected static $secondBase = 1000;
+
+ public static function getInstance()
+ {
+ if (self::$instance === null) {
+ self::$instance = new Format;
+ }
+ return self::$instance;
+ }
+
+ public static function bits($value, $standard = self::STANDARD_SI)
+ {
+ return self::formatForUnits(
+ $value,
+ self::$bitPrefix[$standard],
+ self::$bitBase[$standard]
+ );
+ }
+
+ public static function bytes($value, $standard = self::STANDARD_IEC)
+ {
+ return self::formatForUnits(
+ $value,
+ self::$bytePrefix[$standard],
+ self::$byteBase[$standard]
+ );
+ }
+
+ public static function seconds($value)
+ {
+ if ($value === null) {
+ return '';
+ }
+
+ $absValue = abs($value);
+
+ if ($absValue < 60) {
+ return self::formatForUnits($value, self::$secondPrefix, self::$secondBase);
+ } elseif ($absValue < 3600) {
+ return sprintf('%0.2f m', $value / 60);
+ } elseif ($absValue < 86400) {
+ return sprintf('%0.2f h', $value / 3600);
+ }
+
+ // TODO: Do we need weeks, months and years?
+ return sprintf('%0.2f d', $value / 86400);
+ }
+
+ protected static function formatForUnits($value, &$units, $base)
+ {
+ if ($value === null) {
+ return '';
+ }
+
+ $sign = '';
+ if ($value < 0) {
+ $value = abs($value);
+ $sign = '-';
+ }
+
+ if ($value == 0) {
+ $pow = $result = 0;
+ } else {
+ $pow = floor(log($value, $base));
+ $result = $value / pow($base, $pow);
+ }
+
+ // 1034.23 looks better than 1.03, but 2.03 is fine:
+ if ($pow > 0 && $result < 2) {
+ $result = $value / pow($base, --$pow);
+ }
+
+ return sprintf(
+ '%s%0.2f %s',
+ $sign,
+ $result,
+ $units[abs($pow)]
+ );
+ }
+
+ /**
+ * Return the amount of seconds based on the given month
+ *
+ * @param DateTime|int $dateTimeOrTimestamp The date and time to use
+ *
+ * @return int
+ */
+ public static function secondsByMonth($dateTimeOrTimestamp)
+ {
+ if ($dateTimeOrTimestamp === null) {
+ return 0;
+ }
+
+ if (!($dt = $dateTimeOrTimestamp) instanceof DateTime) {
+ $dt = new DateTime();
+ $dt->setTimestamp($dateTimeOrTimestamp);
+ }
+
+ return (int) $dt->format('t') * 24 * 3600;
+ }
+
+ /**
+ * Return the amount of seconds based on the given year
+ *
+ * @param DateTime|int $dateTimeOrTimestamp The date and time to use
+ *
+ * @return int
+ */
+ public static function secondsByYear($dateTimeOrTimestamp)
+ {
+ if ($dateTimeOrTimestamp === null) {
+ return 0;
+ }
+
+ return (self::isLeapYear($dateTimeOrTimestamp) ? 366 : 365) * 24 * 3600;
+ }
+
+ /**
+ * Return whether the given year is a leap year
+ *
+ * @param DateTime|int $dateTimeOrTimestamp The date and time to use
+ *
+ * @return bool
+ */
+ public static function isLeapYear($dateTimeOrTimestamp)
+ {
+ if ($dateTimeOrTimestamp === null) {
+ return false;
+ }
+
+ if (!($dt = $dateTimeOrTimestamp) instanceof DateTime) {
+ $dt = new DateTime();
+ $dt->setTimestamp($dateTimeOrTimestamp);
+ }
+
+ return $dt->format('L') == 1;
+ }
+
+ /**
+ * Unpack shorthand bytes PHP directives to bytes
+ *
+ * @param string $subject
+ *
+ * @return int
+ */
+ public static function unpackShorthandBytes($subject)
+ {
+ $base = (int) $subject;
+
+ if ($base <= -1) {
+ return INF;
+ }
+
+ switch (strtoupper($subject[strlen($subject) - 1])) {
+ case 'K':
+ $multiplier = 1024;
+ break;
+ case 'M':
+ $multiplier = 1024 ** 2;
+ break;
+ case 'G':
+ $multiplier = 1024 ** 3;
+ break;
+ default:
+ $multiplier = 1;
+ break;
+ }
+
+ return $base * $multiplier;
+ }
+}
diff --git a/library/Icinga/Util/GlobFilter.php b/library/Icinga/Util/GlobFilter.php
new file mode 100644
index 0000000..ac0493a
--- /dev/null
+++ b/library/Icinga/Util/GlobFilter.php
@@ -0,0 +1,182 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Util;
+
+use stdClass;
+
+/**
+ * GLOB-like filter for simple data structures
+ *
+ * e.g. this filters:
+ *
+ * foo.bar.baz
+ * foo.b*r.baz
+ * **.baz
+ *
+ * match this one:
+ *
+ * array(
+ * 'foo' => array(
+ * 'bar' => array(
+ * 'baz' => 'deadbeef' // <---
+ * )
+ * )
+ * )
+ */
+class GlobFilter
+{
+ /**
+ * The prepared filters
+ *
+ * @var array
+ */
+ protected $filters;
+
+ /**
+ * Create a new filter from a comma-separated list of GLOB-like filters or an array of such lists.
+ *
+ * @param string|\Traversable|iterable $filters
+ */
+ public function __construct($filters)
+ {
+ $patterns = array(array(''));
+ $lastIndex1 = $lastIndex2 = 0;
+
+ foreach ((is_string($filters) ? array($filters) : $filters) as $rawPatterns) {
+ $escape = false;
+
+ foreach (str_split($rawPatterns) as $c) {
+ if ($escape) {
+ $escape = false;
+ $patterns[$lastIndex1][$lastIndex2] .= preg_quote($c, '/');
+ } else {
+ switch ($c) {
+ case '\\':
+ $escape = true;
+ break;
+ case ',':
+ $patterns[] = array('');
+ ++$lastIndex1;
+ $lastIndex2 = 0;
+ break;
+ case '.':
+ $patterns[$lastIndex1][] = '';
+ ++$lastIndex2;
+ break;
+ case '*':
+ $patterns[$lastIndex1][$lastIndex2] .= '.*';
+ break;
+ default:
+ $patterns[$lastIndex1][$lastIndex2] .= preg_quote($c, '/');
+ }
+ }
+ }
+
+ if ($escape) {
+ $patterns[$lastIndex1][$lastIndex2] .= '\\\\';
+ }
+ }
+
+ $this->filters = array();
+
+ foreach ($patterns as $pattern) {
+ foreach ($pattern as $i => $subPattern) {
+ if ($subPattern === '') {
+ unset($pattern[$i]);
+ } elseif ($subPattern === '.*.*') {
+ $pattern[$i] = '**';
+ } elseif ($subPattern === '.*') {
+ $pattern[$i] = '/^' . $subPattern . '$/';
+ } else {
+ $pattern[$i] = '/^' . trim($subPattern) . '$/i';
+ }
+ }
+
+ if (! empty($pattern)) {
+ $found = false;
+ foreach ($pattern as $i => $v) {
+ if ($found) {
+ if ($v === '**') {
+ unset($pattern[$i]);
+ } else {
+ $found = false;
+ }
+ } elseif ($v === '**') {
+ $found = true;
+ }
+ }
+
+ if (end($pattern) === '**') {
+ $pattern[] = '/^.*$/';
+ }
+
+ $this->filters[] = array_values($pattern);
+ }
+ }
+ }
+
+ /**
+ * Remove all keys/attributes matching any of $this->filters from $dataStructure
+ *
+ * @param stdClass|array $dataStructure
+ *
+ * @return stdClass|array The modified copy of $dataStructure
+ */
+ public function removeMatching($dataStructure)
+ {
+ foreach ($this->filters as $filter) {
+ $dataStructure = static::removeMatchingRecursive($dataStructure, $filter);
+ }
+ return $dataStructure;
+ }
+
+ /**
+ * Helper method for removeMatching()
+ *
+ * @param stdClass|array $dataStructure
+ * @param array $filter
+ *
+ * @return stdClass|array
+ */
+ protected static function removeMatchingRecursive($dataStructure, $filter)
+ {
+ $multiLevelPattern = $filter[0] === '**';
+ if ($multiLevelPattern) {
+ $dataStructure = static::removeMatchingRecursive($dataStructure, array_slice($filter, 1));
+ }
+
+ $isObject = $dataStructure instanceof stdClass;
+ if ($isObject || is_array($dataStructure)) {
+ if ($isObject) {
+ $dataStructure = (array) $dataStructure;
+ }
+
+ if ($multiLevelPattern) {
+ foreach ($dataStructure as $k => & $v) {
+ $v = static::removeMatchingRecursive($v, $filter);
+ unset($v);
+ }
+ } else {
+ $currentLevel = $filter[0];
+ $nextLevels = count($filter) === 1 ? null : array_slice($filter, 1);
+ foreach ($dataStructure as $k => & $v) {
+ if (preg_match($currentLevel, (string) $k)) {
+ if ($nextLevels === null) {
+ unset($dataStructure[$k]);
+ } else {
+ $v = static::removeMatchingRecursive($v, $nextLevels);
+ }
+ }
+ unset($v);
+ }
+ }
+
+ if ($isObject) {
+ $dataStructure = (object) $dataStructure;
+ }
+ }
+
+ return $dataStructure;
+ }
+}
diff --git a/library/Icinga/Util/Json.php b/library/Icinga/Util/Json.php
new file mode 100644
index 0000000..0b89dcc
--- /dev/null
+++ b/library/Icinga/Util/Json.php
@@ -0,0 +1,151 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Util;
+
+use Icinga\Exception\Json\JsonDecodeException;
+use Icinga\Exception\Json\JsonEncodeException;
+
+/**
+ * Wrap {@link json_encode()} and {@link json_decode()} with error handling
+ */
+class Json
+{
+ /**
+ * {@link json_encode()} wrapper
+ *
+ * @param mixed $value
+ * @param int $options
+ * @param int $depth
+ *
+ * @return string
+ * @throws JsonEncodeException
+ */
+ public static function encode($value, $options = 0, $depth = 512)
+ {
+ return static::encodeAndSanitize($value, $options, $depth, false);
+ }
+
+ /**
+ * {@link json_encode()} wrapper, automatically sanitizes bad UTF-8
+ *
+ * @param mixed $value
+ * @param int $options
+ * @param int $depth
+ *
+ * @return string
+ * @throws JsonEncodeException
+ */
+ public static function sanitize($value, $options = 0, $depth = 512)
+ {
+ return static::encodeAndSanitize($value, $options, $depth, true);
+ }
+
+ /**
+ * {@link json_encode()} wrapper, sanitizes bad UTF-8
+ *
+ * @param mixed $value
+ * @param int $options
+ * @param int $depth
+ * @param bool $autoSanitize Automatically sanitize invalid UTF-8 (if any)
+ *
+ * @return string
+ * @throws JsonEncodeException
+ */
+ protected static function encodeAndSanitize($value, $options, $depth, $autoSanitize)
+ {
+ $encoded = json_encode($value, $options, $depth);
+
+ switch (json_last_error()) {
+ case JSON_ERROR_NONE:
+ return $encoded;
+
+ /** @noinspection PhpMissingBreakStatementInspection */
+ case JSON_ERROR_UTF8:
+ if ($autoSanitize) {
+ return static::encode(static::sanitizeUtf8Recursive($value), $options, $depth);
+ }
+ // Fallthrough
+
+ default:
+ throw new JsonEncodeException('%s: %s', json_last_error_msg(), var_export($value, true));
+ }
+ }
+
+ /**
+ * {@link json_decode()} wrapper
+ *
+ * @param string $json
+ * @param bool $assoc
+ * @param int $depth
+ * @param int $options
+ *
+ * @return mixed
+ * @throws JsonDecodeException
+ */
+ public static function decode($json, $assoc = false, $depth = 512, $options = 0)
+ {
+ $decoded = $json ? json_decode($json, $assoc, $depth, $options) : null;
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new JsonDecodeException('%s: %s', json_last_error_msg(), var_export($json, true));
+ }
+ return $decoded;
+ }
+
+ /**
+ * Replace bad byte sequences in UTF-8 strings inside the given JSON-encodable structure with question marks
+ *
+ * @param mixed $value
+ *
+ * @return mixed
+ */
+ protected static function sanitizeUtf8Recursive($value)
+ {
+ switch (gettype($value)) {
+ case 'string':
+ return static::sanitizeUtf8String($value);
+
+ case 'array':
+ $sanitized = array();
+
+ foreach ($value as $key => $val) {
+ if (is_string($key)) {
+ $key = static::sanitizeUtf8String($key);
+ }
+
+ $sanitized[$key] = static::sanitizeUtf8Recursive($val);
+ }
+
+ return $sanitized;
+
+ case 'object':
+ $sanitized = array();
+
+ foreach ($value as $key => $val) {
+ if (is_string($key)) {
+ $key = static::sanitizeUtf8String($key);
+ }
+
+ $sanitized[$key] = static::sanitizeUtf8Recursive($val);
+ }
+
+ return (object) $sanitized;
+
+ default:
+ return $value;
+ }
+ }
+
+ /**
+ * Replace bad byte sequences in the given UTF-8 string with question marks
+ *
+ * @param string $string
+ *
+ * @return string
+ */
+ protected static function sanitizeUtf8String($string)
+ {
+ return mb_convert_encoding($string, 'UTF-8', 'UTF-8');
+ }
+}
diff --git a/library/Icinga/Util/LessParser.php b/library/Icinga/Util/LessParser.php
new file mode 100644
index 0000000..1e07aa9
--- /dev/null
+++ b/library/Icinga/Util/LessParser.php
@@ -0,0 +1,15 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Util;
+
+use Icinga\Less\Visitor;
+use lessc;
+
+class LessParser extends lessc
+{
+ public function __construct()
+ {
+ $this->setOption('plugins', [new Visitor()]);
+ }
+}
diff --git a/library/Icinga/Util/StringHelper.php b/library/Icinga/Util/StringHelper.php
new file mode 100644
index 0000000..67a836b
--- /dev/null
+++ b/library/Icinga/Util/StringHelper.php
@@ -0,0 +1,184 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Util;
+
+/**
+ * Common string functions
+ */
+class StringHelper
+{
+ /**
+ * Split string into an array and trim spaces
+ *
+ * @param string $value
+ * @param string $delimiter
+ * @param int $limit
+ *
+ * @return array
+ */
+ public static function trimSplit($value, $delimiter = ',', $limit = null)
+ {
+ if ($value === null) {
+ return [];
+ }
+
+ if ($limit !== null) {
+ $exploded = explode($delimiter, $value, $limit);
+ } else {
+ $exploded = explode($delimiter, $value);
+ }
+
+ return array_map('trim', $exploded);
+ }
+
+ /**
+ * Uppercase the first character of each word in a string
+ *
+ * Converts 'first_name' to 'FirstName' for example.
+ *
+ * @param string $name
+ * @param string $separator Word separator
+ *
+ * @return string
+ */
+ public static function cname($name, $separator = '_')
+ {
+ if ($name === null) {
+ return '';
+ }
+
+ return str_replace(' ', '', ucwords(str_replace($separator, ' ', strtolower($name))));
+ }
+
+ /**
+ * Add ellipsis when a string is longer than max length
+ *
+ * @param string $string
+ * @param int $maxLength
+ * @param string $ellipsis
+ *
+ * @return string
+ */
+ public static function ellipsis($string, $maxLength, $ellipsis = '...')
+ {
+ if ($string === null) {
+ return '';
+ }
+
+ if (strlen($string) > $maxLength) {
+ return substr($string, 0, $maxLength - strlen($ellipsis)) . $ellipsis;
+ }
+
+ return $string;
+ }
+
+ /**
+ * Add ellipsis in the center of a string when a string is longer than max length
+ *
+ * @param string $string
+ * @param int $maxLength
+ * @param string $ellipsis
+ *
+ * @return string
+ */
+ public static function ellipsisCenter($string, $maxLength, $ellipsis = '...')
+ {
+ if ($string === null) {
+ return '';
+ }
+
+ $start = ceil($maxLength / 2.0);
+ $end = floor($maxLength / 2.0);
+ if (strlen($string) > $maxLength) {
+ return substr($string, 0, $start - strlen($ellipsis)) . $ellipsis . substr($string, - $end);
+ }
+
+ return $string;
+ }
+
+ /**
+ * Find and return all similar strings in $possibilites matching $string with the given minimum $similarity
+ *
+ * @param string $string
+ * @param array $possibilities
+ * @param float $similarity
+ *
+ * @return array
+ */
+ public static function findSimilar($string, array $possibilities, $similarity = 0.33)
+ {
+ if (empty($string)) {
+ return array();
+ }
+
+ $matches = array();
+ foreach ($possibilities as $possibility) {
+ $distance = levenshtein($string, $possibility);
+ if ($distance / strlen($string) <= $similarity) {
+ $matches[] = $possibility;
+ }
+ }
+
+ return $matches;
+ }
+
+ /**
+ * Test whether the given string ends with the given suffix
+ *
+ * @param string $string The string to test
+ * @param string $suffix The suffix the string must end with
+ *
+ * @return bool
+ */
+ public static function endsWith($string, $suffix)
+ {
+ if ($string === null) {
+ return false;
+ }
+
+ $stringSuffix = substr($string, -strlen($suffix));
+ return $stringSuffix !== false ? $stringSuffix === $suffix : false;
+ }
+
+ /**
+ * Generates an array of strings that constitutes the cartesian product of all passed sets, with all
+ * string combinations concatenated using the passed join-operator.
+ *
+ * <pre>
+ * cartesianProduct(
+ * array(array('foo', 'bar'), array('mumble', 'grumble', null)),
+ * '_'
+ * );
+ * => array('foo_mumble', 'foo_grumble', 'bar_mumble', 'bar_grumble', 'foo', 'bar')
+ * </pre>
+ *
+ * @param array $sets An array of arrays containing all sets for which the cartesian
+ * product should be calculated.
+ * @param string $glue The glue used to join the strings, defaults to ''.
+ *
+ * @returns array The cartesian product in one array of strings.
+ */
+ public static function cartesianProduct(array $sets, $glue = '')
+ {
+ $product = null;
+ foreach ($sets as $set) {
+ if (! isset($product)) {
+ $product = $set;
+ } else {
+ $newProduct = array();
+ foreach ($product as $strA) {
+ foreach ($set as $strB) {
+ if ($strB === null) {
+ $newProduct []= $strA;
+ } else {
+ $newProduct []= $strA . $glue . $strB;
+ }
+ }
+ }
+ $product = $newProduct;
+ }
+ }
+ return $product;
+ }
+}
diff --git a/library/Icinga/Util/TimezoneDetect.php b/library/Icinga/Util/TimezoneDetect.php
new file mode 100644
index 0000000..4967c7f
--- /dev/null
+++ b/library/Icinga/Util/TimezoneDetect.php
@@ -0,0 +1,107 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Util;
+
+/**
+ * Retrieve timezone information from cookie
+ */
+class TimezoneDetect
+{
+ /**
+ * If detection was successful
+ *
+ * @var bool
+ */
+ private static $success;
+
+ /**
+ * Timezone offset in minutes
+ *
+ * @var int
+ */
+ private static $offset = 0;
+
+ /**
+ * @var string
+ */
+ private static $timezoneName;
+
+ /**
+ * Cookie name
+ *
+ * @var string
+ */
+ public static $cookieName = 'icingaweb2-tzo';
+
+ /**
+ * Timezone name
+ *
+ * @var string
+ */
+ private static $timezone;
+
+ /**
+ * Create new object and try to identify the timezone
+ */
+ public function __construct()
+ {
+ if (self::$success !== null) {
+ return;
+ }
+
+ if (array_key_exists(self::$cookieName, $_COOKIE)) {
+ $matches = array();
+ if (preg_match('/\A(-?\d+)[\-,](\d+)\z/', $_COOKIE[self::$cookieName], $matches)) {
+ $offset = $matches[1];
+ $timezoneName = timezone_name_from_abbr('', (int) $offset, (int) $matches[2]);
+
+ self::$success = (bool) $timezoneName;
+ if (self::$success) {
+ self::$offset = $offset;
+ self::$timezoneName = $timezoneName;
+ }
+ }
+ }
+ }
+
+ /**
+ * Get offset
+ *
+ * @return int
+ */
+ public function getOffset()
+ {
+ return self::$offset;
+ }
+
+ /**
+ * Get timezone name
+ *
+ * @return string
+ */
+ public function getTimezoneName()
+ {
+ return self::$timezoneName;
+ }
+
+ /**
+ * True on success
+ *
+ * @return bool
+ */
+ public function success()
+ {
+ return self::$success;
+ }
+
+ /**
+ * Reset object
+ */
+ public function reset()
+ {
+ self::$success = null;
+ self::$timezoneName = null;
+ self::$offset = 0;
+ }
+}
diff --git a/library/Icinga/Web/Announcement.php b/library/Icinga/Web/Announcement.php
new file mode 100644
index 0000000..9835ce0
--- /dev/null
+++ b/library/Icinga/Web/Announcement.php
@@ -0,0 +1,158 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+/**
+ * An announcement to be displayed prominently in the web UI
+ */
+class Announcement
+{
+ /**
+ * @var string
+ */
+ protected $author;
+
+ /**
+ * @var string
+ */
+ protected $message;
+
+ /**
+ * @var int
+ */
+ protected $start;
+
+ /**
+ * @var int
+ */
+ protected $end;
+
+ /**
+ * Hash of the message
+ *
+ * @var string|null
+ */
+ protected $hash = null;
+
+ /**
+ * Announcement constructor
+ *
+ * @param array $properties
+ */
+ public function __construct(array $properties = array())
+ {
+ foreach ($properties as $key => $value) {
+ $method = 'set' . ucfirst($key);
+ if (method_exists($this, $method)) {
+ $this->$method($value);
+ }
+ }
+ }
+
+ /**
+ * Get the author of the acknowledged
+ *
+ * @return string
+ */
+ public function getAuthor()
+ {
+ return $this->author;
+ }
+
+ /**
+ * Set the author of the acknowledged
+ *
+ * @param string $author
+ *
+ * @return $this
+ */
+ public function setAuthor($author)
+ {
+ $this->author = $author;
+ return $this;
+ }
+
+ /**
+ * Get the message of the acknowledged
+ *
+ * @return string
+ */
+ public function getMessage()
+ {
+ return $this->message;
+ }
+
+ /**
+ * Set the message of the acknowledged
+ *
+ * @param string $message
+ *
+ * @return $this
+ */
+ public function setMessage($message)
+ {
+ $this->message = $message;
+ $this->hash = null;
+ return $this;
+ }
+
+ /**
+ * Get the start date and time of the acknowledged
+ *
+ * @return int
+ */
+ public function getStart()
+ {
+ return $this->start;
+ }
+
+ /**
+ * Set the start date and time of the acknowledged
+ *
+ * @param int $start
+ *
+ * @return $this
+ */
+ public function setStart($start)
+ {
+ $this->start = $start;
+ return $this;
+ }
+
+ /**
+ * Get the end date and time of the acknowledged
+ *
+ * @return int
+ */
+ public function getEnd()
+ {
+ return $this->end;
+ }
+
+ /**
+ * Set the end date and time of the acknowledged
+ *
+ * @param int $end
+ *
+ * @return $this
+ */
+ public function setEnd($end)
+ {
+ $this->end = $end;
+ return $this;
+ }
+
+ /**
+ * Get the hash of the acknowledgement
+ *
+ * @return string
+ */
+ public function getHash()
+ {
+ if ($this->hash === null) {
+ $this->hash = md5($this->message);
+ }
+ return $this->hash;
+ }
+}
diff --git a/library/Icinga/Web/Announcement/AnnouncementCookie.php b/library/Icinga/Web/Announcement/AnnouncementCookie.php
new file mode 100644
index 0000000..6d23872
--- /dev/null
+++ b/library/Icinga/Web/Announcement/AnnouncementCookie.php
@@ -0,0 +1,138 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Announcement;
+
+use Icinga\Util\Json;
+use Icinga\Web\Cookie;
+
+/**
+ * Handle acknowledged announcements via cookie
+ */
+class AnnouncementCookie extends Cookie
+{
+ /**
+ * Array of hashes representing acknowledged announcements
+ *
+ * @var string[]
+ */
+ protected $acknowledged = array();
+
+ /**
+ * ETag of the last known announcements.ini
+ *
+ * @var string
+ */
+ protected $etag;
+
+ /**
+ * Timestamp of the next active acknowledgement, if any
+ *
+ * @var int|null
+ */
+ protected $nextActive;
+
+ /**
+ * AnnouncementCookie constructor
+ */
+ public function __construct()
+ {
+ parent::__construct('icingaweb2-announcements');
+ $this->setExpire(2147483648);
+ if (isset($_COOKIE['icingaweb2-announcements'])) {
+ $cookie = json_decode($_COOKIE['icingaweb2-announcements'], true);
+ if ($cookie !== null) {
+ if (isset($cookie['acknowledged'])) {
+ $this->setAcknowledged($cookie['acknowledged']);
+ }
+ if (isset($cookie['etag'])) {
+ $this->setEtag($cookie['etag']);
+ }
+ if (isset($cookie['next'])) {
+ $this->setNextActive($cookie['next']);
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the hashes of the acknowledged announcements
+ *
+ * @return string[]
+ */
+ public function getAcknowledged()
+ {
+ return $this->acknowledged;
+ }
+
+ /**
+ * Set the hashes of the acknowledged announcements
+ *
+ * @param string[] $acknowledged
+ *
+ * @return $this
+ */
+ public function setAcknowledged(array $acknowledged)
+ {
+ $this->acknowledged = $acknowledged;
+ return $this;
+ }
+
+ /**
+ * Get the ETag
+ *
+ * @return string
+ */
+ public function getEtag()
+ {
+ return $this->etag;
+ }
+
+ /**
+ * Set the ETag
+ *
+ * @param string $etag
+ *
+ * @return $this
+ */
+ public function setEtag($etag)
+ {
+ $this->etag = $etag;
+ return $this;
+ }
+
+ /**
+ * Get the timestamp of the next active announcement
+ *
+ * @return ?int
+ */
+ public function getNextActive()
+ {
+ return $this->nextActive;
+ }
+
+ /**
+ * Set the timestamp of the next active announcement
+ *
+ * @param ?int $nextActive
+ *
+ * @return $this
+ */
+ public function setNextActive(?int $nextActive)
+ {
+ $this->nextActive = $nextActive;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValue()
+ {
+ return Json::encode(array(
+ 'acknowledged' => $this->getAcknowledged(),
+ 'etag' => $this->getEtag(),
+ 'next' => $this->getNextActive()
+ ));
+ }
+}
diff --git a/library/Icinga/Web/Announcement/AnnouncementIniRepository.php b/library/Icinga/Web/Announcement/AnnouncementIniRepository.php
new file mode 100644
index 0000000..d972a1d
--- /dev/null
+++ b/library/Icinga/Web/Announcement/AnnouncementIniRepository.php
@@ -0,0 +1,152 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Announcement;
+
+use DateTime;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Data\SimpleQuery;
+use Icinga\Repository\IniRepository;
+use Icinga\Web\Announcement;
+
+/**
+ * A collection of announcements stored in an INI file
+ */
+class AnnouncementIniRepository extends IniRepository
+{
+ protected $queryColumns = array('announcement' => array('id', 'author', 'message', 'hash', 'start', 'end'));
+
+ protected $triggers = array('announcement');
+
+ protected $configs = array('announcement' => array(
+ 'name' => 'announcements',
+ 'keyColumn' => 'id'
+ ));
+
+ protected $conversionRules = array('announcement' => array(
+ 'start' => 'timestamp',
+ 'end' => 'timestamp'
+ ));
+
+ /**
+ * Get a DateTime's timestamp
+ *
+ * @param DateTime $datetime
+ *
+ * @return int|null
+ */
+ protected function persistTimestamp(DateTime $datetime)
+ {
+ return $datetime === null ? null : $datetime->getTimestamp();
+ }
+
+ /**
+ * Before-insert trigger (per row)
+ *
+ * @param ConfigObject $new The original data to insert
+ *
+ * @return ConfigObject The eventually modified data to insert
+ */
+ protected function onInsertAnnouncement(ConfigObject $new)
+ {
+ if (! isset($new->id)) {
+ $new->id = uniqid();
+ }
+
+ if (! isset($new->hash)) {
+ $announcement = new Announcement($new->toArray());
+ $new->hash = $announcement->getHash();
+ }
+
+ return $new;
+ }
+
+ /**
+ * Before-update trigger (per row)
+ *
+ * @param ConfigObject $old The original data as currently stored
+ * @param ConfigObject $new The original data to update
+ *
+ * @return ConfigObject The eventually modified data to update
+ */
+ protected function onUpdateAnnouncement(ConfigObject $old, ConfigObject $new)
+ {
+ if ($new->message !== $old->message) {
+ $announcement = new Announcement($new->toArray());
+ $new->hash = $announcement->getHash();
+ }
+
+ return $new;
+ }
+
+ /**
+ * Get the ETag of the announcements.ini file
+ *
+ * @return string
+ */
+ public function getEtag()
+ {
+ $file = $this->getDataSource('announcement')->getConfigFile();
+
+ if (@is_readable($file)) {
+ $mtime = filemtime($file);
+ $size = filesize($file);
+
+ return hash('crc32', $mtime . $size);
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the query for all active announcements
+ *
+ * @return SimpleQuery
+ */
+ public function findActive()
+ {
+ $now = new DateTime();
+
+ $query = $this
+ ->select(array('hash', 'message', 'start'))
+ ->setFilter(new FilterAnd(array(
+ Filter::expression('start', '<=', $now),
+ Filter::expression('end', '>=', $now)
+ )))
+ ->order('start');
+
+ return $query;
+ }
+
+ /**
+ * Get the timestamp of the next active announcement
+ *
+ * @return int|null
+ */
+ public function findNextActive()
+ {
+ $now = new DateTime();
+
+ $query = $this
+ ->select(array('start', 'end'))
+ ->setFilter(Filter::matchAny(array(
+ Filter::expression('start', '>', $now), Filter::expression('end', '>', $now)
+ )));
+
+ $refresh = null;
+
+ foreach ($query as $row) {
+ $min = min($row->start, $row->end);
+
+ if ($refresh === null) {
+ $refresh = $min;
+ } else {
+ $refresh = min($refresh, $min);
+ }
+ }
+
+ return $refresh;
+ }
+}
diff --git a/library/Icinga/Web/ApplicationStateCookie.php b/library/Icinga/Web/ApplicationStateCookie.php
new file mode 100644
index 0000000..e40c17b
--- /dev/null
+++ b/library/Icinga/Web/ApplicationStateCookie.php
@@ -0,0 +1,74 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Application\Logger;
+use Icinga\Authentication\Auth;
+use Icinga\Exception\Json\JsonDecodeException;
+use Icinga\Util\Json;
+
+/**
+ * Handle acknowledged application state messages via cookie
+ */
+class ApplicationStateCookie extends Cookie
+{
+ /** @var array */
+ protected $acknowledgedMessages = [];
+
+ public function __construct()
+ {
+ parent::__construct('icingaweb2-application-state');
+
+ $this->setExpire(2147483648);
+
+ if (isset($_COOKIE['icingaweb2-application-state'])) {
+ try {
+ $cookie = Json::decode($_COOKIE['icingaweb2-application-state'], true);
+ } catch (JsonDecodeException $e) {
+ Logger::error(
+ "Can't decode the application state cookie of user '%s'. An error occurred: %s",
+ Auth::getInstance()->getUser()->getUsername(),
+ $e
+ );
+
+ return;
+ }
+
+ if (isset($cookie['acknowledged-messages'])) {
+ $this->setAcknowledgedMessages($cookie['acknowledged-messages']);
+ }
+ }
+ }
+
+ /**
+ * Get the acknowledged messages
+ *
+ * @return array
+ */
+ public function getAcknowledgedMessages()
+ {
+ return $this->acknowledgedMessages;
+ }
+
+ /**
+ * Set the acknowledged messages
+ *
+ * @param array $acknowledged
+ *
+ * @return $this
+ */
+ public function setAcknowledgedMessages(array $acknowledged)
+ {
+ $this->acknowledgedMessages = $acknowledged;
+
+ return $this;
+ }
+
+ public function getValue()
+ {
+ return Json::encode([
+ 'acknowledged-messages' => $this->getAcknowledgedMessages()
+ ]);
+ }
+}
diff --git a/library/Icinga/Web/Controller.php b/library/Icinga/Web/Controller.php
new file mode 100644
index 0000000..008fbf6
--- /dev/null
+++ b/library/Icinga/Web/Controller.php
@@ -0,0 +1,264 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Data\Filterable;
+use Icinga\Data\Sortable;
+use Icinga\Data\QueryInterface;
+use Icinga\Exception\Http\HttpBadRequestException;
+use Icinga\Exception\Http\HttpNotFoundException;
+use Icinga\Web\Controller\ModuleActionController;
+use Icinga\Web\Widget\Limiter;
+use Icinga\Web\Widget\Paginator;
+use Icinga\Web\Widget\SortBox;
+
+/**
+ * This is the controller all modules should inherit from
+ * We will flip code with the ModuleActionController as soon as a couple
+ * of pending feature branches are merged back to the master.
+ *
+ * @property View $view
+ */
+class Controller extends ModuleActionController
+{
+ /**
+ * Cache for page size configured via user preferences
+ *
+ * @var false|int
+ */
+ protected $userPageSize;
+
+ /**
+ * @see ActionController::init
+ */
+ public function init()
+ {
+ parent::init();
+ $this->handleSortControlSubmit();
+ }
+
+ /**
+ * Check whether the sort control has been submitted and redirect using GET parameters
+ */
+ protected function handleSortControlSubmit()
+ {
+ $request = $this->getRequest();
+ if (! $request->isPost()) {
+ return;
+ }
+
+ if (($sort = $request->getPost('sort')) || ($direction = $request->getPost('dir'))) {
+ $url = Url::fromRequest();
+ if ($sort) {
+ $url->setParam('sort', $sort);
+ $url->remove('dir');
+ } else {
+ $url->setParam('dir', $direction);
+ }
+
+ $this->redirectNow($url);
+ }
+ }
+
+ /**
+ * Immediately respond w/ HTTP 400
+ *
+ * @param string $message Exception message or exception format string
+ * @param mixed ...$arg Format string argument
+ *
+ * @throws HttpBadRequestException
+ */
+ public function httpBadRequest($message)
+ {
+ throw HttpBadRequestException::create(func_get_args());
+ }
+
+ /**
+ * Immediately respond w/ HTTP 404
+ *
+ * @param string $message Exception message or exception format string
+ * @param mixed ...$arg Format string argument
+ *
+ * @throws HttpNotFoundException
+ */
+ public function httpNotFound($message)
+ {
+ throw HttpNotFoundException::create(func_get_args());
+ }
+
+ /**
+ * Render the given form using a simple view script
+ *
+ * @param Form $form
+ * @param string $tab
+ */
+ public function renderForm(Form $form, $tab)
+ {
+ $this->getTabs()->add(uniqid(), array(
+ 'active' => true,
+ 'label' => $tab,
+ 'url' => Url::fromRequest()
+ ));
+ $this->view->form = $form;
+ $this->render('simple-form', null, true);
+ }
+
+ /**
+ * Create a SortBox widget and apply its sort rules on the given query
+ *
+ * The widget is set on the `sortBox' view property only if the current view has not been requested as compact
+ *
+ * @param array $columns An array containing the sort columns, with the
+ * submit value as the key and the label as the value
+ * @param Sortable $query Query to apply the user chosen sort rules on
+ * @param array $defaults An array containing default sort directions for specific columns
+ *
+ * @return $this
+ */
+ protected function setupSortControl(array $columns, Sortable $query = null, array $defaults = null)
+ {
+ $request = $this->getRequest();
+ $sortBox = SortBox::create('sortbox-' . $request->getActionName(), $columns, $defaults);
+ $sortBox->setRequest($request);
+
+ if ($query) {
+ $sortBox->setQuery($query);
+ $sortBox->handleRequest($request);
+ }
+
+ if (! $this->view->compact) {
+ $this->view->sortBox = $sortBox;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Create a Limiter widget at the `limiter' view property
+ *
+ * In case the current view has been requested as compact this method does nothing.
+ *
+ * @param int $itemsPerPage Default number of items per page
+ *
+ * @return $this
+ */
+ protected function setupLimitControl($itemsPerPage = 25)
+ {
+ if (! $this->view->compact) {
+ $this->view->limiter = new Limiter();
+ $this->view->limiter->setDefaultLimit($this->getPageSize($itemsPerPage));
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the page size configured via user preferences or return the default value
+ *
+ * @param ?int $default
+ *
+ * @return int
+ */
+ protected function getPageSize($default)
+ {
+ if ($this->userPageSize === null) {
+ $user = $this->Auth()->getUser();
+ if ($user !== null) {
+ $pageSize = $user->getPreferences()->getValue('icingaweb', 'default_page_size');
+ $this->userPageSize = $pageSize ? (int) $pageSize : false;
+ } else {
+ $this->userPageSize = false;
+ }
+ }
+
+ return $this->userPageSize !== false ? $this->userPageSize : $default;
+ }
+
+ /**
+ * Apply the given page limit and number on the given query and setup a paginator for it
+ *
+ * The $itemsPerPage and $pageNumber parameters are only applied if not available in the current request.
+ * The paginator is set on the `paginator' view property only if the current view has not been requested as compact.
+ *
+ * @param QueryInterface $query The query to create a paginator for
+ * @param int $itemsPerPage Default number of items per page
+ * @param int $pageNumber Default page number
+ *
+ * @return $this
+ */
+ protected function setupPaginationControl(QueryInterface $query, $itemsPerPage = 25, $pageNumber = 0)
+ {
+ $request = $this->getRequest();
+ $limit = $request->getParam('limit', $this->getPageSize($itemsPerPage));
+ $page = $request->getParam('page', $pageNumber);
+ $query->limit($limit, $page > 0 ? ($page - 1) * $limit : 0);
+
+ if (! $this->view->compact) {
+ $paginator = new Paginator();
+ $paginator->setQuery($query);
+ $this->view->paginator = $paginator;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Create a FilterEditor widget and apply the user's chosen filter options on the given filterable
+ *
+ * The widget is set on the `filterEditor' view property only if the current view has not been requested as compact.
+ * The optional $filterColumns parameter should be an array of key-value pairs where the key is the name of the
+ * column and the value the label to show to the user. The optional $searchColumns parameter should be an array
+ * of column names to be used to handle quick searches.
+ *
+ * If the given filterable is an instance of Icinga\Data\FilterColumns, $filterable->getFilterColumns() and
+ * $filterable->getSearchColumns() is called to provide the respective columns if $filterColumns or $searchColumns
+ * is not given.
+ *
+ * @param Filterable $filterable The filterable to create a filter editor for
+ * @param array $filterColumns The filter columns to offer to the user
+ * @param array $searchColumns The search columns to utilize for quick searches
+ * @param array $preserveParams The url parameters to preserve
+ *
+ * @return $this
+ *
+ * @todo Preserving and ignoring parameters should be configurable (another two method params? property magic?)
+ */
+ protected function setupFilterControl(
+ Filterable $filterable,
+ array $filterColumns = null,
+ array $searchColumns = null,
+ array $preserveParams = null
+ ) {
+ $defaultPreservedParams = array(
+ 'limit', // setupPaginationControl()
+ 'sort', // setupSortControl()
+ 'dir', // setupSortControl()
+ 'backend', // Framework
+ 'showCompact', // Framework
+ '_dev' // Framework
+ );
+
+ $editor = Widget::create('filterEditor');
+ /** @var \Icinga\Web\Widget\FilterEditor $editor */
+ call_user_func_array(
+ array($editor, 'preserveParams'),
+ array_merge($defaultPreservedParams, $preserveParams ?: array())
+ );
+
+ $editor
+ ->setQuery($filterable)
+ ->ignoreParams('page') // setupPaginationControl()
+ ->setColumns($filterColumns)
+ ->setSearchColumns($searchColumns)
+ ->handleRequest($this->getRequest());
+
+ if ($this->view->compact) {
+ $editor->setVisible(false);
+ }
+
+ $this->view->filterEditor = $editor;
+
+ return $this;
+ }
+}
diff --git a/library/Icinga/Web/Controller/ActionController.php b/library/Icinga/Web/Controller/ActionController.php
new file mode 100644
index 0000000..2e36d7d
--- /dev/null
+++ b/library/Icinga/Web/Controller/ActionController.php
@@ -0,0 +1,617 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Controller;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Common\PdfExport;
+use Icinga\File\Pdf;
+use Icinga\Util\Csp;
+use Icinga\Web\View;
+use ipl\I18n\Translation;
+use Zend_Controller_Action;
+use Zend_Controller_Action_HelperBroker;
+use Zend_Controller_Request_Abstract;
+use Zend_Controller_Response_Abstract;
+use Icinga\Application\Benchmark;
+use Icinga\Application\Config;
+use Icinga\Authentication\Auth;
+use Icinga\Exception\Http\HttpMethodNotAllowedException;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Forms\AutoRefreshForm;
+use Icinga\Security\SecurityException;
+use Icinga\Web\Session;
+use Icinga\Web\Url;
+use Icinga\Web\UrlParams;
+use Icinga\Web\Widget\Tabs;
+use Icinga\Web\Window;
+
+/**
+ * Base class for all core action controllers
+ *
+ * All Icinga Web core controllers should extend this class
+ *
+ * @method \Icinga\Web\Request getRequest() {
+ * {@inheritdoc}
+ * @return \Icinga\Web\Request
+ * }
+ *
+ * @method \Icinga\Web\Response getResponse() {
+ * {@inheritdoc}
+ * @return \Icinga\Web\Response
+ * }
+ */
+class ActionController extends Zend_Controller_Action
+{
+ use Translation;
+ use PdfExport {
+ sendAsPdf as private newSendAsPdf;
+ }
+
+ /**
+ * The login route to use when requiring authentication
+ */
+ const LOGIN_ROUTE = 'authentication/login';
+
+ /**
+ * The default page title to use
+ */
+ const DEFAULT_TITLE = 'Icinga Web';
+
+ /**
+ * Whether the controller requires the user to be authenticated
+ *
+ * @var bool
+ */
+ protected $requiresAuthentication = true;
+
+ /**
+ * The current module's name
+ *
+ * @var string
+ */
+ protected $moduleName;
+
+ /**
+ * A page's automatic refresh interval
+ *
+ * The initial value will not be subject to a user's preferences.
+ *
+ * @var int
+ */
+ protected $autorefreshInterval;
+
+ protected $reloadCss = false;
+
+ protected $window;
+
+ protected $rerenderLayout = false;
+
+ /**
+ * The inline layout (inside columns) to use
+ *
+ * @var string
+ */
+ protected $inlineLayout = 'inline';
+
+ /**
+ * The inner layout (inside the body) to use
+ *
+ * @var string
+ */
+ protected $innerLayout = 'body';
+
+ /**
+ * Authentication manager
+ *
+ * @var Auth|null
+ */
+ protected $auth;
+
+ /**
+ * URL parameters
+ *
+ * @var UrlParams
+ */
+ protected $params;
+
+ /**
+ * @var View
+ */
+ public $view;
+
+ /**
+ * The constructor starts benchmarking, loads the configuration and sets
+ * other useful controller properties
+ *
+ * @param Zend_Controller_Request_Abstract $request
+ * @param Zend_Controller_Response_Abstract $response
+ * @param array $invokeArgs Any additional invocation arguments
+ */
+ public function __construct(
+ Zend_Controller_Request_Abstract $request,
+ Zend_Controller_Response_Abstract $response,
+ array $invokeArgs = array()
+ ) {
+ /** @var \Icinga\Web\Request $request */
+ /** @var \Icinga\Web\Response $response */
+ $this->params = UrlParams::fromQueryString();
+
+ $this->setRequest($request)
+ ->setResponse($response)
+ ->_setInvokeArgs($invokeArgs);
+ $this->_helper = new Zend_Controller_Action_HelperBroker($this);
+
+ $moduleName = $this->getModuleName();
+ $this->view->defaultTitle = static::DEFAULT_TITLE;
+ $this->translationDomain = $moduleName !== 'default' ? $moduleName : 'icinga';
+ $this->view->translationDomain = $this->translationDomain;
+ $this->_helper->layout()->isIframe = $request->getUrl()->shift('isIframe');
+ $this->_helper->layout()->showFullscreen = $request->getUrl()->shift('showFullscreen');
+ $this->_helper->layout()->moduleName = $moduleName;
+
+ $this->view->compact = false;
+ if ($request->getUrl()->getParam('view') === 'compact') {
+ $request->getUrl()->remove('view');
+ $this->view->compact = true;
+ }
+ if ($request->getUrl()->shift('showCompact')) {
+ $this->view->compact = true;
+ }
+ $this->rerenderLayout = $request->getUrl()->shift('renderLayout');
+ if ($request->getUrl()->shift('_disableLayout')) {
+ $this->_helper->layout()->disableLayout();
+ }
+
+ // $auth->authenticate($request, $response, $this->requiresLogin());
+ if ($this->requiresLogin()) {
+ if (! $request->isXmlHttpRequest() && $request->isApiRequest()) {
+ Auth::getInstance()->challengeHttp();
+ }
+ $this->redirectToLogin(Url::fromRequest());
+ }
+
+ if (! $this->isXhr() && Config::app()->get('security', 'use_strict_csp', false)) {
+ Csp::createNonce();
+ }
+
+ $this->view->tabs = new Tabs();
+ $this->prepareInit();
+ $this->init();
+ }
+
+ /**
+ * Prepare controller initialization
+ *
+ * As it should not be required for controllers to call the parent's init() method, base controllers should use
+ * prepareInit() in order to prepare the controller initialization.
+ *
+ * @see \Zend_Controller_Action::init() For the controller initialization method.
+ */
+ protected function prepareInit()
+ {
+ }
+
+ /**
+ * Get the authentication manager
+ *
+ * @return Auth
+ */
+ public function Auth()
+ {
+ if ($this->auth === null) {
+ $this->auth = Auth::getInstance();
+ }
+ return $this->auth;
+ }
+
+ /**
+ * Whether the current user has the given permission
+ *
+ * @param string $permission Name of the permission
+ *
+ * @return bool
+ */
+ public function hasPermission($permission)
+ {
+ return $this->Auth()->hasPermission($permission);
+ }
+
+ /**
+ * Assert that the current user has the given permission
+ *
+ * @param string $permission Name of the permission
+ *
+ * @throws SecurityException If the current user lacks the given permission
+ */
+ public function assertPermission($permission)
+ {
+ if (! $this->Auth()->hasPermission($permission)) {
+ throw new SecurityException('No permission for %s', $permission);
+ }
+ }
+
+ /**
+ * Return the current module's name
+ *
+ * @return string
+ */
+ public function getModuleName()
+ {
+ if ($this->moduleName === null) {
+ $this->moduleName = $this->getRequest()->getModuleName();
+ }
+
+ return $this->moduleName;
+ }
+
+ public function Config($file = null)
+ {
+ if ($file === null) {
+ return Config::app();
+ } else {
+ return Config::app($file);
+ }
+ }
+
+ public function Window()
+ {
+ if ($this->window === null) {
+ $this->window = Window::getInstance();
+ }
+
+ return $this->window;
+ }
+
+ protected function reloadCss()
+ {
+ $this->reloadCss = true;
+ return $this;
+ }
+
+ /**
+ * Respond with HTTP 405 if the current request's method is not one of the given methods
+ *
+ * @param string $httpMethod Unlimited number of allowed HTTP methods
+ *
+ * @throws HttpMethodNotAllowedException If the request method is not one of the given methods
+ */
+ public function assertHttpMethod($httpMethod)
+ {
+ $httpMethods = array_flip(array_map('strtoupper', func_get_args()));
+ if (! isset($httpMethods[$this->getRequest()->getMethod()])) {
+ $e = new HttpMethodNotAllowedException($this->translate('Method Not Allowed'));
+ $e->setAllowedMethods(implode(', ', array_keys($httpMethods)));
+ throw $e;
+ }
+ }
+
+ /**
+ * Return restriction information for an eventually authenticated user
+ *
+ * @param string $name Restriction name
+ *
+ * @return array
+ */
+ public function getRestrictions($name)
+ {
+ return $this->Auth()->getRestrictions($name);
+ }
+
+ /**
+ * Check whether the controller requires a login. That is when the controller requires authentication and the
+ * user is currently not authenticated
+ *
+ * @return bool
+ */
+ protected function requiresLogin()
+ {
+ if (! $this->requiresAuthentication) {
+ return false;
+ }
+
+ return ! $this->Auth()->isAuthenticated();
+ }
+
+ /**
+ * Return the tabs
+ *
+ * @return Tabs
+ */
+ public function getTabs()
+ {
+ return $this->view->tabs;
+ }
+
+ protected function ignoreXhrBody()
+ {
+ if ($this->isXhr()) {
+ $this->getResponse()->setHeader('X-Icinga-Container', 'ignore');
+ }
+ }
+
+ /**
+ * Set the interval (in seconds) at which the page should automatically refresh
+ *
+ * This may be adjusted based on the user's preferences. The result could be a
+ * lower or higher rate of the page's automatic refresh. If this is not desired,
+ * the only way to bypass this is to initialize the {@see ActionController::$autorefreshInterval}
+ * property or to set the `autorefreshInterval` property of the layout directly.
+ *
+ * @param int $interval
+ *
+ * @return $this
+ */
+ public function setAutorefreshInterval($interval)
+ {
+ if (! is_int($interval) || $interval < 1) {
+ throw new ProgrammingError(
+ 'Setting autorefresh interval smaller than 1 second is not allowed'
+ );
+ }
+
+ $user = $this->getRequest()->getUser();
+ if ($user !== null) {
+ $speed = (float) $user->getPreferences()->getValue('icingaweb', 'auto_refresh_speed', 1.0);
+ $interval = max(round($interval * $speed), min($interval, 5));
+ }
+
+ $this->autorefreshInterval = $interval;
+
+ return $this;
+ }
+
+ public function disableAutoRefresh()
+ {
+ $this->autorefreshInterval = null;
+
+ return $this;
+ }
+
+ /**
+ * Redirect to login
+ *
+ * XHR will always redirect to __SELF__ if an URL to redirect to after successful login is set. __SELF__ instructs
+ * JavaScript to redirect to the current window's URL if it's an auto-refresh request or to redirect to the URL
+ * which required login if it's not an auto-refreshing one.
+ *
+ * XHR will respond with HTTP status code 403 Forbidden.
+ *
+ * @param Url|string $redirect URL to redirect to after successful login
+ */
+ protected function redirectToLogin($redirect = null)
+ {
+ $login = Url::fromPath(static::LOGIN_ROUTE);
+ if ($this->isXhr()) {
+ if ($redirect !== null) {
+ $login->setParam('redirect', '__SELF__');
+ }
+
+ $this->_response->setHttpResponseCode(403);
+ } elseif ($redirect !== null) {
+ if (! $redirect instanceof Url) {
+ $redirect = Url::fromPath($redirect);
+ }
+
+ if (($relativeUrl = $redirect->getRelativeUrl())) {
+ $login->setParam('redirect', $relativeUrl);
+ }
+ }
+
+ $this->getResponse()->setReloadWindow(true);
+ $this->redirectNow($login);
+ }
+
+ protected function rerenderLayout()
+ {
+ $this->rerenderLayout = true;
+ return $this;
+ }
+
+ public function isXhr()
+ {
+ return $this->getRequest()->isXmlHttpRequest();
+ }
+
+ /**
+ * Issue a redirect that's performed with XHR by the client
+ *
+ * @param Url|string $url
+ *
+ * @return never
+ */
+ protected function redirectXhr($url)
+ {
+ $response = $this->getResponse();
+
+ if ($this->reloadCss) {
+ $response->setReloadCss(true);
+ }
+
+ if ($this->rerenderLayout) {
+ $response->setRerenderLayout(true);
+ }
+
+ $response->redirectAndExit($url);
+ }
+
+ /**
+ * Issue a redirect that's performed as a native HTTP request by the client
+ *
+ * This will effectively reload the window
+ *
+ * @param Url|string $url
+ *
+ * @return never
+ */
+ protected function redirectHttp($url)
+ {
+ if ($this->isXhr()) {
+ $this->getResponse()->setHeader('X-Icinga-Redirect-Http', 'yes');
+ }
+
+ $this->getResponse()->redirectAndExit($url);
+ }
+
+ /**
+ * Redirect to a specific url, updating the browsers URL field
+ *
+ * @param Url|string $url The target to redirect to
+ *
+ * @return never
+ **/
+ public function redirectNow($url)
+ {
+ if ($this->isXhr()) {
+ $this->redirectXhr($url);
+ } else {
+ $this->redirectHttp($url);
+ }
+ }
+
+ /**
+ * @see Zend_Controller_Action::preDispatch()
+ */
+ public function preDispatch()
+ {
+ $form = new AutoRefreshForm();
+ if (! $this->getRequest()->isApiRequest()) {
+ $form->handleRequest();
+ }
+ $this->_helper->layout()->autoRefreshForm = $form;
+ }
+
+ /**
+ * Detect whether the current request requires changes in the layout and apply them before rendering
+ *
+ * @see Zend_Controller_Action::postDispatch()
+ */
+ public function postDispatch()
+ {
+ Benchmark::measure('Action::postDispatch()');
+
+ $req = $this->getRequest();
+ $layout = $this->_helper->layout();
+ $layout->innerLayout = $this->innerLayout;
+ $layout->inlineLayout = $this->inlineLayout;
+
+ if ($user = $req->getUser()) {
+ if ((bool) $user->getPreferences()->getValue('icingaweb', 'show_benchmark', false)) {
+ if ($this->_helper->layout()->isEnabled()) {
+ $layout->benchmark = $this->renderBenchmark();
+ }
+ }
+
+ if (! (bool) $user->getPreferences()->getValue('icingaweb', 'auto_refresh', true)) {
+ $this->disableAutoRefresh();
+ }
+ }
+
+ if ($this->autorefreshInterval !== null) {
+ $layout->autorefreshInterval = $this->autorefreshInterval;
+ }
+
+ if ($req->getParam('error_handler') === null && $req->getParam('format') === 'pdf') {
+ $this->sendAsPdf();
+ $this->shutdownSession();
+ exit;
+ }
+
+ if ($this->isXhr()) {
+ $this->postDispatchXhr();
+ }
+
+ $this->shutdownSession();
+ }
+
+ protected function postDispatchXhr()
+ {
+ $resp = $this->getResponse();
+
+ if ($this->reloadCss) {
+ $resp->setReloadCss(true);
+ }
+
+ if ($this->view->title) {
+ if (preg_match('~[\r\n]~', $this->view->title)) {
+ // TODO: Innocent exception and error log for hack attempts
+ throw new IcingaException('No way, guy');
+ }
+ $resp->setHeader(
+ 'X-Icinga-Title',
+ rawurlencode($this->view->title . ' :: ' . $this->view->defaultTitle),
+ true
+ );
+ } else {
+ $resp->setHeader('X-Icinga-Title', rawurlencode($this->view->defaultTitle), true);
+ }
+
+ $layout = $this->_helper->layout();
+ if ($this->rerenderLayout) {
+ $layout->setLayout($this->innerLayout);
+ $resp->setRerenderLayout(true);
+ } else {
+ // The layout may be disabled and there's no indication that the layout is explicitly desired,
+ // that's why we're passing false as second parameter to setLayout
+ $layout->setLayout($this->inlineLayout, false);
+ }
+
+ if ($this->autorefreshInterval !== null) {
+ $resp->setAutoRefreshInterval($this->autorefreshInterval);
+ }
+ }
+
+ protected function sendAsPdf()
+ {
+ if (Module::exists('pdfexport')) {
+ $this->newSendAsPdf();
+ } else {
+ $pdf = new Pdf();
+ $pdf->renderControllerAction($this);
+ }
+ }
+
+ protected function shutdownSession()
+ {
+ $session = Session::getSession();
+ if ($session->hasChanged()) {
+ $session->write();
+ }
+ }
+
+ /**
+ * Render the benchmark
+ *
+ * @return string Benchmark HTML
+ */
+ protected function renderBenchmark()
+ {
+ $this->_helper->viewRenderer->postDispatch();
+ Benchmark::measure('Response ready');
+ return Benchmark::renderToHtml();
+ }
+
+ /**
+ * Try to call compatible methods from older zend versions
+ *
+ * Methods like getParam and redirect are _getParam/_redirect in older Zend versions (which reside for example
+ * in Debian Wheezy). Using those methods without the "_" causes the application to fail on those platforms, but
+ * using the version with "_" forces us to use deprecated code. So we try to catch this issue by looking for methods
+ * with the same name, but with a "_" prefix prepended.
+ *
+ * @param string $name The method name to check
+ * @param mixed $params The method parameters
+ * @return mixed Anything the method returns
+ */
+ public function __call($name, $params)
+ {
+ $deprecatedMethod = '_' . $name;
+
+ if (method_exists($this, $deprecatedMethod)) {
+ return call_user_func_array(array($this, $deprecatedMethod), $params);
+ }
+
+ parent::__call($name, $params);
+ }
+}
diff --git a/library/Icinga/Web/Controller/AuthBackendController.php b/library/Icinga/Web/Controller/AuthBackendController.php
new file mode 100644
index 0000000..12f8b72
--- /dev/null
+++ b/library/Icinga/Web/Controller/AuthBackendController.php
@@ -0,0 +1,151 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Controller;
+
+use ipl\Web\Compat\CompatController;
+use Zend_Controller_Action_Exception;
+use Icinga\Application\Config;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Authentication\User\UserBackendInterface;
+use Icinga\Authentication\UserGroup\UserGroupBackend;
+use Icinga\Authentication\UserGroup\UserGroupBackendInterface;
+
+/**
+ * Base class for authentication backend controllers
+ */
+class AuthBackendController extends CompatController
+{
+ public function init()
+ {
+ parent::init();
+
+ $this->tabs->disableLegacyExtensions();
+ }
+
+ /**
+ * Redirect to this controller's list action
+ */
+ public function indexAction()
+ {
+ $this->redirectNow($this->getRequest()->getControllerName() . '/list');
+ }
+
+ /**
+ * Return all user backends implementing the given interface
+ *
+ * @param string $interface The class path of the interface, or null if no interface check should be made
+ *
+ * @return array
+ */
+ protected function loadUserBackends($interface = null)
+ {
+ $backends = array();
+ foreach (Config::app('authentication') as $backendName => $backendConfig) {
+ $candidate = UserBackend::create($backendName, $backendConfig);
+ if (! $interface || $candidate instanceof $interface) {
+ $backends[] = $candidate;
+ }
+ }
+
+ return $backends;
+ }
+
+ /**
+ * Return the given user backend or the first match in order
+ *
+ * @param string $name The name of the backend, or null in case the first match should be returned
+ * @param string $interface The interface the backend should implement, no interface check if null
+ *
+ * @return UserBackendInterface
+ *
+ * @throws Zend_Controller_Action_Exception In case the given backend name is invalid
+ */
+ protected function getUserBackend($name = null, $interface = 'Icinga\Data\Selectable')
+ {
+ $backend = null;
+ if ($name !== null) {
+ $config = Config::app('authentication');
+ if (! $config->hasSection($name)) {
+ $this->httpNotFound(sprintf($this->translate('Authentication backend "%s" not found'), $name));
+ } else {
+ $backend = UserBackend::create($name, $config->getSection($name));
+ if ($interface && !$backend instanceof $interface) {
+ $interfaceParts = explode('\\', strtolower($interface));
+ throw new Zend_Controller_Action_Exception(
+ sprintf(
+ $this->translate('Authentication backend "%s" is not %s'),
+ $name,
+ array_pop($interfaceParts)
+ ),
+ 400
+ );
+ }
+ }
+ } else {
+ $backends = $this->loadUserBackends($interface);
+ $backend = array_shift($backends);
+ }
+
+ return $backend;
+ }
+
+ /**
+ * Return all user group backends implementing the given interface
+ *
+ * @param string $interface The class path of the interface, or null if no interface check should be made
+ *
+ * @return array
+ */
+ protected function loadUserGroupBackends($interface = null)
+ {
+ $backends = array();
+ foreach (Config::app('groups') as $backendName => $backendConfig) {
+ $candidate = UserGroupBackend::create($backendName, $backendConfig);
+ if (! $interface || $candidate instanceof $interface) {
+ $backends[] = $candidate;
+ }
+ }
+
+ return $backends;
+ }
+
+ /**
+ * Return the given user group backend or the first match in order
+ *
+ * @param string $name The name of the backend, or null in case the first match should be returned
+ * @param string $interface The interface the backend should implement, no interface check if null
+ *
+ * @return UserGroupBackendInterface
+ *
+ * @throws Zend_Controller_Action_Exception In case the given backend name is invalid
+ */
+ protected function getUserGroupBackend($name = null, $interface = 'Icinga\Data\Selectable')
+ {
+ $backend = null;
+ if ($name !== null) {
+ $config = Config::app('groups');
+ if (! $config->hasSection($name)) {
+ $this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $name));
+ } else {
+ $backend = UserGroupBackend::create($name, $config->getSection($name));
+ if ($interface && !$backend instanceof $interface) {
+ $interfaceParts = explode('\\', strtolower($interface));
+ throw new Zend_Controller_Action_Exception(
+ sprintf(
+ $this->translate('User group backend "%s" is not %s'),
+ $name,
+ array_pop($interfaceParts)
+ ),
+ 400
+ );
+ }
+ }
+ } else {
+ $backends = $this->loadUserGroupBackends($interface);
+ $backend = array_shift($backends);
+ }
+
+ return $backend;
+ }
+}
diff --git a/library/Icinga/Web/Controller/BasePreferenceController.php b/library/Icinga/Web/Controller/BasePreferenceController.php
new file mode 100644
index 0000000..8f2da8f
--- /dev/null
+++ b/library/Icinga/Web/Controller/BasePreferenceController.php
@@ -0,0 +1,39 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Controller;
+
+/**
+ * Base class for Preference Controllers
+ *
+ * Module preferences use this class to make sure they are automatically
+ * added to the general preferences dialog. If you create a subclass of
+ * BasePreferenceController and overwrite @see init(), make sure you call
+ * parent::init(), otherwise you won't have the $tabs property in your view.
+ *
+ */
+class BasePreferenceController extends ActionController
+{
+ /**
+ * Return an array of tabs provided by this preference controller.
+ *
+ * Those tabs will automatically be added to the application's preference dialog
+ *
+ * @return array
+ */
+ public static function createProvidedTabs()
+ {
+ return array();
+ }
+
+ /**
+ * Initialize the controller and collect all tabs for it from the application and its modules
+ *
+ * @see ActionController::init()
+ */
+ public function init()
+ {
+ parent::init();
+ $this->view->tabs = ControllerTabCollector::collectControllerTabs('PreferenceController');
+ }
+}
diff --git a/library/Icinga/Web/Controller/ControllerTabCollector.php b/library/Icinga/Web/Controller/ControllerTabCollector.php
new file mode 100644
index 0000000..b452a20
--- /dev/null
+++ b/library/Icinga/Web/Controller/ControllerTabCollector.php
@@ -0,0 +1,97 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Controller;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Application\Icinga;
+use Icinga\Web\Widget\Tabs;
+
+/**
+ * Static helper class that collects tabs provided by the 'createProvidedTabs' method of controllers
+ */
+class ControllerTabCollector
+{
+ /**
+ * Scan all controllers with given name in the application and (loaded) module folders and collects their provided
+ * tabs
+ *
+ * @param string $controllerName The name of the controllers to use for tab collection
+ *
+ * @return Tabs A {@link Tabs} instance containing the application tabs first followed by the
+ * tabs provided from the modules
+ */
+ public static function collectControllerTabs($controllerName)
+ {
+ $controller = '\Icinga\\' . Dispatcher::CONTROLLER_NAMESPACE . '\\' . $controllerName;
+ $applicationTabs = $controller::createProvidedTabs();
+ $moduleTabs = self::collectModuleTabs($controllerName);
+
+ $tabs = new Tabs();
+ foreach ($applicationTabs as $name => $tab) {
+ $tabs->add($name, $tab);
+ }
+
+ foreach ($moduleTabs as $name => $tab) {
+ // Don't overwrite application tabs if the module wants to
+ if ($tabs->has($name)) {
+ continue;
+ }
+ $tabs->add($name, $tab);
+ }
+ return $tabs;
+ }
+
+ /**
+ * Collect module tabs for all modules containing the given controller
+ *
+ * @param string $controller The controller name to use for tab collection
+ *
+ * @return array An array of Tabs objects or arrays containing Tab descriptions
+ */
+ private static function collectModuleTabs($controller)
+ {
+ $moduleManager = Icinga::app()->getModuleManager();
+ $modules = $moduleManager->listEnabledModules();
+ $tabs = array();
+ foreach ($modules as $module) {
+ $tabs += self::createModuleConfigurationTabs($controller, $moduleManager->getModule($module));
+ }
+
+ return $tabs;
+ }
+
+ /**
+ * Collects the tabs from the createProvidedTabs() method in the configuration controller
+ *
+ * If the module doesn't have the given controller or createProvidedTabs method in the controller an empty array
+ * will be returned
+ *
+ * @param string $controllerName The name of the controller that provides tabs via createProvidedTabs
+ * @param Module $module The module instance that provides the controller
+ *
+ * @return array
+ */
+ private static function createModuleConfigurationTabs($controllerName, Module $module)
+ {
+ // TODO(el): Only works for controllers w/o namepsace: https://dev.icinga.com/issues/4149
+ $controllerDir = $module->getControllerDir();
+ $name = $module->getName();
+
+ $controllerDir = $controllerDir . '/' . $controllerName . '.php';
+ $controllerName = ucfirst($name) . '_' . $controllerName;
+
+ if (is_readable($controllerDir)) {
+ require_once(realpath($controllerDir));
+ if (! method_exists($controllerName, 'createProvidedTabs')) {
+ return array();
+ }
+ $tab = $controllerName::createProvidedTabs();
+ if (! is_array($tab)) {
+ $tab = array($name => $tab);
+ }
+ return $tab;
+ }
+ return array();
+ }
+}
diff --git a/library/Icinga/Web/Controller/Dispatcher.php b/library/Icinga/Web/Controller/Dispatcher.php
new file mode 100644
index 0000000..e2dfb80
--- /dev/null
+++ b/library/Icinga/Web/Controller/Dispatcher.php
@@ -0,0 +1,93 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Controller;
+
+use Exception;
+use Icinga\Util\StringHelper;
+use Zend_Controller_Action;
+use Zend_Controller_Action_Interface;
+use Zend_Controller_Dispatcher_Exception;
+use Zend_Controller_Dispatcher_Standard;
+use Zend_Controller_Request_Abstract;
+use Zend_Controller_Response_Abstract;
+
+/**
+ * Dispatcher supporting Zend-style and namespaced controllers
+ *
+ * Does not support a namespaced default controller in combination w/ the Zend parameter useDefaultControllerAlways.
+ */
+class Dispatcher extends Zend_Controller_Dispatcher_Standard
+{
+ /**
+ * Controller namespace
+ *
+ * @var string
+ */
+ const CONTROLLER_NAMESPACE = 'Controllers';
+
+ /**
+ * Dispatch request to a controller and action
+ *
+ * @param Zend_Controller_Request_Abstract $request
+ * @param Zend_Controller_Response_Abstract $response
+ *
+ * @throws Zend_Controller_Dispatcher_Exception If the controller is not an instance of
+ * Zend_Controller_Action_Interface
+ * @throws Exception If dispatching the request fails
+ */
+ public function dispatch(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response)
+ {
+ $this->setResponse($response);
+ $controllerName = $request->getControllerName();
+ if (! $controllerName) {
+ parent::dispatch($request, $response);
+ return;
+ }
+ $controllerName = StringHelper::cname($controllerName, '-') . 'Controller';
+ $moduleName = $request->getModuleName();
+ if ($moduleName === null || $moduleName === $this->_defaultModule) {
+ $controllerClass = 'Icinga\\' . self::CONTROLLER_NAMESPACE . '\\' . $controllerName;
+ } else {
+ $controllerClass = 'Icinga\\Module\\' . ucfirst($moduleName) . '\\' . self::CONTROLLER_NAMESPACE . '\\'
+ . $controllerName;
+ }
+ if (! class_exists($controllerClass)) {
+ parent::dispatch($request, $response);
+ return;
+ }
+ $controller = new $controllerClass($request, $response, $this->getParams());
+ if (! $controller instanceof Zend_Controller_Action
+ && ! $controller instanceof Zend_Controller_Action_Interface
+ ) {
+ throw new Zend_Controller_Dispatcher_Exception(
+ 'Controller "' . $controllerClass . '" is not an instance of Zend_Controller_Action_Interface'
+ );
+ }
+ $action = $this->getActionMethod($request);
+ $request->setDispatched(true);
+ // Buffer output by default
+ $disableOb = $this->getParam('disableOutputBuffering');
+ $obLevel = ob_get_level();
+ if (empty($disableOb)) {
+ ob_start();
+ }
+ try {
+ $controller->dispatch($action);
+ } catch (Exception $e) {
+ // Clean output buffer on error
+ $curObLevel = ob_get_level();
+ if ($curObLevel > $obLevel) {
+ do {
+ ob_get_clean();
+ $curObLevel = ob_get_level();
+ } while ($curObLevel > $obLevel);
+ }
+ throw $e;
+ }
+ if (empty($disableOb)) {
+ $content = ob_get_clean();
+ $response->appendBody($content);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Controller/ModuleActionController.php b/library/Icinga/Web/Controller/ModuleActionController.php
new file mode 100644
index 0000000..ad66264
--- /dev/null
+++ b/library/Icinga/Web/Controller/ModuleActionController.php
@@ -0,0 +1,80 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Controller;
+
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Manager;
+use Icinga\Application\Modules\Module;
+
+/**
+ * Base class for module action controllers
+ */
+class ModuleActionController extends ActionController
+{
+ protected $config;
+
+ protected $configs = array();
+
+ protected $module;
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Controller\ActionController For the method documentation.
+ */
+ protected function prepareInit()
+ {
+ $this->moduleInit();
+ if (($this->Auth()->isAuthenticated() || $this->requiresLogin())
+ && $this->getFrontController()->getDefaultModule() !== $this->getModuleName()) {
+ $this->assertPermission(Manager::MODULE_PERMISSION_NS . $this->getModuleName());
+ }
+ }
+
+ /**
+ * Prepare module action controller initialization
+ */
+ protected function moduleInit()
+ {
+ }
+
+ public function Config($file = null)
+ {
+ if ($file === null) {
+ if ($this->config === null) {
+ $this->config = Config::module($this->getModuleName());
+ }
+ return $this->config;
+ } else {
+ if (! array_key_exists($file, $this->configs)) {
+ $this->configs[$file] = Config::module($this->getModuleName(), $file);
+ }
+ return $this->configs[$file];
+ }
+ }
+
+ /**
+ * Return this controller's module
+ *
+ * @return Module
+ */
+ public function Module()
+ {
+ if ($this->module === null) {
+ $this->module = Icinga::app()->getModuleManager()->getModule($this->getModuleName());
+ }
+
+ return $this->module;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Controller\ActionController::postDispatchXhr() For the method documentation.
+ */
+ public function postDispatchXhr()
+ {
+ parent::postDispatchXhr();
+ $this->getResponse()->setHeader('X-Icinga-Module', $this->getModuleName(), true);
+ }
+}
diff --git a/library/Icinga/Web/Controller/StaticController.php b/library/Icinga/Web/Controller/StaticController.php
new file mode 100644
index 0000000..f5ce163
--- /dev/null
+++ b/library/Icinga/Web/Controller/StaticController.php
@@ -0,0 +1,87 @@
+<?php
+/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\Controller;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Request;
+
+class StaticController
+{
+ /**
+ * Handle incoming request
+ *
+ * @param Request $request
+ *
+ * @returns void
+ */
+ public function handle(Request $request)
+ {
+ $app = Icinga::app();
+
+ // +4 because strlen('/lib') === 4
+ $assetPath = ltrim(substr($request->getRequestUri(), strlen($request->getBaseUrl()) + 4), '/');
+
+ $library = null;
+ foreach ($app->getLibraries() as $candidate) {
+ if (substr($assetPath, 0, strlen($candidate->getName())) === $candidate->getName()) {
+ $library = $candidate;
+ $assetPath = ltrim(substr($assetPath, strlen($candidate->getName())), '/');
+ break;
+ }
+ }
+
+ if ($library === null) {
+ $app->getResponse()
+ ->setHttpResponseCode(404);
+
+ return;
+ }
+
+ $assetRoot = $library->getStaticAssetPath();
+ if (empty($assetRoot)) {
+ $app->getResponse()
+ ->setHttpResponseCode(404);
+
+ return;
+ }
+
+ $filePath = $assetRoot . DIRECTORY_SEPARATOR . $assetPath;
+ $dirPath = realpath(dirname($filePath)); // dirname, because the file may be a link
+
+ if ($dirPath === false
+ || substr($dirPath, 0, strlen($assetRoot)) !== $assetRoot
+ || ! is_file($filePath)
+ ) {
+ $app->getResponse()
+ ->setHttpResponseCode(404);
+
+ return;
+ }
+
+ $fileStat = stat($filePath);
+ $eTag = sprintf(
+ '%x-%x-%x',
+ $fileStat['ino'],
+ $fileStat['size'],
+ (float) str_pad($fileStat['mtime'], 16, '0')
+ );
+
+ $app->getResponse()->setHeader(
+ 'Cache-Control',
+ 'public, max-age=1814400, stale-while-revalidate=604800',
+ true
+ );
+
+ if ($request->getServer('HTTP_IF_NONE_MATCH') === $eTag) {
+ $app->getResponse()
+ ->setHttpResponseCode(304);
+ } else {
+ $app->getResponse()
+ ->setHeader('ETag', $eTag)
+ ->setHeader('Content-Type', mime_content_type($filePath), true)
+ ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $fileStat['mtime']) . ' GMT')
+ ->setBody(file_get_contents($filePath));
+ }
+ }
+}
diff --git a/library/Icinga/Web/Cookie.php b/library/Icinga/Web/Cookie.php
new file mode 100644
index 0000000..283f07a
--- /dev/null
+++ b/library/Icinga/Web/Cookie.php
@@ -0,0 +1,299 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use InvalidArgumentException;
+
+/**
+ * A HTTP cookie
+ */
+class Cookie
+{
+ /**
+ * Domain of the cookie
+ *
+ * @var string
+ */
+ protected $domain;
+
+ /**
+ * The timestamp at which the cookie expires
+ *
+ * @var int
+ */
+ protected $expire;
+
+ /**
+ * Whether to protect the cookie against client side script code attempts to read the cookie
+ *
+ * Defaults to true.
+ *
+ * @var bool
+ */
+ protected $httpOnly = true;
+
+ /**
+ * Name of the cookie
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * The path on the web server where the cookie is available
+ *
+ * Defaults to the base URL.
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * Whether to send the cookie only over a secure connection
+ *
+ * Defaults to auto-detection so that if the current request was sent over a secure connection the secure flag will
+ * be set to true.
+ *
+ * @var bool
+ */
+ protected $secure;
+
+ /**
+ * Value of the cookie
+ *
+ * @var string
+ */
+ protected $value;
+
+ /**
+ * Create a new cookie
+ *
+ * @param string $name
+ * @param string $value
+ */
+ public function __construct($name, $value = null)
+ {
+ if (preg_match("/[=,; \t\r\n\013\014]/", $name)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Cookie name can\'t contain these characters: =,; \t\r\n\013\014 (%s)',
+ $name
+ ));
+ }
+ if (empty($name)) {
+ throw new InvalidArgumentException('The cookie name can\'t be empty');
+ }
+ $this->name = $name;
+ $this->value = $value;
+ }
+
+ /**
+ * Get the domain of the cookie
+ *
+ * @return string
+ */
+ public function getDomain()
+ {
+ if ($this->domain === null) {
+ $this->domain = Config::app()->get('cookie', 'domain');
+ }
+ return $this->domain;
+ }
+
+ /**
+ * Set the domain of the cookie
+ *
+ * @param string $domain
+ *
+ * @return $this
+ */
+ public function setDomain($domain)
+ {
+ $this->domain = $domain;
+ return $this;
+ }
+
+ /**
+ * Get the timestamp at which the cookie expires
+ *
+ * @return int
+ */
+ public function getExpire()
+ {
+ return $this->expire;
+ }
+
+ /**
+ * Set the timestamp at which the cookie expires
+ *
+ * @param int $expire
+ *
+ * @return $this
+ */
+ public function setExpire($expire)
+ {
+ $this->expire = $expire;
+ return $this;
+ }
+
+ /**
+ * Get whether to protect the cookie against client side script code attempts to read the cookie
+ *
+ * @return bool
+ */
+ public function isHttpOnly()
+ {
+ return $this->httpOnly;
+ }
+
+ /**
+ * Set whether to protect the cookie against client side script code attempts to read the cookie
+ *
+ * @param bool $httpOnly
+ *
+ * @return $this
+ */
+ public function setHttpOnly($httpOnly)
+ {
+ $this->httpOnly = $httpOnly;
+ return $this;
+ }
+
+ /**
+ * Get the name of the cookie
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Get the path on the web server where the cookie is available
+ *
+ * If the path has not been set either via {@link setPath()} or via config, the base URL will be returned.
+ *
+ * @return string
+ */
+ public function getPath()
+ {
+ if ($this->path === null) {
+ $path = Config::app()->get('cookie', 'path');
+ if ($path === null) {
+ // The following call could be used as default for ConfigObject::get(), but we prevent unnecessary
+ // function calls here, if the path is set in the config
+ $path = Icinga::app()->getRequest()->getBaseUrl() . '/'; // Zend has rtrim($baseUrl, '/')
+ }
+ $this->path = $path;
+ }
+ return $this->path;
+ }
+
+ /**
+ * Set the path on the web server where the cookie is available
+ *
+ * @param string $path
+ *
+ * @return $this
+ */
+ public function setPath($path)
+ {
+ $this->path = $path;
+ return $this;
+ }
+
+ /**
+ * Get whether to send the cookie only over a secure connection
+ *
+ * If the secure flag has not been set either via {@link setSecure()} or via config and if the current request was
+ * sent over a secure connection, true will be returned.
+ *
+ * @return bool
+ */
+ public function isSecure()
+ {
+ if ($this->secure === null) {
+ $secure = Config::app()->get('cookie', 'secure');
+ if ($secure === null) {
+ // The following call could be used as default for ConfigObject::get(), but we prevent unnecessary
+ // function calls here, if the secure flag is set in the config
+ $secure = Icinga::app()->getRequest()->isSecure();
+ }
+ $this->secure = $secure;
+ }
+ return $this->secure;
+ }
+
+ /**
+ * Set whether to send the cookie only over a secure connection
+ *
+ * @param bool $secure
+ *
+ * @return $this
+ */
+ public function setSecure($secure)
+ {
+ $this->secure = $secure;
+ return $this;
+ }
+
+ /**
+ * Get the value of the cookie
+ *
+ * @return string
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Set the value of the cookie
+ *
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function setValue($value)
+ {
+ $this->value = $value;
+ return $this;
+ }
+
+ /**
+ * Create invalidation cookie
+ *
+ * This method clones the current cookie and sets its value to null and expire time to 1.
+ * That way, the cookie removes itself when it has been sent to and processed by the client.
+ *
+ * We're cloning the current cookie in order to meet the [RFC6265 spec](https://tools.ietf.org/search/rfc6265)
+ * regarding the `Path` and `Domain` attribute:
+ *
+ * > Finally, to remove a cookie, the server returns a Set-Cookie header with an expiration date in the past.
+ * > The server will be successful in removing the cookie only if the Path and the Domain attribute in the
+ * > Set-Cookie header match the values used when the cookie was created.
+ *
+ * Note that the cookie has to be sent to the client.
+ *
+ * # Example Usage
+ *
+ * ```php
+ * $response->setCookie(
+ * $cookie->forgetMe()
+ * );
+ * ```
+ *
+ * @return static
+ */
+ public function forgetMe()
+ {
+ $forgetMe = clone $this;
+
+ return $forgetMe
+ ->setValue(null)
+ ->setExpire(1);
+ }
+}
diff --git a/library/Icinga/Web/CookieSet.php b/library/Icinga/Web/CookieSet.php
new file mode 100644
index 0000000..019be29
--- /dev/null
+++ b/library/Icinga/Web/CookieSet.php
@@ -0,0 +1,58 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use ArrayIterator;
+use IteratorAggregate;
+use Traversable;
+
+/**
+ * Maintain a set of cookies
+ */
+class CookieSet implements IteratorAggregate
+{
+ /**
+ * Cookies in this set indexed by the cookie names
+ *
+ * @var Cookie[]
+ */
+ protected $cookies = array();
+
+ /**
+ * Get an iterator for traversing the cookies in this set
+ *
+ * @return ArrayIterator An iterator for traversing the cookies in this set
+ */
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->cookies);
+ }
+
+ /**
+ * Add a cookie to the set
+ *
+ * If a cookie with the same name already exists, the cookie will be overridden.
+ *
+ * @param Cookie $cookie The cookie to add
+ *
+ * @return $this
+ */
+ public function add(Cookie $cookie)
+ {
+ $this->cookies[$cookie->getName()] = $cookie;
+ return $this;
+ }
+
+ /**
+ * Get the cookie with the given name from the set
+ *
+ * @param string $name The name of the cookie
+ *
+ * @return Cookie|null The cookie with the given name or null if the cookie does not exist
+ */
+ public function get($name)
+ {
+ return isset($this->cookies[$name]) ? $this->cookies[$name] : null;
+ }
+}
diff --git a/library/Icinga/Web/Dom/DomNodeIterator.php b/library/Icinga/Web/Dom/DomNodeIterator.php
new file mode 100644
index 0000000..1ea20b8
--- /dev/null
+++ b/library/Icinga/Web/Dom/DomNodeIterator.php
@@ -0,0 +1,84 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Dom;
+
+use DOMNode;
+use IteratorIterator;
+use RecursiveIterator;
+
+/**
+ * Recursive iterator over a DOMNode
+ *
+ * Usage example:
+ * <code>
+ * <?php
+ *
+ * namespace Icinga\Example;
+ *
+ * use DOMDocument;
+ * use RecursiveIteratorIterator;
+ * use Icinga\Web\Dom\DomIterator;
+ *
+ * $doc = new DOMDocument();
+ * $doc->loadHTML(...);
+ * $dom = new RecursiveIteratorIterator(new DomNodeIterator($doc), RecursiveIteratorIterator::SELF_FIRST);
+ * foreach ($dom as $node) {
+ * ....
+ * }
+ * </code>
+ */
+class DomNodeIterator implements RecursiveIterator
+{
+ /**
+ * The node's children
+ *
+ * @var IteratorIterator
+ */
+ protected $children;
+
+ /**
+ * Create a new iterator over a DOMNode's children
+ *
+ * @param DOMNode $node
+ */
+ public function __construct(DOMNode $node)
+ {
+ $this->children = new IteratorIterator($node->childNodes);
+ }
+
+ public function current(): ?DOMNode
+ {
+ return $this->children->current();
+ }
+
+ public function key(): int
+ {
+ return $this->children->key();
+ }
+
+ public function next(): void
+ {
+ $this->children->next();
+ }
+
+ public function rewind(): void
+ {
+ $this->children->rewind();
+ }
+
+ public function valid(): bool
+ {
+ return $this->children->valid();
+ }
+
+ public function hasChildren(): bool
+ {
+ return $this->current()->hasChildNodes();
+ }
+
+ public function getChildren(): DomNodeIterator
+ {
+ return new static($this->current());
+ }
+}
diff --git a/library/Icinga/Web/FileCache.php b/library/Icinga/Web/FileCache.php
new file mode 100644
index 0000000..03f0c19
--- /dev/null
+++ b/library/Icinga/Web/FileCache.php
@@ -0,0 +1,293 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+class FileCache
+{
+ /**
+ * FileCache singleton instances
+ *
+ * @var array
+ */
+ protected static $instances = array();
+
+ /**
+ * Cache instance base directory
+ *
+ * @var string
+ */
+ protected $basedir;
+
+ /**
+ * Instance name
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * Whether the cache is enabled
+ *
+ * @var bool
+ */
+ protected $enabled = false;
+
+ /**
+ * The protected constructor creates a new instance with the given name
+ *
+ * @param string $name Cache instance name
+ */
+ protected function __construct($name)
+ {
+ $this->name = $name;
+ $tmpDir = sys_get_temp_dir();
+ $runtimePath = $tmpDir . '/FileCache_' . $name;
+ if (is_dir($runtimePath)) {
+ // Don't combine the following if with the above because else the elseif path will be evaluated if the
+ // runtime path exists and is not writeable
+ if (is_writeable($runtimePath)) {
+ $this->basedir = $runtimePath;
+ $this->enabled = true;
+ }
+ } elseif (is_dir($tmpDir) && is_writeable($tmpDir) && @mkdir($runtimePath, octdec('1750'), true)) {
+ // Suppress mkdir errors because it may error w/ no such file directory if the systemd private tmp directory
+ // for the web server has been removed
+ $this->basedir = $runtimePath;
+ $this->enabled = true;
+ }
+ }
+
+ /**
+ * Store the given content to the desired file name
+ *
+ * @param string $file new (relative) filename
+ * @param string $content the content to be stored
+ *
+ * @return bool whether the file has been stored
+ */
+ public function store($file, $content)
+ {
+ if (! $this->enabled) {
+ return false;
+ }
+
+ return file_put_contents($this->filename($file), $content);
+ }
+
+ /**
+ * Find out whether a given file exists
+ *
+ * @param string $file the (relative) filename
+ * @param int $newerThan optional timestamp to compare against
+ *
+ * @return bool whether such file exists
+ */
+ public function has($file, $newerThan = null)
+ {
+ if (! $this->enabled) {
+ return false;
+ }
+
+ $filename = $this->filename($file);
+
+ if (! file_exists($filename) || ! is_readable($filename)) {
+ return false;
+ }
+
+ if ($newerThan === null) {
+ return true;
+ }
+
+ $info = stat($filename);
+
+ if ($info === false) {
+ return false;
+ }
+
+ return (int) $newerThan < $info['mtime'];
+ }
+
+ /**
+ * Get a specific file or false if no such file available
+ *
+ * @param string $file the disired file name
+ *
+ * @return string|bool Filename content or false
+ */
+ public function get($file)
+ {
+ if ($this->has($file)) {
+ return file_get_contents($this->filename($file));
+ }
+
+ return false;
+ }
+
+ /**
+ * Send a specific file to the browser (output)
+ *
+ * @param string $file the disired file name
+ *
+ * @return bool Whether the file has been sent
+ */
+ public function send($file)
+ {
+ if ($this->has($file)) {
+ readfile($this->filename($file));
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get absolute filename for a given file
+ *
+ * @param string $file the disired file name
+ *
+ * @return string absolute filename
+ */
+ protected function filename($file)
+ {
+ return $this->basedir . '/' . $file;
+ }
+
+ /**
+ * Prepare a sub directory with the given name and return its path
+ *
+ * @param string $name
+ *
+ * @return string|false Returns FALSE in case the cache is not enabled or an error occurred
+ */
+ public function directory($name)
+ {
+ if (! $this->enabled) {
+ return false;
+ }
+
+ $path = $this->filename($name);
+ if (! is_dir($path) && ! @mkdir($path, octdec('1750'), true)) {
+ return false;
+ }
+
+ return $path;
+ }
+
+ /**
+ * Whether the given ETag matches a cached file
+ *
+ * If no ETag is given we'll try to fetch the one from the current
+ * HTTP request.
+ *
+ * @param string $file The cached file you want to check
+ * @param string $match The ETag to match against
+ *
+ * @return string|bool ETag on match, otherwise false
+ */
+ public function etagMatchesCachedFile($file, $match = null)
+ {
+ return self::etagMatchesFiles($this->filename($file), $match);
+ }
+
+ /**
+ * Create an ETag for the given file
+ *
+ * @param string $file The desired cache file
+ *
+ * @return string your ETag
+ */
+ public function etagForCachedFile($file)
+ {
+ return self::etagForFiles($this->filename($file));
+ }
+
+ /**
+ * Whether the given ETag matchesspecific file(s) on disk
+ *
+ * @param string|array $files file(s) to check
+ * @param string $match ETag to match against
+ *
+ * @return string|bool ETag on match, otherwise false
+ */
+ public static function etagMatchesFiles($files, $match = null)
+ {
+ if ($match === null) {
+ $match = isset($_SERVER['HTTP_IF_NONE_MATCH'])
+ ? trim($_SERVER['HTTP_IF_NONE_MATCH'], '"')
+ : false;
+ }
+ if (! $match) {
+ return false;
+ }
+
+ if (preg_match('/([0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{8})-\w+/i', $match, $matches)) {
+ // Removes compression suffixes as our custom algorithm can't handle compressed cache files anyway
+ $match = $matches[1];
+ }
+
+ $etag = self::etagForFiles($files);
+ return $match === $etag ? $etag : false;
+ }
+
+ /**
+ * Create ETag for the given files
+ *
+ * Custom algorithm creating an ETag based on filenames, mtimes
+ * and file sizes. Supports single files or a list of files. This
+ * way we are able to create ETags for virtual files depending on
+ * multiple source files (e.g. compressed JS, CSS).
+ *
+ * @param string|array $files Single file or a list of such
+ *
+ * @return string The generated ETag
+ */
+ public static function etagForFiles($files)
+ {
+ if (is_string($files)) {
+ $files = array($files);
+ }
+
+ $sizes = array();
+ $mtimes = array();
+
+ foreach ($files as $file) {
+ $file = realpath($file);
+ if ($file !== false && $info = stat($file)) {
+ $mtimes[] = $info['mtime'];
+ $sizes[] = $info['size'];
+ } else {
+ $mtimes[] = time();
+ $sizes[] = 0;
+ }
+ }
+
+ return sprintf(
+ '%s-%s-%s',
+ hash('crc32', implode('|', $files)),
+ hash('crc32', implode('|', $sizes)),
+ hash('crc32', implode('|', $mtimes))
+ );
+ }
+
+ /**
+ * Factory creating your cache instance
+ *
+ * @param string $name Instance name
+ *
+ * @return FileCache
+ */
+ public static function instance($name = 'icingaweb')
+ {
+ if ($name !== 'icingaweb') {
+ $name = 'icingaweb/modules/' . $name;
+ }
+
+ if (!array_key_exists($name, self::$instances)) {
+ self::$instances[$name] = new static($name);
+ }
+
+ return self::$instances[$name];
+ }
+}
diff --git a/library/Icinga/Web/Form.php b/library/Icinga/Web/Form.php
new file mode 100644
index 0000000..b421849
--- /dev/null
+++ b/library/Icinga/Web/Form.php
@@ -0,0 +1,1666 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Web\Form\Element\DateTimePicker;
+use ipl\I18n\Translation;
+use Zend_Config;
+use Zend_Form;
+use Zend_Form_Element;
+use Zend_View_Interface;
+use Icinga\Application\Icinga;
+use Icinga\Authentication\Auth;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Security\SecurityException;
+use Icinga\Web\Form\ErrorLabeller;
+use Icinga\Web\Form\Decorator\Autosubmit;
+use Icinga\Web\Form\Element\CsrfCounterMeasure;
+
+/**
+ * Base class for forms providing CSRF protection, confirmation logic and auto submission
+ *
+ * @method \Zend_Form_Element[] getElements() {
+ * {@inheritdoc}
+ * @return \Zend_Form_Element[]
+ * }
+ */
+class Form extends Zend_Form
+{
+ use Translation {
+ translate as i18nTranslate;
+ translatePlural as i18nTranslatePlural;
+ }
+
+ /**
+ * The suffix to append to a field's hidden default field name
+ */
+ const DEFAULT_SUFFIX = '_default';
+
+ /**
+ * A form's default CSS classes
+ */
+ const DEFAULT_CLASSES = 'icinga-form icinga-controls';
+
+ /**
+ * Identifier for notifications of type error
+ */
+ const NOTIFICATION_ERROR = 0;
+
+ /**
+ * Identifier for notifications of type warning
+ */
+ const NOTIFICATION_WARNING = 1;
+
+ /**
+ * Identifier for notifications of type info
+ */
+ const NOTIFICATION_INFO = 2;
+
+ /**
+ * Whether this form has been created
+ *
+ * @var bool
+ */
+ protected $created = false;
+
+ /**
+ * This form's parent
+ *
+ * Gets automatically set upon calling addSubForm().
+ *
+ * @var Form
+ */
+ protected $_parent;
+
+ /**
+ * Whether the form is an API target
+ *
+ * When the form is an API target, the form evaluates as submitted if the request method equals the form method.
+ * That means, that the submit button and form identification are not taken into account. In addition, the CSRF
+ * counter measure will not be added to the form's elements.
+ *
+ * @var bool
+ */
+ protected $isApiTarget = false;
+
+ /**
+ * The request associated with this form
+ *
+ * @var Request
+ */
+ protected $request;
+
+ /**
+ * The callback to call instead of Form::onSuccess()
+ *
+ * @var callable
+ */
+ protected $onSuccess;
+
+ /**
+ * Label to use for the standard submit button
+ *
+ * @var string
+ */
+ protected $submitLabel;
+
+ /**
+ * Label to use for showing the user an activity indicator when submitting the form
+ *
+ * @var string
+ */
+ protected $progressLabel;
+
+ /**
+ * The url to redirect to upon success
+ *
+ * @var Url
+ */
+ protected $redirectUrl;
+
+ /**
+ * The view script to use when rendering this form
+ *
+ * @var string
+ */
+ protected $viewScript;
+
+ /**
+ * Whether this form should NOT add random generated "challenge" tokens that are associated with the user's current
+ * session in order to prevent Cross-Site Request Forgery (CSRF). It is the form's responsibility to verify the
+ * existence and correctness of this token
+ *
+ * @var bool
+ */
+ protected $tokenDisabled = false;
+
+ /**
+ * Name of the CSRF token element
+ *
+ * @var string
+ */
+ protected $tokenElementName = 'CSRFToken';
+
+ /**
+ * Whether this form should add a UID element being used to distinct different forms posting to the same action
+ *
+ * @var bool
+ */
+ protected $uidDisabled = false;
+
+ /**
+ * Name of the form identification element
+ *
+ * @var string
+ */
+ protected $uidElementName = 'formUID';
+
+ /**
+ * Whether the form should validate the sent data when being automatically submitted
+ *
+ * @var bool
+ */
+ protected $validatePartial = false;
+
+ /**
+ * Whether element ids will be protected against collisions by appending a request-specific unique identifier
+ *
+ * @var bool
+ */
+ protected $protectIds = true;
+
+ /**
+ * The cue that is appended to each element's label if it's required
+ *
+ * @var string
+ */
+ protected $requiredCue = '*';
+
+ /**
+ * The descriptions of this form
+ *
+ * @var array
+ */
+ protected $descriptions;
+
+ /**
+ * The notifications of this form
+ *
+ * @var array
+ */
+ protected $notifications;
+
+ /**
+ * The hints of this form
+ *
+ * @var array
+ */
+ protected $hints;
+
+ /**
+ * Whether the Autosubmit decorator should be applied to this form
+ *
+ * If this is true, the Autosubmit decorator is being applied to this form instead of to each of its elements.
+ *
+ * @var bool
+ */
+ protected $useFormAutosubmit = false;
+
+ /**
+ * Authentication manager
+ *
+ * @var Auth|null
+ */
+ private $auth;
+
+ /**
+ * Default element decorators
+ *
+ * @var array
+ */
+ public static $defaultElementDecorators = array(
+ array('Label', array('tag'=>'span', 'separator' => '', 'class' => 'control-label')),
+ array(array('labelWrap' => 'HtmlTag'), array('tag' => 'div', 'class' => 'control-label-group')),
+ array('ViewHelper', array('separator' => '')),
+ array('Help', array()),
+ array('Errors', array('separator' => '')),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group'))
+ );
+
+ /**
+ * (non-PHPDoc)
+ * @see \Zend_Form::construct() For the method documentation.
+ */
+ public function __construct($options = null)
+ {
+ // Zend's plugin loader reverses the order of added prefix paths thus trying our paths first before trying
+ // Zend paths
+ $this->addPrefixPaths(array(
+ array(
+ 'prefix' => 'Icinga\\Web\\Form\\Element\\',
+ 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Element'),
+ 'type' => static::ELEMENT
+ ),
+ array(
+ 'prefix' => 'Icinga\\Web\\Form\\Decorator\\',
+ 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Decorator'),
+ 'type' => static::DECORATOR
+ )
+ ));
+
+ if (! isset($options['attribs']['class'])) {
+ $options['attribs']['class'] = static::DEFAULT_CLASSES;
+ }
+
+ parent::__construct($options);
+ }
+
+ /**
+ * Set this form's parent
+ *
+ * @param Form $form
+ *
+ * @return $this
+ */
+ public function setParent(Form $form)
+ {
+ $this->_parent = $form;
+ return $this;
+ }
+
+ /**
+ * Return this form's parent
+ *
+ * @return Form
+ */
+ public function getParent()
+ {
+ return $this->_parent;
+ }
+
+ /**
+ * Set a callback that is called instead of this form's onSuccess method
+ *
+ * It is called using the following signature: (Form $this).
+ *
+ * @param callable $onSuccess Callback
+ *
+ * @return $this
+ *
+ * @throws ProgrammingError If the callback is not callable
+ */
+ public function setOnSuccess($onSuccess)
+ {
+ if (! is_callable($onSuccess)) {
+ throw new ProgrammingError('The option `onSuccess\' is not callable');
+ }
+ $this->onSuccess = $onSuccess;
+ return $this;
+ }
+
+ /**
+ * Set the label to use for the standard submit button
+ *
+ * @param string $label The label to use for the submit button
+ *
+ * @return $this
+ */
+ public function setSubmitLabel($label)
+ {
+ $this->submitLabel = $label;
+ return $this;
+ }
+
+ /**
+ * Return the label being used for the standard submit button
+ *
+ * @return string
+ */
+ public function getSubmitLabel()
+ {
+ return $this->submitLabel;
+ }
+
+ /**
+ * Set the label to use for showing the user an activity indicator when submitting the form
+ *
+ * @param string $label
+ *
+ * @return $this
+ */
+ public function setProgressLabel($label)
+ {
+ $this->progressLabel = $label;
+ return $this;
+ }
+
+ /**
+ * Return the label to use for showing the user an activity indicator when submitting the form
+ *
+ * @return string
+ */
+ public function getProgressLabel()
+ {
+ return $this->progressLabel;
+ }
+
+ /**
+ * Set the url to redirect to upon success
+ *
+ * @param string|Url $url The url to redirect to
+ *
+ * @return $this
+ *
+ * @throws ProgrammingError In case $url is neither a string nor a instance of Icinga\Web\Url
+ */
+ public function setRedirectUrl($url)
+ {
+ if (is_string($url)) {
+ $url = Url::fromPath($url, array(), $this->getRequest());
+ } elseif (! $url instanceof Url) {
+ throw new ProgrammingError('$url must be a string or instance of Icinga\Web\Url');
+ }
+
+ $this->redirectUrl = $url;
+ return $this;
+ }
+
+ /**
+ * Return the url to redirect to upon success
+ *
+ * @return Url
+ */
+ public function getRedirectUrl()
+ {
+ if ($this->redirectUrl === null) {
+ $this->redirectUrl = $this->getRequest()->getUrl();
+ if ($this->getMethod() === 'get') {
+ // Be sure to remove all form dependent params because we do not want to submit it again
+ $this->redirectUrl = $this->redirectUrl->without(array_keys($this->getElements()));
+ }
+ }
+
+ return $this->redirectUrl;
+ }
+
+ /**
+ * Set the view script to use when rendering this form
+ *
+ * @param string $viewScript The view script to use
+ *
+ * @return $this
+ */
+ public function setViewScript($viewScript)
+ {
+ $this->viewScript = $viewScript;
+ return $this;
+ }
+
+ /**
+ * Return the view script being used when rendering this form
+ *
+ * @return string
+ */
+ public function getViewScript()
+ {
+ return $this->viewScript;
+ }
+
+ /**
+ * Disable CSRF counter measure and remove its field if already added
+ *
+ * @param bool $disabled Set true in order to disable CSRF protection for this form, otherwise false
+ *
+ * @return $this
+ */
+ public function setTokenDisabled($disabled = true)
+ {
+ $this->tokenDisabled = (bool) $disabled;
+
+ if ($disabled && $this->getElement($this->tokenElementName) !== null) {
+ $this->removeElement($this->tokenElementName);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return whether CSRF counter measures are disabled for this form
+ *
+ * @return bool
+ */
+ public function getTokenDisabled()
+ {
+ return $this->tokenDisabled;
+ }
+
+ /**
+ * Set the name to use for the CSRF element
+ *
+ * @param string $name The name to set
+ *
+ * @return $this
+ */
+ public function setTokenElementName($name)
+ {
+ $this->tokenElementName = $name;
+ return $this;
+ }
+
+ /**
+ * Return the name of the CSRF element
+ *
+ * @return string
+ */
+ public function getTokenElementName()
+ {
+ return $this->tokenElementName;
+ }
+
+ /**
+ * Disable form identification and remove its field if already added
+ *
+ * @param bool $disabled Set true in order to disable identification for this form, otherwise false
+ *
+ * @return $this
+ */
+ public function setUidDisabled($disabled = true)
+ {
+ $this->uidDisabled = (bool) $disabled;
+
+ if ($disabled && $this->getElement($this->uidElementName) !== null) {
+ $this->removeElement($this->uidElementName);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return whether identification is disabled for this form
+ *
+ * @return bool
+ */
+ public function getUidDisabled()
+ {
+ return $this->uidDisabled;
+ }
+
+ /**
+ * Set the name to use for the form identification element
+ *
+ * @param string $name The name to set
+ *
+ * @return $this
+ */
+ public function setUidElementName($name)
+ {
+ $this->uidElementName = $name;
+ return $this;
+ }
+
+ /**
+ * Return the name of the form identification element
+ *
+ * @return string
+ */
+ public function getUidElementName()
+ {
+ return $this->uidElementName;
+ }
+
+ /**
+ * Set whether this form should validate the sent data when being automatically submitted
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
+ public function setValidatePartial($state)
+ {
+ $this->validatePartial = $state;
+ return $this;
+ }
+
+ /**
+ * Return whether this form should validate the sent data when being automatically submitted
+ *
+ * @return bool
+ */
+ public function getValidatePartial()
+ {
+ return $this->validatePartial;
+ }
+
+ /**
+ * Set whether each element's id should be altered to avoid duplicates
+ *
+ * @param bool $value
+ *
+ * @return Form
+ */
+ public function setProtectIds($value = true)
+ {
+ $this->protectIds = (bool) $value;
+ return $this;
+ }
+
+ /**
+ * Return whether each element's id is being altered to avoid duplicates
+ *
+ * @return bool
+ */
+ public function getProtectIds()
+ {
+ return $this->protectIds;
+ }
+
+ /**
+ * Set the cue to append to each element's label if it's required
+ *
+ * @param string $cue
+ *
+ * @return Form
+ */
+ public function setRequiredCue($cue)
+ {
+ $this->requiredCue = $cue;
+ return $this;
+ }
+
+ /**
+ * Return the cue being appended to each element's label if it's required
+ *
+ * @return string
+ */
+ public function getRequiredCue()
+ {
+ return $this->requiredCue;
+ }
+
+ /**
+ * Set the descriptions for this form
+ *
+ * @param array $descriptions
+ *
+ * @return Form
+ */
+ public function setDescriptions(array $descriptions)
+ {
+ $this->descriptions = $descriptions;
+ return $this;
+ }
+
+ /**
+ * Add a description for this form
+ *
+ * If $description is an array the second value should be
+ * an array as well containing additional HTML properties.
+ *
+ * @param string|array $description
+ *
+ * @return Form
+ */
+ public function addDescription($description)
+ {
+ $this->descriptions[] = $description;
+ return $this;
+ }
+
+ /**
+ * Return the descriptions of this form
+ *
+ * @return array
+ */
+ public function getDescriptions()
+ {
+ if ($this->descriptions === null) {
+ return array();
+ }
+
+ return $this->descriptions;
+ }
+
+ /**
+ * Set the notifications for this form
+ *
+ * @param array $notifications
+ *
+ * @return $this
+ */
+ public function setNotifications(array $notifications)
+ {
+ $this->notifications = $notifications;
+ return $this;
+ }
+
+ /**
+ * Add a notification for this form
+ *
+ * If $notification is an array the second value should be
+ * an array as well containing additional HTML properties.
+ *
+ * @param string|array $notification
+ * @param int $type
+ *
+ * @return $this
+ */
+ public function addNotification($notification, $type)
+ {
+ $this->notifications[$type][] = $notification;
+ return $this;
+ }
+
+ /**
+ * Return the notifications of this form
+ *
+ * @return array
+ */
+ public function getNotifications()
+ {
+ if ($this->notifications === null) {
+ return array();
+ }
+
+ return $this->notifications;
+ }
+
+ /**
+ * Set the hints for this form
+ *
+ * @param array $hints
+ *
+ * @return $this
+ */
+ public function setHints(array $hints)
+ {
+ $this->hints = $hints;
+ return $this;
+ }
+
+ /**
+ * Add a hint for this form
+ *
+ * If $hint is an array the second value should be an
+ * array as well containing additional HTML properties.
+ *
+ * @param string|array $hint
+ *
+ * @return $this
+ */
+ public function addHint($hint)
+ {
+ $this->hints[] = $hint;
+ return $this;
+ }
+
+ /**
+ * Return the hints of this form
+ *
+ * @return array
+ */
+ public function getHints()
+ {
+ if ($this->hints === null) {
+ return array();
+ }
+
+ return $this->hints;
+ }
+
+ /**
+ * Set whether the Autosubmit decorator should be applied to this form
+ *
+ * If true, the Autosubmit decorator is being applied to this form instead of to each of its elements.
+ *
+ * @param bool $state
+ *
+ * @return Form
+ */
+ public function setUseFormAutosubmit($state = true)
+ {
+ $this->useFormAutosubmit = (bool) $state;
+ if ($this->useFormAutosubmit) {
+ $this->setAttrib('data-progress-element', 'header-' . $this->getId());
+ } else {
+ $this->removeAttrib('data-progress-element');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return whether the Autosubmit decorator is being applied to this form
+ *
+ * @return bool
+ */
+ public function getUseFormAutosubmit()
+ {
+ return $this->useFormAutosubmit;
+ }
+
+ /**
+ * Get whether the form is an API target
+ *
+ * @todo This should probably only return true if the request is also an api request
+ * @return bool
+ */
+ public function getIsApiTarget()
+ {
+ return $this->isApiTarget;
+ }
+
+ /**
+ * Set whether the form is an API target
+ *
+ * @param bool $isApiTarget
+ *
+ * @return $this
+ */
+ public function setIsApiTarget($isApiTarget = true)
+ {
+ $this->isApiTarget = (bool) $isApiTarget;
+ return $this;
+ }
+
+ /**
+ * Create this form
+ *
+ * @param array $formData The data sent by the user
+ *
+ * @return $this
+ */
+ public function create(array $formData = array())
+ {
+ if (! $this->created) {
+ $this->createElements($formData);
+ $this->addFormIdentification()
+ ->addCsrfCounterMeasure()
+ ->addSubmitButton();
+
+ // Use Form::getAttrib() instead of Form::getAction() here because we want to explicitly check against
+ // null. Form::getAction() would return the empty string '' if the action is not set.
+ // For not setting the action attribute use Form::setAction(''). This is required for for the
+ // accessibility's enable/disable auto-refresh mechanic
+ if ($this->getAttrib('action') === null) {
+ $action = $this->getRequest()->getUrl();
+ if ($this->getMethod() === 'get') {
+ $action = $action->without(array_keys($this->getElements()));
+ }
+
+ // TODO(el): Re-evalute this necessity.
+ // JavaScript could use the container'sURL if there's no action set.
+ // We MUST set an action as JS gets confused otherwise, if
+ // this form is being displayed in an additional column
+ $this->setAction($action);
+ }
+
+ $this->created = true;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * Intended to be implemented by concrete form classes.
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ }
+
+ /**
+ * Perform actions after this form was submitted using a valid request
+ *
+ * Intended to be implemented by concrete form classes. The base implementation returns always FALSE.
+ *
+ * @return null|bool Return FALSE in case no redirect should take place
+ */
+ public function onSuccess()
+ {
+ return false;
+ }
+
+ /**
+ * Perform actions when no form dependent data was sent
+ *
+ * Intended to be implemented by concrete form classes.
+ */
+ public function onRequest()
+ {
+ }
+
+ /**
+ * Add a submit button to this form
+ *
+ * Uses the label previously set with Form::setSubmitLabel(). Overwrite this
+ * method in order to add multiple submit buttons or one with a custom name.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ $submitLabel = $this->getSubmitLabel();
+ if ($submitLabel) {
+ $this->addElement(
+ 'submit',
+ 'btn_submit',
+ array(
+ 'class' => 'btn-primary',
+ 'ignore' => true,
+ 'label' => $submitLabel,
+ 'data-progress-label' => $this->getProgressLabel(),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('Spinner', array('separator' => '')),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add a subform
+ *
+ * @param Zend_Form $form The subform to add
+ * @param string $name The name of the subform or null to use the name of $form
+ * @param int $order The location where to insert the form
+ *
+ * @return Zend_Form
+ */
+ public function addSubForm(Zend_Form $form, $name = null, $order = null)
+ {
+ if ($form instanceof self) {
+ $form->setDecorators(array('FormElements')); // TODO: Makes it difficult to customise subform decorators..
+ $form->setSubmitLabel('');
+ $form->setTokenDisabled();
+ $form->setUidDisabled();
+ $form->setParent($this);
+ }
+
+ if ($name === null) {
+ $name = $form->getName();
+ }
+
+ return parent::addSubForm($form, $name, $order);
+ }
+
+ /**
+ * Create a new element
+ *
+ * Icinga Web 2 loads its own default element decorators. For loading Zend's default element decorators set the
+ * `disableLoadDefaultDecorators' option to any other value than `true'. For loading custom element decorators use
+ * the 'decorators' option.
+ *
+ * @param string $type The type of the element
+ * @param string $name The name of the element
+ * @param mixed $options The options for the element
+ *
+ * @return Zend_Form_Element
+ *
+ * @see Form::$defaultElementDecorators For Icinga Web 2's default element decorators.
+ */
+ public function createElement($type, $name, $options = null)
+ {
+ if ($options !== null) {
+ if ($options instanceof Zend_Config) {
+ $options = $options->toArray();
+ }
+ if (! isset($options['decorators'])
+ && ! array_key_exists('disabledLoadDefaultDecorators', $options)
+ ) {
+ $options['decorators'] = static::$defaultElementDecorators;
+ if (! isset($options['data-progress-label']) && ($type === 'submit'
+ || ($type === 'button' && isset($options['type']) && $options['type'] === 'submit'))
+ ) {
+ array_splice($options['decorators'], 1, 0, array(array('Spinner', array('separator' => ''))));
+ } elseif ($type === 'hidden') {
+ $options['decorators'] = array('ViewHelper');
+ }
+ }
+ } else {
+ $options = array('decorators' => static::$defaultElementDecorators);
+ if ($type === 'submit') {
+ array_splice($options['decorators'], 1, 0, array(array('Spinner', array('separator' => ''))));
+ } elseif ($type === 'hidden') {
+ $options['decorators'] = array('ViewHelper');
+ }
+ }
+
+ $el = parent::createElement($type, $name, $options);
+ $el->setTranslator(new ErrorLabeller(array('element' => $el)));
+
+ $el->addPrefixPaths(array(
+ array(
+ 'prefix' => 'Icinga\\Web\\Form\\Validator\\',
+ 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Validator'),
+ 'type' => $el::VALIDATE
+ )
+ ));
+
+ if ($this->protectIds) {
+ $el->setAttrib('id', $this->getRequest()->protectId($this->getId(false) . '_' . $el->getId()));
+ }
+
+ if ($el->getAttrib('autosubmit')) {
+ if ($this->getUseFormAutosubmit()) {
+ $warningId = 'autosubmit_warning_' . $el->getId();
+ $warningText = $this->getView()->escape($this->translate(
+ 'This page will be automatically updated upon change of the value'
+ ));
+ $autosubmitDecorator = $this->_getDecorator('Callback', array(
+ 'placement' => 'PREPEND',
+ 'callback' => function ($content) use ($warningId, $warningText) {
+ return '<span class="sr-only" id="' . $warningId . '">' . $warningText . '</span>';
+ }
+ ));
+ } else {
+ $autosubmitDecorator = new Autosubmit();
+ $autosubmitDecorator->setAccessible();
+ $warningId = $autosubmitDecorator->getWarningId($el);
+ }
+
+ $decorators = $el->getDecorators();
+ $pos = array_search('Zend_Form_Decorator_ViewHelper', array_keys($decorators), true) + 1;
+ $el->setDecorators(
+ array_slice($decorators, 0, $pos, true)
+ + array('autosubmit' => $autosubmitDecorator)
+ + array_slice($decorators, $pos, count($decorators) - $pos, true)
+ );
+
+ if (($describedBy = $el->getAttrib('aria-describedby')) !== null) {
+ $el->setAttrib('aria-describedby', $describedBy . ' ' . $warningId);
+ } else {
+ $el->setAttrib('aria-describedby', $warningId);
+ }
+
+ $class = $el->getAttrib('class');
+ if (is_array($class)) {
+ $class[] = 'autosubmit';
+ } elseif ($class === null) {
+ $class = 'autosubmit';
+ } else {
+ $class .= ' autosubmit';
+ }
+ $el->setAttrib('class', $class);
+
+ unset($el->autosubmit);
+ }
+
+ if ($el->getAttrib('preserveDefault')) {
+ $el->addDecorator(
+ array('preserveDefault' => 'HtmlTag'),
+ array(
+ 'tag' => 'input',
+ 'type' => 'hidden',
+ 'name' => $name . static::DEFAULT_SUFFIX,
+ 'value' => $el instanceof DateTimePicker
+ ? $el->getValue()->format($el->getFormat())
+ : $el->getValue()
+ )
+ );
+
+ unset($el->preserveDefault);
+ }
+
+ return $this->ensureElementAccessibility($el);
+ }
+
+ /**
+ * Add accessibility related attributes
+ *
+ * @param Zend_Form_Element $element
+ *
+ * @return Zend_Form_Element
+ */
+ public function ensureElementAccessibility(Zend_Form_Element $element)
+ {
+ if ($element->isRequired()) {
+ $element->setAttrib('aria-required', 'true'); // ARIA
+ $element->setAttrib('required', ''); // HTML5
+ if (($cue = $this->getRequiredCue()) !== null && ($label = $element->getDecorator('label')) !== false) {
+ $element->setLabel($this->getView()->escape($element->getLabel()));
+ $label->setOption('escape', false);
+ $label->setRequiredSuffix(sprintf(' <span aria-hidden="true">%s</span>', $cue));
+ }
+ }
+
+ if ($element->getDescription() !== null && ($help = $element->getDecorator('help')) !== false) {
+ if (($describedBy = $element->getAttrib('aria-describedby')) !== null) {
+ // Assume that it's because of the element being of type autosubmit or
+ // that one who did set the property manually removes the help decorator
+ // in case it has already an aria-describedby property set
+ $element->setAttrib(
+ 'aria-describedby',
+ $help->setAccessible()->getDescriptionId($element) . ' ' . $describedBy
+ );
+ } else {
+ $element->setAttrib('aria-describedby', $help->setAccessible()->getDescriptionId($element));
+ }
+ }
+
+ return $element;
+ }
+
+ /**
+ * Add a field with a unique and form specific ID
+ *
+ * @return $this
+ */
+ public function addFormIdentification()
+ {
+ if (! $this->uidDisabled && $this->getElement($this->uidElementName) === null) {
+ $this->addElement(
+ 'hidden',
+ $this->uidElementName,
+ array(
+ 'ignore' => true,
+ 'value' => $this->getName(),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add CSRF counter measure field to this form
+ *
+ * @return $this
+ */
+ public function addCsrfCounterMeasure()
+ {
+ if (! $this->tokenDisabled) {
+ $request = $this->getRequest();
+ if (! $request->isXmlHttpRequest()
+ && ($this->getIsApiTarget() || $request->isApiRequest())
+ ) {
+ return $this;
+ }
+ if ($this->getElement($this->tokenElementName) === null) {
+ $this->addElement('CsrfCounterMeasure', $this->tokenElementName);
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Creates the form if not created yet.
+ *
+ * @param array $values
+ *
+ * @return $this
+ */
+ public function setDefaults(array $values)
+ {
+ $this->create($values);
+ return parent::setDefaults($values);
+ }
+
+ /**
+ * Populate the elements with the given values
+ *
+ * @param array $defaults The values to populate the elements with
+ *
+ * @return $this
+ */
+ public function populate(array $defaults)
+ {
+ $this->create($defaults);
+ $this->preserveDefaults($this, $defaults);
+ return parent::populate($defaults);
+ }
+
+ /**
+ * Recurse the given form and unset all unchanged default values
+ *
+ * @param Zend_Form $form
+ * @param array $defaults
+ */
+ protected function preserveDefaults(Zend_Form $form, array &$defaults)
+ {
+ foreach ($form->getElements() as $name => $element) {
+ if ((array_key_exists($name, $defaults)
+ && array_key_exists($name . static::DEFAULT_SUFFIX, $defaults)
+ && $defaults[$name] === $defaults[$name . static::DEFAULT_SUFFIX])
+ || $element->getAttrib('disabled')
+ ) {
+ unset($defaults[$name]);
+ }
+ }
+
+ foreach ($form->getSubForms() as $_ => $subForm) {
+ $this->preserveDefaults($subForm, $defaults);
+ }
+ }
+
+ /**
+ * Process the given request using this form
+ *
+ * Redirects to the url set with setRedirectUrl() upon success. See onSuccess()
+ * and onRequest() wherewith you can customize the processing logic.
+ *
+ * @param Request $request The request to be processed
+ *
+ * @return Request The request supposed to be processed
+ */
+ public function handleRequest(Request $request = null)
+ {
+ if ($request === null) {
+ $request = $this->getRequest();
+ } else {
+ $this->request = $request;
+ }
+
+ $formData = $this->getRequestData();
+ if ($this->getIsApiTarget()
+ // TODO: Very very bad, wasSent() must not be bypassed if it's only an api request but not an qpi target
+ || $this->getRequest()->isApiRequest()
+ || $this->getUidDisabled()
+ || $this->wasSent($formData)
+ ) {
+ $this->populate($formData); // Necessary to get isSubmitted() to work
+ if (! $this->getSubmitLabel() || $this->isSubmitted()) {
+ if ($this->isValid($formData)
+ && (($this->onSuccess !== null && false !== call_user_func($this->onSuccess, $this))
+ || ($this->onSuccess === null && false !== $this->onSuccess()))
+ ) {
+ // TODO: Still bad. An api target must not behave as one if it's not an api request
+ if ($this->getIsApiTarget() || $this->getRequest()->isApiRequest()) {
+ // API targets and API requests will never redirect but immediately respond w/ JSON-encoded
+ // notifications
+ $notifications = Notification::getInstance()->popMessages();
+ $message = null;
+ foreach ($notifications as $notification) {
+ if ($notification->type === Notification::SUCCESS) {
+ $message = $notification->message;
+ break;
+ }
+ }
+ $this->getResponse()->json()
+ ->setSuccessData($message !== null ? array('message' => $message) : null)
+ ->sendResponse();
+ } else {
+ $this->getResponse()->redirectAndExit($this->getRedirectUrl());
+ }
+ // TODO: Still bad. An api target must not behave as one if it's not an api request
+ } elseif ($this->getIsApiTarget() || $this->getRequest()->isApiRequest()) {
+ $this->getResponse()->json()->setFailData($this->getMessages())->sendResponse();
+ }
+ } elseif ($this->getValidatePartial()) {
+ // The form can't be processed but we may want to show validation errors though
+ $this->isValidPartial($formData);
+ }
+ } else {
+ $this->onRequest();
+ }
+
+ return $request;
+ }
+
+ /**
+ * Return whether the submit button of this form was pressed
+ *
+ * When overwriting Form::addSubmitButton() be sure to overwrite this method as well.
+ *
+ * @return bool True in case it was pressed, False otherwise or no submit label was set
+ */
+ public function isSubmitted()
+ {
+ $requestMethod = $this->getRequest()->getMethod();
+ if (strtolower($requestMethod ?: '') !== $this->getMethod()) {
+ return false;
+ }
+ if ($this->getIsApiTarget() || $this->getRequest()->isApiRequest()) {
+ return true;
+ }
+ if ($this->getSubmitLabel()) {
+ return $this->getElement('btn_submit')->isChecked();
+ }
+
+ return false;
+ }
+
+ /**
+ * Return whether the data sent by the user refers to this form
+ *
+ * Ensures that the correct form gets processed in case there are multiple forms
+ * with equal submit button names being posted against the same route.
+ *
+ * @param array $formData The data sent by the user
+ *
+ * @return bool Whether the given data refers to this form
+ */
+ public function wasSent(array $formData)
+ {
+ return isset($formData[$this->uidElementName]) && $formData[$this->uidElementName] === $this->getName();
+ }
+
+ /**
+ * Return whether the given values (possibly incomplete) are valid
+ *
+ * Unlike Zend_Form::isValid() this will not set NULL as value for
+ * an element that is not present in the given data.
+ *
+ * @param array $formData The data to validate
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ $this->create($formData);
+
+ foreach ($this->getElements() as $name => $element) {
+ if (array_key_exists($name, $formData)) {
+ if ($element->getAttrib('disabled')) {
+ // Ensure that disabled elements are not overwritten
+ // (http://www.zendframework.com/issues/browse/ZF-6909)
+ $formData[$name] = $element->getValue();
+ } elseif (array_key_exists($name . static::DEFAULT_SUFFIX, $formData)
+ && $formData[$name] === $formData[$name . static::DEFAULT_SUFFIX]
+ ) {
+ unset($formData[$name]);
+ }
+ }
+ }
+
+ return parent::isValidPartial($formData);
+ }
+
+ /**
+ * Return whether the given values are valid
+ *
+ * @param array $formData The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($formData)
+ {
+ $this->create($formData);
+
+ // Ensure that disabled elements are not overwritten (http://www.zendframework.com/issues/browse/ZF-6909)
+ foreach ($this->getElements() as $name => $element) {
+ if ($element->getAttrib('disabled')) {
+ $formData[$name] = $element->getValue();
+ }
+ }
+
+ return parent::isValid($formData);
+ }
+
+ /**
+ * Remove all elements of this form
+ *
+ * @return self
+ */
+ public function clearElements()
+ {
+ $this->created = false;
+ return parent::clearElements();
+ }
+
+ /**
+ * Load the default decorators
+ *
+ * Overwrites Zend_Form::loadDefaultDecorators to avoid having
+ * the HtmlTag-Decorator added and to provide view script usage
+ *
+ * @return $this
+ */
+ public function loadDefaultDecorators()
+ {
+ if ($this->loadDefaultDecoratorsIsDisabled()) {
+ return $this;
+ }
+
+ $decorators = $this->getDecorators();
+ if (empty($decorators)) {
+ if ($this->viewScript) {
+ $this->addDecorator('ViewScript', array(
+ 'viewScript' => $this->viewScript,
+ 'form' => $this
+ ));
+ } else {
+ $this->addDecorator('Description', array('tag' => 'h1'));
+ if ($this->getUseFormAutosubmit()) {
+ $this->getDecorator('Description')->setEscape(false);
+ $this->addDecorator(
+ 'HtmlTag',
+ array(
+ 'tag' => 'div',
+ 'class' => 'header',
+ 'id' => 'header-' . $this->getId()
+ )
+ );
+ }
+
+ $this->addDecorator('FormDescriptions')
+ ->addDecorator('FormNotifications')
+ ->addDecorator('FormErrors', array('onlyCustomFormErrors' => true))
+ ->addDecorator('FormElements')
+ ->addDecorator('FormHints')
+ //->addDecorator('HtmlTag', array('tag' => 'dl', 'class' => 'zend_form'))
+ ->addDecorator('Form');
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get element id
+ *
+ * Returns the protected id, in case id protection is enabled.
+ *
+ * @param bool $protect
+ *
+ * @return string
+ */
+ public function getId($protect = true)
+ {
+ $id = parent::getId();
+ return $protect && $this->protectIds ? $this->getRequest()->protectId($id) : $id;
+ }
+
+ /**
+ * Return the name of this form
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ $name = parent::getName();
+ if (! $name) {
+ $name = get_class($this);
+ $this->setName($name);
+ $name = parent::getName();
+ }
+ return $name;
+ }
+
+ /**
+ * Retrieve form description
+ *
+ * This will return the escaped description with the autosubmit warning icon if form autosubmit is enabled.
+ *
+ * @return string
+ */
+ public function getDescription()
+ {
+ $description = parent::getDescription();
+ if ($description && $this->getUseFormAutosubmit()) {
+ $autosubmit = $this->_getDecorator('Autosubmit', array('accessible' => true));
+ $autosubmit->setElement($this);
+ $description = $autosubmit->render($this->getView()->escape($description));
+ }
+
+ return $description;
+ }
+
+ /**
+ * Set the action to submit this form against
+ *
+ * Note that if you'll pass a instance of URL, Url::getAbsoluteUrl('&') is called to set the action.
+ *
+ * @param Url|string $action
+ *
+ * @return $this
+ */
+ public function setAction($action)
+ {
+ if ($action instanceof Url) {
+ $action = $action->getAbsoluteUrl('&');
+ }
+
+ return parent::setAction($action);
+ }
+
+ /**
+ * Set form description
+ *
+ * Alias for Zend_Form::setDescription().
+ *
+ * @param string $value
+ *
+ * @return Form
+ */
+ public function setTitle($value)
+ {
+ return $this->setDescription($value);
+ }
+
+ /**
+ * Return the request associated with this form
+ *
+ * Returns the global request if none has been set for this form yet.
+ *
+ * @return Request
+ */
+ public function getRequest()
+ {
+ if ($this->request === null) {
+ $this->request = Icinga::app()->getRequest();
+ }
+
+ return $this->request;
+ }
+
+ /**
+ * Set the request
+ *
+ * @param Request $request
+ *
+ * @return $this
+ */
+ public function setRequest(Request $request)
+ {
+ $this->request = $request;
+ return $this;
+ }
+
+ /**
+ * Return the current Response
+ *
+ * @return Response
+ */
+ public function getResponse()
+ {
+ return Icinga::app()->getFrontController()->getResponse();
+ }
+
+ /**
+ * Return the request data based on this form's request method
+ *
+ * @return array
+ */
+ protected function getRequestData()
+ {
+ $requestMethod = $this->getRequest()->getMethod();
+ if (strtolower($requestMethod ?: '') === $this->getMethod()) {
+ return $this->request->{'get' . ($this->request->isPost() ? 'Post' : 'Query')}();
+ }
+
+ return array();
+ }
+
+ /**
+ * Get the translation domain for this form
+ *
+ * The returned translation domain is either determined based on this form's qualified name or it is the default
+ * 'icinga' domain
+ *
+ * @return string
+ */
+ protected function getTranslationDomain()
+ {
+ $parts = explode('\\', get_called_class());
+ if (count($parts) > 1 && $parts[1] === 'Module') {
+ // Assume format Icinga\Module\ModuleName\Forms\...
+ return strtolower($parts[2]);
+ }
+
+ return 'icinga';
+ }
+
+ /**
+ * Translate a string
+ *
+ * @param string $text The string to translate
+ * @param string|null $context Optional parameter for context based translation
+ *
+ * @return string The translated string
+ */
+ protected function translate($text, $context = null)
+ {
+ $this->translationDomain = $this->getTranslationDomain();
+
+ return $this->i18nTranslate($text, $context);
+ }
+
+ /**
+ * Translate a plural string
+ *
+ * @param string $textSingular The string in singular form to translate
+ * @param string $textPlural The string in plural form to translate
+ * @param integer $number The amount to determine from whether to return singular or plural
+ * @param string|null $context Optional parameter for context based translation
+ *
+ * @return string The translated string
+ */
+ protected function translatePlural($textSingular, $textPlural, $number, $context = null)
+ {
+ $this->translationDomain = $this->getTranslationDomain();
+
+ return $this->i18nTranslatePlural($textSingular, $textPlural, $number, $context);
+ }
+
+ /**
+ * Render this form
+ *
+ * @param Zend_View_Interface $view The view context to use
+ *
+ * @return string
+ */
+ public function render(Zend_View_Interface $view = null)
+ {
+ $this->create();
+ return parent::render($view);
+ }
+
+ /**
+ * Get the authentication manager
+ *
+ * @return Auth
+ */
+ public function Auth()
+ {
+ if ($this->auth === null) {
+ $this->auth = Auth::getInstance();
+ }
+ return $this->auth;
+ }
+
+ /**
+ * Whether the current user has the given permission
+ *
+ * @param string $permission Name of the permission
+ *
+ * @return bool
+ */
+ public function hasPermission($permission)
+ {
+ return $this->Auth()->hasPermission($permission);
+ }
+
+ /**
+ * Assert that the current user has the given permission
+ *
+ * @param string $permission Name of the permission
+ *
+ * @throws SecurityException If the current user lacks the given permission
+ */
+ public function assertPermission($permission)
+ {
+ if (! $this->Auth()->hasPermission($permission)) {
+ throw new SecurityException('No permission for %s', $permission);
+ }
+ }
+
+ /**
+ * Add a error notification
+ *
+ * @param string|array $message The notification message
+ * @param bool $markAsError Whether to prevent the form from being successfully validated or not
+ *
+ * @return $this
+ */
+ public function error($message, $markAsError = true)
+ {
+ if ($this->getIsApiTarget()) {
+ $this->addErrorMessage($message);
+ } else {
+ $this->addNotification($message, self::NOTIFICATION_ERROR);
+ }
+
+ if ($markAsError) {
+ $this->markAsError();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add a warning notification
+ *
+ * @param string|array $message The notification message
+ * @param bool $markAsError Whether to prevent the form from being successfully validated or not
+ *
+ * @return $this
+ */
+ public function warning($message, $markAsError = true)
+ {
+ if ($this->getIsApiTarget()) {
+ $this->addErrorMessage($message);
+ } else {
+ $this->addNotification($message, self::NOTIFICATION_WARNING);
+ }
+
+ if ($markAsError) {
+ $this->markAsError();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add a info notification
+ *
+ * @param string|array $message The notification message
+ * @param bool $markAsError Whether to prevent the form from being successfully validated or not
+ *
+ * @return $this
+ */
+ public function info($message, $markAsError = true)
+ {
+ if ($this->getIsApiTarget()) {
+ $this->addErrorMessage($message);
+ } else {
+ $this->addNotification($message, self::NOTIFICATION_INFO);
+ }
+
+ if ($markAsError) {
+ $this->markAsError();
+ }
+
+ return $this;
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/Autosubmit.php b/library/Icinga/Web/Form/Decorator/Autosubmit.php
new file mode 100644
index 0000000..4405d0b
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/Autosubmit.php
@@ -0,0 +1,133 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Zend_Form_Decorator_Abstract;
+use Icinga\Application\Icinga;
+use Icinga\Web\View;
+use Icinga\Web\Form;
+
+/**
+ * Decorator to add an icon and a submit button encapsulated in noscript-tags
+ *
+ * The icon is shown in JS environments to indicate that a specific form field does automatically request an update
+ * of its form upon it has changed. The button allows users in non-JS environments to trigger the update manually.
+ */
+class Autosubmit extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * Whether a hidden <span> should be created with the same warning as in the icon label
+ *
+ * @var bool
+ */
+ protected $accessible;
+
+ /**
+ * The id used to identify the auto-submit warning associated with the decorated form element
+ *
+ * @var string
+ */
+ protected $warningId;
+
+ /**
+ * Set whether a hidden <span> should be created with the same warning as in the icon label
+ *
+ * @param bool $state
+ *
+ * @return Autosubmit
+ */
+ public function setAccessible($state = true)
+ {
+ $this->accessible = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether a hidden <span> is being created with the same warning as in the icon label
+ *
+ * @return bool
+ */
+ public function getAccessible()
+ {
+ if ($this->accessible === null) {
+ $this->accessible = $this->getOption('accessible') ?: false;
+ }
+
+ return $this->accessible;
+ }
+
+ /**
+ * Return the id used to identify the auto-submit warning associated with the decorated element
+ *
+ * @param mixed $element The element for which to generate a id
+ *
+ * @return string
+ */
+ public function getWarningId($element = null)
+ {
+ if ($this->warningId === null) {
+ $element = $element ?: $this->getElement();
+ $this->warningId = 'autosubmit_warning_' . $element->getId();
+ }
+
+ return $this->warningId;
+ }
+
+ /**
+ * Return the current view
+ *
+ * @return View
+ */
+ protected function getView()
+ {
+ return Icinga::app()->getViewRenderer()->view;
+ }
+
+ /**
+ * Add a auto-submit icon and submit button encapsulated in noscript-tags to the element
+ *
+ * @param string $content The html rendered so far
+ *
+ * @return string The updated html
+ */
+ public function render($content = '')
+ {
+ if ($content) {
+ $isForm = $this->getElement() instanceof Form;
+ $warning = $isForm
+ ? t('This page will be automatically updated upon change of any of this form\'s fields')
+ : t('This page will be automatically updated upon change of the value');
+ $content .= $this->getView()->icon('cw', $warning, array(
+ 'aria-hidden' => $isForm ? 'false' : 'true',
+ 'class' => 'spinner autosubmit-info'
+ ));
+ if (! $isForm && $this->getAccessible()) {
+ $content = '<span id="'
+ . $this->getWarningId()
+ . '" class="sr-only">'
+ . $warning
+ . '</span>'
+ . $content;
+ }
+
+ $content .= sprintf(
+ '<noscript><button'
+ . ' name="noscript_apply"'
+ . ' class="noscript-apply"'
+ . ' type="submit"'
+ . ' value="1"'
+ . ($this->getAccessible() ? ' aria-label="%1$s"' : '')
+ . ' title="%1$s"'
+ . '>%2$s</button></noscript>',
+ $isForm
+ ? t('Push this button to update the form to reflect the changes that were made below')
+ : t('Push this button to update the form to reflect the change'
+ . ' that was made in the field on the left'),
+ $this->getView()->icon('cw') . t('Apply')
+ );
+ }
+
+ return $content;
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/ConditionalHidden.php b/library/Icinga/Web/Form/Decorator/ConditionalHidden.php
new file mode 100644
index 0000000..0f84535
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/ConditionalHidden.php
@@ -0,0 +1,35 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Zend_Form_Decorator_Abstract;
+
+/**
+ * Decorator to hide elements using a &gt;noscript&lt; tag instead of
+ * type='hidden' or css styles.
+ *
+ * This allows to hide depending elements for browsers with javascript
+ * (who can then automatically refresh their pages) but show them in
+ * case JavaScript is disabled
+ */
+class ConditionalHidden extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * Generate a field that will be wrapped in <noscript> tag if the
+ * "condition" attribute is set and false or 0
+ *
+ * @param string $content The tag's content
+ *
+ * @return string The generated tag
+ */
+ public function render($content = '')
+ {
+ $attributes = $this->getElement()->getAttribs();
+ $condition = isset($attributes['condition']) ? $attributes['condition'] : 1;
+ if ($condition != 1) {
+ $content = '<noscript>' . $content . '</noscript>';
+ }
+ return $content;
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/ElementDoubler.php b/library/Icinga/Web/Form/Decorator/ElementDoubler.php
new file mode 100644
index 0000000..2da5646
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/ElementDoubler.php
@@ -0,0 +1,63 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Zend_Form_Element;
+use Zend_Form_Decorator_Abstract;
+
+/**
+ * A decorator that will double a single element of a display group
+ *
+ * The options `condition', `double' and `attributes' can be passed to the constructor and are used to affect whether
+ * the doubling should take effect, which element should be doubled and which HTML attributes should be applied to the
+ * doubled element, respectively.
+ *
+ * `condition' must be an element's name that when it's part of the display group causes the condition to be met.
+ * `double' must be an element's name and must be part of the display group.
+ * `attributes' is just an array of key-value pairs.
+ *
+ * You can also pass `placement' to control whether the doubled element is prepended or appended.
+ */
+class ElementDoubler extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * Return the display group's elements with an additional copy of an element being added if the condition is met
+ *
+ * @param string $content The HTML rendered so far
+ *
+ * @return string
+ */
+ public function render($content)
+ {
+ $group = $this->getElement();
+ if ($group->getElement($this->getOption('condition')) !== null) {
+ if ($this->getPlacement() === static::APPEND) {
+ return $content . $this->applyAttributes($group->getElement($this->getOption('double')))->render();
+ } else { // $this->getPlacement() === static::PREPEND
+ return $this->applyAttributes($group->getElement($this->getOption('double')))->render() . $content;
+ }
+ }
+
+ return $content;
+ }
+
+ /**
+ * Apply all element attributes
+ *
+ * @param Zend_Form_Element $element The element to apply the attributes to
+ *
+ * @return Zend_Form_Element
+ */
+ protected function applyAttributes(Zend_Form_Element $element)
+ {
+ $attributes = $this->getOption('attributes');
+ if ($attributes !== null) {
+ foreach ($attributes as $name => $value) {
+ $element->setAttrib($name, $value);
+ }
+ }
+
+ return $element;
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/FormDescriptions.php b/library/Icinga/Web/Form/Decorator/FormDescriptions.php
new file mode 100644
index 0000000..5bd5f6a
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/FormDescriptions.php
@@ -0,0 +1,76 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Form;
+use Zend_Form_Decorator_Abstract;
+
+/**
+ * Decorator to add a list of descriptions at the top or bottom of a form
+ */
+class FormDescriptions extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * Render form descriptions
+ *
+ * @param string $content The html rendered so far
+ *
+ * @return ?string The updated html
+ */
+ public function render($content = '')
+ {
+ $form = $this->getElement();
+ if (! $form instanceof Form) {
+ return $content;
+ }
+
+ $view = $form->getView();
+ if ($view === null) {
+ return $content;
+ }
+
+ $descriptions = $this->recurseForm($form);
+ if (empty($descriptions)) {
+ return $content;
+ }
+
+ $html = '<div class="form-description">'
+ . Icinga::app()->getViewRenderer()->view->icon('info-circled', '', ['class' => 'form-description-icon'])
+ . '<ul class="form-description-list">';
+
+ foreach ($descriptions as $description) {
+ if (is_array($description)) {
+ list($description, $properties) = $description;
+ $html .= '<li' . $view->propertiesToString($properties) . '>' . $view->escape($description) . '</li>';
+ } else {
+ $html .= '<li>' . $view->escape($description) . '</li>';
+ }
+ }
+
+ switch ($this->getPlacement()) {
+ case self::APPEND:
+ return $content . $html . '</ul></div>';
+ case self::PREPEND:
+ return $html . '</ul></div>' . $content;
+ }
+ }
+
+ /**
+ * Recurse the given form and return the descriptions for it and all of its subforms
+ *
+ * @param Form $form The form to recurse
+ *
+ * @return array
+ */
+ protected function recurseForm(Form $form)
+ {
+ $descriptions = array($form->getDescriptions());
+ foreach ($form->getSubForms() as $subForm) {
+ $descriptions[] = $this->recurseForm($subForm);
+ }
+
+ return call_user_func_array('array_merge', $descriptions);
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/FormHints.php b/library/Icinga/Web/Form/Decorator/FormHints.php
new file mode 100644
index 0000000..2a0f193
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/FormHints.php
@@ -0,0 +1,142 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Zend_Form_Decorator_Abstract;
+use Icinga\Web\Form;
+
+/**
+ * Decorator to add a list of hints at the top or bottom of a form
+ *
+ * The hint for required form elements is automatically being handled.
+ */
+class FormHints extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * A list of element class names to be ignored when detecting which message to use to describe required elements
+ *
+ * @var array
+ */
+ protected $blacklist;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct($options = null)
+ {
+ parent::__construct($options);
+ $this->blacklist = array(
+ 'Zend_Form_Element_Hidden',
+ 'Zend_Form_Element_Submit',
+ 'Zend_Form_Element_Button',
+ 'Icinga\Web\Form\Element\Note',
+ 'Icinga\Web\Form\Element\Button',
+ 'Icinga\Web\Form\Element\CsrfCounterMeasure'
+ );
+ }
+
+ /**
+ * Render form hints
+ *
+ * @param string $content The html rendered so far
+ *
+ * @return ?string The updated html
+ */
+ public function render($content = '')
+ {
+ $form = $this->getElement();
+ if (! $form instanceof Form) {
+ return $content;
+ }
+
+ $view = $form->getView();
+ if ($view === null) {
+ return $content;
+ }
+
+ $hints = $this->recurseForm($form, $entirelyRequired);
+ if ($entirelyRequired !== null) {
+ $hints[] = sprintf(
+ $form->getView()->translate('%s Required field'),
+ $form->getRequiredCue()
+ );
+ }
+
+ if (empty($hints)) {
+ return $content;
+ }
+
+ $html = '<ul class="form-info">';
+ foreach ($hints as $hint) {
+ if (is_array($hint)) {
+ list($hint, $properties) = $hint;
+ $html .= '<li' . $view->propertiesToString($properties) . '>' . $view->escape($hint) . '</li>';
+ } else {
+ $html .= '<li>' . $view->escape($hint) . '</li>';
+ }
+ }
+
+ switch ($this->getPlacement()) {
+ case self::APPEND:
+ return $content . $html . '</ul>';
+ case self::PREPEND:
+ return $html . '</ul>' . $content;
+ }
+ }
+
+ /**
+ * Recurse the given form and return the hints for it and all of its subforms
+ *
+ * @param Form $form The form to recurse
+ * @param mixed $entirelyRequired Set by reference, true means all elements in the hierarchy are
+ * required, false only a partial subset and null none at all
+ * @param bool $elementsPassed Whether there were any elements passed during the recursion until now
+ *
+ * @return array
+ */
+ protected function recurseForm(Form $form, &$entirelyRequired = null, $elementsPassed = false)
+ {
+ $requiredLabels = array();
+ if ($form->getRequiredCue() !== null) {
+ $partiallyRequired = $partiallyOptional = false;
+ foreach ($form->getElements() as $element) {
+ if (! in_array($element->getType(), $this->blacklist)) {
+ if (! $element->isRequired()) {
+ $partiallyOptional = true;
+ if ($entirelyRequired) {
+ $entirelyRequired = false;
+ }
+ } else {
+ $partiallyRequired = true;
+ if (($label = $element->getDecorator('label')) !== false) {
+ $requiredLabels[] = $label;
+ }
+ }
+ }
+ }
+
+ if (! $elementsPassed) {
+ $elementsPassed = $partiallyRequired || $partiallyOptional;
+ if ($entirelyRequired === null && $partiallyRequired) {
+ $entirelyRequired = ! $partiallyOptional;
+ }
+ } elseif ($entirelyRequired === null && $partiallyRequired) {
+ $entirelyRequired = false;
+ }
+ }
+
+ $hints = array($form->getHints());
+ foreach ($form->getSubForms() as $subForm) {
+ $hints[] = $this->recurseForm($subForm, $entirelyRequired, $elementsPassed);
+ }
+
+ if ($entirelyRequired) {
+ foreach ($requiredLabels as $label) {
+ $label->setRequiredSuffix('');
+ }
+ }
+
+ return call_user_func_array('array_merge', $hints);
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/FormNotifications.php b/library/Icinga/Web/Form/Decorator/FormNotifications.php
new file mode 100644
index 0000000..87d12aa
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/FormNotifications.php
@@ -0,0 +1,125 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Icinga\Application\Icinga;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Web\Form;
+use Zend_Form_Decorator_Abstract;
+
+/**
+ * Decorator to add a list of notifications at the top or bottom of a form
+ */
+class FormNotifications extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * Render form notifications
+ *
+ * @param string $content The html rendered so far
+ *
+ * @return ?string The updated html
+ */
+ public function render($content = '')
+ {
+ $form = $this->getElement();
+ if (! $form instanceof Form) {
+ return $content;
+ }
+
+ $view = $form->getView();
+ if ($view === null) {
+ return $content;
+ }
+
+ $notifications = $this->recurseForm($form);
+ if (empty($notifications)) {
+ return $content;
+ }
+
+ $html = '<ul class="form-notification-list">';
+ foreach (array(Form::NOTIFICATION_ERROR, Form::NOTIFICATION_WARNING, Form::NOTIFICATION_INFO) as $type) {
+ if (isset($notifications[$type])) {
+ $html .= '<li><ul class="notification-' . $this->getNotificationTypeName($type) . '">';
+ foreach ($notifications[$type] as $message) {
+ if (is_array($message)) {
+ list($message, $properties) = $message;
+ $html .= '<li' . $view->propertiesToString($properties) . '>'
+ . $view->escape($message)
+ . '</li>';
+ } else {
+ $html .= '<li>' . $view->escape($message) . '</li>';
+ }
+ }
+
+ $html .= '</ul></li>';
+ }
+ }
+
+ if (isset($notifications[Form::NOTIFICATION_ERROR])) {
+ $icon = 'cancel';
+ $class = 'error';
+ } elseif (isset($notifications[Form::NOTIFICATION_WARNING])) {
+ $icon = 'warning-empty';
+ $class = 'warning';
+ } else {
+ $icon = 'info';
+ $class = 'info';
+ }
+
+ $html = "<div class=\"form-notifications $class\">"
+ . Icinga::app()->getViewRenderer()->view->icon($icon, '', ['class' => 'form-notification-icon'])
+ . $html;
+
+ switch ($this->getPlacement()) {
+ case self::APPEND:
+ return $content . $html . '</ul></div>';
+ case self::PREPEND:
+ return $html . '</ul></div>' . $content;
+ }
+ }
+
+ /**
+ * Recurse the given form and return the notifications for it and all of its subforms
+ *
+ * @param Form $form The form to recurse
+ *
+ * @return array
+ */
+ protected function recurseForm(Form $form)
+ {
+ $notifications = $form->getNotifications();
+ foreach ($form->getSubForms() as $subForm) {
+ foreach ($this->recurseForm($subForm) as $type => $messages) {
+ foreach ($messages as $message) {
+ $notifications[$type][] = $message;
+ }
+ }
+ }
+
+ return $notifications;
+ }
+
+ /**
+ * Return the name for the given notification type
+ *
+ * @param int $type
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case the given type is invalid
+ */
+ protected function getNotificationTypeName($type)
+ {
+ switch ($type) {
+ case Form::NOTIFICATION_ERROR:
+ return 'error';
+ case Form::NOTIFICATION_WARNING:
+ return 'warning';
+ case Form::NOTIFICATION_INFO:
+ return 'info';
+ default:
+ throw new ProgrammingError('Invalid notification type "%s" provided', $type);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/Help.php b/library/Icinga/Web/Form/Decorator/Help.php
new file mode 100644
index 0000000..9e30e86
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/Help.php
@@ -0,0 +1,113 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Zend_Form_Element;
+use Zend_Form_Decorator_Abstract;
+use Icinga\Application\Icinga;
+use Icinga\Web\View;
+
+/**
+ * Decorator to add helptext to a form element
+ */
+class Help extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * Whether a hidden <span> should be created to describe the decorated form element
+ *
+ * @var bool
+ */
+ protected $accessible = false;
+
+ /**
+ * The id used to identify the description associated with the decorated form element
+ *
+ * @var string
+ */
+ protected $descriptionId;
+
+ /**
+ * Set whether a hidden <span> should be created to describe the decorated form element
+ *
+ * @param bool $state
+ *
+ * @return Help
+ */
+ public function setAccessible($state = true)
+ {
+ $this->accessible = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return the id used to identify the description associated with the decorated element
+ *
+ * @param Zend_Form_Element $element The element for which to generate a id
+ *
+ * @return string
+ */
+ public function getDescriptionId(Zend_Form_Element $element = null)
+ {
+ if ($this->descriptionId === null) {
+ $element = $element ?: $this->getElement();
+ $this->descriptionId = 'desc_' . $element->getId();
+ }
+
+ return $this->descriptionId;
+ }
+
+ /**
+ * Return the current view
+ *
+ * @return View
+ */
+ protected function getView()
+ {
+ return Icinga::app()->getViewRenderer()->view;
+ }
+
+ /**
+ * Add a help icon to the left of an element
+ *
+ * @param string $content The html rendered so far
+ *
+ * @return ?string The updated html
+ */
+ public function render($content = '')
+ {
+ $element = $this->getElement();
+ $description = $element->getDescription();
+ $requirement = $element->getAttrib('requirement');
+ unset($element->requirement);
+
+ $helpContent = '';
+ if ($description || $requirement) {
+ if ($this->accessible) {
+ $helpContent = '<span id="'
+ . $this->getDescriptionId()
+ . '" class="sr-only">'
+ . $description
+ . ($description && $requirement ? ' ' : '')
+ . $requirement
+ . '</span>';
+ }
+
+ $helpContent = $this->getView()->icon(
+ 'info-circled',
+ $description . ($description && $requirement ? ' ' : '') . $requirement,
+ array(
+ 'class' => 'control-info',
+ 'aria-hidden' => $this->accessible ? 'true' : 'false'
+ )
+ ) . $helpContent;
+ }
+
+ switch ($this->getPlacement()) {
+ case self::APPEND:
+ return $content . $helpContent;
+ case self::PREPEND:
+ return $helpContent . $content;
+ }
+ }
+}
diff --git a/library/Icinga/Web/Form/Decorator/Spinner.php b/library/Icinga/Web/Form/Decorator/Spinner.php
new file mode 100644
index 0000000..09a3ae9
--- /dev/null
+++ b/library/Icinga/Web/Form/Decorator/Spinner.php
@@ -0,0 +1,48 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Decorator;
+
+use Zend_Form_Decorator_Abstract;
+use Icinga\Application\Icinga;
+use Icinga\Web\View;
+
+/**
+ * Decorator to add a spinner next to an element
+ */
+class Spinner extends Zend_Form_Decorator_Abstract
+{
+ /**
+ * Return the current view
+ *
+ * @return View
+ */
+ protected function getView()
+ {
+ return Icinga::app()->getViewRenderer()->view;
+ }
+
+ /**
+ * Add a spinner icon to a form element
+ *
+ * @param string $content The html rendered so far
+ *
+ * @return ?string The updated html
+ */
+ public function render($content = '')
+ {
+ $spinner = '<div '
+ . ($this->getOption('id') !== null ? ' id="' . $this->getOption('id') . '"' : '')
+ . 'class="spinner ' . ($this->getOption('class') ?: '') . '"'
+ . '>'
+ . $this->getView()->icon('spin6')
+ . '</div>';
+
+ switch ($this->getPlacement()) {
+ case self::APPEND:
+ return $content . $spinner;
+ case self::PREPEND:
+ return $spinner . $content;
+ }
+ }
+}
diff --git a/library/Icinga/Web/Form/Element/Button.php b/library/Icinga/Web/Form/Element/Button.php
new file mode 100644
index 0000000..307247e
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/Button.php
@@ -0,0 +1,81 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use Icinga\Web\Request;
+use Icinga\Application\Icinga;
+use Icinga\Web\Form\FormElement;
+use Zend_Config;
+
+/**
+ * A button
+ */
+class Button extends FormElement
+{
+ /**
+ * Use formButton view helper by default
+ *
+ * @var string
+ */
+ public $helper = 'formButton';
+
+ /**
+ * Constructor
+ *
+ * @param string|array|Zend_Config $spec Element name or configuration
+ * @param string|array|Zend_Config $options Element value or configuration
+ */
+ public function __construct($spec, $options = null)
+ {
+ if (is_string($spec) && ((null !== $options) && is_string($options))) {
+ $options = array('label' => $options);
+ }
+
+ if (!isset($options['ignore'])) {
+ $options['ignore'] = true;
+ }
+
+ parent::__construct($spec, $options);
+
+ if ($label = $this->getLabel()) {
+ // Necessary to get the label shown on the generated HTML
+ $this->content = $label;
+ }
+ }
+
+ /**
+ * Validate element value (pseudo)
+ *
+ * There is no need to reset the value
+ *
+ * @param mixed $value Is always ignored
+ * @param mixed $context Is always ignored
+ *
+ * @return bool Returns always TRUE
+ */
+ public function isValid($value, $context = null)
+ {
+ return true;
+ }
+
+ /**
+ * Has this button been selected?
+ *
+ * @return bool
+ */
+ public function isChecked()
+ {
+ return $this->getRequest()->getParam($this->getName()) === $this->getValue();
+ }
+
+ /**
+ * Return the current request
+ *
+ * @return Request
+ */
+ protected function getRequest()
+ {
+ return Icinga::app()->getRequest();
+ }
+}
diff --git a/library/Icinga/Web/Form/Element/Checkbox.php b/library/Icinga/Web/Form/Element/Checkbox.php
new file mode 100644
index 0000000..d4499a0
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/Checkbox.php
@@ -0,0 +1,9 @@
+<?php
+/* Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+class Checkbox extends \Zend_Form_Element_Checkbox
+{
+ public $helper = 'icingaCheckbox';
+}
diff --git a/library/Icinga/Web/Form/Element/CsrfCounterMeasure.php b/library/Icinga/Web/Form/Element/CsrfCounterMeasure.php
new file mode 100644
index 0000000..c59e1f9
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/CsrfCounterMeasure.php
@@ -0,0 +1,99 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use Icinga\Web\Session;
+use Icinga\Web\Form\FormElement;
+use Icinga\Web\Form\InvalidCSRFTokenException;
+
+/**
+ * CSRF counter measure element
+ *
+ * You must not set a value to successfully use this element, just give it a name and you're good to go.
+ */
+class CsrfCounterMeasure extends FormElement
+{
+ /**
+ * Default form view helper to use for rendering
+ *
+ * @var string
+ */
+ public $helper = 'formHidden';
+
+ /**
+ * Counter measure element is required
+ *
+ * @var bool
+ */
+ protected $_ignore = true;
+
+ /**
+ * Ignore element when retrieving values at form level
+ *
+ * @var bool
+ */
+ protected $_required = true;
+
+ /**
+ * Initialize this form element
+ */
+ public function init()
+ {
+ $this->setDecorators(['ViewHelper']);
+ $this->setValue($this->generateCsrfToken());
+ }
+
+ /**
+ * Check whether $value is a valid CSRF token
+ *
+ * @param string $value The value to check
+ * @param mixed $context Context to use
+ *
+ * @return bool True, in case the CSRF token is valid
+ *
+ * @throws InvalidCSRFTokenException In case the CSRF token is not valid
+ */
+ public function isValid($value, $context = null)
+ {
+ if (parent::isValid($value, $context) && $this->isValidCsrfToken($value)) {
+ return true;
+ }
+
+ throw new InvalidCSRFTokenException();
+ }
+
+ /**
+ * Check whether the given value is a valid CSRF token for the current session
+ *
+ * @param string $token The CSRF token
+ *
+ * @return bool
+ */
+ protected function isValidCsrfToken($token)
+ {
+ if (strpos($token, '|') === false) {
+ return false;
+ }
+
+ list($seed, $hash) = explode('|', $token);
+
+ if (false === is_numeric($seed)) {
+ return false;
+ }
+
+ return $hash === hash('sha256', Session::getSession()->getId() . $seed);
+ }
+
+ /**
+ * Generate a new (seed, token) pair
+ *
+ * @return string
+ */
+ protected function generateCsrfToken()
+ {
+ $seed = mt_rand();
+ $hash = hash('sha256', Session::getSession()->getId() . $seed);
+ return sprintf('%s|%s', $seed, $hash);
+ }
+}
diff --git a/library/Icinga/Web/Form/Element/Date.php b/library/Icinga/Web/Form/Element/Date.php
new file mode 100644
index 0000000..8e0985c
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/Date.php
@@ -0,0 +1,19 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use Icinga\Web\Form\FormElement;
+
+/**
+ * A date input control
+ */
+class Date extends FormElement
+{
+ /**
+ * Form view helper to use for rendering
+ *
+ * @var string
+ */
+ public $helper = 'formDate';
+}
diff --git a/library/Icinga/Web/Form/Element/DateTimePicker.php b/library/Icinga/Web/Form/Element/DateTimePicker.php
new file mode 100644
index 0000000..284a744
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/DateTimePicker.php
@@ -0,0 +1,80 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use DateTime;
+use Icinga\Web\Form\FormElement;
+use Icinga\Web\Form\Validator\DateTimeValidator;
+
+/**
+ * A date-and-time input control
+ */
+class DateTimePicker extends FormElement
+{
+ /**
+ * Form view helper to use for rendering
+ *
+ * @var string
+ */
+ public $helper = 'formDateTime';
+
+ /**
+ * @var bool
+ */
+ protected $local = true;
+
+ /**
+ * (non-PHPDoc)
+ * @see Zend_Form_Element::init() For the method documentation.
+ */
+ public function init()
+ {
+ $this->addValidator(
+ new DateTimeValidator($this->local),
+ true // true for breaking the validator chain on failure
+ );
+ }
+
+ /**
+ * Get the expected date and time format of any user input
+ *
+ * @return string
+ */
+ public function getFormat()
+ {
+ return $this->local ? 'Y-m-d\TH:i:s' : DateTime::RFC3339;
+ }
+
+ /**
+ * Is the date and time valid?
+ *
+ * @param string|DateTime $value
+ * @param mixed $context
+ *
+ * @return bool
+ */
+ public function isValid($value, $context = null)
+ {
+ if (is_scalar($value) && $value !== '' && ! preg_match('/\D/', $value)) {
+ $dateTime = new DateTime();
+ $value = $dateTime->setTimestamp($value)->format($this->getFormat());
+ }
+
+ if (! parent::isValid($value, $context)) {
+ return false;
+ }
+
+ if (! $value instanceof DateTime) {
+ $format = $this->getFormat();
+ $dateTime = DateTime::createFromFormat($format, $value);
+ if ($dateTime === false) {
+ $dateTime = DateTime::createFromFormat(substr($format, 0, strrpos($format, ':')), $value);
+ }
+
+ $this->setValue($dateTime);
+ }
+
+ return true;
+ }
+}
diff --git a/library/Icinga/Web/Form/Element/Note.php b/library/Icinga/Web/Form/Element/Note.php
new file mode 100644
index 0000000..9569dee
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/Note.php
@@ -0,0 +1,55 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use Icinga\Web\Form\FormElement;
+
+/**
+ * A note
+ */
+class Note extends FormElement
+{
+ /**
+ * Form view helper to use for rendering
+ *
+ * @var string
+ */
+ public $helper = 'formNote';
+
+ /**
+ * Ignore element when retrieving values at form level
+ *
+ * @var bool
+ */
+ protected $_ignore = true;
+
+ /**
+ * (non-PHPDoc)
+ * @see Zend_Form_Element::init() For the method documentation.
+ */
+ public function init()
+ {
+ if (count($this->getDecorators()) === 0) {
+ $this->setDecorators(array(
+ 'ViewHelper',
+ array(
+ 'HtmlTag',
+ array('tag' => 'p')
+ )
+ ));
+ }
+ }
+
+ /**
+ * Validate element value (pseudo)
+ *
+ * @param mixed $value Ignored
+ *
+ * @return bool Always true
+ */
+ public function isValid($value, $context = null)
+ {
+ return true;
+ }
+}
diff --git a/library/Icinga/Web/Form/Element/Number.php b/library/Icinga/Web/Form/Element/Number.php
new file mode 100644
index 0000000..afbd07d
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/Number.php
@@ -0,0 +1,144 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use Icinga\Web\Form\FormElement;
+
+/**
+ * A number input control
+ */
+class Number extends FormElement
+{
+ /**
+ * Form view helper to use for rendering
+ *
+ * @var string
+ */
+ public $helper = 'formNumber';
+
+ /**
+ * The expected lower bound for the element’s value
+ *
+ * @var float|null
+ */
+ protected $min;
+
+ /**
+ * The expected upper bound for the element’s
+ *
+ * @var float|null
+ */
+ protected $max;
+
+ /**
+ * The value granularity of the element’s value
+ *
+ * Normally, number input controls are limited to an accuracy of integer values.
+ *
+ * @var float|string|null
+ */
+ protected $step;
+
+ /**
+ * (non-PHPDoc)
+ * @see \Zend_Form_Element::init() For the method documentation.
+ */
+ public function init()
+ {
+ if ($this->min !== null || $this->max !== null) {
+ $this->addValidator('Between', true, array(
+ 'min' => $this->min === null ? -INF : $this->min,
+ 'max' => $this->max === null ? INF : $this->max,
+ 'inclusive' => true
+ ));
+ }
+ }
+
+ /**
+ * Set the expected lower bound for the element’s value
+ *
+ * @param float $min
+ *
+ * @return $this
+ */
+ public function setMin($min)
+ {
+ $this->min = (float) $min;
+ return $this;
+ }
+
+ /**
+ * Get the expected lower bound for the element’s value
+ *
+ * @return float|null
+ */
+ public function getMin()
+ {
+ return $this->min;
+ }
+
+ /**
+ * Set the expected upper bound for the element’s value
+ *
+ * @param float $max
+ *
+ * @return $this
+ */
+ public function setMax($max)
+ {
+ $this->max = (float) $max;
+ return $this;
+ }
+
+ /**
+ * Get the expected upper bound for the element’s value
+ *
+ * @return float|null
+ */
+ public function getMax()
+ {
+ return $this->max;
+ }
+
+ /**
+ * Set the value granularity of the element’s value
+ *
+ * @param float|string $step
+ *
+ * @return $this
+ */
+ public function setStep($step)
+ {
+ if ($step !== 'any') {
+ $step = (float) $step;
+ }
+ $this->step = $step;
+ return $this;
+ }
+
+ /**
+ * Get the value granularity of the element’s value
+ *
+ * @return float|string|null
+ */
+ public function getStep()
+ {
+ return $this->step;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Zend_Form_Element::isValid() For the method documentation.
+ */
+ public function isValid($value, $context = null)
+ {
+ $this->setValue($value);
+ $value = $this->getValue();
+ if ($value !== null && $value !== '' && ! is_numeric($value)) {
+ $this->addError(sprintf(t('\'%s\' is not a valid number'), $value));
+ return false;
+ }
+ return parent::isValid($value, $context);
+ }
+}
diff --git a/library/Icinga/Web/Form/Element/Textarea.php b/library/Icinga/Web/Form/Element/Textarea.php
new file mode 100644
index 0000000..119cd56
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/Textarea.php
@@ -0,0 +1,20 @@
+<?php
+/* Icinga Web 2 | (c) 2019 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use Icinga\Web\Form\FormElement;
+
+class Textarea extends FormElement
+{
+ public $helper = 'formTextarea';
+
+ public function __construct($spec, $options = null)
+ {
+ parent::__construct($spec, $options);
+
+ if ($this->getAttrib('rows') === null) {
+ $this->setAttrib('rows', 3);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Form/Element/Time.php b/library/Icinga/Web/Form/Element/Time.php
new file mode 100644
index 0000000..4b76a33
--- /dev/null
+++ b/library/Icinga/Web/Form/Element/Time.php
@@ -0,0 +1,19 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Element;
+
+use Icinga\Web\Form\FormElement;
+
+/**
+ * A time input control
+ */
+class Time extends FormElement
+{
+ /**
+ * Form view helper to use for rendering
+ *
+ * @var string
+ */
+ public $helper = 'formTime';
+}
diff --git a/library/Icinga/Web/Form/ErrorLabeller.php b/library/Icinga/Web/Form/ErrorLabeller.php
new file mode 100644
index 0000000..3f822d5
--- /dev/null
+++ b/library/Icinga/Web/Form/ErrorLabeller.php
@@ -0,0 +1,71 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form;
+
+use BadMethodCallException;
+use Zend_Translate_Adapter;
+use Zend_Validate_NotEmpty;
+use Zend_Validate_File_MimeType;
+use Icinga\Web\Form\Validator\DateTimeValidator;
+use Icinga\Web\Form\Validator\ReadablePathValidator;
+use Icinga\Web\Form\Validator\WritablePathValidator;
+
+class ErrorLabeller extends Zend_Translate_Adapter
+{
+ protected $messages;
+
+ public function __construct($options = array())
+ {
+ if (! isset($options['element'])) {
+ throw new BadMethodCallException('Option "element" is missing');
+ }
+
+ $this->messages = $this->createMessages($options['element']);
+ }
+
+ public function isTranslated($messageId, $original = false, $locale = null)
+ {
+ return array_key_exists($messageId, $this->messages);
+ }
+
+ public function translate($messageId, $locale = null)
+ {
+ if (array_key_exists($messageId, $this->messages)) {
+ return $this->messages[$messageId];
+ }
+
+ return $messageId;
+ }
+
+ protected function createMessages($element)
+ {
+ $label = $element->getLabel() ?: $element->getName();
+
+ return array(
+ Zend_Validate_NotEmpty::IS_EMPTY => sprintf(t('%s is required and must not be empty'), $label),
+ Zend_Validate_File_MimeType::FALSE_TYPE => sprintf(
+ t('%s (%%value%%) has a false MIME type of "%%type%%"'),
+ $label
+ ),
+ Zend_Validate_File_MimeType::NOT_DETECTED => sprintf(t('%s (%%value%%) has no MIME type'), $label),
+ WritablePathValidator::NOT_WRITABLE => sprintf(t('%s is not writable', 'config.path'), $label),
+ WritablePathValidator::DOES_NOT_EXIST => sprintf(t('%s does not exist', 'config.path'), $label),
+ ReadablePathValidator::NOT_READABLE => sprintf(t('%s is not readable', 'config.path'), $label),
+ DateTimeValidator::INVALID_DATETIME_FORMAT => sprintf(
+ t('%s not in the expected format: %%value%%'),
+ $label
+ )
+ );
+ }
+
+ protected function _loadTranslationData($data, $locale, array $options = array())
+ {
+ // nonsense, required as being abstract otherwise...
+ }
+
+ public function toString()
+ {
+ return 'ErrorLabeller'; // nonsense, required as being abstract otherwise...
+ }
+}
diff --git a/library/Icinga/Web/Form/FormElement.php b/library/Icinga/Web/Form/FormElement.php
new file mode 100644
index 0000000..766d916
--- /dev/null
+++ b/library/Icinga/Web/Form/FormElement.php
@@ -0,0 +1,61 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form;
+
+use Zend_Form_Element;
+use Icinga\Web\Form;
+
+/**
+ * Base class for Icinga Web 2 form elements
+ */
+class FormElement extends Zend_Form_Element
+{
+ /**
+ * Whether loading default decorators is disabled
+ *
+ * Icinga Web 2 loads its own default element decorators. For loading Zend's default element decorators set this
+ * property to false.
+ *
+ * @var null|bool
+ */
+ protected $_disableLoadDefaultDecorators;
+
+ /**
+ * Whether loading default decorators is disabled
+ *
+ * @return bool
+ */
+ public function loadDefaultDecoratorsIsDisabled()
+ {
+ return $this->_disableLoadDefaultDecorators === true;
+ }
+
+ /**
+ * Load default decorators
+ *
+ * Icinga Web 2 loads its own default element decorators. For loading Zend's default element decorators set
+ * FormElement::$_disableLoadDefaultDecorators to false.
+ *
+ * @return $this
+ * @see Form::$defaultElementDecorators For Icinga Web 2's default element decorators.
+ */
+ public function loadDefaultDecorators()
+ {
+ if ($this->loadDefaultDecoratorsIsDisabled()) {
+ return $this;
+ }
+
+ if (! isset($this->_disableLoadDefaultDecorators)) {
+ $decorators = $this->getDecorators();
+ if (empty($decorators)) {
+ // Load Icinga Web 2's default element decorators
+ $this->addDecorators(Form::$defaultElementDecorators);
+ }
+ } else {
+ // Load Zend's default decorators
+ parent::loadDefaultDecorators();
+ }
+ return $this;
+ }
+}
diff --git a/library/Icinga/Web/Form/InvalidCSRFTokenException.php b/library/Icinga/Web/Form/InvalidCSRFTokenException.php
new file mode 100644
index 0000000..d0eb68a
--- /dev/null
+++ b/library/Icinga/Web/Form/InvalidCSRFTokenException.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form;
+
+/**
+ * Exceptions for invalid form tokens
+ */
+class InvalidCSRFTokenException extends \Exception
+{
+}
diff --git a/library/Icinga/Web/Form/Validator/DateFormatValidator.php b/library/Icinga/Web/Form/Validator/DateFormatValidator.php
new file mode 100644
index 0000000..eacb29c
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/DateFormatValidator.php
@@ -0,0 +1,61 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use Zend_Validate_Abstract;
+
+/**
+ * Validator that checks if a textfield contains a correct date format
+ */
+class DateFormatValidator extends Zend_Validate_Abstract
+{
+
+ /**
+ * Valid date characters according to @see http://www.php.net/manual/en/function.date.php
+ *
+ * @var array
+ *
+ * @see http://www.php.net/manual/en/function.date.php
+ */
+ private $validChars =
+ array('d', 'D', 'j', 'l', 'N', 'S', 'w', 'z', 'W', 'F', 'm', 'M', 'n', 't', 'L', 'o', 'Y', 'y');
+
+ /**
+ * List of sensible time separators
+ *
+ * @var array
+ */
+ private $validSeparators = array(' ', ':', '-', '/', ';', ',', '.');
+
+ /**
+ * Error templates
+ *
+ * @var array
+ *
+ * @see Zend_Validate_Abstract::$_messageTemplates
+ */
+ protected $_messageTemplates = array(
+ 'INVALID_CHARACTERS' => 'Invalid date format'
+ );
+
+ /**
+ * Validate the input value
+ *
+ * @param string $value The format string to validate
+ * @param null $context The form context (ignored)
+ *
+ * @return bool True when the input is valid, otherwise false
+ *
+ * @see Zend_Validate_Abstract::isValid()
+ */
+ public function isValid($value, $context = null)
+ {
+ $rest = trim($value, join(' ', array_merge($this->validChars, $this->validSeparators)));
+ if (strlen($rest) > 0) {
+ $this->_error('INVALID_CHARACTERS');
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/library/Icinga/Web/Form/Validator/DateTimeValidator.php b/library/Icinga/Web/Form/Validator/DateTimeValidator.php
new file mode 100644
index 0000000..5ef327d
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/DateTimeValidator.php
@@ -0,0 +1,77 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use DateTime;
+use Zend_Validate_Abstract;
+
+/**
+ * Validator for date-and-time input controls
+ *
+ * @see \Icinga\Web\Form\Element\DateTimePicker For the date-and-time input control.
+ */
+class DateTimeValidator extends Zend_Validate_Abstract
+{
+ const INVALID_DATETIME_TYPE = 'invalidDateTimeType';
+ const INVALID_DATETIME_FORMAT = 'invalidDateTimeFormat';
+
+ /**
+ * The messages to write on differen error states
+ *
+ * @var array
+ *
+ * @see Zend_Validate_Abstract::$_messageTemplates‚
+ */
+ protected $_messageTemplates = array(
+ self::INVALID_DATETIME_TYPE => 'Invalid type given. Instance of DateTime or date/time string expected',
+ self::INVALID_DATETIME_FORMAT => 'Date/time string not in the expected format: %value%'
+ );
+
+ protected $local;
+
+ /**
+ * Create a new date-and-time input control validator
+ *
+ * @param bool $local
+ */
+ public function __construct($local)
+ {
+ $this->local = (bool) $local;
+ }
+
+ /**
+ * Is the date and time valid?
+ *
+ * @param string|DateTime $value
+ * @param mixed $context
+ *
+ * @return bool
+ *
+ * @see \Zend_Validate_Interface::isValid()
+ */
+ public function isValid($value, $context = null)
+ {
+ if (! $value instanceof DateTime && ! is_string($value)) {
+ $this->_error(self::INVALID_DATETIME_TYPE);
+ return false;
+ }
+
+ if (! $value instanceof DateTime) {
+ $format = $baseFormat = $this->local === true ? 'Y-m-d\TH:i:s' : DateTime::RFC3339;
+ $dateTime = DateTime::createFromFormat($format, $value);
+
+ if ($dateTime === false) {
+ $format = substr($format, 0, strrpos($format, ':'));
+ $dateTime = DateTime::createFromFormat($format, $value);
+ }
+
+ if ($dateTime === false || $dateTime->format($format) !== $value) {
+ $this->_error(self::INVALID_DATETIME_FORMAT, $baseFormat);
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/library/Icinga/Web/Form/Validator/InArray.php b/library/Icinga/Web/Form/Validator/InArray.php
new file mode 100644
index 0000000..5d3925e
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/InArray.php
@@ -0,0 +1,28 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use Zend_Validate_InArray;
+use Icinga\Util\StringHelper;
+
+class InArray extends Zend_Validate_InArray
+{
+ protected function _error($messageKey, $value = null)
+ {
+ if ($messageKey === static::NOT_IN_ARRAY) {
+ $matches = StringHelper::findSimilar($this->_value, $this->_haystack);
+ if (empty($matches)) {
+ $this->_messages[$messageKey] = sprintf(t('"%s" is not in the list of allowed values.'), $this->_value);
+ } else {
+ $this->_messages[$messageKey] = sprintf(
+ t('"%s" is not in the list of allowed values. Did you mean one of the following?: %s'),
+ $this->_value,
+ implode(', ', $matches)
+ );
+ }
+ } else {
+ parent::_error($messageKey, $value);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Form/Validator/InternalUrlValidator.php b/library/Icinga/Web/Form/Validator/InternalUrlValidator.php
new file mode 100644
index 0000000..f936bb5
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/InternalUrlValidator.php
@@ -0,0 +1,41 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use Icinga\Application\Icinga;
+use Zend_Validate_Abstract;
+use Icinga\Web\Url;
+
+/**
+ * Validator that checks whether a textfield doesn't contain an external URL
+ */
+class InternalUrlValidator extends Zend_Validate_Abstract
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid($value)
+ {
+ $url = Url::fromPath($value);
+ if ($url->getRelativeUrl() === '' || $url->isExternal()) {
+ $this->_error('IS_EXTERNAL');
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function _error($messageKey, $value = null)
+ {
+ if ($messageKey === 'IS_EXTERNAL') {
+ $this->_messages[$messageKey] = t('The url must not be external.');
+ } else {
+ parent::_error($messageKey, $value);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Form/Validator/ReadablePathValidator.php b/library/Icinga/Web/Form/Validator/ReadablePathValidator.php
new file mode 100644
index 0000000..826421c
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/ReadablePathValidator.php
@@ -0,0 +1,53 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use Zend_Validate_Abstract;
+
+/**
+ * Validator that interprets the value as a filepath and checks if it's readable
+ *
+ * This validator should be preferred due to Zend_Validate_File_Exists is
+ * getting confused if there is another element in the form called `name'.
+ */
+class ReadablePathValidator extends Zend_Validate_Abstract
+{
+ const NOT_READABLE = 'notReadable';
+ const DOES_NOT_EXIST = 'doesNotExist';
+
+ /**
+ * The messages to write on different error states
+ *
+ * @var array
+ *
+ * @see Zend_Validate_Abstract::$_messageTemplates‚
+ */
+ protected $_messageTemplates = array(
+ self::NOT_READABLE => 'Path is not readable',
+ self::DOES_NOT_EXIST => 'Path does not exist'
+ );
+
+ /**
+ * Check whether the given value is a readable filepath
+ *
+ * @param string $value The value submitted in the form
+ * @param mixed $context The context of the form
+ *
+ * @return bool Whether the value was successfully validated
+ */
+ public function isValid($value, $context = null)
+ {
+ if (false === file_exists($value)) {
+ $this->_error(self::DOES_NOT_EXIST);
+ return false;
+ }
+
+ if (false === is_readable($value)) {
+ $this->_error(self::NOT_READABLE);
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/library/Icinga/Web/Form/Validator/TimeFormatValidator.php b/library/Icinga/Web/Form/Validator/TimeFormatValidator.php
new file mode 100644
index 0000000..9c1c99a
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/TimeFormatValidator.php
@@ -0,0 +1,58 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use Zend_Validate_Abstract;
+
+/**
+ * Validator that checks if a textfield contains a correct time format
+ */
+class TimeFormatValidator extends Zend_Validate_Abstract
+{
+
+ /**
+ * Valid time characters according to @see http://www.php.net/manual/en/function.date.php
+ *
+ * @var array
+ * @see http://www.php.net/manual/en/function.date.php
+ */
+ private $validChars = array('a', 'A', 'B', 'g', 'G', 'h', 'H', 'i', 's', 'u');
+
+ /**
+ * List of sensible time separators
+ *
+ * @var array
+ */
+ private $validSeparators = array(' ', ':', '-', '/', ';', ',', '.');
+
+ /**
+ * Error templates
+ *
+ * @var array
+ * @see Zend_Validate_Abstract::$_messageTemplates
+ */
+ protected $_messageTemplates = array(
+ 'INVALID_CHARACTERS' => 'Invalid time format'
+ );
+
+ /**
+ * Validate the input value
+ *
+ * @param string $value The format string to validate
+ * @param null $context The form context (ignored)
+ *
+ * @return bool True when the input is valid, otherwise false
+ *
+ * @see Zend_Validate_Abstract::isValid()
+ */
+ public function isValid($value, $context = null)
+ {
+ $rest = trim($value, join(' ', array_merge($this->validChars, $this->validSeparators)));
+ if (strlen($rest) > 0) {
+ $this->_error('INVALID_CHARACTERS');
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/library/Icinga/Web/Form/Validator/UrlValidator.php b/library/Icinga/Web/Form/Validator/UrlValidator.php
new file mode 100644
index 0000000..b1b578f
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/UrlValidator.php
@@ -0,0 +1,40 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use Zend_Validate_Abstract;
+
+/**
+ * Validator that checks whether a textfield doesn't contain raw double quotes
+ */
+class UrlValidator extends Zend_Validate_Abstract
+{
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ $this->_messageTemplates = array('HAS_QUOTES' => t(
+ 'The url must not contain raw double quotes. If you really need double quotes, use %22 instead.'
+ ));
+ }
+
+ /**
+ * Validate the input value
+ *
+ * @param string $value The string to validate
+ *
+ * @return bool true if and only if the input is valid, otherwise false
+ *
+ * @see Zend_Validate_Abstract::isValid()
+ */
+ public function isValid($value)
+ {
+ $hasQuotes = false === strpos($value, '"');
+ if (! $hasQuotes) {
+ $this->_error('HAS_QUOTES');
+ }
+ return $hasQuotes;
+ }
+}
diff --git a/library/Icinga/Web/Form/Validator/WritablePathValidator.php b/library/Icinga/Web/Form/Validator/WritablePathValidator.php
new file mode 100644
index 0000000..76efb58
--- /dev/null
+++ b/library/Icinga/Web/Form/Validator/WritablePathValidator.php
@@ -0,0 +1,72 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Form\Validator;
+
+use Zend_Validate_Abstract;
+
+/**
+ * Validator that interprets the value as a path and checks if it's writable
+ */
+class WritablePathValidator extends Zend_Validate_Abstract
+{
+ const NOT_WRITABLE = 'notWritable';
+ const DOES_NOT_EXIST = 'doesNotExist';
+
+ /**
+ * The messages to write on differen error states
+ *
+ * @var array
+ *
+ * @see Zend_Validate_Abstract::$_messageTemplates‚
+ */
+ protected $_messageTemplates = array(
+ self::NOT_WRITABLE => 'Path is not writable',
+ self::DOES_NOT_EXIST => 'Path does not exist'
+ );
+
+ /**
+ * When true, the file or directory must exist
+ *
+ * @var bool
+ */
+ private $requireExistence = false;
+
+ /**
+ * Set this validator to require the target file to exist
+ */
+ public function setRequireExistence()
+ {
+ $this->requireExistence = true;
+ }
+
+ /**
+ * Check whether the given value is writable path
+ *
+ * @param string $value The value submitted in the form
+ * @param mixed $context The context of the form
+ *
+ * @return bool True when validation worked, otherwise false
+ *
+ * @see Zend_Validate_Abstract::isValid()
+ */
+ public function isValid($value, $context = null)
+ {
+ $value = (string) $value;
+
+ $this->_setValue($value);
+ if ($this->requireExistence && !file_exists($value)) {
+ $this->_error(self::DOES_NOT_EXIST);
+ return false;
+ }
+
+ if ((file_exists($value) && is_writable($value)) ||
+ (is_dir(dirname($value)) && is_writable(dirname($value)))
+ ) {
+ return true;
+ }
+
+ $this->_error(self::NOT_WRITABLE);
+ return false;
+ }
+}
diff --git a/library/Icinga/Web/Helper/CookieHelper.php b/library/Icinga/Web/Helper/CookieHelper.php
new file mode 100644
index 0000000..cc7c448
--- /dev/null
+++ b/library/Icinga/Web/Helper/CookieHelper.php
@@ -0,0 +1,81 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Helper;
+
+use Icinga\Web\Request;
+
+/**
+ * Helper Class Cookie
+ */
+class CookieHelper
+{
+ /**
+ * The name of the control cookie
+ */
+ const CHECK_COOKIE = '_chc';
+
+ /**
+ * The request
+ *
+ * @var Request
+ */
+ protected $request;
+
+ /**
+ * Create a new cookie
+ *
+ * @param Request $request
+ */
+ public function __construct(Request $request)
+ {
+ $this->request = $request;
+ }
+
+ /**
+ * Check whether cookies are supported or not
+ *
+ * @return bool
+ */
+ public function isSupported()
+ {
+ if (! empty($_COOKIE)) {
+ $this->cleanupCheck();
+ return true;
+ }
+
+ $url = $this->request->getUrl();
+
+ if ($url->hasParam('_checkCookie') && empty($_COOKIE)) {
+ return false;
+ }
+
+ if (! $url->hasParam('_checkCookie')) {
+ $this->provideCheck();
+ }
+
+ return false;
+ }
+
+ /**
+ * Prepare check to detect cookie support
+ */
+ public function provideCheck()
+ {
+ setcookie(self::CHECK_COOKIE, '1');
+
+ $requestUri = $this->request->getUrl()->addParams(array('_checkCookie' => 1));
+ $this->request->getResponse()->redirectAndExit($requestUri);
+ }
+
+ /**
+ * Cleanup the cookie support check
+ */
+ public function cleanupCheck()
+ {
+ if ($this->request->getUrl()->hasParam('_checkCookie') && isset($_COOKIE[self::CHECK_COOKIE])) {
+ $requestUri =$this->request->getUrl()->without('_checkCookie');
+ $this->request->getResponse()->redirectAndExit($requestUri);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Helper/HtmlPurifier.php b/library/Icinga/Web/Helper/HtmlPurifier.php
new file mode 100644
index 0000000..19fd207
--- /dev/null
+++ b/library/Icinga/Web/Helper/HtmlPurifier.php
@@ -0,0 +1,95 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Helper;
+
+use Closure;
+use Icinga\Web\FileCache;
+use InvalidArgumentException;
+
+class HtmlPurifier
+{
+ /**
+ * The actual purifier instance
+ *
+ * @var \HTMLPurifier
+ */
+ protected $purifier;
+
+ /**
+ * Create a new HtmlPurifier
+ *
+ * @param array|Closure $config Additional configuration
+ */
+ public function __construct($config = null)
+ {
+ $purifierConfig = \HTMLPurifier_Config::createDefault();
+ $purifierConfig->set('Core.EscapeNonASCIICharacters', true);
+ $purifierConfig->set('Attr.AllowedFrameTargets', array('_blank'));
+
+ if (($cachePath = FileCache::instance()->directory('htmlpurifier.cache')) !== false) {
+ $purifierConfig->set('Cache.SerializerPath', $cachePath);
+ } else {
+ $purifierConfig->set('Cache.DefinitionImpl', null);
+ }
+
+ // This avoids permission problems:
+ // $purifierConfig->set('Core.DefinitionCache', null);
+
+ // $purifierConfig->set('URI.Base', 'http://www.example.com');
+ // $purifierConfig->set('URI.MakeAbsolute', true);
+
+ $this->configure($purifierConfig);
+
+ if ($config instanceof Closure) {
+ call_user_func($config, $purifierConfig);
+ } elseif (is_array($config)) {
+ $purifierConfig->loadArray($config);
+ } elseif ($config !== null) {
+ throw new InvalidArgumentException('$config must be either a Closure or array');
+ }
+
+ $this->purifier = new \HTMLPurifier($purifierConfig);
+ }
+
+ /**
+ * Apply additional default configuration
+ *
+ * May be overwritten by more concrete purifier implementations.
+ *
+ * @param \HTMLPurifier_Config $config
+ */
+ protected function configure($config)
+ {
+ }
+
+ /**
+ * Purify and return the given HTML string
+ *
+ * @param string $html
+ * @param array|Closure $config Configuration to use instead of the default
+ *
+ * @return string
+ */
+ public function purify($html, $config = null)
+ {
+ return $this->purifier->purify($html, $config);
+ }
+
+ /**
+ * Purify and return the given HTML string
+ *
+ * Convenience method to bypass object creation.
+ *
+ * @param string $html
+ * @param array|Closure $config Additional configuration
+ *
+ * @return string
+ */
+ public static function process($html, $config = null)
+ {
+ $purifier = new static($config);
+
+ return $purifier->purify($html);
+ }
+}
diff --git a/library/Icinga/Web/Helper/Markdown.php b/library/Icinga/Web/Helper/Markdown.php
new file mode 100644
index 0000000..cb854b4
--- /dev/null
+++ b/library/Icinga/Web/Helper/Markdown.php
@@ -0,0 +1,34 @@
+<?php
+/* Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\Helper;
+
+use Icinga\Web\Helper\Markdown\LinkTransformer;
+use Parsedown;
+
+class Markdown
+{
+ public static function line($content, $config = null)
+ {
+ if ($config === null) {
+ $config = function (\HTMLPurifier_Config $config) {
+ $config->set('HTML.Parent', 'span'); // Only allow inline elements
+
+ LinkTransformer::attachTo($config);
+ };
+ }
+
+ return HtmlPurifier::process(Parsedown::instance()->line($content), $config);
+ }
+
+ public static function text($content, $config = null)
+ {
+ if ($config === null) {
+ $config = function (\HTMLPurifier_Config $config) {
+ LinkTransformer::attachTo($config);
+ };
+ }
+
+ return HtmlPurifier::process(Parsedown::instance()->text($content), $config);
+ }
+}
diff --git a/library/Icinga/Web/Helper/Markdown/LinkTransformer.php b/library/Icinga/Web/Helper/Markdown/LinkTransformer.php
new file mode 100644
index 0000000..f323085
--- /dev/null
+++ b/library/Icinga/Web/Helper/Markdown/LinkTransformer.php
@@ -0,0 +1,73 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\Helper\Markdown;
+
+use HTMLPurifier_AttrTransform;
+use HTMLPurifier_Config;
+use ipl\Web\Url;
+
+class LinkTransformer extends HTMLPurifier_AttrTransform
+{
+ /**
+ * Link targets that are considered to have a thumbnail
+ *
+ * @var string[]
+ */
+ public static $IMAGE_FILES = [
+ 'jpg',
+ 'jpeg',
+ 'png',
+ 'bmp',
+ 'gif',
+ 'heif',
+ 'heic',
+ 'webp'
+ ];
+
+ public function transform($attr, $config, $context)
+ {
+ if (! isset($attr['href'])) {
+ return $attr;
+ }
+
+ $url = Url::fromPath($attr['href']);
+ $fileName = basename($url->getPath());
+
+ $ext = null;
+ if (($extAt = strrpos($fileName, '.')) !== false) {
+ $ext = substr($fileName, $extAt + 1);
+ }
+
+ $hasThumbnail = $ext !== null && in_array($ext, static::$IMAGE_FILES, true);
+ if ($hasThumbnail) {
+ // I would have liked to not only base this off of the extension, but also by
+ // whether there is an actual img tag inside the anchor. Seems not possible :(
+ $attr['class'] = 'with-thumbnail';
+ }
+
+ if (! isset($attr['target'])) {
+ if ($url->isExternal()) {
+ $attr['target'] = '_blank';
+ } else {
+ $attr['data-base-target'] = '_next';
+ }
+ }
+
+ return $attr;
+ }
+
+ public static function attachTo(HTMLPurifier_Config $config)
+ {
+ $module = $config->getHTMLDefinition(true)
+ ->getAnonymousModule();
+
+ if (isset($module->info['a'])) {
+ $a = $module->info['a'];
+ } else {
+ $a = $module->addBlankElement('a');
+ }
+
+ $a->attr_transform_post[] = new self();
+ }
+}
diff --git a/library/Icinga/Web/Hook.php b/library/Icinga/Web/Hook.php
new file mode 100644
index 0000000..b098518
--- /dev/null
+++ b/library/Icinga/Web/Hook.php
@@ -0,0 +1,16 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Application\Hook as NewHookImplementation;
+
+/**
+ * Icinga Web Hook registry
+ *
+ * @deprecated It is highly recommended to use {@see Icinga\Application\Hook} instead. Though since this message
+ * (or rather the previous message) hasn't been visible for ages... This won't be removed anyway....
+ */
+class Hook extends NewHookImplementation
+{
+}
diff --git a/library/Icinga/Web/JavaScript.php b/library/Icinga/Web/JavaScript.php
new file mode 100644
index 0000000..1865136
--- /dev/null
+++ b/library/Icinga/Web/JavaScript.php
@@ -0,0 +1,269 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Exception\Json\JsonDecodeException;
+use Icinga\Util\Json;
+use JShrink\Minifier;
+
+class JavaScript
+{
+ /** @var string */
+ const DEFINE_RE =
+ '/(?<!\.)define\(\s*([\'"][^\'"]*[\'"])?[,\s]*(\[[^]]*\])?[,\s]*((?>function\s*\([^)]*\)|[^=]*=>|\w+).*)/';
+
+ protected static $jsFiles = [
+ 'js/helpers.js',
+ 'js/icinga.js',
+ 'js/icinga/logger.js',
+ 'js/icinga/storage.js',
+ 'js/icinga/utils.js',
+ 'js/icinga/ui.js',
+ 'js/icinga/timer.js',
+ 'js/icinga/loader.js',
+ 'js/icinga/eventlistener.js',
+ 'js/icinga/events.js',
+ 'js/icinga/history.js',
+ 'js/icinga/module.js',
+ 'js/icinga/timezone.js',
+ 'js/icinga/behavior/application-state.js',
+ 'js/icinga/behavior/autofocus.js',
+ 'js/icinga/behavior/collapsible.js',
+ 'js/icinga/behavior/detach.js',
+ 'js/icinga/behavior/dropdown.js',
+ 'js/icinga/behavior/navigation.js',
+ 'js/icinga/behavior/form.js',
+ 'js/icinga/behavior/actiontable.js',
+ 'js/icinga/behavior/flyover.js',
+ 'js/icinga/behavior/filtereditor.js',
+ 'js/icinga/behavior/selectable.js',
+ 'js/icinga/behavior/modal.js',
+ 'js/icinga/behavior/input-enrichment.js',
+ 'js/icinga/behavior/datetime-picker.js',
+ 'js/icinga/behavior/copy-to-clipboard.js'
+ ];
+
+ protected static $vendorFiles = [];
+
+ protected static $baseFiles = [
+ 'js/define.js'
+ ];
+
+ public static function sendMinified()
+ {
+ self::send(true);
+ }
+
+ /**
+ * Send the client side script code to the client
+ *
+ * Does not cache the client side script code if the HTTP header Cache-Control or Pragma is set to no-cache.
+ *
+ * @param bool $minified Whether to compress the client side script code
+ */
+ public static function send($minified = false)
+ {
+ header('Content-Type: application/javascript');
+ $basedir = Icinga::app()->getBootstrapDirectory();
+ $moduleManager = Icinga::app()->getModuleManager();
+
+ $files = [];
+ $js = $out = '';
+ $min = $minified ? '.min' : '';
+
+ // Prepare vendor file list
+ $vendorFiles = [];
+ foreach (self::$vendorFiles as $file) {
+ $filePath = $basedir . '/' . $file . $min . '.js';
+ $vendorFiles[] = $filePath;
+ $files[] = $filePath;
+ }
+
+ // Prepare base file list
+ $baseFiles = [];
+ foreach (self::$baseFiles as $file) {
+ $filePath = $basedir . '/' . $file;
+ $baseFiles[] = $filePath;
+ $files[] = $filePath;
+ }
+
+ // Prepare library file list
+ foreach (Icinga::app()->getLibraries() as $library) {
+ $files = array_merge($files, $library->getJsAssets());
+ }
+
+ // Prepare core file list
+ $coreFiles = [];
+ foreach (self::$jsFiles as $file) {
+ $filePath = $basedir . '/' . $file;
+ $coreFiles[] = $filePath;
+ $files[] = $filePath;
+ }
+
+ $moduleFiles = [];
+ foreach ($moduleManager->getLoadedModules() as $name => $module) {
+ if ($module->hasJs()) {
+ $jsDir = $module->getJsDir();
+ foreach ($module->getJsFiles() as $path) {
+ if (file_exists($path)) {
+ $moduleFiles[$name][$jsDir][] = $path;
+ $files[] = $path;
+ }
+ }
+ }
+ }
+
+ $request = Icinga::app()->getRequest();
+ $noCache = $request->getHeader('Cache-Control') === 'no-cache' || $request->getHeader('Pragma') === 'no-cache';
+
+ header('Cache-Control: public,no-cache,must-revalidate');
+
+ if (! $noCache && FileCache::etagMatchesFiles($files)) {
+ header("HTTP/1.1 304 Not Modified");
+ return;
+ } else {
+ $etag = FileCache::etagForFiles($files);
+ }
+
+ header('ETag: "' . $etag . '"');
+ header('Content-Type: application/javascript');
+
+ $cacheFile = 'icinga-' . $etag . $min . '.js';
+ $cache = FileCache::instance();
+ if (! $noCache && $cache->has($cacheFile)) {
+ $cache->send($cacheFile);
+ return;
+ }
+
+ // We do not minify vendor files
+ foreach ($vendorFiles as $file) {
+ $out .= ';' . ltrim(trim(file_get_contents($file)), ';') . "\n";
+ }
+
+ $baseJs = '';
+ foreach ($baseFiles as $file) {
+ $baseJs .= file_get_contents($file) . "\n\n\n";
+ }
+
+ // Library files need to be namespaced first before they can be included
+ foreach (Icinga::app()->getLibraries() as $library) {
+ foreach ($library->getJsAssets() as $file) {
+ $alreadyMinified = false;
+ if ($minified && file_exists(($minFile = substr($file, 0, -3) . '.min.js'))) {
+ $alreadyMinified = true;
+ $file = $minFile;
+ }
+
+ $content = self::optimizeDefine(
+ file_get_contents($file),
+ $file,
+ $library->getJsAssetPath(),
+ $library->getName()
+ );
+
+ if ($alreadyMinified) {
+ $out .= ';' . ltrim(trim($content), ';') . "\n";
+ } else {
+ $js .= $content . "\n\n\n";
+ }
+ }
+ }
+
+ foreach ($coreFiles as $file) {
+ $js .= file_get_contents($file) . "\n\n\n";
+ }
+
+ foreach ($moduleFiles as $name => $paths) {
+ foreach ($paths as $basePath => $filePaths) {
+ foreach ($filePaths as $file) {
+ $content = self::optimizeDefine(file_get_contents($file), $file, $basePath, $name);
+ if (substr($file, -7, 7) === '.min.js') {
+ $out .= ';' . ltrim(trim($content), ';') . "\n";
+ } else {
+ $js .= $content . "\n\n\n";
+ }
+ }
+ }
+ }
+
+ if ($minified) {
+ $out .= Minifier::minify($js, ['flaggedComments' => false]);
+ $baseOut = Minifier::minify($baseJs, ['flaggedComments' => false]);
+ $out = ';' . ltrim($baseOut, ';') . "\n" . $out;
+ } else {
+ $out = $baseJs . $out . $js;
+ }
+
+ $cache->store($cacheFile, $out);
+ echo $out;
+ }
+
+ /**
+ * Optimize define() calls in the given JS
+ *
+ * @param string $js
+ * @param string $filePath
+ * @param string $basePath
+ * @param string $packageName
+ *
+ * @return string
+ */
+ public static function optimizeDefine($js, $filePath, $basePath, $packageName)
+ {
+ if (! preg_match(self::DEFINE_RE, $js, $match) || strpos($js, 'define.amd') !== false) {
+ return $js;
+ }
+
+ try {
+ $assetName = $match[1] ? Json::decode($match[1]) : '';
+ if (! $assetName) {
+ $assetName = explode('.', basename($filePath))[0];
+ }
+
+ $assetName = join(DIRECTORY_SEPARATOR, array_filter([
+ $packageName,
+ ltrim(substr(dirname($filePath), strlen($basePath)), DIRECTORY_SEPARATOR),
+ $assetName
+ ]));
+
+ $assetName = Json::encode($assetName, JSON_UNESCAPED_SLASHES);
+ } catch (JsonDecodeException $_) {
+ $assetName = $match[1];
+ Logger::debug('Can\'t optimize name of "%s". Are single quotes used instead of double quotes?', $filePath);
+ }
+
+ try {
+ $dependencies = $match[2] ? Json::decode($match[2]) : [];
+ foreach ($dependencies as &$dependencyName) {
+ if ($dependencyName === 'exports') {
+ // exports is a special keyword and doesn't need optimization
+ continue;
+ }
+
+ if (preg_match('~^((?:\.\.?/)+)*(.*)~', $dependencyName, $natch)) {
+ $dependencyName = join(DIRECTORY_SEPARATOR, array_filter([
+ $packageName,
+ ltrim(substr(
+ realpath(join(DIRECTORY_SEPARATOR, [dirname($filePath), $natch[1]])),
+ strlen(realpath($basePath))
+ ), DIRECTORY_SEPARATOR),
+ $natch[2]
+ ]));
+ }
+ }
+
+ $dependencies = Json::encode($dependencies, JSON_UNESCAPED_SLASHES);
+ } catch (JsonDecodeException $_) {
+ $dependencies = $match[2];
+ Logger::debug(
+ 'Can\'t optimize dependencies of "%s". Are single quotes used instead of double quotes?',
+ $filePath
+ );
+ }
+
+ return str_replace($match[0], sprintf("define(%s, %s, %s", $assetName, $dependencies, $match[3]), $js);
+ }
+}
diff --git a/library/Icinga/Web/LessCompiler.php b/library/Icinga/Web/LessCompiler.php
new file mode 100644
index 0000000..d7eda09
--- /dev/null
+++ b/library/Icinga/Web/LessCompiler.php
@@ -0,0 +1,255 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Application\Logger;
+use Icinga\Util\LessParser;
+use Less_Exception_Parser;
+
+/**
+ * Compile LESS into CSS
+ *
+ * Comments will be removed always. lessc is messing them up.
+ */
+class LessCompiler
+{
+ /**
+ * lessphp compiler
+ *
+ * @var LessParser
+ */
+ protected $lessc;
+
+ /**
+ * Array of LESS files
+ *
+ * @var string[]
+ */
+ protected $lessFiles = array();
+
+ /**
+ * Array of module LESS files indexed by module names
+ *
+ * @var array[]
+ */
+ protected $moduleLessFiles = array();
+
+ /**
+ * LESS source
+ *
+ * @var string
+ */
+ protected $source;
+
+ /**
+ * Path of the LESS theme
+ *
+ * @var string
+ */
+ protected $theme;
+
+ /**
+ * Path of the LESS theme mode
+ *
+ * @var string
+ */
+ protected $themeMode;
+
+ /**
+ * Create a new LESS compiler
+ */
+ public function __construct()
+ {
+ $this->lessc = new LessParser();
+ }
+
+ /**
+ * Add a Web 2 LESS file
+ *
+ * @param string $lessFile Path to the LESS file
+ *
+ * @return $this
+ */
+ public function addLessFile($lessFile)
+ {
+ $this->lessFiles[] = realpath($lessFile);
+ return $this;
+ }
+
+ /**
+ * Add a module LESS file
+ *
+ * @param string $moduleName Name of the module
+ * @param string $lessFile Path to the LESS file
+ *
+ * @return $this
+ */
+ public function addModuleLessFile($moduleName, $lessFile)
+ {
+ if (! isset($this->moduleLessFiles[$moduleName])) {
+ $this->moduleLessFiles[$moduleName] = array();
+ }
+ $this->moduleLessFiles[$moduleName][] = realpath($lessFile);
+ return $this;
+ }
+
+ /**
+ * Get the list of LESS files added to the compiler
+ *
+ * @return string[]
+ */
+ public function getLessFiles()
+ {
+ $lessFiles = $this->lessFiles;
+
+ foreach ($this->moduleLessFiles as $moduleLessFiles) {
+ $lessFiles = array_merge($lessFiles, $moduleLessFiles);
+ }
+
+ if ($this->theme !== null) {
+ $lessFiles[] = $this->theme;
+ }
+
+ if ($this->themeMode !== null) {
+ $lessFiles[] = $this->themeMode;
+ }
+
+ return $lessFiles;
+ }
+
+ /**
+ * Set the path to the LESS theme
+ *
+ * @param ?string $theme Path to the LESS theme
+ *
+ * @return $this
+ */
+ public function setTheme($theme)
+ {
+ if ($theme === null || (is_file($theme) && is_readable($theme))) {
+ $this->theme = $theme;
+ } else {
+ Logger::error('Can\t load theme %s. Make sure that the theme exists and is readable', $theme);
+ }
+ return $this;
+ }
+
+ /**
+ * Set the path to the LESS theme mode
+ *
+ * @param string $themeMode Path to the LESS theme mode
+ *
+ * @return $this
+ */
+ public function setThemeMode($themeMode)
+ {
+ if (is_file($themeMode) && is_readable($themeMode)) {
+ $this->themeMode = $themeMode;
+ } else {
+ Logger::error('Can\t load theme mode %s. Make sure that the theme mode exists and is readable', $themeMode);
+ }
+ return $this;
+ }
+
+ /**
+ * Instruct the compiler to minify CSS
+ *
+ * @return $this
+ */
+ public function compress()
+ {
+ $this->lessc->setFormatter('compressed');
+ return $this;
+ }
+
+ /**
+ * Render to CSS
+ *
+ * @return string
+ */
+ public function render()
+ {
+ foreach ($this->lessFiles as $lessFile) {
+ $this->source .= file_get_contents($lessFile);
+ }
+
+ $moduleCss = '';
+ $exportedVars = [];
+ foreach ($this->moduleLessFiles as $moduleName => $moduleLessFiles) {
+ $moduleCss .= '.icinga-module.module-' . $moduleName . ' {';
+
+ foreach ($moduleLessFiles as $moduleLessFile) {
+ $content = file_get_contents($moduleLessFile);
+
+ $pattern = '/^@exports:\s*{((?:\s*@[^:}]+:[^;]*;\s+)+)};$/m';
+ if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $match) {
+ $content = str_replace($match[0], '', $content);
+ foreach (explode("\n", trim($match[1])) as $line) {
+ list($name, $value) = explode(':', $line, 2);
+ $exportedVars[trim($name)] = trim($value, ' ;');
+ }
+ }
+ }
+
+ $moduleCss .= $content;
+ }
+
+ $moduleCss .= '}';
+ }
+
+ $this->source .= $moduleCss;
+
+ $varExports = '';
+ foreach ($exportedVars as $name => $value) {
+ $varExports .= sprintf("%s: %s;\n", $name, $value);
+ }
+
+ // exported vars are injected at the beginning to avoid that they are
+ // able to override other variables, that's what themes are for
+ $this->source = $varExports . "\n\n" . $this->source;
+
+ if ($this->theme !== null) {
+ $this->source .= file_get_contents($this->theme);
+ }
+
+ if ($this->themeMode !== null) {
+ $this->source .= file_get_contents($this->themeMode);
+ }
+
+ try {
+ return preg_replace(
+ '/(\.icinga-module\.module-[^\s]+) (#layout\.[^\s]+)/m',
+ '\2 \1',
+ $this->lessc->compile($this->source)
+ );
+ } catch (Less_Exception_Parser $e) {
+ $excerpt = substr($this->source, $e->index - 500, 1000);
+
+ $lines = [];
+ $found = false;
+ $pos = $e->index - 500;
+ foreach (explode("\n", $excerpt) as $i => $line) {
+ if ($i === 0) {
+ $pos += strlen($line);
+ $lines[] = '.. ' . $line;
+ } else {
+ $pos += strlen($line) + 1;
+ $sep = ' ';
+ if (! $found && $pos > $e->index) {
+ $found = true;
+ $sep = '!! ';
+ }
+
+ $lines[] = $sep . $line;
+ }
+ }
+
+ $lines[] = '..';
+ $excerpt = join("\n", $lines);
+
+ return sprintf("%s\n%s\n\n\n%s", $e->getMessage(), $e->getTraceAsString(), $excerpt);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php
new file mode 100644
index 0000000..dc1cdc8
--- /dev/null
+++ b/library/Icinga/Web/Menu.php
@@ -0,0 +1,152 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Application\Logger;
+use Icinga\Authentication\Auth;
+use Icinga\Web\Navigation\Navigation;
+
+/**
+ * Main menu for Icinga Web 2
+ */
+class Menu extends Navigation
+{
+ /**
+ * Create the main menu
+ */
+ public function __construct()
+ {
+ $this->init();
+ $this->load('menu-item');
+ }
+
+ /**
+ * Setup the main menu
+ */
+ public function init()
+ {
+ $this->addItem('dashboard', [
+ 'label' => t('Dashboard'),
+ 'url' => 'dashboard',
+ 'icon' => 'dashboard',
+ 'priority' => 10
+ ]);
+ $this->addItem('system', [
+ 'cssClass' => 'system-nav-item',
+ 'label' => t('System'),
+ 'icon' => 'services',
+ 'priority' => 700,
+ 'renderer' => [
+ 'SummaryNavigationItemRenderer',
+ 'state' => 'critical'
+ ],
+ 'children' => [
+ 'about' => [
+ 'icon' => 'info',
+ 'description' => t('Open about page'),
+ 'label' => t('About'),
+ 'url' => 'about',
+ 'priority' => 700
+ ],
+ 'health' => [
+ 'icon' => 'eye',
+ 'description' => t('Open health overview'),
+ 'label' => t('Health'),
+ 'url' => 'health',
+ 'priority' => 710,
+ 'renderer' => 'HealthNavigationRenderer'
+ ],
+ 'announcements' => [
+ 'icon' => 'megaphone',
+ 'description' => t('List announcements'),
+ 'label' => t('Announcements'),
+ 'url' => 'announcements',
+ 'priority' => 720
+ ],
+ 'sessions' => [
+ 'icon' => 'host',
+ 'description' => t('List of users who stay logged in'),
+ 'label' => t('User Sessions'),
+ 'permission' => 'application/sessions',
+ 'url' => 'manage-user-devices',
+ 'priority' => 730
+ ]
+ ]
+ ]);
+ $this->addItem('configuration', [
+ 'cssClass' => 'configuration-nav-item',
+ 'label' => t('Configuration'),
+ 'icon' => 'wrench',
+ 'permission' => 'config/*',
+ 'priority' => 800,
+ 'children' => [
+ 'application' => [
+ 'icon' => 'wrench',
+ 'description' => t('Open application configuration'),
+ 'label' => t('Application'),
+ 'url' => 'config',
+ 'priority' => 810
+ ],
+ 'authentication' => [
+ 'icon' => 'users',
+ 'description' => t('Open access control configuration'),
+ 'label' => t('Access Control'),
+ 'permission' => 'config/access-control/*',
+ 'priority' => 830,
+ 'url' => 'role'
+ ],
+ 'navigation' => [
+ 'icon' => 'sitemap',
+ 'description' => t('Open shared navigation configuration'),
+ 'label' => t('Shared Navigation'),
+ 'url' => 'navigation/shared',
+ 'permission' => 'config/navigation',
+ 'priority' => 840,
+ ],
+ 'modules' => [
+ 'icon' => 'cubes',
+ 'description' => t('Open module configuration'),
+ 'label' => t('Modules'),
+ 'url' => 'config/modules',
+ 'permission' => 'config/modules',
+ 'priority' => 890
+ ]
+ ]
+ ]);
+ $this->addItem('user', [
+ 'cssClass' => 'user-nav-item',
+ 'label' => Auth::getInstance()->getUser()->getUsername(),
+ 'icon' => 'user',
+ 'priority' => 900,
+ 'children' => [
+ 'account' => [
+ 'icon' => 'sliders',
+ 'description' => t('Open your account preferences'),
+ 'label' => t('My Account'),
+ 'priority' => 100,
+ 'url' => 'account'
+ ],
+ 'logout' => [
+ 'icon' => 'off',
+ 'description' => t('Log out'),
+ 'label' => t('Logout'),
+ 'priority' => 200,
+ 'attributes' => ['target' => '_self'],
+ 'url' => 'authentication/logout'
+ ]
+ ]
+ ]);
+
+ if (Logger::writesToFile()) {
+ $this->getItem('system')->addChild($this->createItem('application_log', [
+ 'icon' => 'doc-text',
+ 'description' => t('Open Application Log'),
+ 'label' => t('Application Log'),
+ 'url' => 'list/applicationlog',
+ 'permission' => 'application/log',
+ 'priority' => 900
+ ]));
+ }
+ }
+}
diff --git a/library/Icinga/Web/Navigation/ConfigMenu.php b/library/Icinga/Web/Navigation/ConfigMenu.php
new file mode 100644
index 0000000..583bf42
--- /dev/null
+++ b/library/Icinga/Web/Navigation/ConfigMenu.php
@@ -0,0 +1,327 @@
+<?php
+/* Icinga Web 2 | (c) 2022 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\Navigation;
+
+use Icinga\Application\Hook\HealthHook;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Application\MigrationManager;
+use Icinga\Authentication\Auth;
+use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer;
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\Web\Url;
+use ipl\Web\Widget\Icon;
+use ipl\Web\Widget\StateBadge;
+use Throwable;
+
+class ConfigMenu extends BaseHtmlElement
+{
+ const STATE_OK = 'ok';
+ const STATE_CRITICAL = 'critical';
+ const STATE_WARNING = 'warning';
+ const STATE_PENDING = 'pending';
+ const STATE_UNKNOWN = 'unknown';
+
+ protected $tag = 'ul';
+
+ protected $defaultAttributes = ['class' => 'nav'];
+
+ protected $children;
+
+ protected $selected;
+
+ protected $state;
+
+ public function __construct()
+ {
+ $this->children = [
+ 'system' => [
+ 'title' => t('System'),
+ 'items' => [
+ 'about' => [
+ 'label' => t('About'),
+ 'url' => 'about'
+ ],
+ 'health' => [
+ 'label' => t('Health'),
+ 'url' => 'health',
+ ],
+ 'migrations' => [
+ 'label' => t('Migrations'),
+ 'url' => 'migrations',
+ ],
+ 'announcements' => [
+ 'label' => t('Announcements'),
+ 'url' => 'announcements'
+ ],
+ 'sessions' => [
+ 'label' => t('User Sessions'),
+ 'permission' => 'application/sessions',
+ 'url' => 'manage-user-devices'
+ ]
+ ]
+ ],
+ 'configuration' => [
+ 'title' => t('Configuration'),
+ 'permission' => 'config/*',
+ 'items' => [
+ 'application' => [
+ 'label' => t('Application'),
+ 'url' => 'config/general'
+ ],
+ 'authentication' => [
+ 'label' => t('Access Control'),
+ 'permission' => 'config/access-control/*',
+ 'url' => 'role/list'
+ ],
+ 'navigation' => [
+ 'label' => t('Shared Navigation'),
+ 'permission' => 'config/navigation',
+ 'url' => 'navigation/shared'
+ ],
+ 'modules' => [
+ 'label' => t('Modules'),
+ 'permission' => 'config/modules',
+ 'url' => 'config/modules'
+ ]
+ ]
+ ],
+ 'logout' => [
+ 'items' => [
+ 'logout' => [
+ 'label' => t('Logout'),
+ 'atts' => [
+ 'target' => '_self',
+ 'class' => 'nav-item-logout'
+ ],
+ 'url' => 'authentication/logout'
+ ]
+ ]
+ ]
+ ];
+
+ if (Logger::writesToFile()) {
+ $this->children['system']['items']['application_log'] = [
+ 'label' => t('Application Log'),
+ 'url' => 'list/applicationlog',
+ 'permission' => 'application/log'
+ ];
+ }
+ }
+
+ protected function assembleUserMenuItem(BaseHtmlElement $userMenuItem)
+ {
+ $username = Auth::getInstance()->getUser()->getUsername();
+
+ $userMenuItem->add(
+ new HtmlElement(
+ 'a',
+ Attributes::create(['href' => Url::fromPath('account')]),
+ new HtmlElement(
+ 'i',
+ Attributes::create(['class' => 'user-ball']),
+ Text::create($username[0])
+ ),
+ Text::create($username)
+ )
+ );
+
+ if (Icinga::app()->getRequest()->getUrl()->matches('account')) {
+ $userMenuItem->addAttributes(['class' => 'selected active']);
+ }
+ }
+
+ protected function assembleCogMenuItem($cogMenuItem)
+ {
+ $cogMenuItem->add([
+ HtmlElement::create(
+ 'button',
+ null,
+ [
+ new Icon('cog'),
+ $this->createHealthBadge() ?? $this->createMigrationBadge(),
+ ]
+ ),
+ $this->createLevel2Menu()
+ ]);
+ }
+
+ protected function assembleLevel2Nav(BaseHtmlElement $level2Nav)
+ {
+ $navContent = HtmlElement::create('div', ['class' => 'flyout-content']);
+ foreach ($this->children as $c) {
+ if (isset($c['permission']) && ! Auth::getInstance()->hasPermission($c['permission'])) {
+ continue;
+ }
+
+ if (isset($c['title'])) {
+ $navContent->add(HtmlElement::create(
+ 'h3',
+ null,
+ $c['title']
+ ));
+ }
+
+ $ul = HtmlElement::create('ul', ['class' => 'nav']);
+ foreach ($c['items'] as $key => $item) {
+ $ul->add($this->createLevel2MenuItem($item, $key));
+ }
+
+ $navContent->add($ul);
+ }
+
+ $level2Nav->add($navContent);
+ }
+
+ protected function getHealthCount()
+ {
+ $count = 0;
+ $worstState = null;
+ foreach (HealthHook::collectHealthData()->select() as $result) {
+ if ($worstState === null || $result->state > $worstState) {
+ $worstState = $result->state;
+ $count = 1;
+ } elseif ($worstState === $result->state) {
+ $count++;
+ }
+ }
+
+ switch ($worstState) {
+ case HealthHook::STATE_OK:
+ $count = 0;
+ break;
+ case HealthHook::STATE_WARNING:
+ $this->state = self::STATE_WARNING;
+ break;
+ case HealthHook::STATE_CRITICAL:
+ $this->state = self::STATE_CRITICAL;
+ break;
+ case HealthHook::STATE_UNKNOWN:
+ $this->state = self::STATE_UNKNOWN;
+ break;
+ }
+
+ return $count;
+ }
+
+ protected function isSelectedItem($item)
+ {
+ if ($item !== null && Icinga::app()->getRequest()->getUrl()->matches($item['url'])) {
+ $this->selected = $item;
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function createHealthBadge(): ?StateBadge
+ {
+ $stateBadge = null;
+ if ($this->getHealthCount() > 0) {
+ $stateBadge = new StateBadge($this->getHealthCount(), $this->state);
+ $stateBadge->addAttributes(['class' => 'disabled']);
+ }
+
+ return $stateBadge;
+ }
+
+ protected function createMigrationBadge(): ?StateBadge
+ {
+ try {
+ $mm = MigrationManager::instance();
+ $count = $mm->count();
+ } catch (Throwable $e) {
+ Logger::error('Failed to load pending migrations: %s', $e);
+ $count = 0;
+ }
+
+ $stateBadge = null;
+ if ($count > 0) {
+ $stateBadge = new StateBadge($count, BadgeNavigationItemRenderer::STATE_PENDING);
+ $stateBadge->addAttributes(['class' => 'disabled']);
+ }
+
+ return $stateBadge;
+ }
+
+ protected function createLevel2Menu()
+ {
+ $level2Nav = HtmlElement::create(
+ 'div',
+ Attributes::create(['class' => 'nav-level-1 flyout'])
+ );
+
+ $this->assembleLevel2Nav($level2Nav);
+
+ return $level2Nav;
+ }
+
+ protected function createLevel2MenuItem($item, $key)
+ {
+ if (isset($item['permission']) && ! Auth::getInstance()->hasPermission($item['permission'])) {
+ return null;
+ }
+
+ $stateBadge = null;
+ $class = null;
+ if ($key === 'health') {
+ $class = 'badge-nav-item';
+ $stateBadge = $this->createHealthBadge();
+ } elseif ($key === 'migrations') {
+ $class = 'badge-nav-item';
+ $stateBadge = $this->createMigrationBadge();
+ }
+
+ $li = HtmlElement::create(
+ 'li',
+ $item['atts'] ?? [],
+ [
+ HtmlElement::create(
+ 'a',
+ Attributes::create(['href' => Url::fromPath($item['url'])]),
+ [
+ $item['label'],
+ $stateBadge ?? ''
+ ]
+ ),
+ ]
+ );
+ $li->addAttributes(['class' => $class]);
+
+ if ($this->isSelectedItem($item)) {
+ $li->addAttributes(['class' => 'selected']);
+ }
+
+ return $li;
+ }
+
+ protected function createUserMenuItem()
+ {
+ $userMenuItem = HtmlElement::create('li', ['class' => 'user-nav-item']);
+
+ $this->assembleUserMenuItem($userMenuItem);
+
+ return $userMenuItem;
+ }
+
+ protected function createCogMenuItem()
+ {
+ $cogMenuItem = HtmlElement::create('li', ['class' => 'config-nav-item']);
+
+ $this->assembleCogMenuItem($cogMenuItem);
+
+ return $cogMenuItem;
+ }
+
+ protected function assemble()
+ {
+ $this->add([
+ $this->createUserMenuItem(),
+ $this->createCogMenuItem()
+ ]);
+ }
+}
diff --git a/library/Icinga/Web/Navigation/DashboardPane.php b/library/Icinga/Web/Navigation/DashboardPane.php
new file mode 100644
index 0000000..71b3215
--- /dev/null
+++ b/library/Icinga/Web/Navigation/DashboardPane.php
@@ -0,0 +1,84 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation;
+
+use Icinga\Web\Url;
+
+/**
+ * A dashboard pane
+ */
+class DashboardPane extends NavigationItem
+{
+ /**
+ * This pane's dashlets
+ *
+ * @var array
+ */
+ protected $dashlets;
+
+ protected $disabled;
+
+ /**
+ * Set this pane's dashlets
+ *
+ * @param array $dashlets
+ *
+ * @return $this
+ */
+ public function setDashlets(array $dashlets)
+ {
+ $this->dashlets = $dashlets;
+ return $this;
+ }
+
+ /**
+ * Return this pane's dashlets
+ *
+ * @param bool $ordered Whether to order the dashlets first
+ *
+ * @return array
+ */
+ public function getDashlets($ordered = true)
+ {
+ if ($this->dashlets === null) {
+ return array();
+ }
+
+ if ($ordered) {
+ $dashlets = $this->dashlets;
+ ksort($dashlets);
+ return $dashlets;
+ }
+
+ return $this->dashlets;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setUrl(Url::fromPath('dashboard', array('pane' => $this->getName())));
+ }
+
+ /**
+ * Set disabled state for pane
+ *
+ * @param bool $disabled
+ */
+ public function setDisabled($disabled = true)
+ {
+ $this->disabled = (bool) $disabled;
+ }
+
+ /**
+ * Get disabled state for pane
+ *
+ * @return bool
+ */
+ public function getDisabled()
+ {
+ return $this->disabled;
+ }
+}
diff --git a/library/Icinga/Web/Navigation/DropdownItem.php b/library/Icinga/Web/Navigation/DropdownItem.php
new file mode 100644
index 0000000..2342b96
--- /dev/null
+++ b/library/Icinga/Web/Navigation/DropdownItem.php
@@ -0,0 +1,20 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation;
+
+/**
+ * Dropdown navigation item
+ *
+ * @see \Icinga\Web\Navigation\Navigation For a usage example.
+ */
+class DropdownItem extends NavigationItem
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->children->setLayout(Navigation::LAYOUT_DROPDOWN);
+ }
+}
diff --git a/library/Icinga/Web/Navigation/Navigation.php b/library/Icinga/Web/Navigation/Navigation.php
new file mode 100644
index 0000000..4343c3c
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Navigation.php
@@ -0,0 +1,572 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation;
+
+use ArrayAccess;
+use ArrayIterator;
+use Exception;
+use Countable;
+use InvalidArgumentException;
+use IteratorAggregate;
+use Traversable;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Authentication\Auth;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Util\StringHelper;
+use Icinga\Web\Navigation\Renderer\RecursiveNavigationRenderer;
+
+/**
+ * Container for navigation items
+ */
+class Navigation implements ArrayAccess, Countable, IteratorAggregate
+{
+ /**
+ * The class namespace where to locate navigation type classes
+ *
+ * @var string
+ */
+ const NAVIGATION_NS = 'Web\\Navigation';
+
+ /**
+ * Flag for dropdown layout
+ *
+ * @var int
+ */
+ const LAYOUT_DROPDOWN = 1;
+
+ /**
+ * Flag for tabs layout
+ *
+ * @var int
+ */
+ const LAYOUT_TABS = 2;
+
+ /**
+ * Known navigation types
+ *
+ * @var array
+ */
+ protected static $types;
+
+ /**
+ * This navigation's items
+ *
+ * @var NavigationItem[]
+ */
+ protected $items = array();
+
+ /**
+ * This navigation's layout
+ *
+ * @var int
+ */
+ protected $layout;
+
+ public function offsetExists($offset): bool
+ {
+ return isset($this->items[$offset]);
+ }
+
+ public function offsetGet($offset): ?NavigationItem
+ {
+ return $this->items[$offset] ?? null;
+ }
+
+ public function offsetSet($offset, $value): void
+ {
+ $this->items[$offset] = $value;
+ }
+
+ public function offsetUnset($offset): void
+ {
+ unset($this->items[$offset]);
+ }
+
+ public function count(): int
+ {
+ return count($this->items);
+ }
+
+ public function getIterator(): Traversable
+ {
+ $this->order();
+ return new ArrayIterator($this->items);
+ }
+
+ /**
+ * Create and return a new navigation item for the given configuration
+ *
+ * @param string $name
+ * @param array|ConfigObject $properties
+ *
+ * @return NavigationItem
+ *
+ * @throws InvalidArgumentException If the $properties argument is neither an array nor a ConfigObject
+ */
+ public function createItem($name, $properties)
+ {
+ if ($properties instanceof ConfigObject) {
+ $properties = $properties->toArray();
+ } elseif (! is_array($properties)) {
+ throw new InvalidArgumentException('Argument $properties must be of type array or ConfigObject');
+ }
+
+ $itemType = isset($properties['type']) ? StringHelper::cname($properties['type'], '-') : 'NavigationItem';
+ if (! empty(static::$types) && isset(static::$types[$itemType])) {
+ return new static::$types[$itemType]($name, $properties);
+ }
+
+ $item = null;
+ $classPath = null;
+ foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) {
+ $classPath = 'Icinga\\Module\\'
+ . ucfirst($module->getName())
+ . '\\'
+ . static::NAVIGATION_NS
+ . '\\'
+ . $itemType;
+ if (class_exists($classPath)) {
+ $item = new $classPath($name, $properties);
+ break;
+ }
+ }
+
+ if ($item === null) {
+ $classPath = 'Icinga\\' . static::NAVIGATION_NS . '\\' . $itemType;
+ if (class_exists($classPath)) {
+ $item = new $classPath($name, $properties);
+ }
+ }
+
+ if ($item === null) {
+ if ($itemType !== 'MenuItem') {
+ Logger::debug(
+ 'Failed to find custom navigation item class %s for item %s. Using base class NavigationItem now',
+ $itemType,
+ $name
+ );
+ }
+
+ $item = new NavigationItem($name, $properties);
+ static::$types[$itemType] = 'Icinga\\Web\\Navigation\\NavigationItem';
+ } elseif (! $item instanceof NavigationItem) {
+ throw new ProgrammingError('Class %s must inherit from NavigationItem', $classPath);
+ } else {
+ static::$types[$itemType] = $classPath;
+ }
+
+ return $item;
+ }
+
+ /**
+ * Add a navigation item
+ *
+ * If you do not pass an instance of NavigationItem, this will only add the item
+ * if it does not require a permission or the current user has the permission.
+ *
+ * @param string|NavigationItem $name The name of the item or an instance of NavigationItem
+ * @param array $properties The properties of the item to add (Ignored if $name is not a string)
+ *
+ * @return bool Whether the item was added or not
+ *
+ * @throws InvalidArgumentException In case $name is neither a string nor an instance of NavigationItem
+ */
+ public function addItem($name, array $properties = array())
+ {
+ if (is_string($name)) {
+ if (isset($properties['permission'])) {
+ if (! Auth::getInstance()->hasPermission($properties['permission'])) {
+ return false;
+ }
+
+ unset($properties['permission']);
+ }
+
+ $item = $this->createItem($name, $properties);
+ } elseif (! $name instanceof NavigationItem) {
+ throw new InvalidArgumentException('Argument $name must be of type string or NavigationItem');
+ } else {
+ $item = $name;
+ }
+
+ $this->items[$item->getName()] = $item;
+ return true;
+ }
+
+ /**
+ * Return the item with the given name
+ *
+ * @param string $name
+ * @param mixed $default
+ *
+ * @return NavigationItem|mixed
+ */
+ public function getItem($name, $default = null)
+ {
+ return isset($this->items[$name]) ? $this->items[$name] : $default;
+ }
+
+ /**
+ * Return the currently active item or the first one if none is active
+ *
+ * @return NavigationItem
+ */
+ public function getActiveItem()
+ {
+ foreach ($this->items as $item) {
+ if ($item->getActive()) {
+ return $item;
+ }
+ }
+
+ $firstItem = reset($this->items);
+ return $firstItem ? $firstItem->setActive() : null;
+ }
+
+ /**
+ * Return this navigation's items
+ *
+ * @return array
+ */
+ public function getItems()
+ {
+ return $this->items;
+ }
+
+ /**
+ * Return whether this navigation is empty
+ *
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ return empty($this->items);
+ }
+
+ /**
+ * Return whether this navigation has any renderable items
+ *
+ * @return bool
+ */
+ public function hasRenderableItems()
+ {
+ foreach ($this->getItems() as $item) {
+ if ($item->shouldRender()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Return this navigation's layout
+ *
+ * @return int
+ */
+ public function getLayout()
+ {
+ return $this->layout;
+ }
+
+ /**
+ * Set this navigation's layout
+ *
+ * @param int $layout
+ *
+ * @return $this
+ */
+ public function setLayout($layout)
+ {
+ $this->layout = (int) $layout;
+ return $this;
+ }
+
+ /**
+ * Create and return the renderer for this navigation
+ *
+ * @return RecursiveNavigationRenderer
+ */
+ public function getRenderer()
+ {
+ return new RecursiveNavigationRenderer($this);
+ }
+
+ /**
+ * Return this navigation rendered to HTML
+ *
+ * @return string
+ */
+ public function render()
+ {
+ return $this->getRenderer()->render();
+ }
+
+ /**
+ * Order this navigation's items
+ *
+ * @return $this
+ */
+ public function order()
+ {
+ uasort($this->items, array($this, 'compareItems'));
+ foreach ($this->items as $item) {
+ if ($item->hasChildren()) {
+ $item->getChildren()->order();
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return whether the first item is less than, more than or equal to the second one
+ *
+ * @param NavigationItem $a
+ * @param NavigationItem $b
+ *
+ * @return int
+ */
+ protected function compareItems(NavigationItem $a, NavigationItem $b)
+ {
+ if ($a->getPriority() === $b->getPriority()) {
+ return strcasecmp($a->getLabel(), $b->getLabel());
+ }
+
+ return $a->getPriority() > $b->getPriority() ? 1 : -1;
+ }
+
+ /**
+ * Try to find and return a item with the given or a similar name
+ *
+ * @param string $name
+ *
+ * @return ?NavigationItem
+ */
+ public function findItem($name)
+ {
+ $item = $this->getItem($name);
+ if ($item !== null) {
+ return $item;
+ }
+
+ $loweredName = strtolower($name);
+ foreach ($this->getItems() as $item) {
+ if (strtolower($item->getName()) === $loweredName) {
+ return $item;
+ }
+ }
+ }
+
+ /**
+ * Merge this navigation with the given one
+ *
+ * Any duplicate items of this navigation will be overwritten by the given navigation's items.
+ *
+ * @param Navigation $navigation
+ *
+ * @return $this
+ */
+ public function merge(Navigation $navigation)
+ {
+ foreach ($navigation as $item) {
+ /** @var $item NavigationItem */
+ if (($existingItem = $this->findItem($item->getName())) !== null) {
+ if ($existingItem->conflictsWith($item)) {
+ $name = $item->getName();
+ do {
+ if (preg_match('~_(\d+)$~', $name, $matches)) {
+ $name = preg_replace('~_\d+$~', (int) $matches[1] + 1, $name);
+ } else {
+ $name .= '_2';
+ }
+ } while ($this->getItem($name) !== null);
+
+ $this->addItem($item->setName($name));
+ } else {
+ $existingItem->merge($item);
+ }
+ } else {
+ $this->addItem($item);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Extend this navigation set with all additional items of the given type
+ *
+ * This will fetch navigation items from the following sources:
+ * * User Shareables
+ * * User Preferences
+ * * Modules
+ * Any existing entry will be overwritten by one that is coming later in order.
+ *
+ * @param string $type
+ *
+ * @return $this
+ */
+ public function load($type)
+ {
+ $user = Auth::getInstance()->getUser();
+ if ($type !== 'dashboard-pane') {
+ // Shareables
+ $this->merge(Icinga::app()->getSharedNavigation($type));
+
+ // User Preferences
+ $this->merge($user->getNavigation($type));
+ }
+
+ // Modules
+ $moduleManager = Icinga::app()->getModuleManager();
+ foreach ($moduleManager->getLoadedModules() as $module) {
+ if ($user->can($moduleManager::MODULE_PERMISSION_NS . $module->getName())) {
+ if ($type === 'menu-item') {
+ $this->merge($module->getMenu());
+ } elseif ($type === 'dashboard-pane') {
+ $this->merge($module->getDashboard());
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the global navigation item type configuration
+ *
+ * @return array
+ */
+ public static function getItemTypeConfiguration()
+ {
+ $defaultItemTypes = array(
+ 'menu-item' => array(
+ 'label' => t('Menu Entry'),
+ 'config' => 'menu'
+ )/*, // Disabled, until it is able to fully replace the old implementation
+ 'dashlet' => array(
+ 'label' => 'Dashlet',
+ 'config' => 'dashboard'
+ )*/
+ );
+
+ $moduleItemTypes = array();
+ $moduleManager = Icinga::app()->getModuleManager();
+ foreach ($moduleManager->getLoadedModules() as $module) {
+ if (Auth::getInstance()->hasPermission($moduleManager::MODULE_PERMISSION_NS . $module->getName())) {
+ foreach ($module->getNavigationItems() as $type => $options) {
+ if (! isset($moduleItemTypes[$type])) {
+ $moduleItemTypes[$type] = $options;
+ }
+ }
+ }
+ }
+
+ return array_merge($defaultItemTypes, $moduleItemTypes);
+ }
+
+ /**
+ * Create and return a new set of navigation items for the given configuration
+ *
+ * Note that this is supposed to be utilized for one dimensional structures
+ * only. Multi dimensional structures can be processed by fromArray().
+ *
+ * @param Traversable|array $config
+ *
+ * @return Navigation
+ *
+ * @throws InvalidArgumentException In case the given configuration is invalid
+ * @throws ConfigurationError In case a referenced parent does not exist
+ */
+ public static function fromConfig($config)
+ {
+ if (! is_array($config) && !$config instanceof Traversable) {
+ throw new InvalidArgumentException('Argument $config must be an array or a instance of Traversable');
+ }
+
+ $flattened = $orphans = $topLevel = array();
+ foreach ($config as $sectionName => $sectionConfig) {
+ $parentName = $sectionConfig->parent;
+ unset($sectionConfig->parent);
+
+ if (! $parentName) {
+ $topLevel[$sectionName] = $sectionConfig->toArray();
+ $flattened[$sectionName] = & $topLevel[$sectionName];
+ } elseif (isset($flattened[$parentName])) {
+ $flattened[$parentName]['children'][$sectionName] = $sectionConfig->toArray();
+ $flattened[$sectionName] = & $flattened[$parentName]['children'][$sectionName];
+ } else {
+ $orphans[$parentName][$sectionName] = $sectionConfig->toArray();
+ $flattened[$sectionName] = & $orphans[$parentName][$sectionName];
+ }
+ }
+
+ do {
+ $match = false;
+ foreach ($orphans as $parentName => $children) {
+ if (isset($flattened[$parentName])) {
+ if (isset($flattened[$parentName]['children'])) {
+ $flattened[$parentName]['children'] = array_merge(
+ $flattened[$parentName]['children'],
+ $children
+ );
+ } else {
+ $flattened[$parentName]['children'] = $children;
+ }
+
+ unset($orphans[$parentName]);
+ $match = true;
+ }
+ }
+ } while ($match && !empty($orphans));
+
+ if (! empty($orphans)) {
+ throw new ConfigurationError(
+ t(
+ 'Failed to fully parse navigation configuration. Ensure that'
+ . ' all referenced parents are existing navigation items: %s'
+ ),
+ join(', ', array_keys($orphans))
+ );
+ }
+
+ return static::fromArray($topLevel);
+ }
+
+ /**
+ * Create and return a new set of navigation items for the given array
+ *
+ * @param array $array
+ *
+ * @return Navigation
+ */
+ public static function fromArray(array $array)
+ {
+ $navigation = new static();
+ foreach ($array as $name => $properties) {
+ $navigation->addItem((string) $name, $properties);
+ }
+
+ return $navigation;
+ }
+
+ /**
+ * Return this navigation rendered to HTML
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ try {
+ return $this->render();
+ } catch (Exception $e) {
+ return IcingaException::describe($e);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Navigation/NavigationItem.php b/library/Icinga/Web/Navigation/NavigationItem.php
new file mode 100644
index 0000000..8aaf7b8
--- /dev/null
+++ b/library/Icinga/Web/Navigation/NavigationItem.php
@@ -0,0 +1,948 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation;
+
+use Exception;
+use Icinga\Authentication\Auth;
+use InvalidArgumentException;
+use IteratorAggregate;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Web\Navigation\Renderer\NavigationItemRenderer;
+use Icinga\Web\Url;
+use Traversable;
+
+/**
+ * A navigation item
+ */
+class NavigationItem implements IteratorAggregate
+{
+ /**
+ * Alternative markup element for items without a url
+ *
+ * @var string
+ */
+ const LINK_ALTERNATIVE = 'span';
+
+ /**
+ * The class namespace where to locate navigation type renderer classes
+ */
+ const RENDERER_NS = 'Web\\Navigation\\Renderer';
+
+ /**
+ * Whether this item is active
+ *
+ * @var bool
+ */
+ protected $active;
+
+ /**
+ * Whether this item is selected
+ *
+ * @var bool
+ */
+ protected $selected;
+
+ /**
+ * The CSS class used for the outer li element
+ *
+ * @var string
+ */
+ protected $cssClass;
+
+ /**
+ * This item's priority
+ *
+ * The priority defines when the item is rendered in relation to its parent's childs.
+ *
+ * @var int
+ */
+ protected $priority;
+
+ /**
+ * The attributes of this item's element
+ *
+ * @var array
+ */
+ protected $attributes;
+
+ /**
+ * This item's children
+ *
+ * @var Navigation
+ */
+ protected $children;
+
+ /**
+ * This item's icon
+ *
+ * @var string
+ */
+ protected $icon;
+
+ /**
+ * This item's name
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * This item's label
+ *
+ * @var string
+ */
+ protected $label;
+
+ /**
+ * The item's description
+ *
+ * @var string
+ */
+ protected $description;
+
+ /**
+ * This item's parent
+ *
+ * @var NavigationItem
+ */
+ protected $parent;
+
+ /**
+ * This item's url
+ *
+ * @var Url
+ */
+ protected $url;
+
+ /**
+ * This item's url target
+ *
+ * @var string
+ */
+ protected $target;
+
+ /**
+ * Additional parameters for this item's url
+ *
+ * @var array
+ */
+ protected $urlParameters;
+
+ /**
+ * This item's renderer
+ *
+ * @var NavigationItemRenderer
+ */
+ protected $renderer;
+
+ /**
+ * Whether to render this item
+ *
+ * @var bool
+ */
+ protected $render;
+
+ /**
+ * Create a new NavigationItem
+ *
+ * @param string $name
+ * @param array $properties
+ */
+ public function __construct($name, array $properties = null)
+ {
+ $this->setName($name);
+ $this->children = new Navigation();
+
+ if (! empty($properties)) {
+ $this->setProperties($properties);
+ }
+
+ $this->init();
+ }
+
+ /**
+ * Initialize this NavigationItem
+ */
+ public function init()
+ {
+ }
+
+ /**
+ * @return Navigation
+ */
+ public function getIterator(): Traversable
+ {
+ return $this->getChildren();
+ }
+
+ /**
+ * Return whether this item is active
+ *
+ * @return bool
+ */
+ public function getActive()
+ {
+ if ($this->active === null) {
+ $this->active = false;
+ if ($this->getUrl() !== null && Icinga::app()->getRequest()->getUrl()->matches($this->getUrl())) {
+ $this->setActive();
+ } elseif ($this->hasChildren()) {
+ foreach ($this->getChildren() as $item) {
+ /** @var NavigationItem $item */
+ if ($item->getActive()) {
+ // Do nothing, a true active state is automatically passed to all parents
+ }
+ }
+ }
+ }
+
+ return $this->active;
+ }
+
+ /**
+ * Set whether this item is active
+ *
+ * If it's active and has a parent, the parent gets activated as well.
+ *
+ * @param bool $active
+ *
+ * @return $this
+ */
+ public function setActive($active = true)
+ {
+ $this->active = (bool) $active;
+ if ($this->active && $this->getParent() !== null) {
+ $this->getParent()->setActive();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return whether this item is selected
+ *
+ * @return bool
+ */
+ public function getSelected()
+ {
+ if ($this->selected === null) {
+ $this->active = false;
+ if ($this->getUrl() !== null && Icinga::app()->getRequest()->getUrl()->matches($this->getUrl())) {
+ $this->setSelected();
+ }
+ }
+
+ return $this->selected;
+ }
+
+ /**
+ * Set whether this item is active
+ *
+ * If it's active and has a parent, the parent gets activated as well.
+ *
+ * @param bool $selected
+ *
+ * @return $this
+ */
+ public function setSelected($selected = true)
+ {
+ $this->selected = (bool) $selected;
+
+ return $this;
+ }
+
+ /**
+ * Get the CSS class used for the outer li element
+ *
+ * @return string
+ */
+ public function getCssClass()
+ {
+ return $this->cssClass;
+ }
+
+ /**
+ * Set the CSS class to use for the outer li element
+ *
+ * @param string $class
+ *
+ * @return $this
+ */
+ public function setCssClass($class)
+ {
+ $this->cssClass = (string) $class;
+ return $this;
+ }
+
+ /**
+ * Return this item's priority
+ *
+ * @return int
+ */
+ public function getPriority()
+ {
+ return $this->priority !== null ? $this->priority : 100;
+ }
+
+ /**
+ * Set this item's priority
+ *
+ * @param int $priority
+ *
+ * @return $this
+ */
+ public function setPriority($priority)
+ {
+ $this->priority = (int) $priority;
+ return $this;
+ }
+
+ /**
+ * Return the value of the given element attribute
+ *
+ * @param string $name
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public function getAttribute($name, $default = null)
+ {
+ $attributes = $this->getAttributes();
+ return array_key_exists($name, $attributes) ? $attributes[$name] : $default;
+ }
+
+ /**
+ * Set the value of the given element attribute
+ *
+ * @param string $name
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function setAttribute($name, $value)
+ {
+ $this->attributes[$name] = $value;
+ return $this;
+ }
+
+ /**
+ * Return the attributes of this item's element
+ *
+ * @return array
+ */
+ public function getAttributes()
+ {
+ return $this->attributes ?: array();
+ }
+
+ /**
+ * Set the attributes of this item's element
+ *
+ * @param array $attributes
+ *
+ * @return $this
+ */
+ public function setAttributes(array $attributes)
+ {
+ $this->attributes = $attributes;
+ return $this;
+ }
+
+ /**
+ * Add a child to this item
+ *
+ * If the child is active this item gets activated as well.
+ *
+ * @param NavigationItem $child
+ *
+ * @return $this
+ */
+ public function addChild(NavigationItem $child)
+ {
+ $this->getChildren()->addItem($child->setParent($this));
+ if ($child->getActive()) {
+ $this->setActive();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return this item's children
+ *
+ * @return Navigation
+ */
+ public function getChildren()
+ {
+ return $this->children;
+ }
+
+ /**
+ * Return whether this item has any children
+ *
+ * @return bool
+ */
+ public function hasChildren()
+ {
+ return ! $this->getChildren()->isEmpty();
+ }
+
+ /**
+ * Set this item's children
+ *
+ * @param array|Navigation $children
+ *
+ * @return $this
+ */
+ public function setChildren($children)
+ {
+ if (is_array($children)) {
+ $children = Navigation::fromArray($children);
+ } elseif (! $children instanceof Navigation) {
+ throw new InvalidArgumentException('Argument $children must be of type array or Navigation');
+ }
+
+ foreach ($children as $item) {
+ $item->setParent($this);
+ }
+
+ $this->children = $children;
+ return $this;
+ }
+
+ /**
+ * Return this item's icon
+ *
+ * @return string
+ */
+ public function getIcon()
+ {
+ return $this->icon;
+ }
+
+ /**
+ * Set this item's icon
+ *
+ * @param string $icon
+ *
+ * @return $this
+ */
+ public function setIcon($icon)
+ {
+ $this->icon = $icon;
+ return $this;
+ }
+
+ /**
+ * Return this item's name escaped with only ASCII chars and/or digits
+ *
+ * @return string
+ */
+ protected function getEscapedName()
+ {
+ return preg_replace('~[^a-zA-Z0-9]~', '_', $this->getName());
+ }
+
+ /**
+ * Return a unique version of this item's name
+ *
+ * @return string
+ */
+ public function getUniqueName()
+ {
+ if ($this->getParent() === null) {
+ return 'navigation-' . $this->getEscapedName();
+ }
+
+ return $this->getParent()->getUniqueName() . '-' . $this->getEscapedName();
+ }
+
+ /**
+ * Return this item's name
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set this item's name
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * Set this item's parent
+ *
+ * @param NavigationItem $parent
+ *
+ * @return $this
+ */
+ public function setParent(NavigationItem $parent)
+ {
+ $this->parent = $parent;
+ return $this;
+ }
+
+ /**
+ * Return this item's parent
+ *
+ * @return NavigationItem
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Return this item's label
+ *
+ * @return string
+ */
+ public function getLabel()
+ {
+ return $this->label !== null ? $this->label : $this->getName();
+ }
+
+ /**
+ * Set this item's label
+ *
+ * @param string $label
+ *
+ * @return $this
+ */
+ public function setLabel($label)
+ {
+ $this->label = $label;
+ return $this;
+ }
+
+ /**
+ * Get the item's description
+ *
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * Set the item's description
+ *
+ * @param string $description
+ *
+ * @return $this
+ */
+ public function setDescription($description)
+ {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ /**
+ * Set this item's url target
+ *
+ * @param string $target
+ *
+ * @return $this
+ */
+ public function setTarget($target)
+ {
+ $this->target = $target;
+ return $this;
+ }
+
+ /**
+ * Return this item's url target
+ *
+ * @return string
+ */
+ public function getTarget()
+ {
+ return $this->target;
+ }
+
+ /**
+ * Return this item's url
+ *
+ * @return Url
+ */
+ public function getUrl()
+ {
+ if ($this->url === null && $this->hasChildren()) {
+ $this->setUrl(Url::fromPath('navigation/dashboard', array('name' => strtolower($this->getName()))));
+ }
+
+ return $this->url;
+ }
+
+ /**
+ * Set this item's url
+ *
+ * @param Url|string $url
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException If the given url is neither of type
+ */
+ public function setUrl($url)
+ {
+ if (is_string($url)) {
+ $url = Url::fromPath($this->resolveMacros($url));
+ } elseif ($url instanceof Url) {
+ $url = Url::fromPath($this->resolveMacros($url->getAbsoluteUrl()));
+ } else {
+ throw new InvalidArgumentException('Argument $url must be of type string or Url');
+ }
+
+ $this->url = $url;
+
+ return $this;
+ }
+
+ /**
+ * Return the value of the given url parameter
+ *
+ * @param string $name
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public function getUrlParameter($name, $default = null)
+ {
+ $parameters = $this->getUrlParameters();
+ return isset($parameters[$name]) ? $parameters[$name] : $default;
+ }
+
+ /**
+ * Set the value of the given url parameter
+ *
+ * @param string $name
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function setUrlParameter($name, $value)
+ {
+ $this->urlParameters[$name] = $value;
+ return $this;
+ }
+
+ /**
+ * Return all additional parameters for this item's url
+ *
+ * @return array
+ */
+ public function getUrlParameters()
+ {
+ return $this->urlParameters ?: array();
+ }
+
+ /**
+ * Set additional parameters for this item's url
+ *
+ * @param array $urlParameters
+ *
+ * @return $this
+ */
+ public function setUrlParameters(array $urlParameters)
+ {
+ $this->urlParameters = $urlParameters;
+ return $this;
+ }
+
+ /**
+ * Set this item's properties
+ *
+ * Unknown properties (no matching setter) are considered as element attributes.
+ *
+ * @param array $properties
+ *
+ * @return $this
+ */
+ public function setProperties(array $properties)
+ {
+ foreach ($properties as $name => $value) {
+ $setter = 'set' . ucfirst($name);
+ if (method_exists($this, $setter)) {
+ $this->$setter($value);
+ } else {
+ $this->setAttribute($name, $value);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Merge this item with the given one
+ *
+ * @param NavigationItem $item
+ *
+ * @return $this
+ */
+ public function merge(NavigationItem $item)
+ {
+ if ($this->conflictsWith($item)) {
+ throw new ProgrammingError('Cannot merge, conflict detected.');
+ }
+
+ if ($this->priority === null) {
+ $priority = $item->getPriority();
+ if ($priority !== 100) {
+ $this->setPriority($priority);
+ }
+ }
+
+ if (! $this->getIcon()) {
+ $this->setIcon($item->getIcon());
+ }
+
+ if ($this->getLabel() === $this->getName() && $item->getLabel() !== $item->getName()) {
+ $this->setLabel($item->getLabel());
+ }
+
+ if ($this->target === null && ($target = $item->getTarget()) !== null) {
+ $this->setTarget($target);
+ }
+
+ if ($this->renderer === null) {
+ $renderer = $item->getRenderer();
+ if (get_class($renderer) !== 'NavigationItemRenderer') {
+ $this->setRenderer($renderer);
+ }
+ }
+
+ foreach ($item->getAttributes() as $name => $value) {
+ $this->setAttribute($name, $value);
+ }
+
+ foreach ($item->getUrlParameters() as $name => $value) {
+ $this->setUrlParameter($name, $value);
+ }
+
+ if ($item->hasChildren()) {
+ $this->getChildren()->merge($item->getChildren());
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return whether it's possible to merge this item with the given one
+ *
+ * @param NavigationItem $item
+ *
+ * @return bool
+ */
+ public function conflictsWith(NavigationItem $item)
+ {
+ if (! $item instanceof $this) {
+ return true;
+ }
+
+ if ($this->getUrl() === null || $item->getUrl() === null) {
+ return false;
+ }
+
+ return !$this->getUrl()->matches($item->getUrl());
+ }
+
+ /**
+ * Create and return the given renderer
+ *
+ * @param string|array $name
+ *
+ * @return NavigationItemRenderer
+ */
+ protected function createRenderer($name)
+ {
+ if (is_array($name)) {
+ $options = array_splice($name, 1);
+ $name = $name[0];
+ } else {
+ $options = array();
+ }
+
+ $renderer = null;
+ $classPath = null;
+ foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) {
+ $classPath = 'Icinga\\Module\\' . ucfirst($module->getName()) . '\\' . static::RENDERER_NS . '\\' . $name;
+ if (class_exists($classPath)) {
+ $renderer = new $classPath($options);
+ break;
+ }
+ }
+
+ if ($renderer === null) {
+ $classPath = 'Icinga\\' . static::RENDERER_NS . '\\' . $name;
+ if (class_exists($classPath)) {
+ $renderer = new $classPath($options);
+ }
+ }
+
+ if ($renderer === null) {
+ throw new ProgrammingError(
+ 'Cannot find renderer "%s" for navigation item "%s"',
+ $name,
+ $this->getName()
+ );
+ } elseif (! $renderer instanceof NavigationItemRenderer) {
+ throw new ProgrammingError('Class %s must inherit from NavigationItemRenderer', $classPath);
+ }
+
+ return $renderer;
+ }
+
+ /**
+ * Set this item's renderer
+ *
+ * @param string|array|NavigationItemRenderer $renderer
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException If the $renderer argument is neither a string nor a NavigationItemRenderer
+ */
+ public function setRenderer($renderer)
+ {
+ if (is_string($renderer) || is_array($renderer)) {
+ $renderer = $this->createRenderer($renderer);
+ } elseif (! $renderer instanceof NavigationItemRenderer) {
+ throw new InvalidArgumentException(
+ 'Argument $renderer must be of type string, array or NavigationItemRenderer'
+ );
+ }
+
+ $this->renderer = $renderer;
+ return $this;
+ }
+
+ /**
+ * Return this item's renderer
+ *
+ * @return NavigationItemRenderer
+ */
+ public function getRenderer()
+ {
+ if ($this->renderer === null) {
+ $this->setRenderer('NavigationItemRenderer');
+ }
+
+ return $this->renderer;
+ }
+
+ /**
+ * Set whether this item should be rendered
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
+ public function setRender($state = true)
+ {
+ $this->render = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether this item should be rendered
+ *
+ * @return bool
+ */
+ public function getRender()
+ {
+ if ($this->render === null) {
+ return $this->getUrl() !== null;
+ }
+
+ return $this->render;
+ }
+
+ /**
+ * Return whether this item should be rendered
+ *
+ * Alias for NavigationItem::getRender().
+ *
+ * @return bool
+ */
+ public function shouldRender()
+ {
+ return $this->getRender();
+ }
+
+ /**
+ * Return this item rendered to HTML
+ *
+ * @return string
+ */
+ public function render()
+ {
+ try {
+ return $this->getRenderer()->setItem($this)->render();
+ } catch (Exception $e) {
+ Logger::error(
+ 'Could not invoke custom navigation item renderer. %s in %s:%d with message: %s',
+ get_class($e),
+ $e->getFile(),
+ $e->getLine(),
+ $e->getMessage()
+ );
+
+ $renderer = new NavigationItemRenderer();
+ return $renderer->render($this);
+ }
+ }
+
+ /**
+ * Return this item rendered to HTML
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ try {
+ return $this->render();
+ } catch (Exception $e) {
+ return IcingaException::describe($e);
+ }
+ }
+
+ /**
+ * Resolve all macros in the given URL
+ *
+ * @param string $url
+ *
+ * @return string
+ */
+ protected function resolveMacros($url)
+ {
+ if (strpos($url, '$') === false) {
+ return $url;
+ }
+
+ $macros = [];
+ if (Auth::getInstance()->isAuthenticated()) {
+ $macros['$user.local_name$'] = Auth::getInstance()->getUser()->getLocalUsername();
+ }
+ if (! empty($macros)) {
+ $url = str_replace(array_keys($macros), array_values($macros), $url);
+ }
+
+ return $url;
+ }
+}
diff --git a/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php b/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php
new file mode 100644
index 0000000..8510f70
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php
@@ -0,0 +1,139 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation\Renderer;
+
+use Icinga\Web\Navigation\NavigationItem;
+
+/**
+ * Abstract base class for a NavigationItem with a status badge
+ */
+abstract class BadgeNavigationItemRenderer extends NavigationItemRenderer
+{
+ const STATE_OK = 'ok';
+ const STATE_CRITICAL = 'critical';
+ const STATE_WARNING = 'warning';
+ const STATE_PENDING = 'pending';
+ const STATE_UNKNOWN = 'unknown';
+
+ /**
+ * The tooltip text for the badge
+ *
+ * @var string
+ */
+ protected $title;
+
+ /**
+ * The state identifier being used
+ *
+ * The state identifier defines the background color of the badge.
+ *
+ * @var string
+ */
+ protected $state;
+
+ /**
+ * Set the tooltip text for the badge
+ *
+ * @param string $title
+ *
+ * @return $this
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ /**
+ * Return the tooltip text for the badge
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * Set the state identifier to use
+ *
+ * @param string $state
+ *
+ * @return $this
+ */
+ public function setState($state)
+ {
+ $this->state = $state;
+ return $this;
+ }
+
+ /**
+ * Return the state identifier to use
+ *
+ * @return string
+ */
+ public function getState()
+ {
+ return $this->state;
+ }
+
+ /**
+ * Return the amount of items represented by the badge
+ *
+ * @return int
+ */
+ abstract public function getCount();
+
+ /**
+ * Render the given navigation item as HTML anchor with a badge
+ *
+ * @param NavigationItem $item
+ *
+ * @return string
+ */
+ public function render(NavigationItem $item = null)
+ {
+ if ($item === null) {
+ $item = $this->getItem();
+ }
+
+ $cssClass = '';
+ if ($item->getCssClass() !== null) {
+ $cssClass = ' ' . $item->getCssClass();
+ }
+
+ $item->setCssClass('badge-nav-item' . $cssClass);
+ $this->setEscapeLabel(false);
+ $label = $this->view()->escape($item->getLabel());
+ $item->setLabel($this->renderBadge() . $label);
+ $html = parent::render($item);
+ return $html;
+ }
+
+ /**
+ * Render the badge
+ *
+ * @return string
+ */
+ protected function renderBadge()
+ {
+ if ($count = $this->getCount()) {
+ if ($count > 1000000) {
+ $count = round($count, -6) / 1000000 . 'M';
+ } elseif ($count > 1000) {
+ $count = round($count, -3) / 1000 . 'k';
+ }
+
+ $view = $this->view();
+ return sprintf(
+ '<span title="%s" class="badge state-%s">%s</span>',
+ $view->escape($this->getTitle()),
+ $view->escape($this->getState()),
+ $count
+ );
+ }
+
+ return '';
+ }
+}
diff --git a/library/Icinga/Web/Navigation/Renderer/HealthNavigationRenderer.php b/library/Icinga/Web/Navigation/Renderer/HealthNavigationRenderer.php
new file mode 100644
index 0000000..577895b
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Renderer/HealthNavigationRenderer.php
@@ -0,0 +1,44 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\Navigation\Renderer;
+
+use Icinga\Application\Hook\HealthHook;
+
+class HealthNavigationRenderer extends BadgeNavigationItemRenderer
+{
+ public function getCount()
+ {
+ $count = 0;
+ $title = null;
+ $worstState = null;
+ foreach (HealthHook::collectHealthData()->select() as $result) {
+ if ($worstState === null || $result->state > $worstState) {
+ $worstState = $result->state;
+ $title = $result->message;
+ $count = 1;
+ } elseif ($worstState === $result->state) {
+ $count++;
+ }
+ }
+
+ switch ($worstState) {
+ case HealthHook::STATE_OK:
+ $count = 0;
+ break;
+ case HealthHook::STATE_WARNING:
+ $this->state = self::STATE_WARNING;
+ break;
+ case HealthHook::STATE_CRITICAL:
+ $this->state = self::STATE_CRITICAL;
+ break;
+ case HealthHook::STATE_UNKNOWN:
+ $this->state = self::STATE_UNKNOWN;
+ break;
+ }
+
+ $this->title = $title;
+
+ return $count;
+ }
+}
diff --git a/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php b/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php
new file mode 100644
index 0000000..51136ff
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php
@@ -0,0 +1,235 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation\Renderer;
+
+use Icinga\Application\Icinga;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Util\StringHelper;
+use Icinga\Web\Navigation\NavigationItem;
+use Icinga\Web\Url;
+use Icinga\Web\View;
+
+/**
+ * NavigationItemRenderer
+ */
+class NavigationItemRenderer
+{
+ /**
+ * View
+ *
+ * @var View
+ */
+ protected $view;
+
+ /**
+ * The item being rendered
+ *
+ * @var NavigationItem
+ */
+ protected $item;
+
+ /**
+ * Internal link targets provided by Icinga Web 2
+ *
+ * @var array
+ */
+ protected $internalLinkTargets;
+
+ /**
+ * Whether to escape the label
+ *
+ * @var bool
+ */
+ protected $escapeLabel;
+
+ /**
+ * Create a new NavigationItemRenderer
+ *
+ * @param array $options
+ */
+ public function __construct(array $options = null)
+ {
+ if (! empty($options)) {
+ $this->setOptions($options);
+ }
+
+ $this->internalLinkTargets = array('_main', '_self', '_next');
+ $this->init();
+ }
+
+ /**
+ * Initialize this renderer
+ */
+ public function init()
+ {
+ }
+
+ /**
+ * Set the given options
+ *
+ * @param array $options
+ *
+ * @return $this
+ */
+ public function setOptions(array $options)
+ {
+ foreach ($options as $name => $value) {
+ $setter = 'set' . StringHelper::cname($name);
+ if (method_exists($this, $setter)) {
+ $this->$setter($value);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set the view
+ *
+ * @param View $view
+ *
+ * @return $this
+ */
+ public function setView(View $view)
+ {
+ $this->view = $view;
+ return $this;
+ }
+
+ /**
+ * Return the view
+ *
+ * @return View
+ */
+ public function view()
+ {
+ if ($this->view === null) {
+ $this->setView(Icinga::app()->getViewRenderer()->view);
+ }
+
+ return $this->view;
+ }
+
+ /**
+ * Set the navigation item to render
+ *
+ * @param NavigationItem $item
+ *
+ * @return $this
+ */
+ public function setItem(NavigationItem $item)
+ {
+ $this->item = $item;
+ return $this;
+ }
+
+ /**
+ * Return the navigation item being rendered
+ *
+ * @return NavigationItem
+ */
+ public function getItem()
+ {
+ return $this->item;
+ }
+
+ /**
+ * Set whether to escape the label
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
+ public function setEscapeLabel($state = true)
+ {
+ $this->escapeLabel = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether to escape the label
+ *
+ * @return bool
+ */
+ public function getEscapeLabel()
+ {
+ return $this->escapeLabel !== null ? $this->escapeLabel : true;
+ }
+
+ /**
+ * Render the given navigation item as HTML anchor
+ *
+ * @param NavigationItem $item
+ *
+ * @return string
+ */
+ public function render(NavigationItem $item = null)
+ {
+ if ($item !== null) {
+ $this->setItem($item);
+ } elseif (($item = $this->getItem()) === null) {
+ throw new ProgrammingError(
+ 'Cannot render nothing. Pass the item to render as part'
+ . ' of the call to render() or set it with setItem()'
+ );
+ }
+
+ $label = $this->getEscapeLabel()
+ ? $this->view()->escape($item->getLabel())
+ : $item->getLabel();
+ if (($icon = $item->getIcon()) !== null) {
+ $label = $this->view()->icon($icon) . $label;
+ } elseif ($item->getName()) {
+ $firstLetter = $item->getName()[0];
+ $label = $this->view()->icon('letter', null, ['data-letter' => strtolower($firstLetter)]) . $label;
+ }
+
+ if (($url = $item->getUrl()) !== null) {
+ $url->overwriteParams($item->getUrlParameters());
+
+ $target = $item->getTarget();
+ if ($url->isExternal() && (!$target || in_array($target, $this->internalLinkTargets, true))) {
+ $url = Url::fromPath('iframe', array('url' => $url));
+ }
+
+ $content = sprintf(
+ '<a%s href="%s"%s>%s</a>',
+ $this->view()->propertiesToString($item->getAttributes()),
+ $this->view()->escape($url->getAbsoluteUrl('&')),
+ $this->renderTargetAttribute(),
+ $label
+ );
+ } elseif ($label) {
+ $content = sprintf(
+ '<%1$s%2$s>%3$s</%1$s>',
+ $item::LINK_ALTERNATIVE,
+ $this->view()->propertiesToString($item->getAttributes()),
+ $label
+ );
+ } else {
+ $content = '';
+ }
+
+ return $content;
+ }
+
+ /**
+ * Render and return the attribute to provide a non-default target for the url
+ *
+ * @return string
+ */
+ protected function renderTargetAttribute()
+ {
+ $target = $this->getItem()->getTarget();
+ if ($target === null || $this->getItem()->getUrl()->getAbsoluteUrl() == '#') {
+ return '';
+ }
+
+ if (! in_array($target, $this->internalLinkTargets, true)) {
+ return ' target="' . $this->view()->escape($target) . '"';
+ }
+
+ return ' data-base-target="' . $target . '"';
+ }
+}
diff --git a/library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php b/library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php
new file mode 100644
index 0000000..00c0f9a
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php
@@ -0,0 +1,356 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation\Renderer;
+
+use ArrayIterator;
+use Exception;
+use RecursiveIterator;
+use Icinga\Application\Icinga;
+use Icinga\Exception\IcingaException;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Navigation\NavigationItem;
+use Icinga\Web\View;
+
+/**
+ * Renderer for single level navigation
+ */
+class NavigationRenderer implements RecursiveIterator, NavigationRendererInterface
+{
+ /**
+ * The tag used for the outer element
+ *
+ * @var string
+ */
+ protected $elementTag;
+
+ /**
+ * The CSS class used for the outer element
+ *
+ * @var string
+ */
+ protected $cssClass;
+
+ /**
+ * The navigation's heading text
+ *
+ * @var string
+ */
+ protected $heading;
+
+ /**
+ * The content rendered so far
+ *
+ * @var array
+ */
+ protected $content;
+
+ /**
+ * Whether to skip rendering the outer element
+ *
+ * @var bool
+ */
+ protected $skipOuterElement;
+
+ /**
+ * The navigation's iterator
+ *
+ * @var ArrayIterator
+ */
+ protected $iterator;
+
+ /**
+ * The navigation
+ *
+ * @var Navigation
+ */
+ protected $navigation;
+
+ /**
+ * View
+ *
+ * @var View
+ */
+ protected $view;
+
+ /**
+ * Create a new NavigationRenderer
+ *
+ * @param Navigation $navigation
+ * @param bool $skipOuterElement
+ */
+ public function __construct(Navigation $navigation, $skipOuterElement = false)
+ {
+ $this->skipOuterElement = $skipOuterElement;
+ $this->iterator = $navigation->getIterator();
+ $this->navigation = $navigation;
+ $this->content = array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setElementTag($tag)
+ {
+ $this->elementTag = $tag;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getElementTag()
+ {
+ return $this->elementTag ?: static::OUTER_ELEMENT_TAG;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setCssClass($class)
+ {
+ $this->cssClass = $class;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCssClass()
+ {
+ return $this->cssClass;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setHeading($heading)
+ {
+ $this->heading = $heading;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getHeading()
+ {
+ return $this->heading;
+ }
+
+ /**
+ * Return the view
+ *
+ * @return View
+ */
+ public function view()
+ {
+ if ($this->view === null) {
+ $this->setView(Icinga::app()->getViewRenderer()->view);
+ }
+
+ return $this->view;
+ }
+
+ /**
+ * Set the view
+ *
+ * @param View $view
+ *
+ * @return $this
+ */
+ public function setView(View $view)
+ {
+ $this->view = $view;
+ return $this;
+ }
+
+ public function getChildren(): NavigationRenderer
+ {
+ return new static($this->current()->getChildren(), $this->skipOuterElement);
+ }
+
+ public function hasChildren(): bool
+ {
+ return $this->current()->hasChildren();
+ }
+
+ public function current(): NavigationItem
+ {
+ return $this->iterator->current();
+ }
+
+ public function key(): int
+ {
+ return $this->iterator->key();
+ }
+
+ public function next(): void
+ {
+ $this->iterator->next();
+ }
+
+ public function rewind(): void
+ {
+ $this->iterator->rewind();
+ if (! $this->skipOuterElement) {
+ $this->content[] = $this->beginMarkup();
+ }
+ }
+
+ public function valid(): bool
+ {
+ $valid = $this->iterator->valid();
+ if (! $this->skipOuterElement && !$valid) {
+ $this->content[] = $this->endMarkup();
+ }
+
+ return $valid;
+ }
+
+ /**
+ * Return the opening markup for the navigation
+ *
+ * @return string
+ */
+ public function beginMarkup()
+ {
+ $content = array();
+ $content[] = sprintf(
+ '<%s%s role="navigation">',
+ $this->getElementTag(),
+ $this->getCssClass() !== null ? ' class="' . $this->getCssClass() . '"' : ''
+ );
+ if (($heading = $this->getHeading()) !== null) {
+ $content[] = sprintf(
+ '<h%1$d id="navigation" class="sr-only" tabindex="-1">%2$s</h%1$d>',
+ static::HEADING_RANK,
+ $this->view()->escape($heading)
+ );
+ }
+ $content[] = $this->beginChildrenMarkup();
+ return join("\n", $content);
+ }
+
+ /**
+ * Return the closing markup for the navigation
+ *
+ * @return string
+ */
+ public function endMarkup()
+ {
+ $content = array();
+ $content[] = $this->endChildrenMarkup();
+ $content[] = '</' . $this->getElementTag() . '>';
+ return join("\n", $content);
+ }
+
+ /**
+ * Return the opening markup for multiple navigation items
+ *
+ * @param int $level
+ *
+ * @return string
+ */
+ public function beginChildrenMarkup($level = 1)
+ {
+ $cssClass = array(static::CSS_CLASS_NAV);
+ if ($this->navigation->getLayout() === Navigation::LAYOUT_TABS) {
+ $cssClass[] = static::CSS_CLASS_NAV_TABS;
+ } elseif ($this->navigation->getLayout() === Navigation::LAYOUT_DROPDOWN) {
+ $cssClass[] = static::CSS_CLASS_NAV_DROPDOWN;
+ }
+
+ $cssClass[] = 'nav-level-' . $level;
+
+ return '<ul class="' . join(' ', $cssClass) . '">';
+ }
+
+ /**
+ * Return the closing markup for multiple navigation items
+ *
+ * @return string
+ */
+ public function endChildrenMarkup()
+ {
+ return '</ul>';
+ }
+
+ /**
+ * Return the opening markup for the given navigation item
+ *
+ * @param NavigationItem $item
+ *
+ * @return string
+ */
+ public function beginItemMarkup(NavigationItem $item)
+ {
+ $cssClasses = array(static::CSS_CLASS_ITEM);
+
+ if ($item->hasChildren() && $item->getChildren()->getLayout() === Navigation::LAYOUT_DROPDOWN) {
+ $cssClasses[] = static::CSS_CLASS_DROPDOWN;
+ $item
+ ->setAttribute('class', static::CSS_CLASS_DROPDOWN_TOGGLE)
+ ->setIcon(static::DROPDOWN_TOGGLE_ICON)
+ ->setUrl('#');
+ }
+
+ if ($item->getActive()) {
+ $cssClasses[] = static::CSS_CLASS_ACTIVE;
+ }
+
+ if ($item->getSelected()) {
+ $cssClasses[] = static::CSS_CLASS_SELECTED;
+ }
+
+ if ($cssClass = $item->getCssClass()) {
+ $cssClasses[] = $cssClass;
+ }
+
+ $content = sprintf(
+ '<li class="%s">',
+ join(' ', $cssClasses)
+ );
+ return $content;
+ }
+
+ /**
+ * Return the closing markup for a navigation item
+ *
+ * @return string
+ */
+ public function endItemMarkup()
+ {
+ return '</li>';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render()
+ {
+ foreach ($this as $item) {
+ /** @var NavigationItem $item */
+ if ($item->shouldRender()) {
+ $content = $item->render();
+ $this->content[] = $this->beginItemMarkup($item);
+ $this->content[] = $content;
+ $this->content[] = $this->endItemMarkup();
+ }
+ }
+
+ return join("\n", $this->content);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ try {
+ return $this->render();
+ } catch (Exception $e) {
+ return IcingaException::describe($e);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php b/library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php
new file mode 100644
index 0000000..4495b73
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php
@@ -0,0 +1,142 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation\Renderer;
+
+/**
+ * Interface for navigation renderers
+ */
+interface NavigationRendererInterface
+{
+ /**
+ * CSS class for items
+ *
+ * @var string
+ */
+ const CSS_CLASS_ITEM = 'nav-item';
+
+ /**
+ * CSS class for active items
+ *
+ * @var string
+ */
+ const CSS_CLASS_ACTIVE = 'active';
+
+ /**
+ * CSS class for selected items
+ *
+ * @var string
+ */
+ const CSS_CLASS_SELECTED = 'selected';
+
+ /**
+ * CSS class for dropdown items
+ *
+ * @var string
+ */
+ const CSS_CLASS_DROPDOWN = 'dropdown-nav-item';
+
+ /**
+ * CSS class for a dropdown item's trigger
+ *
+ * @var string
+ */
+ const CSS_CLASS_DROPDOWN_TOGGLE = 'dropdown-toggle';
+
+ /**
+ * CSS class for the ul element
+ *
+ * @var string
+ */
+ const CSS_CLASS_NAV = 'nav';
+
+ /**
+ * CSS class for the ul element with dropdown layout
+ *
+ * @var string
+ */
+ const CSS_CLASS_NAV_DROPDOWN = 'dropdown-nav';
+
+ /**
+ * CSS class for the ul element with tabs layout
+ *
+ * @var string
+ */
+ const CSS_CLASS_NAV_TABS = 'tab-nav';
+
+ /**
+ * Icon for a dropdown item's trigger
+ *
+ * @var string
+ */
+ const DROPDOWN_TOGGLE_ICON = 'menu';
+
+ /**
+ * Default tag for the outer element the navigation will be wrapped with
+ *
+ * @var string
+ */
+ const OUTER_ELEMENT_TAG = 'div';
+
+ /**
+ * The heading's rank
+ *
+ * @var int
+ */
+ const HEADING_RANK = 1;
+
+ /**
+ * Set the tag for the outer element the navigation is wrapped with
+ *
+ * @param string $tag
+ *
+ * @return $this
+ */
+ public function setElementTag($tag);
+
+ /**
+ * Return the tag for the outer element the navigation is wrapped with
+ *
+ * @return string
+ */
+ public function getElementTag();
+
+ /**
+ * Set the CSS class to use for the outer element
+ *
+ * @param string $class
+ *
+ * @return $this
+ */
+ public function setCssClass($class);
+
+ /**
+ * Get the CSS class used for the outer element
+ *
+ * @return string
+ */
+ public function getCssClass();
+
+ /**
+ * Set the navigation's heading text
+ *
+ * @param string $heading
+ *
+ * @return $this
+ */
+ public function setHeading($heading);
+
+ /**
+ * Return the navigation's heading text
+ *
+ * @return string
+ */
+ public function getHeading();
+
+ /**
+ * Return the navigation rendered to HTML
+ *
+ * @return string
+ */
+ public function render();
+}
diff --git a/library/Icinga/Web/Navigation/Renderer/RecursiveNavigationRenderer.php b/library/Icinga/Web/Navigation/Renderer/RecursiveNavigationRenderer.php
new file mode 100644
index 0000000..315c2aa
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Renderer/RecursiveNavigationRenderer.php
@@ -0,0 +1,186 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation\Renderer;
+
+use Exception;
+use RecursiveIteratorIterator;
+use Icinga\Exception\IcingaException;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Navigation\NavigationItem;
+use Icinga\Web\Navigation\Renderer\NavigationItemRenderer;
+
+/**
+ * Renderer for multi level navigation
+ *
+ * @method NavigationRenderer getInnerIterator() {
+ * {@inheritdoc}
+ * }
+ */
+class RecursiveNavigationRenderer extends RecursiveIteratorIterator implements NavigationRendererInterface
+{
+ /**
+ * The content rendered so far
+ *
+ * @var array
+ */
+ protected $content;
+
+ /**
+ * Whether to use the standard item renderer
+ *
+ * @var bool
+ */
+ protected $useStandardRenderer;
+
+ /**
+ * Create a new RecursiveNavigationRenderer
+ *
+ * @param Navigation $navigation
+ */
+ public function __construct(Navigation $navigation)
+ {
+ $this->content = array();
+ parent::__construct(
+ new NavigationRenderer($navigation, true),
+ RecursiveIteratorIterator::SELF_FIRST
+ );
+ }
+
+ /**
+ * Set whether to use the standard navigation item renderer
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
+ public function setUseStandardItemRenderer($state = true)
+ {
+ $this->useStandardRenderer = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether to use the standard navigation item renderer
+ *
+ * @return bool
+ */
+ public function getUseStandardItemRenderer()
+ {
+ return $this->useStandardRenderer;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setElementTag($tag)
+ {
+ $this->getInnerIterator()->setElementTag($tag);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getElementTag()
+ {
+ return $this->getInnerIterator()->getElementTag();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setCssClass($class)
+ {
+ $this->getInnerIterator()->setCssClass($class);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCssClass()
+ {
+ return $this->getInnerIterator()->getCssClass();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setHeading($heading)
+ {
+ $this->getInnerIterator()->setHeading($heading);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getHeading()
+ {
+ return $this->getInnerIterator()->getHeading();
+ }
+
+ public function beginIteration(): void
+ {
+ $this->content[] = $this->getInnerIterator()->beginMarkup();
+ }
+
+ public function endIteration(): void
+ {
+ $this->content[] = $this->getInnerIterator()->endMarkup();
+ }
+
+ public function beginChildren(): void
+ {
+ $this->content[] = $this->getInnerIterator()->beginChildrenMarkup($this->getDepth() + 1);
+ }
+
+ public function endChildren(): void
+ {
+ $this->content[] = $this->getInnerIterator()->endChildrenMarkup();
+ $this->content[] = $this->getInnerIterator()->endItemMarkup();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render()
+ {
+ foreach ($this as $item) {
+ /** @var NavigationItem $item */
+ if ($item->shouldRender()) {
+ if ($this->getDepth() > 0) {
+ $item->setIcon(null);
+ }
+ if ($this->getUseStandardItemRenderer()) {
+ $renderer = new NavigationItemRenderer();
+ $content = $renderer->render($item);
+ } else {
+ $content = $item->render();
+ }
+ $this->content[] = $this->getInnerIterator()->beginItemMarkup($item);
+
+ $this->content[] = $content;
+
+ if (! $item->hasChildren()) {
+ $this->content[] = $this->getInnerIterator()->endItemMarkup();
+ }
+ }
+ }
+
+ return join("\n", $this->content);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __toString()
+ {
+ try {
+ return $this->render();
+ } catch (Exception $e) {
+ return IcingaException::describe($e);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php b/library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php
new file mode 100644
index 0000000..2916f4e
--- /dev/null
+++ b/library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php
@@ -0,0 +1,72 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Navigation\Renderer;
+
+/**
+ * Badge renderer summing up the worst state of its children
+ */
+class SummaryNavigationItemRenderer extends BadgeNavigationItemRenderer
+{
+ /**
+ * Cached count
+ *
+ * @var int
+ */
+ protected $count;
+
+ /**
+ * State to severity map
+ *
+ * @var array
+ */
+ protected static $stateSeverityMap = array(
+ self::STATE_OK => 0,
+ self::STATE_PENDING => 1,
+ self::STATE_UNKNOWN => 2,
+ self::STATE_WARNING => 3,
+ self::STATE_CRITICAL => 4,
+ );
+
+ /**
+ * Severity to state map
+ *
+ * @var array
+ */
+ protected static $severityStateMap = array(
+ self::STATE_OK,
+ self::STATE_PENDING,
+ self::STATE_UNKNOWN,
+ self::STATE_WARNING,
+ self::STATE_CRITICAL
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCount()
+ {
+ if ($this->count === null) {
+ $countMap = array_fill(0, 5, 0);
+ $maxSeverity = 0;
+ $titles = array();
+ foreach ($this->getItem()->getChildren() as $child) {
+ $renderer = $child->getRenderer();
+ if ($renderer instanceof BadgeNavigationItemRenderer) {
+ $count = $renderer->getCount();
+ if ($count) {
+ $severity = static::$stateSeverityMap[$renderer->getState()];
+ $countMap[$severity] += $count;
+ $titles[] = $renderer->getTitle();
+ $maxSeverity = max($maxSeverity, $severity);
+ }
+ }
+ }
+ $this->count = $countMap[$maxSeverity];
+ $this->state = static::$severityStateMap[$maxSeverity];
+ $this->title = implode('. ', $titles);
+ }
+
+ return $this->count;
+ }
+}
diff --git a/library/Icinga/Web/Notification.php b/library/Icinga/Web/Notification.php
new file mode 100644
index 0000000..6f33a32
--- /dev/null
+++ b/library/Icinga/Web/Notification.php
@@ -0,0 +1,220 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Application\Platform;
+use Icinga\Application\Logger;
+use Icinga\Web\Session;
+
+/**
+ * // @TODO(eL): Use Notification not as Singleton but within request:
+ * <code>
+ * <?php
+ * $request->[getUser()]->notify('some message', Notification::INFO);
+ * </code>
+ */
+class Notification
+{
+ /**
+ * Notification type info
+ *
+ * @var string
+ */
+ const INFO = 'info';
+
+ /**
+ * Notification type error
+ *
+ * @var string
+ */
+ const ERROR = 'error';
+
+ /**
+ * Notification type success
+ *
+ * @var string
+ */
+ const SUCCESS = 'success';
+
+ /**
+ * Notification type warning
+ *
+ * @var string
+ */
+ const WARNING = 'warning';
+
+ /**
+ * Name of the session key for notification messages
+ *
+ * @var string
+ */
+ const SESSION_KEY = 'session';
+
+ /**
+ * Singleton instance
+ *
+ * @var self
+ */
+ protected static $instance;
+
+ /**
+ * Whether the platform is CLI
+ *
+ * @var bool
+ */
+ protected $isCli = false;
+
+ /**
+ * Notification messages
+ *
+ * @var array
+ */
+ protected $messages = array();
+
+ /**
+ * Session
+ *
+ * @var Session
+ */
+ protected $session;
+
+ /**
+ * Create the notification instance
+ */
+ final private function __construct()
+ {
+ if (Platform::isCli()) {
+ $this->isCli = true;
+ return;
+ }
+
+ $this->session = Session::getSession();
+ $messages = $this->session->get(self::SESSION_KEY);
+ if (is_array($messages)) {
+ $this->messages = $messages;
+ $this->session->delete(self::SESSION_KEY);
+ $this->session->write();
+ }
+ }
+
+ /**
+ * Get the Notification instance
+ *
+ * @return Notification
+ */
+ public static function getInstance()
+ {
+ if (self::$instance === null) {
+ self::$instance = new self();
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Add info notification
+ *
+ * @param string $msg
+ */
+ public static function info($msg)
+ {
+ self::getInstance()->addMessage($msg, self::INFO);
+ }
+
+ /**
+ * Add error notification
+ *
+ * @param string $msg
+ */
+ public static function error($msg)
+ {
+ self::getInstance()->addMessage($msg, self::ERROR);
+ }
+
+ /**
+ * Add success notification
+ *
+ * @param string $msg
+ */
+ public static function success($msg)
+ {
+ self::getInstance()->addMessage($msg, self::SUCCESS);
+ }
+
+ /**
+ * Add warning notification
+ *
+ * @param string $msg
+ */
+ public static function warning($msg)
+ {
+ self::getInstance()->addMessage($msg, self::WARNING);
+ }
+
+ /**
+ * Add a notification message
+ *
+ * @param string $message
+ * @param string $type
+ */
+ protected function addMessage($message, $type = self::INFO)
+ {
+ if ($this->isCli) {
+ $msg = sprintf('[%s] %s', $type, $message);
+ switch ($type) {
+ case self::INFO:
+ case self::SUCCESS:
+ Logger::info($msg);
+ break;
+ case self::ERROR:
+ Logger::error($msg);
+ break;
+ case self::WARNING:
+ Logger::warning($msg);
+ break;
+ }
+ } else {
+ $this->messages[] = (object) array(
+ 'type' => $type,
+ 'message' => $message,
+ );
+ }
+ }
+
+ /**
+ * Pop the notification messages
+ *
+ * @return array
+ */
+ public function popMessages()
+ {
+ $messages = $this->messages;
+ $this->messages = array();
+ return $messages;
+ }
+
+ /**
+ * Get whether notification messages have been added
+ *
+ * @return bool
+ */
+ public function hasMessages()
+ {
+ return ! empty($this->messages);
+ }
+
+ /**
+ * Destroy the notification instance
+ */
+ final public function __destruct()
+ {
+ if ($this->isCli) {
+ return;
+ }
+ if ($this->hasMessages() && $this->session->get('messages') !== $this->messages) {
+ $this->session->set(self::SESSION_KEY, $this->messages);
+ $this->session->write();
+ }
+ }
+}
diff --git a/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php b/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php
new file mode 100644
index 0000000..6f103e5
--- /dev/null
+++ b/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php
@@ -0,0 +1,84 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Paginator\Adapter;
+
+use Zend_Paginator_Adapter_Interface;
+use Icinga\Data\QueryInterface;
+
+class QueryAdapter implements Zend_Paginator_Adapter_Interface
+{
+ /**
+ * The query being paginated
+ *
+ * @var QueryInterface
+ */
+ protected $query;
+
+ /**
+ * Item count
+ *
+ * @var int
+ */
+ protected $count;
+
+ /**
+ * Create a new QueryAdapter
+ *
+ * @param QueryInterface $query The query to paginate
+ */
+ public function __construct(QueryInterface $query)
+ {
+ $this->setQuery($query);
+ }
+
+ /**
+ * Set the query to paginate
+ *
+ * @param QueryInterface $query
+ *
+ * @return $this
+ */
+ public function setQuery(QueryInterface $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Return the query being paginated
+ *
+ * @return QueryInterface
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Fetch and return the rows in the given range of the query result
+ *
+ * @param int $offset Page offset
+ * @param int $itemCountPerPage Number of items per page
+ *
+ * @return array
+ */
+ public function getItems($offset, $itemCountPerPage)
+ {
+ return $this->query->limit($itemCountPerPage, $offset)->fetchAll();
+ }
+
+ /**
+ * Return the total number of items in the query result
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ if ($this->count === null) {
+ $this->count = $this->query->count();
+ }
+
+ return $this->count;
+ }
+}
diff --git a/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php b/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php
new file mode 100644
index 0000000..d9b2ed9
--- /dev/null
+++ b/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php
@@ -0,0 +1,78 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+/**
+ * @see Zend_Paginator_ScrollingStyle_Interface
+ */
+class Icinga_Web_Paginator_ScrollingStyle_SlidingWithBorder implements Zend_Paginator_ScrollingStyle_Interface
+{
+ /**
+ * Returns an array of "local" pages given a page number and range.
+ *
+ * @param Zend_Paginator $paginator
+ * @param integer $pageRange (Optional) Page range
+ * @return array
+ */
+ public function getPages(Zend_Paginator $paginator, $pageRange = null)
+ {
+ // This is unused
+ if ($pageRange === null) {
+ $pageRange = $paginator->getPageRange();
+ }
+
+ $pageNumber = $paginator->getCurrentPageNumber();
+ $pageCount = count($paginator);
+ $range = array();
+
+ if ($pageCount < 10) {
+ // Show all pages if we have less than 10.
+
+ for ($i = 1; $i < 10; $i++) {
+ if ($i > $pageCount) {
+ break;
+ }
+ $range[$i] = $i;
+ }
+ } else {
+ // More than 10 pages:
+
+ foreach (array(1, 2) as $i) {
+ $range[$i] = $i;
+ }
+ if ($pageNumber < 6) {
+ // We are on page 1-5 from
+ for ($i = 1; $i <= 7; $i++) {
+ $range[$i] = $i;
+ }
+ } else {
+ // Current page > 5
+ $range[] = '...';
+
+ // Less than 5 pages left
+ if (($pageCount - $pageNumber) < 5) {
+ $start = 5 - ($pageCount - $pageNumber);
+ } else {
+ $start = 1;
+ }
+
+ for ($i = $pageNumber - $start; $i < ($pageNumber + (4 - $start)); $i++) {
+ if ($i > $pageCount) {
+ break;
+ }
+ $range[$i] = $i;
+ }
+ }
+ if ($pageNumber < ($pageCount - 2)) {
+ $range[] = '...';
+ }
+
+ foreach (array($pageCount - 1, $pageCount) as $i) {
+ $range[$i] = $i;
+ }
+ }
+ if (empty($range)) {
+ $range[] = 1;
+ }
+ return $range;
+ }
+}
diff --git a/library/Icinga/Web/RememberMe.php b/library/Icinga/Web/RememberMe.php
new file mode 100644
index 0000000..1002396
--- /dev/null
+++ b/library/Icinga/Web/RememberMe.php
@@ -0,0 +1,363 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Application\Config;
+use Icinga\Authentication\Auth;
+use Icinga\Crypt\AesCrypt;
+use Icinga\Common\Database;
+use Icinga\User;
+use ipl\Sql\Expression;
+use ipl\Sql\Select;
+use RuntimeException;
+
+/**
+ * Remember me component
+ *
+ * Retains credentials for 30 days by default in order to stay signed in even after the session is closed.
+ */
+class RememberMe
+{
+ use Database;
+
+ /** @var string Cookie name */
+ const COOKIE = 'icingaweb2-remember-me';
+
+ /** @var string Database table name */
+ const TABLE = 'icingaweb_rememberme';
+
+ /** @var string Encrypted password of the user */
+ protected $encryptedPassword;
+
+ /** @var string */
+ protected $username;
+
+ /** @var AesCrypt Instance for encrypting/decrypting the credentials */
+ protected $aesCrypt;
+
+ /** @var int Timestamp when the remember me cookie expires */
+ protected $expiresAt;
+
+ /**
+ * Get whether staying logged in is possible
+ *
+ * @return bool
+ */
+ public static function isSupported()
+ {
+ $self = new self();
+
+ if (! $self->hasDb()) {
+ return false;
+ }
+
+ try {
+ (new AesCrypt())->getMethod();
+ } catch (RuntimeException $_) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get whether the remember cookie is set
+ *
+ * @return bool
+ */
+ public static function hasCookie()
+ {
+ return isset($_COOKIE[static::COOKIE]);
+ }
+
+ /**
+ * Remove the database entry if exists and unset the remember me cookie from PHP's `$_COOKIE` superglobal
+ *
+ * @return Cookie The invalidation cookie which has to be sent to client in oder to remove the remember me cookie
+ */
+ public static function forget()
+ {
+ if (self::hasCookie()) {
+ $data = explode('|', $_COOKIE[static::COOKIE]);
+ $iv = base64_decode(array_pop($data));
+ (new self())->remove(bin2hex($iv));
+ }
+
+ unset($_COOKIE[static::COOKIE]);
+
+ return (new Cookie(static::COOKIE))
+ ->setHttpOnly(true)
+ ->forgetMe();
+ }
+
+ /**
+ * Create the remember me component from the remember me cookie
+ *
+ * @return static
+ */
+ public static function fromCookie()
+ {
+ $data = explode('|', $_COOKIE[static::COOKIE]);
+ $iv = base64_decode(array_pop($data));
+
+ $select = (new Select())
+ ->from(static::TABLE)
+ ->columns('*')
+ ->where(['random_iv = ?' => bin2hex($iv)]);
+
+ $rememberMe = new static();
+ $rs = $rememberMe->getDb()->select($select)->fetch();
+
+ if (! $rs) {
+ throw new RuntimeException(sprintf(
+ "No database entry found for IV '%s'",
+ bin2hex($iv)
+ ));
+ }
+
+ $rememberMe->aesCrypt = (new AesCrypt())
+ ->setKey(hex2bin($rs->passphrase))
+ ->setIV($iv);
+
+ if (count($data) > 1) {
+ $rememberMe->aesCrypt->setTag(
+ base64_decode(array_pop($data))
+ );
+ } elseif ($rememberMe->aesCrypt->isAuthenticatedEncryptionRequired()) {
+ throw new RuntimeException(
+ "The given decryption method needs a tag, but is not specified. "
+ . "You have probably updated the PHP version."
+ );
+ }
+
+ $rememberMe->username = $rs->username;
+ $rememberMe->encryptedPassword = $data[0];
+
+ return $rememberMe;
+ }
+
+ /**
+ * Create the remember me component from the given username and password
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return static
+ */
+ public static function fromCredentials($username, $password)
+ {
+ $aesCrypt = new AesCrypt();
+ $rememberMe = new static();
+ $rememberMe->encryptedPassword = $aesCrypt->encrypt($password);
+ $rememberMe->username = $username;
+ $rememberMe->aesCrypt = $aesCrypt;
+
+ return $rememberMe;
+ }
+
+ /**
+ * Remove expired remember me information from the database
+ */
+ public static function removeExpired()
+ {
+ $rememberMe = new static();
+ if (! $rememberMe->hasDb()) {
+ return;
+ }
+
+ $rememberMe->getDb()->delete(static::TABLE, [
+ 'expires_at < NOW()'
+ ]);
+ }
+
+ /**
+ * Get the remember me cookie
+ *
+ * @return Cookie
+ */
+ public function getCookie()
+ {
+ $values = [
+ $this->encryptedPassword,
+ base64_encode($this->aesCrypt->getIV()),
+ ];
+
+ if ($this->aesCrypt->isAuthenticatedEncryptionRequired()) {
+ array_splice($values, 1, 0, base64_encode($this->aesCrypt->getTag()));
+ }
+
+ return (new Cookie(static::COOKIE))
+ ->setExpire($this->getExpiresAt())
+ ->setHttpOnly(true)
+ ->setValue(implode('|', $values));
+ }
+
+ /**
+ * Get the timestamp when the cookie expires
+ *
+ * Defaults to now plus 30 days, if not set via {@link setExpiresAt()}.
+ *
+ * @return int
+ */
+ public function getExpiresAt()
+ {
+ if ($this->expiresAt === null) {
+ $this->expiresAt = time() + 60 * 60 * 24 * 30;
+ }
+
+ return $this->expiresAt;
+ }
+
+ /**
+ * Set the timestamp when the cookie expires
+ *
+ * @param int $expiresAt
+ *
+ * @return $this
+ */
+ public function setExpiresAt($expiresAt)
+ {
+ $this->expiresAt = $expiresAt;
+
+ return $this;
+ }
+
+ /**
+ * Authenticate via the remember me cookie
+ *
+ * @return bool
+ *
+ * @throws \Icinga\Exception\AuthenticationException
+ */
+ public function authenticate()
+ {
+ $auth = Auth::getInstance();
+ $authChain = $auth->getAuthChain();
+ $authChain->setSkipExternalBackends(true);
+ $user = new User($this->username);
+ if (! $user->hasDomain()) {
+ $user->setDomain(Config::app()->get('authentication', 'default_domain'));
+ }
+
+ $authenticated = $authChain->authenticate(
+ $user,
+ $this->aesCrypt->decrypt($this->encryptedPassword)
+ );
+
+ if ($authenticated) {
+ $auth->setAuthenticated($user);
+ }
+
+ return $authenticated;
+ }
+
+ /**
+ * Persist the remember me information into the database
+ *
+ * To remove any previous stored information, set the iv
+ *
+ * @param string|null $iv To remove a specific iv record from the database
+ *
+ * @return $this
+ */
+ public function persist($iv = null)
+ {
+ if ($iv) {
+ $this->remove(bin2hex($iv));
+ }
+
+ $this->getDb()->insert(static::TABLE, [
+ 'username' => $this->username,
+ 'passphrase' => bin2hex($this->aesCrypt->getKey()),
+ 'random_iv' => bin2hex($this->aesCrypt->getIV()),
+ 'http_user_agent' => (new UserAgent)->getAgent(),
+ 'expires_at' => date('Y-m-d H:i:s', $this->getExpiresAt()),
+ 'ctime' => new Expression('NOW()'),
+ 'mtime' => new Expression('NOW()')
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * Remove remember me information from the database on the basis of iv
+ *
+ * @param string $iv
+ *
+ * @return $this
+ */
+ public function remove($iv)
+ {
+ $this->getDb()->delete(static::TABLE, [
+ 'random_iv = ?' => $iv
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * Create renewed remember me cookie
+ *
+ * @return static New remember me cookie which has to be sent to the client
+ */
+ public function renew()
+ {
+ return static::fromCredentials(
+ $this->username,
+ $this->aesCrypt->decrypt($this->encryptedPassword)
+ );
+ }
+
+ /**
+ * Get all users using remember me cookie
+ *
+ * @return array Array of users
+ */
+ public static function getAllUser()
+ {
+ $rememberMe = new static();
+ if (! $rememberMe->hasDb()) {
+ return [];
+ }
+
+ $select = (new Select())
+ ->from(static::TABLE)
+ ->columns('username')
+ ->groupBy('username');
+
+ return $rememberMe->getDb()->select($select)->fetchAll();
+ }
+
+ /**
+ * Get all remember me entries from the database of the given user.
+ *
+ * @param $username
+ *
+ * @return array Array of database entries
+ */
+ public static function getAllByUsername($username)
+ {
+ $rememberMe = new static();
+ if (! $rememberMe->hasDb()) {
+ return [];
+ }
+
+ $select = (new Select())
+ ->from(static::TABLE)
+ ->columns(['http_user_agent', 'random_iv'])
+ ->where(['username = ?' => $username]);
+
+ return $rememberMe->getDb()->select($select)->fetchAll();
+ }
+
+ /**
+ * Get the AesCrypt instance
+ *
+ * @return AesCrypt
+ */
+ public function getAesCrypt()
+ {
+ return $this->aesCrypt;
+ }
+}
diff --git a/library/Icinga/Web/RememberMeUserDevicesList.php b/library/Icinga/Web/RememberMeUserDevicesList.php
new file mode 100644
index 0000000..66609de
--- /dev/null
+++ b/library/Icinga/Web/RememberMeUserDevicesList.php
@@ -0,0 +1,144 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web;
+
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Web\Url as iplWebUrl; //alias is needed for php5.6
+use ipl\Web\Widget\Icon;
+use ipl\Web\Widget\Link;
+
+class RememberMeUserDevicesList extends BaseHtmlElement
+{
+ protected $tag = 'table';
+
+ protected $defaultAttributes = [
+ 'class' => 'common-table',
+ 'data-base-target' => '_self'
+ ];
+
+ /**
+ * @var array
+ */
+ protected $devicesList;
+
+ /**
+ * @var string
+ */
+ protected $username;
+
+ /**
+ * @var string
+ */
+ protected $url;
+
+ /**
+ * @return string
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * @param string $url
+ *
+ * @return $this
+ */
+ public function setUrl($url)
+ {
+ $this->url = $url;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getUsername()
+ {
+ return $this->username;
+ }
+
+ /**
+ * @param string $username
+ *
+ * @return $this
+ */
+ public function setUsername($username)
+ {
+ $this->username = $username;
+
+ return $this;
+ }
+
+ /**
+ * @return array List of devices. Each device contains user agent and fingerprint string
+ */
+ public function getDevicesList()
+ {
+ return $this->devicesList;
+ }
+
+ /**
+ * @param $devicesList
+ *
+ * @return $this
+ */
+ public function setDevicesList($devicesList)
+ {
+ $this->devicesList = $devicesList;
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ $thead = Html::tag('thead');
+ $theadRow = Html::tag('tr')
+ ->add(Html::tag(
+ 'th',
+ sprintf(t('List of devices and browsers %s is currently logged in:'), $this->getUsername())
+ ));
+
+ $thead->add($theadRow);
+
+ $head = Html::tag('tr')
+ ->add(Html::tag('th', t('OS')))
+ ->add(Html::tag('th', t('Browser')))
+ ->add(Html::tag('th', t('Fingerprint')));
+
+ $thead->add($head);
+ $tbody = Html::tag('tbody');
+
+ if (empty($this->getDevicesList())) {
+ $tbody->add(Html::tag('td', t('No device found')));
+ } else {
+ foreach ($this->getDevicesList() as $device) {
+ $agent = new UserAgent($device);
+ $element = Html::tag('tr')
+ ->add(Html::tag('td', $agent->getOs()))
+ ->add(Html::tag('td', $agent->getBrowser()))
+ ->add(Html::tag('td', $device->random_iv));
+
+ $link = (new Link(
+ new Icon('trash'),
+ iplWebUrl::fromPath($this->getUrl())
+ ->addParams(
+ [
+ 'name' => $this->getUsername(),
+ 'fingerprint' => $device->random_iv,
+ ]
+ )
+ ));
+
+ $element->add(Html::tag('td', $link));
+ $tbody->add($element);
+ }
+ }
+
+ $this->add($thead);
+ $this->add($tbody);
+ }
+}
diff --git a/library/Icinga/Web/RememberMeUserList.php b/library/Icinga/Web/RememberMeUserList.php
new file mode 100644
index 0000000..bb95dc9
--- /dev/null
+++ b/library/Icinga/Web/RememberMeUserList.php
@@ -0,0 +1,106 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web;
+
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Web\Url as iplWebUrl; //alias is needed for php5.6
+use ipl\Web\Widget\Link;
+
+/**
+ * Class RememberMeUserList
+ *
+ * @package Icinga\Web
+ */
+class RememberMeUserList extends BaseHtmlElement
+{
+ protected $tag = 'table';
+
+ protected $defaultAttributes = [
+ 'class' => 'common-table table-row-selectable',
+ 'data-base-target' => '_next',
+ ];
+
+ /**
+ * @var array
+ */
+ protected $users;
+
+ /**
+ * @var string
+ */
+ protected $url;
+
+ /**
+ * @return string
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * @param string $url
+ *
+ * @return $this
+ */
+ public function setUrl($url)
+ {
+ $this->url = $url;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getUsers()
+ {
+ return $this->users;
+ }
+
+ /**
+ * @param array $users
+ *
+ * @return $this
+ */
+ public function setUsers($users)
+ {
+ $this->users = $users;
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ $thead = Html::tag('thead');
+ $theadRow = Html::tag('tr')
+ ->add(Html::tag(
+ 'th',
+ t('List of users who stay logged in')
+ ));
+
+ $thead->add($theadRow);
+ $tbody = Html::tag('tbody');
+
+ if (empty($this->getUsers())) {
+ $tbody->add(Html::tag('td', t('No user found')));
+ } else {
+ foreach ($this->getUsers() as $user) {
+ $element = Html::tag('tr');
+ $link = new Link(
+ $user->username,
+ iplWebUrl::fromPath($this->getUrl())->addParams(['name' => $user->username]),
+ ['title' => sprintf(t('Device list of %s'), $user->username)]
+ );
+
+ $element->add(Html::tag('td', $link));
+ $tbody->add($element);
+ }
+ }
+
+ $this->add($thead);
+ $this->add($tbody);
+ }
+}
diff --git a/library/Icinga/Web/Request.php b/library/Icinga/Web/Request.php
new file mode 100644
index 0000000..064ce63
--- /dev/null
+++ b/library/Icinga/Web/Request.php
@@ -0,0 +1,142 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Util\Json;
+use Zend_Controller_Request_Http;
+use Icinga\Application\Icinga;
+use Icinga\User;
+
+/**
+ * A request
+ */
+class Request extends Zend_Controller_Request_Http
+{
+ /**
+ * Response
+ *
+ * @var Response
+ */
+ protected $response;
+
+ /**
+ * Unique identifier
+ *
+ * @var string
+ */
+ protected $uniqueId;
+
+ /**
+ * Request URL
+ *
+ * @var Url
+ */
+ protected $url;
+
+ /**
+ * User if authenticated
+ *
+ * @var User|null
+ */
+ protected $user;
+
+ /**
+ * Get the response
+ *
+ * @return Response
+ */
+ public function getResponse()
+ {
+ if ($this->response === null) {
+ $this->response = Icinga::app()->getResponse();
+ }
+
+ return $this->response;
+ }
+
+ /**
+ * Get the request URL
+ *
+ * @return Url
+ */
+ public function getUrl()
+ {
+ if ($this->url === null) {
+ $this->url = Url::fromRequest($this);
+ }
+ return $this->url;
+ }
+
+ /**
+ * Get the user if authenticated
+ *
+ * @return User|null
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * Set the authenticated user
+ *
+ * @param User $user
+ *
+ * @return $this
+ */
+ public function setUser(User $user)
+ {
+ $this->user = $user;
+ return $this;
+ }
+
+ /**
+ * Get whether the request seems to be an API request
+ *
+ * @return bool
+ */
+ public function isApiRequest()
+ {
+ return $this->getHeader('Accept') === 'application/json';
+ }
+
+ /**
+ * Makes an ID unique to this request, to prevent id collisions in different containers
+ *
+ * Call this whenever an ID might show up multiple times in different containers. This function is useful
+ * for ensuring unique ids on sites, even if we combine the HTML of different requests into one site,
+ * while still being able to reference elements uniquely in the same request.
+ *
+ * @param string $id
+ *
+ * @return string The id suffixed w/ an identifier unique to this request
+ */
+ public function protectId($id)
+ {
+ return $id . '-' . Window::getInstance()->getContainerId();
+ }
+
+ public function getPost($key = null, $default = null)
+ {
+ if ($key === null && $this->extractMediaType($this->getHeader('Content-Type')) === 'application/json') {
+ return Json::decode(file_get_contents('php://input'), true);
+ }
+
+ return parent::getPost($key, $default);
+ }
+
+ /**
+ * Extract and return the media type from the given header value
+ *
+ * @param string $headerValue
+ *
+ * @return string
+ */
+ protected function extractMediaType($headerValue)
+ {
+ // Pretty basic and does not care about parameters
+ $parts = explode(';', $headerValue, 2);
+ return strtolower(trim($parts[0]));
+ }
+}
diff --git a/library/Icinga/Web/Response.php b/library/Icinga/Web/Response.php
new file mode 100644
index 0000000..555d3fa
--- /dev/null
+++ b/library/Icinga/Web/Response.php
@@ -0,0 +1,460 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Application\Config;
+use Icinga\Util\Csp;
+use Zend_Controller_Response_Http;
+use Icinga\Application\Icinga;
+use Icinga\Web\Response\JsonResponse;
+
+/**
+ * A HTTP response
+ */
+class Response extends Zend_Controller_Response_Http
+{
+ /**
+ * The default content type being used for responses
+ *
+ * @var string
+ */
+ const DEFAULT_CONTENT_TYPE = 'text/html; charset=UTF-8';
+
+ /**
+ * Auto-refresh interval
+ *
+ * @var int
+ */
+ protected $autoRefreshInterval;
+
+ /**
+ * Set of cookies which are to be sent to the client
+ *
+ * @var CookieSet
+ */
+ protected $cookies;
+
+ /**
+ * Redirect URL
+ *
+ * @var Url|null
+ */
+ protected $redirectUrl;
+
+ /**
+ * Request
+ *
+ * @var Request
+ */
+ protected $request;
+
+ /**
+ * Whether to instruct the client to reload the window
+ *
+ * @var bool
+ */
+ protected $reloadWindow;
+
+ /**
+ * Whether to instruct client side script code to reload CSS
+ *
+ * @var bool
+ */
+ protected $reloadCss;
+
+ /**
+ * Whether to send the rerender layout header on XHR
+ *
+ * @var bool
+ */
+ protected $rerenderLayout = false;
+
+ /**
+ * Whether to send the current window ID to the client
+ *
+ * @var bool
+ */
+ protected $overrideWindowId = false;
+
+ /**
+ * Get the auto-refresh interval
+ *
+ * @return int
+ */
+ public function getAutoRefreshInterval()
+ {
+ return $this->autoRefreshInterval;
+ }
+
+ /**
+ * Set the auto-refresh interval
+ *
+ * @param int $autoRefreshInterval
+ *
+ * @return $this
+ */
+ public function setAutoRefreshInterval($autoRefreshInterval)
+ {
+ $this->autoRefreshInterval = $autoRefreshInterval;
+ return $this;
+ }
+
+ /**
+ * Get the set of cookies which are to be sent to the client
+ *
+ * @return CookieSet
+ */
+ public function getCookies()
+ {
+ if ($this->cookies === null) {
+ $this->cookies = new CookieSet();
+ }
+ return $this->cookies;
+ }
+
+ /**
+ * Get the cookie with the given name from the set of cookies which are to be sent to the client
+ *
+ * @param string $name The name of the cookie
+ *
+ * @return Cookie|null The cookie with the given name or null if the cookie does not exist
+ */
+ public function getCookie($name)
+ {
+ return $this->getCookies()->get($name);
+ }
+
+ /**
+ * Set the given cookie for sending it to the client
+ *
+ * @param Cookie $cookie The cookie to send to the client
+ *
+ * @return $this
+ */
+ public function setCookie(Cookie $cookie)
+ {
+ $this->getCookies()->add($cookie);
+ return $this;
+ }
+
+ /**
+ * Get the redirect URL
+ *
+ * @return Url|null
+ */
+ protected function getRedirectUrl()
+ {
+ return $this->redirectUrl;
+ }
+
+ /**
+ * Set the redirect URL
+ *
+ * Unlike {@link setRedirect()} this method only sets a redirect URL on the response for later usage.
+ * {@link prepare()} will take care of the correct redirect handling and HTTP headers on XHR and "normal" browser
+ * requests.
+ *
+ * @param string|Url $redirectUrl
+ *
+ * @return $this
+ */
+ protected function setRedirectUrl($redirectUrl)
+ {
+ if (! $redirectUrl instanceof Url) {
+ $redirectUrl = Url::fromPath((string) $redirectUrl);
+ }
+ $redirectUrl->getParams()->setSeparator('&');
+ $this->redirectUrl = $redirectUrl;
+ return $this;
+ }
+
+ /**
+ * Get an array of all header values for the given name
+ *
+ * @param string $name The name of the header
+ * @param bool $lastOnly If this is true, the last value will be returned as a string
+ *
+ * @return null|array|string
+ */
+ public function getHeader($name, $lastOnly = false)
+ {
+ $result = ($lastOnly ? null : array());
+ $headers = $this->getHeaders();
+ foreach ($headers as $header) {
+ if ($header['name'] === $name) {
+ if ($lastOnly) {
+ $result = $header['value'];
+ } else {
+ $result[] = $header['value'];
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get the request
+ *
+ * @return Request
+ */
+ public function getRequest()
+ {
+ if ($this->request === null) {
+ $this->request = Icinga::app()->getRequest();
+ }
+ return $this->request;
+ }
+
+ /**
+ * Get whether to instruct the client to reload the window
+ *
+ * @return bool
+ */
+ public function isWindowReloaded()
+ {
+ return $this->reloadWindow;
+ }
+
+ /**
+ * Set whether to instruct the client to reload the window
+ *
+ * @param bool $reloadWindow
+ *
+ * @return $this
+ */
+ public function setReloadWindow($reloadWindow)
+ {
+ $this->reloadWindow = $reloadWindow;
+
+ return $this;
+ }
+
+ /**
+ * Get whether to instruct client side script code to reload CSS
+ *
+ * @return bool
+ */
+ public function isReloadCss()
+ {
+ return $this->reloadCss;
+ }
+
+ /**
+ * Set whether to instruct client side script code to reload CSS
+ *
+ * @param bool $reloadCss
+ *
+ * @return $this
+ */
+ public function setReloadCss($reloadCss)
+ {
+ $this->reloadCss = $reloadCss;
+ return $this;
+ }
+
+ /**
+ * Get whether to send the rerender layout header on XHR
+ *
+ * @return bool
+ */
+ public function getRerenderLayout()
+ {
+ return $this->rerenderLayout;
+ }
+
+ /**
+ * Get whether to send the rerender layout header on XHR
+ *
+ * @param bool $rerenderLayout
+ *
+ * @return $this
+ */
+ public function setRerenderLayout($rerenderLayout = true)
+ {
+ $this->rerenderLayout = (bool) $rerenderLayout;
+ return $this;
+ }
+
+ /**
+ * Get whether to send the current window ID to the client
+ *
+ * @return bool
+ */
+ public function getOverrideWindowId()
+ {
+ return $this->overrideWindowId;
+ }
+
+ /**
+ * Set whether to send the current window ID to the client
+ *
+ * @param bool $overrideWindowId
+ *
+ * @return $this
+ */
+ public function setOverrideWindowId($overrideWindowId = true)
+ {
+ $this->overrideWindowId = $overrideWindowId;
+ return $this;
+ }
+
+ /**
+ * Entry point for HTTP responses in JSON format
+ *
+ * @return JsonResponse
+ */
+ public function json()
+ {
+ $response = new JsonResponse();
+ $response->copyMetaDataFrom($this);
+ return $response;
+ }
+
+ /**
+ * Prepare the request before sending
+ */
+ protected function prepare()
+ {
+ $request = $this->getRequest();
+ $redirectUrl = $this->getRedirectUrl();
+ if ($request->isXmlHttpRequest()) {
+ if ($redirectUrl !== null) {
+ if ($request->isGet() && Icinga::app()->getViewRenderer()->view->compact) {
+ if ($redirectUrl->getParam('redirect') !== '__SELF__') {
+ $redirectUrl->getParams()->set('showCompact', true);
+ }
+ }
+
+ $encodedRedirectUrl = rawurlencode($redirectUrl->getAbsoluteUrl());
+
+ // TODO: Compatibility only. Remove once v2.14 is out.
+ $targetId = $request->getHeader('X-Icinga-Container');
+ $redirectTargetId = $this->getHeader('X-Icinga-Container', true) ?? $targetId;
+ if ($request->isPost()
+ && ! $this->getRerenderLayout()
+ && $targetId === 'col2'
+ && $redirectTargetId === $targetId
+ && $request->getHeader('X-Icinga-Col2-State')
+ ) {
+ $col1State = Url::fromPath($request->getHeader('X-Icinga-Col1-State'));
+ $col2State = Url::fromPath($request->getHeader('X-Icinga-Col2-State'));
+ if ($col2State->getPath() !== $redirectUrl->getPath()
+ && $col1State->getPath() === $redirectUrl->getPath()
+ ) {
+ $encodedRedirectUrl = '__CLOSE__';
+ }
+ }
+
+ $this->setHeader('X-Icinga-Redirect', $encodedRedirectUrl, true);
+ if ($this->getRerenderLayout()) {
+ $this->setHeader('X-Icinga-Rerender-Layout', 'yes', true);
+ }
+ }
+ if ($this->getOverrideWindowId()) {
+ $this->setHeader('X-Icinga-WindowId', Window::getInstance()->getId(), true);
+ }
+ if ($this->getRerenderLayout()) {
+ $this->setHeader('X-Icinga-Container', 'layout', true);
+ }
+ if ($this->isWindowReloaded()) {
+ $this->setHeader('X-Icinga-Reload-Window', 'yes', true);
+ }
+ if ($this->isReloadCss()) {
+ $this->setHeader('X-Icinga-Reload-Css', 'now', true);
+ }
+ if (($autoRefreshInterval = $this->getAutoRefreshInterval()) !== null) {
+ $this->setHeader('X-Icinga-Refresh', $autoRefreshInterval, true);
+ }
+
+ $notifications = Notification::getInstance();
+ if ($notifications->hasMessages()) {
+ $notificationList = array();
+ foreach ($notifications->popMessages() as $m) {
+ $notificationList[] = rawurlencode($m->type . ' ' . $m->message);
+ }
+ $this->setHeader('X-Icinga-Notification', implode('&', $notificationList), true);
+ }
+ } else {
+ if ($redirectUrl !== null) {
+ $this->setRedirect($redirectUrl->getAbsoluteUrl());
+ }
+
+ if (Csp::getStyleNonce() && Config::app()->get('security', 'use_strict_csp', false)) {
+ Csp::addHeader($this);
+ }
+ }
+
+ if (! $this->getHeader('Content-Type', true)) {
+ $this->setHeader('Content-Type', static::DEFAULT_CONTENT_TYPE);
+ }
+ }
+
+ /**
+ * Redirect to the given URL and exit immediately
+ *
+ * @param string|Url $url
+ *
+ * @return never
+ */
+ public function redirectAndExit($url)
+ {
+ $this->setRedirectUrl($url);
+
+ $session = Session::getSession();
+ if ($session->hasChanged()) {
+ $session->write();
+ }
+
+ $this->sendHeaders();
+ exit;
+ }
+
+ /**
+ * Send the cookies to the client
+ */
+ public function sendCookies()
+ {
+ foreach ($this->getCookies() as $cookie) {
+ /** @var Cookie $cookie */
+ setcookie(
+ $cookie->getName(),
+ $cookie->getValue() ?? '',
+ $cookie->getExpire() ?? 0,
+ $cookie->getPath(),
+ $cookie->getDomain() ?? '',
+ $cookie->isSecure(),
+ $cookie->isHttpOnly() ?? true
+ );
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sendHeaders()
+ {
+ $this->prepare();
+ if (! $this->getRequest()->isApiRequest()) {
+ $this->sendCookies();
+ }
+ return parent::sendHeaders();
+ }
+
+ /**
+ * Copies non-body-related response data from $response
+ *
+ * @param Response $response
+ *
+ * @return $this
+ */
+ protected function copyMetaDataFrom(self $response)
+ {
+ $this->_headers = $response->_headers;
+ $this->_headersRaw = $response->_headersRaw;
+ $this->_httpResponseCode = $response->_httpResponseCode;
+ $this->headersSentThrowsException = $response->headersSentThrowsException;
+ return $this;
+ }
+}
diff --git a/library/Icinga/Web/Response/JsonResponse.php b/library/Icinga/Web/Response/JsonResponse.php
new file mode 100644
index 0000000..025e88d
--- /dev/null
+++ b/library/Icinga/Web/Response/JsonResponse.php
@@ -0,0 +1,241 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Response;
+
+use Icinga\Util\Json;
+use Zend_Controller_Action_HelperBroker;
+use Icinga\Web\Response;
+
+/**
+ * HTTP response in JSON format
+ */
+class JsonResponse extends Response
+{
+ /**
+ * {@inheritdoc}
+ */
+ const DEFAULT_CONTENT_TYPE = 'application/json';
+
+ /**
+ * Status identifier for failed API calls due to an error on the server
+ *
+ * @var string
+ */
+ const STATUS_ERROR = 'error';
+
+ /**
+ * Status identifier for rejected API calls most due to invalid data or call conditions
+ *
+ * @var string
+ */
+ const STATUS_FAIL = 'fail';
+
+ /**
+ * Status identifier for successful API requests
+ *
+ * @var string
+ */
+ const STATUS_SUCCESS = 'success';
+
+ /**
+ * JSON encoding options
+ *
+ * @var int
+ */
+ protected $encodingOptions = 0;
+
+ /**
+ * Whether to automatically sanitize invalid UTF-8 (if any)
+ *
+ * @var bool
+ */
+ protected $autoSanitize = false;
+
+ /**
+ * Error message if the API call failed due to a server error
+ *
+ * @var string|null
+ */
+ protected $errorMessage;
+
+ /**
+ * Fail data for rejected API calls
+ *
+ * @var array|null
+ */
+ protected $failData;
+
+ /**
+ * API request status
+ *
+ * @var string
+ */
+ protected $status;
+
+ /**
+ * Success data for successful API requests
+ *
+ * @var array|null
+ */
+ protected $successData;
+
+ /**
+ * Get the JSON encoding options
+ *
+ * @return int
+ */
+ public function getEncodingOptions()
+ {
+ return $this->encodingOptions;
+ }
+
+ /**
+ * Set the JSON encoding options
+ *
+ * @param int $encodingOptions
+ *
+ * @return $this
+ */
+ public function setEncodingOptions($encodingOptions)
+ {
+ $this->encodingOptions = (int) $encodingOptions;
+ return $this;
+ }
+
+ /**
+ * Get whether to automatically sanitize invalid UTF-8 (if any)
+ *
+ * @return bool
+ */
+ public function getAutoSanitize()
+ {
+ return $this->autoSanitize;
+ }
+
+ /**
+ * Set whether to automatically sanitize invalid UTF-8 (if any)
+ *
+ * @param bool $autoSanitize
+ *
+ * @return $this
+ */
+ public function setAutoSanitize($autoSanitize = true)
+ {
+ $this->autoSanitize = $autoSanitize;
+
+ return $this;
+ }
+
+ /**
+ * Get the error message if the API call failed due to a server error
+ *
+ * @return string|null
+ */
+ public function getErrorMessage()
+ {
+ return $this->errorMessage;
+ }
+
+ /**
+ * Set the error message if the API call failed due to a server error
+ *
+ * @param string $errorMessage
+ *
+ * @return $this
+ */
+ public function setErrorMessage($errorMessage)
+ {
+ $this->errorMessage = (string) $errorMessage;
+ $this->status = static::STATUS_ERROR;
+ return $this;
+ }
+
+ /**
+ * Get the fail data for rejected API calls
+ *
+ * @return array|null
+ */
+ public function getFailData()
+ {
+ return (! is_array($this->failData) || empty($this->failData)) ? null : $this->failData;
+ }
+
+ /**
+ * Set the fail data for rejected API calls
+ *
+ * @param array $failData
+ *
+ * @return $this
+ */
+ public function setFailData(array $failData)
+ {
+ $this->failData = $failData;
+ $this->status = static::STATUS_FAIL;
+ return $this;
+ }
+
+ /**
+ * Get the data for successful API requests
+ *
+ * @return array|null
+ */
+ public function getSuccessData()
+ {
+ return (! is_array($this->successData) || empty($this->successData)) ? null : $this->successData;
+ }
+
+ /**
+ * Set the data for successful API requests
+ *
+ * @param array $successData
+ *
+ * @return $this
+ */
+ public function setSuccessData(array $successData = null)
+ {
+ $this->successData = $successData;
+ $this->status = static::STATUS_SUCCESS;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function outputBody()
+ {
+ $body = array(
+ 'status' => $this->status
+ );
+ switch ($this->status) {
+ /** @noinspection PhpMissingBreakStatementInspection */
+ case static::STATUS_ERROR:
+ $body['message'] = $this->getErrorMessage();
+ // Fallthrough
+ case static::STATUS_FAIL:
+ $failData = $this->getFailData();
+ if ($failData !== null || $this->status === static::STATUS_FAIL) {
+ $body['data'] = $failData;
+ }
+ break;
+ case static::STATUS_SUCCESS:
+ $body['data'] = $this->getSuccessData();
+ break;
+ }
+ echo $this->getAutoSanitize()
+ ? Json::sanitize($body, $this->getEncodingOptions())
+ : Json::encode($body, $this->getEncodingOptions());
+ }
+
+ /**
+ * Send the response, including all headers, excluding a rendered view.
+ *
+ * @return never
+ */
+ public function sendResponse()
+ {
+ Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer')->setNoRender(true);
+ parent::sendResponse();
+ exit;
+ }
+}
diff --git a/library/Icinga/Web/Session.php b/library/Icinga/Web/Session.php
new file mode 100644
index 0000000..40df89f
--- /dev/null
+++ b/library/Icinga/Web/Session.php
@@ -0,0 +1,54 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Web\Session\PhpSession;
+use Icinga\Web\Session\Session as BaseSession;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Session container
+ */
+class Session
+{
+ /**
+ * The current session
+ *
+ * @var BaseSession $session
+ */
+ private static $session;
+
+ /**
+ * Create the session
+ *
+ * @param BaseSession $session
+ *
+ * @return BaseSession
+ */
+ public static function create(BaseSession $session = null)
+ {
+ if ($session === null) {
+ self::$session = PhpSession::create();
+ } else {
+ self::$session = $session;
+ }
+
+ return self::$session;
+ }
+
+ /**
+ * Return the current session
+ *
+ * @return BaseSession
+ * @throws ProgrammingError
+ */
+ public static function getSession()
+ {
+ if (self::$session === null) {
+ self::create();
+ }
+
+ return self::$session;
+ }
+}
diff --git a/library/Icinga/Web/Session/Php72Session.php b/library/Icinga/Web/Session/Php72Session.php
new file mode 100644
index 0000000..e6a6b19
--- /dev/null
+++ b/library/Icinga/Web/Session/Php72Session.php
@@ -0,0 +1,37 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Session;
+
+use Icinga\Application\Logger;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Web\Cookie;
+
+/**
+ * Session implementation in PHP
+ */
+class Php72Session extends PhpSession
+{
+ /**
+ * Open a PHP session
+ */
+ protected function open()
+ {
+ session_name($this->sessionName);
+
+ $cookie = new Cookie('bogus');
+ session_set_cookie_params(
+ 0,
+ $cookie->getPath(),
+ $cookie->getDomain(),
+ $cookie->isSecure(),
+ true
+ );
+
+ session_start(array(
+ 'use_cookies' => true,
+ 'use_only_cookies' => true,
+ 'use_trans_sid' => false
+ ));
+ }
+}
diff --git a/library/Icinga/Web/Session/PhpSession.php b/library/Icinga/Web/Session/PhpSession.php
new file mode 100644
index 0000000..36dd84e
--- /dev/null
+++ b/library/Icinga/Web/Session/PhpSession.php
@@ -0,0 +1,256 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Session;
+
+use Icinga\Application\Logger;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Web\Cookie;
+
+/**
+ * Session implementation in PHP
+ */
+class PhpSession extends Session
+{
+ /**
+ * The namespace prefix
+ *
+ * Used to differentiate between standard session keys and namespace identifiers
+ */
+ const NAMESPACE_PREFIX = 'ns.';
+
+ /**
+ * Whether the session has already been closed
+ *
+ * @var bool
+ */
+ protected $hasBeenTouched = false;
+
+ /**
+ * Name of the session
+ *
+ * @var string
+ */
+ protected $sessionName = 'Icingaweb2';
+
+ /**
+ * Create a new PHPSession object using the provided options (if any)
+ *
+ * @param array $options An optional array of ini options to set
+ *
+ * @return static
+ *
+ * @throws ConfigurationError
+ * @see http://php.net/manual/en/session.configuration.php
+ */
+ public static function create(array $options = null)
+ {
+ return version_compare(PHP_VERSION, '7.2.0') < 0 ? new self($options) : new Php72Session($options);
+ }
+
+ /**
+ * Create a new PHPSession object using the provided options (if any)
+ *
+ * @param array $options An optional array of ini options to set
+ *
+ * @throws ConfigurationError
+ * @see http://php.net/manual/en/session.configuration.php
+ */
+ public function __construct(array $options = null)
+ {
+ $defaultCookieOptions = array(
+ 'use_trans_sid' => false,
+ 'use_cookies' => true,
+ 'cookie_httponly' => true,
+ 'use_only_cookies' => true
+ );
+
+ if (version_compare(PHP_VERSION, '7.1.0') < 0) {
+ $defaultCookieOptions['hash_function'] = true;
+ $defaultCookieOptions['hash_bits_per_character'] = 5;
+ } else {
+ $defaultCookieOptions['sid_bits_per_character'] = 5;
+ }
+
+ if ($options !== null) {
+ $options = array_merge($defaultCookieOptions, $options);
+ } else {
+ $options = $defaultCookieOptions;
+ }
+
+ if (array_key_exists('test_session_name', $options)) {
+ $this->sessionName = $options['test_session_name'];
+ unset($options['test_session_name']);
+ }
+
+ foreach ($options as $sessionVar => $value) {
+ if (ini_set("session." . $sessionVar, $value) === false) {
+ Logger::warning(
+ 'Could not set php.ini setting %s = %s. This might affect your sessions behaviour.',
+ $sessionVar,
+ $value
+ );
+ }
+ }
+
+ $sessionSavePath = session_save_path() ?: sys_get_temp_dir();
+ if (session_module_name() === 'files' && !is_writable($sessionSavePath)) {
+ throw new ConfigurationError("Can't save session, path '$sessionSavePath' is not writable.");
+ }
+
+ if ($this->exists()) {
+ // We do not want to start a new session here if there is not any
+ $this->read();
+ }
+ }
+
+ /**
+ * Open a PHP session
+ */
+ protected function open()
+ {
+ session_name($this->sessionName);
+
+ if ($this->hasBeenTouched) {
+ $cacheLimiter = ini_get('session.cache_limiter');
+ ini_set('session.use_cookies', false);
+ ini_set('session.use_only_cookies', false);
+ ini_set('session.cache_limiter', null);
+ }
+
+ $cookie = new Cookie('bogus');
+ session_set_cookie_params(
+ 0,
+ $cookie->getPath(),
+ $cookie->getDomain(),
+ $cookie->isSecure(),
+ true
+ );
+
+ session_start();
+
+ if ($this->hasBeenTouched) {
+ ini_set('session.use_cookies', true);
+ ini_set('session.use_only_cookies', true);
+ /** @noinspection PhpUndefinedVariableInspection */
+ ini_set('session.cache_limiter', $cacheLimiter);
+ }
+ }
+
+ /**
+ * Read all values written to the underling session and make them accessible.
+ */
+ public function read()
+ {
+ $this->clear();
+ $this->open();
+
+ foreach ($_SESSION as $key => $value) {
+ if (strpos($key, self::NAMESPACE_PREFIX) === 0) {
+ $namespace = new SessionNamespace();
+ $namespace->setAll($value);
+ $this->namespaces[substr($key, strlen(self::NAMESPACE_PREFIX))] = $namespace;
+ } else {
+ $this->set($key, $value);
+ }
+ }
+
+ session_write_close();
+ $this->hasBeenTouched = true;
+ }
+
+ /**
+ * Write all values of this session object to the underlying session implementation
+ */
+ public function write()
+ {
+ $this->open();
+
+ foreach ($this->removed as $key) {
+ unset($_SESSION[$key]);
+ }
+ foreach ($this->values as $key => $value) {
+ $_SESSION[$key] = $value;
+ }
+ foreach ($this->removedNamespaces as $identifier) {
+ unset($_SESSION[self::NAMESPACE_PREFIX . $identifier]);
+ }
+ foreach ($this->namespaces as $identifier => $namespace) {
+ $_SESSION[self::NAMESPACE_PREFIX . $identifier] = $namespace->getAll();
+ }
+
+ session_write_close();
+ $this->hasBeenTouched = true;
+ }
+
+ /**
+ * Delete the current session, causing all session information to be lost
+ */
+ public function purge()
+ {
+ $this->open();
+ $_SESSION = array();
+ $this->clear();
+ session_destroy();
+ $this->clearCookies();
+ session_write_close();
+ $this->hasBeenTouched = true;
+ }
+
+ /**
+ * Remove session cookies
+ */
+ protected function clearCookies()
+ {
+ if (ini_get('session.use_cookies')) {
+ Logger::debug('Clear session cookie');
+ $params = session_get_cookie_params();
+ setcookie(
+ session_name(),
+ '',
+ time() - 42000,
+ $params['path'],
+ $params['domain'],
+ $params['secure'],
+ $params['httponly']
+ );
+ }
+ }
+
+ /**
+ * @see Session::getId()
+ */
+ public function getId()
+ {
+ if (($id = session_id()) === '') {
+ // Make sure we actually get a id
+ $this->open();
+ session_write_close();
+ $this->hasBeenTouched = true;
+ $id = session_id();
+ }
+
+ return $id;
+ }
+
+ /**
+ * Assign a new sessionId to the currently active session
+ */
+ public function refreshId()
+ {
+ $this->open();
+ if ($this->exists()) {
+ session_regenerate_id();
+ }
+ session_write_close();
+ $this->hasBeenTouched = true;
+ }
+
+ /**
+ * @see Session::exists()
+ */
+ public function exists()
+ {
+ return isset($_COOKIE[$this->sessionName]);
+ }
+}
diff --git a/library/Icinga/Web/Session/Session.php b/library/Icinga/Web/Session/Session.php
new file mode 100644
index 0000000..e73e9b4
--- /dev/null
+++ b/library/Icinga/Web/Session/Session.php
@@ -0,0 +1,126 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Session;
+
+use Icinga\Exception\NotImplementedError;
+
+/**
+ * Base class for handling sessions
+ */
+abstract class Session extends SessionNamespace
+{
+ /**
+ * Container for session namespaces
+ *
+ * @var array
+ */
+ protected $namespaces = array();
+
+ /**
+ * The identifiers of all namespaces removed from this session
+ *
+ * @var array
+ */
+ protected $removedNamespaces = array();
+
+ /**
+ * Read all values from the underlying session implementation
+ */
+ abstract public function read();
+
+ /**
+ * Persists changes to the underlying session implementation
+ */
+ public function write()
+ {
+ throw new NotImplementedError('You are required to implement write() in your session implementation');
+ }
+
+ /**
+ * Return whether a session exists
+ *
+ * @return bool
+ */
+ abstract public function exists();
+
+ /**
+ * Purge session
+ */
+ abstract public function purge();
+
+ /**
+ * Assign a new session id to this session.
+ */
+ abstract public function refreshId();
+
+ /**
+ * Return the id of this session
+ *
+ * @return string
+ */
+ abstract public function getId();
+
+ /**
+ * Get or create a new session namespace
+ *
+ * @param string $identifier The namespace's identifier
+ *
+ * @return SessionNamespace
+ */
+ public function getNamespace($identifier)
+ {
+ if (!isset($this->namespaces[$identifier])) {
+ if (in_array($identifier, $this->removedNamespaces, true)) {
+ unset($this->removedNamespaces[array_search($identifier, $this->removedNamespaces, true)]);
+ }
+
+ $this->namespaces[$identifier] = new SessionNamespace();
+ }
+
+ return $this->namespaces[$identifier];
+ }
+
+ /**
+ * Return whether the given session namespace exists
+ *
+ * @param string $identifier The namespace's identifier to check
+ *
+ * @return bool
+ */
+ public function hasNamespace($identifier)
+ {
+ return isset($this->namespaces[$identifier]);
+ }
+
+ /**
+ * Remove the given session namespace
+ *
+ * @param string $identifier The identifier of the namespace to remove
+ */
+ public function removeNamespace($identifier)
+ {
+ unset($this->namespaces[$identifier]);
+ $this->removedNamespaces[] = $identifier;
+ }
+
+ /**
+ * Return whether the session has changed
+ *
+ * @return bool
+ */
+ public function hasChanged()
+ {
+ return parent::hasChanged() || false === empty($this->namespaces) || false === empty($this->removedNamespaces);
+ }
+
+ /**
+ * Clear all values and namespaces from the session cache
+ */
+ public function clear()
+ {
+ parent::clear();
+ $this->namespaces = array();
+ $this->removedNamespaces = array();
+ }
+}
diff --git a/library/Icinga/Web/Session/SessionNamespace.php b/library/Icinga/Web/Session/SessionNamespace.php
new file mode 100644
index 0000000..1c9c13f
--- /dev/null
+++ b/library/Icinga/Web/Session/SessionNamespace.php
@@ -0,0 +1,201 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Session;
+
+use Exception;
+use ArrayIterator;
+use Icinga\Exception\IcingaException;
+use IteratorAggregate;
+use Traversable;
+
+/**
+ * Container for session values
+ */
+class SessionNamespace implements IteratorAggregate
+{
+ /**
+ * The actual values stored in this container
+ *
+ * @var array
+ */
+ protected $values = array();
+
+ /**
+ * The names of all values removed from this container
+ *
+ * @var array
+ */
+ protected $removed = array();
+
+ /**
+ * Return an iterator for all values in this namespace
+ *
+ * @return ArrayIterator
+ */
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->getAll());
+ }
+
+ /**
+ * Set a session value by property access
+ *
+ * @param string $key The value's name
+ * @param mixed $value The value
+ */
+ public function __set($key, $value)
+ {
+ $this->set($key, $value);
+ }
+
+ /**
+ * Return a session value by property access
+ *
+ * @param string $key The value's name
+ *
+ * @return mixed The value
+ * @throws Exception When the given value-name is not found
+ */
+ public function __get($key)
+ {
+ if (!array_key_exists($key, $this->values)) {
+ throw new IcingaException(
+ 'Cannot access non-existent session value "%s"',
+ $key
+ );
+ }
+
+ return $this->get($key);
+ }
+
+ /**
+ * Return whether the given session value is set
+ *
+ * @param string $key The value's name
+ * @return bool
+ */
+ public function __isset($key)
+ {
+ return isset($this->values[$key]);
+ }
+
+ /**
+ * Unset the given session value
+ *
+ * @param string $key The value's name
+ */
+ public function __unset($key)
+ {
+ $this->delete($key);
+ }
+
+ /**
+ * Setter for session values
+ *
+ * @param string $key Name of value
+ * @param mixed $value Value to set
+ *
+ * @return $this
+ */
+ public function set($key, $value)
+ {
+ $this->values[$key] = $value;
+
+ if (in_array($key, $this->removed, true)) {
+ unset($this->removed[array_search($key, $this->removed, true)]);
+ }
+
+ return $this;
+ }
+
+ public function setByRef($key, &$value)
+ {
+ $this->values[$key] = & $value;
+
+ if (in_array($key, $this->removed, true)) {
+ unset($this->removed[array_search($key, $this->removed, true)]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Getter for session values
+ *
+ * @param string $key Name of the value to return
+ * @param mixed $default Default value to return
+ *
+ * @return mixed
+ */
+ public function get($key, $default = null)
+ {
+ return isset($this->values[$key]) ? $this->values[$key] : $default;
+ }
+
+ public function & getByRef($key, $default = null)
+ {
+ $value = $default;
+ if (isset($this->values[$key])) {
+ $value = & $this->values[$key];
+ }
+
+ return $value;
+ }
+
+ /**
+ * Delete the given value from the session
+ *
+ * @param string $key The value's name
+ */
+ public function delete($key)
+ {
+ $this->removed[] = $key;
+ unset($this->values[$key]);
+ }
+
+ /**
+ * Getter for all session values
+ *
+ * @return array
+ */
+ public function getAll()
+ {
+ return $this->values;
+ }
+
+ /**
+ * Put an array into the session
+ *
+ * @param array $values Values to set
+ * @param bool $overwrite Overwrite existing values
+ */
+ public function setAll(array $values, $overwrite = false)
+ {
+ foreach ($values as $key => $value) {
+ if ($this->get($key, $value) !== $value && !$overwrite) {
+ continue;
+ }
+ $this->set($key, $value);
+ }
+ }
+
+ /**
+ * Return whether the session namespace has been changed
+ *
+ * @return bool
+ */
+ public function hasChanged()
+ {
+ return false === empty($this->values) || false === empty($this->removed);
+ }
+
+ /**
+ * Clear all values from the session namespace
+ */
+ public function clear()
+ {
+ $this->values = array();
+ $this->removed = array();
+ }
+}
diff --git a/library/Icinga/Web/StyleSheet.php b/library/Icinga/Web/StyleSheet.php
new file mode 100644
index 0000000..65cbb97
--- /dev/null
+++ b/library/Icinga/Web/StyleSheet.php
@@ -0,0 +1,342 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Exception;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Authentication\Auth;
+use Icinga\Exception\IcingaException;
+
+/**
+ * Send CSS for Web 2 and all loaded modules to the client
+ */
+class StyleSheet
+{
+ /**
+ * The name of the default theme
+ *
+ * @var string
+ */
+ const DEFAULT_THEME = 'Icinga';
+
+ /**
+ * The name of the default theme mode
+ *
+ * @var string
+ */
+ const DEFAULT_MODE = 'none';
+
+ /**
+ * The themes that are compatible with the default theme
+ *
+ * @var array
+ */
+ const THEME_WHITELIST = [
+ 'colorblind',
+ 'high-contrast',
+ 'Winter'
+ ];
+
+ /**
+ * Sequence that signals that a theme supports light mode
+ *
+ * @var string
+ */
+ const LIGHT_MODE_IDENTIFIER = '@light-mode:';
+
+ /**
+ * Array of core LESS files Web 2 sends to the client
+ *
+ * @var string[]
+ */
+ protected static $lessFiles = [
+ '../application/fonts/fontello-ifont/css/ifont-embedded.css',
+ 'css/vendor/normalize.css',
+ 'css/icinga/base.less',
+ 'css/icinga/badges.less',
+ 'css/icinga/configmenu.less',
+ 'css/icinga/mixins.less',
+ 'css/icinga/grid.less',
+ 'css/icinga/nav.less',
+ 'css/icinga/main.less',
+ 'css/icinga/animation.less',
+ 'css/icinga/layout.less',
+ 'css/icinga/layout-structure.less',
+ 'css/icinga/menu.less',
+ 'css/icinga/tabs.less',
+ 'css/icinga/forms.less',
+ 'css/icinga/setup.less',
+ 'css/icinga/widgets.less',
+ 'css/icinga/login.less',
+ 'css/icinga/about.less',
+ 'css/icinga/controls.less',
+ 'css/icinga/dev.less',
+ 'css/icinga/spinner.less',
+ 'css/icinga/compat.less',
+ 'css/icinga/print.less',
+ 'css/icinga/responsive.less',
+ 'css/icinga/modal.less',
+ 'css/icinga/audit.less',
+ 'css/icinga/health.less',
+ 'css/icinga/php-diff.less',
+ 'css/icinga/pending-migration.less',
+ ];
+
+ /**
+ * Application instance
+ *
+ * @var \Icinga\Application\EmbeddedWeb
+ */
+ protected $app;
+
+ /** @var string[] Pre-compiled CSS files */
+ protected $cssFiles = [];
+
+ /**
+ * Less compiler
+ *
+ * @var LessCompiler
+ */
+ protected $lessCompiler;
+
+ /**
+ * Path to the public directory
+ *
+ * @var string
+ */
+ protected $pubPath;
+
+ /**
+ * Create the StyleSheet
+ */
+ public function __construct()
+ {
+ $app = Icinga::app();
+ $this->app = $app;
+ $this->lessCompiler = new LessCompiler();
+ $this->pubPath = $app->getBaseDir('public');
+ $this->collect();
+ }
+
+ /**
+ * Collect Web 2 and module LESS files and add them to the LESS compiler
+ */
+ protected function collect()
+ {
+ foreach ($this->app->getLibraries() as $library) {
+ foreach ($library->getCssAssets() as $lessFile) {
+ if (substr($lessFile, -4) === '.css') {
+ $this->cssFiles[] = $lessFile;
+ } else {
+ $this->lessCompiler->addLessFile($lessFile);
+ }
+ }
+ }
+
+ foreach (self::$lessFiles as $lessFile) {
+ $this->lessCompiler->addLessFile($this->pubPath . '/' . $lessFile);
+ }
+
+ $mm = $this->app->getModuleManager();
+
+ foreach ($mm->getLoadedModules() as $moduleName => $module) {
+ if ($module->hasCss()) {
+ foreach ($module->getCssFiles() as $lessFilePath) {
+ $this->lessCompiler->addModuleLessFile($moduleName, $lessFilePath);
+ }
+ }
+ }
+
+ $themingConfig = $this->app->getConfig()->getSection('themes');
+ $defaultTheme = $themingConfig->get('default');
+ $theme = null;
+ if ($defaultTheme !== null && $defaultTheme !== self::DEFAULT_THEME) {
+ $theme = $defaultTheme;
+ }
+
+ if (! (bool) $themingConfig->get('disabled', false)) {
+ $auth = Auth::getInstance();
+ if ($auth->isAuthenticated()) {
+ $userTheme = $auth->getUser()->getPreferences()->getValue('icingaweb', 'theme');
+ if ($userTheme !== null) {
+ $theme = $userTheme;
+ }
+ }
+ }
+
+ if ($themePath = self::getThemeFile($theme)) {
+ if ($this->app->isCli() || is_file($themePath) && is_readable($themePath)) {
+ $this->lessCompiler->setTheme($themePath);
+ } else {
+ $themePath = null;
+ Logger::warning(sprintf(
+ 'Theme "%s" set by user "%s" has not been found.',
+ $theme,
+ ($user = Auth::getInstance()->getUser()) !== null ? $user->getUsername() : 'anonymous'
+ ));
+ }
+ }
+
+ if (! $themePath || in_array($theme, self::THEME_WHITELIST, true)) {
+ $this->lessCompiler->addLessFile($this->pubPath . '/css/icinga/login-orbs.less');
+ }
+
+ $mode = 'none';
+ if ($user = Auth::getInstance()->getUser()) {
+ $file = $themePath !== null ? @file_get_contents($themePath) : false;
+ if (! $file || strpos($file, self::LIGHT_MODE_IDENTIFIER) !== false) {
+ $mode = $user->getPreferences()->getValue('icingaweb', 'theme_mode', self::DEFAULT_MODE);
+ }
+ }
+
+ $this->lessCompiler->setThemeMode($this->pubPath . '/css/modes/'. $mode . '.less');
+ }
+
+ /**
+ * Get all collected files
+ *
+ * @return string[]
+ */
+ protected function getFiles(): array
+ {
+ return array_merge($this->cssFiles, $this->lessCompiler->getLessFiles());
+ }
+
+ /**
+ * Get the stylesheet for PDF export
+ *
+ * @return $this
+ */
+ public static function forPdf()
+ {
+ $styleSheet = new self();
+ $styleSheet->lessCompiler->setTheme(null);
+ $styleSheet->lessCompiler->setThemeMode($styleSheet->pubPath . '/css/modes/none.less');
+ $styleSheet->lessCompiler->addLessFile($styleSheet->pubPath . '/css/pdf/pdfprint.less');
+ // TODO(el): Caching
+ return $styleSheet;
+ }
+
+ /**
+ * Render the stylesheet
+ *
+ * @param bool $minified Whether to compress the stylesheet
+ *
+ * @return string CSS
+ */
+ public function render($minified = false)
+ {
+ if ($minified) {
+ $this->lessCompiler->compress();
+ }
+
+ $css = '';
+ foreach ($this->cssFiles as $cssFile) {
+ $css .= file_get_contents($cssFile);
+ }
+
+ return $css . $this->lessCompiler->render();
+ }
+
+ /**
+ * Send the stylesheet to the client
+ *
+ * Does not cache the stylesheet if the HTTP header Cache-Control or Pragma is set to no-cache.
+ *
+ * @param bool $minified Whether to compress the stylesheet
+ */
+ public static function send($minified = false)
+ {
+ $styleSheet = new self();
+
+ $request = $styleSheet->app->getRequest();
+ $response = $styleSheet->app->getResponse();
+ $response->setHeader('Cache-Control', 'private,no-cache,must-revalidate', true);
+
+ $noCache = $request->getHeader('Cache-Control') === 'no-cache' || $request->getHeader('Pragma') === 'no-cache';
+
+ $collectedFiles = $styleSheet->getFiles();
+ if (! $noCache && FileCache::etagMatchesFiles($collectedFiles)) {
+ $response
+ ->setHttpResponseCode(304)
+ ->sendHeaders();
+ return;
+ }
+
+ $etag = FileCache::etagForFiles($collectedFiles);
+
+ $response->setHeader('ETag', $etag, true)
+ ->setHeader('Content-Type', 'text/css', true);
+
+ $cacheFile = 'icinga-' . $etag . ($minified ? '.min' : '') . '.css';
+ $cache = FileCache::instance();
+
+ if (! $noCache && $cache->has($cacheFile)) {
+ $response->setBody($cache->get($cacheFile));
+ } else {
+ $css = $styleSheet->render($minified);
+ $response->setBody($css);
+ $cache->store($cacheFile, $css);
+ }
+
+ $response->sendResponse();
+ }
+
+ /**
+ * Render the stylesheet
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ try {
+ return $this->render();
+ } catch (Exception $e) {
+ Logger::error($e);
+ return IcingaException::describe($e);
+ }
+ }
+
+ /**
+ * Get the path to the current LESS theme file
+ *
+ * @param $theme
+ *
+ * @return string|null Return null if self::DEFAULT_THEME is set as theme, path otherwise
+ */
+ public static function getThemeFile($theme)
+ {
+ $app = Icinga::app();
+
+ if ($theme && $theme !== self::DEFAULT_THEME) {
+ if (Hook::has('ThemeLoader')) {
+ try {
+ $path = Hook::first('ThemeLoader')->getThemeFile($theme);
+ } catch (Exception $e) {
+ Logger::error('Failed to call ThemeLoader hook: %s', $e);
+ $path = null;
+ }
+
+ if ($path !== null) {
+ return $path;
+ }
+ }
+
+ if (($pos = strpos($theme, '/')) !== false) {
+ $moduleName = substr($theme, 0, $pos);
+ $theme = substr($theme, $pos + 1);
+ if ($app->getModuleManager()->hasLoaded($moduleName)) {
+ $module = $app->getModuleManager()->getModule($moduleName);
+
+ return $module->getCssDir() . '/themes/' . $theme . '.less';
+ }
+ } else {
+ return $app->getBaseDir('public') . '/css/themes/' . $theme . '.less';
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/library/Icinga/Web/Url.php b/library/Icinga/Web/Url.php
new file mode 100644
index 0000000..c90ca48
--- /dev/null
+++ b/library/Icinga/Web/Url.php
@@ -0,0 +1,806 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Application\Icinga;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Url class that provides convenient access to parameters, allows to modify query parameters and
+ * returns Urls reflecting all changes made to the url and to the parameters.
+ *
+ * Direct instantiation is prohibited and should be done either with @see Url::fromRequest() or
+ * @see Url::fromPath()
+ */
+class Url
+{
+ /**
+ * Whether this url points to an external resource
+ *
+ * @var bool
+ */
+ protected $external;
+
+ /**
+ * An array of all parameters stored in this Url
+ *
+ * @var UrlParams
+ */
+ protected $params;
+
+ /**
+ * The site anchor after the '#'
+ *
+ * @var string
+ */
+ protected $anchor = '';
+
+ /**
+ * The relative path of this Url, without query parameters
+ *
+ * @var string
+ */
+ protected $path = '';
+
+ /**
+ * The basePath of this Url
+ *
+ * @var string
+ */
+ protected $basePath;
+
+ /**
+ * The host of this Url
+ *
+ * @var string
+ */
+ protected $host;
+
+ /**
+ * The port of this Url
+ *
+ * @var string
+ */
+ protected $port;
+
+ /**
+ * The scheme of this Url
+ *
+ * @var string
+ */
+ protected $scheme;
+
+ /**
+ * The username passed with this Url
+ *
+ * @var string
+ */
+ protected $username;
+
+ /**
+ * The password passed with this Url
+ *
+ * @var string
+ */
+ protected $password;
+
+ protected function __construct()
+ {
+ $this->params = UrlParams::fromQueryString(''); // TODO: ::create()
+ }
+
+ /**
+ * Create a new Url class representing the current request
+ *
+ * If $params are given, those will be added to the request's parameters
+ * and overwrite any existing parameters
+ *
+ * @param UrlParams|array $params Parameters that should additionally be considered for the url
+ * @param Request $request A request to use instead of the default one
+ *
+ * @return static
+ */
+ public static function fromRequest($params = array(), $request = null)
+ {
+ if ($request === null) {
+ $request = static::getRequest();
+ }
+
+ $url = new static();
+ $url->setPath(ltrim($request->getPathInfo(), '/'));
+
+ // $urlParams = UrlParams::fromQueryString($request->getQuery());
+ if (isset($_SERVER['QUERY_STRING'])) {
+ $urlParams = UrlParams::fromQueryString($_SERVER['QUERY_STRING']);
+ } else {
+ $urlParams = UrlParams::fromQueryString('');
+ foreach ($request->getQuery() as $k => $v) {
+ $urlParams->set($k, $v);
+ }
+ }
+
+ foreach ($params as $k => $v) {
+ $urlParams->set($k, $v);
+ }
+ $url->setParams($urlParams);
+ $url->setBasePath($request->getBaseUrl());
+ return $url;
+ }
+
+ /**
+ * Return a request object that should be used for determining the URL
+ *
+ * @return Request
+ */
+ protected static function getRequest()
+ {
+ $app = Icinga::app();
+ if ($app->isCli()) {
+ throw new ProgrammingError(
+ 'Url::fromRequest and Url::fromPath are currently not supported for CLI operations'
+ );
+ } else {
+ return $app->getRequest();
+ }
+ }
+
+ /**
+ * Create a new Url class representing the given url
+ *
+ * If $params are given, those will be added to the urls parameters
+ * and overwrite any existing parameters
+ *
+ * @param string $url The string representation of the url to parse
+ * @param array $params An array of parameters that should additionally be considered for the url
+ * @param Request $request A request to use instead of the default one
+ *
+ * @return static
+ */
+ public static function fromPath($url, array $params = array(), $request = null)
+ {
+ if ($request === null) {
+ $request = static::getRequest();
+ }
+
+ if (! is_string($url)) {
+ throw new ProgrammingError(
+ 'url %s is not a string',
+ var_export($url, true)
+ );
+ }
+
+ $urlObject = new static();
+
+ if ($url === '#') {
+ $urlObject->setPath($url);
+ return $urlObject;
+ }
+
+ $urlParts = parse_url($url);
+ if (isset($urlParts['scheme']) && (
+ $urlParts['scheme'] !== $request->getScheme()
+ || (isset($urlParts['host']) && $urlParts['host'] !== $request->getServer('SERVER_NAME'))
+ || (isset($urlParts['port']) && $urlParts['port'] != $request->getServer('SERVER_PORT')))
+ ) {
+ $urlObject->setIsExternal();
+ }
+
+ if (isset($urlParts['path'])) {
+ $urlPath = $urlParts['path'];
+ if ($urlPath && $urlPath[0] === '/') {
+ if ($urlObject->isExternal() || isset($urlParts['user'])) {
+ $urlPath = ltrim($urlPath, '/');
+ } else {
+ $requestBaseUrl = $request->getBaseUrl();
+ if ($requestBaseUrl && $requestBaseUrl !== '/' && strpos($urlPath, $requestBaseUrl) === 0) {
+ $urlPath = ltrim(substr($urlPath, strlen($requestBaseUrl)), '/');
+ $urlObject->setBasePath($requestBaseUrl);
+ }
+ }
+ } elseif (! $urlObject->isExternal()) {
+ $urlObject->setBasePath($request->getBaseUrl());
+ }
+
+ $urlObject->setPath($urlPath);
+ } elseif (! $urlObject->isExternal()) {
+ $urlObject->setBasePath($request->getBaseUrl());
+ }
+
+ // TODO: This has been used by former filter implementation, remove it:
+ if (isset($urlParts['query'])) {
+ $params = UrlParams::fromQueryString($urlParts['query'])->mergeValues($params);
+ }
+ if (isset($urlParts['fragment'])) {
+ $urlObject->setAnchor($urlParts['fragment']);
+ }
+
+ if (isset($urlParts['user']) || $urlObject->isExternal()) {
+ if (isset($urlParts['user'])) {
+ $urlObject->setUsername($urlParts['user']);
+ }
+ if (isset($urlParts['host'])) {
+ $urlObject->setHost($urlParts['host']);
+ }
+ if (isset($urlParts['port'])) {
+ $urlObject->setPort($urlParts['port']);
+ }
+ if (isset($urlParts['scheme'])) {
+ $urlObject->setScheme($urlParts['scheme']);
+ }
+ if (isset($urlParts['pass'])) {
+ $urlObject->setPassword($urlParts['pass']);
+ }
+ }
+
+ $urlObject->setParams($params);
+ return $urlObject;
+ }
+
+ /**
+ * Create a new filter that needs to fullfill the base filter and the optional filter (if it exists)
+ *
+ * @param string $url The url to apply the new filter to
+ * @param Filter $filter The base filter
+ * @param ?Filter $optional The optional filter
+ *
+ * @return static The altered URL containing the new filter
+ * @throws ProgrammingError
+ */
+ public static function urlAddFilterOptional($url, $filter, $optional)
+ {
+ $url = static::fromPath($url);
+ $f = $filter;
+ if (isset($optional)) {
+ $f = Filter::matchAll($filter, $optional);
+ }
+ return $url->setQueryString($f->toQueryString());
+ }
+
+ /**
+ * Add the given filter to the current filter of the URL
+ *
+ * @param Filter $and
+ *
+ * @return $this
+ */
+ public function addFilter($and)
+ {
+ $this->setQueryString(
+ Filter::fromQueryString($this->getQueryString())
+ ->andFilter($and)
+ ->toQueryString()
+ );
+ return $this;
+ }
+
+ /**
+ * Set the basePath for this url
+ *
+ * @param string $basePath New basePath of this url
+ *
+ * @return $this
+ */
+ public function setBasePath($basePath)
+ {
+ $this->basePath = rtrim($basePath, '/ ');
+ return $this;
+ }
+
+ /**
+ * Return the basePath set for this url
+ *
+ * @return string
+ */
+ public function getBasePath()
+ {
+ return $this->basePath;
+ }
+
+ /**
+ * Set the host for this url
+ *
+ * @param string $host New host of this Url
+ *
+ * @return $this
+ */
+ public function setHost($host)
+ {
+ $this->host = $host;
+ return $this;
+ }
+
+ /**
+ * Return the host set for this url
+ *
+ * @return string
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * Set the port for this url
+ *
+ * @param string $port New port of this url
+ *
+ * @return $this
+ */
+ public function setPort($port)
+ {
+ $this->port = $port;
+ return $this;
+ }
+
+ /**
+ * Return the port set for this url
+ *
+ * @return string
+ */
+ public function getPort()
+ {
+ return $this->port;
+ }
+
+ /**
+ * Set the scheme for this url
+ *
+ * @param string $scheme The scheme used for this url
+ *
+ * @return $this
+ */
+ public function setScheme($scheme)
+ {
+ $this->scheme = $scheme;
+ return $this;
+ }
+
+ /**
+ * Return the scheme set for this url
+ *
+ * @return string
+ */
+ public function getScheme()
+ {
+ return $this->scheme;
+ }
+
+ /**
+ * Set the relative path of this url, without query parameters
+ *
+ * @param string $path The path to set
+ *
+ * @return $this
+ */
+ public function setPath($path)
+ {
+ $this->path = $path;
+ return $this;
+ }
+
+ /**
+ * Return the relative path of this url, without query parameters
+ *
+ * If you want the relative path with query parameters use getRelativeUrl
+ *
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * Set whether this url points to an external resource
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
+ public function setIsExternal($state = true)
+ {
+ $this->external = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether this url points to an external resource
+ *
+ * @return bool
+ */
+ public function isExternal()
+ {
+ return $this->external;
+ }
+
+ /**
+ * Set the username passed with this url
+ *
+ * @param string $username The username to set
+ *
+ * @return $this
+ */
+ public function setUsername($username)
+ {
+ $this->username = $username;
+ return $this;
+ }
+
+ /**
+ * Return the username passed with this url
+ *
+ * @return string
+ */
+ public function getUsername()
+ {
+ return $this->username;
+ }
+
+ /**
+ * Set the username passed with this url
+ *
+ * @param string $password The password to set
+ *
+ * @return $this
+ */
+ public function setPassword($password)
+ {
+ $this->password = $password;
+ return $this;
+ }
+
+ /**
+ * Return the password passed with this url
+ *
+ * @return string
+ */
+ public function getPassword()
+ {
+ return $this->password;
+ }
+
+ /**
+ * Return the relative url
+ *
+ * @return string
+ */
+ public function getRelativeUrl($separator = '&')
+ {
+ $path = $this->buildPathQueryAndFragment($separator);
+ if ($path && $path[0] === '/') {
+ return '';
+ }
+
+ return $path;
+ }
+
+ /**
+ * Return this url's path with its query parameters and fragment as string
+ *
+ * @return string
+ */
+ protected function buildPathQueryAndFragment($querySeparator)
+ {
+ $anchor = $this->getAnchor();
+ if ($anchor) {
+ $anchor = '#' . $anchor;
+ }
+
+ $query = $this->getQueryString($querySeparator);
+ if ($query) {
+ $query = '?' . $query;
+ }
+
+ return $this->getPath() . $query . $anchor;
+ }
+
+ public function setQueryString($queryString)
+ {
+ $this->params = UrlParams::fromQueryString($queryString);
+ return $this;
+ }
+
+ public function getQueryString($separator = null)
+ {
+ return $this->params->toString($separator);
+ }
+
+ /**
+ * Return the absolute url with query parameters as a string
+ *
+ * @return string
+ */
+ public function getAbsoluteUrl($separator = '&')
+ {
+ $path = $this->buildPathQueryAndFragment($separator);
+ if ($path && ($path === '#' || $path[0] === '/')) {
+ return $path;
+ }
+
+ $basePath = $this->getBasePath();
+ if (! $basePath) {
+ $basePath = '/';
+ }
+
+ if ($this->getUsername() || $this->isExternal()) {
+ $urlString = '';
+ if ($this->getScheme()) {
+ $urlString .= $this->getScheme() . '://';
+ }
+ if ($this->getPassword()) {
+ $urlString .= $this->getUsername() . ':' . $this->getPassword() . '@';
+ } elseif ($this->getUsername()) {
+ $urlString .= $this->getUsername() . '@';
+ }
+ if ($this->getHost()) {
+ $urlString .= $this->getHost();
+ }
+ if ($this->getPort()) {
+ $urlString .= ':' . $this->getPort();
+ }
+
+ return $urlString . $basePath . ($basePath !== '/' && $path ? '/' : '') . $path;
+ } else {
+ return $basePath . ($basePath !== '/' && $path ? '/' : '') . $path;
+ }
+ }
+
+ /**
+ * Add a set of parameters to the query part if the keys don't exist yet
+ *
+ * @param array $params The parameters to add
+ *
+ * @return $this
+ */
+ public function addParams(array $params)
+ {
+ foreach ($params as $k => $v) {
+ $this->params->add($k, $v);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set and overwrite the given params if one if the same key already exists
+ *
+ * @param array $params The parameters to set
+ *
+ * @return $this
+ */
+ public function overwriteParams(array $params)
+ {
+ foreach ($params as $k => $v) {
+ $this->params->set($k, $v);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Overwrite the parameters used in the query part
+ *
+ * @param UrlParams|array $params The new parameters to use for the query part
+ *
+ * @return $this
+ */
+ public function setParams($params)
+ {
+ if ($params instanceof UrlParams) {
+ $this->params = $params;
+ } elseif (is_array($params)) {
+ $urlParams = UrlParams::fromQueryString('');
+ foreach ($params as $k => $v) {
+ $urlParams->set($k, $v);
+ }
+ $this->params = $urlParams;
+ } else {
+ throw new ProgrammingError(
+ 'Url params needs to be either an array or an UrlParams instance'
+ );
+ }
+ return $this;
+ }
+
+ /**
+ * Return all parameters that will be used in the query part
+ *
+ * @return UrlParams An instance of UrlParam containing all parameters
+ */
+ public function getParams()
+ {
+ return $this->params;
+ }
+
+ /**
+ * Return true if a urls' query parameter exists, otherwise false
+ *
+ * @param string $param The url parameter name to check
+ *
+ * @return bool
+ */
+ public function hasParam($param)
+ {
+ return $this->params->has($param);
+ }
+
+ /**
+ * Return a url's query parameter if it exists, otherwise $default
+ *
+ * @param string $param A query parameter name to return if existing
+ * @param mixed $default A value to return when the parameter doesn't exist
+ *
+ * @return mixed
+ */
+ public function getParam($param, $default = null)
+ {
+ return $this->params->get($param, $default);
+ }
+
+ /**
+ * Set a single parameter, overwriting any existing one with the same name
+ *
+ * @param string $param The query parameter name
+ * @param array|string|bool $value An array or string to set as the parameter value
+ *
+ * @return $this
+ */
+ public function setParam($param, $value = true)
+ {
+ $this->params->set($param, $value);
+ return $this;
+ }
+
+ /**
+ * Set the url anchor-part
+ *
+ * @param string $anchor The site's anchor string without the '#'
+ *
+ * @return $this
+ */
+ public function setAnchor($anchor)
+ {
+ $this->anchor = $anchor;
+ return $this;
+ }
+
+ /**
+ * Return the url anchor-part
+ *
+ * @return string The site's anchor string without the '#'
+ */
+ public function getAnchor()
+ {
+ return $this->anchor;
+ }
+
+ /**
+ * Remove provided key (if string) or keys (if array of string) from the query parameter array
+ *
+ * @param string|array $keyOrArrayOfKeys An array of strings or a string representing the key(s)
+ * of the parameters to be removed
+ * @return $this
+ */
+ public function remove($keyOrArrayOfKeys)
+ {
+ $this->params->remove($keyOrArrayOfKeys);
+ return $this;
+ }
+
+ /**
+ * Shift a query parameter from this URL if it exists, otherwise $default
+ *
+ * @param string $param Parameter name
+ * @param mixed $default Default value in case $param does not exist
+ *
+ * @return mixed
+ */
+ public function shift($param, $default = null)
+ {
+ return $this->params->shift($param, $default);
+ }
+
+ /**
+ * Whether the given URL matches this URL object
+ *
+ * This does an exact match, parameters MUST be in the same order
+ *
+ * @param Url|string $url the URL to compare against
+ *
+ * @return bool whether the URL matches
+ */
+ public function matches($url)
+ {
+ if (! $url instanceof static) {
+ $url = static::fromPath($url);
+ }
+ return (string) $url === (string) $this;
+ }
+
+ /**
+ * Return a copy of this url without the parameter given
+ *
+ * The argument can be either a single query parameter name or an array of parameter names to
+ * remove from the query list
+ *
+ * @param string|array $keyOrArrayOfKeys A single string or an array containing parameter names
+ *
+ * @return static
+ */
+ public function getUrlWithout($keyOrArrayOfKeys)
+ {
+ return $this->without($keyOrArrayOfKeys);
+ }
+
+ public function without($keyOrArrayOfKeys)
+ {
+ $url = clone($this);
+ $url->remove($keyOrArrayOfKeys);
+ return $url;
+ }
+
+ /**
+ * Return a copy of this url with the given parameter(s)
+ *
+ * The argument can be either a single query parameter name or an array of parameter names to
+ * remove from the query list
+ *
+ * @param string|array $param A single string or an array containing parameter names
+ * @param mixed $values an optional values array
+ *
+ * @return static
+ */
+ public function with($param, $values = null)
+ {
+ $url = clone($this);
+ $url->params->mergeValues($param, $values);
+ return $url;
+ }
+
+ /**
+ * Return a copy of this url with only the given parameter(s)
+ *
+ * The argument can be either a single query parameter name or
+ * an array of parameter names to keep on on the query
+ *
+ * @param string|array $keyOrArrayOfKeys
+ *
+ * @return static
+ */
+ public function onlyWith($keyOrArrayOfKeys)
+ {
+ if (! is_array($keyOrArrayOfKeys)) {
+ $keyOrArrayOfKeys = [$keyOrArrayOfKeys];
+ }
+
+ $url = clone $this;
+ foreach ($url->getParams()->toArray(false) as $param => $value) {
+ if (is_int($param)) {
+ $param = $value;
+ }
+
+ if (! in_array($param, $keyOrArrayOfKeys, true)) {
+ $url->remove($param);
+ }
+ }
+
+ return $url;
+ }
+
+ public function __clone()
+ {
+ $this->params = clone $this->params;
+ }
+
+ /**
+ * Alias for @see Url::getAbsoluteUrl()
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return htmlspecialchars($this->getAbsoluteUrl(), ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8', true);
+ }
+}
diff --git a/library/Icinga/Web/UrlParams.php b/library/Icinga/Web/UrlParams.php
new file mode 100644
index 0000000..2265235
--- /dev/null
+++ b/library/Icinga/Web/UrlParams.php
@@ -0,0 +1,433 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Exception\MissingParameterException;
+
+class UrlParams
+{
+ protected $separator = '&';
+
+ protected $params = array();
+
+ protected $index = array();
+
+ public function isEmpty()
+ {
+ return empty($this->index);
+ }
+
+ public function setSeparator($separator)
+ {
+ $this->separator = $separator;
+ return $this;
+ }
+
+ /**
+ * Get the given parameter
+ *
+ * Returns the last URL param if defined multiple times, $default if not
+ * given at all
+ *
+ * @param string $param The parameter you're interested in
+ * @param string|int|bool|null $default An optional default value
+ *
+ * @return mixed
+ */
+ public function get($param, $default = null)
+ {
+ if (! $this->has($param)) {
+ return $default;
+ }
+
+ return rawurldecode($this->params[ end($this->index[$param]) ][ 1 ]);
+ }
+
+ /**
+ * Require a parameter
+ *
+ * @param string $name Name of the parameter
+ * @param bool $strict Whether the parameter's value must not be the empty string
+ *
+ * @return mixed
+ *
+ * @throws MissingParameterException If the parameter was not given
+ */
+ public function getRequired($name, $strict = true)
+ {
+ if ($this->has($name)) {
+ $value = $this->get($name);
+ if (! $strict || strlen($value) > 0) {
+ return $value;
+ }
+ }
+ $e = new MissingParameterException(t('Required parameter \'%s\' missing'), $name);
+ $e->setParameter($name);
+ throw $e;
+ }
+
+ /**
+ * Get all instances of the given parameter
+ *
+ * Returns an array containing all values defined for a given parameter,
+ * $default if none.
+ *
+ * @param string $param The parameter you're interested in
+ * @param array $default An optional default value
+ *
+ * @return mixed
+ */
+ public function getValues($param, $default = array())
+ {
+ if (! $this->has($param)) {
+ return $default;
+ }
+
+ $ret = array();
+ foreach ($this->index[$param] as $key) {
+ $ret[] = rawurldecode($this->params[$key][1]);
+ }
+ return $ret;
+ }
+
+ /**
+ * Whether the given parameter exists
+ *
+ * Returns true if such a parameter has been defined, false otherwise.
+ *
+ * @param string $param The parameter you're interested in
+ *
+ * @return boolean
+ */
+ public function has($param)
+ {
+ return array_key_exists($param, $this->index);
+ }
+
+ /**
+ * Get and remove the given parameter
+ *
+ * Returns the last URL param if defined multiple times, $default if not
+ * given at all. The parameter will be removed from this object.
+ *
+ * @param string $param The parameter you're interested in
+ * @param string $default An optional default value
+ *
+ * @return mixed
+ */
+ public function shift($param = null, $default = null)
+ {
+ if ($param === null) {
+ if (empty($this->params)) {
+ return $default;
+ }
+ $ret = array_shift($this->params);
+ $ret[0] = rawurldecode($ret[0]);
+ $ret[1] = rawurldecode($ret[1]);
+ } else {
+ if (! $this->has($param)) {
+ return $default;
+ }
+ $key = reset($this->index[$param]);
+ $ret = rawurldecode($this->params[$key][1]);
+ unset($this->params[$key]);
+ }
+
+ $this->reIndexAll();
+ return $ret;
+ }
+
+ /**
+ * Require and remove a parameter
+ *
+ * @param string $name Name of the parameter
+ * @param bool $strict Whether the parameter's value must not be the empty string
+ *
+ * @return mixed
+ *
+ * @throws MissingParameterException If the parameter was not given
+ */
+ public function shiftRequired($name, $strict = true)
+ {
+ if ($this->has($name)) {
+ $value = $this->get($name);
+ if (! $strict || strlen($value) > 0) {
+ $this->shift($name);
+ return $value;
+ }
+ }
+ $e = new MissingParameterException(t('Required parameter \'%s\' missing'), $name);
+ $e->setParameter($name);
+ throw $e;
+ }
+
+ public function addEncoded($param, $value = true)
+ {
+ $this->params[] = array($param, $this->cleanupValue($value));
+ $this->indexLastOne();
+ return $this;
+ }
+
+ protected function urlEncode($value)
+ {
+ return rawurlencode($value instanceof Url ? $value->getAbsoluteUrl() : (string) $value);
+ }
+
+ /**
+ * Add the given parameter with the given value
+ *
+ * This will add the given parameter, regardless of whether it already
+ * exists.
+ *
+ * @param string $param The parameter you're interested in
+ * @param string|bool $value The value to be stored
+ *
+ * @return $this
+ */
+ public function add($param, $value = true)
+ {
+ return $this->addEncoded($this->urlEncode($param), $this->urlEncode($value));
+ }
+
+ /**
+ * Adds a list of parameters
+ *
+ * This may be used with either a list of values for a single parameter or
+ * with a list of parameter / value pairs.
+ *
+ * @param string|array $param Parameter name or param/value list
+ * @param ?array $value The value to be stored
+ *
+ * @return $this
+ */
+ public function addValues($param, $values = null)
+ {
+ if ($values === null && is_array($param)) {
+ foreach ($param as $k => $v) {
+ $this->add($k, $v);
+ }
+ } else {
+ foreach ($values as $value) {
+ $this->add($param, $value);
+ }
+ }
+
+ return $this;
+ }
+
+ protected function clearValues()
+ {
+ $this->params = array();
+ $this->index = array();
+ }
+
+ public function mergeValues($param, $values = null)
+ {
+ if ($values === null && is_array($param)) {
+ foreach ($param as $k => $v) {
+ $this->set($k, $v);
+ }
+ } else {
+ if (! is_array($values)) {
+ $values = array($values);
+ }
+ foreach ($values as $value) {
+ $this->set($param, $value);
+ }
+ }
+
+ return $this;
+ }
+
+ public function setValues($param, $values = null)
+ {
+ $this->clearValues();
+ return $this->addValues($param, $values);
+ }
+
+ /**
+ * Add the given parameter with the given value in front of all other values
+ *
+ * This will add the given parameter in front of all others, regardless of
+ * whether it already exists.
+ *
+ * @param string $param The parameter you're interested in
+ * @param string $value The value to be stored
+ *
+ * @return $this
+ */
+ public function unshift($param, $value)
+ {
+ array_unshift($this->params, array($this->urlEncode($param), $this->urlEncode($value)));
+ $this->reIndexAll();
+ return $this;
+ }
+
+ /**
+ * Set the given parameter with the given value
+ *
+ * This will set the given parameter, and override eventually existing ones.
+ *
+ * @param string $param The parameter you want to set
+ * @param string $value The value to be stored
+ *
+ * @return $this
+ */
+ public function set($param, $value)
+ {
+ if (! $this->has($param)) {
+ return $this->add($param, $value);
+ }
+
+ while (count($this->index[$param]) > 1) {
+ $remove = array_pop($this->index[$param]);
+ unset($this->params[$remove]);
+ }
+
+ $this->params[$this->index[$param][0]] = array(
+ $this->urlEncode($param),
+ $this->urlEncode($this->cleanupValue($value))
+ );
+ $this->reIndexAll();
+
+ return $this;
+ }
+
+ public function remove($param)
+ {
+ $changed = false;
+
+ if (! is_array($param)) {
+ $param = array($param);
+ }
+
+ foreach ($param as $p) {
+ if ($this->has($p)) {
+ foreach ($this->index[$p] as $key) {
+ unset($this->params[$key]);
+ }
+ $changed = true;
+ }
+ }
+
+ if ($changed) {
+ $this->reIndexAll();
+ }
+
+ return $this;
+ }
+
+ public function without($param)
+ {
+ $params = clone $this;
+ return $params->remove($param);
+ }
+
+ // TODO: push, pop?
+
+ protected function indexLastOne()
+ {
+ end($this->params);
+ $key = key($this->params);
+ $param = $this->params[$key][0];
+ $this->addParamToIndex($param, $key);
+ }
+
+ protected function addParamToIndex($param, $key)
+ {
+ if (! $this->has($param)) {
+ $this->index[$param] = array();
+ }
+ $this->index[$param][] = $key;
+ }
+
+ protected function reIndexAll()
+ {
+ $this->index = array();
+ $this->params = array_values($this->params);
+ foreach ($this->params as $key => & $param) {
+ $this->addParamToIndex($param[0], $key);
+ }
+ }
+
+ protected function cleanupValue($value)
+ {
+ return is_bool($value) ? $value : (string) $value;
+ }
+
+ protected function parseQueryString($queryString)
+ {
+ $parts = preg_split('~&~', $queryString, -1, PREG_SPLIT_NO_EMPTY);
+ foreach ($parts as $part) {
+ $this->parseQueryStringPart($part);
+ }
+ }
+
+ protected function parseQueryStringPart($part)
+ {
+ if (strpos($part, '=') === false) {
+ $this->addEncoded($part, true);
+ } else {
+ list($key, $val) = preg_split('/=/', $part, 2);
+ $this->addEncoded($key, $val);
+ }
+ }
+
+ /**
+ * Return the parameters of this url as sequenced or associative array
+ *
+ * @param bool $sequenced
+ *
+ * @return array
+ */
+ public function toArray($sequenced = true)
+ {
+ if ($sequenced) {
+ return $this->params;
+ }
+
+ $params = array();
+ foreach ($this->params as $param) {
+ if ($param[1] === true) {
+ $params[] = $param[0];
+ } else {
+ $params[$param[0]] = $param[1];
+ }
+ }
+
+ return $params;
+ }
+
+ public function toString($separator = null)
+ {
+ if ($separator === null) {
+ $separator = $this->separator;
+ }
+ $parts = array();
+ foreach ($this->params as $p) {
+ if ($p[1] === true) {
+ $parts[] = $p[0];
+ } else {
+ $parts[] = $p[0] . '=' . $p[1];
+ }
+ }
+ return implode($separator, $parts);
+ }
+
+ public function __toString()
+ {
+ return $this->toString();
+ }
+
+ public static function fromQueryString($queryString = null)
+ {
+ if ($queryString === null) {
+ $queryString = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
+ }
+ $params = new static();
+ $params->parseQueryString($queryString);
+
+ return $params;
+ }
+}
diff --git a/library/Icinga/Web/UserAgent.php b/library/Icinga/Web/UserAgent.php
new file mode 100644
index 0000000..71c1a8b
--- /dev/null
+++ b/library/Icinga/Web/UserAgent.php
@@ -0,0 +1,86 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web;
+
+/**
+ * Class UserAgent
+ *
+ * This class helps to get user agent information like OS type and browser name
+ *
+ * @package Icinga\Web
+ */
+class UserAgent
+{
+ /**
+ * $_SERVER['HTTP_USER_AGENT'] output string
+ *
+ * @var string|null
+ */
+ private $agent;
+
+ public function __construct($agent = null)
+ {
+ $this->agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : null;
+
+ if ($agent) {
+ $this->agent = $agent->http_user_agent;
+ }
+ }
+
+ /**
+ * Return $_SERVER['HTTP_USER_AGENT'] output string of given or current device
+ *
+ * @return string
+ */
+ public function getAgent()
+ {
+ return $this->agent;
+ }
+
+ /**
+ * Get Browser name
+ *
+ * @return string Browser name or unknown if not found
+ */
+ public function getBrowser()
+ {
+ // key => regex value
+ $browsers = [
+ "Internet Explorer" => "/MSIE(.*)/i",
+ "Seamonkey" => "/Seamonkey(.*)/i",
+ "MS Edge" => "/Edg(.*)/i",
+ "Opera" => "/Opera(.*)/i",
+ "Opera Browser" => "/OPR(.*)/i",
+ "Chromium" => "/Chromium(.*)/i",
+ "Firefox" => "/Firefox(.*)/i",
+ "Google Chrome" => "/Chrome(.*)/i",
+ "Safari" => "/Safari(.*)/i"
+ ];
+ //TODO find a way to return also the version of the browser
+ foreach ($browsers as $browser => $regex) {
+ if (preg_match($regex, $this->agent)) {
+ return $browser;
+ }
+ }
+
+ return 'unknown';
+ }
+
+ /**
+ * Get Operating system information
+ *
+ * @return string os information
+ */
+ public function getOs()
+ {
+ // get string before the first appearance of ')'
+ $device = strstr($this->agent, ')', true);
+ if (! $device) {
+ return 'unknown';
+ }
+
+ // return string after the first appearance of '('
+ return substr($device, strpos($device, '(') + 1);
+ }
+}
diff --git a/library/Icinga/Web/View.php b/library/Icinga/Web/View.php
new file mode 100644
index 0000000..2c80d1d
--- /dev/null
+++ b/library/Icinga/Web/View.php
@@ -0,0 +1,254 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Closure;
+use Icinga\Application\Icinga;
+use ipl\I18n\Translation;
+use Zend_View_Abstract;
+use Icinga\Authentication\Auth;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Icinga view
+ *
+ * @method Url href($path = null, $params = null) {
+ * @param Url|string|null $path
+ * @param string[]|null $params
+ * }
+ *
+ * @method Url url($path = null, $params = null) {
+ * @param Url|string|null $path
+ * @param string[]|null $params
+ * }
+ *
+ * @method Url qlink($title, $url, $params = null, $properties = null, $escape = true) {
+ * @param string $title
+ * @param Url|string|null $url
+ * @param string[]|null $params
+ * @param string[]|null $properties
+ * @param bool $escape
+ * }
+ *
+ * @method string img($url, $params = null, array $properties = array()) {
+ * @param Url|string|null $url
+ * @param string[]|null $params
+ * @param string[] $properties
+ * }
+ *
+ * @method string icon($img, $title = null, array $properties = array()) {
+ * @param string $img
+ * @param string|null $title
+ * @param string[] $properties
+ * }
+ *
+ * @method string propertiesToString($properties) {
+ * @param string[] $properties
+ * }
+ *
+ * @method string attributeToString($key, $value) {
+ * @param string $key
+ * @param string $value
+ * }
+ */
+class View extends Zend_View_Abstract
+{
+ use Translation;
+
+ /**
+ * Charset to be used - we only support UTF-8
+ */
+ const CHARSET = 'UTF-8';
+
+ /**
+ * Registered helper functions
+ */
+ private $helperFunctions = array();
+
+ /**
+ * Authentication manager
+ *
+ * @var Auth|null
+ */
+ private $auth;
+
+ /**
+ * Create a new view object
+ *
+ * @param array $config
+ * @see Zend_View_Abstract::__construct
+ */
+ public function __construct($config = array())
+ {
+ $config['helperPath']['Icinga\\Web\\View\\Helper\\'] = Icinga::app()->getLibraryDir('Icinga/Web/View/Helper');
+
+ parent::__construct($config);
+ }
+
+ /**
+ * Initialize the view
+ *
+ * @see Zend_View_Abstract::init
+ */
+ public function init()
+ {
+ $this->loadGlobalHelpers();
+ }
+
+ /**
+ * Escape the given value top be safely used in view scripts
+ *
+ * @param ?string $var The output to be escaped
+ * @return string
+ */
+ public function escape($var)
+ {
+ return htmlspecialchars($var ?? '', ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, self::CHARSET, true);
+ }
+
+ /**
+ * Whether a specific helper (closure) has been registered
+ *
+ * @param string $name The desired function name
+ * @return boolean
+ */
+ public function hasHelperFunction($name)
+ {
+ return array_key_exists($name, $this->helperFunctions);
+ }
+
+ /**
+ * Add a new helper function
+ *
+ * @param string $name The desired function name
+ * @param Closure $function An anonymous function
+ * @return $this
+ */
+ public function addHelperFunction($name, Closure $function)
+ {
+ if ($this->hasHelperFunction($name)) {
+ throw new ProgrammingError(
+ 'Cannot assign the same helper function twice: "%s"',
+ $name
+ );
+ }
+
+ $this->helperFunctions[$name] = $function;
+ return $this;
+ }
+
+ /**
+ * Set or overwrite a helper function
+ *
+ * @param string $name
+ * @param Closure $function
+ *
+ * @return $this
+ */
+ public function setHelperFunction($name, Closure $function)
+ {
+ $this->helperFunctions[$name] = $function;
+ return $this;
+ }
+
+ /**
+ * Drop a helper function
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function dropHelperFunction($name)
+ {
+ unset($this->helperFunctions[$name]);
+ return $this;
+ }
+
+ /**
+ * Call a helper function
+ *
+ * @param string $name The desired function name
+ * @param Array $args Function arguments
+ * @return mixed
+ */
+ public function callHelperFunction($name, $args)
+ {
+ return call_user_func_array(
+ $this->helperFunctions[$name],
+ $args
+ );
+ }
+
+ /**
+ * Load helpers
+ */
+ private function loadGlobalHelpers()
+ {
+ $pattern = dirname(__FILE__) . '/View/helpers/*.php';
+ $files = glob($pattern);
+ foreach ($files as $file) {
+ require_once $file;
+ }
+ }
+
+ /**
+ * Get the authentication manager
+ *
+ * @return Auth
+ */
+ public function Auth()
+ {
+ if ($this->auth === null) {
+ $this->auth = Auth::getInstance();
+ }
+ return $this->auth;
+ }
+
+ /**
+ * Whether the current user has the given permission
+ *
+ * @param string $permission Name of the permission
+ *
+ * @return bool
+ */
+ public function hasPermission($permission)
+ {
+ return $this->Auth()->hasPermission($permission);
+ }
+
+ /**
+ * Use to include the view script in a scope that only allows public
+ * members.
+ *
+ * @return mixed
+ *
+ * @see Zend_View_Abstract::run
+ */
+ protected function _run()
+ {
+ foreach ($this->getVars() as $k => $v) {
+ // Exporting global variables to view scripts:
+ $$k = $v;
+ }
+
+ include func_get_arg(0);
+ }
+
+ /**
+ * Accesses a helper object from within a script
+ *
+ * @param string $name
+ * @param array $args
+ *
+ * @return string
+ */
+ public function __call($name, $args)
+ {
+ if ($this->hasHelperFunction($name)) {
+ return $this->callHelperFunction($name, $args);
+ } else {
+ return parent::__call($name, $args);
+ }
+ }
+}
diff --git a/library/Icinga/Web/View/AppHealth.php b/library/Icinga/Web/View/AppHealth.php
new file mode 100644
index 0000000..c66ca05
--- /dev/null
+++ b/library/Icinga/Web/View/AppHealth.php
@@ -0,0 +1,89 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\View;
+
+use Icinga\Application\Hook\HealthHook;
+use ipl\Html\FormattedString;
+use ipl\Html\HtmlElement;
+use ipl\Html\Table;
+use ipl\Web\Common\BaseTarget;
+use ipl\Web\Widget\Link;
+use Traversable;
+
+class AppHealth extends Table
+{
+ use BaseTarget;
+
+ protected $defaultAttributes = ['class' => ['app-health', 'common-table', 'table-row-selectable']];
+
+ /** @var Traversable */
+ protected $data;
+
+ public function __construct(Traversable $data)
+ {
+ $this->data = $data;
+
+ $this->setBaseTarget('_next');
+ }
+
+ protected function assemble()
+ {
+ foreach ($this->data as $row) {
+ $this->add(Table::tr([
+ Table::th(HtmlElement::create('span', ['class' => [
+ 'ball',
+ 'ball-size-xl',
+ $this->getStateClass($row->state)
+ ]])),
+ Table::td([
+ new HtmlElement('header', null, FormattedString::create(
+ t('%s by %s is %s', '<check> by <module> is <state-text>'),
+ $row->url
+ ? new Link(HtmlElement::create('span', null, $row->name), $row->url)
+ : HtmlElement::create('span', null, $row->name),
+ HtmlElement::create('span', null, $row->module),
+ HtmlElement::create('span', null, $this->getStateText($row->state))
+ )),
+ HtmlElement::create('section', null, $row->message)
+ ])
+ ]));
+ }
+ }
+
+ protected function getStateClass($state)
+ {
+ if ($state === null) {
+ $state = HealthHook::STATE_UNKNOWN;
+ }
+
+ switch ($state) {
+ case HealthHook::STATE_OK:
+ return 'state-ok';
+ case HealthHook::STATE_WARNING:
+ return 'state-warning';
+ case HealthHook::STATE_CRITICAL:
+ return 'state-critical';
+ case HealthHook::STATE_UNKNOWN:
+ return 'state-unknown';
+ }
+ }
+
+ protected function getStateText($state)
+ {
+ if ($state === null) {
+ $state = t('UNKNOWN');
+ }
+
+ switch ($state) {
+ case HealthHook::STATE_OK:
+ return t('OK');
+ case HealthHook::STATE_WARNING:
+ return t('WARNING');
+ case HealthHook::STATE_CRITICAL:
+ return t('CRITICAL');
+ case HealthHook::STATE_UNKNOWN:
+ return t('UNKNOWN');
+ }
+ }
+}
diff --git a/library/Icinga/Web/View/Helper/IcingaCheckbox.php b/library/Icinga/Web/View/Helper/IcingaCheckbox.php
new file mode 100644
index 0000000..07cf01f
--- /dev/null
+++ b/library/Icinga/Web/View/Helper/IcingaCheckbox.php
@@ -0,0 +1,30 @@
+<?php
+/* Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\View\Helper;
+
+class IcingaCheckbox extends \Zend_View_Helper_FormCheckbox
+{
+ public function icingaCheckbox($name, $value = null, $attribs = null, array $checkedOptions = null)
+ {
+ if (! isset($attribs['id'])) {
+ $attribs['id'] = $this->view->protectId('icingaCheckbox_' . $name);
+ }
+
+ $attribs['class'] = (isset($attribs['class']) ? $attribs['class'] . ' ' : '') . 'sr-only';
+ $html = parent::formCheckbox($name, $value, $attribs, $checkedOptions);
+
+ $class = 'toggle-switch';
+ if (isset($attribs['disabled'])) {
+ $class .= ' disabled';
+ }
+
+ return $html
+ . '<label for="'
+ . $attribs['id']
+ . '" aria-hidden="true"'
+ . ' class="'
+ . $class
+ . '"><span class="toggle-slider"></span></label>';
+ }
+}
diff --git a/library/Icinga/Web/View/PrivilegeAudit.php b/library/Icinga/Web/View/PrivilegeAudit.php
new file mode 100644
index 0000000..fcb4083
--- /dev/null
+++ b/library/Icinga/Web/View/PrivilegeAudit.php
@@ -0,0 +1,622 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\View;
+
+use Icinga\Authentication\Role;
+use Icinga\Forms\Security\RoleForm;
+use Icinga\Util\StringHelper;
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\HtmlElement;
+use ipl\Html\HtmlString;
+use ipl\Html\Text;
+use ipl\Stdlib\Filter;
+use ipl\Web\Common\BaseTarget;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+use ipl\Web\Widget\Icon;
+use ipl\Web\Widget\Link;
+
+class PrivilegeAudit extends BaseHtmlElement
+{
+ use BaseTarget;
+
+ /** @var string */
+ const UNRESTRICTED_PERMISSION = 'unrestricted';
+
+ protected $tag = 'ul';
+
+ protected $defaultAttributes = ['class' => 'privilege-audit'];
+
+ /** @var Role[] */
+ protected $roles;
+
+ public function __construct(array $roles)
+ {
+ $this->roles = $roles;
+ $this->setBaseTarget('_next');
+ }
+
+ protected function auditPermission($permission)
+ {
+ $grantedBy = [];
+ $refusedBy = [];
+ foreach ($this->roles as $role) {
+ if ($permission === self::UNRESTRICTED_PERMISSION) {
+ if ($role->isUnrestricted()) {
+ $grantedBy[] = $role->getName();
+ }
+ } elseif ($role->denies($permission)) {
+ $refusedBy[] = $role->getName();
+ } elseif ($role->grants($permission, false, false)) {
+ $grantedBy[] = $role->getName();
+ }
+ }
+
+ $header = new HtmlElement('summary');
+ if (! empty($refusedBy)) {
+ $header->add([
+ new Icon('times-circle', ['class' => 'refused']),
+ count($refusedBy) > 2
+ ? sprintf(
+ tp(
+ 'Refused by %s and %s as well as one other',
+ 'Refused by %s and %s as well as %d others',
+ count($refusedBy) - 2
+ ),
+ $refusedBy[0],
+ $refusedBy[1],
+ count($refusedBy) - 2
+ )
+ : sprintf(
+ tp('Refused by %s', 'Refused by %s and %s', count($refusedBy)),
+ ...$refusedBy
+ )
+ ]);
+ } elseif (! empty($grantedBy)) {
+ $header->add([
+ new Icon('check-circle', ['class' => 'granted']),
+ count($grantedBy) > 2
+ ? sprintf(
+ tp(
+ 'Granted by %s and %s as well as one other',
+ 'Granted by %s and %s as well as %d others',
+ count($grantedBy) - 2
+ ),
+ $grantedBy[0],
+ $grantedBy[1],
+ count($grantedBy) - 2
+ )
+ : sprintf(
+ tp('Granted by %s', 'Granted by %s and %s', count($grantedBy)),
+ ...$grantedBy
+ )
+ ]);
+ } else {
+ $header->add([new Icon('minus-circle'), t('Not granted or refused by any role')]);
+ }
+
+ $vClass = null;
+ $rolePaths = [];
+ foreach (array_reverse($this->roles) as $role) {
+ if (! in_array($role->getName(), $refusedBy, true) && ! in_array($role->getName(), $grantedBy, true)) {
+ continue;
+ }
+
+ /** @var Role[] $rolesReversed */
+ $rolesReversed = [];
+
+ do {
+ array_unshift($rolesReversed, $role);
+ } while (($role = $role->getParent()) !== null);
+
+ $path = new HtmlElement('ol');
+
+ $class = null;
+ $setInitiator = false;
+ foreach ($rolesReversed as $role) {
+ $granted = false;
+ $refused = false;
+ $icon = new Icon('minus-circle');
+ if ($permission === self::UNRESTRICTED_PERMISSION) {
+ if ($role->isUnrestricted()) {
+ $granted = true;
+ $icon = new Icon('check-circle', ['class' => 'granted']);
+ }
+ } elseif ($role->denies($permission, true)) {
+ $refused = true;
+ $icon = new Icon('times-circle', ['class' => 'refused']);
+ } elseif ($role->grants($permission, true, false)) {
+ $granted = true;
+ $icon = new Icon('check-circle', ['class' => 'granted']);
+ }
+
+ $connector = null;
+ if ($role->getParent() !== null) {
+ $connector = HtmlElement::create('li', ['class' => ['connector', $class]]);
+ if ($setInitiator) {
+ $setInitiator = false;
+ $connector->getAttributes()->add('class', 'initiator');
+ }
+
+ $path->prependHtml($connector);
+ }
+
+ $path->prependHtml(new HtmlElement('li', Attributes::create([
+ 'class' => ['role', $class],
+ 'title' => $role->getName()
+ ]), new Link([$icon, $role->getName()], Url::fromPath('role/edit', ['role' => $role->getName()]))));
+
+ if ($refused) {
+ $setInitiator = $class !== 'refused';
+ $class = 'refused';
+ } elseif ($granted) {
+ $setInitiator = $class === null;
+ $class = $class ?: 'granted';
+ }
+ }
+
+ if ($vClass === null || $vClass === 'granted') {
+ $vClass = $class;
+ }
+
+ array_unshift($rolePaths, $path->prepend([
+ empty($rolePaths) ? null : HtmlElement::create('li', ['class' => ['vertical-line', $vClass]]),
+ new HtmlElement('li', Attributes::create(['class' => [
+ 'connector',
+ $class,
+ $setInitiator ? 'initiator' : null
+ ]]))
+ ]));
+ }
+
+ if (empty($rolePaths)) {
+ return [
+ empty($refusedBy) ? (empty($grantedBy) ? null : true) : false,
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'inheritance-paths']),
+ $header->setTag('div')
+ )
+ ];
+ }
+
+ return [
+ empty($refusedBy) ? (empty($grantedBy) ? null : true) : false,
+ HtmlElement::create('details', [
+ 'class' => ['collapsible', 'inheritance-paths'],
+ 'data-no-persistence' => true,
+ 'open' => getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf'
+ ], [
+ $header->addAttributes(['class' => 'collapsible-control']),
+ $rolePaths
+ ])
+ ];
+ }
+
+ protected function auditRestriction($restriction)
+ {
+ $restrictedBy = [];
+ $restrictions = [];
+ foreach ($this->roles as $role) {
+ if ($role->isUnrestricted()) {
+ $restrictedBy = [];
+ $restrictions = [];
+ break;
+ }
+
+ foreach ($this->collectRestrictions($role, $restriction) as $role => $roleRestriction) {
+ $restrictedBy[] = $role;
+ $restrictions[] = $roleRestriction;
+ }
+ }
+
+ $header = new HtmlElement('summary');
+ if (! empty($restrictedBy)) {
+ $header->add([
+ new Icon('filter', ['class' => 'restricted']),
+ count($restrictedBy) > 2
+ ? sprintf(
+ tp(
+ 'Restricted by %s and %s as well as one other',
+ 'Restricted by %s and %s as well as %d others',
+ count($restrictedBy) - 2
+ ),
+ $restrictedBy[0]->getName(),
+ $restrictedBy[1]->getName(),
+ count($restrictedBy) - 2
+ )
+ : sprintf(
+ tp('Restricted by %s', 'Restricted by %s and %s', count($restrictedBy)),
+ ...array_map(function ($role) {
+ return $role->getName();
+ }, $restrictedBy)
+ )
+ ]);
+ } else {
+ $header->add([new Icon('filter'), t('Not restricted by any role')]);
+ }
+
+ $roles = [];
+ if (! empty($restrictions) && count($restrictions) > 1) {
+ list($combinedRestrictions, $combinedLinks) = $this->createRestrictionLinks($restriction, $restrictions);
+ $roles[] = HtmlElement::create('li', null, [
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'flex-overflow']),
+ HtmlElement::create('span', [
+ 'class' => 'role',
+ 'title' => t('All roles combined')
+ ], join(' | ', array_map(function ($role) {
+ return $role->getName();
+ }, $restrictedBy))),
+ HtmlElement::create('code', ['class' => 'restriction'], $combinedRestrictions)
+ ),
+ $combinedLinks ? new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'previews']),
+ HtmlElement::create('em', null, t('Previews:')),
+ $combinedLinks
+ ) : null
+ ]);
+ }
+
+ foreach ($restrictedBy as $role) {
+ list($roleRestriction, $restrictionLinks) = $this->createRestrictionLinks(
+ $restriction,
+ [$role->getRestrictions($restriction)]
+ );
+
+ $roles[] = HtmlElement::create('li', null, [
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'flex-overflow']),
+ new Link($role->getName(), Url::fromPath('role/edit', ['role' => $role->getName()]), [
+ 'class' => 'role',
+ 'title' => $role->getName()
+ ]),
+ HtmlElement::create('code', ['class' => 'restriction'], $roleRestriction)
+ ),
+ $restrictionLinks ? new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'previews']),
+ HtmlElement::create('em', null, t('Previews:')),
+ $restrictionLinks
+ ) : null
+ ]);
+ }
+
+ if (empty($roles)) {
+ return [
+ ! empty($restrictedBy),
+ new HtmlElement(
+ 'div',
+ Attributes::create(['class' => 'restrictions']),
+ $header->setTag('div')
+ )
+ ];
+ }
+
+ return [
+ ! empty($restrictedBy),
+ new HtmlElement(
+ 'details',
+ Attributes::create([
+ 'class' => ['collapsible', 'restrictions'],
+ 'data-no-persistence' => true,
+ 'open' => getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf'
+ ]),
+ $header->addAttributes(['class' => 'collapsible-control']),
+ new HtmlElement('ul', null, ...$roles)
+ )
+ ];
+ }
+
+ protected function assemble()
+ {
+ list($permissions, $restrictions) = RoleForm::collectProvidedPrivileges();
+ list($wildcardState, $wildcardAudit) = $this->auditPermission('*');
+ list($unrestrictedState, $unrestrictedAudit) = $this->auditPermission(self::UNRESTRICTED_PERMISSION);
+
+ $this->addHtml(new HtmlElement(
+ 'li',
+ null,
+ new HtmlElement(
+ 'details',
+ Attributes::create([
+ 'class' => ['collapsible', 'privilege-section'],
+ 'open' => ($wildcardState || $unrestrictedState) && getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf'
+ ]),
+ new HtmlElement(
+ 'summary',
+ Attributes::create(['class' => [
+ 'collapsible-control', // Helps JS, improves performance a bit
+ ]]),
+ new HtmlElement('span', null, Text::create(t('Administrative Privileges'))),
+ HtmlElement::create(
+ 'span',
+ ['class' => 'audit-preview'],
+ $wildcardState || $unrestrictedState
+ ? new Icon('check-circle', ['class' => 'granted'])
+ : null
+ ),
+ new Icon('angles-down', ['class' => 'collapse-icon']),
+ new Icon('angles-left', ['class' => 'expand-icon'])
+ ),
+ new HtmlElement(
+ 'ol',
+ Attributes::create(['class' => 'privilege-list']),
+ new HtmlElement(
+ 'li',
+ null,
+ HtmlElement::create('p', ['class' => 'privilege-label'], t('Administrative Access')),
+ HtmlElement::create('div', ['class' => 'spacer']),
+ $wildcardAudit
+ ),
+ new HtmlElement(
+ 'li',
+ null,
+ HtmlElement::create('p', ['class' => 'privilege-label'], t('Unrestricted Access')),
+ HtmlElement::create('div', ['class' => 'spacer']),
+ $unrestrictedAudit
+ )
+ )
+ )
+ ));
+
+ $privilegeSources = array_unique(array_merge(array_keys($permissions), array_keys($restrictions)));
+ foreach ($privilegeSources as $source) {
+ $anythingGranted = false;
+ $anythingRefused = false;
+ $anythingRestricted = false;
+
+ $permissionList = new HtmlElement('ol', Attributes::create(['class' => 'privilege-list']));
+ foreach (isset($permissions[$source]) ? $permissions[$source] : [] as $permission => $metaData) {
+ list($permissionState, $permissionAudit) = $this->auditPermission($permission);
+ if ($permissionState !== null) {
+ if ($permissionState) {
+ $anythingGranted = true;
+ } else {
+ $anythingRefused = true;
+ }
+ }
+
+ $permissionList->addHtml(new HtmlElement(
+ 'li',
+ null,
+ HtmlElement::create(
+ 'p',
+ ['class' => 'privilege-label'],
+ isset($metaData['label'])
+ ? $metaData['label']
+ : array_map(function ($segment) {
+ return $segment[0] === '/' ? [
+ // Adds a zero-width char after each slash to help browsers break onto newlines
+ new HtmlString('/&#8203;'),
+ HtmlElement::create('span', ['class' => 'no-wrap'], substr($segment, 1))
+ ] : HtmlElement::create('em', null, $segment);
+ }, preg_split(
+ '~(/[^/]+)~',
+ $permission,
+ -1,
+ PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
+ ))
+ ),
+ new HtmlElement('div', Attributes::create(['class' => 'spacer'])),
+ $permissionAudit
+ ));
+ }
+
+ $restrictionList = new HtmlElement('ol', Attributes::create(['class' => 'privilege-list']));
+ foreach (isset($restrictions[$source]) ? $restrictions[$source] : [] as $restriction => $metaData) {
+ list($restrictionState, $restrictionAudit) = $this->auditRestriction($restriction);
+ if ($restrictionState) {
+ $anythingRestricted = true;
+ }
+
+ $restrictionList->addHtml(new HtmlElement(
+ 'li',
+ null,
+ HtmlElement::create(
+ 'p',
+ ['class' => 'privilege-label'],
+ isset($metaData['label'])
+ ? $metaData['label']
+ : array_map(function ($segment) {
+ return $segment[0] === '/' ? [
+ // Adds a zero-width char after each slash to help browsers break onto newlines
+ new HtmlString('/&#8203;'),
+ HtmlElement::create('span', ['class' => 'no-wrap'], substr($segment, 1))
+ ] : HtmlElement::create('em', null, $segment);
+ }, preg_split(
+ '~(/[^/]+)~',
+ $restriction,
+ -1,
+ PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
+ ))
+ ),
+ new HtmlElement('div', Attributes::create(['class' => 'spacer'])),
+ $restrictionAudit
+ ));
+ }
+
+ if ($source === 'application') {
+ $label = 'Icinga Web 2';
+ } else {
+ $label = [$source, ' ', HtmlElement::create('em', null, t('Module'))];
+ }
+
+ $this->addHtml(new HtmlElement(
+ 'li',
+ null,
+ HtmlElement::create('details', [
+ 'class' => ['collapsible', 'privilege-section'],
+ 'open' => ($anythingGranted || $anythingRefused || $anythingRestricted)
+ && getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf'
+ ], [
+ new HtmlElement(
+ 'summary',
+ Attributes::create(['class' => [
+ 'collapsible-control', // Helps JS, improves performance a bit
+ ]]),
+ HtmlElement::create('span', null, $label),
+ HtmlElement::create('span', ['class' => 'audit-preview'], [
+ $anythingGranted ? new Icon('check-circle', ['class' => 'granted']) : null,
+ $anythingRefused ? new Icon('times-circle', ['class' => 'refused']) : null,
+ $anythingRestricted ? new Icon('filter', ['class' => 'restricted']) : null
+ ]),
+ new Icon('angles-down', ['class' => 'collapse-icon']),
+ new Icon('angles-left', ['class' => 'expand-icon'])
+ ),
+ $permissionList->isEmpty() ? null : [
+ HtmlElement::create('h4', null, t('Permissions')),
+ $permissionList
+ ],
+ $restrictionList->isEmpty() ? null : [
+ HtmlElement::create('h4', null, t('Restrictions')),
+ $restrictionList
+ ]
+ ])
+ ));
+ }
+ }
+
+ private function collectRestrictions(Role $role, $restrictionName)
+ {
+ do {
+ $restriction = $role->getRestrictions($restrictionName);
+ if ($restriction) {
+ yield $role => $restriction;
+ }
+ } while (($role = $role->getParent()) !== null);
+ }
+
+ private function createRestrictionLinks($restrictionName, array $restrictions)
+ {
+ // TODO: Remove this hardcoded mess. Do this based on the restriction's meta data
+ switch ($restrictionName) {
+ case 'icingadb/filter/objects':
+ $filterString = join('|', $restrictions);
+ $list = new HtmlElement(
+ 'ul',
+ Attributes::create(['class' => 'links']),
+ new HtmlElement('li', null, new Link(
+ 'icingadb/hosts',
+ Url::fromPath('icingadb/hosts')->setQueryString($filterString)
+ )),
+ new HtmlElement('li', null, new Link(
+ 'icingadb/services',
+ Url::fromPath('icingadb/services')->setQueryString($filterString)
+ )),
+ new HtmlElement('li', null, new Link(
+ 'icingadb/hostgroups',
+ Url::fromPath('icingadb/hostgroups')->setQueryString($filterString)
+ )),
+ new HtmlElement('li', null, new Link(
+ 'icingadb/servicegroups',
+ Url::fromPath('icingadb/servicegroups')->setQueryString($filterString)
+ ))
+ );
+
+ break;
+ case 'icingadb/filter/hosts':
+ $filterString = join('|', $restrictions);
+ $list = new HtmlElement(
+ 'ul',
+ Attributes::create(['class' => 'links']),
+ new HtmlElement('li', null, new Link(
+ 'icingadb/hosts',
+ Url::fromPath('icingadb/hosts')->setQueryString($filterString)
+ )),
+ new HtmlElement('li', null, new Link(
+ 'icingadb/services',
+ Url::fromPath('icingadb/services')->setQueryString($filterString)
+ ))
+ );
+
+ break;
+ case 'icingadb/filter/services':
+ $filterString = join('|', $restrictions);
+ $list = new HtmlElement(
+ 'ul',
+ Attributes::create(['class' => 'links']),
+ new HtmlElement('li', null, new Link(
+ 'icingadb/services',
+ Url::fromPath('icingadb/services')->setQueryString($filterString)
+ ))
+ );
+
+ break;
+ case 'monitoring/filter/objects':
+ $filterString = join('|', $restrictions);
+ $list = new HtmlElement(
+ 'ul',
+ Attributes::create(['class' => 'links']),
+ new HtmlElement('li', null, new Link(
+ 'monitoring/list/hosts',
+ Url::fromPath('monitoring/list/hosts')->setQueryString($filterString)
+ )),
+ new HtmlElement('li', null, new Link(
+ 'monitoring/list/services',
+ Url::fromPath('monitoring/list/services')->setQueryString($filterString)
+ )),
+ new HtmlElement('li', null, new Link(
+ 'monitoring/list/hostgroups',
+ Url::fromPath('monitoring/list/hostgroups')->setQueryString($filterString)
+ )),
+ new HtmlElement('li', null, new Link(
+ 'monitoring/list/servicegroups',
+ Url::fromPath('monitoring/list/servicegroups')->setQueryString($filterString)
+ ))
+ );
+
+ break;
+ case 'application/share/users':
+ $filter = Filter::any();
+ foreach ($restrictions as $roleRestriction) {
+ $userNames = StringHelper::trimSplit($roleRestriction);
+ foreach ($userNames as $userName) {
+ $filter->add(Filter::equal('user_name', $userName));
+ }
+ }
+
+ $filterString = QueryString::render($filter);
+ $list = new HtmlElement(
+ 'ul',
+ Attributes::create(['class' => 'links']),
+ new HtmlElement('li', null, new Link(
+ 'user/list',
+ Url::fromPath('user/list')->setQueryString($filterString)
+ ))
+ );
+
+ break;
+ case 'application/share/groups':
+ $filter = Filter::any();
+ foreach ($restrictions as $roleRestriction) {
+ $groupNames = StringHelper::trimSplit($roleRestriction);
+ foreach ($groupNames as $groupName) {
+ $filter->add(Filter::equal('group_name', $groupName));
+ }
+ }
+
+ $filterString = QueryString::render($filter);
+ $list = new HtmlElement(
+ 'ul',
+ Attributes::create(['class' => 'links']),
+ new HtmlElement('li', null, new Link(
+ 'group/list',
+ Url::fromPath('group/list')->setQueryString($filterString)
+ ))
+ );
+
+ break;
+ default:
+ $filterString = join(', ', $restrictions);
+ $list = null;
+ }
+
+ return [$filterString, $list];
+ }
+}
diff --git a/library/Icinga/Web/View/helpers/format.php b/library/Icinga/Web/View/helpers/format.php
new file mode 100644
index 0000000..4008583
--- /dev/null
+++ b/library/Icinga/Web/View/helpers/format.php
@@ -0,0 +1,72 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\View;
+
+use Icinga\Date\DateFormatter;
+use Icinga\Util\Format;
+
+$this->addHelperFunction('format', function () {
+ return Format::getInstance();
+});
+
+$this->addHelperFunction('formatDate', function ($date) {
+ if (! $date) {
+ return '';
+ }
+ return DateFormatter::formatDate($date);
+});
+
+$this->addHelperFunction('formatDateTime', function ($dateTime) {
+ if (! $dateTime) {
+ return '';
+ }
+ return DateFormatter::formatDateTime($dateTime);
+});
+
+$this->addHelperFunction('formatDuration', function ($seconds) {
+ if (! $seconds) {
+ return '';
+ }
+ return DateFormatter::formatDuration($seconds);
+});
+
+$this->addHelperFunction('formatTime', function ($time) {
+ if (! $time) {
+ return '';
+ }
+ return DateFormatter::formatTime($time);
+});
+
+$this->addHelperFunction('timeAgo', function ($time, $timeOnly = false, $requireTime = false) {
+ if (! $time) {
+ return '';
+ }
+ return sprintf(
+ '<span class="relative-time time-ago" title="%s">%s</span>',
+ DateFormatter::formatDateTime($time),
+ DateFormatter::timeAgo($time, $timeOnly, $requireTime)
+ );
+});
+
+$this->addHelperFunction('timeSince', function ($time, $timeOnly = false, $requireTime = false) {
+ if (! $time) {
+ return '';
+ }
+ return sprintf(
+ '<span class="relative-time time-since" title="%s">%s</span>',
+ DateFormatter::formatDateTime($time),
+ DateFormatter::timeSince($time, $timeOnly, $requireTime)
+ );
+});
+
+$this->addHelperFunction('timeUntil', function ($time, $timeOnly = false, $requireTime = false) {
+ if (! $time) {
+ return '';
+ }
+ return sprintf(
+ '<span class="relative-time time-until" title="%s">%s</span>',
+ DateFormatter::formatDateTime($time),
+ DateFormatter::timeUntil($time, $timeOnly, $requireTime)
+ );
+});
diff --git a/library/Icinga/Web/View/helpers/generic.php b/library/Icinga/Web/View/helpers/generic.php
new file mode 100644
index 0000000..bfd3f86
--- /dev/null
+++ b/library/Icinga/Web/View/helpers/generic.php
@@ -0,0 +1,15 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\View;
+
+use Icinga\Authentication\Auth;
+use Icinga\Web\Widget;
+
+$this->addHelperFunction('auth', function () {
+ return Auth::getInstance();
+});
+
+$this->addHelperFunction('widget', function ($name, $options = null) {
+ return Widget::create($name, $options);
+});
diff --git a/library/Icinga/Web/View/helpers/string.php b/library/Icinga/Web/View/helpers/string.php
new file mode 100644
index 0000000..b3f667b
--- /dev/null
+++ b/library/Icinga/Web/View/helpers/string.php
@@ -0,0 +1,36 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\View;
+
+use Icinga\Util\StringHelper;
+use Icinga\Web\Helper\Markdown;
+
+$this->addHelperFunction('ellipsis', function ($string, $maxLength, $ellipsis = '...') {
+ return StringHelper::ellipsis($string, $maxLength, $ellipsis);
+});
+
+$this->addHelperFunction('nl2br', function ($string) {
+ return nl2br(str_replace(array('\r\n', '\r', '\n'), '<br>', $string), false);
+});
+
+$this->addHelperFunction('markdown', function ($content, $containerAttribs = null) {
+ if (! isset($containerAttribs['class'])) {
+ $containerAttribs['class'] = 'markdown';
+ } else {
+ $containerAttribs['class'] .= ' markdown';
+ }
+
+ return '<section' . $this->propertiesToString($containerAttribs) . '>' . Markdown::text($content) . '</section>';
+});
+
+$this->addHelperFunction('markdownLine', function ($content, $containerAttribs = null) {
+ if (! isset($containerAttribs['class'])) {
+ $containerAttribs['class'] = 'markdown inline';
+ } else {
+ $containerAttribs['class'] .= ' markdown inline';
+ }
+
+ return '<section' . $this->propertiesToString($containerAttribs) . '>' .
+ Markdown::line($content) . '</section>';
+});
diff --git a/library/Icinga/Web/View/helpers/url.php b/library/Icinga/Web/View/helpers/url.php
new file mode 100644
index 0000000..277c237
--- /dev/null
+++ b/library/Icinga/Web/View/helpers/url.php
@@ -0,0 +1,158 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\View;
+
+use Icinga\Web\Url;
+use Icinga\Exception\ProgrammingError;
+
+$view = $this;
+
+$this->addHelperFunction('href', function ($path = null, $params = null) use ($view) {
+ return $view->url($path, $params);
+});
+
+$this->addHelperFunction('url', function ($path = null, $params = null) {
+ if ($path === null) {
+ $url = Url::fromRequest();
+ } elseif ($path instanceof Url) {
+ $url = $path;
+ } else {
+ $url = Url::fromPath($path);
+ }
+
+ if ($params !== null) {
+ if ($url === $path) {
+ $url = clone $url;
+ }
+
+ $url->overwriteParams($params);
+ }
+
+ return $url;
+});
+
+$this->addHelperFunction(
+ 'qlink',
+ function ($title, $url, $params = null, $properties = null, $escape = true) use ($view) {
+ $icon = '';
+ if ($properties) {
+ if (array_key_exists('title', $properties) && !array_key_exists('aria-label', $properties)) {
+ $properties['aria-label'] = $properties['title'];
+ }
+
+ if (array_key_exists('icon', $properties)) {
+ $icon = $view->icon($properties['icon']);
+ unset($properties['icon']);
+ }
+
+ if (array_key_exists('img', $properties)) {
+ $icon = $view->img($properties['img']);
+ unset($properties['img']);
+ }
+ }
+
+ return sprintf(
+ '<a href="%s"%s>%s</a>',
+ $view->url($url, $params),
+ $view->propertiesToString($properties),
+ $icon . ($escape ? $view->escape($title) : $title)
+ );
+ }
+);
+
+$this->addHelperFunction('img', function ($url, $params = null, array $properties = array()) use ($view) {
+ if (! array_key_exists('alt', $properties)) {
+ $properties['alt'] = '';
+ }
+
+ $ariaHidden = array_key_exists('aria-hidden', $properties) ? $properties['aria-hidden'] : null;
+ if (array_key_exists('title', $properties)) {
+ if (! array_key_exists('aria-label', $properties) && $ariaHidden !== 'true') {
+ $properties['aria-label'] = $properties['title'];
+ }
+ } elseif ($ariaHidden === null) {
+ $properties['aria-hidden'] = 'true';
+ }
+
+ return sprintf(
+ '<img src="%s"%s />',
+ $view->escape($view->url($url, $params)->getAbsoluteUrl()),
+ $view->propertiesToString($properties)
+ );
+});
+
+$this->addHelperFunction('icon', function ($img, $title = null, array $properties = array()) use ($view) {
+ if (strpos($img, '.') !== false) {
+ if (array_key_exists('class', $properties)) {
+ $properties['class'] .= ' icon';
+ } else {
+ $properties['class'] = 'icon';
+ }
+ if (strpos($img, '/') === false) {
+ return $view->img('img/icons/' . $img, null, $properties);
+ } else {
+ return $view->img($img, null, $properties);
+ }
+ }
+
+ $ariaHidden = array_key_exists('aria-hidden', $properties) ? $properties['aria-hidden'] : null;
+ if ($title !== null) {
+ $properties['role'] = 'img';
+ $properties['title'] = $title;
+
+ if (! array_key_exists('aria-label', $properties) && $ariaHidden !== 'true') {
+ $properties['aria-label'] = $title;
+ }
+ } elseif ($ariaHidden === null) {
+ $properties['aria-hidden'] = 'true';
+ }
+
+ if (isset($properties['class'])) {
+ $properties['class'] .= ' icon-' . $img;
+ } else {
+ $properties['class'] = 'icon-' . $img;
+ }
+
+ return sprintf('<i %s></i>', $view->propertiesToString($properties));
+});
+
+$this->addHelperFunction('propertiesToString', function ($properties) use ($view) {
+ if (empty($properties)) {
+ return '';
+ }
+ $attributes = array();
+
+ foreach ($properties as $key => $val) {
+ if ($key === 'style' && is_array($val)) {
+ if (empty($val)) {
+ continue;
+ }
+ $parts = array();
+ foreach ($val as $k => $v) {
+ $parts[] = "$k: $v";
+ }
+ $val = implode('; ', $parts);
+ continue;
+ }
+
+ $attributes[] = $view->attributeToString($key, $val);
+ }
+ return ' ' . implode(' ', $attributes);
+});
+
+$this->addHelperFunction('attributeToString', function ($key, $value) use ($view) {
+ // TODO: Doublecheck this!
+ if (! preg_match('~^[a-zA-Z0-9-]+$~', $key)) {
+ throw new ProgrammingError(
+ 'Trying to set an invalid HTML attribute name: %s',
+ $key
+ );
+ }
+
+ return sprintf(
+ '%s="%s"',
+ $key,
+ $view->escape($value)
+ );
+});
diff --git a/library/Icinga/Web/Widget.php b/library/Icinga/Web/Widget.php
new file mode 100644
index 0000000..48ae7bd
--- /dev/null
+++ b/library/Icinga/Web/Widget.php
@@ -0,0 +1,49 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Web\Widget\AbstractWidget;
+
+/**
+ * Web widgets make things easier for you!
+ *
+ * This class provides nothing but a static factory method for widget creation.
+ * Usually it will not be used directly as there are widget()-helpers available
+ * in your action controllers and view scripts.
+ *
+ * Usage example:
+ * <code>
+ * $tabs = Widget::create('tabs');
+ * </code>
+ *
+ * @copyright Copyright (c) 2013 Icinga-Web Team <info@icinga.com>
+ * @author Icinga-Web Team <info@icinga.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
+ */
+class Widget
+{
+ /**
+ * Create a new widget
+ *
+ * @param string $name Widget name
+ * @param array $options Widget constructor options
+ *
+ * @return AbstractWidget
+ */
+ public static function create($name, $options = array(), $module_name = null)
+ {
+ $class = 'Icinga\\Web\\Widget\\' . ucfirst($name);
+
+ if (! class_exists($class)) {
+ throw new ProgrammingError(
+ 'There is no such widget: %s',
+ $name
+ );
+ }
+
+ $widget = new $class($options, $module_name);
+ return $widget;
+ }
+}
diff --git a/library/Icinga/Web/Widget/AbstractWidget.php b/library/Icinga/Web/Widget/AbstractWidget.php
new file mode 100644
index 0000000..1090548
--- /dev/null
+++ b/library/Icinga/Web/Widget/AbstractWidget.php
@@ -0,0 +1,121 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Application\Icinga;
+use Exception;
+use Zend_View_Abstract;
+
+/**
+ * Web widgets MUST extend this class
+ *
+ * AbstractWidget implements getters and setters for widget options stored in
+ * the protected options array. If you want to allow options for your own
+ * widget, you have to set a default value (may be null) for each single option
+ * in this array.
+ *
+ * Please have a look at the available widgets in this folder to get a better
+ * idea on what they should look like.
+ *
+ * @copyright Copyright (c) 2013 Icinga-Web Team <info@icinga.com>
+ * @author Icinga-Web Team <info@icinga.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
+ */
+abstract class AbstractWidget
+{
+ /**
+ * If you are going to access the current view with the view() function,
+ * its instance is stored here for performance reasons.
+ *
+ * @var Zend_View_Abstract
+ */
+ protected static $view;
+
+ // TODO: Should we kick this?
+ protected $properties = array();
+
+ /**
+ * Getter for widget properties
+ *
+ * @param string $key The option you're interested in
+ *
+ * @throws ProgrammingError for unknown property name
+ *
+ * @return mixed
+ */
+ public function __get($key)
+ {
+ if (array_key_exists($key, $this->properties)) {
+ return $this->properties[$key];
+ }
+
+ throw new ProgrammingError(
+ 'Trying to get invalid "%s" property for %s',
+ $key,
+ get_class($this)
+ );
+ }
+
+ /**
+ * Setter for widget properties
+ *
+ * @param string $key The option you want to set
+ * @param string $val The new value going to be assigned to this option
+ *
+ * @throws ProgrammingError for unknown property name
+ *
+ * @return mixed
+ */
+ public function __set($key, $val)
+ {
+ if (array_key_exists($key, $this->properties)) {
+ $this->properties[$key] = $val;
+ return;
+ }
+
+ throw new ProgrammingError(
+ 'Trying to set invalid "%s" property in %s. Allowed are: %s',
+ $key,
+ get_class($this),
+ empty($this->properties)
+ ? 'none'
+ : implode(', ', array_keys($this->properties))
+ );
+ }
+
+ abstract public function render();
+
+ /**
+ * Access the current view
+ *
+ * Will instantiate a new one if none exists
+ * // TODO: App->getView
+ *
+ * @return Zend_View_Abstract
+ */
+ protected function view()
+ {
+ if (self::$view === null) {
+ self::$view = Icinga::app()->getViewRenderer()->view;
+ }
+
+ return self::$view;
+ }
+
+ /**
+ * Cast this widget to a string. Will call your render() function
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ try {
+ $html = $this->render();
+ } catch (Exception $e) {
+ return htmlspecialchars($e->getMessage());
+ }
+ return (string) $html;
+ }
+}
diff --git a/library/Icinga/Web/Widget/Announcements.php b/library/Icinga/Web/Widget/Announcements.php
new file mode 100644
index 0000000..e0fac77
--- /dev/null
+++ b/library/Icinga/Web/Widget/Announcements.php
@@ -0,0 +1,55 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget;
+
+use Icinga\Application\Icinga;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\Announcement\AcknowledgeAnnouncementForm;
+use Icinga\Web\Announcement\AnnouncementCookie;
+use Icinga\Web\Announcement\AnnouncementIniRepository;
+use Icinga\Web\Helper\Markdown;
+
+/**
+ * Render announcements
+ */
+class Announcements extends AbstractWidget
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function render()
+ {
+ $repo = new AnnouncementIniRepository();
+ $etag = $repo->getEtag();
+ $cookie = new AnnouncementCookie();
+ if ($cookie->getEtag() !== $etag) {
+ $cookie->setEtag($etag);
+ $cookie->setNextActive($repo->findNextActive());
+ Icinga::app()->getResponse()->setCookie($cookie);
+ }
+ $acked = array();
+ foreach ($cookie->getAcknowledged() as $hash) {
+ $acked[] = Filter::expression('hash', '!=', $hash);
+ }
+ $acked = Filter::matchAll($acked);
+ $announcements = $repo->findActive();
+ $announcements->applyFilter($acked);
+ if ($announcements->hasResult()) {
+ $html = '<ul role="alert">';
+ foreach ($announcements as $announcement) {
+ $ackForm = new AcknowledgeAnnouncementForm();
+ $ackForm->populate(array('hash' => $announcement->hash));
+ $html .= '<li><div class="message">'
+ . Markdown::text($announcement->message)
+ . '</div>'
+ . $ackForm
+ . '</li>';
+ }
+ $html .= '</ul>';
+ return $html;
+ }
+ // Force container update on XHR
+ return '<div hidden></div>';
+ }
+}
diff --git a/library/Icinga/Web/Widget/ApplicationStateMessages.php b/library/Icinga/Web/Widget/ApplicationStateMessages.php
new file mode 100644
index 0000000..99d3bb2
--- /dev/null
+++ b/library/Icinga/Web/Widget/ApplicationStateMessages.php
@@ -0,0 +1,74 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget;
+
+use Icinga\Application\Config;
+use Icinga\Application\Hook\ApplicationStateHook;
+use Icinga\Authentication\Auth;
+use Icinga\Forms\AcknowledgeApplicationStateMessageForm;
+use Icinga\Web\ApplicationStateCookie;
+use Icinga\Web\Helper\Markdown;
+
+/**
+ * Render application state messages
+ */
+class ApplicationStateMessages extends AbstractWidget
+{
+ protected function getMessages()
+ {
+ $cookie = new ApplicationStateCookie();
+
+ $acked = array_flip($cookie->getAcknowledgedMessages());
+ $messages = ApplicationStateHook::getAllMessages();
+
+ $active = array_diff_key($messages, $acked);
+
+ return $active;
+ }
+
+ public function render()
+ {
+ $enabled = Auth::getInstance()
+ ->getUser()
+ ->getPreferences()
+ ->getValue('icingaweb', 'show_application_state_messages', 'system');
+
+ if ($enabled === 'system') {
+ $enabled = Config::app()->get('global', 'show_application_state_messages', true);
+ }
+
+ if (! (bool) $enabled) {
+ return '<div hidden></div>';
+ }
+
+ $active = $this->getMessages();
+
+ if (empty($active)) {
+ // Force container update on XHR
+ return '<div hidden></div>';
+ }
+
+ $html = '<div>';
+
+ reset($active);
+
+ $id = key($active);
+ $spec = current($active);
+ $message = array_pop($spec); // We don't use state and timestamp here
+
+
+ $ackForm = new AcknowledgeApplicationStateMessageForm();
+ $ackForm->populate(['id' => $id]);
+
+ $html .= '<section class="markdown">';
+ $html .= Markdown::text($message);
+ $html .= '</section>';
+
+ $html .= $ackForm;
+
+ $html .= '</div>';
+
+ return $html;
+ }
+}
diff --git a/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php b/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
new file mode 100644
index 0000000..b7b50d0
--- /dev/null
+++ b/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
@@ -0,0 +1,400 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget\Chart;
+
+use DateInterval;
+use DateTime;
+use Icinga\Util\Color;
+use Icinga\Util\Csp;
+use Icinga\Web\Widget\AbstractWidget;
+use ipl\Web\Style;
+
+/**
+ * Display a colored grid that visualizes a set of values for each day
+ * on a given time-frame.
+ */
+class HistoryColorGrid extends AbstractWidget
+{
+ const CAL_GROW_INTO_PAST = 'past';
+ const CAL_GROW_INTO_PRESENT = 'present';
+
+ const ORIENTATION_VERTICAL = 'vertical';
+ const ORIENTATION_HORIZONTAL = 'horizontal';
+
+ public $weekFlow = self::CAL_GROW_INTO_PAST;
+ public $orientation = self::ORIENTATION_VERTICAL;
+ public $weekStartMonday = true;
+
+ private $maxValue = 1;
+
+ private $start = null;
+ private $end = null;
+ private $data = array();
+ private $color;
+ public $opacity = 1.0;
+
+ /** @var array<string, array<string, string>> History grid css rulesets */
+ protected $rulesets = [];
+
+ public function __construct($color = '#51e551', $start = null, $end = null)
+ {
+ $this->setColor($color);
+ if (isset($start)) {
+ $this->start = $this->tsToDateStr($start);
+ }
+ if (isset($end)) {
+ $this->end = $this->tsToDateStr($end);
+ }
+ }
+
+ /**
+ * Set the displayed data-set
+ *
+ * @param $events array The history events to display as an array of arrays:
+ * value: The value to display
+ * caption: The caption on mouse-over
+ * url: The url to open on click.
+ */
+ public function setData(array $events)
+ {
+ $this->data = $events;
+ $start = time();
+ $end = time();
+ foreach ($this->data as $entry) {
+ $entry['value'] = intval($entry['value']);
+ }
+ foreach ($this->data as $date => $entry) {
+ $time = strtotime($date);
+ if ($entry['value'] > $this->maxValue) {
+ $this->maxValue = $entry['value'];
+ }
+ if ($time > $end) {
+ $end = $time;
+ }
+ if ($time < $start) {
+ $start = $time;
+ }
+ }
+ if (!isset($this->start)) {
+ $this->start = $this->tsToDateStr($start);
+ }
+ if (!isset($this->end)) {
+ $this->end = $this->tsToDateStr($end);
+ }
+ }
+
+ /**
+ * Set the used color.
+ *
+ * @param $color
+ */
+ public function setColor($color)
+ {
+ $this->color = $color;
+ }
+
+ /**
+ * Set the used opacity
+ *
+ * @param $opacity
+ */
+ public function setOpacity($opacity)
+ {
+ $this->opacity = $opacity;
+ }
+
+ /**
+ * Calculate the color to display for the given value.
+ *
+ * @param $value integer
+ *
+ * @return string The color-string to use for this entry.
+ */
+ private function calculateColor($value)
+ {
+ $saturation = $value / $this->maxValue;
+ return Color::changeSaturation($this->color, $saturation);
+ }
+
+ /**
+ * Render the html to display the given $day
+ *
+ * @param $day string The day to display YYYY-MM-DD
+ *
+ * @return string The rendered html
+ */
+ private function renderDay($day)
+ {
+ if (array_key_exists($day, $this->data) && $this->data[$day]['value'] > 0) {
+ $entry = $this->data[$day];
+ $this->rulesets['.grid-day-with-entry-' . $entry['value']] = [
+ 'background-color' => $this->calculateColor($entry['value']),
+ 'opacity' => $this->opacity
+ ];
+
+ return '<a class="grid-day-with-entry-'
+ . $entry['value']
+ . '" '
+ . 'aria-label="' . $entry['caption']
+ . '" '
+ . 'title="' . $entry['caption']
+ . '" '
+ . 'href="' . $entry['url']
+ . '" '
+ . '"></a>';
+ } else {
+ if (! isset($this->rulesets['.grid-day-no-entry'])) {
+ $this->rulesets['.grid-day-no-entry'] = [
+ 'background-color' => $this->calculateColor(0),
+ 'opacity' => $this->opacity
+ ];
+ }
+
+ return '<span class="grid-day-no-entry"' . ' title="No entries for ' . $day . '"></span>';
+ }
+ }
+
+ /**
+ * Render the grid with an horizontal alignment.
+ *
+ * @param array $grid The values returned from the createGrid function
+ *
+ * @return string The rendered html
+ */
+ private function renderHorizontal($grid)
+ {
+ $weeks = $grid['weeks'];
+ $months = $grid['months'];
+ $years = $grid['years'];
+ $html = '<table class="historycolorgrid">';
+ $html .= '<tr><th></th>';
+ $old = -1;
+ foreach ($months as $week => $month) {
+ if ($old !== $month) {
+ $old = $month;
+ $txt = $this->monthName($month, $years[$week]);
+ } else {
+ $txt = '';
+ }
+ $html .= '<th>' . $txt . '</th>';
+ }
+ $html .= '</tr>';
+ for ($i = 0; $i < 7; $i++) {
+ $html .= $this->renderWeekdayHorizontal($i, $weeks);
+ }
+ $html .= '</table>';
+ return $html;
+ }
+
+ /**
+ * @param $grid
+ *
+ * @return string
+ */
+ private function renderVertical($grid)
+ {
+ $years = $grid['years'];
+ $weeks = $grid['weeks'];
+ $months = $grid['months'];
+ $html = '<table class="historycolorgrid">';
+ $html .= '<tr>';
+ for ($i = 0; $i < 7; $i++) {
+ $html .= '<th>' . $this->weekdayName($this->weekStartMonday ? $i + 1 : $i) . "</th>";
+ }
+ $html .= '</tr>';
+ $old = -1;
+ foreach ($weeks as $index => $week) {
+ for ($i = 0; $i < 7; $i++) {
+ if (array_key_exists($i, $week)) {
+ $html .= '<td>' . $this->renderDay($week[$i]) . '</td>';
+ } else {
+ $html .= '<td></td>';
+ }
+ }
+ if ($old !== $months[$index]) {
+ $old = $months[$index];
+ $txt = $this->monthName($old, $years[$index]);
+ } else {
+ $txt = '';
+ }
+ $html .= '<td class="weekday">' . $txt . '</td></tr>';
+ }
+ $html .= '</table>';
+ return $html;
+ }
+
+ /**
+ * Render the row for the given weekday.
+ *
+ * @param integer $weekday The day to render (0-6)
+ * @param array $weeks The weeks
+ *
+ * @return string The formatted table-row
+ */
+ private function renderWeekdayHorizontal($weekday, &$weeks)
+ {
+ $html = '<tr><td class="weekday">'
+ . $this->weekdayName($this->weekStartMonday ? $weekday + 1 : $weekday)
+ . '</td>';
+ foreach ($weeks as $week) {
+ if (array_key_exists($weekday, $week)) {
+ $html .= '<td>' . $this->renderDay($week[$weekday]) . '</td>';
+ } else {
+ $html .= '<td></td>';
+ }
+ }
+ $html .= '</tr>';
+ return $html;
+ }
+
+
+
+ /**
+ * @return array
+ */
+ private function createGrid()
+ {
+ $weeks = array(array());
+ $week = 0;
+ $months = array();
+ $years = array();
+ $start = strtotime($this->start);
+ $year = intval(date('Y', $start));
+ $month = intval(date('n', $start));
+ $day = intval(date('j', $start));
+ $weekday = intval(date('w', $start));
+ if ($this->weekStartMonday) {
+ // 0 => monday, 6 => sunday
+ $weekday = $weekday === 0 ? 6 : $weekday - 1;
+ }
+
+ $date = $this->toDateStr($day, $month, $year);
+ $weeks[0][$weekday] = $date;
+ $years[0] = $year;
+ $months[0] = $month;
+ while ($date !== $this->end) {
+ $day++;
+ $weekday++;
+ if ($weekday > 6) {
+ $weekday = 0;
+ $weeks[] = array();
+ // PRESENT => The last day of week determines the month
+ if ($this->weekFlow === self::CAL_GROW_INTO_PRESENT) {
+ $months[$week] = $month;
+ $years[$week] = $year;
+ }
+ $week++;
+ }
+ if ($day > date('t', mktime(0, 0, 0, $month, 1, $year))) {
+ $month++;
+ if ($month > 12) {
+ $year++;
+ $month = 1;
+ }
+ $day = 1;
+ }
+ if ($weekday === 0) {
+ // PAST => The first day of each week determines the month
+ if ($this->weekFlow === self::CAL_GROW_INTO_PAST) {
+ $months[$week] = $month;
+ $years[$week] = $year;
+ }
+ }
+ $date = $this->toDateStr($day, $month, $year);
+ $weeks[$week][$weekday] = $date;
+ };
+ $years[$week] = $year;
+ $months[$week] = $month;
+ if ($this->weekFlow == self::CAL_GROW_INTO_PAST) {
+ return array(
+ 'weeks' => array_reverse($weeks),
+ 'months' => array_reverse($months),
+ 'years' => array_reverse($years)
+ );
+ }
+ return array(
+ 'weeks' => $weeks,
+ 'months' => $months,
+ 'years' => $years
+ );
+ }
+
+ /**
+ * Get the localized month-name for the given month
+ *
+ * @param integer $month The month-number
+ *
+ * @return string The
+ */
+ private function monthName($month, $year)
+ {
+ // TODO: find a way to render years without messing up the layout
+ $dt = new DateTime($year . '-' . $month . '-01');
+ return $dt->format('M');
+ }
+
+ /**
+ * @param $weekday
+ *
+ * @return string
+ */
+ private function weekdayName($weekday)
+ {
+ $sun = new DateTime('last Sunday');
+ $interval = new DateInterval('P' . $weekday . 'D');
+ $sun->add($interval);
+ return substr($sun->format('D'), 0, 2);
+ }
+
+ /**
+ *
+ *
+ * @param $timestamp
+ *
+ * @return bool|string
+ */
+ private function tsToDateStr($timestamp)
+ {
+ return date('Y-m-d', $timestamp);
+ }
+
+ /**
+ * @param $day
+ * @param $mon
+ * @param $year
+ *
+ * @return string
+ */
+ private function toDateStr($day, $mon, $year)
+ {
+ $day = $day > 9 ? (string)$day : '0' . (string)$day;
+ $mon = $mon > 9 ? (string)$mon : '0' . (string)$mon;
+ return $year . '-' . $mon . '-' . $day;
+ }
+
+ /**
+ * @return string
+ */
+ public function render()
+ {
+ if (empty($this->data)) {
+ return '<div>No entries</div>';
+ }
+ $grid = $this->createGrid();
+ if ($this->orientation === self::ORIENTATION_HORIZONTAL) {
+ $html = $this->renderHorizontal($grid);
+ } else {
+ $html = $this->renderVertical($grid);
+ }
+
+ $historyGridStyle = new Style();
+ $historyGridStyle->setNonce(Csp::getStyleNonce());
+
+ foreach ($this->rulesets as $selector => $properties) {
+ $historyGridStyle->add($selector, $properties);
+ }
+
+ return $html . $historyGridStyle;
+ }
+}
diff --git a/library/Icinga/Web/Widget/Chart/InlinePie.php b/library/Icinga/Web/Widget/Chart/InlinePie.php
new file mode 100644
index 0000000..21b4ca4
--- /dev/null
+++ b/library/Icinga/Web/Widget/Chart/InlinePie.php
@@ -0,0 +1,257 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget\Chart;
+
+use Icinga\Chart\PieChart;
+use Icinga\Module\Monitoring\Plugin\PerfdataSet;
+use Icinga\Util\StringHelper;
+use Icinga\Web\Widget\AbstractWidget;
+use Icinga\Web\Url;
+use Icinga\Util\Format;
+use Icinga\Application\Logger;
+use Icinga\Exception\IcingaException;
+use stdClass;
+
+/**
+ * A SVG-PieChart intended to be displayed as a small icon next to labels, to offer a better visualization of the
+ * shown data
+ *
+ * NOTE: When InlinePies are shown in a dynamically loaded view, like the side-bar or in the dashboard, the SVGs will
+ * be replaced with a jQuery-Sparkline to save resources @see loader.js
+ *
+ * @package Icinga\Web\Widget\Chart
+ */
+class InlinePie extends AbstractWidget
+{
+ const NUMBER_FORMAT_NONE = 'none';
+ const NUMBER_FORMAT_TIME = 'time';
+ const NUMBER_FORMAT_BYTES = 'bytes';
+ const NUMBER_FORMAT_RATIO = 'ratio';
+
+ public static $colorsHostStates = array(
+ '#44bb77', // up
+ '#ff99aa', // down
+ '#cc77ff', // unreachable
+ '#77aaff' // pending
+ );
+
+ public static $colorsHostStatesHandledUnhandled = array(
+ '#44bb77', // up
+ '#44bb77',
+ '#ff99aa', // down
+ '#ff5566',
+ '#cc77ff', // unreachable
+ '#aa44ff',
+ '#77aaff', // pending
+ '#77aaff'
+ );
+
+ public static $colorsServiceStates = array(
+ '#44bb77', // Ok
+ '#ffaa44', // Warning
+ '#ff99aa', // Critical
+ '#aa44ff', // Unknown
+ '#77aaff' // Pending
+ );
+
+ public static $colorsServiceStatesHandleUnhandled = array(
+ '#44bb77', // Ok
+ '#44bb77',
+ '#ffaa44', // Warning
+ '#ffcc66',
+ '#ff99aa', // Critical
+ '#ff5566',
+ '#cc77ff', // Unknown
+ '#aa44ff',
+ '#77aaff', // Pending
+ '#77aaff'
+ );
+
+ /**
+ * The template string used for rendering this widget
+ *
+ * @var string
+ */
+ private $template = '<div class="inline-pie {class}">{svg}</div>';
+
+ /**
+ * The colors used to display the slices of this pie-chart.
+ *
+ * @var array
+ */
+ private $colors = array('#049BAF', '#ffaa44', '#ff5566', '#ddccdd');
+
+ /**
+ * The title of the chart
+ *
+ * @var string
+ */
+ private $title;
+
+ /**
+ * @var int
+ */
+ private $size = 16;
+
+ /**
+ * The data displayed by the pie-chart
+ *
+ * @var array
+ */
+ private $data;
+
+ /**
+ * @var
+ */
+ private $class = '';
+
+ /**
+ * Set the data to be displayed.
+ *
+ * @param $data array
+ *
+ * @return $this
+ */
+ public function setData(array $data)
+ {
+ $this->data = $data;
+
+ return $this;
+ }
+
+ /**
+ * Set the size of the inline pie
+ *
+ * @param int $size Sets both, the height and width
+ *
+ * @return $this
+ */
+ public function setSize($size = null)
+ {
+ $this->size = $size;
+
+ return $this;
+ }
+
+ /**
+ * Set the class to define the
+ *
+ * @param $class
+ *
+ * @return $this
+ */
+ public function setSparklineClass($class)
+ {
+ $this->class = $class;
+
+ return $this;
+ }
+
+ /**
+ * Set the colors used by the slices of the pie chart.
+ *
+ * @param array $colors
+ *
+ * @return $this
+ */
+ public function setColors(array $colors = null)
+ {
+ $this->colors = $colors;
+
+ return $this;
+ }
+
+ /**
+ * Set the title of the displayed Data
+ *
+ * @param string $title
+ *
+ * @return $this
+ */
+ public function setTitle($title)
+ {
+ $this->title = $this->view()->escape($title);
+
+ return $this;
+ }
+
+ /**
+ * Create a new InlinePie
+ *
+ * @param array $data The data displayed by the slices
+ * @param string $title The title of this Pie
+ * @param array $colors An array of RGB-Color values to use
+ */
+ public function __construct(array $data, $title, $colors = null)
+ {
+ $this->setTitle($title);
+
+ if (array_key_exists('data', $data)) {
+ $this->data = $data['data'];
+ if (array_key_exists('colors', $data)) {
+ $this->colors = $data['colors'];
+ }
+ } else {
+ $this->setData($data);
+ }
+
+ if (isset($colors)) {
+ $this->setColors($colors);
+ } else {
+ $this->setColors($this->colors);
+ }
+ }
+
+ /**
+ * Renders this widget via the given view and returns the
+ * HTML as a string
+ *
+ * @return string
+ */
+ public function render()
+ {
+ $pie = new PieChart();
+ $pie->alignTopLeft();
+ $pie->disableLegend();
+ $pie->drawPie([
+ 'data' => $this->data,
+ 'colors' => $this->colors
+ ]);
+
+ if ($this->view()->layout()->getLayout() === 'pdf') {
+ try {
+ $png = $pie->toPng($this->size, $this->size);
+ return '<img class="inlinepie" src="data:image/png;base64,' . base64_encode($png) . '" />';
+ } catch (IcingaException $_) {
+ return '';
+ }
+ }
+
+ $pie->title = $this->title;
+ $pie->description = $this->title;
+
+ $template = $this->template;
+ $template = str_replace('{class}', $this->class, $template);
+ $template = str_replace('{svg}', $pie->render(), $template);
+
+ return $template;
+ }
+
+ public static function createFromStateSummary(stdClass $states, $title, array $colors)
+ {
+ $handledUnhandledStates = [];
+ foreach ($states as $key => $value) {
+ if (StringHelper::endsWith($key, '_handled') || StringHelper::endsWith($key, '_unhandled')) {
+ $handledUnhandledStates[$key] = $value;
+ }
+ }
+
+ $chart = new self(array_values($handledUnhandledStates), $title, $colors);
+
+ return $chart
+ ->setSize(50)
+ ->setTitle('')
+ ->setSparklineClass('sparkline-multi');
+ }
+}
diff --git a/library/Icinga/Web/Widget/Dashboard.php b/library/Icinga/Web/Widget/Dashboard.php
new file mode 100644
index 0000000..5a8796d
--- /dev/null
+++ b/library/Icinga/Web/Widget/Dashboard.php
@@ -0,0 +1,475 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget;
+
+use Icinga\Application\Config;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\NotReadableError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Legacy\DashboardConfig;
+use Icinga\User;
+use Icinga\Web\Navigation\DashboardPane;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Dashboard\Dashlet as DashboardDashlet;
+use Icinga\Web\Widget\Dashboard\Pane;
+
+/**
+ * Dashboards display multiple views on a single page
+ *
+ * The terminology is as follows:
+ * - Dashlet: A single view showing a specific url
+ * - Pane: Aggregates one or more dashlets on one page, displays its title as a tab
+ * - Dashboard: Shows all panes
+ *
+ */
+class Dashboard extends AbstractWidget
+{
+ /**
+ * An array containing all panes of this dashboard
+ *
+ * @var array
+ */
+ private $panes = array();
+
+ /**
+ * The @see Icinga\Web\Widget\Tabs object for displaying displayable panes
+ *
+ * @var Tabs
+ */
+ protected $tabs;
+
+ /**
+ * The parameter that will be added to identify panes
+ *
+ * @var string
+ */
+ private $tabParam = 'pane';
+
+ /**
+ * @var User
+ */
+ private $user;
+
+ /**
+ * Set the given tab name as active.
+ *
+ * @param string $name The tab name to activate
+ *
+ */
+ public function activate($name)
+ {
+ $this->getTabs()->activate($name);
+ }
+
+ /**
+ * Load Pane items provided by all enabled modules
+ *
+ * @return $this
+ */
+ public function load()
+ {
+ $navigation = new Navigation();
+ $navigation->load('dashboard-pane');
+
+ $panes = array();
+ foreach ($navigation as $dashboardPane) {
+ /** @var DashboardPane $dashboardPane */
+ $pane = new Pane($dashboardPane->getLabel());
+ foreach ($dashboardPane->getChildren() as $dashlet) {
+ $pane->addDashlet($dashlet->getLabel(), $dashlet->getUrl());
+ }
+
+ $panes[] = $pane;
+ }
+
+ $this->mergePanes($panes);
+ $this->loadUserDashboards($navigation);
+ return $this;
+ }
+
+ /**
+ * Create and return a Config object for this dashboard
+ *
+ * @return Config
+ */
+ public function getConfig()
+ {
+ $output = array();
+ foreach ($this->panes as $pane) {
+ if ($pane->isUserWidget()) {
+ $output[$pane->getName()] = $pane->toArray();
+ }
+ foreach ($pane->getDashlets() as $dashlet) {
+ if ($dashlet->isUserWidget()) {
+ $output[$pane->getName() . '.' . $dashlet->getName()] = $dashlet->toArray();
+ }
+ }
+ }
+
+ return DashboardConfig::fromArray($output)->setConfigFile($this->getConfigFile())->setUser($this->user);
+ }
+
+ /**
+ * Load user dashboards from all config files that match the username
+ */
+ protected function loadUserDashboards(Navigation $navigation)
+ {
+ foreach (DashboardConfig::listConfigFilesForUser($this->user) as $file) {
+ $this->loadUserDashboardsFromFile($file, $navigation);
+ }
+ }
+
+ /**
+ * Load user dashboards from the given config file
+ *
+ * @param string $file
+ *
+ * @return bool
+ */
+ protected function loadUserDashboardsFromFile($file, Navigation $dashboardNavigation)
+ {
+ try {
+ $config = Config::fromIni($file);
+ } catch (NotReadableError $e) {
+ return false;
+ }
+
+ if (! count($config)) {
+ return false;
+ }
+ $panes = array();
+ $dashlets = array();
+ foreach ($config as $key => $part) {
+ if (strpos($key, '.') === false) {
+ $dashboardPane = $dashboardNavigation->getItem($key);
+ if ($dashboardPane !== null) {
+ $key = $dashboardPane->getLabel();
+ }
+ if ($this->hasPane($key)) {
+ $panes[$key] = $this->getPane($key);
+ } else {
+ $panes[$key] = new Pane($key);
+ $panes[$key]->setTitle($part->title);
+ }
+ $panes[$key]->setUserWidget();
+ if ((bool) $part->get('disabled', false) === true) {
+ $panes[$key]->setDisabled();
+ }
+ } else {
+ list($paneName, $dashletName) = explode('.', $key, 2);
+ $dashboardPane = $dashboardNavigation->getItem($paneName);
+ if ($dashboardPane !== null) {
+ $paneName = $dashboardPane->getLabel();
+ $dashletItem = $dashboardPane->getChildren()->getItem($dashletName);
+ if ($dashletItem !== null) {
+ $dashletName = $dashletItem->getLabel();
+ }
+ }
+ $part->pane = $paneName;
+ $part->dashlet = $dashletName;
+ $dashlets[] = $part;
+ }
+ }
+ foreach ($dashlets as $dashletData) {
+ $pane = null;
+
+ if (array_key_exists($dashletData->pane, $panes) === true) {
+ $pane = $panes[$dashletData->pane];
+ } elseif (array_key_exists($dashletData->pane, $this->panes) === true) {
+ $pane = $this->panes[$dashletData->pane];
+ } else {
+ continue;
+ }
+ $dashlet = new DashboardDashlet(
+ $dashletData->title,
+ $dashletData->url,
+ $pane
+ );
+ $dashlet->setName($dashletData->dashlet);
+
+ if ((bool) $dashletData->get('disabled', false) === true) {
+ $dashlet->setDisabled(true);
+ }
+
+ $dashlet->setUserWidget();
+ $pane->addDashlet($dashlet);
+ }
+
+ $this->mergePanes($panes);
+
+ return true;
+ }
+
+ /**
+ * Merge panes with existing panes
+ *
+ * @param array $panes
+ *
+ * @return $this
+ */
+ public function mergePanes(array $panes)
+ {
+ /** @var $pane Pane */
+ foreach ($panes as $pane) {
+ if ($this->hasPane($pane->getName()) === true) {
+ /** @var $current Pane */
+ $current = $this->panes[$pane->getName()];
+ $current->addDashlets($pane->getDashlets());
+ } else {
+ $this->panes[$pane->getName()] = $pane;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the tab object used to navigate through this dashboard
+ *
+ * @return Tabs
+ */
+ public function getTabs()
+ {
+ $url = Url::fromPath('dashboard')->getUrlWithout($this->tabParam);
+ if ($this->tabs === null) {
+ $this->tabs = new Tabs();
+
+ foreach ($this->panes as $key => $pane) {
+ if ($pane->getDisabled()) {
+ continue;
+ }
+ $this->tabs->add(
+ $key,
+ array(
+ 'title' => sprintf(
+ t('Show %s', 'dashboard.pane.tooltip'),
+ $pane->getTitle()
+ ),
+ 'label' => $pane->getTitle(),
+ 'url' => clone($url),
+ 'urlParams' => array($this->tabParam => $key)
+ )
+ );
+ }
+ }
+ return $this->tabs;
+ }
+
+ /**
+ * Return all panes of this dashboard
+ *
+ * @return array
+ */
+ public function getPanes()
+ {
+ return $this->panes;
+ }
+
+
+ /**
+ * Creates a new empty pane with the given title
+ *
+ * @param string $title
+ *
+ * @return $this
+ */
+ public function createPane($title)
+ {
+ $pane = new Pane($title);
+ $pane->setTitle($title);
+ $this->addPane($pane);
+
+ return $this;
+ }
+
+ /**
+ * Checks if the current dashboard has any panes
+ *
+ * @return bool
+ */
+ public function hasPanes()
+ {
+ return ! empty($this->panes);
+ }
+
+ /**
+ * Check if a panel exist
+ *
+ * @param string $pane
+ * @return bool
+ */
+ public function hasPane($pane)
+ {
+ return $pane && array_key_exists($pane, $this->panes);
+ }
+
+ /**
+ * Add a pane object to this dashboard
+ *
+ * @param Pane $pane The pane to add
+ *
+ * @return $this
+ */
+ public function addPane(Pane $pane)
+ {
+ $this->panes[$pane->getName()] = $pane;
+ return $this;
+ }
+
+ public function removePane($title)
+ {
+ if ($this->hasPane($title) === true) {
+ $pane = $this->getPane($title);
+ if ($pane->isUserWidget() === true) {
+ unset($this->panes[$pane->getName()]);
+ } else {
+ $pane->setDisabled();
+ $pane->setUserWidget();
+ }
+ } else {
+ throw new ProgrammingError('Pane not found: ' . $title);
+ }
+ }
+
+ /**
+ * Return the pane with the provided name
+ *
+ * @param string $name The name of the pane to return
+ *
+ * @return Pane The pane or null if no pane with the given name exists
+ * @throws ProgrammingError
+ */
+ public function getPane($name)
+ {
+ if (! array_key_exists($name, $this->panes)) {
+ throw new ProgrammingError(
+ 'Trying to retrieve invalid dashboard pane "%s"',
+ $name
+ );
+ }
+ return $this->panes[$name];
+ }
+
+ /**
+ * Return an array with pane name=>title format used for comboboxes
+ *
+ * @return array
+ */
+ public function getPaneKeyTitleArray()
+ {
+ $list = array();
+ foreach ($this->panes as $name => $pane) {
+ $list[$name] = $pane->getTitle();
+ }
+ return $list;
+ }
+
+ /**
+ * @see Icinga\Web\Widget::render
+ */
+ public function render()
+ {
+ if (empty($this->panes)) {
+ return '';
+ }
+
+ return $this->determineActivePane()->render();
+ }
+
+ /**
+ * Activates the default pane of this dashboard and returns its name
+ *
+ * @return mixed
+ */
+ private function setDefaultPane()
+ {
+ $active = null;
+
+ foreach ($this->panes as $key => $pane) {
+ if ($pane->getDisabled() === false) {
+ $active = $key;
+ break;
+ }
+ }
+
+ if ($active !== null) {
+ $this->activate($active);
+ }
+ return $active;
+ }
+
+ /**
+ * @see determineActivePane()
+ */
+ public function getActivePane()
+ {
+ return $this->determineActivePane();
+ }
+
+ /**
+ * Determine the active pane either by the selected tab or the current request
+ *
+ * @throws \Icinga\Exception\ConfigurationError
+ * @throws \Icinga\Exception\ProgrammingError
+ *
+ * @return Pane The currently active pane
+ */
+ public function determineActivePane()
+ {
+ $active = $this->getTabs()->getActiveName();
+ if (! $active) {
+ if ($active = Url::fromRequest()->getParam($this->tabParam)) {
+ if ($this->hasPane($active)) {
+ $this->activate($active);
+ } else {
+ throw new ProgrammingError(
+ 'Try to get an inexistent pane.'
+ );
+ }
+ } else {
+ $active = $this->setDefaultPane();
+ }
+ }
+
+ if (isset($this->panes[$active])) {
+ return $this->panes[$active];
+ }
+
+ throw new ConfigurationError('Could not determine active pane');
+ }
+
+ /**
+ * Setter for user object
+ *
+ * @param User $user
+ */
+ public function setUser(User $user)
+ {
+ $this->user = $user;
+ }
+
+ /**
+ * Getter for user object
+ *
+ * @return User
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * Get config file
+ *
+ * @return string
+ */
+ public function getConfigFile()
+ {
+ if ($this->user === null) {
+ throw new ProgrammingError('Can\'t load dashboards. User is not set');
+ }
+ return Config::resolvePath('dashboards/' . strtolower($this->user->getUsername()) . '/dashboard.ini');
+ }
+}
diff --git a/library/Icinga/Web/Widget/Dashboard/Dashlet.php b/library/Icinga/Web/Widget/Dashboard/Dashlet.php
new file mode 100644
index 0000000..2ba26df
--- /dev/null
+++ b/library/Icinga/Web/Widget/Dashboard/Dashlet.php
@@ -0,0 +1,315 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget\Dashboard;
+
+use Icinga\Web\Url;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\IcingaException;
+
+/**
+ * A dashboard pane dashlet
+ *
+ * This is the element displaying a specific view in icinga2web
+ *
+ */
+class Dashlet extends UserWidget
+{
+ /**
+ * The url of this Dashlet
+ *
+ * @var Url|null
+ */
+ private $url;
+
+ private $name;
+
+ /**
+ * The title being displayed on top of the dashlet
+ * @var
+ */
+ private $title;
+
+ /**
+ * The pane containing this dashlet, needed for the 'remove button'
+ * @var Pane
+ */
+ private $pane;
+
+ /**
+ * The disabled option is used to "delete" default dashlets provided by modules
+ *
+ * @var bool
+ */
+ private $disabled = false;
+
+ /**
+ * The progress label being used
+ *
+ * @var string
+ */
+ private $progressLabel;
+
+ /**
+ * The template string used for rendering this widget
+ *
+ * @var string
+ */
+ private $template =<<<'EOD'
+
+ <div class="container" data-icinga-url="{URL}">
+ <h1><a href="{FULL_URL}" aria-label="{TOOLTIP}" title="{TOOLTIP}" data-base-target="col1">{TITLE}</a></h1>
+ <p class="progress-label">{PROGRESS_LABEL}<span>.</span><span>.</span><span>.</span></p>
+ <noscript>
+ <div class="iframe-container">
+ <iframe
+ src="{IFRAME_URL}"
+ frameborder="no"
+ title="{TITLE_PREFIX}{TITLE}">
+ </iframe>
+ </div>
+ </noscript>
+ </div>
+EOD;
+
+ /**
+ * The template string used for rendering this widget in case of an error
+ *
+ * @var string
+ */
+ private $errorTemplate = <<<'EOD'
+
+ <div class="container">
+ <h1 title="{TOOLTIP}">{TITLE}</h1>
+ <p class="error-message">{ERROR_MESSAGE}</p>
+ </div>
+EOD;
+
+ /**
+ * Create a new dashlet displaying the given url in the provided pane
+ *
+ * @param string $title The title to use for this dashlet
+ * @param Url|string $url The url this dashlet uses for displaying information
+ * @param Pane $pane The pane this Dashlet will be added to
+ */
+ public function __construct($title, $url, Pane $pane)
+ {
+ $this->name = $title;
+ $this->title = $title;
+ $this->pane = $pane;
+ $this->url = $url;
+ }
+
+ public function setName($name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Retrieve the dashlets title
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * @param string $title
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ }
+
+ /**
+ * Retrieve the dashlets url
+ *
+ * @return Url|null
+ */
+ public function getUrl()
+ {
+ if ($this->url !== null && ! $this->url instanceof Url) {
+ $this->url = Url::fromPath($this->url);
+ }
+ return $this->url;
+ }
+
+ /**
+ * Set the dashlets URL
+ *
+ * @param string|Url $url The url to use, either as an Url object or as a path
+ *
+ * @return $this
+ */
+ public function setUrl($url)
+ {
+ $this->url = $url;
+ return $this;
+ }
+
+ /**
+ * Set the disabled property
+ *
+ * @param boolean $disabled
+ */
+ public function setDisabled($disabled)
+ {
+ $this->disabled = $disabled;
+ }
+
+ /**
+ * Get the disabled property
+ *
+ * @return boolean
+ */
+ public function getDisabled()
+ {
+ return $this->disabled;
+ }
+
+ /**
+ * Set the progress label to use
+ *
+ * @param string $label
+ *
+ * @return $this
+ */
+ public function setProgressLabel($label)
+ {
+ $this->progressLabel = $label;
+ return $this;
+ }
+
+ /**
+ * Return the progress label to use
+ *
+ * @return string
+ */
+ public function getProgressLabe()
+ {
+ if ($this->progressLabel === null) {
+ return $this->view()->translate('Loading');
+ }
+
+ return $this->progressLabel;
+ }
+
+ /**
+ * Return this dashlet's structure as array
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ $array = array(
+ 'url' => $this->getUrl()->getRelativeUrl(),
+ 'title' => $this->getTitle()
+ );
+ if ($this->getDisabled() === true) {
+ $array['disabled'] = 1;
+ }
+ return $array;
+ }
+
+ /**
+ * @see Widget::render()
+ */
+ public function render()
+ {
+ if ($this->disabled === true) {
+ return '';
+ }
+
+ $view = $this->view();
+
+ if (! $this->url) {
+ $searchTokens = array(
+ '{TOOLTIP}',
+ '{TITLE}',
+ '{ERROR_MESSAGE}'
+ );
+
+ $replaceTokens = array(
+ sprintf($view->translate('Show %s', 'dashboard.dashlet.tooltip'), $view->escape($this->getTitle())),
+ $view->escape($this->getTitle()),
+ $view->escape(
+ sprintf($view->translate('Cannot create dashboard dashlet "%s" without valid URL'), $this->title)
+ )
+ );
+
+ return str_replace($searchTokens, $replaceTokens, $this->errorTemplate);
+ }
+
+ $url = $this->getUrl();
+ $url->setParam('showCompact', true);
+ $iframeUrl = clone $url;
+ $iframeUrl->setParam('isIframe');
+
+ $searchTokens = array(
+ '{URL}',
+ '{IFRAME_URL}',
+ '{FULL_URL}',
+ '{TOOLTIP}',
+ '{TITLE}',
+ '{TITLE_PREFIX}',
+ '{PROGRESS_LABEL}'
+ );
+
+ $replaceTokens = array(
+ $url,
+ $iframeUrl,
+ $url->getUrlWithout(['showCompact', 'limit', 'view']),
+ sprintf($view->translate('Show %s', 'dashboard.dashlet.tooltip'), $view->escape($this->getTitle())),
+ $view->escape($this->getTitle()),
+ $view->translate('Dashlet') . ': ',
+ $this->getProgressLabe()
+ );
+
+ return str_replace($searchTokens, $replaceTokens, $this->template);
+ }
+
+ /**
+ * Create a @see Dashlet instance from the given Zend config, using the provided title
+ *
+ * @param $title The title for this dashlet
+ * @param ConfigObject $config The configuration defining url, parameters, height, width, etc.
+ * @param Pane $pane The pane this dashlet belongs to
+ *
+ * @return Dashlet A newly created Dashlet for use in the Dashboard
+ */
+ public static function fromIni($title, ConfigObject $config, Pane $pane)
+ {
+ $height = null;
+ $width = null;
+ $url = $config->get('url');
+ $parameters = $config->toArray();
+ unset($parameters['url']); // otherwise there's an url = parameter in the Url
+
+ $cmp = new Dashlet($title, Url::fromPath($url, $parameters), $pane);
+ return $cmp;
+ }
+
+ /**
+ * @param \Icinga\Web\Widget\Dashboard\Pane $pane
+ */
+ public function setPane(Pane $pane)
+ {
+ $this->pane = $pane;
+ }
+
+ /**
+ * @return \Icinga\Web\Widget\Dashboard\Pane
+ */
+ public function getPane()
+ {
+ return $this->pane;
+ }
+}
diff --git a/library/Icinga/Web/Widget/Dashboard/Pane.php b/library/Icinga/Web/Widget/Dashboard/Pane.php
new file mode 100644
index 0000000..c8b14c5
--- /dev/null
+++ b/library/Icinga/Web/Widget/Dashboard/Pane.php
@@ -0,0 +1,335 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget\Dashboard;
+
+use Icinga\Data\ConfigObject;
+use Icinga\Web\Widget\AbstractWidget;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\ConfigurationError;
+
+/**
+ * A pane, displaying different Dashboard dashlets
+ */
+class Pane extends UserWidget
+{
+ /**
+ * The name of this pane, as defined in the ini file
+ *
+ * @var string
+ */
+ private $name;
+
+ /**
+ * The title of this pane, as displayed in the dashboard tabs
+ *
+ * @var string
+ */
+ private $title;
+
+ /**
+ * An array of @see Dashlets that are displayed in this pane
+ *
+ * @var array
+ */
+ private $dashlets = array();
+
+ /**
+ * Disabled flag of a pane
+ *
+ * @var bool
+ */
+ private $disabled = false;
+
+ /**
+ * Create a new pane
+ *
+ * @param string $name The pane to create
+ */
+ public function __construct($name)
+ {
+ $this->name = $name;
+ $this->title = $name;
+ }
+
+ /**
+ * Set the name of this pane
+ *
+ * @param string $name
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ }
+
+ /**
+ * Returns the name of this pane
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Returns the title of this pane
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * Overwrite the title of this pane
+ *
+ * @param string $title The new title to use for this pane
+ *
+ * @return $this
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ /**
+ * Return true if a dashlet with the given title exists in this pane
+ *
+ * @param string $title The title of the dashlet to check for existence
+ *
+ * @return bool
+ */
+ public function hasDashlet($title)
+ {
+ return array_key_exists($title, $this->dashlets);
+ }
+
+ /**
+ * Checks if the current pane has any dashlets
+ *
+ * @return bool
+ */
+ public function hasDashlets()
+ {
+ return ! empty($this->dashlets);
+ }
+
+ /**
+ * Return a dashlet with the given name if existing
+ *
+ * @param string $title The title of the dashlet to return
+ *
+ * @return Dashlet The dashlet with the given title
+ * @throws ProgrammingError If the dashlet doesn't exist
+ */
+ public function getDashlet($title)
+ {
+ if ($this->hasDashlet($title)) {
+ return $this->dashlets[$title];
+ }
+ throw new ProgrammingError(
+ 'Trying to access invalid dashlet: %s',
+ $title
+ );
+ }
+
+ /**
+ * Removes the dashlet with the given title if it exists in this pane
+ *
+ * @param string $title The pane
+ * @return Pane $this
+ */
+ public function removeDashlet($title)
+ {
+ if ($this->hasDashlet($title)) {
+ $dashlet = $this->getDashlet($title);
+ if ($dashlet->isUserWidget() === true) {
+ unset($this->dashlets[$title]);
+ } else {
+ $dashlet->setDisabled(true);
+ $dashlet->setUserWidget();
+ }
+ } else {
+ throw new ProgrammingError('Dashlet does not exist: ' . $title);
+ }
+ return $this;
+ }
+
+ /**
+ * Removes all or a given list of dashlets from this pane
+ *
+ * @param array $dashlets Optional list of dashlet titles
+ * @return Pane $this
+ */
+ public function removeDashlets(array $dashlets = null)
+ {
+ if ($dashlets === null) {
+ $this->dashlets = array();
+ } else {
+ foreach ($dashlets as $dashlet) {
+ $this->removeDashlet($dashlet);
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Return all dashlets added at this pane
+ *
+ * @return array
+ */
+ public function getDashlets()
+ {
+ return $this->dashlets;
+ }
+
+ /**
+ * @see Widget::render
+ */
+ public function render()
+ {
+ $dashlets = array_filter(
+ $this->dashlets,
+ function ($e) {
+ return ! $e->getDisabled();
+ }
+ );
+ return implode("\n", $dashlets) . "\n";
+ }
+
+ /**
+ * Create, add and return a new dashlet
+ *
+ * @param string $title
+ * @param string $url
+ *
+ * @return Dashlet
+ */
+ public function createDashlet($title, $url = null)
+ {
+ $dashlet = new Dashlet($title, $url, $this);
+ $this->addDashlet($dashlet);
+ return $dashlet;
+ }
+
+ /**
+ * Add a dashlet to this pane, optionally creating it if $dashlet is a string
+ *
+ * @param string|Dashlet $dashlet The dashlet object or title
+ * (if a new dashlet will be created)
+ * @param string|null $url An Url to be used when dashlet is a string
+ *
+ * @return $this
+ * @throws \Icinga\Exception\ConfigurationError
+ */
+ public function addDashlet($dashlet, $url = null)
+ {
+ if ($dashlet instanceof Dashlet) {
+ $this->dashlets[$dashlet->getName()] = $dashlet;
+ } elseif (is_string($dashlet) && $url !== null) {
+ $this->createDashlet($dashlet, $url);
+ } else {
+ throw new ConfigurationError('Invalid dashlet added: %s', $dashlet);
+ }
+ return $this;
+ }
+
+ /**
+ * Add new dashlets to existing dashlets
+ *
+ * @param array $dashlets
+ * @return $this
+ */
+ public function addDashlets(array $dashlets)
+ {
+ /* @var $dashlet Dashlet */
+ foreach ($dashlets as $dashlet) {
+ if (array_key_exists($dashlet->getName(), $this->dashlets)) {
+ if (preg_match('/_(\d+)$/', $dashlet->getName(), $m)) {
+ $name = preg_replace('/_\d+$/', $m[1]++, $dashlet->getName());
+ } else {
+ $name = $dashlet->getName() . '_2';
+ }
+ $this->dashlets[$name] = $dashlet;
+ } else {
+ $this->dashlets[$dashlet->getName()] = $dashlet;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add a dashlet to the current pane
+ *
+ * @param $title
+ * @param $url
+ * @return Dashlet
+ *
+ * @see addDashlet()
+ */
+ public function add($title, $url = null)
+ {
+ $this->addDashlet($title, $url);
+
+ return $this->dashlets[$title];
+ }
+
+ /**
+ * Return the this pane's structure as array
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ $pane = array(
+ 'title' => $this->getTitle(),
+ );
+
+ if ($this->getDisabled() === true) {
+ $pane['disabled'] = 1;
+ }
+
+ return $pane;
+ }
+
+ /**
+ * Create a new pane with the title $title from the given configuration
+ *
+ * @param $title The title for this pane
+ * @param ConfigObject $config The configuration to use for setup
+ *
+ * @return Pane
+ */
+ public static function fromIni($title, ConfigObject $config)
+ {
+ $pane = new Pane($title);
+ if ($config->get('title', false)) {
+ $pane->setTitle($config->get('title'));
+ }
+ return $pane;
+ }
+
+ /**
+ * Setter for disabled
+ *
+ * @param boolean $disabled
+ */
+ public function setDisabled($disabled = true)
+ {
+ $this->disabled = (bool) $disabled;
+ }
+
+ /**
+ * Getter for disabled
+ *
+ * @return boolean
+ */
+ public function getDisabled()
+ {
+ return $this->disabled;
+ }
+}
diff --git a/library/Icinga/Web/Widget/Dashboard/UserWidget.php b/library/Icinga/Web/Widget/Dashboard/UserWidget.php
new file mode 100644
index 0000000..164d58b
--- /dev/null
+++ b/library/Icinga/Web/Widget/Dashboard/UserWidget.php
@@ -0,0 +1,36 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget\Dashboard;
+
+use Icinga\Web\Widget\AbstractWidget;
+
+abstract class UserWidget extends AbstractWidget
+{
+ /**
+ * Flag if widget is created by an user
+ *
+ * @var bool
+ */
+ protected $userWidget = false;
+
+ /**
+ * Set the user widget flag
+ *
+ * @param boolean $userWidget
+ */
+ public function setUserWidget($userWidget = true)
+ {
+ $this->userWidget = (bool) $userWidget;
+ }
+
+ /**
+ * Getter for user widget flag
+ *
+ * @return boolean
+ */
+ public function isUserWidget()
+ {
+ return $this->userWidget;
+ }
+}
diff --git a/library/Icinga/Web/Widget/FilterEditor.php b/library/Icinga/Web/Widget/FilterEditor.php
new file mode 100644
index 0000000..24f4b15
--- /dev/null
+++ b/library/Icinga/Web/Widget/FilterEditor.php
@@ -0,0 +1,811 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget;
+
+use Icinga\Data\Filterable;
+use Icinga\Data\FilterColumns;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterOr;
+use Icinga\Web\Url;
+use Icinga\Application\Icinga;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Web\Notification;
+use Exception;
+
+/**
+ * Filter
+ */
+class FilterEditor extends AbstractWidget
+{
+ /**
+ * The filter
+ *
+ * @var Filter
+ */
+ private $filter;
+
+ /**
+ * The query to filter
+ *
+ * @var Filterable
+ */
+ protected $query;
+
+ protected $url;
+
+ protected $addTo;
+
+ protected $cachedColumnSelect;
+
+ protected $preserveParams = array();
+
+ protected $preservedParams = array();
+
+ protected $preservedUrl;
+
+ protected $ignoreParams = array();
+
+ protected $searchColumns;
+
+ /**
+ * @var string
+ */
+ private $selectedIdx;
+
+ /**
+ * Whether the filter control is visible
+ *
+ * @var bool
+ */
+ protected $visible = true;
+
+ /**
+ * Create a new FilterEditor
+ *
+ * @param Filter $filter Your filter
+ */
+ public function __construct($props)
+ {
+ if (array_key_exists('filter', $props)) {
+ $this->setFilter($props['filter']);
+ }
+ if (array_key_exists('query', $props)) {
+ $this->setQuery($props['query']);
+ }
+ }
+
+ public function setFilter(Filter $filter)
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ public function getFilter()
+ {
+ if ($this->filter === null) {
+ $this->filter = Filter::fromQueryString((string) $this->url()->getParams());
+ }
+ return $this->filter;
+ }
+
+ /**
+ * Set columns to search in
+ *
+ * @param array $searchColumns
+ *
+ * @return $this
+ */
+ public function setSearchColumns(array $searchColumns = null)
+ {
+ $this->searchColumns = $searchColumns;
+ return $this;
+ }
+
+ public function setUrl($url)
+ {
+ $this->url = $url;
+ return $this;
+ }
+
+ protected function url()
+ {
+ if ($this->url === null) {
+ $this->url = Url::fromRequest();
+ }
+ return $this->url;
+ }
+
+ protected function preservedUrl()
+ {
+ if ($this->preservedUrl === null) {
+ $this->preservedUrl = $this->url()->with($this->preservedParams);
+ }
+ return $this->preservedUrl;
+ }
+
+ /**
+ * Set the query to filter
+ *
+ * @param Filterable $query
+ *
+ * @return $this
+ */
+ public function setQuery(Filterable $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ public function ignoreParams()
+ {
+ $this->ignoreParams = func_get_args();
+ return $this;
+ }
+
+ public function preserveParams()
+ {
+ $this->preserveParams = func_get_args();
+ return $this;
+ }
+
+ /**
+ * Get whether the filter control is visible
+ *
+ * @return bool
+ */
+ public function isVisible()
+ {
+ return $this->visible;
+ }
+
+ /**
+ * Set whether the filter control is visible
+ *
+ * @param bool $visible
+ *
+ * @return $this
+ */
+ public function setVisible($visible)
+ {
+ $this->visible = (bool) $visible;
+
+ return $this;
+ }
+
+ protected function redirectNow($url)
+ {
+ $response = Icinga::app()->getFrontController()->getResponse();
+ $response->redirectAndExit($url);
+ }
+
+ protected function mergeRootExpression($filter, $column, $sign, $expression)
+ {
+ $found = false;
+ if ($filter->isChain() && $filter->getOperatorName() === 'AND') {
+ foreach ($filter->filters() as $f) {
+ if ($f->isExpression()
+ && $f->getColumn() === $column
+ && $f->getSign() === $sign
+ ) {
+ $f->setExpression($expression);
+ $found = true;
+ break;
+ }
+ }
+ } elseif ($filter->isExpression()) {
+ if ($filter->getColumn() === $column && $filter->getSign() === $sign) {
+ $filter->setExpression($expression);
+ $found = true;
+ }
+ }
+ if (! $found) {
+ $filter = $filter->andFilter(
+ Filter::expression($column, $sign, $expression)
+ );
+ }
+ return $filter;
+ }
+
+ protected function resetSearchColumns(Filter &$filter)
+ {
+ if ($filter->isChain()) {
+ $filters = &$filter->filters();
+ if (!($empty = empty($filters))) {
+ foreach ($filters as $k => &$f) {
+ if (false === $this->resetSearchColumns($f)) {
+ unset($filters[$k]);
+ }
+ }
+ }
+ return $empty || !empty($filters);
+ }
+ return $filter->isExpression() ? !(
+ in_array($filter->getColumn(), $this->searchColumns)
+ &&
+ $filter->getSign() === '='
+ ) : true;
+ }
+
+ public function handleRequest($request)
+ {
+ $this->setUrl($request->getUrl()->without($this->ignoreParams));
+ $params = $this->url()->getParams();
+
+ $preserve = array();
+ foreach ($this->preserveParams as $key) {
+ if (null !== ($value = $params->shift($key))) {
+ $preserve[$key] = $value;
+ }
+ }
+ $this->preservedParams = $preserve;
+
+ $add = $params->shift('addFilter');
+ $remove = $params->shift('removeFilter');
+ $strip = $params->shift('stripFilter');
+ $modify = $params->shift('modifyFilter');
+
+
+
+ $search = null;
+ if ($request->isPost()) {
+ $search = $request->getPost('q');
+ }
+
+ if ($search === null) {
+ $search = $params->shift('q');
+ }
+
+ $filter = $this->getFilter();
+
+ if ($search !== null) {
+ if (strpos($search, '=') !== false) {
+ list($k, $v) = preg_split('/=/', $search);
+ $filter = $this->mergeRootExpression($filter, trim($k), '=', ltrim($v));
+ } else {
+ if ($this->searchColumns === null && $this->query instanceof FilterColumns) {
+ $this->searchColumns = $this->query->getSearchColumns($search);
+ }
+
+ if (! empty($this->searchColumns)) {
+ if (! $this->resetSearchColumns($filter)) {
+ $filter = Filter::matchAll();
+ }
+ $filters = array();
+ $search = trim($search);
+ foreach ($this->searchColumns as $searchColumn) {
+ $filters[] = Filter::expression($searchColumn, '=', "*$search*");
+ }
+ $filter = $filter->andFilter(new FilterOr($filters));
+ } else {
+ Notification::error(mt('monitoring', 'Cannot search here'));
+ return $this;
+ }
+ }
+
+ $url = Url::fromRequest()->onlyWith($this->preserveParams);
+ $urlParams = $url->getParams();
+ $url->setQueryString($filter->toQueryString());
+ foreach ($urlParams->toArray(false) as $key => $value) {
+ $url->getParams()->addEncoded($key, $value);
+ }
+
+ $this->redirectNow($url);
+ }
+
+ if ($remove) {
+ $redirect = $this->url();
+ if ($filter->getById($remove)->isRootNode()) {
+ $redirect->setQueryString('');
+ } else {
+ $filter->removeId($remove);
+ $redirect->setQueryString($filter->toQueryString())->getParams()->add('modifyFilter');
+ }
+ $this->redirectNow($redirect->addParams($preserve));
+ }
+
+ if ($strip) {
+ $redirect = $this->url();
+ $subId = $strip . '-1';
+ if ($filter->getId() === $strip) {
+ $filter = $filter->getById($strip . '-1');
+ } else {
+ $filter->replaceById($strip, $filter->getById($strip . '-1'));
+ }
+ $redirect->setQueryString($filter->toQueryString())->getParams()->add('modifyFilter');
+ $this->redirectNow($redirect->addParams($preserve));
+ }
+
+
+ if ($modify) {
+ if ($request->isPost()) {
+ if ($request->get('cancel') === 'Cancel') {
+ $this->redirectNow($this->preservedUrl()->without('modifyFilter'));
+ }
+ if ($request->get('formUID') === 'FilterEditor') {
+ $filter = $this->applyChanges($request->getPost());
+ $url = $this->url()->setQueryString($filter->toQueryString())->addParams($preserve);
+ $url->getParams()->add('modifyFilter');
+
+ $addFilter = $request->get('add_filter');
+ if ($addFilter !== null) {
+ $url->setParam('addFilter', $addFilter);
+ }
+
+ $removeFilter = $request->get('remove_filter');
+ if ($removeFilter !== null) {
+ $url->setParam('removeFilter', $removeFilter);
+ }
+
+ $this->redirectNow($url);
+ }
+ }
+ $this->url()->getParams()->add('modifyFilter');
+ }
+
+ if ($add) {
+ $this->addFilterToId($add);
+ }
+
+ if ($this->query !== null && $request->isGet()) {
+ $this->query->applyFilter($this->getFilter());
+ }
+
+ return $this;
+ }
+
+ protected function select($name, $list, $selected, $attributes = null)
+ {
+ $view = $this->view();
+ if ($attributes === null) {
+ $attributes = '';
+ } else {
+ $attributes = $view->propertiesToString($attributes);
+ }
+ $html = sprintf(
+ '<select name="%s"%s class="autosubmit">' . "\n",
+ $view->escape($name),
+ $attributes
+ );
+
+ foreach ($list as $k => $v) {
+ $active = '';
+ if ($k === $selected) {
+ $active = ' selected="selected"';
+ }
+ $html .= sprintf(
+ ' <option value="%s"%s>%s</option>' . "\n",
+ $view->escape($k),
+ $active,
+ $view->escape($v)
+ );
+ }
+ $html .= '</select>' . "\n\n";
+ return $html;
+ }
+
+ protected function addFilterToId($id)
+ {
+ $this->addTo = $id;
+ return $this;
+ }
+
+ protected function removeIndex($idx)
+ {
+ $this->selectedIdx = $idx;
+ return $this;
+ }
+
+ protected function removeLink(Filter $filter)
+ {
+ return "<button type='submit' name='remove_filter' value='{$filter->getId()}'>"
+ . $this->view()->icon('trash', t('Remove this part of your filter'))
+ . '</button>';
+ }
+
+ protected function addLink(Filter $filter)
+ {
+ return "<button type='submit' name='add_filter' value='{$filter->getId()}'>"
+ . $this->view()->icon('plus', t('Add another filter'))
+ . '</button>';
+ }
+
+ protected function stripLink(Filter $filter)
+ {
+ return $this->view()->qlink(
+ '',
+ $this->preservedUrl()->with('stripFilter', $filter->getId()),
+ null,
+ array(
+ 'icon' => 'minus',
+ 'title' => t('Strip this filter')
+ )
+ );
+ }
+
+ protected function cancelLink()
+ {
+ return $this->view()->qlink(
+ '',
+ $this->preservedUrl()->without('addFilter'),
+ null,
+ array(
+ 'icon' => 'cancel',
+ 'title' => t('Cancel this operation')
+ )
+ );
+ }
+
+ protected function renderFilter($filter, $level = 0)
+ {
+ if ($level === 0 && $filter->isChain() && $filter->isEmpty()) {
+ return '<ul class="datafilter"><li class="active">' . $this->renderNewFilter() . '</li></ul>';
+ }
+
+ if ($filter instanceof FilterChain) {
+ return $this->renderFilterChain($filter, $level);
+ } elseif ($filter instanceof FilterExpression) {
+ return $this->renderFilterExpression($filter);
+ } else {
+ throw new ProgrammingError('Got a Filter being neither expression nor chain');
+ }
+ }
+
+ protected function renderFilterChain(FilterChain $filter, $level)
+ {
+ $html = '<span class="handle"> </span>'
+ . $this->selectOperator($filter)
+ . $this->removeLink($filter)
+ . ($filter->count() === 1 ? $this->stripLink($filter) : '')
+ . $this->addLink($filter);
+
+ if ($filter->isEmpty() && ! $this->addTo) {
+ return $html;
+ }
+
+ $parts = array();
+ foreach ($filter->filters() as $f) {
+ $parts[] = '<li>' . $this->renderFilter($f, $level + 1) . '</li>';
+ }
+
+ if ($this->addTo && $this->addTo == $filter->getId()) {
+ $parts[] = '<li class="new-filter">' . $this->renderNewFilter() .$this->cancelLink(). '</li>';
+ }
+
+ $class = $level === 0 ? ' class="datafilter"' : '';
+ $html .= sprintf(
+ "<ul%s>\n%s</ul>\n",
+ $class,
+ implode("", $parts)
+ );
+ return $html;
+ }
+
+ protected function renderFilterExpression(FilterExpression $filter)
+ {
+ if ($this->addTo && $this->addTo === $filter->getId()) {
+ return
+ preg_replace(
+ '/ class="autosubmit"/',
+ ' class="autofocus"',
+ $this->selectOperator()
+ )
+ . '<ul><li>'
+ . $this->selectColumn($filter)
+ . $this->selectSign($filter)
+ . $this->text($filter)
+ . $this->removeLink($filter)
+ . $this->addLink($filter)
+ . '</li><li class="active">'
+ . $this->renderNewFilter() .$this->cancelLink()
+ . '</li></ul>'
+ ;
+ } else {
+ return $this->selectColumn($filter)
+ . $this->selectSign($filter)
+ . $this->text($filter)
+ . $this->removeLink($filter)
+ . $this->addLink($filter)
+ ;
+ }
+ }
+
+ protected function text(Filter $filter = null)
+ {
+ $value = $filter === null ? '' : $filter->getExpression();
+ if (is_array($value)) {
+ $value = '(' . implode('|', $value) . ')';
+ }
+ return sprintf(
+ '<input type="text" name="%s" value="%s" />',
+ $this->elementId('value', $filter),
+ $this->view()->escape($value)
+ );
+ }
+
+ protected function renderNewFilter()
+ {
+ $html = $this->selectColumn()
+ . $this->selectSign()
+ . $this->text();
+
+ return preg_replace(
+ '/ class="autosubmit"/',
+ '',
+ $html
+ );
+ }
+
+ protected function arrayForSelect($array, $flip = false)
+ {
+ $res = array();
+ foreach ($array as $k => $v) {
+ if (is_int($k)) {
+ $res[$v] = ucwords(str_replace('_', ' ', $v));
+ } elseif ($flip) {
+ $res[$v] = $k;
+ } else {
+ $res[$k] = $v;
+ }
+ }
+ // sort($res);
+ return $res;
+ }
+
+ protected function elementId($prefix, Filter $filter = null)
+ {
+ if ($filter === null) {
+ return $prefix . '_new_' . ($this->addTo ?: '0');
+ } else {
+ return $prefix . '_' . $filter->getId();
+ }
+ }
+
+ protected function selectOperator(Filter $filter = null)
+ {
+ $ops = array(
+ 'AND' => 'AND',
+ 'OR' => 'OR',
+ 'NOT' => 'NOT'
+ );
+
+ return $this->select(
+ $this->elementId('operator', $filter),
+ $ops,
+ $filter === null ? null : $filter->getOperatorName(),
+ ['class' => 'filter-operator']
+ );
+ }
+
+ protected function selectSign(Filter $filter = null)
+ {
+ $signs = array(
+ '=' => '=',
+ '!=' => '!=',
+ '>' => '>',
+ '<' => '<',
+ '>=' => '>=',
+ '<=' => '<=',
+ );
+
+ return $this->select(
+ $this->elementId('sign', $filter),
+ $signs,
+ $filter === null ? null : $filter->getSign(),
+ ['class' => 'filter-rule']
+ );
+ }
+
+ public function setColumns(array $columns = null)
+ {
+ $this->cachedColumnSelect = $columns ? $this->arrayForSelect($columns) : null;
+ return $this;
+ }
+
+ protected function selectColumn(Filter $filter = null)
+ {
+ $active = $filter === null ? null : $filter->getColumn();
+
+ if ($this->cachedColumnSelect === null && $this->query === null) {
+ return sprintf(
+ '<input type="text" name="%s" value="%s" />',
+ $this->elementId('column', $filter),
+ $this->view()->escape($active) // Escape attribute?
+ );
+ }
+
+ if ($this->cachedColumnSelect === null && $this->query instanceof FilterColumns) {
+ $this->cachedColumnSelect = $this->arrayForSelect($this->query->getFilterColumns(), true);
+ asort($this->cachedColumnSelect);
+ } elseif ($this->cachedColumnSelect === null) {
+ throw new ProgrammingError('No columns set nor does the query provide any');
+ }
+
+ $cols = $this->cachedColumnSelect;
+ if ($active && !isset($cols[$active])) {
+ $cols[$active] = str_replace('_', ' ', ucfirst(ltrim($active, '_')));
+ }
+
+ return $this->select($this->elementId('column', $filter), $cols, $active);
+ }
+
+ protected function applyChanges($changes)
+ {
+ $filter = $this->filter;
+ $pairs = array();
+ $addTo = null;
+ $add = array();
+ foreach ($changes as $k => $v) {
+ if (preg_match('/^(column|value|sign|operator)((?:_new)?)_([\d-]+)$/', $k, $m)) {
+ if ($m[2] === '_new') {
+ if ($addTo !== null && $addTo !== $m[3]) {
+ throw new \Exception('F...U');
+ }
+ $addTo = $m[3];
+ $add[$m[1]] = $v;
+ } else {
+ $pairs[$m[3]][$m[1]] = $v;
+ }
+ }
+ }
+
+ $operators = array();
+ foreach ($pairs as $id => $fs) {
+ if (array_key_exists('operator', $fs)) {
+ $operators[$id] = $fs['operator'];
+ } else {
+ $f = $filter->getById($id);
+ $f->setColumn($fs['column']);
+ if ($f->getSign() !== $fs['sign']) {
+ if ($f->isRootNode()) {
+ $filter = $f->setSign($fs['sign']);
+ } else {
+ $filter->replaceById($id, $f->setSign($fs['sign']));
+ }
+ }
+ $f->setExpression($fs['value']);
+ }
+ }
+
+ krsort($operators, SORT_NATURAL);
+ foreach ($operators as $id => $operator) {
+ $f = $filter->getById($id);
+ if ($f->getOperatorName() !== $operator) {
+ if ($f->isRootNode()) {
+ $filter = $f->setOperatorName($operator);
+ } else {
+ $filter->replaceById($id, $f->setOperatorName($operator));
+ }
+ }
+ }
+
+ if ($addTo !== null) {
+ if ($addTo === '0') {
+ $filter = Filter::expression($add['column'], $add['sign'], $add['value']);
+ } else {
+ $parent = $filter->getById($addTo);
+ $f = Filter::expression($add['column'], $add['sign'], $add['value']);
+ if (isset($add['operator'])) {
+ switch ($add['operator']) {
+ case 'AND':
+ if ($parent->isExpression()) {
+ if ($parent->isRootNode()) {
+ $filter = Filter::matchAll(clone $parent, $f);
+ } else {
+ $filter = $filter->replaceById($addTo, Filter::matchAll(clone $parent, $f));
+ }
+ } else {
+ $parent->addFilter(Filter::matchAll($f));
+ }
+ break;
+ case 'OR':
+ if ($parent->isExpression()) {
+ if ($parent->isRootNode()) {
+ $filter = Filter::matchAny(clone $parent, $f);
+ } else {
+ $filter = $filter->replaceById($addTo, Filter::matchAny(clone $parent, $f));
+ }
+ } else {
+ $parent->addFilter(Filter::matchAny($f));
+ }
+ break;
+ case 'NOT':
+ if ($parent->isExpression()) {
+ if ($parent->isRootNode()) {
+ $filter = Filter::not(Filter::matchAll($parent, $f));
+ } else {
+ $filter = $filter->replaceById($addTo, Filter::not(Filter::matchAll($parent, $f)));
+ }
+ } else {
+ $parent->addFilter(Filter::not($f));
+ }
+ break;
+ }
+ } else {
+ $parent->addFilter($f);
+ }
+ }
+ }
+
+ return $filter;
+ }
+
+ public function renderSearch()
+ {
+ $preservedUrl = $this->preservedUrl();
+
+ $html = ' <form method="post" class="search inline" action="'
+ . $preservedUrl
+ . '"><input type="text" name="q" class="search search-input" value="" placeholder="'
+ . t('Search...')
+ . '" /></form>';
+
+ if ($this->filter->isEmpty()) {
+ $title = t('Filter this list');
+ } else {
+ $title = t('Modify this filter');
+ if (! $this->filter->isEmpty()) {
+ $title .= ': ' . $this->view()->escape($this->filter);
+ }
+ }
+
+ return $html
+ . '<a href="'
+ . $preservedUrl->with('modifyFilter', ! $preservedUrl->getParam('modifyFilter'))
+ . '" aria-label="'
+ . $title
+ . '" title="'
+ . $title
+ . '">'
+ . '<i aria-hidden="true" class="icon-filter"></i>'
+ . '</a>';
+ }
+
+ public function render()
+ {
+ if (! $this->visible) {
+ return '';
+ }
+ if (! $this->preservedUrl()->getParam('modifyFilter')) {
+ return '<div class="filter icinga-controls">'
+ . $this->renderSearch()
+ . $this->view()->escape($this->shorten($this->filter, 50))
+ . '</div>';
+ }
+ return '<div class="filter icinga-controls">'
+ . $this->renderSearch()
+ . '<form action="'
+ . Url::fromRequest()
+ . '" class="editor" method="POST">'
+ . '<input type="submit" name="submit" value="Apply" hidden/>'
+ . '<ul class="tree"><li>'
+ . $this->renderFilter($this->filter)
+ . '</li></ul>'
+ . '<div class="buttons">'
+ . '<input type="submit" name="cancel" value="Cancel" class="button btn-cancel" />'
+ . '<input type="submit" name="submit" value="Apply" class="button btn-primary"/>'
+ . '</div>'
+ . '<input type="hidden" name="formUID" value="FilterEditor">'
+ . '</form>'
+ . '</div>';
+ }
+
+ protected function shorten($string, $length)
+ {
+ if (strlen($string) > $length) {
+ return substr($string, 0, $length) . '...';
+ }
+ return $string;
+ }
+
+ public function __toString()
+ {
+ try {
+ return $this->render();
+ } catch (Exception $e) {
+ return 'ERROR in FilterEditor: ' . $e->getMessage();
+ }
+ }
+}
diff --git a/library/Icinga/Web/Widget/ItemList/MigrationFileListItem.php b/library/Icinga/Web/Widget/ItemList/MigrationFileListItem.php
new file mode 100644
index 0000000..007a730
--- /dev/null
+++ b/library/Icinga/Web/Widget/ItemList/MigrationFileListItem.php
@@ -0,0 +1,92 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\Widget\ItemList;
+
+use Icinga\Application\Hook\Common\DbMigrationStep;
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Html\HtmlElement;
+use ipl\Html\HtmlString;
+use ipl\Html\Text;
+use ipl\I18n\Translation;
+use ipl\Web\Common\BaseListItem;
+use ipl\Web\Widget\EmptyState;
+use ipl\Web\Widget\Icon;
+
+class MigrationFileListItem extends BaseListItem
+{
+ use Translation;
+
+ /** @var DbMigrationStep Just for type hint */
+ protected $item;
+
+ protected function assembleVisual(BaseHtmlElement $visual): void
+ {
+ if ($this->item->getLastState()) {
+ $visual->getAttributes()->add('class', 'upgrade-failed');
+ $visual->addHtml(new Icon('circle-xmark'));
+ }
+ }
+
+ protected function assembleTitle(BaseHtmlElement $title): void
+ {
+ $scriptPath = $this->item->getScriptPath();
+ /** @var string $parentDirs */
+ $parentDirs = substr($scriptPath, (int) strpos($scriptPath, 'schema'));
+ $parentDirs = substr($parentDirs, 0, strrpos($parentDirs, '/') + 1);
+
+ $title->addHtml(
+ new HtmlElement('span', null, Text::create($parentDirs)),
+ new HtmlElement(
+ 'span',
+ Attributes::create(['class' => 'version']),
+ Text::create($this->item->getVersion() . '.sql')
+ )
+ );
+
+ if ($this->item->getLastState()) {
+ $title->addHtml(
+ new HtmlElement(
+ 'span',
+ Attributes::create(['class' => 'upgrade-failed']),
+ Text::create($this->translate('Upgrade failed'))
+ )
+ );
+ }
+ }
+
+ protected function assembleHeader(BaseHtmlElement $header): void
+ {
+ $header->addHtml($this->createTitle());
+ }
+
+ protected function assembleCaption(BaseHtmlElement $caption): void
+ {
+ if ($this->item->getDescription()) {
+ $caption->addHtml(Text::create($this->item->getDescription()));
+ } else {
+ $caption->addHtml(new EmptyState(Text::create($this->translate('No description provided.'))));
+ }
+ }
+
+ protected function assembleFooter(BaseHtmlElement $footer): void
+ {
+ if ($this->item->getLastState()) {
+ $footer->addHtml(
+ new HtmlElement(
+ 'section',
+ Attributes::create(['class' => 'caption']),
+ new HtmlElement('pre', null, new HtmlString(Html::escape($this->item->getLastState())))
+ )
+ );
+ }
+ }
+
+ protected function assembleMain(BaseHtmlElement $main): void
+ {
+ $main->addHtml($this->createHeader(), $this->createCaption());
+ }
+}
diff --git a/library/Icinga/Web/Widget/ItemList/MigrationList.php b/library/Icinga/Web/Widget/ItemList/MigrationList.php
new file mode 100644
index 0000000..43699d3
--- /dev/null
+++ b/library/Icinga/Web/Widget/ItemList/MigrationList.php
@@ -0,0 +1,133 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\Widget\ItemList;
+
+use Generator;
+use Icinga\Application\Hook\Common\DbMigrationStep;
+use Icinga\Application\Hook\DbMigrationHook;
+use Icinga\Application\MigrationManager;
+use Icinga\Forms\MigrationForm;
+use ipl\I18n\Translation;
+use ipl\Web\Common\BaseItemList;
+use ipl\Web\Widget\EmptyStateBar;
+
+class MigrationList extends BaseItemList
+{
+ use Translation;
+
+ protected $baseAttributes = ['class' => 'item-list'];
+
+ /** @var Generator<DbMigrationHook> */
+ protected $data;
+
+ /** @var ?MigrationForm */
+ protected $migrationForm;
+
+ /** @var bool Whether to render minimal migration list items */
+ protected $minimal = true;
+
+ /**
+ * Create a new migration list
+ *
+ * @param Generator<DbMigrationHook>|array<DbMigrationStep|DbMigrationHook> $data
+ *
+ * @param ?MigrationForm $form
+ */
+ public function __construct($data, MigrationForm $form = null)
+ {
+ parent::__construct($data);
+
+ $this->migrationForm = $form;
+ }
+
+ /**
+ * Set whether to render minimal migration list items
+ *
+ * @param bool $minimal
+ *
+ * @return $this
+ */
+ public function setMinimal(bool $minimal): self
+ {
+ $this->minimal = $minimal;
+
+ return $this;
+ }
+
+ /**
+ * Get whether to render minimal migration list items
+ *
+ * @return bool
+ */
+ public function isMinimal(): bool
+ {
+ return $this->minimal;
+ }
+
+ protected function getItemClass(): string
+ {
+ if ($this->isMinimal()) {
+ return MigrationListItem::class;
+ }
+
+ return MigrationFileListItem::class;
+ }
+
+ protected function assemble(): void
+ {
+ $itemClass = $this->getItemClass();
+ if (! $this->isMinimal()) {
+ $this->getAttributes()->add('class', 'file-list');
+ }
+
+ /** @var DbMigrationHook $data */
+ foreach ($this->data as $data) {
+ /** @var MigrationFileListItem|MigrationListItem $item */
+ $item = new $itemClass($data, $this);
+ if ($item instanceof MigrationListItem && $this->migrationForm) {
+ $migrateButton = $this->migrationForm->createElement(
+ 'submit',
+ sprintf('migrate-%s', $data->getModuleName()),
+ [
+ 'required' => false,
+ 'label' => $this->translate('Migrate'),
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'Migrate %d pending migration',
+ 'Migrate all %d pending migrations',
+ $data->count()
+ ),
+ $data->count()
+ )
+ ]
+ );
+
+ $mm = MigrationManager::instance();
+ if ($data->isModule() && $mm->hasMigrations(DbMigrationHook::DEFAULT_MODULE)) {
+ $migrateButton->getAttributes()
+ ->set('disabled', true)
+ ->set(
+ 'title',
+ $this->translate(
+ 'Please apply all the pending migrations of Icinga Web first or use the apply all'
+ . ' button instead.'
+ )
+ );
+ }
+
+ $this->migrationForm->registerElement($migrateButton);
+
+ $item->setMigrateButton($migrateButton);
+ }
+
+ $this->addHtml($item);
+ }
+
+ if ($this->isEmpty()) {
+ $this->setTag('div');
+ $this->addHtml(new EmptyStateBar(t('No items found.')));
+ }
+ }
+}
diff --git a/library/Icinga/Web/Widget/ItemList/MigrationListItem.php b/library/Icinga/Web/Widget/ItemList/MigrationListItem.php
new file mode 100644
index 0000000..284ce4c
--- /dev/null
+++ b/library/Icinga/Web/Widget/ItemList/MigrationListItem.php
@@ -0,0 +1,151 @@
+<?php
+
+/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\Widget\ItemList;
+
+use Icinga\Application\Hook\Common\DbMigrationStep;
+use Icinga\Application\Hook\DbMigrationHook;
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Contract\FormElement;
+use ipl\Html\FormattedString;
+use ipl\Html\Html;
+use ipl\Html\HtmlElement;
+use ipl\Html\HtmlString;
+use ipl\Html\Text;
+use ipl\I18n\Translation;
+use ipl\Web\Common\BaseListItem;
+use ipl\Web\Url;
+use ipl\Web\Widget\EmptyState;
+use ipl\Web\Widget\Icon;
+use ipl\Web\Widget\Link;
+use LogicException;
+
+class MigrationListItem extends BaseListItem
+{
+ use Translation;
+
+ /** @var ?FormElement */
+ protected $migrateButton;
+
+ /** @var DbMigrationHook Just for type hint */
+ protected $item;
+
+ /**
+ * Set a migration form of this list item
+ *
+ * @param FormElement $migrateButton
+ *
+ * @return $this
+ */
+ public function setMigrateButton(FormElement $migrateButton): self
+ {
+ $this->migrateButton = $migrateButton;
+
+ return $this;
+ }
+
+ protected function assembleTitle(BaseHtmlElement $title): void
+ {
+ $title->addHtml(
+ FormattedString::create(
+ t('%s ', '<name>'),
+ HtmlElement::create('span', ['class' => 'subject'], $this->item->getName())
+ )
+ );
+ }
+
+ protected function assembleHeader(BaseHtmlElement $header): void
+ {
+ if ($this->migrateButton === null) {
+ throw new LogicException('Please set the migrate submit button beforehand');
+ }
+
+ $header->addHtml($this->createTitle());
+ $header->addHtml($this->migrateButton);
+ }
+
+ protected function assembleCaption(BaseHtmlElement $caption): void
+ {
+ $migrations = $this->item->getMigrations();
+ /** @var DbMigrationStep $migration */
+ $migration = array_shift($migrations);
+ if ($migration->getLastState()) {
+ if ($migration->getDescription()) {
+ $caption->addHtml(Text::create($migration->getDescription()));
+ } else {
+ $caption->addHtml(new EmptyState(Text::create($this->translate('No description provided.'))));
+ }
+
+ $scriptPath = $migration->getScriptPath();
+ /** @var string $parentDirs */
+ $parentDirs = substr($scriptPath, (int) strpos($scriptPath, 'schema'));
+ $parentDirs = substr($parentDirs, 0, strrpos($parentDirs, '/') + 1);
+
+ $title = new HtmlElement('div', Attributes::create(['class' => 'title']));
+ $title->addHtml(
+ new HtmlElement('span', null, Text::create($parentDirs)),
+ new HtmlElement(
+ 'span',
+ Attributes::create(['class' => 'version']),
+ Text::create($migration->getVersion() . '.sql')
+ ),
+ new HtmlElement(
+ 'span',
+ Attributes::create(['class' => 'upgrade-failed']),
+ Text::create($this->translate('Upgrade failed'))
+ )
+ );
+
+ $error = new HtmlElement('div', Attributes::create([
+ 'class' => 'collapsible',
+ 'data-visible-height' => '58',
+ ]));
+ $error->addHtml(new HtmlElement('pre', null, new HtmlString(Html::escape($migration->getLastState()))));
+
+ $errorSection = new HtmlElement('div', Attributes::create(['class' => 'errors-section',]));
+ $errorSection->addHtml(
+ new HtmlElement('header', null, new Icon('circle-xmark', ['class' => 'status-icon']), $title),
+ $caption,
+ $error
+ );
+
+ $caption->prependWrapper($errorSection);
+ }
+ }
+
+ protected function assembleFooter(BaseHtmlElement $footer): void
+ {
+ $footer->addHtml((new MigrationList($this->item->getLatestMigrations(3)))->setMinimal(false));
+ if ($this->item->count() > 3) {
+ $footer->addHtml(
+ new Link(
+ sprintf($this->translate('Show all %d migrations'), $this->item->count()),
+ Url::fromPath(
+ 'migrations/migration',
+ [DbMigrationHook::MIGRATION_PARAM => $this->item->getModuleName()]
+ ),
+ [
+ 'data-base-target' => '_next',
+ 'class' => 'show-more'
+ ]
+ )
+ );
+ }
+ }
+
+ protected function assembleMain(BaseHtmlElement $main): void
+ {
+ $main->addHtml($this->createHeader());
+ $caption = $this->createCaption();
+ if (! $caption->isEmpty()) {
+ $main->addHtml($caption);
+ }
+
+ $footer = $this->createFooter();
+ if ($footer) {
+ $main->addHtml($footer);
+ }
+ }
+}
diff --git a/library/Icinga/Web/Widget/Limiter.php b/library/Icinga/Web/Widget/Limiter.php
new file mode 100644
index 0000000..d127aca
--- /dev/null
+++ b/library/Icinga/Web/Widget/Limiter.php
@@ -0,0 +1,54 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget;
+
+use Icinga\Forms\Control\LimiterControlForm;
+
+/**
+ * Limiter control widget
+ */
+class Limiter extends AbstractWidget
+{
+ /**
+ * Default limit for this instance
+ *
+ * @var int|null
+ */
+ protected $defaultLimit;
+
+ /**
+ * Get the default limit
+ *
+ * @return int|null
+ */
+ public function getDefaultLimit()
+ {
+ return $this->defaultLimit;
+ }
+
+ /**
+ * Set the default limit
+ *
+ * @param int $defaultLimit
+ *
+ * @return $this
+ */
+ public function setDefaultLimit($defaultLimit)
+ {
+ $this->defaultLimit = (int) $defaultLimit;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render()
+ {
+ $control = new LimiterControlForm();
+ $control
+ ->setDefaultLimit($this->defaultLimit)
+ ->handleRequest();
+ return (string)$control;
+ }
+}
diff --git a/library/Icinga/Web/Widget/Paginator.php b/library/Icinga/Web/Widget/Paginator.php
new file mode 100644
index 0000000..5f3ef04
--- /dev/null
+++ b/library/Icinga/Web/Widget/Paginator.php
@@ -0,0 +1,167 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget;
+
+use Icinga\Data\Paginatable;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Paginator
+ */
+class Paginator extends AbstractWidget
+{
+ /**
+ * The query the paginator widget is created for
+ *
+ * @var Paginatable
+ */
+ protected $query;
+
+ /**
+ * The view script in use
+ *
+ * @var string|array
+ */
+ protected $viewScript = array('mixedPagination.phtml', 'default');
+
+ /**
+ * Set the query to create the paginator widget for
+ *
+ * @param Paginatable $query
+ *
+ * @return $this
+ */
+ public function setQuery(Paginatable $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Set the view script to use
+ *
+ * @param string|array $script
+ *
+ * @return $this
+ */
+ public function setViewScript($script)
+ {
+ $this->viewScript = $script;
+ return $this;
+ }
+
+ /**
+ * Render this paginator
+ */
+ public function render()
+ {
+ if ($this->query === null) {
+ throw new ProgrammingError('Need a query to create the paginator widget for');
+ }
+
+ $itemCountPerPage = $this->query->getLimit();
+ if (! $itemCountPerPage) {
+ return ''; // No pagination required
+ }
+
+ $totalItemCount = count($this->query);
+ $pageCount = (int) ceil($totalItemCount / $itemCountPerPage);
+ $currentPage = $this->query->hasOffset() ? ($this->query->getOffset() / $itemCountPerPage) + 1 : 1;
+ $pagesInRange = $this->getPages($pageCount, $currentPage);
+ $variables = array(
+ 'totalItemCount' => $totalItemCount,
+ 'pageCount' => $pageCount,
+ 'itemCountPerPage' => $itemCountPerPage,
+ 'first' => 1,
+ 'current' => $currentPage,
+ 'last' => $pageCount,
+ 'pagesInRange' => $pagesInRange,
+ 'firstPageInRange' => min($pagesInRange),
+ 'lastPageInRange' => max($pagesInRange)
+ );
+
+ if ($currentPage > 1) {
+ $variables['previous'] = $currentPage - 1;
+ }
+
+ if ($currentPage < $pageCount) {
+ $variables['next'] = $currentPage + 1;
+ }
+
+ if (is_array($this->viewScript)) {
+ if ($this->viewScript[1] !== null) {
+ return $this->view()->partial($this->viewScript[0], $this->viewScript[1], $variables);
+ }
+
+ return $this->view()->partial($this->viewScript[0], $variables);
+ }
+
+ return $this->view()->partial($this->viewScript, $variables);
+ }
+
+ /**
+ * Returns an array of "local" pages given the page count and current page number
+ *
+ * @return array
+ */
+ protected function getPages($pageCount, $currentPage)
+ {
+ $range = array();
+
+ if ($pageCount < 10) {
+ // Show all pages if we have less than 10
+ for ($i = 1; $i < 10; $i++) {
+ if ($i > $pageCount) {
+ break;
+ }
+
+ $range[$i] = $i;
+ }
+ } else {
+ // More than 10 pages:
+ foreach (array(1, 2) as $i) {
+ $range[$i] = $i;
+ }
+
+ if ($currentPage < 6) {
+ // We are on page 1-5 from
+ for ($i = 1; $i <= 7; $i++) {
+ $range[$i] = $i;
+ }
+ } else {
+ // Current page > 5
+ $range[] = '...';
+
+ if (($pageCount - $currentPage) < 5) {
+ // Less than 5 pages left
+ $start = 5 - ($pageCount - $currentPage);
+ } else {
+ $start = 1;
+ }
+
+ for ($i = $currentPage - $start; $i < ($currentPage + (4 - $start)); $i++) {
+ if ($i > $pageCount) {
+ break;
+ }
+
+ $range[$i] = $i;
+ }
+ }
+
+ if ($currentPage < ($pageCount - 2)) {
+ $range[] = '...';
+ }
+
+ foreach (array($pageCount - 1, $pageCount) as $i) {
+ $range[$i] = $i;
+ }
+ }
+
+ if (empty($range)) {
+ $range[] = 1;
+ }
+
+ return $range;
+ }
+}
diff --git a/library/Icinga/Web/Widget/SearchDashboard.php b/library/Icinga/Web/Widget/SearchDashboard.php
new file mode 100644
index 0000000..1ce4c46
--- /dev/null
+++ b/library/Icinga/Web/Widget/SearchDashboard.php
@@ -0,0 +1,111 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget;
+
+use Zend_Controller_Action_Exception;
+use Icinga\Application\Icinga;
+use Icinga\Web\Url;
+
+/**
+ * Class SearchDashboard display multiple search views on a single search page
+ */
+class SearchDashboard extends Dashboard
+{
+ /**
+ * Name for the search pane
+ *
+ * @var string
+ */
+ const SEARCH_PANE = 'search';
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTabs()
+ {
+ if ($this->tabs === null) {
+ $this->tabs = new Tabs();
+ $this->tabs->add(
+ 'search',
+ array(
+ 'title' => t('Show Search', 'dashboard.pane.tooltip'),
+ 'label' => t('Search'),
+ 'url' => Url::fromRequest()
+ )
+ );
+ }
+ return $this->tabs;
+ }
+
+ /**
+ * Load all available search dashlets from modules
+ *
+ * @param string $searchString
+ *
+ * @return $this
+ */
+ public function search($searchString = '')
+ {
+ $pane = $this->createPane(self::SEARCH_PANE)->getPane(self::SEARCH_PANE)->setTitle(t('Search'));
+ $this->activate(self::SEARCH_PANE);
+
+ $manager = Icinga::app()->getModuleManager();
+ $searchUrls = array();
+
+ foreach ($manager->getLoadedModules() as $module) {
+ if ($this->getUser()->can($manager::MODULE_PERMISSION_NS . $module->getName())) {
+ $moduleSearchUrls = $module->getSearchUrls();
+ if (! empty($moduleSearchUrls)) {
+ if ($searchString === '') {
+ $pane->add(t('Ready to search'), 'search/hint');
+ return $this;
+ }
+ $searchUrls = array_merge($searchUrls, $moduleSearchUrls);
+ }
+ }
+ }
+
+ usort($searchUrls, array($this, 'compareSearchUrls'));
+
+ foreach (array_reverse($searchUrls) as $searchUrl) {
+ $pane->createDashlet(
+ $searchUrl->title . ': ' . $searchString,
+ Url::fromPath($searchUrl->url, array('q' => $searchString))
+ )->setProgressLabel(t('Searching'));
+ }
+
+ return $this;
+ }
+
+ /**
+ * Renders the output
+ *
+ * @return string
+ *
+ * @throws Zend_Controller_Action_Exception
+ */
+ public function render()
+ {
+ if (! $this->getPane(self::SEARCH_PANE)->hasDashlets()) {
+ throw new Zend_Controller_Action_Exception(t('Page not found'), 404);
+ }
+ return parent::render();
+ }
+
+ /**
+ * Compare search URLs based on their priority
+ *
+ * @param object $a
+ * @param object $b
+ *
+ * @return int
+ */
+ private function compareSearchUrls($a, $b)
+ {
+ if ($a->priority === $b->priority) {
+ return 0;
+ }
+ return ($a->priority < $b->priority) ? -1 : 1;
+ }
+}
diff --git a/library/Icinga/Web/Widget/SingleValueSearchControl.php b/library/Icinga/Web/Widget/SingleValueSearchControl.php
new file mode 100644
index 0000000..470518c
--- /dev/null
+++ b/library/Icinga/Web/Widget/SingleValueSearchControl.php
@@ -0,0 +1,200 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Web\Widget;
+
+use Icinga\Application\Icinga;
+use ipl\Html\Attributes;
+use ipl\Html\Form;
+use ipl\Html\FormElement\InputElement;
+use ipl\Html\HtmlElement;
+use ipl\Web\Control\SearchBar\Suggestions;
+use ipl\Web\Url;
+
+class SingleValueSearchControl extends Form
+{
+ /** @var string */
+ const DEFAULT_SEARCH_PARAMETER = 'q';
+
+ protected $defaultAttributes = ['class' => 'icinga-controls inline'];
+
+ /** @var string */
+ protected $searchParameter = self::DEFAULT_SEARCH_PARAMETER;
+
+ /** @var string */
+ protected $inputLabel;
+
+ /** @var string */
+ protected $submitLabel;
+
+ /** @var Url */
+ protected $suggestionUrl;
+
+ /** @var array */
+ protected $metaDataNames;
+
+ /**
+ * Set the search parameter to use
+ *
+ * @param string $name
+ * @return $this
+ */
+ public function setSearchParameter($name)
+ {
+ $this->searchParameter = $name;
+
+ return $this;
+ }
+
+ /**
+ * Set the input's label
+ *
+ * @param string $label
+ *
+ * @return $this
+ */
+ public function setInputLabel($label)
+ {
+ $this->inputLabel = $label;
+
+ return $this;
+ }
+
+ /**
+ * Set the submit button's label
+ *
+ * @param string $label
+ *
+ * @return $this
+ */
+ public function setSubmitLabel($label)
+ {
+ $this->submitLabel = $label;
+
+ return $this;
+ }
+
+ /**
+ * Set the suggestion url
+ *
+ * @param Url $url
+ *
+ * @return $this
+ */
+ public function setSuggestionUrl(Url $url)
+ {
+ $this->suggestionUrl = $url;
+
+ return $this;
+ }
+
+ /**
+ * Set names for which hidden meta data elements should be created
+ *
+ * @param string ...$names
+ *
+ * @return $this
+ */
+ public function setMetaDataNames(...$names)
+ {
+ $this->metaDataNames = $names;
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ $suggestionsId = Icinga::app()->getRequest()->protectId('single-value-suggestions');
+
+ $this->addElement(
+ 'text',
+ $this->searchParameter,
+ [
+ 'required' => true,
+ 'minlength' => 1,
+ 'autocomplete' => 'off',
+ 'class' => 'search',
+ 'data-enrichment-type' => 'completion',
+ 'data-term-suggestions' => '#' . $suggestionsId,
+ 'data-suggest-url' => $this->suggestionUrl,
+ 'placeholder' => $this->inputLabel
+ ]
+ );
+
+ if (! empty($this->metaDataNames)) {
+ $fieldset = new HtmlElement('fieldset');
+ foreach ($this->metaDataNames as $name) {
+ $hiddenElement = $this->createElement('hidden', $this->searchParameter . '-' . $name);
+ $this->registerElement($hiddenElement);
+ $fieldset->addHtml($hiddenElement);
+ }
+
+ $this->getElement($this->searchParameter)->prependWrapper($fieldset);
+ }
+
+ $this->addElement(
+ 'submit',
+ 'btn_sumit',
+ [
+ 'label' => $this->submitLabel,
+ 'class' => 'btn-primary'
+ ]
+ );
+
+ $this->add(HtmlElement::create('div', [
+ 'id' => $suggestionsId,
+ 'class' => 'search-suggestions'
+ ]));
+ }
+
+ /**
+ * Create a list of search suggestions based on the given groups
+ *
+ * @param array $groups
+ *
+ * @return HtmlElement
+ */
+ public static function createSuggestions(array $groups)
+ {
+ $ul = new HtmlElement('ul');
+ foreach ($groups as list($name, $entries)) {
+ if ($name) {
+ if ($entries === false) {
+ $ul->addHtml(HtmlElement::create('li', ['class' => 'failure-message'], [
+ HtmlElement::create('em', null, t('Can\'t search:')),
+ $name
+ ]));
+ continue;
+ } elseif (empty($entries)) {
+ $ul->addHtml(HtmlElement::create('li', ['class' => 'failure-message'], [
+ HtmlElement::create('em', null, t('No results:')),
+ $name
+ ]));
+ continue;
+ } else {
+ $ul->addHtml(
+ HtmlElement::create('li', ['class' => Suggestions::SUGGESTION_TITLE_CLASS], $name)
+ );
+ }
+ }
+
+ $index = 0;
+ foreach ($entries as list($label, $metaData)) {
+ $attributes = [
+ 'value' => $label,
+ 'type' => 'button',
+ 'tabindex' => -1
+ ];
+ foreach ($metaData as $key => $value) {
+ $attributes['data-' . $key] = $value;
+ }
+
+ $liAtrs = ['class' => $index === 0 ? 'default' : null];
+ $ul->addHtml(new HtmlElement('li', Attributes::create($liAtrs), new InputElement(null, $attributes)));
+ $index++;
+ }
+ }
+
+ return $ul;
+ }
+}
diff --git a/library/Icinga/Web/Widget/SortBox.php b/library/Icinga/Web/Widget/SortBox.php
new file mode 100644
index 0000000..72b6f58
--- /dev/null
+++ b/library/Icinga/Web/Widget/SortBox.php
@@ -0,0 +1,260 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget;
+
+use Icinga\Application\Icinga;
+use Icinga\Data\Sortable;
+use Icinga\Data\SortRules;
+use Icinga\Web\Form;
+use Icinga\Web\Request;
+
+/**
+ * SortBox widget
+ *
+ * The "SortBox" Widget allows you to create a generic sort input for sortable views. It automatically creates a select
+ * box with all sort options and a dropbox with the sort direction. It also handles automatic submission of sorting
+ * changes and draws an additional submit button when JavaScript is disabled.
+ *
+ * The constructor takes a string for the component name and an array containing the select options, where the key is
+ * the value to be submitted and the value is the label that will be shown. You then should call setRequest in order
+ * to make sure the form is correctly populated when a request with a sort parameter is being made.
+ *
+ * Call setQuery in case you'll do not want to handle URL parameters manually, but to automatically apply the user's
+ * chosen sort rules on the given sortable query. This will also allow the SortBox to display the user the correct
+ * default sort rules if the given query provides already some sort rules.
+ */
+class SortBox extends AbstractWidget
+{
+ /**
+ * An array containing all sort columns with their associated labels
+ *
+ * @var array
+ */
+ protected $sortFields;
+
+ /**
+ * An array containing default sort directions for specific columns
+ *
+ * The first entry will be used as default sort column.
+ *
+ * @var array
+ */
+ protected $sortDefaults;
+
+ /**
+ * The name used to uniquely identfy the forms being created
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * The request to fetch sort rules from
+ *
+ * @var Request
+ */
+ protected $request;
+
+ /**
+ * The query to apply sort rules on
+ *
+ * @var Sortable
+ */
+ protected $query;
+
+ /**
+ * Create a SortBox with the entries from $sortFields
+ *
+ * @param string $name The name for the SortBox
+ * @param array $sortFields An array containing the columns and their labels to be displayed in the SortBox
+ * @param array $sortDefaults An array containing default sort directions for specific columns
+ */
+ public function __construct($name, array $sortFields, array $sortDefaults = null)
+ {
+ $this->name = $name;
+ $this->sortFields = $sortFields;
+ $this->sortDefaults = $sortDefaults;
+ }
+
+ /**
+ * Create a SortBox
+ *
+ * @param string $name The name for the SortBox
+ * @param array $sortFields An array containing the columns and their labels to be displayed in the SortBox
+ * @param array $sortDefaults An array containing default sort directions for specific columns
+ *
+ * @return SortBox
+ */
+ public static function create($name, array $sortFields, array $sortDefaults = null)
+ {
+ return new static($name, $sortFields, $sortDefaults);
+ }
+
+ /**
+ * Set the request to fetch sort rules from
+ *
+ * @param Request $request
+ *
+ * @return $this
+ */
+ public function setRequest($request)
+ {
+ $this->request = $request;
+ return $this;
+ }
+
+ /**
+ * Set the query to apply sort rules on
+ *
+ * @param Sortable $query
+ *
+ * @return $this
+ */
+ public function setQuery(Sortable $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Return the default sort rule for the query
+ *
+ * @param string $column An optional column
+ *
+ * @return array An array of two values: $column, $direction
+ */
+ protected function getSortDefaults($column = null)
+ {
+ $direction = null;
+ if (! empty($this->sortDefaults) && ($column === null || isset($this->sortDefaults[$column]))) {
+ if ($column === null) {
+ reset($this->sortDefaults);
+ $column = key($this->sortDefaults);
+ }
+
+ $direction = $this->sortDefaults[$column];
+ } elseif ($this->query !== null && $this->query instanceof SortRules) {
+ $sortRules = $this->query->getSortRules();
+ if ($column === null) {
+ $column = key($sortRules);
+ }
+
+ if ($column !== null && isset($sortRules[$column]['order'])) {
+ $direction = strtoupper($sortRules[$column]['order']) === Sortable::SORT_DESC ? 'desc' : 'asc';
+ }
+ } elseif ($column === null) {
+ reset($this->sortFields);
+ $column = key($this->sortFields);
+ }
+
+ return array($column, $direction);
+ }
+
+ /**
+ * Apply the sort rules from the given or current request on the query
+ *
+ * @param Request $request
+ *
+ * @return $this
+ */
+ public function handleRequest(Request $request = null)
+ {
+ if ($this->query !== null) {
+ if ($request === null) {
+ $request = Icinga::app()->getRequest();
+ }
+
+ if (! ($sort = $request->getParam('sort'))) {
+ list($sort, $dir) = $this->getSortDefaults();
+ } else {
+ list($_, $dir) = $this->getSortDefaults($sort);
+ }
+
+ $this->query->order($sort, $request->getParam('dir', $dir));
+ }
+
+ return $this;
+ }
+
+ /**
+ * Render this SortBox as HTML
+ *
+ * @return string
+ */
+ public function render()
+ {
+ $columnForm = new Form();
+ $columnForm->setTokenDisabled();
+ $columnForm->setName($this->name . '-column');
+ $columnForm->setAttrib('class', 'icinga-controls inline');
+ $columnForm->addElement(
+ 'select',
+ 'sort',
+ array(
+ 'autosubmit' => true,
+ 'label' => $this->view()->translate('Sort by'),
+ 'multiOptions' => $this->sortFields,
+ 'decorators' => array(
+ array('ViewHelper'),
+ array('Label')
+ )
+ )
+ );
+
+ $column = null;
+ if ($this->request) {
+ $url = $this->request->getUrl();
+ if ($url->hasParam('sort')) {
+ $column = $url->getParam('sort');
+
+ if ($url->hasParam('dir')) {
+ $direction = $url->getParam('dir');
+ } else {
+ list($_, $direction) = $this->getSortDefaults($column);
+ }
+ } elseif ($url->hasParam('dir')) {
+ $direction = $url->getParam('dir');
+ list($column, $_) = $this->getSortDefaults();
+ }
+ }
+
+ if ($column === null) {
+ list($column, $direction) = $this->getSortDefaults();
+ }
+
+ // TODO(el): ToggleButton :)
+ $toggle = array('asc' => 'sort-name-down', 'desc' => 'sort-name-up');
+ unset($toggle[isset($direction) ? strtolower($direction) : 'asc']);
+ $newDirection = key($toggle);
+ $icon = current($toggle);
+
+ $orderForm = new Form();
+ $orderForm->setTokenDisabled();
+ $orderForm->setName($this->name . '-order');
+ $orderForm->setAttrib('class', 'inline sort-direction-control');
+ $orderForm->addElement(
+ 'hidden',
+ 'dir'
+ );
+ $orderForm->addElement(
+ 'button',
+ 'btn_submit',
+ array(
+ 'ignore' => true,
+ 'type' => 'submit',
+ 'label' => $this->view()->icon($icon),
+ 'decorators' => array('ViewHelper'),
+ 'escape' => false,
+ 'class' => 'link-button spinner',
+ 'value' => 'submit',
+ 'title' => t('Change sort direction'),
+ )
+ );
+
+
+ $columnForm->populate(array('sort' => $column));
+ $orderForm->populate(array('dir' => $newDirection));
+ return '<div class="sort-control">' . $columnForm . $orderForm . '</div>';
+ }
+}
diff --git a/library/Icinga/Web/Widget/Tab.php b/library/Icinga/Web/Widget/Tab.php
new file mode 100644
index 0000000..a367f00
--- /dev/null
+++ b/library/Icinga/Web/Widget/Tab.php
@@ -0,0 +1,323 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget;
+
+use Icinga\Web\Url;
+
+/**
+ * A single tab, usually used through the tabs widget
+ *
+ * Will generate an &lt;li&gt; list item, with an optional link and icon
+ *
+ * @property string $name Tab identifier
+ * @property string $title Tab title
+ * @property string $icon Icon URL, preferrably relative to the Icinga
+ * base URL
+ * @property string|URL $url Action URL, preferrably relative to the Icinga
+ * base URL
+ * @property string $urlParams Action URL Parameters
+ *
+ */
+class Tab extends AbstractWidget
+{
+ /**
+ * Whether this tab is currently active
+ *
+ * @var bool
+ */
+ private $active = false;
+
+ /**
+ * Default values for widget properties
+ *
+ * @var array
+ */
+ private $name = null;
+
+ /**
+ * The title displayed for this tab
+ *
+ * @var string
+ */
+ private $title = '';
+
+ /**
+ * The label displayed for this tab
+ *
+ * @var string
+ */
+ private $label = '';
+
+ /**
+ * The Url this tab points to
+ *
+ * @var Url|null
+ */
+ private $url = null;
+
+ /**
+ * The parameters for this tab's Url
+ *
+ * @var array
+ */
+ private $urlParams = array();
+
+ /**
+ * The icon image to use for this tab or null if none
+ *
+ * @var string|null
+ */
+ private $icon = null;
+
+ /**
+ * The icon class to use if $icon is null
+ *
+ * @var string|null
+ */
+ private $iconCls = null;
+
+ /**
+ * Additional a tag attributes
+ *
+ * @var array
+ */
+ private $tagParams;
+
+ /**
+ * Whether to open the link target on a new page
+ *
+ * @var boolean
+ */
+ private $targetBlank = false;
+
+ /**
+ * Data base target that determines if the link will be opened in a side-bar or in the main container
+ *
+ * @var null
+ */
+ private $baseTarget = null;
+
+ /**
+ * Sets an icon image for this tab
+ *
+ * @param string $icon The url of the image to use
+ */
+ public function setIcon($icon)
+ {
+ if (is_string($icon) && strpos($icon, '.') !== false) {
+ $icon = Url::fromPath($icon);
+ }
+ $this->icon = $icon;
+ }
+
+ /**
+ * Set's an icon class that will be used in an <i> tag if no icon image is set
+ *
+ * @param string $iconCls The CSS class of the icon to use
+ */
+ public function setIconCls($iconCls)
+ {
+ $this->iconCls = $iconCls;
+ }
+
+ /**
+ * @param mixed $name
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set the tab label
+ *
+ * @param string $label
+ */
+ public function setLabel($label)
+ {
+ $this->label = $label;
+ }
+
+ /**
+ * Get the tab label
+ *
+ * @return string
+ */
+ public function getLabel()
+ {
+ if (! $this->label) {
+ return $this->title;
+ }
+
+ return $this->label;
+ }
+
+ /**
+ * @param mixed $title
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ }
+
+ /**
+ * Set the Url this tab points to
+ *
+ * @param string|Url $url The Url to use for this tab
+ */
+ public function setUrl($url)
+ {
+ if (is_string($url)) {
+ $url = Url::fromPath($url);
+ }
+ $this->url = $url;
+ }
+
+ /**
+ * Get the tab's target URL
+ *
+ * @return Url
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * Set the parameters to be set for this tabs Url
+ *
+ * @param array $url The Url parameters to set
+ */
+ public function setUrlParams(array $urlParams)
+ {
+ $this->urlParams = $urlParams;
+ }
+
+ /**
+ * Set additional a tag attributes
+ *
+ * @param array $tagParams
+ */
+ public function setTagParams(array $tagParams)
+ {
+ $this->tagParams = $tagParams;
+ }
+
+ public function setTargetBlank($value = true)
+ {
+ $this->targetBlank = $value;
+ }
+
+ public function setBaseTarget($value)
+ {
+ $this->baseTarget = $value;
+ }
+
+ /**
+ * Create a new Tab with the given properties
+ *
+ * Allowed properties are all properties for which a setter exists
+ *
+ * @param array $properties An array of properties
+ */
+ public function __construct(array $properties = array())
+ {
+ foreach ($properties as $name => $value) {
+ $setter = 'set' . ucfirst($name);
+ if (method_exists($this, $setter)) {
+ $this->$setter($value);
+ }
+ }
+ }
+
+ /**
+ * Set this tab active (default) or inactive
+ *
+ * This is usually done through the tabs container widget, therefore it
+ * is not a good idea to directly call this function
+ *
+ * @param bool $active Whether the tab should be active
+ *
+ * @return $this
+ */
+ public function setActive($active = true)
+ {
+ $this->active = (bool) $active;
+ return $this;
+ }
+
+ /**
+ * @see Widget::render()
+ */
+ public function render()
+ {
+ $view = $this->view();
+ $classes = array();
+ if ($this->active) {
+ $classes[] = 'active';
+ }
+
+ $caption = $view->escape($this->getLabel());
+ $tagParams = $this->tagParams;
+ if ($this->targetBlank) {
+ // add warning to links that open in new tabs to improve accessibility, as recommended by WCAG20 G201
+ $caption .= '<span class="info-box display-on-hover"> opens in new window </span>';
+ $tagParams['target'] ='_blank';
+ }
+
+ if ($this->title) {
+ if ($tagParams !== null) {
+ $tagParams['title'] = $this->title;
+ $tagParams['aria-label'] = $this->title;
+ } else {
+ $tagParams = array(
+ 'title' => $this->title,
+ 'aria-label' => $this->title
+ );
+ }
+ }
+
+ if ($this->baseTarget !== null) {
+ $tagParams['data-base-target'] = $this->baseTarget;
+ }
+
+ if ($this->icon !== null) {
+ if (strpos($this->icon, '.') === false) {
+ $caption = $view->icon($this->icon) . $caption;
+ } else {
+ $caption = $view->img($this->icon, null, array('class' => 'icon')) . $caption;
+ }
+ }
+
+ if ($this->url !== null) {
+ $this->url->overwriteParams($this->urlParams);
+
+ if ($tagParams !== null) {
+ $params = $view->propertiesToString($tagParams);
+ } else {
+ $params = '';
+ }
+
+ $tab = sprintf(
+ '<a href="%s"%s>%s</a>',
+ $this->view()->escape($this->url->getAbsoluteUrl()),
+ $params,
+ $caption
+ );
+ } else {
+ $tab = $caption;
+ }
+
+ $class = empty($classes) ? '' : sprintf(' class="%s"', implode(' ', $classes));
+ return '<li ' . $class . '>' . $tab . "</li>\n";
+ }
+}
diff --git a/library/Icinga/Web/Widget/Tabextension/DashboardAction.php b/library/Icinga/Web/Widget/Tabextension/DashboardAction.php
new file mode 100644
index 0000000..a3e6c43
--- /dev/null
+++ b/library/Icinga/Web/Widget/Tabextension/DashboardAction.php
@@ -0,0 +1,35 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget\Tabextension;
+
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabs;
+
+/**
+ * Tabextension that allows to add the current URL to a dashboard
+ *
+ * Displayed as a dropdown field in the tabs
+ */
+class DashboardAction implements Tabextension
+{
+ /**
+ * Applies the dashboard actions to the provided tabset
+ *
+ * @param Tabs $tabs The tabs object to extend with
+ */
+ public function apply(Tabs $tabs)
+ {
+ $tabs->addAsDropdown(
+ 'dashboard',
+ array(
+ 'icon' => 'dashboard',
+ 'label' => t('Add To Dashboard'),
+ 'url' => Url::fromPath('dashboard/new-dashlet'),
+ 'urlParams' => array(
+ 'url' => rawurlencode(Url::fromRequest()->getRelativeUrl())
+ )
+ )
+ );
+ }
+}
diff --git a/library/Icinga/Web/Widget/Tabextension/DashboardSettings.php b/library/Icinga/Web/Widget/Tabextension/DashboardSettings.php
new file mode 100644
index 0000000..fc7412a
--- /dev/null
+++ b/library/Icinga/Web/Widget/Tabextension/DashboardSettings.php
@@ -0,0 +1,39 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget\Tabextension;
+
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabs;
+
+/**
+ * Dashboard settings
+ */
+class DashboardSettings implements Tabextension
+{
+ /**
+ * Apply this tabextension to the provided tabs
+ *
+ * @param Tabs $tabs The tabbar to modify
+ */
+ public function apply(Tabs $tabs)
+ {
+ $tabs->addAsDropdown(
+ 'dashboard_add',
+ array(
+ 'icon' => 'dashboard',
+ 'label' => t('Add Dashlet'),
+ 'url' => Url::fromPath('dashboard/new-dashlet')
+ )
+ );
+
+ $tabs->addAsDropdown(
+ 'dashboard_settings',
+ array(
+ 'icon' => 'dashboard',
+ 'label' => t('Settings'),
+ 'url' => Url::fromPath('dashboard/settings')
+ )
+ );
+ }
+}
diff --git a/library/Icinga/Web/Widget/Tabextension/MenuAction.php b/library/Icinga/Web/Widget/Tabextension/MenuAction.php
new file mode 100644
index 0000000..d713892
--- /dev/null
+++ b/library/Icinga/Web/Widget/Tabextension/MenuAction.php
@@ -0,0 +1,35 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget\Tabextension;
+
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabs;
+
+/**
+ * Tabextension that allows to add the current URL as menu entry
+ *
+ * Displayed as a dropdown field in the tabs
+ */
+class MenuAction implements Tabextension
+{
+ /**
+ * Applies the menu actions to the provided tabset
+ *
+ * @param Tabs $tabs The tabs object to extend with
+ */
+ public function apply(Tabs $tabs)
+ {
+ $tabs->addAsDropdown(
+ 'menu-entry',
+ array(
+ 'icon' => 'menu',
+ 'label' => t('Add To Menu'),
+ 'url' => Url::fromPath('navigation/add'),
+ 'urlParams' => array(
+ 'url' => rawurlencode(Url::fromRequest()->getRelativeUrl())
+ )
+ )
+ );
+ }
+}
diff --git a/library/Icinga/Web/Widget/Tabextension/OutputFormat.php b/library/Icinga/Web/Widget/Tabextension/OutputFormat.php
new file mode 100644
index 0000000..d5d83af
--- /dev/null
+++ b/library/Icinga/Web/Widget/Tabextension/OutputFormat.php
@@ -0,0 +1,114 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget\Tabextension;
+
+use Icinga\Application\Platform;
+use Icinga\Application\Hook;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tab;
+use Icinga\Web\Widget\Tabs;
+
+/**
+ * Tabextension that offers different output formats for the user in the dropdown area
+ */
+class OutputFormat implements Tabextension
+{
+ /**
+ * PDF output type
+ */
+ const TYPE_PDF = 'pdf';
+
+ /**
+ * JSON output type
+ */
+ const TYPE_JSON = 'json';
+
+ /**
+ * CSV output type
+ */
+ const TYPE_CSV = 'csv';
+
+ /**
+ * An array of tabs to be added to the dropdown area
+ *
+ * @var array
+ */
+ private $tabs = array();
+
+ /**
+ * Create a new OutputFormat extender
+ *
+ * In general, it's assumed that all types are supported when an outputFormat extension
+ * is added, so this class offers to remove specific types instead of adding ones
+ *
+ * @param array $disabled An array of output types to <b>not</b> show.
+ */
+ public function __construct(array $disabled = array())
+ {
+ foreach ($this->getSupportedTypes() as $type => $tabConfig) {
+ if (!in_array($type, $disabled)) {
+ $tabConfig['url'] = Url::fromRequest();
+ $tab = new Tab($tabConfig);
+ $tab->setTargetBlank();
+ $this->tabs[] = $tab;
+ }
+ }
+ }
+
+ /**
+ * Applies the format selectio to the provided tabset
+ *
+ * @param Tabs $tabs The tabs object to extend with
+ *
+ * @see Tabextension::apply()
+ */
+ public function apply(Tabs $tabs)
+ {
+ foreach ($this->tabs as $tab) {
+ $tabs->addAsDropdown($tab->getName(), $tab);
+ }
+ }
+
+ /**
+ * Return an array containing the tab definitions for all supported types
+ *
+ * Using array_keys on this array or isset allows to check whether a
+ * requested type is supported
+ *
+ * @return array
+ */
+ public function getSupportedTypes()
+ {
+ $supportedTypes = array();
+
+ $pdfexport = Hook::has('Pdfexport');
+
+ if ($pdfexport || Platform::extensionLoaded('gd')) {
+ $supportedTypes[self::TYPE_PDF] = array(
+ 'name' => 'pdf',
+ 'label' => 'PDF',
+ 'icon' => 'file-pdf',
+ 'urlParams' => array('format' => 'pdf'),
+ );
+ }
+
+ $supportedTypes[self::TYPE_CSV] = array(
+ 'name' => 'csv',
+ 'label' => 'CSV',
+ 'icon' => 'file-excel',
+ 'urlParams' => array('format' => 'csv')
+ );
+
+ if (Platform::extensionLoaded('json')) {
+ $supportedTypes[self::TYPE_JSON] = array(
+ 'name' => 'json',
+ 'label' => 'JSON',
+ 'icon' => 'doc-text',
+ 'urlParams' => array('format' => 'json')
+ );
+ }
+
+ return $supportedTypes;
+ }
+}
diff --git a/library/Icinga/Web/Widget/Tabextension/Tabextension.php b/library/Icinga/Web/Widget/Tabextension/Tabextension.php
new file mode 100644
index 0000000..ea49c4b
--- /dev/null
+++ b/library/Icinga/Web/Widget/Tabextension/Tabextension.php
@@ -0,0 +1,25 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget\Tabextension;
+
+use Icinga\Web\Widget\Tabs;
+
+/**
+ * Tabextension interface that allows to extend a tabbar with reusable components
+ *
+ * Tabs can be either extended by creating a `Tabextension` and calling the `apply()` method
+ * or by calling the `\Icinga\Web\Widget\Tabs` `extend()` method and providing
+ * a tab extension
+ *
+ * @see \Icinga\Web\Widget\Tabs::extend()
+ */
+interface Tabextension
+{
+ /**
+ * Apply this tabextension to the provided tabs
+ *
+ * @param Tabs $tabs The tabbar to modify
+ */
+ public function apply(Tabs $tabs);
+}
diff --git a/library/Icinga/Web/Widget/Tabs.php b/library/Icinga/Web/Widget/Tabs.php
new file mode 100644
index 0000000..9efa423
--- /dev/null
+++ b/library/Icinga/Web/Widget/Tabs.php
@@ -0,0 +1,453 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget;
+
+use Exception;
+use Icinga\Exception\Http\HttpNotFoundException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\Tabextension;
+use Icinga\Application\Icinga;
+use Countable;
+
+/**
+ * Navigation tab widget
+ */
+class Tabs extends AbstractWidget implements Countable
+{
+ /**
+ * Template used for the base tabs
+ *
+ * @var string
+ */
+ private $baseTpl = <<< 'EOT'
+<ul class="tabs primary-nav nav">
+ {TABS}
+ {DROPDOWN}
+ {REFRESH}
+ {CLOSE}
+</ul>
+EOT;
+
+ /**
+ * Template used for the tabs dropdown
+ *
+ * @var string
+ */
+ private $dropdownTpl = <<< 'EOT'
+<li class="dropdown-nav-item">
+ <a href="#" class="dropdown-toggle" title="{TITLE}" aria-label="{TITLE}">
+ <i aria-hidden="true" class="icon-down-open"></i>
+ </a>
+ <ul class="nav">
+ {TABS}
+ </ul>
+</li>
+EOT;
+
+ /**
+ * Template used for the close-button
+ *
+ * @var string
+ */
+ private $closeTpl = <<< 'EOT'
+<li class="close-container-btn">
+ <a href="#" title="{TITLE}" aria-label="{TITLE}" class="close-container-control">
+ <i aria-hidden="true" class="icon-cancel"></i>
+ </a>
+</li>
+EOT;
+
+ /**
+ * Template used for the refresh icon
+ *
+ * @var string
+ */
+ private $refreshTpl = <<< 'EOT'
+<li>
+ <a class="refresh-container-control spinner" href="{URL}" title="{TITLE}" aria-label="{LABEL}">
+ <i aria-hidden="true" class="icon-cw"></i>
+ </a>
+</li>
+EOT;
+
+ /**
+ * This is where single tabs added to this container will be stored
+ *
+ * @var array
+ */
+ private $tabs = array();
+
+ /**
+ * The name of the currently activated tab
+ *
+ * @var string
+ */
+ private $active;
+
+ /**
+ * Array of tab names which should be displayed in a dropdown
+ *
+ * @var array
+ */
+ private $dropdownTabs = array();
+
+ /**
+ * Whether only the close-button should by rendered for this tab
+ *
+ * @var bool
+ */
+ private $closeButtonOnly = false;
+
+ /**
+ * Whether the tabs should contain a close-button
+ *
+ * @var bool
+ */
+ private $closeTab = true;
+
+ /**
+ * CSS class name(s) for the &lt;ul&gt; element
+ *
+ * @var string
+ */
+ private $tab_class;
+
+ /**
+ * Set whether the current tab is closable
+ */
+ public function hideCloseButton()
+ {
+ $this->closeTab = false;
+ }
+
+ /**
+ * Activate the tab with the given name
+ *
+ * If another tab is currently active it will be deactivated
+ *
+ * @param string $name Name of the tab going to be activated
+ *
+ * @return $this
+ *
+ * @throws HttpNotFoundException When the tab w/ the given name does not exist
+ *
+ */
+ public function activate($name)
+ {
+ if (! $this->has($name)) {
+ throw new HttpNotFoundException('Can\'t activate tab %s. Tab does not exist', $name);
+ }
+
+ if ($this->active !== null) {
+ $this->tabs[$this->active]->setActive(false);
+ }
+ $this->get($name)->setActive();
+ $this->active = $name;
+
+ return $this;
+ }
+
+ /**
+ * Return the name of the active tab
+ *
+ * @return string
+ */
+ public function getActiveName()
+ {
+ return $this->active;
+ }
+
+ /**
+ * Set the CSS class name(s) for the &lt;ul&gt; element
+ *
+ * @param string $name CSS class name(s)
+ *
+ * @return $this
+ */
+ public function setClass($name)
+ {
+ $this->tab_class = $name;
+ return $this;
+ }
+
+ /**
+ * Whether the given tab name exists
+ *
+ * @param string $name Tab name
+ *
+ * @return bool
+ */
+ public function has($name)
+ {
+ return array_key_exists($name, $this->tabs);
+ }
+
+ /**
+ * Whether the given tab name exists
+ *
+ * @param string $name The tab you're interested in
+ *
+ * @return Tab
+ *
+ * @throws ProgrammingError When the given tab name doesn't exist
+ */
+ public function get($name)
+ {
+ if (!$this->has($name)) {
+ return null;
+ }
+ return $this->tabs[$name];
+ }
+
+ /**
+ * Add a new tab
+ *
+ * A unique tab name is required, the Tab itself can either be an array
+ * with tab properties or an instance of an existing Tab
+ *
+ * @param string $name The new tab name
+ * @param array|Tab $tab The tab itself of its properties
+ *
+ * @return $this
+ *
+ * @throws ProgrammingError When the tab name already exists
+ */
+ public function add($name, $tab)
+ {
+ if ($this->has($name)) {
+ throw new ProgrammingError(
+ 'Cannot add a tab named "%s" twice"',
+ $name
+ );
+ }
+ return $this->set($name, $tab);
+ }
+
+ /**
+ * Set a tab
+ *
+ * A unique tab name is required, will be replaced in case it already
+ * exists. The tab can either be an array with tab properties or an instance
+ * of an existing Tab
+ *
+ * @param string $name The new tab name
+ * @param array|Tab $tab The tab itself of its properties
+ *
+ * @return $this
+ */
+ public function set($name, $tab)
+ {
+ if ($tab instanceof Tab) {
+ $this->tabs[$name] = $tab;
+ } else {
+ $this->tabs[$name] = new Tab($tab + array('name' => $name));
+ }
+ return $this;
+ }
+
+ /**
+ * Remove a tab
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function remove($name)
+ {
+ if ($this->has($name)) {
+ unset($this->tabs[$name]);
+ if (($dropdownIndex = array_search($name, $this->dropdownTabs, true)) !== false) {
+ array_splice($this->dropdownTabs, $dropdownIndex, 1);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add a tab to the dropdown on the right side of the tab-bar.
+ *
+ * @param $name
+ * @param $tab
+ */
+ public function addAsDropdown($name, $tab)
+ {
+ $this->set($name, $tab);
+ $this->dropdownTabs[] = $name;
+ $this->dropdownTabs = array_unique($this->dropdownTabs);
+ }
+
+ /**
+ * Render the dropdown area with its tabs and return the resulting HTML
+ *
+ * @return mixed|string
+ */
+ private function renderDropdownTabs()
+ {
+ if (empty($this->dropdownTabs)) {
+ return '';
+ }
+ $tabs = '';
+ foreach ($this->dropdownTabs as $tabname) {
+ $tab = $this->get($tabname);
+ if ($tab === null) {
+ continue;
+ }
+ $tabs .= $tab;
+ }
+ return str_replace(array('{TABS}', '{TITLE}'), array($tabs, t('Dropdown menu')), $this->dropdownTpl);
+ }
+
+ /**
+ * Render all tabs, except the ones in dropdown area and return the resulting HTML
+ *
+ * @return string
+ */
+ private function renderTabs()
+ {
+ $tabs = '';
+ foreach ($this->tabs as $name => $tab) {
+ // ignore tabs added to dropdown
+ if (in_array($name, $this->dropdownTabs)) {
+ continue;
+ }
+ $tabs .= $tab;
+ }
+ return $tabs;
+ }
+
+ private function renderCloseTab()
+ {
+ return str_replace('{TITLE}', t('Close container'), $this->closeTpl);
+ }
+
+ private function renderRefreshTab()
+ {
+ $url = Url::fromRequest();
+ $tab = $this->get($this->getActiveName());
+
+ if ($tab !== null) {
+ $label = $this->view()->escape(
+ $tab->getLabel()
+ );
+ }
+
+ if (! empty($label)) {
+ $caption = $label;
+ } else {
+ $caption = t('Content');
+ }
+
+ $label = sprintf(t('Refresh the %s'), $caption);
+ $title = $label;
+
+ $tpl = str_replace(
+ array(
+ '{URL}',
+ '{TITLE}',
+ '{LABEL}'
+ ),
+ array(
+ $this->view()->escape($url->getAbsoluteUrl()),
+ $title,
+ $label
+ ),
+ $this->refreshTpl
+ );
+
+ return $tpl;
+ }
+
+ /**
+ * Render to HTML
+ *
+ * @see Widget::render
+ */
+ public function render()
+ {
+ if (empty($this->tabs) || true === $this->closeButtonOnly) {
+ $tabs = '';
+ $drop = '';
+ } else {
+ $tabs = $this->renderTabs();
+ $drop = $this->renderDropdownTabs();
+ }
+ $close = $this->closeTab ? $this->renderCloseTab() : '';
+ $refresh = $this->renderRefreshTab();
+
+ return str_replace(
+ array(
+ '{TABS}',
+ '{DROPDOWN}',
+ '{REFRESH}',
+ '{CLOSE}'
+ ),
+ array(
+ $tabs,
+ $drop,
+ $refresh,
+ $close
+ ),
+ $this->baseTpl
+ );
+ }
+
+ public function __toString()
+ {
+ try {
+ $html = $this->render();
+ } catch (Exception $e) {
+ return htmlspecialchars($e->getMessage());
+ }
+ return $html;
+ }
+
+ /**
+ * Return the number of tabs
+ *
+ * @return int
+ *
+ * @see Countable
+ */
+ public function count(): int
+ {
+ return count($this->tabs);
+ }
+
+ /**
+ * Return all tabs contained in this tab panel
+ *
+ * @return array
+ */
+ public function getTabs()
+ {
+ return $this->tabs;
+ }
+
+ /**
+ * Whether to hide all elements except of the close button
+ *
+ * @param bool $value
+ * @return Tabs fluent interface
+ */
+ public function showOnlyCloseButton($value = true)
+ {
+ $this->closeButtonOnly = $value;
+ return $this;
+ }
+
+ /**
+ * Apply a Tabextension on this tabs object
+ *
+ * @param Tabextension $tabextension
+ *
+ * @return $this
+ */
+ public function extend(Tabextension $tabextension)
+ {
+ $tabextension->apply($this);
+ return $this;
+ }
+}
diff --git a/library/Icinga/Web/Widget/Widget.php b/library/Icinga/Web/Widget/Widget.php
new file mode 100644
index 0000000..879858a
--- /dev/null
+++ b/library/Icinga/Web/Widget/Widget.php
@@ -0,0 +1,24 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web\Widget;
+
+use Icinga\Web\View;
+use Zend_View_Abstract;
+
+/**
+ * Abstract class for reusable view elements that can be
+ * rendered to a view
+ *
+ */
+interface Widget
+{
+ /**
+ * Renders this widget via the given view and returns the
+ * HTML as a string
+ *
+ * @param \Zend_View_Abstract $view
+ * @return string
+ */
+ // public function render(Zend_View_Abstract $view);
+}
diff --git a/library/Icinga/Web/Window.php b/library/Icinga/Web/Window.php
new file mode 100644
index 0000000..158483a
--- /dev/null
+++ b/library/Icinga/Web/Window.php
@@ -0,0 +1,125 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Session\SessionNamespace;
+
+class Window
+{
+ const UNDEFINED = 'undefined';
+
+ /** @var Window */
+ protected static $window;
+
+ /** @var string */
+ protected $id;
+
+ /** @var string */
+ protected $containerId;
+
+ public function __construct($id)
+ {
+ $parts = explode('_', $id, 2);
+ if (isset($parts[1])) {
+ $this->id = $parts[0];
+ $this->containerId = $id;
+ } else {
+ $this->id = $id;
+ }
+ }
+
+ /**
+ * Get whether the window's ID is undefined
+ *
+ * @return bool
+ */
+ public function isUndefined()
+ {
+ return $this->id === self::UNDEFINED;
+ }
+
+ /**
+ * Get the window's ID
+ *
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Get the container's ID
+ *
+ * @return string
+ */
+ public function getContainerId()
+ {
+ return $this->containerId ?: $this->id;
+ }
+
+ /**
+ * Return a window-aware session by using the given prefix
+ *
+ * @param string $prefix The prefix to use
+ * @param bool $reset Whether to reset any existing session-data
+ *
+ * @return SessionNamespace
+ */
+ public function getSessionNamespace($prefix, $reset = false)
+ {
+ $session = Session::getSession();
+
+ $identifier = $prefix . '_' . $this->getId();
+ if ($reset && $session->hasNamespace($identifier)) {
+ $session->removeNamespace($identifier);
+ }
+
+ $namespace = $session->getNamespace($identifier);
+ $nsUndef = $prefix . '_' . self::UNDEFINED;
+
+ if (! $reset && ! $this->isUndefined() && $session->hasNamespace($nsUndef)) {
+ // We may not have any window-id on the very first request. Now we add
+ // all values from the namespace, that has been created in this case,
+ // to the new one and remove it afterwards.
+ foreach ($session->getNamespace($nsUndef) as $name => $value) {
+ $namespace->set($name, $value);
+ }
+
+ $session->removeNamespace($nsUndef);
+ }
+
+ return $namespace;
+ }
+
+ /**
+ * Generate a random string
+ *
+ * @return string
+ */
+ public static function generateId()
+ {
+ $letters = 'abcefghijklmnopqrstuvwxyz';
+ return substr(str_shuffle($letters), 0, 12);
+ }
+
+ /**
+ * @return Window
+ */
+ public static function getInstance()
+ {
+ if (! isset(static::$window)) {
+ $id = Icinga::app()->getRequest()->getHeader('X-Icinga-WindowId');
+ if (empty($id) || $id === static::UNDEFINED) {
+ Icinga::app()->getResponse()->setOverrideWindowId();
+ $id = static::generateId();
+ }
+
+ static::$window = new Window($id);
+ }
+
+ return static::$window;
+ }
+}
diff --git a/library/Icinga/Web/Wizard.php b/library/Icinga/Web/Wizard.php
new file mode 100644
index 0000000..9a1b8b6
--- /dev/null
+++ b/library/Icinga/Web/Wizard.php
@@ -0,0 +1,720 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Web;
+
+use Icinga\Forms\ConfigForm;
+use Icinga\Module\Setup\Forms\ModulePage;
+use LogicException;
+use InvalidArgumentException;
+use Icinga\Web\Session\SessionNamespace;
+use Icinga\Web\Form\Decorator\ElementDoubler;
+
+/**
+ * Container and controller for form based wizards
+ */
+class Wizard
+{
+ /**
+ * An integer describing the wizard's forward direction
+ */
+ const FORWARD = 0;
+
+ /**
+ * An integer describing the wizard's backward direction
+ */
+ const BACKWARD = 1;
+
+ /**
+ * An integer describing that the wizard does not change its position
+ */
+ const NO_CHANGE = 2;
+
+ /**
+ * The name of the button to advance the wizard's position
+ */
+ const BTN_NEXT = 'btn_next';
+
+ /**
+ * The name of the button to rewind the wizard's position
+ */
+ const BTN_PREV = 'btn_prev';
+
+ /**
+ * The name and id of the element for showing the user an activity indicator when advancing the wizard
+ */
+ const PROGRESS_ELEMENT = 'wizard_progress';
+
+ /**
+ * This wizard's parent
+ *
+ * @var Wizard
+ */
+ protected $parent;
+
+ /**
+ * The name of the wizard's current page
+ *
+ * @var string
+ */
+ protected $currentPage;
+
+ /**
+ * The pages being part of this wizard
+ *
+ * @var array
+ */
+ protected $pages = array();
+
+ /**
+ * Initialize a new wizard
+ */
+ public function __construct()
+ {
+ $this->init();
+ }
+
+ /**
+ * Run additional initialization routines
+ *
+ * Should be implemented by subclasses to add pages to the wizard.
+ */
+ protected function init()
+ {
+ }
+
+ /**
+ * Return this wizard's parent or null in case it has none
+ *
+ * @return Wizard|null
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Set this wizard's parent
+ *
+ * @param Wizard $wizard The parent wizard
+ *
+ * @return $this
+ */
+ public function setParent(Wizard $wizard)
+ {
+ $this->parent = $wizard;
+ return $this;
+ }
+
+ /**
+ * Return the pages being part of this wizard
+ *
+ * In case this is a nested wizard a flattened array of all contained pages is returned.
+ *
+ * @return array
+ */
+ public function getPages()
+ {
+ $pages = array();
+ foreach ($this->pages as $page) {
+ if ($page instanceof self) {
+ $pages = array_merge($pages, $page->getPages());
+ } else {
+ $pages[] = $page;
+ }
+ }
+
+ return $pages;
+ }
+
+ /**
+ * Return the page with the given name
+ *
+ * Note that it's also possible to retrieve a nested wizard's page by using this method.
+ *
+ * @param string $name The name of the page to return
+ *
+ * @return ModulePage|Form|null The page or null in case there is no page with the given name
+ */
+ public function getPage($name)
+ {
+ foreach ($this->getPages() as $page) {
+ if ($name === $page->getName()) {
+ return $page;
+ }
+ }
+ }
+
+ /**
+ * Add a new page or wizard to this wizard
+ *
+ * @param Form|Wizard $page The page or wizard to add to the wizard
+ *
+ * @return $this
+ */
+ public function addPage($page)
+ {
+ if (! $page instanceof Form && ! $page instanceof self) {
+ throw new InvalidArgumentException(
+ 'The $page argument must be an instance of Icinga\Web\Form '
+ . 'or Icinga\Web\Wizard but is of type: ' . get_class($page)
+ );
+ } elseif ($page instanceof self) {
+ $page->setParent($this);
+ }
+
+ $this->pages[] = $page;
+ return $this;
+ }
+
+ /**
+ * Add multiple pages or wizards to this wizard
+ *
+ * @param array $pages The pages or wizards to add to the wizard
+ *
+ * @return $this
+ */
+ public function addPages(array $pages)
+ {
+ foreach ($pages as $page) {
+ $this->addPage($page);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Assert that this wizard has any pages
+ *
+ * @throws LogicException In case this wizard has no pages
+ */
+ protected function assertHasPages()
+ {
+ $pages = $this->getPages();
+ if (count($pages) < 2) {
+ throw new LogicException("Although Chuck Norris can advance a wizard with less than two pages, you can't.");
+ }
+ }
+
+ /**
+ * Return the current page of this wizard
+ *
+ * @return Form
+ *
+ * @throws LogicException In case the name of the current page currently being set is invalid
+ */
+ public function getCurrentPage()
+ {
+ if ($this->parent) {
+ return $this->parent->getCurrentPage();
+ }
+
+ if ($this->currentPage === null) {
+ $this->assertHasPages();
+ $pages = $this->getPages();
+ $this->currentPage = $this->getSession()->get('current_page', $pages[0]->getName());
+ }
+
+ if (($page = $this->getPage($this->currentPage)) === null) {
+ throw new LogicException(sprintf('No page found with name "%s"', $this->currentPage));
+ }
+
+ return $page;
+ }
+
+ /**
+ * Set the current page of this wizard
+ *
+ * @param Form $page The page to set as current page
+ *
+ * @return $this
+ */
+ public function setCurrentPage(Form $page)
+ {
+ $this->currentPage = $page->getName();
+ $this->getSession()->set('current_page', $this->currentPage);
+ return $this;
+ }
+
+ /**
+ * Setup the given page that is either going to be displayed or validated
+ *
+ * Implement this method in a subclass to populate default values and/or other data required to process the form.
+ *
+ * @param Form $page The page to setup
+ * @param Request $request The current request
+ */
+ public function setupPage(Form $page, Request $request)
+ {
+ }
+
+ /**
+ * Process the given request using this wizard
+ *
+ * Validate the request data using the current page, update the wizard's
+ * position and redirect to the page's redirect url upon success.
+ *
+ * @param Request $request The request to be processed
+ *
+ * @return Request The request supposed to be processed
+ */
+ public function handleRequest(Request $request = null)
+ {
+ $page = $this->getCurrentPage();
+
+ if (($wizard = $this->findWizard($page)) !== null) {
+ return $wizard->handleRequest($request);
+ }
+
+ if ($request === null) {
+ $request = $page->getRequest();
+ }
+
+ $this->setupPage($page, $request);
+ $requestData = $this->getRequestData($page, $request);
+ if ($page->wasSent($requestData)) {
+ if (($requestedPage = $this->getRequestedPage($requestData)) !== null) {
+ $isValid = false;
+ $direction = $this->getDirection($request);
+ if ($direction === static::FORWARD && $page->isValid($requestData)) {
+ $isValid = true;
+ if ($this->isLastPage($page)) {
+ $this->setIsFinished();
+ }
+ } elseif ($direction === static::BACKWARD) {
+ $page->populate($requestData);
+ $isValid = true;
+ }
+
+ if ($isValid) {
+ $pageData = & $this->getPageData();
+ $pageData[$page->getName()] = ConfigForm::transformEmptyValuesToNull($page->getValues());
+ $this->setCurrentPage($this->getNewPage($requestedPage, $page));
+ $page->getResponse()->redirectAndExit($page->getRedirectUrl());
+ }
+ } elseif ($page->getValidatePartial()) {
+ $page->isValidPartial($requestData);
+ } else {
+ $page->populate($requestData);
+ }
+ } elseif (($pageData = $this->getPageData($page->getName())) !== null) {
+ $page->populate($pageData);
+ }
+
+ return $request;
+ }
+
+ /**
+ * Return the wizard for the given page or null if its not part of a wizard
+ *
+ * @param Form $page The page to return its wizard for
+ *
+ * @return Wizard|null
+ */
+ protected function findWizard(Form $page)
+ {
+ foreach ($this->getWizards() as $wizard) {
+ if ($wizard->getPage($page->getName()) === $page) {
+ return $wizard;
+ }
+ }
+ }
+
+ /**
+ * Return this wizard's child wizards
+ *
+ * @return array
+ */
+ protected function getWizards()
+ {
+ $wizards = array();
+ foreach ($this->pages as $pageOrWizard) {
+ if ($pageOrWizard instanceof self) {
+ $wizards[] = $pageOrWizard;
+ }
+ }
+
+ return $wizards;
+ }
+
+ /**
+ * Return the request data based on given form's request method
+ *
+ * @param Form $page The page to fetch the data for
+ * @param Request $request The request to fetch the data from
+ *
+ * @return array
+ */
+ protected function getRequestData(Form $page, Request $request)
+ {
+ if (strtolower($request->getMethod()) === $page->getMethod()) {
+ return $request->{'get' . ($request->isPost() ? 'Post' : 'Query')}();
+ }
+
+ return array();
+ }
+
+ /**
+ * Return the name of the requested page
+ *
+ * @param array $requestData The request's data
+ *
+ * @return null|string The name of the requested page or null in case no page has been requested
+ */
+ protected function getRequestedPage(array $requestData)
+ {
+ if ($this->parent) {
+ return $this->parent->getRequestedPage($requestData);
+ }
+
+ if (isset($requestData[static::BTN_NEXT])) {
+ return $requestData[static::BTN_NEXT];
+ } elseif (isset($requestData[static::BTN_PREV])) {
+ return $requestData[static::BTN_PREV];
+ }
+ }
+
+ /**
+ * Return the direction of this wizard using the given request
+ *
+ * @param Request $request The request to use
+ *
+ * @return int The direction @see Wizard::FORWARD @see Wizard::BACKWARD @see Wizard::NO_CHANGE
+ */
+ protected function getDirection(Request $request = null)
+ {
+ if ($this->parent) {
+ return $this->parent->getDirection($request);
+ }
+
+ $currentPage = $this->getCurrentPage();
+
+ if ($request === null) {
+ $request = $currentPage->getRequest();
+ }
+
+ $requestData = $this->getRequestData($currentPage, $request);
+ if (isset($requestData[static::BTN_NEXT])) {
+ return static::FORWARD;
+ } elseif (isset($requestData[static::BTN_PREV])) {
+ return static::BACKWARD;
+ }
+
+ return static::NO_CHANGE;
+ }
+
+ /**
+ * Return the new page to set as current page
+ *
+ * Permission is checked by verifying that the requested page or its previous page has page data available.
+ * The requested page is automatically permitted without any checks if the origin page is its previous
+ * page or one that occurs later in order.
+ *
+ * @param string $requestedPage The name of the requested page
+ * @param Form $originPage The origin page
+ *
+ * @return Form The new page
+ *
+ * @throws InvalidArgumentException In case the requested page does not exist or is not permitted yet
+ */
+ protected function getNewPage($requestedPage, Form $originPage)
+ {
+ if ($this->parent) {
+ return $this->parent->getNewPage($requestedPage, $originPage);
+ }
+
+ if (($page = $this->getPage($requestedPage)) !== null) {
+ $permitted = true;
+
+ $pages = $this->getPages();
+ if (! $this->hasPageData($requestedPage) && ($index = array_search($page, $pages, true)) > 0) {
+ $previousPage = $pages[$index - 1];
+ if ($originPage === null || ($previousPage->getName() !== $originPage->getName()
+ && array_search($originPage, $pages, true) < $index)) {
+ $permitted = $this->hasPageData($previousPage->getName());
+ }
+ }
+
+ if ($permitted) {
+ return $page;
+ }
+ }
+
+ throw new InvalidArgumentException(
+ sprintf('"%s" is either an unknown page or one you are not permitted to view', $requestedPage)
+ );
+ }
+
+ /**
+ * Return the next or previous page based on the given one
+ *
+ * @param Form $page The page to skip
+ *
+ * @return Form
+ */
+ protected function skipPage(Form $page)
+ {
+ if ($this->parent) {
+ return $this->parent->skipPage($page);
+ }
+
+ if ($this->hasPageData($page->getName())) {
+ $pageData = & $this->getPageData();
+ unset($pageData[$page->getName()]);
+ }
+
+ $pages = $this->getPages();
+ if ($this->getDirection() === static::FORWARD) {
+ $nextPage = $pages[array_search($page, $pages, true) + 1];
+ $newPage = $this->getNewPage($nextPage->getName(), $page);
+ } else { // $this->getDirection() === static::BACKWARD
+ $previousPage = $pages[array_search($page, $pages, true) - 1];
+ $newPage = $this->getNewPage($previousPage->getName(), $page);
+ }
+
+ return $newPage;
+ }
+
+ /**
+ * Return whether the given page is this wizard's last page
+ *
+ * @param Form $page The page to check
+ *
+ * @return bool
+ */
+ protected function isLastPage(Form $page)
+ {
+ if ($this->parent) {
+ return $this->parent->isLastPage($page);
+ }
+
+ $pages = $this->getPages();
+ return $page->getName() === end($pages)->getName();
+ }
+
+ /**
+ * Return whether all of this wizard's pages were visited by the user
+ *
+ * The base implementation just verifies that the very last page has page data available.
+ *
+ * @return bool
+ */
+ public function isComplete()
+ {
+ $pages = $this->getPages();
+ return $this->hasPageData($pages[count($pages) - 1]->getName());
+ }
+
+ /**
+ * Set whether this wizard has been completed
+ *
+ * @param bool $state Whether this wizard has been completed
+ *
+ * @return $this
+ */
+ public function setIsFinished($state = true)
+ {
+ $this->getSession()->set('isFinished', $state);
+ return $this;
+ }
+
+ /**
+ * Return whether this wizard has been completed
+ *
+ * @return bool
+ */
+ public function isFinished()
+ {
+ return $this->getSession()->get('isFinished', false);
+ }
+
+ /**
+ * Return the overall page data or one for a particular page
+ *
+ * Note that this method returns by reference so in order to update the
+ * returned array set this method's return value also by reference.
+ *
+ * @param string $pageName The page for which to return the data
+ *
+ * @return array
+ */
+ public function & getPageData($pageName = null)
+ {
+ $session = $this->getSession();
+
+ if (false === isset($session->page_data)) {
+ $session->page_data = array();
+ }
+
+ $pageData = & $session->getByRef('page_data');
+ if ($pageName !== null) {
+ $data = null;
+ if (isset($pageData[$pageName])) {
+ $data = & $pageData[$pageName];
+ }
+
+ return $data;
+ }
+
+ return $pageData;
+ }
+
+ /**
+ * Return whether there is any data for the given page
+ *
+ * @param string $pageName The name of the page to check
+ *
+ * @return bool
+ */
+ public function hasPageData($pageName)
+ {
+ return $this->getPageData($pageName) !== null;
+ }
+
+ /**
+ * Return a session to be used by this wizard
+ *
+ * @return SessionNamespace
+ */
+ public function getSession()
+ {
+ if ($this->parent) {
+ return $this->parent->getSession();
+ }
+
+ return Session::getSession()->getNamespace(get_class($this));
+ }
+
+ /**
+ * Clear the session being used by this wizard
+ */
+ public function clearSession()
+ {
+ $this->getSession()->clear();
+ }
+
+ /**
+ * Add buttons to the given page based on its position in the page-chain
+ *
+ * @param Form $page The page to add the buttons to
+ */
+ protected function addButtons(Form $page)
+ {
+ $pages = $this->getPages();
+ $index = array_search($page, $pages, true);
+ if ($index === 0) {
+ $page->addElement(
+ 'button',
+ static::BTN_NEXT,
+ array(
+ 'class' => 'control-button btn-primary',
+ 'type' => 'submit',
+ 'value' => $pages[1]->getName(),
+ 'label' => t('Next'),
+ 'decorators' => array('ViewHelper', 'Spinner')
+ )
+ );
+ } elseif ($index < count($pages) - 1) {
+ $page->addElement(
+ 'button',
+ static::BTN_PREV,
+ array(
+ 'class' => 'control-button',
+ 'type' => 'submit',
+ 'value' => $pages[$index - 1]->getName(),
+ 'label' => t('Back'),
+ 'decorators' => array('ViewHelper'),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ $page->addElement(
+ 'button',
+ static::BTN_NEXT,
+ array(
+ 'class' => 'control-button btn-primary',
+ 'type' => 'submit',
+ 'value' => $pages[$index + 1]->getName(),
+ 'label' => t('Next'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ } else {
+ $page->addElement(
+ 'button',
+ static::BTN_PREV,
+ array(
+ 'class' => 'control-button',
+ 'type' => 'submit',
+ 'value' => $pages[$index - 1]->getName(),
+ 'label' => t('Back'),
+ 'decorators' => array('ViewHelper'),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ $page->addElement(
+ 'button',
+ static::BTN_NEXT,
+ array(
+ 'class' => 'control-button btn-primary',
+ 'type' => 'submit',
+ 'value' => $page->getName(),
+ 'label' => t('Finish'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ }
+
+ $page->setAttrib('data-progress-element', static::PROGRESS_ELEMENT);
+ $page->addElement(
+ 'note',
+ static::PROGRESS_ELEMENT,
+ array(
+ 'order' => 99, // Ensures that it's shown on the right even if a sub-class adds another button
+ 'decorators' => array(
+ 'ViewHelper',
+ array('Spinner', array('id' => static::PROGRESS_ELEMENT))
+ )
+ )
+ );
+
+ $page->addDisplayGroup(
+ array(static::BTN_PREV, static::BTN_NEXT, static::PROGRESS_ELEMENT),
+ 'buttons',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ new ElementDoubler(array(
+ 'double' => static::BTN_NEXT,
+ 'condition' => static::BTN_PREV,
+ 'placement' => ElementDoubler::PREPEND,
+ 'attributes' => array('tabindex' => -1, 'class' => 'double')
+ )),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'buttons'))
+ )
+ )
+ );
+ }
+
+ /**
+ * Return the current page of this wizard with appropriate buttons being added
+ *
+ * @return Form
+ */
+ public function getForm()
+ {
+ $form = $this->getCurrentPage();
+ $form->create(); // Make sure that buttons are displayed at the very bottom
+ $this->addButtons($form);
+ return $form;
+ }
+
+ /**
+ * Return the current page of this wizard rendered as HTML
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return (string) $this->getForm();
+ }
+}
diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php
new file mode 100644
index 0000000..f6ff738
--- /dev/null
+++ b/modules/doc/application/controllers/IcingawebController.php
@@ -0,0 +1,62 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Controllers;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Doc\DocController;
+
+class IcingawebController extends DocController
+{
+ /**
+ * Get the path to Icinga Web 2's documentation
+ *
+ * @return ?string
+ *
+ * @throws \Icinga\Exception\Http\HttpNotFoundException If Icinga Web 2's documentation is not available
+ */
+ protected function getPath()
+ {
+ $path = Icinga::app()->getBaseDir('doc');
+ if (is_dir($path)) {
+ return $path;
+ }
+ if (($path = $this->Config()->get('documentation', 'icingaweb2')) !== null) {
+ if (is_dir($path)) {
+ return $path;
+ }
+ }
+ $this->httpNotFound($this->translate('Documentation for Icinga Web 2 is not available'));
+ }
+
+ /**
+ * View the toc of Icinga Web 2's documentation
+ */
+ public function tocAction()
+ {
+ $this->renderToc($this->getPath(), 'Icinga Web 2', 'doc/icingaweb/chapter');
+ }
+
+ /**
+ * View a chapter of Icinga Web 2's documentation
+ *
+ * @throws \Icinga\Exception\MissingParameterException If the required parameter 'chapter' is missing
+ */
+ public function chapterAction()
+ {
+ $chapter = $this->params->getRequired('chapter');
+ $this->renderChapter(
+ $this->getPath(),
+ $chapter,
+ 'doc/icingaweb/chapter'
+ );
+ }
+
+ /**
+ * View Icinga Web 2's documentation as PDF
+ */
+ public function pdfAction()
+ {
+ $this->renderPdf($this->getPath(), 'Icinga Web 2', 'doc/icingaweb/chapter');
+ }
+}
diff --git a/modules/doc/application/controllers/IndexController.php b/modules/doc/application/controllers/IndexController.php
new file mode 100644
index 0000000..3ff5aa1
--- /dev/null
+++ b/modules/doc/application/controllers/IndexController.php
@@ -0,0 +1,27 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Controllers;
+
+use Icinga\Module\Doc\DocController;
+use Icinga\Web\Url;
+
+/**
+ * Documentation module index
+ */
+class IndexController extends DocController
+{
+ /**
+ * Documentation module landing page
+ *
+ * Lists documentation links
+ */
+ public function indexAction()
+ {
+ $this->getTabs()->add('documentation', array(
+ 'active' => true,
+ 'title' => $this->translate('Documentation', 'Tab title'),
+ 'url' => Url::fromRequest()
+ ));
+ }
+}
diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
new file mode 100644
index 0000000..9e94a61
--- /dev/null
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -0,0 +1,206 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Controllers;
+
+use finfo;
+use SplFileInfo;
+use Icinga\Application\Icinga;
+use Icinga\Module\Doc\DocController;
+use Icinga\Module\Doc\Exception\DocException;
+use Icinga\Web\Url;
+
+class ModuleController extends DocController
+{
+ /**
+ * Get the path to a module documentation
+ *
+ * @param string $module The name of the module
+ * @param string $default The default path
+ * @param bool $suppressErrors Whether to not throw an exception if the module documentation is not available
+ *
+ * @return string|null Path to the documentation or null if the module documentation is not available
+ * and errors are suppressed
+ *
+ * @throws \Icinga\Exception\Http\HttpNotFoundException If the module documentation is not available and errors
+ * are not suppressed
+ */
+ protected function getPath($module, $default, $suppressErrors = false)
+ {
+ if (is_dir($default)) {
+ return $default;
+ }
+ if (($path = $this->Config()->get('documentation', 'modules')) !== null) {
+ $path = str_replace('{module}', $module, $path);
+ if (is_dir($path)) {
+ return $path;
+ }
+ }
+ if ($suppressErrors) {
+ return null;
+ }
+ $this->httpNotFound($this->translate('Documentation for module \'%s\' is not available'), $module);
+ }
+
+ /**
+ * List modules which are enabled and having the 'doc' directory
+ */
+ public function indexAction()
+ {
+ $moduleManager = Icinga::app()->getModuleManager();
+ $modules = array();
+ foreach ($moduleManager->listInstalledModules() as $module) {
+ $path = $this->getPath($module, $moduleManager->getModuleDir($module, '/doc'), true);
+ if ($path !== null) {
+ $modules[] = $moduleManager->getModule($module, false);
+ }
+ }
+ $this->view->modules = $modules;
+ $this->getTabs()->add('module-documentation', array(
+ 'active' => true,
+ 'title' => $this->translate('Module Documentation', 'Tab title'),
+ 'url' => Url::fromRequest()
+ ));
+ }
+
+ /**
+ * Assert that the given module is installed
+ *
+ * @param string $moduleName
+ *
+ * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed
+ */
+ protected function assertModuleInstalled($moduleName)
+ {
+ $moduleManager = Icinga::app()->getModuleManager();
+ if (! $moduleManager->hasInstalled($moduleName)) {
+ $this->httpNotFound($this->translate('Module \'%s\' is not installed'), $moduleName);
+ }
+ }
+
+ /**
+ * View the toc of a module's documentation
+ *
+ * @throws \Icinga\Exception\MissingParameterException If the required parameter 'moduleName' is empty
+ * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed
+ * @see assertModuleInstalled()
+ */
+ public function tocAction()
+ {
+ $module = $this->params->getRequired('moduleName');
+ $this->assertModuleInstalled($module);
+ $moduleManager = Icinga::app()->getModuleManager();
+ $name = $moduleManager->getModule($module, false)->getTitle();
+ try {
+ $this->renderToc(
+ $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')),
+ $name,
+ 'doc/module/chapter',
+ array('moduleName' => $module)
+ );
+ } catch (DocException $e) {
+ $this->httpNotFound($e->getMessage());
+ }
+ }
+
+ /**
+ * View a chapter of a module's documentation
+ *
+ * @throws \Icinga\Exception\MissingParameterException If one of the required parameters 'moduleName' and
+ * 'chapter' is empty
+ * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed
+ * @see assertModuleInstalled()
+ */
+ public function chapterAction()
+ {
+ $module = $this->params->getRequired('moduleName');
+ $this->assertModuleInstalled($module);
+ $chapter = $this->params->getRequired('chapter');
+ $this->view->moduleName = $module;
+ try {
+ $this->renderChapter(
+ $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')),
+ $chapter,
+ 'doc/module/chapter',
+ 'doc/module/img',
+ array('moduleName' => $module)
+ );
+ } catch (DocException $e) {
+ $this->httpNotFound($e->getMessage());
+ }
+ }
+
+ /**
+ * 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 || substr($imagePath, 0, strlen($docPath)) !== $docPath) {
+ $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($_SERVER['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;
+ }
+ }
+
+ $this->getResponse()
+ ->setHeader('ETag', $etag)
+ ->setHeader('Cache-Control', 'no-transform,public,max-age=3600,must-revalidate', true);
+
+ if ($match) {
+ $this->getResponse()->setHttpResponseCode(304);
+ } else {
+ $finfo = new finfo();
+ $this->getResponse()
+ ->setHeader('Content-Type', $finfo->file($imagePath, FILEINFO_MIME_TYPE))
+ ->setHeader('Content-Length', $imageInfo->getSize())
+ ->sendHeaders();
+
+ ob_end_clean();
+ readfile($imagePath);
+ }
+ }
+
+ /**
+ * View a module's documentation as PDF
+ *
+ * @throws \Icinga\Exception\MissingParameterException If the required parameter 'moduleName' is empty
+ * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed
+ * @see assertModuleInstalled()
+ */
+ public function pdfAction()
+ {
+ $module = $this->params->getRequired('moduleName');
+ $this->assertModuleInstalled($module);
+ $this->renderPdf(
+ $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')),
+ $module,
+ 'doc/module/chapter',
+ array('moduleName' => $module)
+ );
+ }
+}
diff --git a/modules/doc/application/controllers/SearchController.php b/modules/doc/application/controllers/SearchController.php
new file mode 100644
index 0000000..6d26f52
--- /dev/null
+++ b/modules/doc/application/controllers/SearchController.php
@@ -0,0 +1,97 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Controllers;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Doc\DocController;
+use Icinga\Module\Doc\DocParser;
+use Icinga\Module\Doc\Exception\DocException;
+use Icinga\Module\Doc\Renderer\DocSearchRenderer;
+use Icinga\Module\Doc\Search\DocSearch;
+use Icinga\Module\Doc\Search\DocSearchIterator;
+
+class SearchController extends DocController
+{
+ /**
+ * Render search
+ */
+ public function indexAction()
+ {
+ $parser = new DocParser($this->getWebPath());
+ $search = new DocSearchRenderer(
+ new DocSearchIterator(
+ $parser->getDocTree()->getIterator(),
+ new DocSearch($this->params->get('q'))
+ )
+ );
+ $search->setUrl('doc/icingaweb/chapter');
+ if (strlen($this->params->get('q')) < 3) {
+ $this->view->searches = array();
+ return;
+ }
+ $searches = array(
+ 'Icinga Web 2' => $search
+ );
+ foreach (Icinga::app()->getModuleManager()->listEnabledModules() as $module) {
+ if (($path = $this->getModulePath($module)) !== null) {
+ try {
+ $parser = new DocParser($path);
+ $search = new DocSearchRenderer(
+ new DocSearchIterator(
+ $parser->getDocTree()->getIterator(),
+ new DocSearch($this->params->get('q'))
+ )
+ );
+ } catch (DocException $e) {
+ continue;
+ }
+ $search
+ ->setUrl('doc/module/chapter')
+ ->setUrlParams(array('moduleName' => $module));
+ $searches[$module] = $search;
+ }
+ }
+ $this->view->searches = $searches;
+ }
+
+ /**
+ * Get the path to a module's documentation
+ *
+ * @param string $module
+ *
+ * @return string|null
+ */
+ protected function getModulePath($module)
+ {
+ if (is_dir(($path = Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')))) {
+ return $path;
+ }
+ if (($path = $this->Config()->get('documentation', 'modules')) !== null) {
+ $path = str_replace('{module}', $module, $path);
+ if (is_dir($path)) {
+ return $path;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the path to Icinga Web 2's documentation
+ *
+ * @return ?string
+ */
+ protected function getWebPath()
+ {
+ $path = Icinga::app()->getBaseDir('doc');
+ if (is_dir($path)) {
+ return $path;
+ }
+ if (($path = $this->Config()->get('documentation', 'icingaweb2')) !== null) {
+ if (is_dir($path)) {
+ return $path;
+ }
+ }
+ $this->httpNotFound($this->translate('Documentation for Icinga Web 2 is not available'));
+ }
+}
diff --git a/modules/doc/application/controllers/StyleController.php b/modules/doc/application/controllers/StyleController.php
new file mode 100644
index 0000000..5890367
--- /dev/null
+++ b/modules/doc/application/controllers/StyleController.php
@@ -0,0 +1,41 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Controllers;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Controller;
+use Icinga\Web\Widget;
+
+class StyleController extends Controller
+{
+ public function guideAction()
+ {
+ $this->view->tabs = $this->tabs()->activate('guide');
+ }
+
+ public function fontAction()
+ {
+ $this->view->tabs = $this->tabs()->activate('font');
+ $confFile = Icinga::app()->getApplicationDir('fonts/fontello-ifont/config.json');
+ $this->view->font = json_decode(file_get_contents($confFile));
+ }
+
+ protected function tabs()
+ {
+ return Widget::create('tabs')->add(
+ 'guide',
+ array(
+ 'label' => $this->translate('Style Guide'),
+ 'url' => 'doc/style/guide'
+ )
+ )->add(
+ 'font',
+ array(
+ 'label' => $this->translate('Icons'),
+ 'title' => $this->translate('List all available icons'),
+ 'url' => 'doc/style/font'
+ )
+ );
+ }
+}
diff --git a/modules/doc/application/views/scripts/chapter.phtml b/modules/doc/application/views/scripts/chapter.phtml
new file mode 100644
index 0000000..8cd4f6e
--- /dev/null
+++ b/modules/doc/application/views/scripts/chapter.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= /** @var \Icinga\Web\Widget\Tabs $tabs */ $this->tabs ?>
+</div>
+<div class="content chapter">
+ <?= /** @var \Icinga\Module\Doc\Renderer\DocSectionRenderer $section */ $section ?>
+</div>
diff --git a/modules/doc/application/views/scripts/index/index.phtml b/modules/doc/application/views/scripts/index/index.phtml
new file mode 100644
index 0000000..9bf745a
--- /dev/null
+++ b/modules/doc/application/views/scripts/index/index.phtml
@@ -0,0 +1,19 @@
+<div class="controls">
+ <?= /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?>
+</div>
+<div class="content">
+ <ul>
+ <li><?= $this->qlink(
+ 'Icinga Web 2',
+ 'doc/icingaweb/toc',
+ null,
+ array('title' => $this->translate('Show the documentation\'s table of contents for Icinga Web 2'))
+ ) ?></li>
+ <li><?= $this->qlink(
+ $this->translate('Module documentations'),
+ 'doc/module/',
+ null,
+ array('title' => $this->translate('List all modules for which documentation is available'))
+ ) ?></li>
+ </ul>
+</div>
diff --git a/modules/doc/application/views/scripts/module/index.phtml b/modules/doc/application/views/scripts/module/index.phtml
new file mode 100644
index 0000000..f70d69a
--- /dev/null
+++ b/modules/doc/application/views/scripts/module/index.phtml
@@ -0,0 +1,19 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+
+<div class="content">
+ <ul>
+ <?php foreach ($modules as $module): /** @var \Icinga\Application\Modules\Module $module */ ?>
+ <li><?= $this->qlink(
+ $module->getTitle(),
+ 'doc/module/toc',
+ array('moduleName' => $module->getName()),
+ array('title' => sprintf(
+ $this->translate('Show the documentation\'s table of contents for the %s'),
+ $module->getTitle()
+ ))
+ ) ?></li>
+ <?php endforeach ?>
+ </ul>
+</div>
diff --git a/modules/doc/application/views/scripts/pdf.phtml b/modules/doc/application/views/scripts/pdf.phtml
new file mode 100644
index 0000000..2666efb
--- /dev/null
+++ b/modules/doc/application/views/scripts/pdf.phtml
@@ -0,0 +1,5 @@
+<div class="content">
+ <h1><?= /** @var string $title */ $title ?></h1>
+ <?= /** @var \Icinga\Module\Doc\Renderer\DocTocRenderer $toc */ $toc ?>
+ <?= /** @var \Icinga\Module\Doc\Renderer\DocSectionRenderer $section */ $section ?>
+</div>
diff --git a/modules/doc/application/views/scripts/search/index.phtml b/modules/doc/application/views/scripts/search/index.phtml
new file mode 100644
index 0000000..c613f04
--- /dev/null
+++ b/modules/doc/application/views/scripts/search/index.phtml
@@ -0,0 +1,8 @@
+<div class="content">
+ <?php foreach (/** @var \Icinga\Module\Doc\Renderer\DocSearchRenderer[] $searches */ $searches as $title => $search): ?>
+ <h2><?= $this->escape($title) ?></h2>
+ <?= $search->isEmpty()
+ ? $this->translate('No documentation found matching the filter')
+ : $search ?>
+ <?php endforeach ?>
+</div>
diff --git a/modules/doc/application/views/scripts/style/font.phtml b/modules/doc/application/views/scripts/style/font.phtml
new file mode 100644
index 0000000..40a70ee
--- /dev/null
+++ b/modules/doc/application/views/scripts/style/font.phtml
@@ -0,0 +1,14 @@
+<div class="controls">
+<?= $this->tabs ?>
+<h1>Icinga Web 2 Icons</h1>
+</div>
+
+<div class="content">
+<?php foreach ($this->font->glyphs as $icon): ?>
+<div class="icon <?=
+ $this->font->css_prefix_text . $icon->css
+?>">
+<?= $this->escape($icon->css) ?> <span class="icon-code">(0x<?= dechex($icon->code) ?>)</span>
+</div>
+<?php endforeach ?>
+</div>
diff --git a/modules/doc/application/views/scripts/style/guide.phtml b/modules/doc/application/views/scripts/style/guide.phtml
new file mode 100644
index 0000000..f2f57d2
--- /dev/null
+++ b/modules/doc/application/views/scripts/style/guide.phtml
@@ -0,0 +1,112 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+
+<div class="content styleguide">
+ <div class="section">
+ <h1>Icinga Web 2 Design Guidelines</h1>
+
+ <ul class="toc">
+ <li><a href="#headings">Headings</a></li>
+ <li><a href="#block-content">Block Content</a></li>
+ <li><a href="#tables">Tables</a></li>
+ <li><a href="#comment-list">Comment List</a></li>
+ <li><a href="#blockquote">Blockquote</a></li>
+ </ul>
+ </div>
+
+ <div class="section">
+ <h2 id="headings">Headings</h2>
+ <h1>Header h1</h1>
+ <h2>Header h2</h2>
+ <h3>Header h3</h3>
+ <h4>Header h4</h4>
+ <h5>Header h5</h5>
+ <h6>Header h6</h6>
+ </div>
+
+ <div class="section">
+ <h2 id="block-content">Block Content</h2>
+ <h3>Paragraph</h3>
+ <p>
+ This is a paragraph. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor
+ invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo
+ dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
+ A <a href="#">link inside a paragraph</a>.
+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
+ dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
+ Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
+ </p>
+ </div>
+
+ <div class="section">
+ <h2 id="tables">Tables</h2>
+ <table class="common-table">
+ <thead>
+ <tr>
+ <th>Table Head - th in thead</th>
+ <td>td in thead<td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th>Tbody - th</th>
+ <td>Tbody - td</td>
+ </tr>
+ <tr>
+ <th>Tbody - th</th>
+ <td>Tbody - td</td>
+ </tr>
+ <tr>
+ <th>Tbody - th</th>
+ <td>Tbody - td</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class="section">
+ <h2 id="comment-list"><?= $this->translate('Comment List') ?></h2>
+ <dl class="comment-list">
+ <dt>
+ John Doe
+ <span class="comment-time">
+ <?= $this->translate('commented') ?>
+ <span class="relative-time"><?= $this->translate('some time ago') ?></span>
+ </span>
+ <i class="remove-action icon-cancel"></i>
+ </dt>
+ <dd>
+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore
+ <br>
+ et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
+ </dd>
+ <dt>
+ Richard Roe
+ <span class="comment-time">
+ <?= $this->translate('commented') ?>
+ <span class="relative-time"><?= $this->translate('some time ago') ?></span>
+ </span>
+ <i class="remove-action icon-cancel"></i>
+ </dt>
+ <dd>
+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore
+ <br>
+ et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
+ </dd>
+ </dl>
+ </div>
+
+ <div class="section">
+ <h2 id="blockquote"><?= $this->translate('Blockquote') ?></h2>
+ <blockquote>
+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor
+ invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
+ At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren,
+ no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
+ consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore
+ magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
+ Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
+ </blockquote>
+ </div>
+</div>
diff --git a/modules/doc/application/views/scripts/toc.phtml b/modules/doc/application/views/scripts/toc.phtml
new file mode 100644
index 0000000..d08830b
--- /dev/null
+++ b/modules/doc/application/views/scripts/toc.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?>
+</div>
+<div class="content">
+ <?= /** @var \Icinga\Module\Doc\Renderer\DocTocRenderer $toc */ $toc ?>
+</div>
diff --git a/modules/doc/configuration.php b/modules/doc/configuration.php
new file mode 100644
index 0000000..8b909ef
--- /dev/null
+++ b/modules/doc/configuration.php
@@ -0,0 +1,24 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/** @var $this \Icinga\Application\Modules\Module */
+
+$section = $this->menuSection(N_('Documentation'), array(
+ 'title' => 'Documentation',
+ 'icon' => 'book',
+ 'url' => 'doc',
+ 'priority' => 700
+));
+
+$section->add('Icinga Web 2', array(
+ 'url' => 'doc/icingaweb/toc',
+));
+$section->add('Module documentations', array(
+ 'url' => 'doc/module',
+));
+$section->add(N_('Developer - Style'), array(
+ 'url' => 'doc/style/guide',
+ 'priority' => 790
+));
+
+$this->provideSearchUrl($this->translate('Doc'), 'doc/search', -10);
diff --git a/modules/doc/doc/01-About.md b/modules/doc/doc/01-About.md
new file mode 100644
index 0000000..02e2cbf
--- /dev/null
+++ b/modules/doc/doc/01-About.md
@@ -0,0 +1,6 @@
+# About the Doc Module <a id="doc-module-about"></a>
+
+Please read the following chapters for more insights on this module:
+
+* [Installation](02-Installation.md#doc-module-installation)
+* [Module Documentation](03-Module-Documentation.md#module-documentation)
diff --git a/modules/doc/doc/02-Installation.md b/modules/doc/doc/02-Installation.md
new file mode 100644
index 0000000..6d93d42
--- /dev/null
+++ b/modules/doc/doc/02-Installation.md
@@ -0,0 +1,15 @@
+# Doc Module Installation <a id="doc-module-installation"></a>
+
+This module is provided with the Icinga Web 2 package and does
+not need any extra installation step.
+
+## Enable the Module <a id="monitoring-module-enable"></a>
+
+Navigate to `Configuration` -> `Modules` -> `doc` and enable
+the module.
+
+You can also enable the module during the setup wizard, or on the CLI:
+
+```
+icingacli module enable doc
+```
diff --git a/modules/doc/doc/03-Module-Documentation.md b/modules/doc/doc/03-Module-Documentation.md
new file mode 100644
index 0000000..5ce4a9a
--- /dev/null
+++ b/modules/doc/doc/03-Module-Documentation.md
@@ -0,0 +1,87 @@
+# Writing Module Documentation <a id="module-documentation"></a>
+
+![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
+[Markdown Syntax Documentation](http://daringfireball.net/projects/markdown/syntax) for Markdown's formatting syntax.
+
+## Where to Put Module Documentation? <a id="module-documentation-location"></a>
+
+By default, your module's Markdown documentation files must be placed in the `doc` directory beneath your module's root
+directory, e.g.:
+
+```
+example-module/doc
+```
+
+## Chapters <a id="module-documentation-chapters"></a>
+
+Each Markdown documentation file represents a chapter of your module's documentation. The first found heading inside
+each file is the chapter's title. The order of chapters is based on the case insensitive "Natural Order" of your files'
+names. <dfn>Natural Order</dfn> means that the file names are ordered in the way which seems natural to humans.
+It is best practice to prefix Markdown documentation file names with numbers to ensure that they appear in the correct
+order, e.g.:
+
+```
+01-About.md
+02-Installation.md
+03-Configuration.md
+```
+
+## Table Of Contents <a id="module-documentation-toc"></a>
+
+The table of contents for your module's documentation is auto-generated based on all found headings inside each
+Markdown documentation file.
+
+## Linking Between Headings <a id="module-documentation-linking"></a>
+
+For linking between headings, place an anchor **after the text** where you want to link to, e.g.:
+
+```
+# Heading <a id="heading"></a> Heading
+```
+
+Please note that anchors have to be unique across all your Markdown documentation files.
+
+Now you can reference the anchor either in the same or **in another** Markdown documentation file, e.g.:
+
+```
+This is a link to [Heading](#heading).
+```
+
+Other tools support linking between headings by giving the filename plus the anchor to link to, e.g.:
+
+```
+This is a link to [About/Heading](01-About.md#heading)
+```
+
+This syntax is also supported in Icinga Web 2.
+
+## Including Images <a id="module-documentation-images"></a>
+
+Images must placed in the `doc` directory beneath your module's root directory, e.g.:
+
+```
+/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}/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.:
+
+```
+![Alt text](http://path/to/img.png "Optional Title")
+```
+
+URLs to images inside your Markdown documentation files must be specified without the base URL, e.g.:
+
+```
+![Example](img/example.png)
+```
diff --git a/modules/doc/doc/img/markdown.png b/modules/doc/doc/img/markdown.png
new file mode 100644
index 0000000..93e729b
--- /dev/null
+++ b/modules/doc/doc/img/markdown.png
Binary files differ
diff --git a/modules/doc/library/Doc/DocController.php b/modules/doc/library/Doc/DocController.php
new file mode 100644
index 0000000..0caf3ad
--- /dev/null
+++ b/modules/doc/library/Doc/DocController.php
@@ -0,0 +1,116 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc;
+
+use Icinga\Module\Doc\Renderer\DocSectionRenderer;
+use Icinga\Module\Doc\Renderer\DocTocRenderer;
+use Icinga\Web\Controller;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+
+class DocController extends Controller
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function moduleInit()
+ {
+ // Our UrlParams object does not take parameters from custom routes into account which is why we have to set
+ // them explicitly
+ 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'));
+ }
+ }
+
+ /**
+ * Render a chapter
+ *
+ * @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, $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);
+ $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' => $title,
+ 'url' => Url::fromRequest()
+ ))
+ ->extend(new OutputFormat(array(OutputFormat::TYPE_CSV, OutputFormat::TYPE_JSON)));
+ $this->render('chapter', null, true);
+ }
+
+ /**
+ * Render a toc
+ *
+ * @param string $path Path to the documentation
+ * @param string $name Name of the documentation
+ * @param string $url URL to replace links with
+ * @param array $urlParams Additional URL parameters
+ */
+ protected function renderToc($path, $name, $url, array $urlParams = array())
+ {
+ $parser = new DocParser($path);
+ $toc = new DocTocRenderer($parser->getDocTree()->getIterator());
+ $this->view->toc = $toc
+ ->setUrl($url)
+ ->setUrlParams($urlParams);
+ $name = ucfirst($name);
+ $title = sprintf($this->translate('%s Documentation'), $name);
+ $this->getTabs()
+ ->add('toc', array(
+ 'active' => true,
+ 'title' => $title,
+ 'url' => Url::fromRequest()
+ ))
+ ->extend(new OutputFormat(array(OutputFormat::TYPE_CSV, OutputFormat::TYPE_JSON)));
+ $this->render('toc', null, true);
+ }
+
+ /**
+ * Render a pdf
+ *
+ * @param string $path Path to the documentation
+ * @param string $name Name of the documentation
+ * @param string $url
+ * @param array $urlParams
+ */
+ protected function renderPdf($path, $name, $url, array $urlParams = array())
+ {
+ $parser = new DocParser($path);
+ $toc = new DocTocRenderer($parser->getDocTree()->getIterator());
+ $this->view->toc = $toc
+ ->setUrl($url)
+ ->setUrlParams($urlParams);
+ $section = new DocSectionRenderer($parser->getDocTree());
+ $this->view->section = $section
+ ->setUrl($url)
+ ->setUrlParams($urlParams);
+ $this->view->title = sprintf($this->translate('%s Documentation'), $name);
+ $this->_request->setParam('format', 'pdf');
+ $this->_helper->viewRenderer->setRender('pdf', null, true);
+ }
+}
diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
new file mode 100644
index 0000000..7ddeaa9
--- /dev/null
+++ b/modules/doc/library/Doc/DocParser.php
@@ -0,0 +1,235 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc;
+
+use CachingIterator;
+use RecursiveIteratorIterator;
+use SplFileObject;
+use SplStack;
+use Icinga\Data\Tree\SimpleTree;
+use Icinga\Exception\NotReadableError;
+use Icinga\Util\DirectoryIterator;
+use Icinga\Module\Doc\Exception\DocException;
+
+/**
+ * Parser for documentation written in Markdown
+ */
+class DocParser
+{
+ /**
+ * Internal identifier for Atx-style headers
+ *
+ * @var int
+ */
+ const HEADER_ATX = 1;
+
+ /**
+ * Internal identifier for Setext-style headers
+ *
+ * @var int
+ */
+ const HEADER_SETEXT = 2;
+
+ /**
+ * Path to the documentation
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * Iterator over documentation files
+ *
+ * @var DirectoryIterator
+ */
+ protected $docIterator;
+
+ /**
+ * Create a new documentation parser for the given path
+ *
+ * @param string $path Path to the documentation
+ *
+ * @throws DocException If the documentation directory does not exist
+ * @throws NotReadableError If the documentation directory is not readable
+ */
+ public function __construct($path)
+ {
+ if (! DirectoryIterator::isReadable($path)) {
+ throw new DocException(
+ mt('doc', 'Documentation directory \'%s\' is not readable'),
+ $path
+ );
+ }
+ $this->path = $path;
+ $this->docIterator = new DirectoryIterator($path, 'md', DirectoryIterator::FILES_FIRST);
+ }
+
+ /**
+ * Extract atx- or setext-style headers from the given lines
+ *
+ * @param string $line
+ * @param string $nextLine
+ *
+ * @return array|null An array containing the header and the header level or null if there's nothing to extract
+ */
+ protected function extractHeader($line, $nextLine)
+ {
+ if (! $line) {
+ return null;
+ }
+ $header = null;
+ if ($line
+ && $line[0] === '#'
+ && preg_match('/^#+/', $line, $match) === 1
+ ) {
+ // Atx
+ $level = strlen($match[0]);
+ $header = trim(substr($line, $level));
+ if (! $header) {
+ return null;
+ }
+ $headerStyle = static::HEADER_ATX;
+ } elseif ($nextLine
+ && ($nextLine[0] === '=' || $nextLine[0] === '-')
+ && preg_match('/^[=-]+\s*$/', $nextLine, $match) === 1
+ ) {
+ // Setext
+ $header = trim($line);
+ if (! $header) {
+ return null;
+ }
+ if ($match[0][0] === '=') {
+ $level = 1;
+ } else {
+ $level = 2;
+ }
+ $headerStyle = static::HEADER_SETEXT;
+ }
+ if ($header === null) {
+ return null;
+ }
+ if (strpos($header, '<') !== false
+ && preg_match('#(?:<(?P<tag>a|span) (?:id|name)="(?P<id>.+)"></(?P=tag)>)\s*#u', $header, $match)
+ ) {
+ $header = str_replace($match[0], '', $header);
+ $id = $match['id'];
+ } 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
+ *
+ * @return SimpleTree
+ */
+ public function getDocTree()
+ {
+ $tree = new SimpleTree();
+ foreach (new RecursiveIteratorIterator($this->docIterator) as $filename) {
+ $file = new SplFileObject($filename);
+ $file->setFlags(SplFileObject::READ_AHEAD);
+ $stack = new SplStack();
+ $cachingIterator = new CachingIterator($file);
+ $insideFencedCodeBlock = false;
+
+ for ($cachingIterator->rewind(); $cachingIterator->valid(); $cachingIterator->next()) {
+ $line = $cachingIterator->current();
+ $header = null;
+
+ if (substr($line, 0, 3) === '```') {
+ $insideFencedCodeBlock = ! $insideFencedCodeBlock;
+ } elseif (! $insideFencedCodeBlock) {
+ $fileIterator = $cachingIterator->getInnerIterator();
+ $header = $this->extractHeader($line, $fileIterator->valid() ? $fileIterator->current() : null);
+ }
+
+ if ($header !== null) {
+ list($title, $id, $level, $headerStyle) = $header;
+ while (! $stack->isEmpty() && $stack->top()->getLevel() >= $level) {
+ $stack->pop();
+ }
+ if ($id === null) {
+ $path = array();
+ foreach ($stack as $section) {
+ /** @var $section DocSection */
+ $path[] = $section->getTitle();
+ }
+ $path[] = $title;
+ $id = implode('-', $path);
+ $noFollow = true;
+ } else {
+ $noFollow = false;
+ }
+
+ $id = $this->uuid($id, $filename, $tree);
+
+ $section = new DocSection();
+ $section
+ ->setId($id)
+ ->setTitle($title)
+ ->setLevel($level)
+ ->setNoFollow($noFollow);
+ if ($stack->isEmpty()) {
+ $section->setChapter($section);
+ $tree->addChild($section);
+ } else {
+ $section->setChapter($stack->bottom());
+ $tree->addChild($section, $stack->top());
+ }
+ $stack->push($section);
+ if ($headerStyle === static::HEADER_SETEXT) {
+ $cachingIterator->next();
+ continue;
+ }
+ } else {
+ if ($stack->isEmpty()) {
+ $title = ucfirst($file->getBasename('.' . pathinfo($file->getFilename(), PATHINFO_EXTENSION)));
+ $id = $this->uuid($title, $filename, $tree);
+ $section = new DocSection();
+ $section
+ ->setId($id)
+ ->setTitle($title)
+ ->setLevel(1)
+ ->setNoFollow(true);
+ $section->setChapter($section);
+ $tree->addChild($section);
+ $stack->push($section);
+ }
+ $stack->top()->appendContent($line);
+ }
+ }
+ }
+ return $tree;
+ }
+}
diff --git a/modules/doc/library/Doc/DocSection.php b/modules/doc/library/Doc/DocSection.php
new file mode 100644
index 0000000..ce5297e
--- /dev/null
+++ b/modules/doc/library/Doc/DocSection.php
@@ -0,0 +1,159 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc;
+
+use Icinga\Data\Tree\TreeNode;
+
+/**
+ * A section of a documentation
+ */
+class DocSection extends TreeNode
+{
+ /**
+ * Chapter the section belongs to
+ *
+ * @var DocSection
+ */
+ protected $chapter;
+
+ /**
+ * Content of the section
+ *
+ * @var array
+ */
+ protected $content = array();
+
+ /**
+ * Header level
+ *
+ * @var int
+ */
+ protected $level;
+
+ /**
+ * Whether to instruct search engines to not index the link to the section
+ *
+ * @var bool
+ */
+ protected $noFollow;
+
+ /**
+ * Title of the section
+ *
+ * @var string
+ */
+ protected $title;
+
+ /**
+ * Set the chapter the section belongs to
+ *
+ * @param DocSection $section
+ *
+ * @return $this
+ */
+ public function setChapter(DocSection $section)
+ {
+ $this->chapter = $section;
+ return $this;
+ }
+
+ /**
+ * Get the chapter the section belongs to
+ *
+ * @return DocSection
+ */
+ public function getChapter()
+ {
+ return $this->chapter;
+ }
+
+ /**
+ * Append content
+ *
+ * @param string $content
+ */
+ public function appendContent($content)
+ {
+ $this->content[] = $content;
+ }
+
+ /**
+ * Get the content of the section
+ *
+ * @return array
+ */
+ public function getContent()
+ {
+ return $this->content;
+ }
+
+ /**
+ * Set the header level
+ *
+ * @param int $level Header level
+ *
+ * @return $this
+ */
+ public function setLevel($level)
+ {
+ $this->level = (int) $level;
+ return $this;
+ }
+
+ /**
+ * Get the header level
+ *
+ * @return int
+ */
+ public function getLevel()
+ {
+ return $this->level;
+ }
+
+ /**
+ * Set whether to instruct search engines to not index the link to the section
+ *
+ * @param bool $noFollow Whether to instruct search engines to not index the link to the section
+ *
+ * @return $this
+ */
+ public function setNoFollow($noFollow = true)
+ {
+ $this->noFollow = (bool) $noFollow;
+ return $this;
+ }
+
+ /**
+ * Get whether to instruct search engines to not index the link to the section
+ *
+ * @return bool
+ */
+ public function getNoFollow()
+ {
+ return $this->noFollow;
+ }
+
+ /**
+ * Set the title of the section
+ *
+ * @param string $title Title of the section
+ *
+ * @return $this
+ */
+ public function setTitle($title)
+ {
+ $this->title = (string) $title;
+ return $this;
+ }
+
+ /**
+ * Get the title of the section
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+}
diff --git a/modules/doc/library/Doc/DocSectionFilterIterator.php b/modules/doc/library/Doc/DocSectionFilterIterator.php
new file mode 100644
index 0000000..bac5a67
--- /dev/null
+++ b/modules/doc/library/Doc/DocSectionFilterIterator.php
@@ -0,0 +1,73 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc;
+
+use Countable;
+use RecursiveFilterIterator;
+use Icinga\Data\Tree\TreeNodeIterator;
+
+/**
+ * Recursive filter iterator over sections that are part of a particular chapter
+ *
+ * @method TreeNodeIterator getInnerIterator() {
+ * {@inheritdoc}
+ * }
+ */
+class DocSectionFilterIterator extends RecursiveFilterIterator implements Countable
+{
+ /**
+ * Chapter to filter for
+ *
+ * @var string
+ */
+ protected $chapter;
+
+ /**
+ * Create a new recursive filter iterator over sections that are part of a particular chapter
+ *
+ * @param TreeNodeIterator $iterator
+ * @param string $chapter The chapter to filter for
+ */
+ public function __construct(TreeNodeIterator $iterator, $chapter)
+ {
+ parent::__construct($iterator);
+ $this->chapter = $chapter;
+ }
+
+ /**
+ * Accept sections that are part of the given chapter
+ *
+ * @return bool Whether the current element of the iterator is acceptable
+ * through this filter
+ */
+ public function accept(): bool
+ {
+ $section = $this->current();
+ /** @var \Icinga\Module\Doc\DocSection $section */
+ if ($section->getChapter()->getId() === $this->chapter) {
+ return true;
+ }
+ return false;
+ }
+
+ public function getChildren(): self
+ {
+ return new static($this->getInnerIterator()->getChildren(), $this->chapter);
+ }
+
+ public function count(): int
+ {
+ return iterator_count($this);
+ }
+
+ /**
+ * Whether the filter swallowed every section
+ *
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ return $this->count() === 0;
+ }
+}
diff --git a/modules/doc/library/Doc/Exception/ChapterNotFoundException.php b/modules/doc/library/Doc/Exception/ChapterNotFoundException.php
new file mode 100644
index 0000000..7fa7807
--- /dev/null
+++ b/modules/doc/library/Doc/Exception/ChapterNotFoundException.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Exception;
+
+/**
+ * Exception thrown if a chapter was not found
+ */
+class ChapterNotFoundException extends DocException
+{
+}
diff --git a/modules/doc/library/Doc/Exception/DocException.php b/modules/doc/library/Doc/Exception/DocException.php
new file mode 100644
index 0000000..1d9e871
--- /dev/null
+++ b/modules/doc/library/Doc/Exception/DocException.php
@@ -0,0 +1,13 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Exception;
+
+use Icinga\Exception\IcingaException;
+
+/**
+ * Exception thrown if an error in the documentation module's library occurs
+ */
+class DocException extends IcingaException
+{
+}
diff --git a/modules/doc/library/Doc/Renderer/DocRenderer.php b/modules/doc/library/Doc/Renderer/DocRenderer.php
new file mode 100644
index 0000000..cb1bc39
--- /dev/null
+++ b/modules/doc/library/Doc/Renderer/DocRenderer.php
@@ -0,0 +1,208 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Renderer;
+
+use Exception;
+use Icinga\Exception\IcingaException;
+use RecursiveIteratorIterator;
+use Icinga\Application\Icinga;
+use Icinga\Web\View;
+
+/**
+ * Base class for toc and section renderer
+ */
+abstract class DocRenderer extends RecursiveIteratorIterator
+{
+ /**
+ * URL to images
+ *
+ * @var string
+ */
+ protected $imageUrl;
+
+ /**
+ * URL to replace links with
+ *
+ * @var string
+ */
+ protected $url;
+
+ /**
+ * Additional URL parameters
+ *
+ * @var array
+ */
+ protected $urlParams = array();
+
+ /**
+ * View
+ *
+ * @var View|null
+ */
+ 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
+ *
+ * @param string $url
+ *
+ * @return $this
+ */
+ public function setUrl($url)
+ {
+ $this->url = (string) $url;
+ return $this;
+ }
+
+ /**
+ * Get additional URL parameters
+ *
+ * @return array
+ */
+ public function getUrlParams()
+ {
+ return $this->urlParams;
+ }
+
+ /**
+ * Set additional URL parameters
+ *
+ * @param array $urlParams
+ *
+ * @return $this
+ */
+ public function setUrlParams(array $urlParams)
+ {
+ $this->urlParams = array_map(array($this, 'encodeUrlParam'), $urlParams);
+ return $this;
+ }
+
+ /**
+ * Get the view
+ *
+ * @return View
+ */
+ public function getView()
+ {
+ if ($this->view === null) {
+ $this->view = Icinga::app()->getViewRenderer()->view;
+ }
+ return $this->view;
+ }
+
+ /**
+ * Set the view
+ *
+ * @param View $view
+ *
+ * @return $this
+ */
+ public function setView(View $view)
+ {
+ $this->view = $view;
+ return $this;
+ }
+
+ /**
+ * Encode an anchor identifier
+ *
+ * @param string $anchor
+ *
+ * @return string
+ */
+ public static function encodeAnchor($anchor)
+ {
+ return rawurlencode($anchor);
+ }
+
+ /**
+ * Decode an anchor identifier
+ *
+ * @param string $anchor
+ *
+ * @return string
+ */
+ public static function decodeAnchor($anchor)
+ {
+ return rawurldecode($anchor);
+ }
+
+ /**
+ * Encode a URL parameter
+ *
+ * @param string $param
+ *
+ * @return string
+ */
+ public static function encodeUrlParam($param)
+ {
+ return str_replace(array('%2F','%5C'), array('%252F','%255C'), rawurlencode($param));
+ }
+
+ /**
+ * Decode a URL parameter
+ *
+ * @param string $param
+ *
+ * @return string
+ */
+ public static function decodeUrlParam($param)
+ {
+ return str_replace(array('%2F', '%5C'), array('/', '\\'), $param);
+ }
+
+ /**
+ * Render to HTML
+ *
+ * @return string
+ */
+ abstract public function render();
+
+ /**
+ * Render to HTML
+ *
+ * @return string
+ * @see \Icinga\Module\Doc\Renderer::render() For the render method.
+ */
+ public function __toString()
+ {
+ try {
+ return $this->render();
+ } catch (Exception $e) {
+ return $e->getMessage() . ': ' . IcingaException::getConfidentialTraceAsString($e);
+ }
+ }
+}
diff --git a/modules/doc/library/Doc/Renderer/DocSearchRenderer.php b/modules/doc/library/Doc/Renderer/DocSearchRenderer.php
new file mode 100644
index 0000000..c6e9ae2
--- /dev/null
+++ b/modules/doc/library/Doc/Renderer/DocSearchRenderer.php
@@ -0,0 +1,131 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Renderer;
+
+use RecursiveIteratorIterator;
+use Icinga\Module\Doc\Search\DocSearchIterator;
+use Icinga\Module\Doc\Search\DocSearchMatch;
+
+/**
+ * Renderer for doc searches
+ */
+class DocSearchRenderer extends DocRenderer
+{
+ /**
+ * The content to render
+ *
+ * @var array
+ */
+ protected $content = array();
+
+ /**
+ * Create a new renderer for doc searches
+ *
+ * @param DocSearchIterator $iterator
+ */
+ public function __construct(DocSearchIterator $iterator)
+ {
+ parent::__construct($iterator, RecursiveIteratorIterator::SELF_FIRST);
+ }
+
+ public function beginIteration(): void
+ {
+ $this->content[] = '<nav role="navigation"><ul class="toc">';
+ }
+
+ public function endIteration(): void
+ {
+ $this->content[] = '</ul></nav>';
+ }
+
+ public function beginChildren(): void
+ {
+ if ($this->getInnerIterator()->getMatches()) {
+ $this->content[] = '<ul class="toc">';
+ }
+ }
+
+ public function endChildren(): void
+ {
+ if ($this->getInnerIterator()->getMatches()) {
+ $this->content[] = '</ul>';
+ }
+ }
+
+ public function render()
+ {
+ foreach ($this as $section) {
+ if (($matches = $this->getInnerIterator()->getMatches()) === null) {
+ continue;
+ }
+ $title = $this->getView()->escape($section->getTitle());
+ $contentMatches = array();
+ foreach ($matches as $match) {
+ if ($match->getMatchType() === DocSearchMatch::MATCH_HEADER) {
+ $title = $match->highlight();
+ } else {
+ $contentMatches[] = sprintf(
+ '<p>%s</p>',
+ $match->highlight()
+ );
+ }
+ }
+ $path = $this->getView()->getHelper('Url')->url(
+ array_merge(
+ $this->getUrlParams(),
+ array(
+ 'chapter' => $this->encodeUrlParam($section->getChapter()->getId())
+ )
+ ),
+ $this->url,
+ false,
+ false
+ );
+ $url = $this->getView()->url(
+ $path,
+ array('highlight-search' => $this->getInnerIterator()->getSearch()->getInput())
+ );
+ /** @var \Icinga\Web\Url $url */
+ $url->setAnchor($this->encodeAnchor($section->getId()));
+ $urlAttributes = array(
+ 'data-base-target' => '_next',
+ 'title' => $section->getId() === $section->getChapter()->getId()
+ ? sprintf(
+ $this->getView()->translate(
+ 'Show all matches of "%s" in the chapter "%s"',
+ 'search.render.section.link'
+ ),
+ $this->getInnerIterator()->getSearch()->getInput(),
+ $section->getChapter()->getTitle()
+ )
+ : sprintf(
+ $this->getView()->translate(
+ 'Show all matches of "%s" in the section "%s" of the chapter "%s"',
+ 'search.render.section.link'
+ ),
+ $this->getInnerIterator()->getSearch()->getInput(),
+ $section->getTitle(),
+ $section->getChapter()->getTitle()
+ )
+ );
+ if ($section->getNoFollow()) {
+ $urlAttributes['rel'] = 'nofollow';
+ }
+ $this->content[] = '<li>' . $this->getView()->qlink(
+ $title,
+ $url->getAbsoluteUrl(),
+ null,
+ $urlAttributes,
+ false
+ );
+ if (! empty($contentMatches)) {
+ $this->content = array_merge($this->content, $contentMatches);
+ }
+ if (! $section->hasChildren()) {
+ $this->content[] = '</li>';
+ }
+ }
+ return implode("\n", $this->content);
+ }
+}
diff --git a/modules/doc/library/Doc/Renderer/DocSectionRenderer.php b/modules/doc/library/Doc/Renderer/DocSectionRenderer.php
new file mode 100644
index 0000000..c61dfac
--- /dev/null
+++ b/modules/doc/library/Doc/Renderer/DocSectionRenderer.php
@@ -0,0 +1,345 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Renderer;
+
+use DOMDocument;
+use DOMXPath;
+use Icinga\Module\Doc\DocSection;
+use Parsedown;
+use RecursiveIteratorIterator;
+use Icinga\Data\Tree\SimpleTree;
+use Icinga\Module\Doc\Exception\ChapterNotFoundException;
+use Icinga\Module\Doc\DocSectionFilterIterator;
+use Icinga\Module\Doc\Search\DocSearch;
+use Icinga\Module\Doc\Search\DocSearchMatch;
+use Icinga\Web\Dom\DomNodeIterator;
+use Icinga\Web\Url;
+use Icinga\Web\View;
+
+/**
+ * Section renderer
+ */
+class DocSectionRenderer extends DocRenderer
+{
+ /**
+ * Content to render
+ *
+ * @var array
+ */
+ protected $content = array();
+
+ /**
+ * Search criteria to highlight
+ *
+ * @var string
+ */
+ protected $highlightSearch;
+
+ /**
+ * Parsedown instance
+ *
+ * @var Parsedown
+ */
+ protected $parsedown;
+
+ /**
+ * Documentation tree
+ *
+ * @var SimpleTree
+ */
+ protected $tree;
+
+ /**
+ * Create a new section renderer
+ *
+ * @param SimpleTree $tree The documentation tree
+ * @param string|null $chapter If not null, the chapter to filter for
+ *
+ * @throws ChapterNotFoundException If the chapter to filter for was not found
+ */
+ public function __construct(SimpleTree $tree, $chapter = null)
+ {
+ if ($chapter !== null) {
+ $filter = new DocSectionFilterIterator($tree->getIterator(), $chapter);
+ if ($filter->isEmpty()) {
+ throw new ChapterNotFoundException(
+ mt('doc', 'Chapter %s not found'),
+ $chapter
+ );
+ }
+ parent::__construct(
+ $filter,
+ RecursiveIteratorIterator::SELF_FIRST
+ );
+ } else {
+ parent::__construct($tree->getIterator(), RecursiveIteratorIterator::SELF_FIRST);
+ }
+ $this->tree = $tree;
+ $this->parsedown = Parsedown::instance();
+ }
+
+ /**
+ * Set the search criteria to highlight
+ *
+ * @param string $highlightSearch
+ *
+ * @return $this
+ */
+ public function setHighlightSearch($highlightSearch)
+ {
+ $this->highlightSearch = $highlightSearch;
+ return $this;
+ }
+
+ /**
+ * Get the search criteria to highlight
+ *
+ * @return string
+ */
+ public function getHighlightSearch()
+ {
+ return $this->highlightSearch;
+ }
+
+ /**
+ * Syntax highlighting for PHP code
+ *
+ * @param array $match
+ *
+ * @return string
+ */
+ protected function highlightPhp($match)
+ {
+ return '<pre>' . highlight_string(htmlspecialchars_decode($match[1]), true) . '</pre>';
+ }
+
+ /**
+ * Highlight search criteria
+ *
+ * @param string $html
+ * @param DocSearch $search Search criteria
+ *
+ * @return string
+ */
+ protected function highlightSearch($html, DocSearch $search)
+ {
+ $doc = new DOMDocument();
+ @$doc->loadHTML($html);
+ $iter = new RecursiveIteratorIterator(new DomNodeIterator($doc), RecursiveIteratorIterator::SELF_FIRST);
+ foreach ($iter as $node) {
+ if ($node->nodeType !== XML_TEXT_NODE
+ || ($node->parentNode->nodeType === XML_ELEMENT_NODE && $node->parentNode->tagName === 'code')
+ ) {
+ continue;
+ }
+ $text = $node->nodeValue;
+ if (($match = $search->search($text)) === null) {
+ continue;
+ }
+ $matches = $match->getMatches();
+ ksort($matches);
+ $offset = 0;
+ $fragment = $doc->createDocumentFragment();
+ foreach ($matches as $position => $match) {
+ $fragment->appendChild($doc->createTextNode(substr($text, $offset, $position - $offset)));
+ $fragment->appendChild($doc->createElement('span', $match))
+ ->setAttribute('class', DocSearchMatch::HIGHLIGHT_CSS_CLASS);
+ $offset = $position + strlen($match);
+ }
+ $fragment->appendChild($doc->createTextNode(substr($text, $offset)));
+ $node->parentNode->replaceChild($fragment, $node);
+ }
+ // Remove <!DOCTYPE
+ $doc->removeChild($doc->doctype);
+ // Remove <html><body> and </body></html>
+ return substr($doc->saveHTML(), 12, -15);
+ }
+
+ /**
+ * Markup notes
+ *
+ * @param array $match
+ *
+ * @return string
+ */
+ protected function markupNotes($match)
+ {
+ $doc = new DOMDocument();
+ $doc->loadHTML($match[0]);
+ $xpath = new DOMXPath($doc);
+ $blockquote = $xpath->query('//blockquote[1]')->item(0);
+ /** @var \DOMElement $blockquote */
+ if (strtolower(substr(trim($blockquote->nodeValue), 0, 5)) === 'note:') {
+ $blockquote->setAttribute('class', 'note');
+ }
+ return $doc->saveXML($blockquote);
+ }
+
+ /**
+ * Replace img src tags
+ *
+ * @param $match
+ *
+ * @return string
+ */
+ protected function replaceImg($match)
+ {
+ $doc = new DOMDocument();
+ $doc->loadHTML($match[0]);
+ $xpath = new DOMXPath($doc);
+ $img = $xpath->query('//img[1]')->item(0);
+ /** @var \DOMElement $img */
+ $path = $this->getView()->getHelper('Url')->url(
+ array_merge(
+ array(
+ 'image' => trim($img->getAttribute('src'))
+ ),
+ $this->urlParams
+ ),
+ $this->imageUrl,
+ false,
+ false
+ );
+ $url = $this->getView()->url($path);
+ /** @var Url $url */
+ $img->setAttribute('src', $url->getAbsoluteUrl());
+ return substr_replace($doc->saveXML($img), '', -2, 1); // Replace '/>' with '>'
+ }
+
+ /**
+ * Replace chapter link
+ *
+ * @param array $match
+ *
+ * @return string
+ */
+ protected function replaceChapterLink($match)
+ {
+ if (($chapter = $this->tree->getNode($this->decodeAnchor($match['chapter']))) === null) {
+ return $match[0];
+ }
+ /** @var DocSection $section */
+ $path = $this->getView()->getHelper('Url')->url(
+ array_merge(
+ $this->urlParams,
+ array(
+ 'chapter' => $this->encodeUrlParam($chapter->getChapter()->getId())
+ )
+ ),
+ $this->url,
+ false,
+ false
+ );
+ $url = $this->getView()->url($path);
+ /** @var Url $url */
+ return sprintf(
+ '<a %s%shref="%s"',
+ strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '',
+ $chapter->getNoFollow() ? 'rel="nofollow" ' : '',
+ $url->getAbsoluteUrl()
+ );
+ }
+
+ /**
+ * Replace section link
+ *
+ * @param array $match
+ *
+ * @return string
+ */
+ protected function replaceSectionLink($match)
+ {
+ if (($section = $this->tree->getNode($this->decodeAnchor($match['section']))) === null) {
+ return $match[0];
+ }
+ /** @var DocSection $section */
+ $path = $this->getView()->getHelper('Url')->url(
+ array_merge(
+ $this->urlParams,
+ array(
+ 'chapter' => $this->encodeUrlParam($section->getChapter()->getId())
+ )
+ ),
+ $this->url,
+ false,
+ false
+ );
+ $url = $this->getView()->url($path);
+ /** @var Url $url */
+ $url->setAnchor($this->encodeAnchor($section->getId()));
+ return sprintf(
+ '<a %s%shref="%s"',
+ strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '',
+ $section->getNoFollow() ? 'rel="nofollow" ' : '',
+ $url->getAbsoluteUrl()
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render()
+ {
+ $search = null;
+ if (($highlightSearch = $this->getHighlightSearch()) !== null) {
+ $search = new DocSearch($highlightSearch);
+ }
+ foreach ($this as $section) {
+ $title = $section->getTitle();
+ if ($search !== null && ($match = $search->search($title)) !== null) {
+ $title = $match->highlight();
+ } else {
+ $title = $this->getView()->escape($title);
+ }
+ $number = '';
+ for ($i = 0; $i < $this->getDepth() + 1; ++$i) {
+ if ($i > 0) {
+ $number .= '.';
+ }
+ $number .= $this->getSubIterator($i)->key() + 1;
+ }
+ $this->content[] = sprintf(
+ '<a name="%1$s"></a><h%2$d>%3$s. %4$s</h%2$d>',
+ static::encodeAnchor($section->getId()),
+ $section->getLevel(),
+ $number,
+ $title
+ );
+ $html = $this->parsedown->text(implode('', $section->getContent()));
+ if (empty($html)) {
+ continue;
+ }
+ $html = preg_replace_callback(
+ '#<pre><code class="language-php">(.*?)</code></pre>#s',
+ array($this, 'highlightPhp'),
+ $html
+ );
+ $html = preg_replace_callback(
+ '/<img[^>]+>/',
+ array($this, 'replaceImg'),
+ $html
+ );
+ $html = preg_replace_callback(
+ '#<blockquote>.+?</blockquote>#ms',
+ array($this, 'markupNotes'),
+ $html
+ );
+ $html = preg_replace_callback(
+ '/<a\s+(?P<attribs>[^>]*?\s+)?href="(?:(?!http:\/\/)[^"#]*)#(?P<section>[^"]+)"/',
+ array($this, 'replaceSectionLink'),
+ $html
+ );
+ $html = preg_replace_callback(
+ '/<a\s+(?P<attribs>[^>]*?\s+)?href="(?:\d+-)?(?P<chapter>[^\/"#]+).md"/',
+ array($this, 'replaceChapterLink'),
+ $html
+ );
+ if ($search !== null) {
+ $html = $this->highlightSearch($html, $search);
+ }
+ $this->content[] = $html;
+ }
+ return implode("\n", $this->content);
+ }
+}
diff --git a/modules/doc/library/Doc/Renderer/DocTocRenderer.php b/modules/doc/library/Doc/Renderer/DocTocRenderer.php
new file mode 100644
index 0000000..09e9a1d
--- /dev/null
+++ b/modules/doc/library/Doc/Renderer/DocTocRenderer.php
@@ -0,0 +1,117 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Renderer;
+
+use Icinga\Data\Tree\TreeNodeIterator;
+use RecursiveIteratorIterator;
+
+/**
+ * TOC renderer
+ */
+class DocTocRenderer extends DocRenderer
+{
+ /**
+ * CSS class for the HTML list element
+ *
+ * @var string
+ */
+ const CSS_CLASS = 'toc';
+
+ /**
+ * Tag for the HTML list element
+ *
+ * @var string
+ */
+ const HTML_LIST_TAG = 'ol';
+
+ /**
+ * Content to render
+ *
+ * @var array
+ */
+ protected $content = array();
+
+ /**
+ * Create a new toc renderer
+ *
+ * @param TreeNodeIterator $iterator
+ */
+ public function __construct(TreeNodeIterator $iterator)
+ {
+ parent::__construct($iterator, RecursiveIteratorIterator::SELF_FIRST);
+ }
+
+ public function beginIteration(): void
+ {
+ $this->content[] = sprintf('<nav role="navigation"><%s class="%s">', static::HTML_LIST_TAG, static::CSS_CLASS);
+ }
+
+ public function endIteration(): void
+ {
+ $this->content[] = sprintf('</%s></nav>', static::HTML_LIST_TAG);
+ }
+
+ public function beginChildren(): void
+ {
+ $this->content[] = sprintf('<%s class="%s">', static::HTML_LIST_TAG, static::CSS_CLASS);
+ }
+
+ public function endChildren(): void
+ {
+ $this->content[] = sprintf('</%s>', static::HTML_LIST_TAG);
+ }
+
+ public function render()
+ {
+ if ($this->getInnerIterator()->isEmpty()) {
+ return '<p>' . mt('doc', 'Documentation is empty.') . '</p>';
+ }
+ $view = $this->getView();
+ $zendUrlHelper = $view->getHelper('Url');
+ foreach ($this as $section) {
+ $path = $zendUrlHelper->url(
+ array_merge(
+ $this->urlParams,
+ array(
+ 'chapter' => $this->encodeUrlParam($section->getChapter()->getId())
+ )
+ ),
+ $this->url,
+ false,
+ false
+ );
+ $url = $view->url($path);
+ /** @var \Icinga\Web\Url $url */
+ if ($this->getDepth() > 0) {
+ $url->setAnchor($this->encodeAnchor($section->getId()));
+ }
+ $urlAttributes = array(
+ 'data-base-target' => '_next',
+ 'title' => $section->getId() === $section->getChapter()->getId()
+ ? sprintf(
+ $view->translate('Show the chapter "%s"', 'toc.render.section.link'),
+ $section->getChapter()->getTitle()
+ )
+ : sprintf(
+ $view->translate('Show the section "%s" of the chapter "%s"', 'toc.render.section.link'),
+ $section->getTitle(),
+ $section->getChapter()->getTitle()
+ )
+ );
+ if ($section->getNoFollow()) {
+ $urlAttributes['rel'] = 'nofollow';
+ }
+ $this->content[] = '<li>' . $this->getView()->qlink(
+ $section->getTitle(),
+ $url->getAbsoluteUrl(),
+ null,
+ $urlAttributes
+ );
+ if (! $section->hasChildren()) {
+ $this->content[] = '</li>';
+ }
+ }
+ return implode("\n", $this->content);
+ }
+}
diff --git a/modules/doc/library/Doc/Search/DocSearch.php b/modules/doc/library/Doc/Search/DocSearch.php
new file mode 100644
index 0000000..20493e4
--- /dev/null
+++ b/modules/doc/library/Doc/Search/DocSearch.php
@@ -0,0 +1,95 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Search;
+
+/**
+ * Search documentation for a given search string
+ */
+class DocSearch
+{
+ /**
+ * Search string
+ *
+ * @var string
+ */
+ protected $input;
+
+ /**
+ * Search criteria
+ *
+ * @var array
+ */
+ protected $search;
+
+ /**
+ * Create a new doc search from the given search string
+ *
+ * @param string $search
+ */
+ public function __construct($search)
+ {
+ $this->input = $search = (string) $search;
+ $criteria = array();
+ if (preg_match_all('/"(?P<search>[^"]*)"/', $search, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
+ $unquoted = array();
+ $offset = 0;
+ foreach ($matches as $match) {
+ $fullMatch = $match[0];
+ $searchMatch = $match['search'];
+ $unquoted[] = substr($search, $offset, $fullMatch[1] - $offset);
+ $offset = $fullMatch[1] + strlen($fullMatch[0]);
+ if (strlen($searchMatch[0]) > 0) {
+ $criteria[] = $searchMatch[0];
+ }
+ }
+ $unquoted[] = substr($search, $offset);
+ $search = implode(' ', $unquoted);
+ }
+ $this->search = array_map(
+ 'strtolower',
+ array_unique(array_merge($criteria, array_filter(explode(' ', trim($search)))))
+ );
+ }
+
+ /**
+ * Get the search criteria
+ *
+ * @return array
+ */
+ public function getCriteria()
+ {
+ return $this->search;
+ }
+
+ /**
+ * Get the search string
+ *
+ * @return string
+ */
+ public function getInput()
+ {
+ return $this->input;
+ }
+
+ /**
+ * Search in the given line
+ *
+ * @param string $line
+ *
+ * @return DocSearchMatch|null
+ */
+ public function search($line)
+ {
+ $match = new DocSearchMatch();
+ $match->setLine($line);
+ foreach ($this->search as $criteria) {
+ $offset = 0;
+ while (($position = stripos($line, $criteria, $offset)) !== false) {
+ $match->appendMatch(substr($line, $position, strlen($criteria)), $position);
+ $offset = $position + 1;
+ }
+ }
+ return $match->isEmpty() ? null : $match;
+ }
+}
diff --git a/modules/doc/library/Doc/Search/DocSearchIterator.php b/modules/doc/library/Doc/Search/DocSearchIterator.php
new file mode 100644
index 0000000..f262b5d
--- /dev/null
+++ b/modules/doc/library/Doc/Search/DocSearchIterator.php
@@ -0,0 +1,114 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Search;
+
+use Icinga\Module\Doc\DocSection;
+use RecursiveFilterIterator;
+use RecursiveIteratorIterator;
+use Icinga\Data\Tree\TreeNodeIterator;
+
+/**
+ * Iterator over doc sections that match a given search criteria
+ */
+class DocSearchIterator extends RecursiveFilterIterator
+{
+ /**
+ * Search criteria
+ *
+ * @var DocSearch
+ */
+ protected $search;
+
+ /**
+ * Current search matches
+ *
+ * @var DocSearchMatch[]|null
+ */
+ protected $matches;
+
+ /**
+ * Create a new iterator over doc sections that match the given search criteria
+ *
+ * @param TreeNodeIterator $iterator
+ * @param DocSearch $search
+ */
+ public function __construct(TreeNodeIterator $iterator, DocSearch $search)
+ {
+ $this->search = $search;
+ parent::__construct($iterator);
+ }
+
+ /**
+ * Accept sections that match the search
+ *
+ * @return bool Whether the current element of the iterator is acceptable
+ * through this filter
+ */
+ public function accept(): bool
+ {
+ /** @var $section DocSection */
+ $section = $this->current();
+ $matches = array();
+ if (($match = $this->search->search($section->getTitle())) !== null) {
+ $matches[] = $match->setMatchType(DocSearchMatch::MATCH_HEADER);
+ }
+ foreach ($section->getContent() as $lineno => $line) {
+ if (($match = $this->search->search($line)) !== null) {
+ $matches[] = $match
+ ->setMatchType(DocSearchMatch::MATCH_CONTENT)
+ ->setLineno($lineno);
+ }
+ }
+ if (! empty($matches)) {
+ $this->matches = $matches;
+ return true;
+ }
+ if ($section->hasChildren()) {
+ $this->matches = null;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get the search criteria
+ *
+ * @return DocSearch
+ */
+ public function getSearch()
+ {
+ return $this->search;
+ }
+
+ public function getChildren(): self
+ {
+ return new static($this->getInnerIterator()->getChildren(), $this->search);
+ }
+
+ /**
+ * Whether the search did not yield any match
+ *
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ $iter = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST);
+ foreach ($iter as $section) {
+ if ($iter->getInnerIterator()->getMatches() !== null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Get current matches
+ *
+ * @return DocSearchMatch[]|null
+ */
+ public function getMatches()
+ {
+ return $this->matches;
+ }
+}
diff --git a/modules/doc/library/Doc/Search/DocSearchMatch.php b/modules/doc/library/Doc/Search/DocSearchMatch.php
new file mode 100644
index 0000000..0f21748
--- /dev/null
+++ b/modules/doc/library/Doc/Search/DocSearchMatch.php
@@ -0,0 +1,215 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Search;
+
+use UnexpectedValueException;
+use Icinga\Application\Icinga;
+use Icinga\Web\View;
+
+/**
+ * A doc search match
+ */
+class DocSearchMatch
+{
+ /**
+ * CSS class for highlighting matches
+ *
+ * @var string
+ */
+ const HIGHLIGHT_CSS_CLASS = 'search-highlight';
+
+ /**
+ * Header match
+ *
+ * @var int
+ */
+ const MATCH_HEADER = 1;
+
+ /**
+ * Content match
+ *
+ * @var int
+ */
+ const MATCH_CONTENT = 2;
+
+ /**
+ * Line
+ *
+ * @var string
+ */
+ protected $line;
+
+ /**
+ * Line number
+ *
+ * @var int
+ */
+ protected $lineno;
+
+ /**
+ * Type of the match
+ *
+ * @var int
+ */
+ protected $matchType;
+
+ /**
+ * Matches
+ *
+ * @var array
+ */
+ protected $matches = array();
+
+ /**
+ * View
+ *
+ * @var View|null
+ */
+ protected $view;
+
+ /**
+ * Set the line
+ *
+ * @param string $line
+ *
+ * @return $this
+ */
+ public function setLine($line)
+ {
+ $this->line = (string) $line;
+ return $this;
+ }
+
+ /**
+ * Get the line
+ *
+ * @return string
+ */
+ public function getLine()
+ {
+ return $this->line;
+ }
+
+ /**
+ * Set the line number
+ *
+ * @param int $lineno
+ *
+ * @return $this
+ */
+ public function setLineno($lineno)
+ {
+ $this->lineno = (int) $lineno;
+ return $this;
+ }
+
+ /**
+ * Set the match type
+ *
+ * @param int $matchType
+ *
+ * @return $this
+ */
+ public function setMatchType($matchType)
+ {
+ $matchType = (int) $matchType;
+ if ($matchType !== static::MATCH_HEADER && $matchType !== static::MATCH_CONTENT) {
+ throw new UnexpectedValueException();
+ }
+ $this->matchType = $matchType;
+ return $this;
+ }
+
+ /**
+ * Get the match type
+ *
+ * @return int
+ */
+ public function getMatchType()
+ {
+ return $this->matchType;
+ }
+
+ /**
+ * Append a match
+ *
+ * @param string $match
+ * @param int $position
+ *
+ * @return $this
+ */
+ public function appendMatch($match, $position)
+ {
+ $this->matches[(int) $position] = (string) $match;
+ return $this;
+ }
+
+ /**
+ * Get the matches
+ *
+ * @return array
+ */
+ public function getMatches()
+ {
+ return $this->matches;
+ }
+
+ /**
+ * Set the view
+ *
+ * @param View $view
+ *
+ * @return $this
+ */
+ public function setView(View $view)
+ {
+ $this->view = $view;
+ return $this;
+ }
+
+ /**
+ * Get the view
+ *
+ * @return View
+ */
+ public function getView()
+ {
+ if ($this->view === null) {
+ $this->view = Icinga::app()->getViewRenderer()->view;
+ }
+ return $this->view;
+ }
+
+ /**
+ * Get the line having matches highlighted
+ *
+ * @return string
+ */
+ public function highlight()
+ {
+ $highlighted = '';
+ $offset = 0;
+ $matches = $this->getMatches();
+ ksort($matches);
+ foreach ($matches as $position => $match) {
+ $highlighted .= $this->getView()->escape(substr($this->line, $offset, $position - $offset))
+ . '<span class="' . static::HIGHLIGHT_CSS_CLASS .'">'
+ . $this->getView()->escape($match)
+ . '</span>';
+ $offset = $position + strlen($match);
+ }
+ $highlighted .= $this->getView()->escape(substr($this->line, $offset));
+ return $highlighted;
+ }
+
+ /**
+ * Whether the match is empty
+ *
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ return empty($this->matches);
+ }
+}
diff --git a/modules/doc/module.info b/modules/doc/module.info
new file mode 100644
index 0000000..0de946d
--- /dev/null
+++ b/modules/doc/module.info
@@ -0,0 +1,4 @@
+Module: doc
+Version: 2.12.1
+Description: Documentation module
+ Extracts, shows and exports documentation for Icinga Web 2 and its modules.
diff --git a/modules/doc/public/css/module.less b/modules/doc/public/css/module.less
new file mode 100644
index 0000000..da322a2
--- /dev/null
+++ b/modules/doc/public/css/module.less
@@ -0,0 +1,120 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+// Mixins
+
+.gradient(@a: @gray-lighter; @b: @gray-lightest) {
+ background: @a;
+ background: -webkit-gradient(linear, left top, left bottom, from(@a), to(@b));
+ background: -webkit-linear-gradient(top, @a, @b);
+ background: -moz-linear-gradient(top, @a, @b);
+ background: -ms-linear-gradient(top, @a, @b);
+ background: -o-linear-gradient(top, @a, @b);
+ background: linear-gradient(to bottom, @a, @b);
+}
+
+// General styles
+
+code {
+ color: @icinga-blue;
+ font-family: @font-family-fixed;
+}
+
+pre > code {
+ color: inherit;
+}
+
+.chapter a {
+ border-bottom: 1px dotted @gray-light;
+ font-weight: @font-weight-bold;
+
+ &:hover {
+ border-bottom: 1px solid @text-color;
+ text-decoration: none;
+ }
+}
+
+.content {
+ font-size: 1.167em;
+}
+
+.search-highlight {
+ .rounded-corners();
+
+ background: @icinga-blue;
+ color: @text-color-on-icinga-blue;
+ padding: 0 0.3em 0 0.3em;
+}
+
+.toc {
+ counter-reset: li;
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+
+ li {
+ counter-increment: li;
+ margin-top: 0.25em;
+
+ > .toc {
+ margin-left: 2em;
+ }
+
+ a {
+ &:before {
+ color: @icinga-blue;
+ content: counters(li,".") " ";
+ display: inline-block;
+ font-size: small;
+ font-weight: @font-weight-bold;
+ min-width: 1.5em;
+ padding: 0.25em;
+ text-align: center;
+ }
+
+ display: block;
+ }
+ }
+}
+
+// Table styles
+
+table {
+ margin-bottom: 1em;
+ width: 100%;
+}
+
+tbody > tr:nth-child(odd) {
+ .gradient()
+}
+
+tbody > tr:nth-child(even) {
+ background: @body-bg-color;
+}
+
+td, th {
+ padding: 0.5em;
+}
+
+td {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+th {
+ border-bottom: 2px solid @icinga-blue;
+ font-weight: @font-weight-bold;
+ text-align: left;
+ text-transform: uppercase;
+}
+
+.icon {
+ width: 33%;
+ font-size: 1.5em;
+ height: 2em;
+ float: left;
+}
+
+.icon-code {
+ font-size: 0.6em;
+}
diff --git a/modules/doc/public/js/module.js b/modules/doc/public/js/module.js
new file mode 100644
index 0000000..d5571ee
--- /dev/null
+++ b/modules/doc/public/js/module.js
@@ -0,0 +1,30 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+(function(Icinga) {
+
+ var Doc = function(module) {
+ this.module = module;
+ this.initialize();
+ this.module.icinga.logger.debug('Doc module loaded');
+ };
+
+ Doc.prototype = {
+
+ initialize: function()
+ {
+ this.module.on('rendered', this.rendered);
+ this.module.icinga.logger.debug('Doc module initialized');
+ },
+
+ rendered: function(event) {
+ var $container = $(event.currentTarget);
+ if ($('> .content.styleguide', $container).length) {
+ $container.removeClass('module-doc');
+ }
+ }
+ };
+
+ Icinga.availableModules.doc = Doc;
+
+}(Icinga));
+
diff --git a/modules/doc/run.php b/modules/doc/run.php
new file mode 100644
index 0000000..df9dd09
--- /dev/null
+++ b/modules/doc/run.php
@@ -0,0 +1,64 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+use Icinga\Application\Icinga;
+
+if (Icinga::app()->isCli()) {
+ return;
+}
+
+$docModuleChapter = new Zend_Controller_Router_Route(
+ 'doc/module/:moduleName/chapter/:chapter',
+ array(
+ 'controller' => 'module',
+ 'action' => 'chapter',
+ 'module' => 'doc'
+ )
+);
+
+$docIcingaWebChapter = new Zend_Controller_Router_Route(
+ 'doc/icingaweb/chapter/:chapter',
+ array(
+ 'controller' => 'icingaweb',
+ 'action' => 'chapter',
+ 'module' => 'doc'
+ )
+);
+
+$docModuleToc = new Zend_Controller_Router_Route(
+ 'doc/module/:moduleName/toc',
+ array(
+ 'controller' => 'module',
+ 'action' => 'toc',
+ 'module' => 'doc'
+ )
+);
+
+$docModulePdf = new Zend_Controller_Router_Route(
+ 'doc/module/:moduleName/pdf',
+ array(
+ 'controller' => 'module',
+ 'action' => 'pdf',
+ 'module' => 'doc'
+ )
+);
+
+$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/migrate/application/clicommands/ConfigCommand.php b/modules/migrate/application/clicommands/ConfigCommand.php
new file mode 100644
index 0000000..a5be144
--- /dev/null
+++ b/modules/migrate/application/clicommands/ConfigCommand.php
@@ -0,0 +1,119 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Migrate\Clicommands;
+
+use Icinga\Cli\Command;
+use Icinga\Module\Migrate\Config\UserDomainMigration;
+use Icinga\User;
+use Icinga\Util\StringHelper;
+
+class ConfigCommand extends Command
+{
+ /**
+ * Rename users and user configurations according to a given domain
+ *
+ * The following configurations are taken into account:
+ * - Announcements
+ * - Preferences
+ * - Dashboards
+ * - Custom navigation items
+ * - Role configuration
+ * - Users and group memberships in database backends, if configured
+ *
+ * USAGE:
+ *
+ * icingacli migrate config users [options]
+ *
+ * OPTIONS:
+ *
+ * --to-domain=<to-domain> The new domain for the users
+ *
+ * --from-domain=<from-domain> Migrate only the users with the given domain.
+ * Use this switch in combination with --to-domain.
+ *
+ * --user=<user> Migrate only the given user in the format <user> or <user@domain>
+ *
+ * --map-file=<mapfile> File to use for renaming users
+ *
+ * --separator=<separator> Separator for the map file
+ *
+ * EXAMPLES:
+ *
+ * icingacli migrate config users ...
+ *
+ * Add the domain "icinga.com" to all users:
+ *
+ * --to-domain icinga.com
+ *
+ * Set the domain "example.com" on all users that have the domain "icinga.com":
+ *
+ * --to-domain example.com --from-domain icinga.com
+ *
+ * Set the domain "icinga.com" on the user "icingaadmin":
+ *
+ * --to-domain icinga.com --user icingaadmin
+ *
+ * Set the domain "icinga.com" on the users "icingaadmin@icinga.com"
+ *
+ * --to-domain example.com --user icingaadmin@icinga.com
+ *
+ * Rename users according to a map file:
+ *
+ * --map-file /path/to/mapfile --separator :
+ *
+ * MAPFILE:
+ *
+ * You may rename users according to a given map file. The map file must be separated by newlines. Each line then
+ * is specified in the format <from><separator><to>. The separator is specified with the --separator switch.
+ *
+ * Example content:
+ *
+ * icingaadmin:icingaadmin@icinga.com
+ * jdoe@example.com:jdoe@icinga.com
+ * rroe@icinga:rroe@icinga.com
+ */
+ public function usersAction()
+ {
+ if ($this->params->has('map-file')) {
+ $mapFile = $this->params->get('map-file');
+ $separator = $this->params->getRequired('separator');
+
+ $source = trim(file_get_contents($mapFile));
+ $source = StringHelper::trimSplit($source, "\n");
+
+ $map = array();
+
+ array_walk($source, function ($item) use ($separator, &$map) {
+ list($from, $to) = StringHelper::trimSplit($item, $separator, 2);
+ $map[$from] = $to;
+ });
+
+ $migration = UserDomainMigration::fromMap($map);
+ } else {
+ $toDomain = $this->params->getRequired('to-domain');
+ $fromDomain = $this->params->get('from-domain');
+ $user = $this->params->get('user');
+
+ if ($user === null) {
+ $migration = UserDomainMigration::fromDomains($toDomain, $fromDomain);
+ } else {
+ if ($fromDomain !== null) {
+ $this->fail(
+ "Ambiguous arguments: Can't use --user in combination with --from-domain."
+ . " Please use the user@domain syntax for the --user switch instead."
+ );
+ }
+
+ $user = new User($user);
+
+ $migrated = clone $user;
+ $migrated->setDomain($toDomain);
+
+ $migration = UserDomainMigration::fromMap(array($user->getUsername() => $migrated->getUsername()));
+ }
+ }
+
+ $migration->migrate();
+ }
+}
diff --git a/modules/migrate/application/clicommands/NavigationCommand.php b/modules/migrate/application/clicommands/NavigationCommand.php
new file mode 100644
index 0000000..28ae8a1
--- /dev/null
+++ b/modules/migrate/application/clicommands/NavigationCommand.php
@@ -0,0 +1,20 @@
+<?php
+
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Migrate\Clicommands;
+
+use Icinga\Application\Logger;
+use Icinga\Cli\Command;
+
+class NavigationCommand extends Command
+{
+ /**
+ * Deprecated. Use `icingacli icingadb migrate navigation` instead.
+ */
+ public function indexAction(): void
+ {
+ Logger::error('Deprecated. Use `icingacli icingadb migrate navigation` instead.');
+ exit(1);
+ }
+}
diff --git a/modules/migrate/application/clicommands/PreferencesCommand.php b/modules/migrate/application/clicommands/PreferencesCommand.php
new file mode 100644
index 0000000..11d1edb
--- /dev/null
+++ b/modules/migrate/application/clicommands/PreferencesCommand.php
@@ -0,0 +1,131 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Migrate\Clicommands;
+
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Cli\Command;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\NotReadableError;
+use Icinga\Exception\NotWritableError;
+use Icinga\File\Ini\IniParser;
+use Icinga\User;
+use Icinga\User\Preferences\PreferencesStore;
+use Icinga\Util\DirectoryIterator;
+
+class PreferencesCommand extends Command
+{
+ /**
+ * Migrate local INI user preferences to a database
+ *
+ * USAGE
+ *
+ * icingacli migrate preferences [options]
+ *
+ * OPTIONS:
+ *
+ * --resource=<resource-name> The resource to use, if no current database config backend is configured.
+ * --no-set-config-backend Do not set the given resource as config backend automatically
+ */
+ public function indexAction()
+ {
+ $resource = Config::app()->get('global', 'config_resource');
+ if (empty($resource)) {
+ $resource = $this->params->getRequired('resource');
+ }
+
+ $resourceConfig = ResourceFactory::getResourceConfig($resource);
+ if ($resourceConfig->db === 'mysql') {
+ $resourceConfig->charset = 'utf8mb4';
+ }
+
+ $connection = ResourceFactory::createResource($resourceConfig);
+
+ $preferencesPath = Config::resolvePath('preferences');
+ if (! file_exists($preferencesPath)) {
+ Logger::info('There are no local user preferences to migrate');
+ return;
+ }
+
+ $rc = 0;
+
+ $preferenceDirs = new DirectoryIterator($preferencesPath);
+ foreach ($preferenceDirs as $preferenceDir) {
+ if (! is_dir($preferenceDir)) {
+ continue;
+ }
+
+ $userName = basename($preferenceDir);
+
+ Logger::info('Migrating INI preferences for user "%s" to database...', $userName);
+
+ $dbStore = new PreferencesStore(new ConfigObject(['connection' => $connection]), new User($userName));
+
+ try {
+ $dbStore->load();
+ $dbStore->save(
+ new User\Preferences(
+ $this->loadIniFile($preferencesPath, (new User($userName))->getUsername())
+ )
+ );
+ } catch (NotReadableError $e) {
+ if ($e->getPrevious() !== null) {
+ Logger::error('%s: %s', $e->getMessage(), $e->getPrevious()->getMessage());
+ } else {
+ Logger::error($e->getMessage());
+ }
+
+ $rc = 128;
+ } catch (NotWritableError $e) {
+ Logger::error('%s: %s', $e->getMessage(), $e->getPrevious()->getMessage());
+ $rc = 256;
+ }
+ }
+
+ if ($rc > 0) {
+ Logger::error('Failed to migrate some user preferences');
+ exit($rc);
+ }
+
+ if ($this->params->has('resource') && ! $this->params->has('no-set-config-backend')) {
+ $appConfig = Config::app();
+ $globalConfig = $appConfig->getSection('global');
+ $globalConfig['config_resource'] = $resource;
+
+ try {
+ $appConfig->saveIni();
+ } catch (NotWritableError $e) {
+ Logger::error('Failed to update general configuration: %s', $e->getMessage());
+ exit(256);
+ }
+ }
+
+ Logger::info('Successfully migrated all local user preferences to database');
+ }
+
+ private function loadIniFile(string $filePath, string $username): array
+ {
+ $preferences = [];
+ $preferencesFile = sprintf(
+ '%s/%s/config.ini',
+ $filePath,
+ strtolower($username)
+ );
+
+ if (file_exists($preferencesFile)) {
+ if (! is_readable($preferencesFile)) {
+ throw new NotReadableError(
+ 'Preferences INI file %s for user %s is not readable',
+ $preferencesFile,
+ $username
+ );
+ } else {
+ $preferences = IniParser::parseIniFile($preferencesFile)->toArray();
+ }
+ }
+
+ return $preferences;
+ }
+}
diff --git a/modules/migrate/library/Migrate/Config/UserDomainMigration.php b/modules/migrate/library/Migrate/Config/UserDomainMigration.php
new file mode 100644
index 0000000..855a0ab
--- /dev/null
+++ b/modules/migrate/library/Migrate/Config/UserDomainMigration.php
@@ -0,0 +1,378 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Migrate\Config;
+
+use Icinga\Application\Config;
+use Icinga\Data\Db\DbConnection;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\ResourceFactory;
+use Icinga\User;
+use Icinga\Util\DirectoryIterator;
+use Icinga\Util\StringHelper;
+use Icinga\Web\Announcement\AnnouncementIniRepository;
+
+class UserDomainMigration
+{
+ protected $toDomain;
+
+ protected $fromDomain;
+
+ protected $map;
+
+ public static function fromMap(array $map)
+ {
+ $static = new static();
+
+ $static->map = $map;
+
+ return $static;
+ }
+
+ public static function fromDomains($toDomain, $fromDomain = null)
+ {
+ $static = new static();
+
+ $static->toDomain = $toDomain;
+ $static->fromDomain = $fromDomain;
+
+ return $static;
+ }
+
+ protected function mustMigrate(User $user)
+ {
+ if ($user->getUsername() === '*') {
+ return false;
+ }
+
+ if ($this->map !== null) {
+ return isset($this->map[$user->getUsername()]);
+ }
+
+ if ($this->fromDomain !== null && $user->hasDomain() && $user->getDomain() !== $this->fromDomain) {
+ return false;
+ }
+
+ return true;
+ }
+
+ protected function migrateUser(User $user)
+ {
+ $migrated = clone $user;
+
+ if ($this->map !== null) {
+ $migrated->setUsername($this->map[$user->getUsername()]);
+ } else {
+ $migrated->setDomain($this->toDomain);
+ }
+
+ return $migrated;
+ }
+
+ protected function migrateAnnounces()
+ {
+ $announces = new AnnouncementIniRepository();
+
+ $query = $announces->select(array('author'));
+
+ if ($this->map !== null) {
+ $query->where('author', array_keys($this->map));
+ }
+
+ $migratedUsers = array();
+
+ foreach ($announces->select(array('author')) as $announce) {
+ $user = new User($announce->author);
+
+ if (! $this->mustMigrate($user)) {
+ continue;
+ }
+
+ if (isset($migratedUsers[$user->getUsername()])) {
+ continue;
+ }
+
+ $migrated = $this->migrateUser($user);
+
+ $announces->update(
+ 'announcement',
+ array('author' => $migrated->getUsername()),
+ Filter::where('author', $user->getUsername())
+ );
+
+ $migratedUsers[$user->getUsername()] = true;
+ }
+ }
+
+ protected function migrateDashboards()
+ {
+ $directory = Config::resolvePath('dashboards');
+
+ $migration = array();
+
+ if (DirectoryIterator::isReadable($directory)) {
+ foreach (new DirectoryIterator($directory) as $username => $path) {
+ $user = new User($username);
+
+ if (! $this->mustMigrate($user)) {
+ continue;
+ }
+
+ $migrated = $this->migrateUser($user);
+
+ $migration[$path] = dirname($path) . '/' . $migrated->getUsername();
+ }
+
+ foreach ($migration as $from => $to) {
+ rename($from, $to);
+ }
+ }
+ }
+
+ protected function migrateNavigation()
+ {
+ $directory = Config::resolvePath('navigation');
+
+ foreach (new DirectoryIterator($directory, 'ini') as $file) {
+ $config = Config::fromIni($file);
+
+ foreach ($config as $navigation) {
+ $owner = $navigation->owner;
+
+ if (! empty($owner)) {
+ $user = new User($owner);
+
+ if ($this->mustMigrate($user)) {
+ $migrated = $this->migrateUser($user);
+
+ $navigation->owner = $migrated->getUsername();
+ }
+ }
+
+ $users = $navigation->users;
+
+ if (! empty($users)) {
+ $users = StringHelper::trimSplit($users);
+
+ foreach ($users as &$username) {
+ $user = new User($username);
+
+ if (! $this->mustMigrate($user)) {
+ continue;
+ }
+
+ $migrated = $this->migrateUser($user);
+
+ $username = $migrated->getUsername();
+ }
+
+ $navigation->users = implode(',', $users);
+ }
+ }
+
+ $config->saveIni();
+ }
+ }
+
+ protected function migratePreferences()
+ {
+ $config = Config::app();
+
+ $resourceConfig = ResourceFactory::getResourceConfig($config->get('global', 'config_resource'));
+ if ($resourceConfig->db === 'mysql') {
+ $resourceConfig->charset = 'utf8mb4';
+ }
+
+ /** @var DbConnection $conn */
+ $conn = ResourceFactory::createResource($resourceConfig);
+
+ $query = $conn
+ ->select()
+ ->from('icingaweb_user_preference', array('username'))
+ ->group('username');
+
+ if ($this->map !== null) {
+ $query->applyFilter(Filter::matchAny(Filter::where('username', array_keys($this->map))));
+ }
+
+ $users = $query->fetchColumn();
+
+ $migration = array();
+
+ foreach ($users as $username) {
+ $user = new User($username);
+
+ if (! $this->mustMigrate($user)) {
+ continue;
+ }
+
+ $migrated = $this->migrateUser($user);
+
+ $migration[$username] = $migrated->getUsername();
+ }
+
+ if (! empty($migration)) {
+ $conn->getDbAdapter()->beginTransaction();
+
+ foreach ($migration as $originalUsername => $username) {
+ $conn->update(
+ 'icingaweb_user_preference',
+ array('username' => $username),
+ Filter::where('username', $originalUsername)
+ );
+ }
+
+ $conn->getDbAdapter()->commit();
+ }
+ }
+
+ protected function migrateRoles()
+ {
+ $roles = Config::app('roles');
+
+ foreach ($roles as $role) {
+ $users = $role->users;
+
+ if (empty($users)) {
+ continue;
+ }
+
+ $users = StringHelper::trimSplit($users);
+
+ foreach ($users as &$username) {
+ $user = new User($username);
+
+ if (! $this->mustMigrate($user)) {
+ continue;
+ }
+
+ $migrated = $this->migrateUser($user);
+
+ $username = $migrated->getUsername();
+ }
+
+ $role->users = implode(',', $users);
+ }
+
+ $roles->saveIni();
+ }
+
+ protected function migrateUsers()
+ {
+ foreach (Config::app('authentication') as $name => $config) {
+ if (strtolower($config->backend) !== 'db') {
+ continue;
+ }
+
+ $resourceConfig = ResourceFactory::getResourceConfig($config->resource);
+ if ($resourceConfig->db === 'mysql') {
+ $resourceConfig->charset = 'utf8mb4';
+ }
+
+ /** @var DbConnection $conn */
+ $conn = ResourceFactory::createResource($resourceConfig);
+
+ $query = $conn
+ ->select()
+ ->from('icingaweb_user', array('name'))
+ ->group('name');
+
+ if ($this->map !== null) {
+ $query->applyFilter(Filter::matchAny(Filter::where('name', array_keys($this->map))));
+ }
+
+ $users = $query->fetchColumn();
+
+ $migration = array();
+
+ foreach ($users as $username) {
+ $user = new User($username);
+
+ if (! $this->mustMigrate($user)) {
+ continue;
+ }
+
+ $migrated = $this->migrateUser($user);
+
+ $migration[$username] = $migrated->getUsername();
+ }
+
+ if (! empty($migration)) {
+ $conn->getDbAdapter()->beginTransaction();
+
+ foreach ($migration as $originalUsername => $username) {
+ $conn->update(
+ 'icingaweb_user',
+ array('name' => $username),
+ Filter::where('name', $originalUsername)
+ );
+ }
+
+ $conn->getDbAdapter()->commit();
+ }
+ }
+
+ foreach (Config::app('groups') as $name => $config) {
+ if (strtolower($config->backend) !== 'db') {
+ continue;
+ }
+
+ $resourceConfig = ResourceFactory::getResourceConfig($config->resource);
+ if ($resourceConfig->db === 'mysql') {
+ $resourceConfig->charset = 'utf8mb4';
+ }
+
+ /** @var DbConnection $conn */
+ $conn = ResourceFactory::createResource($resourceConfig);
+
+ $query = $conn
+ ->select()
+ ->from('icingaweb_group_membership', array('username'))
+ ->group('username');
+
+ if ($this->map !== null) {
+ $query->applyFilter(Filter::matchAny(Filter::where('username', array_keys($this->map))));
+ }
+
+ $users = $query->fetchColumn();
+
+ $migration = array();
+
+ foreach ($users as $username) {
+ $user = new User($username);
+
+ if (! $this->mustMigrate($user)) {
+ continue;
+ }
+
+ $migrated = $this->migrateUser($user);
+
+ $migration[$username] = $migrated->getUsername();
+ }
+
+ if (! empty($migration)) {
+ $conn->getDbAdapter()->beginTransaction();
+
+ foreach ($migration as $originalUsername => $username) {
+ $conn->update(
+ 'icingaweb_group_membership',
+ array('username' => $username),
+ Filter::where('username', $originalUsername)
+ );
+ }
+
+ $conn->getDbAdapter()->commit();
+ }
+ }
+ }
+
+ public function migrate()
+ {
+ $this->migrateAnnounces();
+ $this->migrateDashboards();
+ $this->migrateNavigation();
+ $this->migratePreferences();
+ $this->migrateRoles();
+ $this->migrateUsers();
+ }
+}
diff --git a/modules/migrate/module.info b/modules/migrate/module.info
new file mode 100644
index 0000000..fb6055b
--- /dev/null
+++ b/modules/migrate/module.info
@@ -0,0 +1,5 @@
+Module: migrate
+Version: 2.12.1
+Description: Migrate module
+ This module was introduced with the domain-aware authentication feature in version 2.5.0.
+ It helps you migrating users and user configurations according to a given domain.
diff --git a/modules/monitoring/application/clicommands/ListCommand.php b/modules/monitoring/application/clicommands/ListCommand.php
new file mode 100644
index 0000000..6dc4193
--- /dev/null
+++ b/modules/monitoring/application/clicommands/ListCommand.php
@@ -0,0 +1,400 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Clicommands;
+
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Module\Monitoring\Cli\CliUtils;
+use Icinga\Date\DateFormatter;
+use Icinga\Cli\Command;
+use Icinga\File\Csv;
+use Icinga\Module\Monitoring\Plugin\PerfdataSet;
+use Exception;
+use Icinga\Util\Json;
+
+/**
+ * Icinga monitoring objects
+ *
+ * This module is your interface to the Icinga monitoring application.
+ */
+class ListCommand extends Command
+{
+ protected $backend;
+ protected $dumpSql;
+ protected $defaultActionName = 'status';
+
+ public function init()
+ {
+ $this->backend = MonitoringBackend::instance($this->params->shift('backend'));
+ $this->dumpSql = $this->params->shift('showsql');
+ }
+
+ protected function getQuery($table, $columns)
+ {
+ $limit = $this->params->shift('limit');
+ $format = $this->params->shift('format');
+ if ($format !== null) {
+ if ($this->params->has('columns')) {
+ $columnParams = preg_split(
+ '/,/',
+ $this->params->shift('columns')
+ );
+ $columns = array();
+ foreach ($columnParams as $col) {
+ if (false !== ($pos = strpos($col, '='))) {
+ $columns[substr($col, 0, $pos)] = substr($col, $pos + 1);
+ } else {
+ $columns[] = $col;
+ }
+ }
+ }
+ }
+
+ $query = $this->backend->select()->from($table, $columns);
+ if ($limit) {
+ $query->limit($limit, $this->params->shift('offset'));
+ }
+ foreach ($this->params->getParams() as $col => $filter) {
+ $query->where($col, $filter);
+ }
+ // $query->applyFilters($this->params->getParams());
+ if ($this->dumpSql) {
+ echo wordwrap($query->dump(), 72);
+ exit;
+ }
+
+ if ($format !== null) {
+ $this->showFormatted($query, $format, $columns);
+ }
+
+ return $query;
+ }
+
+ protected function showFormatted($query, $format, $columns)
+ {
+ $query = $query->getQuery();
+ switch ($format) {
+ case 'json':
+ echo Json::sanitize($query->fetchAll());
+ break;
+ case 'csv':
+ Csv::fromQuery($query)->dump();
+ break;
+ default:
+ preg_match_all('~\$([a-z0-9_-]+)\$~', $format, $m);
+ $words = array();
+ foreach ($columns as $key => $col) {
+ if (is_numeric($key)) {
+ if (in_array($col, $m[1])) {
+ $words[] = $col;
+ }
+ } else {
+ if (in_array($key, $m[1])) {
+ $words[] = $key;
+ }
+ }
+ }
+ foreach ($query->fetchAll() as $row) {
+ $output = $format;
+ foreach ($words as $word) {
+ $output = preg_replace(
+ '~\$' . $word . '\$~',
+ $row->{$word},
+ $output
+ );
+ }
+ echo $output . "\n";
+ }
+ }
+ exit;
+ }
+
+ /**
+ * List and filter hosts
+ *
+ * This command allows you to search and visualize your hosts in
+ * different ways.
+ *
+ * USAGE
+ *
+ * icingacli monitoring list hosts [options]
+ *
+ * OPTIONS
+ *
+ * --verbose Show detailled output
+ * --showsql Dump generated SQL query (DB backend only)
+ *
+ * --format=<csv|json|<custom>>
+ * Dump columns in the given format. <custom> format allows $column$
+ * placeholders, e.g. --format='$host$: $service$'. This requires
+ * that the columns are specified within the --columns parameter.
+ *
+ * --<column>[=filter]
+ * Filter given column by optional filter. Boolean (1/0) columns are
+ * true if no filter value is given.
+ *
+ * --problems
+ * Only show unhandled problems (HARD state and not acknowledged/in downtime).
+ *
+ * --columns='<comma separated list of host/service columns>'
+ * Add a limited set of columns to the output. The following host
+ * attributes can be fetched: state, handled, output, acknowledged, in_downtime, perfdata last_state_change
+ *
+ * EXAMPLES
+ *
+ * icingacli monitoring list hosts --problems
+ * icingacli monitoring list hosts --problems --host_state_type 0
+ * icingacli monitoring list hosts --host=local*
+ * icingacli monitoring list hosts --columns 'host,host_output' \
+ * --format='$host$ ($host_output$)'
+ */
+ public function hostsAction()
+ {
+ $columns = array(
+ 'host_name',
+ 'host_state',
+ 'host_output',
+ 'host_handled',
+ 'host_acknowledged',
+ 'host_in_downtime'
+ );
+ $query = $this->getQuery('hoststatus', $columns)
+ ->order('host_name');
+ echo $this->renderStatusQuery($query);
+ }
+
+ /**
+ * List and filter services
+ *
+ * This command allows you to search and visualize your services in
+ * different ways.
+ *
+ * USAGE
+ *
+ * icingacli monitoring list services [options]
+ *
+ * OPTIONS
+ *
+ * --verbose Show detailled output
+ * --showsql Dump generated SQL query (DB backend only)
+ *
+ * --format=<csv|json|<custom>>
+ * Dump columns in the given format. <custom> format allows $column$
+ * placeholders, e.g. --format='$host$: $service$'. This requires
+ * that the columns are specified within the --columns parameter.
+ *
+ * --<column>[=filter]
+ * Filter given column by optional filter. Boolean (1/0) columns are
+ * true if no filter value is given.
+ *
+ * --problems
+ * Only show unhandled problems (HARD state and not acknowledged/in downtime).
+ *
+ * --columns='<comma separated list of host/service columns>'
+ * Add a limited set of columns to the output. The following service
+ * attributes can be fetched: state, handled, output, acknowledged, in_downtime, perfdata last_state_change
+ *
+ * EXAMPLES
+ *
+ * icingacli monitoring list services --problems
+ * icingacli monitoring list services --problems --service_state_type 0
+ * icingacli monitoring list services --host=local* --service=*disk*
+ * icingacli monitoring list services --columns 'host,service,service_output' \
+ * --format='$host$: $service$ ($service_output$)'
+ */
+ public function servicesAction()
+ {
+ $columns = array(
+ 'host_name',
+ 'host_state',
+ 'host_output',
+ 'host_handled',
+ 'host_acknowledged',
+ 'host_in_downtime',
+ 'service_description',
+ 'service_state',
+ 'service_acknowledged',
+ 'service_in_downtime',
+ 'service_handled',
+ 'service_output',
+ 'service_perfdata',
+ 'service_last_state_change'
+ );
+ $query = $this->getQuery('servicestatus', $columns)
+ ->order('host_name');
+ echo $this->renderStatusQuery($query);
+ }
+
+ protected function renderStatusQuery($query)
+ {
+ $out = '';
+ $last_host = null;
+ $screen = $this->screen;
+ $utils = new CliUtils($screen);
+ $maxCols = $screen->getColumns();
+ $query = $query->getQuery();
+ $rows = $query->fetchAll();
+ $count = $query->count();
+ $count = count($rows);
+
+ for ($i = 0; $i < $count; $i++) {
+ $row = & $rows[$i];
+
+ $utils->setHostState($row->host_state);
+ if (! array_key_exists($i + 1, $rows)
+ || $row->host_name !== $rows[$i + 1]->host_name
+ ) {
+ $lastService = true;
+ } else {
+ $lastService = false;
+ }
+
+ $hostUnhandled = ! ($row->host_state == 0 || $row->host_handled);
+
+ if ($row->host_name !== $last_host) {
+ if (isset($row->service_description)) {
+ $out .= "\n";
+ }
+
+ $hostTxt = $utils->shortHostState();
+ if ($hostUnhandled) {
+ $out .= $utils->hostStateBackground(
+ sprintf(' %s ', $utils->shortHostState())
+ );
+ } else {
+ $out .= sprintf(
+ '%s %s ',
+ $utils->hostStateBackground(' '),
+ $utils->shortHostState()
+ );
+ }
+ $out .= sprintf(
+ " %s%s: %s\n",
+ $screen->underline($row->host_name),
+ $screen->colorize($utils->objectStateFlags('host', $row), 'lightblue'),
+ $row->host_output
+ );
+
+ if (isset($row->services_ok)) {
+ $out .= sprintf(
+ "%d services, %d problems (%d unhandled), %d OK\n",
+ $row->services_cnt,
+ $row->services_problem,
+ $row->services_problem_unhandled,
+ $row->services_ok
+ );
+ }
+ }
+
+ $last_host = $row->host_name;
+ if (! isset($row->service_description)) {
+ continue;
+ }
+
+ $utils->setServiceState($row->service_state);
+ $serviceUnhandled = ! (
+ $row->service_state == 0 || $row->service_handled
+ );
+
+ if ($lastService) {
+ $straight = ' ';
+ $leaf = '└';
+ } else {
+ $straight = '│';
+ $leaf = '├';
+ }
+ $out .= $utils->hostStateBackground(' ');
+
+ if ($serviceUnhandled) {
+ $out .= $utils->serviceStateBackground(
+ sprintf(' %s ', $utils->shortServiceState())
+ );
+ $emptyBg = ' ';
+ $emptySpace = '';
+ } else {
+ $out .= sprintf(
+ '%s %s ',
+ $utils->serviceStateBackground(' '),
+ $utils->shortServiceState()
+ );
+ $emptyBg = ' ';
+ $emptySpace = ' ';
+ }
+
+ $emptyLine = "\n"
+ . $utils->hostStateBackground(' ')
+ . $utils->serviceStateBackground($emptyBg)
+ . $emptySpace
+ . ' ' . $straight . ' ';
+
+ $perf = '';
+ try {
+ $pset = PerfdataSet::fromString($row->service_perfdata);
+ $perfs = array();
+ foreach ($pset as $p) {
+ if ($percent = $p->getPercentage()) {
+ if ($percent < 0 || $percent > 100) {
+ continue;
+ }
+ $perfs[] = ' '
+ . $p->getLabel()
+ . ': '
+ . $this->getPercentageSign($percent)
+ . ' '
+ . number_format($percent, 2, ',', '.')
+ . '%';
+ }
+ }
+ if (! empty($perfs)) {
+ $perf = ', ' . implode($perfs);
+ }
+ // TODO: fix wordwarp, then remove this line:
+ $perf = '';
+ } catch (Exception $e) {
+ // Ignoring perfdata errors right now, we could show some hint
+ }
+
+ $wrappedOutput = wordwrap(
+ preg_replace('~\@{3,}~', '@@@', $row->service_output),
+ $maxCols - 13
+ ) . "\n";
+ $out .= sprintf(
+ " %1s─ %s%s (%s)",
+ $leaf,
+ $screen->underline($row->service_description),
+ $screen->colorize($utils->objectStateFlags('service', $row) . $perf, 'lightblue'),
+ ucfirst(DateFormatter::timeSince($row->service_last_state_change))
+ );
+ if ($this->isVerbose) {
+ $out .= $emptyLine . preg_replace(
+ '/\n/',
+ $emptyLine,
+ $wrappedOutput
+ ) . "\n";
+ } else {
+ $out .= "\n";
+ }
+ }
+
+ $out .= "\n";
+ return $out;
+ }
+
+ protected function getPercentageSign($percent)
+ {
+ $circles = array(
+ 0 => '○',
+ 15 => '◔',
+ 40 => '◑',
+ 65 => '◕',
+ 90 => '●',
+ );
+ $last = $circles[0];
+ foreach ($circles as $cur => $circle) {
+ if ($percent < $cur) {
+ return $last;
+ }
+ $last = $circle;
+ }
+ }
+}
diff --git a/modules/monitoring/application/clicommands/NrpeCommand.php b/modules/monitoring/application/clicommands/NrpeCommand.php
new file mode 100644
index 0000000..fe82322
--- /dev/null
+++ b/modules/monitoring/application/clicommands/NrpeCommand.php
@@ -0,0 +1,58 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Clicommands;
+
+use Icinga\Protocol\Nrpe\Connection;
+use Icinga\Cli\Command;
+use Exception;
+
+/**
+ * NRPE
+ */
+class NrpeCommand extends Command
+{
+ protected $defaultActionName = 'check';
+
+ /**
+ * Execute an NRPE command
+ *
+ * This command will execute an NRPE check, fire it against the given host
+ * and also pass through all your parameters. Output will be shown, exit
+ * code respected.
+ *
+ * USAGE
+ *
+ * icingacli monitoring nrpe <host> <command> [--ssl] [nrpe options]
+ *
+ * EXAMPLE
+ *
+ * icingacli monitoring nrpe 127.0.0.1 CheckMEM --ssl --MaxWarn=80% \
+ * --MaxCrit=90% --type=physical
+ */
+ public function checkAction()
+ {
+ $host = $this->params->shift();
+ if (! $host) {
+ echo $this->showUsage();
+ exit(3);
+ }
+ $command = $this->params->shift(null, '_NRPE_CHECK');
+ $port = $this->params->shift('port', 5666);
+ try {
+ $nrpe = new Connection($host, $port);
+ if ($this->params->shift('ssl')) {
+ $nrpe->useSsl();
+ }
+ $args = array();
+ foreach ($this->params->getParams() as $k => $v) {
+ $args[] = $k . '=' . $v;
+ }
+ echo $nrpe->sendCommand($command, $args) . "\n";
+ exit($nrpe->getLastReturnCode());
+ } catch (Exception $e) {
+ echo $e->getMessage() . "\n";
+ exit(3);
+ }
+ }
+}
diff --git a/modules/monitoring/application/controllers/ActionsController.php b/modules/monitoring/application/controllers/ActionsController.php
new file mode 100644
index 0000000..bc13e21
--- /dev/null
+++ b/modules/monitoring/application/controllers/ActionsController.php
@@ -0,0 +1,135 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimesCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleHostDowntimeCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleServiceDowntimeCommandForm;
+use Icinga\Module\Monitoring\Object\HostList;
+use Icinga\Module\Monitoring\Object\ServiceList;
+
+/**
+ * Monitoring API
+ */
+class ActionsController extends Controller
+{
+ /**
+ * Get the filter from URL parameters or exit immediately if the filter is empty
+ *
+ * @return Filter
+ */
+ protected function getFilterOrExitIfEmpty()
+ {
+ $filter = Filter::fromQueryString((string) $this->params);
+ if ($filter->isEmpty()) {
+ $this->getResponse()->json()
+ ->setFailData(array('filter' => 'Filter is required and must not be empty'))
+ ->sendResponse();
+ }
+ return $filter;
+ }
+
+ /**
+ * Schedule host downtimes
+ */
+ public function scheduleHostDowntimeAction()
+ {
+ $filter = $this->getFilterOrExitIfEmpty();
+ $hostList = new HostList($this->backend);
+ $hostList
+ ->applyFilter($this->getRestriction('monitoring/filter/objects'))
+ ->applyFilter($filter);
+ if (! $hostList->count()) {
+ $this->getResponse()->json()
+ ->setFailData(array('filter' => 'No hosts found matching the filter'))
+ ->sendResponse();
+ }
+ $form = new ScheduleHostDowntimeCommandForm();
+ $form
+ ->setIsApiTarget(true)
+ ->setBackend($this->backend)
+ ->setObjects($hostList->fetch())
+ ->handleRequest($this->getRequest());
+ }
+
+ /**
+ * Remove host downtimes
+ */
+ public function removeHostDowntimeAction()
+ {
+ $filter = $this->getFilterOrExitIfEmpty();
+ $downtimes = $this->backend
+ ->select()
+ ->from('downtime', array('host_name', 'id' => 'downtime_internal_id', 'name' => 'downtime_name'))
+ ->where('object_type', 'host')
+ ->applyFilter($this->getRestriction('monitoring/filter/objects'))
+ ->applyFilter($filter);
+ if (! $downtimes->count()) {
+ $this->getResponse()->json()
+ ->setFailData(array('filter' => 'No downtimes found matching the filter'))
+ ->sendResponse();
+ }
+ $form = new DeleteDowntimesCommandForm();
+ $form
+ ->setIsApiTarget(true)
+ ->setDowntimes($downtimes->fetchAll())
+ ->handleRequest($this->getRequest());
+ // @TODO(el): Respond w/ the downtimes deleted instead of the notifiaction added by
+ // DeleteDowntimesCommandForm::onSuccess().
+ }
+
+ /**
+ * Schedule service downtimes
+ */
+ public function scheduleServiceDowntimeAction()
+ {
+ $filter = $this->getFilterOrExitIfEmpty();
+ $serviceList = new ServiceList($this->backend);
+ $serviceList
+ ->applyFilter($this->getRestriction('monitoring/filter/objects'))
+ ->applyFilter($filter);
+ if (! $serviceList->count()) {
+ $this->getResponse()->json()
+ ->setFailData(array('filter' => 'No services found matching the filter'))
+ ->sendResponse();
+ }
+ $form = new ScheduleServiceDowntimeCommandForm();
+ $form
+ ->setIsApiTarget(true)
+ ->setBackend($this->backend)
+ ->setObjects($serviceList->fetch())
+ ->handleRequest($this->getRequest());
+ }
+
+ /**
+ * Remove service downtimes
+ */
+ public function removeServiceDowntimeAction()
+ {
+ $filter = $this->getFilterOrExitIfEmpty();
+ $downtimes = $this->backend
+ ->select()
+ ->from(
+ 'downtime',
+ array('host_name', 'service_description', 'id' => 'downtime_internal_id', 'name' => 'downtime_name')
+ )
+ ->where('object_type', 'service')
+ ->applyFilter($this->getRestriction('monitoring/filter/objects'))
+ ->applyFilter($filter);
+ if (! $downtimes->count()) {
+ $this->getResponse()->json()
+ ->setFailData(array('filter' => 'No downtimes found matching the filter'))
+ ->sendResponse();
+ }
+ $form = new DeleteDowntimesCommandForm();
+ $form
+ ->setIsApiTarget(true)
+ ->setDowntimes($downtimes->fetchAll())
+ ->handleRequest($this->getRequest());
+ // @TODO(el): Respond w/ the downtimes deleted instead of the notifiaction added by
+ // DeleteDowntimesCommandForm::onSuccess().
+ }
+}
diff --git a/modules/monitoring/application/controllers/CommentController.php b/modules/monitoring/application/controllers/CommentController.php
new file mode 100644
index 0000000..e50473f
--- /dev/null
+++ b/modules/monitoring/application/controllers/CommentController.php
@@ -0,0 +1,91 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use Icinga\Application\Hook;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentCommandForm;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+
+/**
+ * Display detailed information about a comment
+ */
+class CommentController extends Controller
+{
+ /**
+ * The fetched comment
+ *
+ * @var object
+ */
+ protected $comment;
+
+ /**
+ * Fetch the first comment with the given id and add tabs
+ */
+ public function init()
+ {
+ $commentId = $this->params->getRequired('comment_id');
+
+ $query = $this->backend->select()->from('comment', array(
+ 'id' => 'comment_internal_id',
+ 'objecttype' => 'object_type',
+ 'comment' => 'comment_data',
+ 'author' => 'comment_author_name',
+ 'timestamp' => 'comment_timestamp',
+ 'type' => 'comment_type',
+ 'persistent' => 'comment_is_persistent',
+ 'expiration' => 'comment_expiration',
+ 'name' => 'comment_name',
+ 'host_name',
+ 'service_description',
+ 'host_display_name',
+ 'service_display_name'
+ ))->where('comment_internal_id', $commentId);
+ $this->applyRestriction('monitoring/filter/objects', $query);
+
+ if (false === $this->comment = $query->fetchRow()) {
+ $this->httpNotFound($this->translate('Comment not found'));
+ }
+
+ $this->getTabs()->add(
+ 'comment',
+ array(
+ 'icon' => 'comment-empty',
+ 'label' => $this->translate('Comment'),
+ 'title' => $this->translate('Display detailed information about a comment.'),
+ 'url' =>'monitoring/comments/show'
+ )
+ )->activate('comment')->extend(new DashboardAction())->extend(new MenuAction());
+
+ if (Hook::has('ticket')) {
+ $this->view->tickets = Hook::first('ticket');
+ }
+ }
+
+ /**
+ * Display comment detail view
+ */
+ public function showAction()
+ {
+ $this->view->comment = $this->comment;
+ $this->view->title = $this->translate('Comments');
+
+ if ($this->hasPermission('monitoring/command/comment/delete')) {
+ $listUrl = Url::fromPath('monitoring/list/comments')
+ ->setQueryString('comment_type=comment|comment_type=ack');
+ $form = new DeleteCommentCommandForm();
+ $form
+ ->populate(array(
+ 'comment_id' => $this->comment->id,
+ 'comment_is_service' => isset($this->comment->service_description),
+ 'comment_name' => $this->comment->name,
+ 'redirect' => $listUrl
+ ))
+ ->handleRequest();
+ $this->view->delCommentForm = $form;
+ }
+ }
+}
diff --git a/modules/monitoring/application/controllers/CommentsController.php b/modules/monitoring/application/controllers/CommentsController.php
new file mode 100644
index 0000000..9de19a0
--- /dev/null
+++ b/modules/monitoring/application/controllers/CommentsController.php
@@ -0,0 +1,108 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentsCommandForm;
+use Icinga\Web\Url;
+
+/**
+ * Display detailed information about comments
+ */
+class CommentsController extends Controller
+{
+ /**
+ * The comments view
+ *
+ * @var \Icinga\Module\Monitoring\DataView\Comment
+ */
+ protected $comments;
+
+ /**
+ * Filter from request
+ *
+ * @var Filter
+ */
+ protected $filter;
+
+ /**
+ * Fetch all comments matching the current filter and add tabs
+ */
+ public function init()
+ {
+ $this->filter = Filter::fromQueryString(str_replace(
+ 'comment_id',
+ 'comment_internal_id',
+ (string) $this->params
+ ));
+ $query = $this->backend->select()->from('comment', array(
+ 'id' => 'comment_internal_id',
+ 'objecttype' => 'object_type',
+ 'comment' => 'comment_data',
+ 'author' => 'comment_author_name',
+ 'timestamp' => 'comment_timestamp',
+ 'type' => 'comment_type',
+ 'persistent' => 'comment_is_persistent',
+ 'expiration' => 'comment_expiration',
+ 'name' => 'comment_name',
+ 'host_name',
+ 'service_description',
+ 'host_display_name',
+ 'service_display_name'
+ ))->addFilter($this->filter);
+ $this->applyRestriction('monitoring/filter/objects', $query);
+
+ $this->comments = $query;
+
+ $this->view->title = $this->translate('Comments');
+ $this->getTabs()->add(
+ 'comments',
+ array(
+ 'icon' => 'comment-empty',
+ 'label' => $this->translate('Comments') . sprintf(' (%d)', $query->count()),
+ 'title' => $this->translate(
+ 'Display detailed information about multiple comments.'
+ ),
+ 'url' =>'monitoring/comments/show'
+ )
+ )->activate('comments');
+ }
+
+ /**
+ * Display the detail view for a comment list
+ */
+ public function showAction()
+ {
+ $this->view->comments = $this->comments;
+ $this->view->listAllLink = Url::fromPath('monitoring/list/comments')
+ ->setQueryString($this->filter->toQueryString());
+ $this->view->removeAllLink = Url::fromPath('monitoring/comments/delete-all')
+ ->setParams($this->params);
+ }
+
+ /**
+ * Display the form for removing a comment list
+ */
+ public function deleteAllAction()
+ {
+ $this->assertPermission('monitoring/command/comment/delete');
+
+ $listCommentsLink = Url::fromPath('monitoring/list/comments')
+ ->setQueryString('comment_type=(comment|ack)');
+ $delCommentForm = new DeleteCommentsCommandForm();
+ $delCommentForm->setTitle($this->view->translate('Remove all Comments'));
+ $delCommentForm->addDescription(sprintf(
+ $this->translate('Confirm removal of %d comments.'),
+ $this->comments->count()
+ ));
+ $delCommentForm->setComments($this->comments->fetchAll())
+ ->setRedirectUrl($listCommentsLink)
+ ->handleRequest();
+ $this->view->delCommentForm = $delCommentForm;
+ $this->view->comments = $this->comments;
+ $this->view->listAllLink = Url::fromPath('monitoring/list/comments')
+ ->setQueryString($this->filter->toQueryString());
+ }
+}
diff --git a/modules/monitoring/application/controllers/ConfigController.php b/modules/monitoring/application/controllers/ConfigController.php
new file mode 100644
index 0000000..b8ca0a1
--- /dev/null
+++ b/modules/monitoring/application/controllers/ConfigController.php
@@ -0,0 +1,298 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use Exception;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\ConfirmRemovalForm;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Module\Monitoring\Forms\Config\TransportReorderForm;
+use Icinga\Web\Controller;
+use Icinga\Web\Notification;
+use Icinga\Module\Monitoring\Forms\Config\BackendConfigForm;
+use Icinga\Module\Monitoring\Forms\Config\SecurityConfigForm;
+use Icinga\Module\Monitoring\Forms\Config\TransportConfigForm;
+
+/**
+ * Configuration controller for editing monitoring resources
+ */
+class ConfigController extends Controller
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->assertPermission('config/modules');
+ $this->view->title = $this->translate('Backends');
+ $this->view->defaultTitle = 'monitoring :: ' . $this->view->defaultTitle;
+ parent::init();
+ }
+
+ /**
+ * Display a list of available backends and command transports
+ */
+ public function indexAction()
+ {
+ $this->view->commandTransportReorderForm = $form = new TransportReorderForm();
+ $form->handleRequest();
+
+ $this->view->backendsConfig = $this->Config('backends');
+ $this->view->tabs = $this->Module()->getConfigTabs()->activate('backends');
+ }
+
+ /**
+ * Edit a monitoring backend
+ */
+ public function editbackendAction()
+ {
+ $backendName = $this->params->getRequired('backend-name');
+
+ $form = new BackendConfigForm();
+ $form->setRedirectUrl('monitoring/config');
+ $form->setTitle(sprintf($this->translate('Edit Monitoring Backend %s'), $backendName));
+ $form->setIniConfig($this->Config('backends'));
+ $form->setResourceConfig(ResourceFactory::getResourceConfigs());
+ $form->setOnSuccess(function (BackendConfigForm $form) use ($backendName) {
+ try {
+ $form->edit($backendName, array_map(
+ function ($v) {
+ return $v !== '' ? $v : null;
+ },
+ $form->getValues()
+ ));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(sprintf(t('Monitoring backend "%s" successfully updated'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+
+ try {
+ $form->load($backendName);
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Monitoring backend "%s" not found'), $backendName));
+ }
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Create a new monitoring backend
+ */
+ public function createbackendAction()
+ {
+ $form = new BackendConfigForm();
+ $form->setRedirectUrl('monitoring/config');
+ $form->setTitle($this->translate('Create New Monitoring Backend'));
+ $form->setIniConfig($this->Config('backends'));
+
+ try {
+ $form->setResourceConfig(ResourceFactory::getResourceConfigs());
+ } catch (ConfigurationError $e) {
+ if ($this->hasPermission('config/resources')) {
+ Notification::error($e->getMessage());
+ $this->redirectNow('config/createresource');
+ }
+
+ throw $e; // No permission for resource configuration, show the error
+ }
+
+ $form->setOnSuccess(function (BackendConfigForm $form) {
+ try {
+ $form->add($form::transformEmptyValuesToNull($form->getValues()));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(t('Monitoring backend successfully created'));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Display a confirmation form to remove the backend identified by the 'backend' parameter
+ */
+ public function removebackendAction()
+ {
+ $backendName = $this->params->getRequired('backend-name');
+
+ $backendForm = new BackendConfigForm();
+ $backendForm->setIniConfig($this->Config('backends'));
+ $form = new ConfirmRemovalForm();
+ $form->setRedirectUrl('monitoring/config');
+ $form->setTitle(sprintf($this->translate('Remove Monitoring Backend %s'), $backendName));
+ $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($backendName, $backendForm) {
+ try {
+ $backendForm->delete($backendName);
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($backendForm->save()) {
+ Notification::success(sprintf(t('Monitoring backend "%s" successfully removed'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Remove a command transport
+ */
+ public function removetransportAction()
+ {
+ $transportName = $this->params->getRequired('transport');
+
+ $transportForm = new TransportConfigForm();
+ $transportForm->setIniConfig($this->Config('commandtransports'));
+ $form = new ConfirmRemovalForm();
+ $form->setRedirectUrl('monitoring/config');
+ $form->setTitle(sprintf($this->translate('Remove Command Transport %s'), $transportName));
+ $form->info(
+ $this->translate(
+ 'If you still have any environments or views referring to this transport, '
+ . 'you won\'t be able to send commands anymore after deletion.'
+ ),
+ false
+ );
+ $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($transportName, $transportForm) {
+ try {
+ $transportForm->delete($transportName);
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($transportForm->save()) {
+ Notification::success(sprintf(t('Command transport "%s" successfully removed'), $transportName));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Edit a command transport
+ */
+ public function edittransportAction()
+ {
+ $transportName = $this->params->getRequired('transport');
+
+ $form = new TransportConfigForm();
+ $form->setRedirectUrl('monitoring/config');
+ $form->setTitle(sprintf($this->translate('Edit Command Transport %s'), $transportName));
+ $form->setIniConfig($this->Config('commandtransports'));
+ $form->setInstanceNames(
+ MonitoringBackend::instance()->select()->from('instance', array('instance_name'))->fetchColumn()
+ );
+ $form->setOnSuccess(function (TransportConfigForm $form) use ($transportName) {
+ try {
+ $form->edit($transportName, array_map(
+ function ($v) {
+ return $v !== '' ? $v : null;
+ },
+ $form->getValues()
+ ));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(sprintf(t('Command transport "%s" successfully updated'), $transportName));
+ return true;
+ }
+
+ return false;
+ });
+
+ try {
+ $form->load($transportName);
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Command transport "%s" not found'), $transportName));
+ }
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Create a new command transport
+ */
+ public function createtransportAction()
+ {
+ $form = new TransportConfigForm();
+ $form->setRedirectUrl('monitoring/config');
+ $form->setTitle($this->translate('Create New Command Transport'));
+ $form->setIniConfig($this->Config('commandtransports'));
+ $form->setInstanceNames(
+ MonitoringBackend::instance()->select()->from('instance', array('instance_name'))->fetchColumn()
+ );
+ $form->setOnSuccess(function (TransportConfigForm $form) {
+ try {
+ $form->add($form::transformEmptyValuesToNull($form->getValues()));
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(t('Command transport successfully created'));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Display a form to adjust security relevant settings
+ */
+ public function securityAction()
+ {
+ $form = new SecurityConfigForm();
+ $form->setIniConfig($this->Config());
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->title = $this->translate('Security');
+ $this->view->tabs = $this->Module()->getConfigTabs()->activate('security');
+ }
+}
diff --git a/modules/monitoring/application/controllers/DowntimeController.php b/modules/monitoring/application/controllers/DowntimeController.php
new file mode 100644
index 0000000..83c03dd
--- /dev/null
+++ b/modules/monitoring/application/controllers/DowntimeController.php
@@ -0,0 +1,108 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use Icinga\Application\Hook;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimeCommandForm;
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+
+/**
+ * Display detailed information about a downtime
+ */
+class DowntimeController extends Controller
+{
+ /**
+ * The fetched downtime
+ *
+ * @var object
+ */
+ protected $downtime;
+
+ /**
+ * Fetch the downtime matching the given id and add tabs
+ */
+ public function init()
+ {
+ $downtimeId = $this->params->getRequired('downtime_id');
+
+ $query = $this->backend->select()->from('downtime', array(
+ 'id' => 'downtime_internal_id',
+ 'objecttype' => 'object_type',
+ 'comment' => 'downtime_comment',
+ 'author_name' => 'downtime_author_name',
+ 'start' => 'downtime_start',
+ 'scheduled_start' => 'downtime_scheduled_start',
+ 'scheduled_end' => 'downtime_scheduled_end',
+ 'end' => 'downtime_end',
+ 'duration' => 'downtime_duration',
+ 'is_flexible' => 'downtime_is_flexible',
+ 'is_fixed' => 'downtime_is_fixed',
+ 'is_in_effect' => 'downtime_is_in_effect',
+ 'entry_time' => 'downtime_entry_time',
+ 'name' => 'downtime_name',
+ 'host_state',
+ 'service_state',
+ 'host_name',
+ 'service_description',
+ 'host_display_name',
+ 'service_display_name'
+ ))->where('downtime_internal_id', $downtimeId);
+ $this->applyRestriction('monitoring/filter/objects', $query);
+
+ if (false === $this->downtime = $query->fetchRow()) {
+ $this->httpNotFound($this->translate('Downtime not found'));
+ }
+
+ $this->getTabs()->add(
+ 'downtime',
+ array(
+
+ 'icon' => 'plug',
+ 'label' => $this->translate('Downtime'),
+ 'title' => $this->translate('Display detailed information about a downtime.'),
+ 'url' =>'monitoring/downtimes/show'
+ )
+ )->activate('downtime')->extend(new DashboardAction())->extend(new MenuAction());
+
+ if (Hook::has('ticket')) {
+ $this->view->tickets = Hook::first('ticket');
+ }
+ }
+
+ /**
+ * Display the detail view for a downtime
+ */
+ public function showAction()
+ {
+ $isService = isset($this->downtime->service_description);
+ $this->view->downtime = $this->downtime;
+ $this->view->isService = $isService;
+ $this->view->listAllLink = Url::fromPath('monitoring/list/downtimes');
+ $this->view->showHostLink = Url::fromPath('monitoring/host/show')->setParam('host', $this->downtime->host_name);
+ $this->view->showServiceLink = Url::fromPath('monitoring/service/show')
+ ->setParam('host', $this->downtime->host_name)
+ ->setParam('service', $this->downtime->service_description);
+ $this->view->stateName = $isService ? Service::getStateText($this->downtime->service_state)
+ : Host::getStateText($this->downtime->host_state);
+
+ $this->view->title = $this->translate('Downtimes');
+ if ($this->hasPermission('monitoring/command/downtime/delete')) {
+ $form = new DeleteDowntimeCommandForm();
+ $form
+ ->populate(array(
+ 'downtime_id' => $this->downtime->id,
+ 'downtime_is_service' => $isService,
+ 'downtime_name' => $this->downtime->name,
+ 'redirect' => Url::fromPath('monitoring/list/downtimes'),
+ ))
+ ->handleRequest();
+ $this->view->delDowntimeForm = $form;
+ }
+ }
+}
diff --git a/modules/monitoring/application/controllers/DowntimesController.php b/modules/monitoring/application/controllers/DowntimesController.php
new file mode 100644
index 0000000..4891203
--- /dev/null
+++ b/modules/monitoring/application/controllers/DowntimesController.php
@@ -0,0 +1,108 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimesCommandForm;
+use Icinga\Web\Url;
+
+/**
+ * Display detailed information about downtimes
+ */
+class DowntimesController extends Controller
+{
+ /**
+ * The downtimes view
+ *
+ * @var \Icinga\Module\Monitoring\DataView\Downtime
+ */
+ protected $downtimes;
+
+ /**
+ * Filter from request
+ *
+ * @var Filter
+ */
+ protected $filter;
+
+ /**
+ * Fetch all downtimes matching the current filter and add tabs
+ */
+ public function init()
+ {
+ $this->filter = Filter::fromQueryString(str_replace(
+ 'downtime_id',
+ 'downtime_internal_id',
+ (string) $this->params
+ ));
+ $query = $this->backend->select()->from('downtime', array(
+ 'id' => 'downtime_internal_id',
+ 'objecttype' => 'object_type',
+ 'comment' => 'downtime_comment',
+ 'author_name' => 'downtime_author_name',
+ 'start' => 'downtime_start',
+ 'scheduled_start' => 'downtime_scheduled_start',
+ 'scheduled_end' => 'downtime_scheduled_end',
+ 'end' => 'downtime_end',
+ 'duration' => 'downtime_duration',
+ 'is_flexible' => 'downtime_is_flexible',
+ 'is_fixed' => 'downtime_is_fixed',
+ 'is_in_effect' => 'downtime_is_in_effect',
+ 'entry_time' => 'downtime_entry_time',
+ 'name' => 'downtime_name',
+ 'host_state',
+ 'service_state',
+ 'host_name',
+ 'service_description',
+ 'host_display_name',
+ 'service_display_name'
+ ))->addFilter($this->filter);
+ $this->applyRestriction('monitoring/filter/objects', $query);
+
+ $this->downtimes = $query;
+
+ $this->view->title = $this->translate('Downtimes');
+ $this->getTabs()->add(
+ 'downtimes',
+ array(
+ 'icon' => 'plug',
+ 'label' => $this->translate('Downtimes') . sprintf(' (%d)', $query->count()),
+ 'title' => $this->translate('Display detailed information about multiple downtimes.'),
+ 'url' =>'monitoring/downtimes/show'
+ )
+ )->activate('downtimes');
+ }
+
+ /**
+ * Display the detail view for a downtime list
+ */
+ public function showAction()
+ {
+ $this->view->downtimes = $this->downtimes;
+ $this->view->listAllLink = Url::fromPath('monitoring/list/downtimes')
+ ->setQueryString($this->filter->toQueryString());
+ $this->view->removeAllLink = Url::fromPath('monitoring/downtimes/delete-all')->setParams($this->params);
+ }
+
+ /**
+ * Display the form for removing a downtime list
+ */
+ public function deleteAllAction()
+ {
+ $this->assertPermission('monitoring/command/downtime/delete');
+ $this->view->downtimes = $this->downtimes;
+ $this->view->listAllLink = Url::fromPath('monitoring/list/downtimes')
+ ->setQueryString($this->filter->toQueryString());
+ $delDowntimeForm = new DeleteDowntimesCommandForm();
+ $delDowntimeForm->setTitle($this->view->translate('Remove all Downtimes'));
+ $delDowntimeForm->addDescription(sprintf(
+ $this->translate('Confirm removal of %d downtimes.'),
+ $this->downtimes->count()
+ ));
+ $delDowntimeForm->setRedirectUrl(Url::fromPath('monitoring/list/downtimes'));
+ $delDowntimeForm->setDowntimes($this->downtimes->fetchAll())->handleRequest();
+ $this->view->delAllDowntimeForm = $delDowntimeForm;
+ }
+}
diff --git a/modules/monitoring/application/controllers/EventController.php b/modules/monitoring/application/controllers/EventController.php
new file mode 100644
index 0000000..13bf537
--- /dev/null
+++ b/modules/monitoring/application/controllers/EventController.php
@@ -0,0 +1,551 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use DateTime;
+use DateTimeZone;
+use Icinga\Module\Monitoring\Hook\EventDetailsExtensionHook;
+use Icinga\Application\Hook;
+use InvalidArgumentException;
+use Icinga\Data\Queryable;
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Util\TimezoneDetect;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+
+class EventController extends Controller
+{
+ /**
+ * @var string[]
+ */
+ protected $dataViewsByType = array(
+ 'notify' => 'notificationevent',
+ 'comment' => 'commentevent',
+ 'comment_deleted' => 'commentevent',
+ 'ack' => 'commentevent',
+ 'ack_deleted' => 'commentevent',
+ 'dt_comment' => 'commentevent',
+ 'dt_comment_deleted' => 'commentevent',
+ 'flapping' => 'flappingevent',
+ 'flapping_deleted' => 'flappingevent',
+ 'hard_state' => 'statechangeevent',
+ 'soft_state' => 'statechangeevent',
+ 'dt_start' => 'downtimeevent',
+ 'dt_end' => 'downtimeevent'
+ );
+
+ public function init()
+ {
+ if (Hook::has('ticket')) {
+ $this->view->tickets = Hook::first('ticket');
+ }
+ }
+
+ public function showAction()
+ {
+ $type = $this->params->shiftRequired('type');
+ $id = $this->params->shiftRequired('id');
+
+ if (! isset($this->dataViewsByType[$type])
+ || $this->applyRestriction(
+ 'monitoring/filter/objects',
+ $this->backend->select()->from('eventhistory', array('id'))->where('id', $id)
+ )->fetchRow() === false
+ ) {
+ $this->httpNotFound($this->translate('Event not found'));
+ }
+
+ $event = $this->query($type, $id)->fetchRow();
+
+ if ($event === false) {
+ $this->httpNotFound($this->translate('Event not found'));
+ }
+
+ $this->view->object = $object = $event->service_description === null
+ ? new Host($this->backend, $event->host_name)
+ : new Service($this->backend, $event->host_name, $event->service_description);
+ $object->fetch();
+
+ list($icon, $label) = $this->getIconAndLabel($type);
+
+ $this->view->details = array_merge(
+ array(array($this->view->escape($this->translate('Type')), $label)),
+ $this->getDetails($type, $event)
+ );
+
+ $this->view->extensionsHtml = array();
+ /** @var EventDetailsExtensionHook $hook */
+ foreach (Hook::all('Monitoring\\EventDetailsExtension') as $hook) {
+ try {
+ $html = $hook->getHtmlForEvent($event);
+ } catch (\Exception $e) {
+ $html = $this->view->escape($e->getMessage());
+ }
+
+ if ($html) {
+ $module = $this->view->escape($hook->getModule()->getName());
+ $this->view->extensionsHtml[] =
+ '<div class="icinga-module module-' . $module . '" data-icinga-module="' . $module . '">'
+ . $html
+ . '</div>';
+ }
+ }
+
+ $this->view->title = $this->translate('Event Overview');
+ $this->getTabs()
+ ->add('event', array(
+ 'title' => $label,
+ 'label' => $label,
+ 'url' => Url::fromRequest(),
+ 'active' => true
+ ))
+ ->extend(new OutputFormat())
+ ->extend(new DashboardAction())
+ ->extend(new MenuAction());
+ }
+
+ /**
+ * Return translated and escaped 'Yes' if the given condition is true, 'No' otherwise, 'N/A' if NULL
+ *
+ * @param bool|null $condition
+ *
+ * @return string
+ */
+ protected function yesOrNo($condition)
+ {
+ if ($condition === null) {
+ return $this->view->escape($this->translate('N/A'));
+ }
+
+ return $this->view->escape($condition ? $this->translate('Yes') : $this->translate('No'));
+ }
+
+ /**
+ * Render the given duration in seconds as human readable HTML or 'N/A' if NULL
+ *
+ * @param int|null $seconds
+ *
+ * @return string
+ */
+ protected function duration($seconds)
+ {
+ return $this->view->escape(
+ $seconds === null ? $this->translate('N/A') : DateFormatter::formatDuration($seconds)
+ );
+ }
+
+ /**
+ * Render the given percent number as human readable HTML or 'N/A' if NULL
+ *
+ * @param float|null $percent
+ *
+ * @return string
+ */
+ protected function percent($percent)
+ {
+ return $this->view->escape(
+ $percent === null ? $this->translate('N/A') : sprintf($this->translate('%.2f%%'), $percent)
+ );
+ }
+
+ /**
+ * Render the given comment message as HTML or 'N/A' if NULL
+ *
+ * @param string|null $message
+ *
+ * @return string
+ */
+ protected function comment($message)
+ {
+ return $this->view->nl2br($this->view->createTicketLinks($this->view->markdown($message)));
+ }
+
+ /**
+ * Render a link to the given contact or 'N/A' if NULL
+ *
+ * @param string|null $name
+ *
+ * @return string
+ */
+ protected function contact($name)
+ {
+ return $name === null
+ ? $this->view->escape($this->translate('N/A'))
+ : $this->view->qlink($name, Url::fromPath('monitoring/show/contact', array('contact_name' => $name)));
+ }
+
+ /**
+ * Render the given monitored object state as human readable HTML or 'N/A' if NULL
+ *
+ * @param bool $isService
+ * @param int|null $state
+ *
+ * @return string
+ */
+ protected function state($isService, $state)
+ {
+ if ($state === null) {
+ return $this->view->escape($this->translate('N/A'));
+ }
+
+ try {
+ $stateText = $isService
+ ? Service::getStateText($state, true)
+ : Host::getStateText($state, true);
+ } catch (InvalidArgumentException $e) {
+ return $this->view->escape($this->translate('N/A'));
+ }
+
+ return '<span class="badge state-' . ($isService ? Service::getStateText($state) : Host::getStateText($state))
+ . '">&nbsp;</span><span class="state-label">' . $this->view->escape($stateText) . '</span>';
+ }
+
+ /**
+ * Render the given plugin output as human readable HTML
+ *
+ * @param string $output
+ *
+ * @return string
+ */
+ protected function pluginOutput($output)
+ {
+ return $this->view->getHelper('PluginOutput')->pluginOutput($output);
+ }
+
+ /**
+ * Return the icon and the label for the given event type
+ *
+ * @param string $eventType
+ *
+ * @return ?string[]
+ */
+ protected function getIconAndLabel($eventType)
+ {
+ switch ($eventType) {
+ case 'notify':
+ return array('bell', $this->translate('Notification', 'tooltip'));
+ case 'comment':
+ return array('comment-empty', $this->translate('Comment', 'tooltip'));
+ case 'comment_deleted':
+ return array('cancel', $this->translate('Comment removed', 'tooltip'));
+ case 'ack':
+ return array('ok', $this->translate('Acknowledged', 'tooltip'));
+ case 'ack_deleted':
+ return array('ok', $this->translate('Acknowledgement removed', 'tooltip'));
+ case 'dt_comment':
+ return array('plug', $this->translate('Downtime scheduled', 'tooltip'));
+ case 'dt_comment_deleted':
+ return array('plug', $this->translate('Downtime removed', 'tooltip'));
+ case 'flapping':
+ return array('flapping', $this->translate('Flapping started', 'tooltip'));
+ case 'flapping_deleted':
+ return array('flapping', $this->translate('Flapping stopped', 'tooltip'));
+ case 'hard_state':
+ return array('warning-empty', $this->translate('Hard state change'));
+ case 'soft_state':
+ return array('spinner', $this->translate('Soft state change'));
+ case 'dt_start':
+ return array('plug', $this->translate('Downtime started', 'tooltip'));
+ case 'dt_end':
+ return array('plug', $this->translate('Downtime ended', 'tooltip'));
+ }
+ }
+
+ /**
+ * Return a query for the given event ID of the given type
+ *
+ * @param string $type
+ * @param int $id
+ *
+ * @return ?Queryable
+ */
+ protected function query($type, $id)
+ {
+ switch ($this->dataViewsByType[$type]) {
+ case 'downtimeevent':
+ return $this->backend->select()
+ ->from('downtimeevent', array(
+ 'entry_time' => 'downtimeevent_entry_time',
+ 'author_name' => 'downtimeevent_author_name',
+ 'comment_data' => 'downtimeevent_comment_data',
+ 'is_fixed' => 'downtimeevent_is_fixed',
+ 'scheduled_start_time' => 'downtimeevent_scheduled_start_time',
+ 'scheduled_end_time' => 'downtimeevent_scheduled_end_time',
+ 'was_started' => 'downtimeevent_was_started',
+ 'actual_start_time' => 'downtimeevent_actual_start_time',
+ 'actual_end_time' => 'downtimeevent_actual_end_time',
+ 'was_cancelled' => 'downtimeevent_was_cancelled',
+ 'is_in_effect' => 'downtimeevent_is_in_effect',
+ 'trigger_time' => 'downtimeevent_trigger_time',
+ 'host_name',
+ 'service_description'
+ ))
+ ->where('downtimeevent_id', $id);
+ case 'commentevent':
+ return $this->backend->select()
+ ->from('commentevent', array(
+ 'entry_type' => 'commentevent_entry_type',
+ 'comment_time' => 'commentevent_comment_time',
+ 'author_name' => 'commentevent_author_name',
+ 'comment_data' => 'commentevent_comment_data',
+ 'is_persistent' => 'commentevent_is_persistent',
+ 'comment_source' => 'commentevent_comment_source',
+ 'expires' => 'commentevent_expires',
+ 'expiration_time' => 'commentevent_expiration_time',
+ 'deletion_time' => 'commentevent_deletion_time',
+ 'host_name',
+ 'service_description'
+ ))
+ ->where('commentevent_id', $id);
+ case 'flappingevent':
+ return $this->backend->select()
+ ->from('flappingevent', array(
+ 'event_time' => 'flappingevent_event_time',
+ 'reason_type' => 'flappingevent_reason_type',
+ 'percent_state_change' => 'flappingevent_percent_state_change',
+ 'low_threshold' => 'flappingevent_low_threshold',
+ 'high_threshold' => 'flappingevent_high_threshold',
+ 'host_name',
+ 'service_description'
+ ))
+ ->where('flappingevent_id', $id)
+ ->where('flappingevent_event_type', $type);
+ case 'notificationevent':
+ return $this->backend->select()
+ ->from('notificationevent', array(
+ 'notification_reason' => 'notificationevent_reason',
+ 'start_time' => 'notificationevent_start_time',
+ 'end_time' => 'notificationevent_end_time',
+ 'state' => 'notificationevent_state',
+ 'output' => 'notificationevent_output',
+ 'long_output' => 'notificationevent_long_output',
+ 'escalated' => 'notificationevent_escalated',
+ 'contacts_notified' => 'notificationevent_contacts_notified',
+ 'host_name',
+ 'service_description'
+ ))
+ ->where('notificationevent_id', $id);
+ case 'statechangeevent':
+ return $this->backend->select()
+ ->from('statechangeevent', array(
+ 'state_time' => 'statechangeevent_state_time',
+ 'state' => 'statechangeevent_state',
+ 'current_check_attempt' => 'statechangeevent_current_check_attempt',
+ 'max_check_attempts' => 'statechangeevent_max_check_attempts',
+ 'last_state' => 'statechangeevent_last_state',
+ 'last_hard_state' => 'statechangeevent_last_hard_state',
+ 'output' => 'statechangeevent_output',
+ 'long_output' => 'statechangeevent_long_output',
+ 'check_source' => 'statechangeevent_check_source',
+ 'host_name',
+ 'service_description'
+ ))
+ ->where('statechangeevent_id', $id)
+ ->where('statechangeevent_state_change', 1)
+ ->where('statechangeevent_state_type', $type);
+ }
+ }
+
+ /**
+ * Return the given event's data prepared for a name-value table
+ *
+ * @param string $type
+ * @param \stdClass $event
+ *
+ * @return ?string[][]
+ */
+ protected function getDetails($type, $event)
+ {
+ switch ($type) {
+ case 'dt_start':
+ case 'dt_end':
+ $details = array(array(
+ array($this->translate('Entry time'), DateFormatter::formatDateTime($event->entry_time)),
+ array($this->translate('Is fixed'), $this->yesOrNo($event->is_fixed)),
+ array($this->translate('Is in effect'), $this->yesOrNo($event->is_in_effect)),
+ array($this->translate('Was started'), $this->yesOrNo($event->was_started))
+ ));
+
+ if ($type === 'dt_end') {
+ $details[] = array(
+ array($this->translate('Was cancelled'), $this->yesOrNo($event->was_cancelled))
+ );
+ }
+
+ $details[] = array(
+ array($this->translate('Trigger time'), DateFormatter::formatDateTime($event->trigger_time)),
+ array(
+ $this->translate('Scheduled start time'),
+ DateFormatter::formatDateTime($event->scheduled_start_time)
+ ),
+ array(
+ $this->translate('Actual start time'),
+ DateFormatter::formatDateTime($event->actual_start_time)
+ ),
+ array(
+ $this->translate('Scheduled end time'),
+ DateFormatter::formatDateTime($event->scheduled_end_time)
+ )
+ );
+
+ if ($type === 'dt_end') {
+ $details[] = array(
+ array(
+ $this->translate('Actual end time'),
+ DateFormatter::formatDateTime($event->actual_end_time)
+ )
+ );
+ }
+
+ $details[] = array(
+ array($this->translate('Author'), $this->contact($event->author_name)),
+ array($this->translate('Comment'), $this->comment($event->comment_data))
+ );
+
+ return call_user_func_array('array_merge', $details);
+ case 'comment':
+ case 'comment_deleted':
+ case 'ack':
+ case 'ack_deleted':
+ case 'dt_comment':
+ case 'dt_comment_deleted':
+ switch ($event->entry_type) {
+ case 'comment':
+ $entryType = $this->translate('User comment');
+ break;
+ case 'downtime':
+ $entryType = $this->translate('Scheduled downtime');
+ break;
+ case 'flapping':
+ $entryType = $this->translate('Flapping');
+ break;
+ case 'ack':
+ $entryType = $this->translate('Acknowledgement');
+ break;
+ default:
+ $entryType = $this->translate('N/A');
+ }
+
+ switch ($event->comment_source) {
+ case 'icinga':
+ $commentSource = $this->translate('Icinga');
+ break;
+ case 'user':
+ $commentSource = $this->translate('User');
+ break;
+ default:
+ $commentSource = $this->translate('N/A');
+ }
+
+ return array(
+ array($this->translate('Time'), DateFormatter::formatDateTime($event->comment_time)),
+ array($this->translate('Source'), $this->view->escape($commentSource)),
+ array($this->translate('Entry type'), $this->view->escape($entryType)),
+ array($this->translate('Author'), $this->contact($event->author_name)),
+ array($this->translate('Is persistent'), $this->yesOrNo($event->is_persistent)),
+ array($this->translate('Expires'), $this->yesOrNo($event->expires)),
+ array($this->translate('Expiration time'), DateFormatter::formatDateTime($event->expiration_time)),
+ array($this->translate('Deletion time'), DateFormatter::formatDateTime($event->deletion_time)),
+ array($this->translate('Message'), $this->comment($event->comment_data))
+ );
+ case 'flapping':
+ case 'flapping_deleted':
+ switch ($event->reason_type) {
+ case 'stopped':
+ $reasonType = $this->translate('Flapping stopped normally');
+ break;
+ case 'disabled':
+ $reasonType = $this->translate('Flapping was disabled');
+ break;
+ default:
+ $reasonType = $this->translate('N/A');
+ }
+
+ return array(
+ array($this->translate('Event time'), DateFormatter::formatDateTime($event->event_time)),
+ array($this->translate('Reason'), $this->view->escape($reasonType)),
+ array($this->translate('State change'), $this->percent($event->percent_state_change)),
+ array($this->translate('Low threshold'), $this->percent($event->low_threshold)),
+ array($this->translate('High threshold'), $this->percent($event->high_threshold))
+ );
+ case 'notify':
+ switch ($event->notification_reason) {
+ case 'normal_notification':
+ $notificationReason = $this->translate('Normal notification');
+ break;
+ case 'ack':
+ $notificationReason = $this->translate('Problem acknowledgement');
+ break;
+ case 'flapping_started':
+ $notificationReason = $this->translate('Flapping started');
+ break;
+ case 'flapping_stopped':
+ $notificationReason = $this->translate('Flapping stopped');
+ break;
+ case 'flapping_disabled':
+ $notificationReason = $this->translate('Flapping was disabled');
+ break;
+ case 'dt_start':
+ $notificationReason = $this->translate('Downtime started');
+ break;
+ case 'dt_end':
+ $notificationReason = $this->translate('Downtime ended');
+ break;
+ case 'dt_cancel':
+ $notificationReason = $this->translate('Downtime was cancelled');
+ break;
+ case 'custom_notification':
+ $notificationReason = $this->translate('Custom notification');
+ break;
+ default:
+ $notificationReason = $this->translate('N/A');
+ }
+
+ $details = array(
+ array($this->translate('Start time'), DateFormatter::formatDateTime($event->start_time)),
+ array($this->translate('End time'), DateFormatter::formatDateTime($event->end_time)),
+ array($this->translate('Reason'), $this->view->escape($notificationReason)),
+ array(
+ $this->translate('State'),
+ $this->state($event->service_description !== null, $event->state)
+ ),
+ array($this->translate('Escalated'), $this->yesOrNo($event->escalated)),
+ array($this->translate('Contacts notified'), (int) $event->contacts_notified),
+ array(
+ $this->translate('Output'),
+ $this->pluginOutput($event->output) . $this->pluginOutput($event->long_output)
+ )
+ );
+
+ return $details;
+ case 'hard_state':
+ case 'soft_state':
+ $isService = $event->service_description !== null;
+
+ $details = array(
+ array($this->translate('State time'), DateFormatter::formatDateTime($event->state_time)),
+ array($this->translate('State'), $this->state($isService, $event->state)),
+ array($this->translate('Check source'), $event->check_source),
+ array($this->translate('Check attempt'), $this->view->escape(sprintf(
+ $this->translate('%d of %d'),
+ (int) $event->current_check_attempt,
+ (int) $event->max_check_attempts
+ ))),
+ array($this->translate('Last state'), $this->state($isService, $event->last_state)),
+ array($this->translate('Last hard state'), $this->state($isService, $event->last_hard_state)),
+ array(
+ $this->translate('Output'),
+ $this->pluginOutput($event->output) . $this->pluginOutput($event->long_output)
+ )
+ );
+
+ return $details;
+ }
+ }
+}
diff --git a/modules/monitoring/application/controllers/HealthController.php b/modules/monitoring/application/controllers/HealthController.php
new file mode 100644
index 0000000..31fc37a
--- /dev/null
+++ b/modules/monitoring/application/controllers/HealthController.php
@@ -0,0 +1,197 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\Forms\Command\Instance\DisableNotificationsExpireCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Instance\ToggleInstanceFeaturesCommandForm;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+
+/**
+ * Display process and performance information of the monitoring host and program-wide commands
+ */
+class HealthController extends Controller
+{
+ /**
+ * Add tabs
+ *
+ * @see \Icinga\Web\Controller\ActionController::init()
+ */
+ public function init()
+ {
+ $this
+ ->getTabs()
+ ->add(
+ 'info',
+ array(
+ 'title' => $this->translate(
+ 'Show information about the current monitoring instance\'s process'
+ . ' and it\'s performance as well as available features'
+ ),
+ 'label' => $this->translate('Process Information'),
+ 'url' =>'monitoring/health/info'
+ )
+ )
+ ->add(
+ 'stats',
+ array(
+ 'title' => $this->translate(
+ 'Show statistics about the monitored objects'
+ ),
+ 'label' => $this->translate('Stats'),
+ 'url' =>'monitoring/health/stats'
+ )
+ )
+ ->extend(new DashboardAction())->extend(new MenuAction());
+ }
+
+ /**
+ * Display process information and program-wide commands
+ */
+ public function infoAction()
+ {
+ $this->view->title = $this->translate('Process Information');
+ $this->getTabs()->activate('info');
+ $this->setAutorefreshInterval(10);
+ $this->view->backendName = $this->backend->getName();
+ $programStatus = $this->backend
+ ->select()
+ ->from(
+ 'programstatus',
+ array(
+ 'is_currently_running',
+ 'process_id',
+ 'endpoint_name',
+ 'program_start_time',
+ 'status_update_time',
+ 'program_version',
+ 'last_command_check',
+ 'last_log_rotation',
+ 'global_service_event_handler',
+ 'global_host_event_handler',
+ 'notifications_enabled',
+ 'disable_notif_expire_time',
+ 'active_service_checks_enabled',
+ 'passive_service_checks_enabled',
+ 'active_host_checks_enabled',
+ 'passive_host_checks_enabled',
+ 'event_handlers_enabled',
+ 'obsess_over_services',
+ 'obsess_over_hosts',
+ 'flap_detection_enabled',
+ 'process_performance_data'
+ )
+ )
+ ->getQuery();
+ $this->handleFormatRequest($programStatus);
+ $programStatus = $programStatus->fetchRow();
+ if ($programStatus === false) {
+ $this->render('not-running', true, null);
+ return;
+ }
+ $this->view->programStatus = $programStatus;
+ $toggleFeaturesForm = new ToggleInstanceFeaturesCommandForm();
+ $toggleFeaturesForm
+ ->setBackend($this->backend)
+ ->setStatus($programStatus)
+ ->load($programStatus)
+ ->handleRequest();
+ $this->view->toggleFeaturesForm = $toggleFeaturesForm;
+
+ $this->view->runtimevariables = (object) $this->backend->select()
+ ->from('runtimevariables', array('varname', 'varvalue'))
+ ->getQuery()->fetchPairs();
+
+ $this->view->checkperformance = $this->backend->select()
+ ->from('runtimesummary')
+ ->getQuery()->fetchAll();
+ }
+
+ /**
+ * Display stats about current checks and monitored objects
+ */
+ public function statsAction()
+ {
+ $this->view->title = $this->translate('Stats');
+ $this->getTabs()->activate('stats');
+
+ $servicestats = $this->backend->select()->from('servicestatussummary', array(
+ 'services_critical',
+ 'services_critical_handled',
+ 'services_critical_unhandled',
+ 'services_ok',
+ 'services_pending',
+ 'services_total',
+ 'services_unknown',
+ 'services_unknown_handled',
+ 'services_unknown_unhandled',
+ 'services_warning',
+ 'services_warning_handled',
+ 'services_warning_unhandled'
+ ));
+ $this->applyRestriction('monitoring/filter/objects', $servicestats);
+ $this->view->servicestats = $servicestats->fetchRow();
+ $this->view->unhandledServiceProblems = $this->view->servicestats->services_critical_unhandled
+ + $this->view->servicestats->services_unknown_unhandled
+ + $this->view->servicestats->services_warning_unhandled;
+
+ $hoststats = $this->backend->select()->from('hoststatussummary', array(
+ 'hosts_total',
+ 'hosts_up',
+ 'hosts_down',
+ 'hosts_down_handled',
+ 'hosts_down_unhandled',
+ 'hosts_unreachable',
+ 'hosts_unreachable_handled',
+ 'hosts_unreachable_unhandled',
+ 'hosts_pending',
+ ));
+ $this->applyRestriction('monitoring/filter/objects', $hoststats);
+ $this->view->hoststats = $hoststats->fetchRow();
+ $this->view->unhandledhostProblems = $this->view->hoststats->hosts_down_unhandled
+ + $this->view->hoststats->hosts_unreachable_unhandled;
+
+ $this->view->unhandledProblems = $this->view->unhandledhostProblems
+ + $this->view->unhandledServiceProblems;
+
+ $this->view->runtimevariables = (object) $this->backend->select()
+ ->from('runtimevariables', array('varname', 'varvalue'))
+ ->getQuery()->fetchPairs();
+
+ $this->view->checkperformance = $this->backend->select()
+ ->from('runtimesummary')
+ ->getQuery()->fetchAll();
+ }
+
+ /**
+ * Disable notifications w/ an optional expire time
+ */
+ public function disableNotificationsAction()
+ {
+ $this->assertPermission('monitoring/command/feature/instance');
+ $this->view->title = $this->translate('Disable Notifications');
+ $programStatus = $this->backend
+ ->select()
+ ->from(
+ 'programstatus',
+ array(
+ 'notifications_enabled',
+ 'disable_notif_expire_time'
+ )
+ )
+ ->getQuery()
+ ->fetchRow();
+ $this->view->programStatus = $programStatus;
+ if ((bool) $programStatus->notifications_enabled === false) {
+ return;
+ } else {
+ $form = new DisableNotificationsExpireCommandForm();
+ $form
+ ->setRedirectUrl('monitoring/health/info')
+ ->handleRequest();
+ $this->view->form = $form;
+ }
+ }
+}
diff --git a/modules/monitoring/application/controllers/HostController.php b/modules/monitoring/application/controllers/HostController.php
new file mode 100644
index 0000000..94f1a60
--- /dev/null
+++ b/modules/monitoring/application/controllers/HostController.php
@@ -0,0 +1,185 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use Icinga\Module\Monitoring\Forms\Command\Object\AcknowledgeProblemCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\AddCommentCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ProcessCheckResultCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleHostCheckCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleHostDowntimeCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\SendCustomNotificationCommandForm;
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Web\Controller\MonitoredObjectController;
+use Icinga\Web\Hook;
+use Icinga\Web\Navigation\Navigation;
+
+class HostController extends MonitoredObjectController
+{
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $commandRedirectUrl = 'monitoring/host/show';
+
+ /**
+ * Fetch the requested host from the monitoring backend
+ */
+ public function init()
+ {
+ $host = new Host($this->backend, $this->params->getRequired('host'));
+ $this->applyRestriction('monitoring/filter/objects', $host);
+ if ($host->fetch() === false) {
+ $this->httpNotFound($this->translate('Host not found'));
+ }
+ $this->object = $host;
+ $this->createTabs();
+ $this->getTabs()->activate('host');
+ $this->view->title = $host->host_display_name;
+ $this->view->defaultTitle = $this->translate('Hosts') . ' :: ' . $this->view->defaultTitle;
+ }
+
+ /**
+ * Get host actions from hook
+ *
+ * @return Navigation
+ */
+ protected function getHostActions()
+ {
+ $navigation = new Navigation();
+ foreach (Hook::all('Monitoring\\HostActions') as $hook) {
+ $navigation->merge($hook->getNavigation($this->object));
+ }
+
+ return $navigation;
+ }
+
+ /**
+ * Show a host
+ */
+ public function showAction()
+ {
+ $this->view->actions = $this->getHostActions();
+ parent::showAction();
+ }
+
+ /**
+ * List a host's services
+ */
+ public function servicesAction()
+ {
+ $this->setAutorefreshInterval(10);
+ $this->getTabs()->activate('services');
+ $query = $this->backend->select()->from('servicestatus', array(
+ 'host_name',
+ 'host_display_name',
+ 'host_state',
+ 'host_state_type',
+ 'host_last_state_change',
+ 'host_address',
+ 'host_address6',
+ 'host_handled',
+ 'service_description',
+ 'service_display_name',
+ 'service_state',
+ 'service_in_downtime',
+ 'service_acknowledged',
+ 'service_handled',
+ 'service_output',
+ 'service_perfdata',
+ 'service_attempt',
+ 'service_last_state_change',
+ 'service_icon_image',
+ 'service_icon_image_alt',
+ 'service_is_flapping',
+ 'service_state_type',
+ 'service_handled',
+ 'service_severity',
+ 'service_last_check',
+ 'service_notifications_enabled',
+ 'service_action_url',
+ 'service_notes_url',
+ 'service_active_checks_enabled',
+ 'service_passive_checks_enabled',
+ 'current_check_attempt' => 'service_current_check_attempt',
+ 'max_check_attempts' => 'service_max_check_attempts',
+ 'service_check_command',
+ 'service_next_update'
+ ));
+ $this->applyRestriction('monitoring/filter/objects', $query);
+ $this->view->services = $query->where('host_name', $this->object->getName());
+ $this->view->object = $this->object;
+ }
+
+ /**
+ * Acknowledge a host problem
+ */
+ public function acknowledgeProblemAction()
+ {
+ $this->assertPermission('monitoring/command/acknowledge-problem');
+
+ $form = new AcknowledgeProblemCommandForm();
+ $form->setTitle($this->translate('Acknowledge Host Problem'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Add a host comment
+ */
+ public function addCommentAction()
+ {
+ $this->assertPermission('monitoring/command/comment/add');
+
+ $form = new AddCommentCommandForm();
+ $form->setTitle($this->translate('Add Host Comment'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Reschedule a host check
+ */
+ public function rescheduleCheckAction()
+ {
+ $this->assertPermission('monitoring/command/schedule-check');
+
+ $form = new ScheduleHostCheckCommandForm();
+ $form->setTitle($this->translate('Reschedule Host Check'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Schedule a host downtime
+ */
+ public function scheduleDowntimeAction()
+ {
+ $this->assertPermission('monitoring/command/downtime/schedule');
+
+ $form = new ScheduleHostDowntimeCommandForm();
+ $form->setTitle($this->translate('Schedule Host Downtime'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Submit a passive host check result
+ */
+ public function processCheckResultAction()
+ {
+ $this->assertPermission('monitoring/command/process-check-result');
+
+ $form = new ProcessCheckResultCommandForm();
+ $form->setTitle($this->translate('Submit Passive Host Check Result'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Send a custom notification for host
+ */
+ public function sendCustomNotificationAction()
+ {
+ $this->assertPermission('monitoring/command/send-custom-notification');
+
+ $form = new SendCustomNotificationCommandForm();
+ $form->setTitle($this->translate('Send Custom Host Notification'));
+ $this->handleCommandForm($form);
+ }
+}
diff --git a/modules/monitoring/application/controllers/HostsController.php b/modules/monitoring/application/controllers/HostsController.php
new file mode 100644
index 0000000..400c3ad
--- /dev/null
+++ b/modules/monitoring/application/controllers/HostsController.php
@@ -0,0 +1,260 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use Exception;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterEqual;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\Forms\Command\Object\AcknowledgeProblemCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\AddCommentCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\CheckNowCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ObjectsCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ProcessCheckResultCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\RemoveAcknowledgementCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleHostCheckCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleHostDowntimeCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\SendCustomNotificationCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ToggleObjectFeaturesCommandForm;
+use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook;
+use Icinga\Module\Monitoring\Object\HostList;
+use Icinga\Web\Hook;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+
+class HostsController extends Controller
+{
+ /**
+ * @var HostList
+ */
+ protected $hostList;
+
+ public function init()
+ {
+ $hostList = new HostList($this->backend);
+ $this->applyRestriction('monitoring/filter/objects', $hostList);
+ $hostList->addFilter(Filter::fromQueryString((string) $this->params));
+ $this->hostList = $hostList;
+ $this->hostList->setColumns(array(
+ 'host_acknowledged',
+ 'host_active_checks_enabled',
+ 'host_display_name',
+ 'host_event_handler_enabled',
+ 'host_flap_detection_enabled',
+ 'host_handled',
+ 'host_in_downtime',
+ 'host_is_flapping',
+ 'host_last_state_change',
+ 'host_name',
+ 'host_notifications_enabled',
+ 'host_obsessing',
+ 'host_passive_checks_enabled',
+ 'host_problem',
+ 'host_state',
+ 'instance_name'
+ ));
+ $this->view->baseFilter = $this->hostList->getFilter();
+ $this->getTabs()->add(
+ 'show',
+ array(
+ 'label' => $this->translate('Hosts') . sprintf(' (%d)', count($this->hostList)),
+ 'title' => sprintf(
+ $this->translate('Show summarized information for %u hosts'),
+ count($this->hostList)
+ ),
+ 'url' => Url::fromRequest()
+ )
+ )->extend(new DashboardAction())->extend(new MenuAction())->activate('show');
+ $this->view->listAllLink = Url::fromRequest()->setPath('monitoring/list/hosts');
+ $this->view->title = $this->translate('Hosts');
+ }
+
+ protected function handleCommandForm(ObjectsCommandForm $form)
+ {
+ $form
+ ->setBackend($this->backend)
+ ->setObjects($this->hostList)
+ ->setRedirectUrl(Url::fromPath('monitoring/hosts/show')->setParams(
+ $this->params->without('host_active_checks_enabled')
+ ))
+ ->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->objects = $this->hostList;
+ $this->view->stats = $this->hostList->getStateSummary();
+ $this->_helper->viewRenderer('partials/command/objects-command-form', null, true);
+ return $form;
+ }
+
+ public function showAction()
+ {
+ $this->setAutorefreshInterval(15);
+ $activeChecksEnabled = $this->hostList->getFeatureStatus()['active_checks_enabled'] !== 0;
+ if ($this->Auth()->hasPermission('monitoring/command/schedule-check')
+ || ($this->Auth()->hasPermission('monitoring/command/schedule-check/active-only')
+ && $activeChecksEnabled
+ )
+ ) {
+ $checkNowForm = new CheckNowCommandForm();
+ $checkNowForm
+ ->setObjects($this->hostList)
+ ->handleRequest();
+ $this->view->checkNowForm = $checkNowForm;
+ }
+
+ $acknowledgedObjects = $this->hostList->getAcknowledgedObjects();
+ if ($acknowledgedObjects->count()) {
+ $removeAckForm = new RemoveAcknowledgementCommandForm();
+ $removeAckForm
+ ->setObjects($acknowledgedObjects)
+ ->handleRequest();
+ $this->view->removeAckForm = $removeAckForm;
+ }
+
+ $featureStatus = $this->hostList->getFeatureStatus();
+ $toggleFeaturesForm = new ToggleObjectFeaturesCommandForm(array(
+ 'backend' => $this->backend,
+ 'objects' => $this->hostList
+ ));
+ $toggleFeaturesForm
+ ->load((object) $featureStatus)
+ ->handleRequest();
+ $this->view->toggleFeaturesForm = $toggleFeaturesForm;
+
+ $hostStates = $this->hostList->getStateSummary();
+
+ if ($activeChecksEnabled) {
+ $this->view->rescheduleAllLink = Url::fromRequest()
+ ->setPath('monitoring/hosts/reschedule-check')
+ ->addParams(['host_active_checks_enabled' => true]);
+ }
+
+ $this->view->downtimeAllLink = Url::fromRequest()->setPath('monitoring/hosts/schedule-downtime');
+ $this->view->processCheckResultAllLink = Url::fromRequest()->setPath('monitoring/hosts/process-check-result');
+ $this->view->addCommentLink = Url::fromRequest()->setPath('monitoring/hosts/add-comment');
+ $this->view->stats = $hostStates;
+ $this->view->objects = $this->hostList;
+ $this->view->unhandledObjects = $this->hostList->getUnhandledObjects();
+ $this->view->problemObjects = $this->hostList->getProblemObjects();
+ $this->view->acknowledgeUnhandledLink = Url::fromPath('monitoring/hosts/acknowledge-problem')
+ ->setQueryString($this->hostList->getUnhandledObjects()->objectsFilter()->toQueryString());
+ $this->view->downtimeUnhandledLink = Url::fromPath('monitoring/hosts/schedule-downtime')
+ ->setQueryString($this->hostList->getUnhandledObjects()->objectsFilter()->toQueryString());
+ $this->view->downtimeLink = Url::fromPath('monitoring/hosts/schedule-downtime')
+ ->setQueryString($this->hostList->getProblemObjects()->objectsFilter()->toQueryString());
+ $this->view->acknowledgedObjects = $this->hostList->getAcknowledgedObjects();
+ $this->view->acknowledgeLink = Url::fromPath('monitoring/hosts/acknowledge-problem')
+ ->setQueryString($this->hostList->getUnacknowledgedObjects()->objectsFilter()->toQueryString());
+ $this->view->unacknowledgedObjects = $this->hostList->getUnacknowledgedObjects();
+ $this->view->objectsInDowntime = $this->hostList->getObjectsInDowntime();
+ $this->view->inDowntimeLink = Url::fromPath('monitoring/list/hosts')
+ ->setQueryString(
+ $this->hostList
+ ->getObjectsInDowntime()
+ ->objectsFilter()
+ ->toQueryString()
+ );
+ $this->view->showDowntimesLink = Url::fromPath('monitoring/list/downtimes')
+ ->setQueryString(
+ $this->hostList
+ ->objectsFilter()
+ ->andFilter(FilterEqual::where('object_type', 'host'))
+ ->toQueryString()
+ );
+ $this->view->commentsLink = Url::fromRequest()->setPath('monitoring/list/comments');
+ $this->view->sendCustomNotificationLink = Url::fromRequest()
+ ->setPath('monitoring/hosts/send-custom-notification');
+
+ $this->view->extensionsHtml = array();
+ foreach (Hook::all('Monitoring\DetailviewExtension') as $hook) {
+ /** @var DetailviewExtensionHook $hook */
+ try {
+ $html = $hook->setView($this->view)->getHtmlForObjects($this->hostList);
+ } catch (Exception $e) {
+ $html = $this->view->escape($e->getMessage());
+ }
+
+ if ($html) {
+ $module = $this->view->escape($hook->getModule()->getName());
+ $this->view->extensionsHtml[] =
+ '<div class="icinga-module module-' . $module . '" data-icinga-module="' . $module . '">'
+ . $html
+ . '</div>';
+ }
+ }
+ }
+
+ /**
+ * Add a host comments
+ */
+ public function addCommentAction()
+ {
+ $this->assertPermission('monitoring/command/comment/add');
+
+ $form = new AddCommentCommandForm();
+ $form->setTitle($this->translate('Add Host Comments'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Acknowledge host problems
+ */
+ public function acknowledgeProblemAction()
+ {
+ $this->assertPermission('monitoring/command/acknowledge-problem');
+
+ $form = new AcknowledgeProblemCommandForm();
+ $form->setTitle($this->translate('Acknowledge Host Problems'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Reschedule host checks
+ */
+ public function rescheduleCheckAction()
+ {
+ $this->assertPermission('monitoring/command/schedule-check');
+
+ $form = new ScheduleHostCheckCommandForm();
+ $form->setTitle($this->translate('Reschedule Host Checks'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Schedule host downtimes
+ */
+ public function scheduleDowntimeAction()
+ {
+ $this->assertPermission('monitoring/command/downtime/schedule');
+
+ $form = new ScheduleHostDowntimeCommandForm();
+ $form->setTitle($this->translate('Schedule Host Downtimes'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Submit passive host check results
+ */
+ public function processCheckResultAction()
+ {
+ $this->assertPermission('monitoring/command/process-check-result');
+
+ $form = new ProcessCheckResultCommandForm();
+ $form->setTitle($this->translate('Submit Passive Host Check Results'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Send a custom notification for hosts
+ */
+ public function sendCustomNotificationAction()
+ {
+ $this->assertPermission('monitoring/command/send-custom-notification');
+
+ $form = new SendCustomNotificationCommandForm();
+ $form->setTitle($this->translate('Send Custom Host Notification'));
+ $this->handleCommandForm($form);
+ }
+}
diff --git a/modules/monitoring/application/controllers/ListController.php b/modules/monitoring/application/controllers/ListController.php
new file mode 100644
index 0000000..4729201
--- /dev/null
+++ b/modules/monitoring/application/controllers/ListController.php
@@ -0,0 +1,808 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Security\SecurityException;
+use Icinga\Util\GlobFilter;
+use Icinga\Web\Form;
+use Zend_Form;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\DataView\DataView;
+use Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimeCommandForm;
+use Icinga\Module\Monitoring\Forms\StatehistoryForm;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use Icinga\Web\Widget\Tabs;
+
+class ListController extends Controller
+{
+ /**
+ * @see ActionController::init
+ */
+ public function init()
+ {
+ parent::init();
+ $this->createTabs();
+ }
+
+ /**
+ * Overwrite the backend to use (used for testing)
+ *
+ * @param MonitoringBackend $backend The Backend that should be used for querying
+ */
+ public function setBackend($backend)
+ {
+ $this->backend = $backend;
+ }
+
+ /**
+ * 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';
+ $stateChangeColumn = 'host_last_hard_state_change';
+ } else {
+ $stateColumn = 'host_state';
+ $stateChangeColumn = 'host_last_state_change';
+ }
+
+ $hosts = $this->backend->select()->from('hoststatus', array_merge(array(
+ 'host_icon_image',
+ 'host_icon_image_alt',
+ 'host_name',
+ 'host_display_name',
+ 'host_state' => $stateColumn,
+ 'host_acknowledged',
+ 'host_output',
+ 'host_attempt',
+ 'host_in_downtime',
+ 'host_is_flapping',
+ 'host_state_type',
+ 'host_handled',
+ 'host_last_state_change' => $stateChangeColumn,
+ 'host_notifications_enabled',
+ 'host_active_checks_enabled',
+ 'host_passive_checks_enabled',
+ 'host_check_command',
+ 'host_next_update'
+ ), $this->addColumns()));
+
+ $this->setupPaginationControl($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')
+ ), $hosts);
+ $this->filterQuery($hosts);
+ $this->setupLimitControl();
+
+ $stats = $this->backend->select()->from('hoststatussummary', array(
+ 'hosts_total',
+ 'hosts_up',
+ 'hosts_down',
+ 'hosts_down_handled',
+ 'hosts_down_unhandled',
+ 'hosts_unreachable',
+ 'hosts_unreachable_handled',
+ 'hosts_unreachable_unhandled',
+ 'hosts_pending',
+ ));
+ $this->applyRestriction('monitoring/filter/objects', $stats);
+
+ $this->view->hosts = $hosts;
+ $this->view->stats = $stats;
+ }
+
+ /**
+ * 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';
+ $stateChangeColumn = 'service_last_hard_state_change';
+ } else {
+ $stateColumn = 'service_state';
+ $stateChangeColumn = 'service_last_state_change';
+ }
+
+ $this->setAutorefreshInterval(10);
+
+ $services = $this->backend->select()->from('servicestatus', array_merge(array(
+ 'host_name',
+ 'host_display_name',
+ 'host_state',
+ 'service_description',
+ 'service_display_name',
+ 'service_state' => $stateColumn,
+ 'service_in_downtime',
+ 'service_acknowledged',
+ 'service_handled',
+ 'service_output',
+ 'service_perfdata',
+ 'service_attempt',
+ 'service_last_state_change' => $stateChangeColumn,
+ 'service_icon_image',
+ 'service_icon_image_alt',
+ 'service_is_flapping',
+ 'service_state_type',
+ 'service_handled',
+ 'service_severity',
+ 'service_notifications_enabled',
+ 'service_active_checks_enabled',
+ 'service_passive_checks_enabled',
+ 'service_check_command',
+ 'service_next_update'
+ ), $this->addColumns()));
+
+ $this->setupPaginationControl($services);
+ $this->setupSortControl(array(
+ 'service_severity' => $this->translate('Service Severity'),
+ 'service_state' => $this->translate('Current Service State'),
+ 'service_display_name' => $this->translate('Service Name'),
+ 'service_last_check' => $this->translate('Last Service Check'),
+ 'service_last_state_change' => $this->translate('Last State Change'),
+ 'host_severity' => $this->translate('Host Severity'),
+ 'host_state' => $this->translate('Current Host State'),
+ 'host_display_name' => $this->translate('Hostname'),
+ 'host_address' => $this->translate('Host Address'),
+ 'host_last_check' => $this->translate('Last Host Check')
+ ), $services);
+ $this->filterQuery($services);
+ $this->setupLimitControl();
+
+ $stats = $this->backend->select()->from('servicestatussummary', array(
+ 'services_critical',
+ 'services_critical_handled',
+ 'services_critical_unhandled',
+ 'services_ok',
+ 'services_pending',
+ 'services_total',
+ 'services_unknown',
+ 'services_unknown_handled',
+ 'services_unknown_unhandled',
+ 'services_warning',
+ 'services_warning_handled',
+ '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;
+ }
+ }
+
+ /**
+ * List downtimes
+ */
+ public function downtimesAction()
+ {
+ $this->addTitleTab(
+ 'downtimes',
+ $this->translate('Downtimes'),
+ $this->translate('List downtimes')
+ );
+
+ $this->setAutorefreshInterval(12);
+
+ $downtimes = $this->backend->select()->from('downtime', array(
+ 'id' => 'downtime_internal_id',
+ 'objecttype' => 'object_type',
+ 'comment' => 'downtime_comment',
+ 'author_name' => 'downtime_author_name',
+ 'start' => 'downtime_start',
+ 'scheduled_start' => 'downtime_scheduled_start',
+ 'scheduled_end' => 'downtime_scheduled_end',
+ 'end' => 'downtime_end',
+ 'duration' => 'downtime_duration',
+ 'is_flexible' => 'downtime_is_flexible',
+ 'is_fixed' => 'downtime_is_fixed',
+ 'is_in_effect' => 'downtime_is_in_effect',
+ 'entry_time' => 'downtime_entry_time',
+ 'name' => 'downtime_name',
+ 'host_state',
+ 'service_state',
+ 'host_name',
+ 'service_description',
+ 'host_display_name',
+ 'service_display_name'
+ ));
+
+ $this->setupPaginationControl($downtimes);
+ $this->setupSortControl(array(
+ 'downtime_is_in_effect' => $this->translate('Is In Effect'),
+ 'host_display_name' => $this->translate('Host'),
+ 'service_display_name' => $this->translate('Service'),
+ 'downtime_entry_time' => $this->translate('Entry Time'),
+ 'downtime_author' => $this->translate('Author'),
+ 'downtime_start' => $this->translate('Start Time'),
+ 'downtime_end' => $this->translate('End Time'),
+ 'downtime_scheduled_start' => $this->translate('Scheduled Start'),
+ 'downtime_scheduled_end' => $this->translate('Scheduled End'),
+ 'downtime_duration' => $this->translate('Duration')
+ ), $downtimes);
+ $this->filterQuery($downtimes);
+ $this->setupLimitControl();
+
+ $this->view->downtimes = $downtimes;
+
+ if ($this->Auth()->hasPermission('monitoring/command/downtime/delete')) {
+ $this->view->delDowntimeForm = new DeleteDowntimeCommandForm();
+ $this->view->delDowntimeForm->handleRequest();
+ }
+ }
+
+ /**
+ * List notifications
+ */
+ public function notificationsAction()
+ {
+ $this->addTitleTab(
+ 'notifications',
+ $this->translate('Notifications'),
+ $this->translate('List notifications')
+ );
+
+ $this->setAutorefreshInterval(15);
+
+ $notifications = $this->backend->select()->from('notification', array(
+ 'id',
+ 'host_display_name',
+ 'host_name',
+ 'notification_contact_name',
+ 'notification_output',
+ 'notification_state',
+ 'notification_timestamp',
+ 'service_description',
+ 'service_display_name'
+ ));
+
+ $this->setupPaginationControl($notifications);
+ $this->setupSortControl(array(
+ 'notification_timestamp' => $this->translate('Notification Start')
+ ), $notifications);
+ $this->filterQuery($notifications);
+ $this->setupLimitControl();
+
+ $this->view->notifications = $notifications;
+ }
+
+ /**
+ * List contacts
+ */
+ public function contactsAction()
+ {
+ if (! $this->hasPermission('*') && $this->hasPermission('no-monitoring/contacts')) {
+ throw new SecurityException('No permission for %s', 'monitoring/contacts');
+ }
+
+ $this->addTitleTab(
+ 'contacts',
+ $this->translate('Contacts'),
+ $this->translate('List contacts')
+ );
+
+ $contacts = $this->backend->select()->from('contact', array(
+ 'contact_name',
+ 'contact_alias',
+ 'contact_email',
+ 'contact_pager',
+ 'contact_notify_service_timeperiod',
+ 'contact_notify_host_timeperiod'
+ ));
+
+ $this->setupPaginationControl($contacts);
+ $this->setupSortControl(array(
+ 'contact_name' => $this->translate('Name'),
+ 'contact_alias' => $this->translate('Alias'),
+ 'contact_email' => $this->translate('Email'),
+ 'contact_pager' => $this->translate('Pager Address / Number')
+ ), $contacts);
+ $this->filterQuery($contacts);
+ $this->setupLimitControl();
+
+ $this->view->contacts = $contacts;
+ }
+
+ public function eventgridAction()
+ {
+ $this->addTitleTab('eventgrid', $this->translate('Event Grid'), $this->translate('Show the Event Grid'));
+
+ $form = new StatehistoryForm();
+ $form->setEnctype(Zend_Form::ENCTYPE_URLENCODED);
+ $form->setMethod('get');
+ $form->setTokenDisabled();
+ $form->setUidDisabled();
+ $form->render();
+ $this->view->form = $form;
+
+ $this->params
+ ->remove('showCompact')
+ ->remove('format');
+ $orientation = $this->params->shift('vertical', 0) ? 'vertical' : 'horizontal';
+/*
+ $orientationBox = new SelectBox(
+ 'orientation',
+ array(
+ '0' => mt('monitoring', 'Vertical'),
+ '1' => mt('monitoring', 'Horizontal')
+ ),
+ mt('monitoring', 'Orientation'),
+ 'horizontal'
+ );
+ $orientationBox->applyRequest($this->getRequest());
+*/
+ $objectType = $form->getValue('objecttype');
+ $from = $form->getValue('from');
+ $query = $this->backend->select()->from(
+ 'eventgrid' . $objectType,
+ array('day', $form->getValue('state'))
+ );
+ $this->params->remove(array('objecttype', 'from', 'to', 'state', 'btn_submit'));
+ $this->view->filter = Filter::fromQueryString((string) $this->params);
+ $query->applyFilter($this->view->filter);
+ $query->applyFilter(Filter::fromQueryString('timestamp>=' . $from));
+ $this->applyRestriction('monitoring/filter/objects', $query);
+ $this->view->summary = $query;
+ $this->view->column = $form->getValue('state');
+// $this->view->orientationBox = $orientationBox;
+ $this->view->orientation = $orientation;
+ }
+
+ /**
+ * List contact groups
+ */
+ public function contactgroupsAction()
+ {
+ if (! $this->hasPermission('*') && $this->hasPermission('no-monitoring/contacts')) {
+ throw new SecurityException('No permission for %s', 'monitoring/contacts');
+ }
+
+ $this->addTitleTab(
+ 'contactgroups',
+ $this->translate('Contact Groups'),
+ $this->translate('List contact groups')
+ );
+
+ $contactGroups = $this->backend->select()->from('contactgroup', array(
+ 'contactgroup_name',
+ 'contactgroup_alias',
+ 'contact_count'
+ ));
+
+ $this->setupPaginationControl($contactGroups);
+ $this->setupSortControl(array(
+ 'contactgroup_name' => $this->translate('Contactgroup Name'),
+ 'contactgroup_alias' => $this->translate('Contactgroup Alias')
+ ), $contactGroups);
+ $this->filterQuery($contactGroups);
+ $this->setupLimitControl();
+
+ $this->view->contactGroups = $contactGroups;
+ }
+
+ /**
+ * List all comments
+ */
+ public function commentsAction()
+ {
+ $this->addTitleTab(
+ 'comments',
+ $this->translate('Comments'),
+ $this->translate('List comments')
+ );
+
+ $this->setAutorefreshInterval(12);
+
+ $comments = $this->backend->select()->from('comment', array(
+ 'id' => 'comment_internal_id',
+ 'objecttype' => 'object_type',
+ 'comment' => 'comment_data',
+ 'author' => 'comment_author_name',
+ 'timestamp' => 'comment_timestamp',
+ 'type' => 'comment_type',
+ 'persistent' => 'comment_is_persistent',
+ 'expiration' => 'comment_expiration',
+ 'name' => 'comment_name',
+ 'host_name',
+ 'service_description',
+ 'host_display_name',
+ 'service_display_name'
+ ));
+
+ $this->setupPaginationControl($comments);
+ $this->setupSortControl(
+ array(
+ 'comment_timestamp' => $this->translate('Comment Timestamp'),
+ 'host_display_name' => $this->translate('Host'),
+ 'service_display_name' => $this->translate('Service'),
+ 'comment_type' => $this->translate('Comment Type'),
+ 'comment_expiration' => $this->translate('Expiration')
+ ),
+ $comments
+ );
+ $this->filterQuery($comments);
+ $this->setupLimitControl();
+
+ $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(
+ 'servicegroups',
+ $this->translate('Service Groups'),
+ $this->translate('List service groups')
+ );
+
+ $this->setAutorefreshInterval(12);
+
+ $serviceGroups = $this->backend->select()->from('servicegroupsummary', array(
+ 'servicegroup_alias',
+ 'servicegroup_name',
+ 'services_critical_handled',
+ 'services_critical_unhandled',
+ 'services_ok',
+ 'services_pending',
+ 'services_total',
+ 'services_unknown_handled',
+ 'services_unknown_unhandled',
+ 'services_warning_handled',
+ 'services_warning_unhandled'
+ ));
+
+ $this->setupPaginationControl($serviceGroups);
+ $this->setupSortControl(array(
+ 'servicegroup_alias' => $this->translate('Service Group Name'),
+ 'services_severity' => $this->translate('Severity'),
+ 'services_total' => $this->translate('Total Services')
+ ), $serviceGroups);
+ $this->filterQuery($serviceGroups);
+ $this->setupLimitControl();
+
+ $this->view->serviceGroups = $serviceGroups;
+ }
+
+ /**
+ * List service groups
+ */
+ public function servicegroupGridAction()
+ {
+ $this->addTitleTab(
+ 'servicegroup-grid',
+ $this->translate('Service Group Grid'),
+ $this->translate('Show the Service Group Grid')
+ );
+
+ $this->setAutorefreshInterval(15);
+
+ $serviceGroups = $this->backend->select()->from('servicegroupsummary', array(
+ 'servicegroup_alias',
+ 'servicegroup_name',
+ 'services_critical_handled',
+ 'services_critical_unhandled',
+ 'services_ok',
+ 'services_pending',
+ 'services_total',
+ 'services_unknown_handled',
+ 'services_unknown_unhandled',
+ 'services_warning_handled',
+ 'services_warning_unhandled'
+ ));
+ $this->filterQuery($serviceGroups);
+
+ $this->setupSortControl(array(
+ 'servicegroup_alias' => $this->translate('Service Group Name'),
+ 'services_severity' => $this->translate('Severity'),
+ 'services_total' => $this->translate('Total Services')
+ ), $serviceGroups, ['services_severity' => 'desc']);
+
+ $this->view->serviceGroups = $serviceGroups;
+ }
+
+ /**
+ * List host groups
+ */
+ public function hostgroupsAction()
+ {
+ $this->addTitleTab(
+ 'hostgroups',
+ $this->translate('Host Groups'),
+ $this->translate('List host groups')
+ );
+
+ $this->setAutorefreshInterval(12);
+
+ $hostGroups = $this->backend->select()->from('hostgroupsummary', array(
+ 'hostgroup_alias',
+ 'hostgroup_name',
+ 'hosts_down_handled',
+ 'hosts_down_unhandled',
+ 'hosts_pending',
+ 'hosts_total',
+ 'hosts_unreachable_handled',
+ 'hosts_unreachable_unhandled',
+ 'hosts_up',
+ 'services_critical_handled',
+ 'services_critical_unhandled',
+ 'services_ok',
+ 'services_pending',
+ 'services_total',
+ 'services_unknown_handled',
+ 'services_unknown_unhandled',
+ 'services_warning_handled',
+ 'services_warning_unhandled'
+ ));
+
+ $this->setupPaginationControl($hostGroups);
+ $this->setupSortControl(array(
+ 'hostgroup_alias' => $this->translate('Host Group Name'),
+ 'hosts_severity' => $this->translate('Severity'),
+ 'hosts_total' => $this->translate('Total Hosts'),
+ 'services_total' => $this->translate('Total Services')
+ ), $hostGroups);
+ $this->filterQuery($hostGroups);
+ $this->setupLimitControl();
+
+ $this->view->hostGroups = $hostGroups;
+ }
+
+ /**
+ * List host groups
+ */
+ public function hostgroupGridAction()
+ {
+ $this->addTitleTab(
+ 'hostgroup-grid',
+ $this->translate('Host Group Grid'),
+ $this->translate('Show the Host Group Grid')
+ );
+
+ $this->setAutorefreshInterval(15);
+
+ $hostGroups = $this->backend->select()->from('hostgroupsummary', [
+ 'hostgroup_alias',
+ 'hostgroup_name',
+ 'hosts_down_handled',
+ 'hosts_down_unhandled',
+ 'hosts_pending',
+ 'hosts_total',
+ 'hosts_unreachable_handled',
+ 'hosts_unreachable_unhandled',
+ 'hosts_up'
+ ]);
+ $this->filterQuery($hostGroups);
+
+ $this->setupSortControl([
+ 'hosts_severity' => $this->translate('Severity'),
+ 'hostgroup_alias' => $this->translate('Host Group Name'),
+ 'hosts_total' => $this->translate('Total Hosts'),
+ 'services_total' => $this->translate('Total Services')
+ ], $hostGroups, ['hosts_severity' => 'desc']);
+
+ $this->view->hostGroups = $hostGroups;
+ }
+
+ public function eventhistoryAction()
+ {
+ $this->addTitleTab(
+ 'eventhistory',
+ $this->translate('Event Overview'),
+ $this->translate('List event records')
+ );
+
+ $query = $this->backend->select()->from('eventhistory', array(
+ 'id',
+ 'host_name',
+ 'host_display_name',
+ 'service_description',
+ 'service_display_name',
+ 'object_type',
+ 'timestamp',
+ 'state',
+ 'output',
+ 'type'
+ ));
+
+ $this->view->history = $query;
+
+ $this->setupSortControl(array(
+ 'timestamp' => $this->translate('Occurence')
+ ), $query);
+ $this->filterQuery($query);
+ $this->setupLimitControl();
+ }
+
+ public function servicegridAction()
+ {
+ if ($this->params->has('noscript_apply')) {
+ $this->redirectNow($this->getRequest()->getUrl()->without('noscript_apply'));
+ }
+
+ $this->addTitleTab('servicegrid', $this->translate('Service Grid'), $this->translate('Show the Service Grid'));
+ $this->setAutorefreshInterval(15);
+ $query = $this->backend->select()->from('servicestatus', array(
+ 'host_display_name',
+ 'host_name',
+ 'service_description',
+ 'service_display_name',
+ 'service_handled',
+ 'service_output',
+ 'service_state'
+ ));
+ $this->filterQuery($query);
+ $filter = (bool) $this->params->shift('problems', false) ? Filter::where('service_problem', 1) : null;
+
+ $this->view->problemToggle = $problemToggle = new Form(['method' => 'GET']);
+ $problemToggle->setUidDisabled();
+ $problemToggle->setTokenDisabled();
+ $problemToggle->setAttrib('class', 'filter-toggle inline icinga-controls');
+ $problemToggle->addElement('checkbox', 'problems', [
+ 'disableHidden' => true,
+ 'autosubmit' => true,
+ 'value' => $filter !== null,
+ 'label' => $this->translate('Problems Only'),
+ 'decorators' => ['ViewHelper', ['Label', ['placement' => 'APPEND']]]
+ ]);
+
+ if ($this->params->get('flipped', false)) {
+ $pivot = $query
+ ->pivot(
+ 'host_name',
+ 'service_description',
+ $filter,
+ $filter ? clone $filter : null
+ )
+ ->setYAxisHeader('service_display_name')
+ ->setXAxisHeader('host_display_name');
+ } else {
+ $pivot = $query
+ ->pivot(
+ 'service_description',
+ 'host_name',
+ $filter,
+ $filter ? clone $filter : null
+ )
+ ->setXAxisHeader('service_display_name')
+ ->setYAxisHeader('host_display_name');
+ }
+ $this->setupSortControl(array(
+ 'host_display_name' => $this->translate('Hostname'),
+ 'service_display_name' => $this->translate('Service Name')
+ ), $pivot);
+ $this->view->horizontalPaginator = $pivot->paginateXAxis();
+ $this->view->verticalPaginator = $pivot->paginateYAxis();
+ list($pivotData, $pivotHeader) = $pivot->toArray();
+ $this->view->pivotData = $pivotData;
+ $this->view->pivotHeader = $pivotHeader;
+ if ($this->params->get('flipped', false)) {
+ $this->render('servicegrid-flipped');
+ }
+ }
+
+ /**
+ * Apply filters on a DataView
+ *
+ * @param DataView $dataView The DataView to apply filters on
+ *
+ * @return DataView $dataView
+ */
+ protected function filterQuery(DataView $dataView)
+ {
+ $this->setupFilterControl($dataView, null, null, array(
+ 'format', // handleFormatRequest()
+ 'stateType', // hostsAction() and servicesAction()
+ 'addColumns', // addColumns()
+ 'problems', // servicegridAction()
+ 'flipped' // servicegridAction()
+ ));
+
+ if ($this->params->get('format') !== 'sql' || $this->hasPermission('config/authentication/roles/show')) {
+ $this->applyRestriction('monitoring/filter/objects', $dataView);
+ }
+
+ $this->handleFormatRequest($dataView);
+
+ return $dataView;
+ }
+
+ /**
+ * Get columns to be added from URL parameter 'addColumns'
+ * and assign to $this->view->addColumns (as array)
+ *
+ * @return array
+ */
+ protected function addColumns()
+ {
+ $columns = preg_split(
+ '~,~',
+ $this->params->shift('addColumns', ''),
+ -1,
+ PREG_SPLIT_NO_EMPTY
+ );
+
+ $customVars = [];
+ $additionalCols = [];
+ foreach ($columns as $column) {
+ if (preg_match('~^_(host|service)_([a-zA-Z0-9_]+)$~', $column, $m)) {
+ $customVars[$m[1]]['vars'][$m[2]] = null;
+ } else {
+ $additionalCols[] = $column;
+ }
+ }
+
+ if (! empty($customVars)) {
+ $blacklistedProperties = new GlobFilter(
+ $this->getRestrictions('monitoring/blacklist/properties')
+ );
+ $customVars = $blacklistedProperties->removeMatching($customVars);
+ foreach ($customVars as $type => $vars) {
+ foreach ($vars['vars'] as $var => $_) {
+ $additionalCols[] = '_' . $type . '_' . $var;
+ }
+ }
+ }
+
+ $this->view->addColumns = $additionalCols;
+ return $additionalCols;
+ }
+
+ protected function addTitleTab($action, $title, $tip)
+ {
+ $this->getTabs()->add($action, array(
+ 'title' => $tip,
+ 'label' => $title,
+ 'url' => Url::fromRequest()
+ ))->activate($action);
+ $this->view->title = $title;
+ }
+
+ /**
+ * Return all tabs for this controller
+ *
+ * @return Tabs
+ */
+ private function createTabs()
+ {
+ return $this->getTabs()->extend(new OutputFormat())->extend(new DashboardAction())->extend(new MenuAction());
+ }
+}
diff --git a/modules/monitoring/application/controllers/ServiceController.php b/modules/monitoring/application/controllers/ServiceController.php
new file mode 100644
index 0000000..d3eeb1c
--- /dev/null
+++ b/modules/monitoring/application/controllers/ServiceController.php
@@ -0,0 +1,147 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use Icinga\Module\Monitoring\Forms\Command\Object\AcknowledgeProblemCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\AddCommentCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ProcessCheckResultCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleServiceCheckCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleServiceDowntimeCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\SendCustomNotificationCommandForm;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Module\Monitoring\Web\Controller\MonitoredObjectController;
+use Icinga\Web\Hook;
+use Icinga\Web\Navigation\Navigation;
+
+class ServiceController extends MonitoredObjectController
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $commandRedirectUrl = 'monitoring/service/show';
+
+ /**
+ * Fetch the requested service from the monitoring backend
+ */
+ public function init()
+ {
+ $service = new Service(
+ $this->backend,
+ $this->params->getRequired('host'),
+ $this->params->getRequired('service')
+ );
+
+ $this->applyRestriction('monitoring/filter/objects', $service);
+
+ if ($service->fetch() === false) {
+ $this->httpNotFound($this->translate('Service not found'));
+ }
+ $this->object = $service;
+ $this->createTabs();
+ $this->getTabs()->activate('service');
+ $this->view->title = $service->service_display_name;
+ $this->view->defaultTitle = join(' :: ', [
+ $service->host_display_name,
+ $this->translate('Services'),
+ $this->view->defaultTitle
+ ]);
+ }
+
+ /**
+ * Get service actions from hook
+ *
+ * @return Navigation
+ */
+ protected function getServiceActions()
+ {
+ $navigation = new Navigation();
+ foreach (Hook::all('Monitoring\\ServiceActions') as $hook) {
+ $navigation->merge($hook->getNavigation($this->object));
+ }
+
+ return $navigation;
+ }
+
+ /**
+ * Show a service
+ */
+ public function showAction()
+ {
+ $this->view->actions = $this->getServiceActions();
+ parent::showAction();
+ }
+
+
+ /**
+ * Acknowledge a service problem
+ */
+ public function acknowledgeProblemAction()
+ {
+ $this->assertPermission('monitoring/command/acknowledge-problem');
+
+ $form = new AcknowledgeProblemCommandForm();
+ $form->setTitle($this->translate('Acknowledge Service Problem'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Add a service comment
+ */
+ public function addCommentAction()
+ {
+ $this->assertPermission('monitoring/command/comment/add');
+
+ $form = new AddCommentCommandForm();
+ $form->setTitle($this->translate('Add Service Comment'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Reschedule a service check
+ */
+ public function rescheduleCheckAction()
+ {
+ $this->assertPermission('monitoring/command/schedule-check');
+
+ $form = new ScheduleServiceCheckCommandForm();
+ $form->setTitle($this->translate('Reschedule Service Check'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Schedule a service downtime
+ */
+ public function scheduleDowntimeAction()
+ {
+ $this->assertPermission('monitoring/command/downtime/schedule');
+
+ $form = new ScheduleServiceDowntimeCommandForm();
+ $form->setTitle($this->translate('Schedule Service Downtime'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Submit a passive service check result
+ */
+ public function processCheckResultAction()
+ {
+ $this->assertPermission('monitoring/command/process-check-result');
+
+ $form = new ProcessCheckResultCommandForm();
+ $form->setTitle($this->translate('Submit Passive Service Check Result'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Send a custom notification for a service
+ */
+ public function sendCustomNotificationAction()
+ {
+ $this->assertPermission('monitoring/command/send-custom-notification');
+
+ $form = new SendCustomNotificationCommandForm();
+ $form->setTitle($this->translate('Send Custom Service Notification'));
+ $this->handleCommandForm($form);
+ }
+}
diff --git a/modules/monitoring/application/controllers/ServicesController.php b/modules/monitoring/application/controllers/ServicesController.php
new file mode 100644
index 0000000..6df6992
--- /dev/null
+++ b/modules/monitoring/application/controllers/ServicesController.php
@@ -0,0 +1,262 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use Exception;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\Forms\Command\Object\AcknowledgeProblemCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\AddCommentCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\CheckNowCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ObjectsCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ProcessCheckResultCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\RemoveAcknowledgementCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleServiceCheckCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ScheduleServiceDowntimeCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\SendCustomNotificationCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ToggleObjectFeaturesCommandForm;
+use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook;
+use Icinga\Module\Monitoring\Object\ServiceList;
+use Icinga\Web\Hook;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+
+class ServicesController extends Controller
+{
+ /**
+ * @var ServiceList
+ */
+ protected $serviceList;
+
+ public function init()
+ {
+ $serviceList = new ServiceList($this->backend);
+ $this->applyRestriction('monitoring/filter/objects', $serviceList);
+ $serviceList->addFilter(Filter::fromQueryString(
+ (string) $this->params->without(array('service_problem', 'service_handled', 'showCompact'))
+ ));
+ $this->serviceList = $serviceList;
+ $this->serviceList->setColumns(array(
+ 'host_display_name',
+ 'host_handled',
+ 'host_name',
+ 'host_problem',
+ 'host_state',
+ 'instance_name',
+ 'service_acknowledged',
+ 'service_active_checks_enabled',
+ 'service_description',
+ 'service_display_name',
+ 'service_event_handler_enabled',
+ 'service_flap_detection_enabled',
+ 'service_handled',
+ 'service_in_downtime',
+ 'service_is_flapping',
+ 'service_last_state_change',
+ 'service_notifications_enabled',
+ 'service_obsessing',
+ 'service_passive_checks_enabled',
+ 'service_problem',
+ 'service_state'
+ ));
+ $this->view->baseFilter = $this->serviceList->getFilter();
+ $this->view->listAllLink = Url::fromRequest()->setPath('monitoring/list/services');
+ $this->getTabs()->add(
+ 'show',
+ array(
+ 'label' => $this->translate('Services') . sprintf(' (%d)', count($this->serviceList)),
+ 'title' => sprintf(
+ $this->translate('Show summarized information for %u services'),
+ count($this->serviceList)
+ ),
+ 'url' => Url::fromRequest()
+ )
+ )->extend(new DashboardAction())->extend(new MenuAction())->activate('show');
+ $this->view->title = $this->translate('Services');
+ }
+
+ protected function handleCommandForm(ObjectsCommandForm $form)
+ {
+ $form
+ ->setBackend($this->backend)
+ ->setObjects($this->serviceList)
+ ->setRedirectUrl(Url::fromPath('monitoring/services/show')->setParams(
+ $this->params->without('service_active_checks_enabled')
+ ))
+ ->handleRequest();
+
+ $this->view->form = $form;
+ $this->view->objects = $this->serviceList;
+ $this->view->stats = $this->serviceList->getServiceStateSummary();
+ $this->view->serviceStates = true;
+ $this->_helper->viewRenderer('partials/command/objects-command-form', null, true);
+ return $form;
+ }
+
+ public function showAction()
+ {
+ $this->setAutorefreshInterval(15);
+ $activeChecksEnabled = $this->serviceList->getFeatureStatus()['active_checks_enabled'] !== 0;
+ if ($this->Auth()->hasPermission('monitoring/command/schedule-check')
+ || ($this->Auth()->hasPermission('monitoring/command/schedule-check/active-only')
+ && $activeChecksEnabled
+ )
+ ) {
+ $checkNowForm = new CheckNowCommandForm();
+ $checkNowForm
+ ->setObjects($this->serviceList)
+ ->handleRequest();
+ $this->view->checkNowForm = $checkNowForm;
+ }
+
+ $acknowledgedObjects = $this->serviceList->getAcknowledgedObjects();
+ if ($acknowledgedObjects->count()) {
+ $removeAckForm = new RemoveAcknowledgementCommandForm();
+ $removeAckForm
+ ->setObjects($acknowledgedObjects)
+ ->handleRequest();
+ $this->view->removeAckForm = $removeAckForm;
+ }
+
+ $featureStatus = $this->serviceList->getFeatureStatus();
+ $toggleFeaturesForm = new ToggleObjectFeaturesCommandForm(array(
+ 'backend' => $this->backend,
+ 'objects' => $this->serviceList
+ ));
+ $toggleFeaturesForm
+ ->load((object) $featureStatus)
+ ->handleRequest();
+ $this->view->toggleFeaturesForm = $toggleFeaturesForm;
+
+ if ($activeChecksEnabled) {
+ $this->view->rescheduleAllLink = Url::fromRequest()
+ ->setPath('monitoring/services/reschedule-check')
+ ->addParams(['service_active_checks_enabled' => true]);
+ }
+
+ $this->view->downtimeAllLink = Url::fromRequest()->setPath('monitoring/services/schedule-downtime');
+ $this->view->processCheckResultAllLink = Url::fromRequest()->setPath(
+ 'monitoring/services/process-check-result'
+ );
+ $this->view->addCommentLink = Url::fromRequest()->setPath('monitoring/services/add-comment');
+ $this->view->deleteCommentLink = Url::fromRequest()->setPath('monitoring/services/delete-comment');
+ $this->view->stats = $this->serviceList->getServiceStateSummary();
+ $this->view->objects = $this->serviceList;
+ $this->view->unhandledObjects = $this->serviceList->getUnhandledObjects();
+ $this->view->problemObjects = $this->serviceList->getProblemObjects();
+ $this->view->downtimeUnhandledLink = Url::fromPath('monitoring/services/schedule-downtime')
+ ->setQueryString($this->serviceList->getUnhandledObjects()->objectsFilter()->toQueryString());
+ $this->view->downtimeLink = Url::fromPath('monitoring/services/schedule-downtime')
+ ->setQueryString($this->serviceList->getProblemObjects()->objectsFilter()->toQueryString());
+ $this->view->acknowledgedObjects = $acknowledgedObjects;
+ $this->view->acknowledgeLink = Url::fromPath('monitoring/services/acknowledge-problem')
+ ->setQueryString($this->serviceList->getUnacknowledgedObjects()->objectsFilter()->toQueryString());
+ $this->view->unacknowledgedObjects = $this->serviceList->getUnacknowledgedObjects();
+ $this->view->objectsInDowntime = $this->serviceList->getObjectsInDowntime();
+ $this->view->inDowntimeLink = Url::fromPath('monitoring/list/services')
+ ->setQueryString($this->serviceList->getObjectsInDowntime()
+ ->objectsFilter(array('host' => 'host_name', 'service' => 'service_description'))->toQueryString());
+ $this->view->showDowntimesLink = Url::fromPath('monitoring/downtimes/show')
+ ->setQueryString(
+ $this->serviceList->getObjectsInDowntime()
+ ->objectsFilter()->andFilter(Filter::where('object_type', 'service'))->toQueryString()
+ );
+ $this->view->commentsLink = Url::fromRequest()
+ ->setPath('monitoring/list/comments');
+ $this->view->sendCustomNotificationLink = Url::fromRequest()->setPath(
+ 'monitoring/services/send-custom-notification'
+ );
+
+ $this->view->extensionsHtml = array();
+ foreach (Hook::all('Monitoring\DetailviewExtension') as $hook) {
+ /** @var DetailviewExtensionHook $hook */
+ try {
+ $html = $hook->setView($this->view)->getHtmlForObjects($this->serviceList);
+ } catch (Exception $e) {
+ $html = $this->view->escape($e->getMessage());
+ }
+
+ if ($html) {
+ $module = $this->view->escape($hook->getModule()->getName());
+ $this->view->extensionsHtml[] =
+ '<div class="icinga-module module-' . $module . '" data-icinga-module="' . $module . '">'
+ . $html
+ . '</div>';
+ }
+ }
+ }
+
+ /**
+ * Add a service comment
+ */
+ public function addCommentAction()
+ {
+ $this->assertPermission('monitoring/command/comment/add');
+
+ $form = new AddCommentCommandForm();
+ $form->setTitle($this->translate('Add Service Comments'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Acknowledge service problems
+ */
+ public function acknowledgeProblemAction()
+ {
+ $this->assertPermission('monitoring/command/acknowledge-problem');
+
+ $form = new AcknowledgeProblemCommandForm();
+ $form->setTitle($this->translate('Acknowledge Service Problems'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Reschedule service checks
+ */
+ public function rescheduleCheckAction()
+ {
+ $this->assertPermission('monitoring/command/schedule-check');
+
+ $form = new ScheduleServiceCheckCommandForm();
+ $form->setTitle($this->translate('Reschedule Service Checks'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Schedule service downtimes
+ */
+ public function scheduleDowntimeAction()
+ {
+ $this->assertPermission('monitoring/command/downtime/schedule');
+
+ $form = new ScheduleServiceDowntimeCommandForm();
+ $form->setTitle($this->translate('Schedule Service Downtimes'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Submit passive service check results
+ */
+ public function processCheckResultAction()
+ {
+ $this->assertPermission('monitoring/command/process-check-result');
+
+ $form = new ProcessCheckResultCommandForm();
+ $form->setTitle($this->translate('Submit Passive Service Check Results'));
+ $this->handleCommandForm($form);
+ }
+
+ /**
+ * Send a custom notification for services
+ */
+ public function sendCustomNotificationAction()
+ {
+ $this->assertPermission('monitoring/command/send-custom-notification');
+
+ $form = new SendCustomNotificationCommandForm();
+ $form->setTitle($this->translate('Send Custom Service Notification'));
+ $this->handleCommandForm($form);
+ }
+}
diff --git a/modules/monitoring/application/controllers/ShowController.php b/modules/monitoring/application/controllers/ShowController.php
new file mode 100644
index 0000000..f1da561
--- /dev/null
+++ b/modules/monitoring/application/controllers/ShowController.php
@@ -0,0 +1,101 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use Icinga\Data\Filter\FilterEqual;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Security\SecurityException;
+use Icinga\Web\Url;
+
+/**
+ * Class Monitoring_ShowController
+ *
+ * Actions for show context
+ */
+class ShowController extends Controller
+{
+ /**
+ * @var MonitoringBackend
+ */
+ protected $backend;
+
+ public function init()
+ {
+ $this->view->defaultTitle = $this->translate('Contacts') . ' :: ' . $this->view->defaultTitle;
+
+ parent::init();
+ }
+
+ public function contactAction()
+ {
+ if (! $this->hasPermission('*') && $this->hasPermission('no-monitoring/contacts')) {
+ throw new SecurityException('No permission for %s', 'monitoring/contacts');
+ }
+
+ $contactName = $this->params->getRequired('contact_name');
+
+ $this->getTabs()->add('contact-detail', [
+ 'title' => $this->translate('Contact details'),
+ 'label' => $this->translate('Contact'),
+ 'url' => Url::fromRequest(),
+ 'active' => true
+ ]);
+
+ $query = $this->backend->select()->from('contact', array(
+ 'contact_name',
+ 'contact_id',
+ 'contact_alias',
+ 'contact_email',
+ 'contact_pager',
+ 'contact_notify_service_timeperiod',
+ 'contact_notify_service_recovery',
+ 'contact_notify_service_warning',
+ 'contact_notify_service_critical',
+ 'contact_notify_service_unknown',
+ 'contact_notify_service_flapping',
+ 'contact_notify_service_downtime',
+ 'contact_notify_host_timeperiod',
+ 'contact_notify_host_recovery',
+ 'contact_notify_host_down',
+ 'contact_notify_host_unreachable',
+ 'contact_notify_host_flapping',
+ 'contact_notify_host_downtime',
+ ));
+ $this->applyRestriction('monitoring/filter/objects', $query);
+ $query->whereEx(new FilterEqual('contact_name', '=', $contactName));
+ $contact = $query->getQuery()->fetchRow();
+
+ if ($contact) {
+ $commands = $this->backend->select()->from('command', array(
+ 'command_line',
+ 'command_name'
+ ))->where('contact_id', $contact->contact_id);
+
+ $this->view->commands = $commands;
+
+ $notifications = $this->backend->select()->from('notification', array(
+ 'id',
+ 'host_name',
+ 'service_description',
+ 'notification_output',
+ 'notification_contact_name',
+ 'notification_timestamp',
+ 'notification_state',
+ 'host_display_name',
+ 'service_display_name'
+ ));
+
+ $notifications->where('notification_contact_name', $contactName);
+ $this->applyRestriction('monitoring/filter/objects', $notifications);
+ $this->view->notifications = $notifications;
+ $this->setupLimitControl();
+ $this->setupPaginationControl($this->view->notifications);
+ $this->view->title = $contact->contact_name;
+ }
+
+ $this->view->contact = $contact;
+ $this->view->contactName = $contactName;
+ }
+}
diff --git a/modules/monitoring/application/controllers/TacticalController.php b/modules/monitoring/application/controllers/TacticalController.php
new file mode 100644
index 0000000..b147d45
--- /dev/null
+++ b/modules/monitoring/application/controllers/TacticalController.php
@@ -0,0 +1,128 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use Icinga\Chart\Donut;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+
+class TacticalController extends Controller
+{
+ public function indexAction()
+ {
+ $this->setAutorefreshInterval(15);
+
+ $this->view->title = $this->translate('Tactical Overview');
+ $this->getTabs()->add(
+ 'tactical_overview',
+ array(
+ 'title' => $this->translate(
+ 'Show an overview of all hosts and services, their current'
+ . ' states and monitoring feature utilisation'
+ ),
+ 'label' => $this->translate('Tactical Overview'),
+ 'url' => Url::fromRequest()
+ )
+ )->extend(new DashboardAction())->extend(new MenuAction())->activate('tactical_overview');
+
+ $stats = $this->backend->select()->from(
+ 'statussummary',
+ array(
+ 'hosts_up',
+ 'hosts_down_handled',
+ 'hosts_down_unhandled',
+ 'hosts_unreachable_handled',
+ 'hosts_unreachable_unhandled',
+ 'hosts_pending',
+ 'hosts_pending_not_checked',
+ 'hosts_not_checked',
+
+ 'services_ok',
+ 'services_warning_handled',
+ 'services_warning_unhandled',
+ 'services_critical_handled',
+ 'services_critical_unhandled',
+ 'services_unknown_handled',
+ 'services_unknown_unhandled',
+ 'services_pending',
+ 'services_pending_not_checked',
+ 'services_not_checked',
+ )
+ );
+ $this->applyRestriction('monitoring/filter/objects', $stats);
+
+ $this->setupFilterControl($stats, null, ['host', 'service'], ['format']);
+ $this->view->setHelperFunction('filteredUrl', function ($path, array $params) {
+ $filter = clone $this->view->filterEditor->getFilter();
+
+ return $this->view->url($path)->setParams($params)->addFilter($filter);
+ });
+
+ $this->handleFormatRequest($stats);
+ $summary = $stats->fetchRow();
+
+ // Correct pending counts. Done here instead of in the query for compatibility reasons.
+ $summary->hosts_pending -= $summary->hosts_pending_not_checked;
+ $summary->services_pending -= $summary->services_pending_not_checked;
+
+ $hostSummaryChart = new Donut();
+ $hostSummaryChart
+ ->addSlice($summary->hosts_up, array('class' => 'slice-state-ok'))
+ ->addSlice($summary->hosts_down_handled, array('class' => 'slice-state-critical-handled'))
+ ->addSlice($summary->hosts_down_unhandled, array('class' => 'slice-state-critical'))
+ ->addSlice($summary->hosts_unreachable_handled, array('class' => 'slice-state-unreachable-handled'))
+ ->addSlice($summary->hosts_unreachable_unhandled, array('class' => 'slice-state-unreachable'))
+ ->addSlice($summary->hosts_pending, array('class' => 'slice-state-pending'))
+ ->addSlice($summary->hosts_pending_not_checked, array('class' => 'slice-state-not-checked'))
+ ->setLabelBig($summary->hosts_down_unhandled)
+ ->setLabelBigEyeCatching($summary->hosts_down_unhandled > 0)
+ ->setLabelSmall($this->translate('Hosts Down'));
+
+ $serviceSummaryChart = new Donut();
+ $serviceSummaryChart
+ ->addSlice($summary->services_ok, array('class' => 'slice-state-ok'))
+ ->addSlice($summary->services_warning_handled, array('class' => 'slice-state-warning-handled'))
+ ->addSlice($summary->services_warning_unhandled, array('class' => 'slice-state-warning'))
+ ->addSlice($summary->services_critical_handled, array('class' => 'slice-state-critical-handled'))
+ ->addSlice($summary->services_critical_unhandled, array('class' => 'slice-state-critical'))
+ ->addSlice($summary->services_unknown_handled, array('class' => 'slice-state-unknown-handled'))
+ ->addSlice($summary->services_unknown_unhandled, array('class' => 'slice-state-unknown'))
+ ->addSlice($summary->services_pending, array('class' => 'slice-state-pending'))
+ ->addSlice($summary->services_pending_not_checked, array('class' => 'slice-state-not-checked'))
+ ->setLabelBig($summary->services_critical_unhandled ?: $summary->services_unknown_unhandled)
+ ->setLabelBigState($summary->services_critical_unhandled > 0 ? 'critical' : (
+ $summary->services_unknown_unhandled > 0 ? 'unknown' : null
+ ))
+ ->setLabelSmall($summary->services_critical_unhandled > 0 || $summary->services_unknown_unhandled < 1
+ ? $this->translate('Services Critical')
+ : $this->translate('Services Unknown'));
+
+ $this->view->hostStatusSummaryChart = $hostSummaryChart
+ ->setLabelBigUrl($this->view->filteredUrl(
+ 'monitoring/list/hosts',
+ array(
+ 'host_state' => 1,
+ 'host_handled' => 0,
+ 'sort' => 'host_last_check',
+ 'dir' => 'asc'
+ )
+ ))
+ ->render();
+ $this->view->serviceStatusSummaryChart = $serviceSummaryChart
+ ->setLabelBigUrl($this->view->filteredUrl(
+ 'monitoring/list/services',
+ array(
+ 'service_state' => $summary->services_critical_unhandled > 0
+ || ! $summary->services_unknown_unhandled ? 2 : 3,
+ 'service_handled' => 0,
+ 'sort' => 'service_last_check',
+ 'dir' => 'asc'
+ )
+ ))
+ ->render();
+ $this->view->statusSummary = $summary;
+ }
+}
diff --git a/modules/monitoring/application/controllers/TimelineController.php b/modules/monitoring/application/controllers/TimelineController.php
new file mode 100644
index 0000000..deeeb36
--- /dev/null
+++ b/modules/monitoring/application/controllers/TimelineController.php
@@ -0,0 +1,325 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Controllers;
+
+use DateInterval;
+use DateTime;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\Timeline\TimeLine;
+use Icinga\Module\Monitoring\Timeline\TimeRange;
+use Icinga\Module\Monitoring\Web\Widget\SelectBox;
+use Icinga\Util\Format;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+
+class TimelineController extends Controller
+{
+ public function indexAction()
+ {
+ $this->getTabs()->add(
+ 'timeline',
+ array(
+ 'title' => $this->translate('Show the number of historical event records grouped by time and type'),
+ 'label' => $this->translate('Timeline'),
+ 'url' => Url::fromRequest()
+ )
+ )->extend(new DashboardAction())->extend(new MenuAction())->activate('timeline');
+ $this->view->title = $this->translate('Timeline');
+
+ // TODO: filter for hard_states (precedence adjustments necessary!)
+ $this->setupIntervalBox();
+ list($displayRange, $forecastRange) = $this->buildTimeRanges();
+
+ $detailUrl = Url::fromPath('monitoring/list/eventhistory');
+
+ $timeline = new TimeLine(
+ $this->applyRestriction(
+ 'monitoring/filter/objects',
+ $this->backend->select()->from(
+ 'eventhistory',
+ array(
+ 'name' => 'type',
+ 'time' => 'timestamp'
+ )
+ )
+ ),
+ array(
+ 'notification_ack' => array(
+ 'class' => 'timeline-notification',
+ 'detailUrl' => $detailUrl,
+ 'label' => mt('monitoring', 'Notifications'),
+ 'groupBy' => 'notification_*'
+ ),
+ 'notification_flapping' => array(
+ 'class' => 'timeline-notification',
+ 'detailUrl' => $detailUrl,
+ 'label' => mt('monitoring', 'Notifications'),
+ 'groupBy' => 'notification_*'
+ ),
+ 'notification_flapping_end' => array(
+ 'class' => 'timeline-notification',
+ 'detailUrl' => $detailUrl,
+ 'label' => mt('monitoring', 'Notifications'),
+ 'groupBy' => 'notification_*'
+ ),
+ 'notification_dt_start' => array(
+ 'class' => 'timeline-notification',
+ 'detailUrl' => $detailUrl,
+ 'label' => mt('monitoring', 'Notifications'),
+ 'groupBy' => 'notification_*'
+ ),
+ 'notification_dt_end' => array(
+ 'class' => 'timeline-notification',
+ 'detailUrl' => $detailUrl,
+ 'label' => mt('monitoring', 'Notifications'),
+ 'groupBy' => 'notification_*'
+ ),
+ 'notification_custom' => array(
+ 'class' => 'timeline-notification',
+ 'detailUrl' => $detailUrl,
+ 'label' => mt('monitoring', 'Notifications'),
+ 'groupBy' => 'notification_*'
+ ),
+ 'notification_state' => array(
+ 'class' => 'timeline-notification',
+ 'detailUrl' => $detailUrl,
+ 'label' => mt('monitoring', 'Notifications'),
+ 'groupBy' => 'notification_*'
+ ),
+ 'hard_state' => array(
+ 'class' => 'timeline-hard-state',
+ 'detailUrl' => $detailUrl,
+ 'label' => mt('monitoring', 'Hard state changes')
+ ),
+ 'comment' => array(
+ 'class' => 'timeline-comment',
+ 'detailUrl' => $detailUrl,
+ 'label' => mt('monitoring', 'Comments')
+ ),
+ 'ack' => array(
+ 'class' => 'timeline-ack',
+ 'detailUrl' => $detailUrl,
+ 'label' => mt('monitoring', 'Acknowledgements')
+ ),
+ 'dt_start' => array(
+ 'class' => 'timeline-downtime-start',
+ 'detailUrl' => $detailUrl,
+ 'label' => mt('monitoring', 'Started downtimes')
+ ),
+ 'dt_end' => array(
+ 'class' => 'timeline-downtime-end',
+ 'detailUrl' => $detailUrl,
+ 'label' => mt('monitoring', 'Ended downtimes')
+ )
+ )
+ );
+ $timeline->setMaximumCircleWidth('6em');
+ $timeline->setMinimumCircleWidth('0.3em');
+ $timeline->setDisplayRange($displayRange);
+ $timeline->setForecastRange($forecastRange);
+ $beingExtended = $this->getRequest()->getParam('extend') == 1;
+ $timeline->setSession($this->Window()->getSessionNamespace('timeline', !$beingExtended));
+
+ $this->view->timeline = $timeline;
+ $this->view->nextRange = $forecastRange;
+ $this->view->beingExtended = $beingExtended;
+ $this->view->intervalFormat = $this->getIntervalFormat();
+ $oldBase = $timeline->getCalculationBase(false);
+ $this->view->switchedContext = $oldBase !== null && $oldBase !== $timeline->getCalculationBase(true);
+ }
+
+ /**
+ * Create a select box the user can choose the timeline interval from
+ */
+ private function setupIntervalBox()
+ {
+ $box = new SelectBox(
+ 'intervalBox',
+ array(
+ '4h' => mt('monitoring', '4 Hours'),
+ '1d' => mt('monitoring', 'One day'),
+ '1w' => mt('monitoring', 'One week'),
+ '1m' => mt('monitoring', 'One month'),
+ '1y' => mt('monitoring', 'One year')
+ ),
+ mt('monitoring', 'TimeLine interval'),
+ 'interval'
+ );
+ $box->applyRequest($this->getRequest());
+ $this->view->intervalBox = $box;
+ }
+
+ /**
+ * Return the chosen interval
+ *
+ * @return DateInterval The chosen interval
+ */
+ private function getTimelineInterval()
+ {
+ switch ($this->view->intervalBox->getInterval()) {
+ case '1d':
+ return new DateInterval('P1D');
+ case '1w':
+ return new DateInterval('P1W');
+ case '1m':
+ return new DateInterval('P1M');
+ case '1y':
+ return new DateInterval('P1Y');
+ default:
+ return new DateInterval('PT4H');
+ }
+ }
+
+ /**
+ * Get an appropriate datetime format string for the chosen interval
+ *
+ * @return string
+ */
+ private function getIntervalFormat()
+ {
+ switch ($this->view->intervalBox->getInterval()) {
+ case '1d':
+ return $this->getDateFormat();
+ case '1w':
+ return '\W\e\ek W\<b\r\>\of Y';
+ case '1m':
+ return 'F Y';
+ case '1y':
+ return 'Y';
+ default:
+ return $this->getDateFormat() . '\<b\r\>' . $this->getTimeFormat();
+ }
+ }
+
+ /**
+ * Return a preload interval based on the chosen timeline interval and the given date and time
+ *
+ * @param DateTime $dateTime The date and time to use
+ *
+ * @return DateInterval The interval to pre-load
+ */
+ private function getPreloadInterval(DateTime $dateTime)
+ {
+ switch ($this->view->intervalBox->getInterval()) {
+ case '1d':
+ return DateInterval::createFromDateString('1 week -1 second');
+ case '1w':
+ return DateInterval::createFromDateString('8 weeks -1 second');
+ case '1m':
+ $dateCopy = clone $dateTime;
+ for ($i = 0; $i < 6; $i++) {
+ $dateCopy->sub(new DateInterval('PT' . Format::secondsByMonth($dateCopy) . 'S'));
+ }
+ return $dateCopy->add(new DateInterval('PT1S'))->diff($dateTime);
+ case '1y':
+ $dateCopy = clone $dateTime;
+ for ($i = 0; $i < 4; $i++) {
+ $dateCopy->sub(new DateInterval('PT' . Format::secondsByYear($dateCopy) . 'S'));
+ }
+ return $dateCopy->add(new DateInterval('PT1S'))->diff($dateTime);
+ default:
+ return DateInterval::createFromDateString('1 day -1 second');
+ }
+ }
+
+ /**
+ * Extrapolate the given datetime based on the chosen timeline interval
+ *
+ * @param DateTime $dateTime The datetime to extrapolate
+ */
+ private function extrapolateDateTime(DateTime &$dateTime)
+ {
+ switch ($this->view->intervalBox->getInterval()) {
+ case '1d':
+ $dateTime->setTimestamp(strtotime('tomorrow', $dateTime->getTimestamp()) - 1);
+ break;
+ case '1w':
+ $dateTime->setTimestamp(strtotime('next monday', $dateTime->getTimestamp()) - 1);
+ break;
+ case '1m':
+ $dateTime->setTimestamp(
+ strtotime(
+ 'last day of this month',
+ strtotime(
+ 'tomorrow',
+ $dateTime->getTimestamp()
+ ) - 1
+ )
+ );
+ break;
+ case '1y':
+ $dateTime->setTimestamp(strtotime('1 january next year', $dateTime->getTimestamp()) - 1);
+ break;
+ default:
+ $hour = $dateTime->format('G');
+ $end = $hour < 4 ? 4 : ($hour < 8 ? 8 : ($hour < 12 ? 12 : ($hour < 16 ? 16 : ($hour < 20 ? 20 : 24))));
+ $dateTime = DateTime::createFromFormat(
+ 'd/m/y G:i:s',
+ $dateTime->format('d/m/y') . ($end - 1) . ':59:59'
+ );
+ }
+ }
+
+ /**
+ * Return a display- and forecast time range
+ *
+ * Assembles a time range each for display and forecast purposes based on the start- and
+ * end time if given in the current request otherwise based on the current time and a
+ * end time that is calculated based on the chosen timeline interval.
+ *
+ * @return array The resulting time ranges
+ */
+ private function buildTimeRanges()
+ {
+ $startTime = new DateTime();
+ $startParam = $this->_request->getParam('start');
+ $startTimestamp = is_numeric($startParam) ? intval($startParam) : strtotime($startParam ?? '');
+ if ($startTimestamp !== false) {
+ $startTime->setTimestamp($startTimestamp);
+ } else {
+ $this->extrapolateDateTime($startTime);
+ }
+
+ $endTime = clone $startTime;
+ $endParam = $this->_request->getParam('end');
+ $endTimestamp = is_numeric($endParam) ? intval($endParam) : strtotime($endParam ?? '');
+ if ($endTimestamp !== false) {
+ $endTime->setTimestamp($endTimestamp);
+ } else {
+ $endTime->sub($this->getPreloadInterval($startTime));
+ }
+
+ $forecastStart = clone $endTime;
+ $forecastStart->sub(new DateInterval('PT1S'));
+ $forecastEnd = clone $forecastStart;
+ $forecastEnd->sub($this->getPreloadInterval($forecastStart));
+
+ $timelineInterval = $this->getTimelineInterval();
+ return array(
+ new TimeRange($startTime, $endTime, $timelineInterval),
+ new TimeRange($forecastStart, $forecastEnd, $timelineInterval)
+ );
+ }
+
+ /**
+ * Get the user's preferred time format or the application's default
+ *
+ * @return string
+ */
+ private function getTimeFormat()
+ {
+ return 'H:i';
+ }
+
+ /**
+ * Get the user's preferred date format or the application's default
+ *
+ * @return string
+ */
+ private function getDateFormat()
+ {
+ return 'Y-m-d';
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/CommandForm.php b/modules/monitoring/application/forms/Command/CommandForm.php
new file mode 100644
index 0000000..34391cf
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/CommandForm.php
@@ -0,0 +1,92 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command;
+
+use Icinga\Exception\ConfigurationError;
+use Icinga\Web\Form;
+use Icinga\Web\Request;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Module\Monitoring\Command\Transport\CommandTransport;
+use Icinga\Module\Monitoring\Command\Transport\CommandTransportInterface;
+
+/**
+ * Base class for command forms
+ */
+abstract class CommandForm extends Form
+{
+ /**
+ * Monitoring backend
+ *
+ * @var MonitoringBackend
+ */
+ protected $backend;
+
+ /**
+ * Set the monitoring backend
+ *
+ * @param MonitoringBackend $backend
+ *
+ * @return $this
+ */
+ public function setBackend(MonitoringBackend $backend)
+ {
+ $this->backend = $backend;
+ return $this;
+ }
+
+ /**
+ * Get the monitoring backend
+ *
+ * @return MonitoringBackend
+ */
+ public function getBackend()
+ {
+ return $this->backend;
+ }
+
+ /**
+ * Get the transport used to send commands
+ *
+ * @param Request $request
+ *
+ * @return CommandTransportInterface
+ *
+ * @throws ConfigurationError
+ */
+ public function getTransport(Request $request)
+ {
+ if (($transportName = $request->getParam('transport')) !== null) {
+ $config = CommandTransport::getConfig();
+ if ($config->hasSection($transportName)) {
+ $transport = CommandTransport::createTransport($config->getSection($transportName));
+ } else {
+ throw new ConfigurationError(sprintf(
+ mt('monitoring', 'Command transport "%s" not found.'),
+ $transportName
+ ));
+ }
+ } else {
+ $transport = new CommandTransport();
+ }
+
+ return $transport;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRedirectUrl()
+ {
+ $redirectUrl = parent::getRedirectUrl();
+ // TODO(el): Forms should provide event handling. This is quite hackish
+ $formData = $this->getRequestData();
+ if ($this->wasSent($formData)
+ && (! $this->getSubmitLabel() || $this->isSubmitted())
+ && $this->isValid($formData)
+ ) {
+ $this->getResponse()->setAutoRefreshInterval(1);
+ }
+ return $redirectUrl;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Instance/DisableNotificationsExpireCommandForm.php b/modules/monitoring/application/forms/Command/Instance/DisableNotificationsExpireCommandForm.php
new file mode 100644
index 0000000..ee49962
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Instance/DisableNotificationsExpireCommandForm.php
@@ -0,0 +1,64 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Instance;
+
+use DateTime;
+use DateInterval;
+use Icinga\Module\Monitoring\Command\Instance\DisableNotificationsExpireCommand;
+use Icinga\Module\Monitoring\Forms\Command\CommandForm;
+use Icinga\Web\Notification;
+
+/**
+ * Form for disabling host and service notifications w/ an optional expire date and time on an Icinga instance
+ */
+class DisableNotificationsExpireCommandForm extends CommandForm
+{
+ /**
+ * (non-PHPDoc)
+ * @see \Zend_Form::init() For the method documentation.
+ */
+ public function init()
+ {
+ $this->setRequiredCue(null);
+ $this->setSubmitLabel($this->translate('Disable Notifications'));
+ $this->addDescription($this->translate(
+ 'This command is used to disable host and service notifications for a specific time.'
+ ));
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::createElements() For the method documentation.
+ */
+ public function createElements(array $formData = array())
+ {
+ $expireTime = new DateTime();
+ $expireTime->add(new DateInterval('PT1H'));
+ $this->addElement(
+ 'dateTimePicker',
+ 'expire_time',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Expire Time'),
+ 'description' => $this->translate('Set the expire time.'),
+ 'value' => $expireTime
+ )
+ );
+ return $this;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::onSuccess() For the method documentation.
+ */
+ public function onSuccess()
+ {
+ $disableNotifications = new DisableNotificationsExpireCommand();
+ $disableNotifications
+ ->setExpireTime($this->getElement('expire_time')->getValue()->getTimestamp());
+ $this->getTransport($this->request)->send($disableNotifications);
+ Notification::success($this->translate('Disabling host and service notifications..'));
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php b/modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php
new file mode 100644
index 0000000..8b01399
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php
@@ -0,0 +1,279 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Instance;
+
+use Icinga\Module\Monitoring\Command\Instance\ToggleInstanceFeatureCommand;
+use Icinga\Module\Monitoring\Forms\Command\CommandForm;
+use Icinga\Web\Notification;
+
+/**
+ * Form for enabling or disabling features of Icinga instances
+ */
+class ToggleInstanceFeaturesCommandForm extends CommandForm
+{
+ /**
+ * Instance status
+ *
+ * @var object
+ */
+ protected $status;
+
+ /**
+ * (non-PHPDoc)
+ * @see \Zend_Form::init() For the method documentation.
+ */
+ public function init()
+ {
+ $this->setUseFormAutosubmit();
+ $this->setAttrib('class', self::DEFAULT_CLASSES . ' instance-features');
+ }
+
+ /**
+ * Set the instance status
+ *
+ * @param object $status
+ *
+ * @return $this
+ */
+ public function setStatus($status)
+ {
+ $this->status = (object) $status;
+ return $this;
+ }
+
+ /**
+ * Get the instance status
+ *
+ * @return object
+ */
+ public function getStatus()
+ {
+ return $this->status;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::createElements() For the method documentation.
+ */
+ public function createElements(array $formData = array())
+ {
+ $notificationDescription = null;
+ $isIcinga2 = $this->getBackend()->isIcinga2($this->status->program_version);
+
+ if (! $isIcinga2) {
+ if ((bool) $this->status->notifications_enabled) {
+ if ($this->hasPermission('monitoring/command/feature/instance')) {
+ $notificationDescription = sprintf(
+ '<a aria-label="%1$s" class="action-link" title="%1$s"'
+ . ' href="%2$s" data-base-target="_next">%3$s</a>',
+ $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(
+ $this->translate('Notifications will be re-enabled in <strong>%s</strong>'),
+ $this->getView()->timeUntil($this->status->disable_notif_expire_time)
+ );
+ }
+ }
+
+ $toggleDisabled = $this->hasPermission('monitoring/command/feature/instance') ? null : '';
+
+ $this->addElement(
+ 'checkbox',
+ ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS,
+ array(
+ 'label' => $this->translate('Active Host Checks'),
+ 'autosubmit' => true,
+ 'disabled' => $toggleDisabled
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS,
+ array(
+ 'label' => $this->translate('Active Service Checks'),
+ 'autosubmit' => true,
+ 'disabled' => $toggleDisabled
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS,
+ array(
+ 'label' => $this->translate('Event Handlers'),
+ 'autosubmit' => true,
+ 'disabled' => $toggleDisabled
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION,
+ array(
+ 'label' => $this->translate('Flap Detection'),
+ 'autosubmit' => true,
+ 'disabled' => $toggleDisabled
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS,
+ array(
+ 'label' => $this->translate('Notifications'),
+ 'autosubmit' => true,
+ 'description' => $notificationDescription,
+ 'decorators' => array(
+ array('Label', array('tag'=>'span', 'separator' => '', 'class' => 'control-label')),
+ array(
+ 'Description',
+ array('tag' => 'span', 'class' => 'description', 'escape' => false)
+ ),
+ array(array('labelWrap' => 'HtmlTag'), array('tag' => 'div', 'class' => 'control-label-group')),
+ array('ViewHelper', array('separator' => '')),
+ array('Errors', array('separator' => '')),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group'))
+ ),
+ 'disabled' => $toggleDisabled
+ )
+ );
+
+ if (! $isIcinga2) {
+ $this->addElement(
+ 'checkbox',
+ ToggleInstanceFeatureCommand::FEATURE_HOST_OBSESSING,
+ array(
+ 'label' => $this->translate('Obsessing Over Hosts'),
+ 'autosubmit' => true,
+ 'disabled' => $toggleDisabled
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ ToggleInstanceFeatureCommand::FEATURE_SERVICE_OBSESSING,
+ array(
+ 'label' => $this->translate('Obsessing Over Services'),
+ 'autosubmit' => true,
+ 'disabled' => $toggleDisabled
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ ToggleInstanceFeatureCommand::FEATURE_PASSIVE_HOST_CHECKS,
+ array(
+ 'label' => $this->translate('Passive Host Checks'),
+ 'autosubmit' => true,
+ 'disabled' => $toggleDisabled
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ ToggleInstanceFeatureCommand::FEATURE_PASSIVE_SERVICE_CHECKS,
+ array(
+ 'label' => $this->translate('Passive Service Checks'),
+ 'autosubmit' => true,
+ 'disabled' => $toggleDisabled
+ )
+ );
+ }
+
+ $this->addElement(
+ 'checkbox',
+ ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA,
+ array(
+ 'label' => $this->translate('Performance Data'),
+ 'autosubmit' => true,
+ 'disabled' => $toggleDisabled
+ )
+ );
+ }
+
+ /**
+ * Load feature status
+ *
+ * @param object $instanceStatus
+ *
+ * @return $this
+ */
+ public function load($instanceStatus)
+ {
+ $this->create();
+ foreach ($this->getValues() as $feature => $enabled) {
+ $this->getElement($feature)->setChecked($instanceStatus->{$feature});
+ }
+
+ return $this;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::onSuccess() For the method documentation.
+ */
+ public function onSuccess()
+ {
+ $this->assertPermission('monitoring/command/feature/instance');
+
+ $notifications = array(
+ ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS => array(
+ $this->translate('Enabling active host checks..'),
+ $this->translate('Disabling active host checks..')
+ ),
+ ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS => array(
+ $this->translate('Enabling active service checks..'),
+ $this->translate('Disabling active service checks..')
+ ),
+ ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS => array(
+ $this->translate('Enabling event handlers..'),
+ $this->translate('Disabling event handlers..')
+ ),
+ ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION => array(
+ $this->translate('Enabling flap detection..'),
+ $this->translate('Disabling flap detection..')
+ ),
+ ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS => array(
+ $this->translate('Enabling notifications..'),
+ $this->translate('Disabling notifications..')
+ ),
+ ToggleInstanceFeatureCommand::FEATURE_HOST_OBSESSING => array(
+ $this->translate('Enabling obsessing over hosts..'),
+ $this->translate('Disabling obsessing over hosts..')
+ ),
+ ToggleInstanceFeatureCommand::FEATURE_SERVICE_OBSESSING => array(
+ $this->translate('Enabling obsessing over services..'),
+ $this->translate('Disabling obsessing over services..')
+ ),
+ ToggleInstanceFeatureCommand::FEATURE_PASSIVE_HOST_CHECKS => array(
+ $this->translate('Enabling passive host checks..'),
+ $this->translate('Disabling passive host checks..')
+ ),
+ ToggleInstanceFeatureCommand::FEATURE_PASSIVE_SERVICE_CHECKS => array(
+ $this->translate('Enabling passive service checks..'),
+ $this->translate('Disabling passive service checks..')
+ ),
+ ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA => array(
+ $this->translate('Enabling performance data..'),
+ $this->translate('Disabling performance data..')
+ )
+ );
+
+ foreach ($this->getValues() as $feature => $enabled) {
+ if ((bool) $this->status->{$feature} !== (bool) $enabled) {
+ $toggleFeature = new ToggleInstanceFeatureCommand();
+ $toggleFeature
+ ->setFeature($feature)
+ ->setEnabled($enabled);
+ $this->getTransport($this->request)->send($toggleFeature);
+
+ Notification::success(
+ $notifications[$feature][$enabled ? 0 : 1]
+ );
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php b/modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php
new file mode 100644
index 0000000..c7caf5d
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php
@@ -0,0 +1,172 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use DateTime;
+use DateInterval;
+use Icinga\Application\Config;
+use Icinga\Module\Monitoring\Command\Object\AcknowledgeProblemCommand;
+use Icinga\Web\Notification;
+
+/**
+ * Form for acknowledging host or service problems
+ */
+class AcknowledgeProblemCommandForm extends ObjectsCommandForm
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->addDescription($this->translate(
+ 'This command is used to acknowledge host or service problems. When a problem is acknowledged,'
+ . ' future notifications about problems are temporarily disabled until the host or service'
+ . ' recovers.'
+ ));
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::getSubmitLabel() For the method documentation.
+ */
+ public function getSubmitLabel()
+ {
+ return $this->translatePlural('Acknowledge problem', 'Acknowledge problems', count($this->objects));
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::createElements() For the method documentation.
+ */
+ public function createElements(array $formData = array())
+ {
+ $config = Config::module('monitoring');
+
+ $acknowledgeExpire = (bool) $config->get('settings', 'acknowledge_expire', false);
+
+ $this->addElements(array(
+ array(
+ 'textarea',
+ 'comment',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Comment'),
+ 'description' => $this->translate(
+ 'If you work with other administrators, you may find it useful to share information about'
+ . ' the host or service that is having problems. Make sure you enter a brief description of'
+ . ' what you are doing.'
+ ),
+ 'attribs' => array('class' => 'autofocus')
+ )
+ ),
+ array(
+ 'checkbox',
+ 'persistent',
+ array(
+ 'label' => $this->translate('Persistent Comment'),
+ 'value' => (bool) $config->get('settings', 'acknowledge_persistent', false),
+ 'description' => $this->translate(
+ 'If you would like the comment to remain even when the acknowledgement is removed, check this'
+ . ' option.'
+ )
+ )
+ ),
+ array(
+ 'checkbox',
+ 'expire',
+ array(
+ 'label' => $this->translate('Use Expire Time'),
+ 'value' => $acknowledgeExpire,
+ 'description' => $this->translate(
+ 'If the acknowledgement should expire, check this option.'
+ ),
+ 'autosubmit' => true
+ )
+ )
+ ));
+ $expire = isset($formData['expire']) ? $formData['expire'] : $acknowledgeExpire;
+ if ($expire) {
+ $expireTime = new DateTime();
+ $expireTime->add(new DateInterval($config->get('settings', 'acknowledge_expire_time', 'PT1H')));
+ $this->addElement(
+ 'dateTimePicker',
+ 'expire_time',
+ array(
+ 'label' => $this->translate('Expire Time'),
+ 'value' => $expireTime,
+ 'description' => $this->translate(
+ 'Enter the expire date and time for this acknowledgement here. Icinga will delete the'
+ . ' acknowledgement after this time expired.'
+ )
+ )
+ );
+ $this->addDisplayGroup(
+ array('expire', 'expire_time'),
+ 'expire-expire_time',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div'))
+ )
+ )
+ );
+ }
+ $this->addElements(array(
+ array(
+ 'checkbox',
+ 'sticky',
+ array(
+ 'label' => $this->translate('Sticky Acknowledgement'),
+ 'value' => (bool) $config->get('settings', 'acknowledge_sticky', false),
+ 'description' => $this->translate(
+ 'If you want the acknowledgement to remain until the host or service recovers even if the host'
+ . ' or service changes state, check this option.'
+ )
+ )
+ ),
+ array(
+ 'checkbox',
+ 'notify',
+ array(
+ 'label' => $this->translate('Send Notification'),
+ 'value' => (bool) $config->get('settings', 'acknowledge_notify', true),
+ 'description' => $this->translate(
+ 'If you do not want an acknowledgement notification to be sent out to the appropriate contacts,'
+ . ' uncheck this option.'
+ )
+ )
+ )
+ ));
+ return $this;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::onSuccess() For the method documentation.
+ */
+ public function onSuccess()
+ {
+ foreach ($this->objects as $object) {
+ /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+ $ack = new AcknowledgeProblemCommand();
+ $ack
+ ->setObject($object)
+ ->setComment($this->getElement('comment')->getValue())
+ ->setAuthor($this->request->getUser()->getUsername())
+ ->setPersistent($this->getElement('persistent')->isChecked())
+ ->setSticky($this->getElement('sticky')->isChecked())
+ ->setNotify($this->getElement('notify')->isChecked());
+ if ($this->getElement('expire')->isChecked()) {
+ $ack->setExpireTime($this->getElement('expire_time')->getValue()->getTimestamp());
+ }
+ $this->getTransport($this->request)->send($ack);
+ }
+ Notification::success($this->translatePlural(
+ 'Acknowledging problem..',
+ 'Acknowledging problems..',
+ count($this->objects)
+ ));
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php b/modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php
new file mode 100644
index 0000000..72133a0
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php
@@ -0,0 +1,148 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use DateInterval;
+use DateTime;
+use Icinga\Application\Config;
+use Icinga\Module\Monitoring\Command\Object\AddCommentCommand;
+use Icinga\Web\Notification;
+
+/**
+ * Form for adding host or service comments
+ */
+class AddCommentCommandForm extends ObjectsCommandForm
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->addDescription($this->translate('This command is used to add host or service comments.'));
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::getSubmitLabel() For the method documentation.
+ */
+ public function getSubmitLabel()
+ {
+ return $this->translatePlural('Add comment', 'Add comments', count($this->objects));
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::createElements() For the method documentation.
+ */
+ public function createElements(array $formData = array())
+ {
+ $this->addElement(
+ 'textarea',
+ 'comment',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Comment'),
+ 'description' => $this->translate(
+ 'If you work with other administrators, you may find it useful to share information about'
+ . ' the host or service that is having problems. Make sure you enter a brief description of'
+ . ' what you are doing.'
+ ),
+ 'attribs' => array('class' => 'autofocus')
+ )
+ );
+ if (! $this->getBackend()->isIcinga2()) {
+ $this->addElement(
+ 'checkbox',
+ 'persistent',
+ array(
+ 'label' => $this->translate('Persistent'),
+ 'value' => (bool) Config::module('monitoring')->get('settings', 'comment_persistent', true),
+ 'description' => $this->translate(
+ 'If you uncheck this option, the comment will automatically be deleted the next time Icinga is'
+ . ' restarted.'
+ )
+ )
+ );
+ }
+
+ if (version_compare($this->getBackend()->getProgramVersion(), '2.13.0', '>=')) {
+ $config = Config::module('monitoring');
+ $commentExpire = (bool) $config->get('settings', 'comment_expire', false);
+
+ $this->addElement(
+ 'checkbox',
+ 'expire',
+ [
+ 'label' => $this->translate('Use Expire Time'),
+ 'value' => $commentExpire,
+ 'description' => $this->translate('If the comment should expire, check this option.'),
+ 'autosubmit' => true
+ ]
+ );
+
+ if (isset($formData['expire']) ? $formData['expire'] : $commentExpire) {
+ $expireTime = new DateTime();
+ $expireTime->add(new DateInterval($config->get('settings', 'comment_expire_time', 'PT1H')));
+
+ $this->addElement(
+ 'dateTimePicker',
+ 'expire_time',
+ [
+ 'label' => $this->translate('Expire Time'),
+ 'value' => $expireTime,
+ 'description' => $this->translate(
+ 'Enter the expire date and time for this comment here. Icinga will delete the'
+ . ' comment after this time expired.'
+ )
+ ]
+ );
+
+ $this->addDisplayGroup(
+ ['expire', 'expire_time'],
+ 'expire-expire_time',
+ [
+ 'decorators' => [
+ 'FormElements',
+ ['HtmlTag', ['tag' => 'div']]
+ ]
+ ]
+ );
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::onSuccess() For the method documentation.
+ */
+ public function onSuccess()
+ {
+ foreach ($this->objects as $object) {
+ /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+ $comment = new AddCommentCommand();
+ $comment->setObject($object);
+ $comment->setComment($this->getElement('comment')->getValue());
+ $comment->setAuthor($this->request->getUser()->getUsername());
+ if (($persistent = $this->getElement('persistent')) !== null) {
+ $comment->setPersistent($persistent->isChecked());
+ }
+
+ $expire = $this->getElement('expire');
+
+ if ($expire !== null && $expire->isChecked()) {
+ $comment->setExpireTime($this->getElement('expire_time')->getValue()->getTimestamp());
+ }
+
+ $this->getTransport($this->request)->send($comment);
+ }
+ Notification::success($this->translatePlural(
+ 'Adding comment..',
+ 'Adding comments..',
+ count($this->objects)
+ ));
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/CheckNowCommandForm.php b/modules/monitoring/application/forms/Command/Object/CheckNowCommandForm.php
new file mode 100644
index 0000000..a586d2f
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/CheckNowCommandForm.php
@@ -0,0 +1,87 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use Icinga\Module\Monitoring\Command\Object\ScheduleHostCheckCommand;
+use Icinga\Module\Monitoring\Command\Object\ScheduleServiceCheckCommand;
+use Icinga\Web\Notification;
+
+/**
+ * Form for immediately checking hosts or services
+ */
+class CheckNowCommandForm extends ObjectsCommandForm
+{
+ /**
+ * (non-PHPDoc)
+ * @see \Zend_Form::init() For the method documentation.
+ */
+ public function init()
+ {
+ $this->setAttrib('class', 'inline');
+ $this->setSubmitLabel($this->translate('Check now'));
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::addSubmitButton() For the method documentation.
+ */
+ public function addSubmitButton()
+ {
+ $this->addElements(array(
+ array(
+ 'button',
+ 'btn_submit',
+ array(
+ 'class' => 'link-button spinner',
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'escape' => false,
+ 'ignore' => true,
+ 'label' => $this->getView()->icon('arrows-cw') . $this->translate('Check now'),
+ 'type' => 'submit',
+ 'title' => $this->translate('Schedule the next active check to run immediately'),
+ 'value' => $this->translate('Check now')
+ )
+ )
+ ));
+
+ return $this;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::onSuccess() For the method documentation.
+ */
+ public function onSuccess()
+ {
+ foreach ($this->objects as $object) {
+ /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+ if (! $object->active_checks_enabled
+ && ! $this->Auth()->hasPermission('monitoring/command/schedule-check')
+ ) {
+ continue;
+ }
+
+ if ($object->getType() === $object::TYPE_HOST) {
+ $check = new ScheduleHostCheckCommand();
+ } else {
+ $check = new ScheduleServiceCheckCommand();
+ }
+ $check
+ ->setObject($object)
+ ->setForced()
+ ->setCheckTime(time());
+ $this->getTransport($this->request)->send($check);
+ }
+ Notification::success(mtp(
+ 'monitoring',
+ 'Scheduling check..',
+ 'Scheduling checks..',
+ count($this->objects)
+ ));
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php b/modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php
new file mode 100644
index 0000000..cd15b19
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php
@@ -0,0 +1,109 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use Icinga\Module\Monitoring\Command\Object\DeleteCommentCommand;
+use Icinga\Module\Monitoring\Forms\Command\CommandForm;
+use Icinga\Web\Notification;
+
+/**
+ * Form for deleting host or service comments
+ */
+class DeleteCommentCommandForm extends CommandForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setAttrib('class', 'inline');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addSubmitButton()
+ {
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ array(
+ 'class' => 'link-button spinner',
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'escape' => false,
+ 'ignore' => true,
+ 'label' => $this->getView()->icon('cancel'),
+ 'title' => $this->translate('Delete this comment'),
+ 'type' => 'submit'
+ )
+ );
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData = array())
+ {
+ $this->addElements(
+ array(
+ array(
+ 'hidden',
+ 'comment_id',
+ array(
+ 'required' => true,
+ 'validators' => array('NotEmpty'),
+ 'decorators' => array('ViewHelper')
+ )
+ ),
+ array(
+ 'hidden',
+ 'comment_is_service',
+ array(
+ 'filters' => array('Boolean'),
+ 'decorators' => array('ViewHelper')
+ )
+ ),
+ array(
+ 'hidden',
+ 'comment_name',
+ array(
+ 'decorators' => array('ViewHelper')
+ )
+ ),
+ array(
+ 'hidden',
+ 'redirect',
+ array(
+ 'decorators' => array('ViewHelper')
+ )
+ )
+ )
+ );
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ $cmd = new DeleteCommentCommand();
+ $cmd
+ ->setAuthor($this->Auth()->getUser()->getUsername())
+ ->setCommentId($this->getElement('comment_id')->getValue())
+ ->setCommentName($this->getElement('comment_name')->getValue())
+ ->setIsService($this->getElement('comment_is_service')->getValue());
+ $this->getTransport($this->request)->send($cmd);
+ $redirect = $this->getElement('redirect')->getValue();
+ if (! empty($redirect)) {
+ $this->setRedirectUrl($redirect);
+ }
+ Notification::success($this->translate('Deleting comment..'));
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php b/modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php
new file mode 100644
index 0000000..70ea7b8
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php
@@ -0,0 +1,89 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use Icinga\Module\Monitoring\Command\Object\DeleteCommentCommand;
+use Icinga\Module\Monitoring\Forms\Command\CommandForm;
+use Icinga\Web\Notification;
+
+/**
+ * Form for deleting host or service comments
+ */
+class DeleteCommentsCommandForm extends CommandForm
+{
+ /**
+ * The comments to delete
+ *
+ * @var array
+ */
+ protected $comments;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setAttrib('class', 'inline');
+ }
+
+ /**
+ * Set the comments to delete
+ *
+ * @param iterable $comments
+ *
+ * @return $this
+ */
+ public function setComments($comments)
+ {
+ $this->comments = $comments;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData = array())
+ {
+ $this->addElements(array(
+ array(
+ 'hidden',
+ 'redirect',
+ array('decorators' => array('ViewHelper'))
+ )
+ ));
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSubmitLabel()
+ {
+ return $this->translatePlural('Remove', 'Remove All', count($this->comments));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ foreach ($this->comments as $comment) {
+ $cmd = new DeleteCommentCommand();
+ $cmd
+ ->setCommentId($comment->id)
+ ->setCommentName($comment->name)
+ ->setAuthor($this->Auth()->getUser()->getUsername())
+ ->setIsService(isset($comment->service_description));
+ $this->getTransport($this->request)->send($cmd);
+ }
+ $redirect = $this->getElement('redirect')->getValue();
+ if (! empty($redirect)) {
+ $this->setRedirectUrl($redirect);
+ }
+ Notification::success(
+ $this->translatePlural('Deleting comment..', 'Deleting comments..', count($this->comments))
+ );
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php b/modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php
new file mode 100644
index 0000000..79700cb
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php
@@ -0,0 +1,129 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use Icinga\Module\Monitoring\Command\Object\DeleteDowntimeCommand;
+use Icinga\Module\Monitoring\Exception\CommandTransportException;
+use Icinga\Module\Monitoring\Forms\Command\CommandForm;
+use Icinga\Web\Notification;
+
+/**
+ * Form for deleting host or service downtimes
+ */
+class DeleteDowntimeCommandForm extends CommandForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setAttrib('class', 'inline');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addSubmitButton()
+ {
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ array(
+ 'class' => 'link-button spinner',
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'escape' => false,
+ 'ignore' => true,
+ 'label' => $this->getView()->icon('cancel'),
+ 'title' => $this->translate('Delete this downtime'),
+ 'type' => 'submit'
+ )
+ );
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData = array())
+ {
+ $this->addElements(
+ array(
+ array(
+ 'hidden',
+ 'downtime_id',
+ array(
+ 'decorators' => array('ViewHelper'),
+ 'required' => true,
+ 'validators' => array('NotEmpty')
+ )
+ ),
+ array(
+ 'hidden',
+ 'downtime_is_service',
+ array(
+ 'decorators' => array('ViewHelper'),
+ 'filters' => array('Boolean')
+ )
+ ),
+ array(
+ 'hidden',
+ 'downtime_name',
+ array(
+ 'decorators' => array('ViewHelper')
+ )
+ ),
+ array(
+ 'hidden',
+ 'redirect',
+ array(
+ 'decorators' => array('ViewHelper')
+ )
+ )
+ )
+ );
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ $cmd = new DeleteDowntimeCommand();
+ $cmd
+ ->setAuthor($this->Auth()->getUser()->getUsername())
+ ->setDowntimeId($this->getElement('downtime_id')->getValue())
+ ->setDowntimeName($this->getElement('downtime_name')->getValue())
+ ->setIsService($this->getElement('downtime_is_service')->getValue());
+
+ $errorMsg = null;
+
+ try {
+ $this->getTransport($this->request)->send($cmd);
+ } catch (CommandTransportException $e) {
+ $errorMsg = $e->getMessage();
+ }
+
+ if (! $errorMsg) {
+ $redirect = $this->getElement('redirect')->getValue();
+ Notification::success($this->translate('Deleting downtime.'));
+ } else {
+ if (! $this->getIsApiTarget()) {
+ $redirect = $this->getRequest()->getUrl();
+ }
+
+ Notification::error($errorMsg);
+ }
+
+ if (! empty($redirect)) {
+ $this->setRedirectUrl($redirect);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php b/modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php
new file mode 100644
index 0000000..d4ee803
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php
@@ -0,0 +1,89 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use Icinga\Module\Monitoring\Command\Object\DeleteDowntimeCommand;
+use Icinga\Module\Monitoring\Forms\Command\CommandForm;
+use Icinga\Web\Notification;
+
+/**
+ * Form for deleting host or service downtimes
+ */
+class DeleteDowntimesCommandForm extends CommandForm
+{
+ /**
+ * The downtimes to delete
+ *
+ * @var array
+ */
+ protected $downtimes;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setAttrib('class', 'inline');
+ }
+
+ /**
+ * Set the downtimes to delete
+ *
+ * @param iterable $downtimes
+ *
+ * @return $this
+ */
+ public function setDowntimes($downtimes)
+ {
+ $this->downtimes = $downtimes;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData = array())
+ {
+ $this->addElements(array(
+ array(
+ 'hidden',
+ 'redirect',
+ array('decorators' => array('ViewHelper'))
+ )
+ ));
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSubmitLabel()
+ {
+ return $this->translatePlural('Remove', 'Remove All', count($this->downtimes));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ foreach ($this->downtimes as $downtime) {
+ $delDowntime = new DeleteDowntimeCommand();
+ $delDowntime
+ ->setDowntimeId($downtime->id)
+ ->setDowntimeName($downtime->name)
+ ->setAuthor($this->Auth()->getUser()->getUsername())
+ ->setIsService(isset($downtime->service_description));
+ $this->getTransport($this->request)->send($delDowntime);
+ }
+ $redirect = $this->getElement('redirect')->getValue();
+ if (! empty($redirect)) {
+ $this->setRedirectUrl($redirect);
+ }
+ Notification::success(
+ $this->translatePlural('Deleting downtime..', 'Deleting downtimes..', count($this->downtimes))
+ );
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/ObjectsCommandForm.php b/modules/monitoring/application/forms/Command/Object/ObjectsCommandForm.php
new file mode 100644
index 0000000..928c365
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/ObjectsCommandForm.php
@@ -0,0 +1,47 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use Icinga\Module\Monitoring\Forms\Command\CommandForm;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+
+/**
+ * Base class for Icinga object command forms
+ */
+abstract class ObjectsCommandForm extends CommandForm
+{
+ /**
+ * Involved Icinga objects
+ *
+ * @var array|\Traversable|\ArrayAccess
+ */
+ protected $objects;
+
+ /**
+ * Set the involved Icinga objects
+ *
+ * @param $objects MonitoredObject|array|\Traversable|\ArrayAccess
+ *
+ * @return $this
+ */
+ public function setObjects($objects)
+ {
+ if ($objects instanceof MonitoredObject) {
+ $this->objects = array($objects);
+ } else {
+ $this->objects = $objects;
+ }
+ return $this;
+ }
+
+ /**
+ * Get the involved Icinga objects
+ *
+ * @return array|\ArrayAccess|\Traversable
+ */
+ public function getObjects()
+ {
+ return $this->objects;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php b/modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php
new file mode 100644
index 0000000..7a196ec
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php
@@ -0,0 +1,140 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use Icinga\Web\Notification;
+use Icinga\Module\Monitoring\Command\Object\ProcessCheckResultCommand;
+
+/**
+ * Form for submitting a passive host or service check result
+ */
+class ProcessCheckResultCommandForm extends ObjectsCommandForm
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->addDescription($this->translate(
+ 'This command is used to submit passive host or service check results.'
+ ));
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::getSubmitLabel() For the method documentation.
+ */
+ public function getSubmitLabel()
+ {
+ return $this->translatePlural(
+ 'Submit Passive Check Result',
+ 'Submit Passive Check Results',
+ count($this->objects)
+ );
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::createElements() For the method documentation.
+ */
+ public function createElements(array $formData)
+ {
+ $object = null;
+ foreach ($this->getObjects() as $object) {
+ /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+ // Nasty, but as getObjects() returns everything but an object with a real
+ // iterator interface this is the only way to fetch just the first element
+ break;
+ }
+
+ $this->addElement(
+ 'select',
+ 'status',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Status'),
+ 'description' => $this->translate('The state this check result should report'),
+ 'multiOptions' => $object->getType() === $object::TYPE_HOST ? $this->getHostMultiOptions() : array(
+ ProcessCheckResultCommand::SERVICE_OK => $this->translate('OK', 'icinga.state'),
+ ProcessCheckResultCommand::SERVICE_WARNING => $this->translate('WARNING', 'icinga.state'),
+ ProcessCheckResultCommand::SERVICE_CRITICAL => $this->translate('CRITICAL', 'icinga.state'),
+ ProcessCheckResultCommand::SERVICE_UNKNOWN => $this->translate('UNKNOWN', 'icinga.state')
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'output',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Output'),
+ 'description' => $this->translate('The plugin output of this check result')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'perfdata',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Performance Data'),
+ 'description' => $this->translate(
+ 'The performance data of this check result. Leave empty'
+ . ' if this check result has no performance data'
+ )
+ )
+ );
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::onSuccess() For the method documentation.
+ */
+ public function onSuccess()
+ {
+ foreach ($this->objects as $object) {
+ /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+ if (! $object->passive_checks_enabled) {
+ continue;
+ }
+
+ $command = new ProcessCheckResultCommand();
+ $command->setObject($object);
+ $command->setStatus($this->getValue('status'));
+ $command->setOutput($this->getValue('output'));
+
+ if ($perfdata = $this->getValue('perfdata')) {
+ $command->setPerformanceData($perfdata);
+ }
+
+ $this->getTransport($this->request)->send($command);
+ }
+
+ Notification::success($this->translatePlural(
+ 'Processing check result..',
+ 'Processing check results..',
+ count($this->objects)
+ ));
+
+ return true;
+ }
+
+ /**
+ * Returns the available host options based on the program version
+ *
+ * @return array
+ */
+ protected function getHostMultiOptions()
+ {
+ $options = array(
+ ProcessCheckResultCommand::HOST_UP => $this->translate('UP', 'icinga.state'),
+ ProcessCheckResultCommand::HOST_DOWN => $this->translate('DOWN', 'icinga.state')
+ );
+
+ if (! $this->getBackend()->isIcinga2()) {
+ $options[ProcessCheckResultCommand::HOST_UNREACHABLE] = $this->translate('UNREACHABLE', 'icinga.state');
+ }
+
+ return $options;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/RemoveAcknowledgementCommandForm.php b/modules/monitoring/application/forms/Command/Object/RemoveAcknowledgementCommandForm.php
new file mode 100644
index 0000000..e45a055
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/RemoveAcknowledgementCommandForm.php
@@ -0,0 +1,122 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use Icinga\Module\Monitoring\Command\Object\RemoveAcknowledgementCommand;
+use Icinga\Web\Notification;
+
+/**
+ * Form for removing host or service problem acknowledgements
+ */
+class RemoveAcknowledgementCommandForm extends ObjectsCommandForm
+{
+ /**
+ * Whether to show the submit label next to the remove icon
+ *
+ * The submit label is disabled in detail views but should be enabled in multi-select views.
+ *
+ * @var bool
+ */
+ protected $labelEnabled = false;
+
+ /**
+ * Whether to show the submit label next to the remove icon
+ *
+ * @return bool
+ */
+ public function isLabelEnabled()
+ {
+ return $this->labelEnabled;
+ }
+
+ /**
+ * Set whether to show the submit label next to the remove icon
+ *
+ * @param bool $labelEnabled
+ *
+ * @return $this
+ */
+ public function setLabelEnabled($labelEnabled)
+ {
+ $this->labelEnabled = (bool) $labelEnabled;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setAttrib('class', 'inline');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addSubmitButton()
+ {
+ $this->addElement(
+ 'button',
+ 'btn_submit',
+ array(
+ 'class' => 'link-button spinner',
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'escape' => false,
+ 'ignore' => true,
+ 'label' => $this->getSubmitLabel(),
+ 'title' => $this->translatePlural(
+ 'Remove acknowledgement',
+ 'Remove acknowledgements',
+ count($this->objects)
+ ),
+ 'type' => 'submit'
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSubmitLabel()
+ {
+ $label = $this->getView()->icon('cancel');
+ if ($this->isLabelEnabled()) {
+ $label .= $this->translatePlural(
+ 'Remove acknowledgement',
+ 'Remove acknowledgements',
+ count($this->objects)
+ );
+ }
+
+ return $label;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ foreach ($this->objects as $object) {
+ /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+ $removeAck = new RemoveAcknowledgementCommand();
+ $removeAck->setObject($object);
+ $removeAck->setAuthor($this->Auth()->getUser()->getUsername());
+ $this->getTransport($this->request)->send($removeAck);
+ }
+ Notification::success(mtp(
+ 'monitoring',
+ 'Removing acknowledgement..',
+ 'Removing acknowledgements..',
+ count($this->objects)
+ ));
+
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/ScheduleHostCheckCommandForm.php b/modules/monitoring/application/forms/Command/Object/ScheduleHostCheckCommandForm.php
new file mode 100644
index 0000000..55b044f
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/ScheduleHostCheckCommandForm.php
@@ -0,0 +1,67 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use Icinga\Application\Config;
+use Icinga\Module\Monitoring\Command\Object\ScheduleHostCheckCommand;
+use Icinga\Web\Notification;
+
+/**
+ * Form for scheduling host checks
+ */
+class ScheduleHostCheckCommandForm extends ScheduleServiceCheckCommandForm
+{
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::createElements() For the method documentation.
+ */
+ public function createElements(array $formData = array())
+ {
+ $config = Config::module('monitoring');
+
+ parent::createElements($formData);
+ $this->addElements(array(
+ array(
+ 'checkbox',
+ 'all_services',
+ array(
+ 'label' => $this->translate('All Services'),
+ 'value' => (bool) $config->get('settings', 'hostcheck_all_services', false),
+ 'description' => $this->translate(
+ 'Schedule check for all services on the hosts and the hosts themselves.'
+ )
+ )
+ )
+ ));
+ return $this;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::onSuccess() For the method documentation.
+ */
+ public function onSuccess()
+ {
+ foreach ($this->objects as $object) {
+ /** @var \Icinga\Module\Monitoring\Object\Host $object */
+ if (! $object->active_checks_enabled
+ && ! $this->Auth()->hasPermission('monitoring/command/schedule-check')
+ ) {
+ continue;
+ }
+
+ $check = new ScheduleHostCheckCommand();
+ $check
+ ->setObject($object)
+ ->setOfAllServices($this->getElement('all_services')->isChecked());
+ $this->scheduleCheck($check, $this->request);
+ }
+ Notification::success($this->translatePlural(
+ 'Scheduling host check..',
+ 'Scheduling host checks..',
+ count($this->objects)
+ ));
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php b/modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php
new file mode 100644
index 0000000..89db1ce
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php
@@ -0,0 +1,178 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use DateInterval;
+use DateTime;
+use Icinga\Application\Config;
+use Icinga\Module\Monitoring\Command\Object\ApiScheduleHostDowntimeCommand;
+use Icinga\Module\Monitoring\Command\Object\PropagateHostDowntimeCommand;
+use Icinga\Module\Monitoring\Command\Object\ScheduleHostDowntimeCommand;
+use Icinga\Module\Monitoring\Command\Object\ScheduleServiceDowntimeCommand;
+use Icinga\Module\Monitoring\Command\Transport\ApiCommandTransport;
+use Icinga\Module\Monitoring\Command\Transport\CommandTransport;
+use Icinga\Web\Notification;
+
+/**
+ * Form for scheduling host downtimes
+ */
+class ScheduleHostDowntimeCommandForm extends ScheduleServiceDowntimeCommandForm
+{
+ /** @var bool */
+ protected $hostDowntimeAllServices;
+
+ public function init()
+ {
+ $this->start = new DateTime();
+ $config = Config::module('monitoring');
+ $this->commentText = $config->get('settings', 'hostdowntime_comment_text');
+
+ $this->hostDowntimeAllServices = (bool) $config->get('settings', 'hostdowntime_all_services', false);
+
+ $fixedEnd = clone $this->start;
+ $fixed = $config->get('settings', 'hostdowntime_end_fixed', 'PT1H');
+ $this->fixedEnd = $fixedEnd->add(new DateInterval($fixed));
+
+ $flexibleEnd = clone $this->start;
+ $flexible = $config->get('settings', 'hostdowntime_end_flexible', 'PT1H');
+ $this->flexibleEnd = $flexibleEnd->add(new DateInterval($flexible));
+
+ $flexibleDuration = $config->get('settings', 'hostdowntime_flexible_duration', 'PT2H');
+ $this->flexibleDuration = new DateInterval($flexibleDuration);
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::createElements() For the method documentation.
+ */
+ public function createElements(array $formData = array())
+ {
+ parent::createElements($formData);
+
+ $this->addElement(
+ 'checkbox',
+ 'all_services',
+ array(
+ 'description' => $this->translate(
+ 'Schedule downtime for all services on the hosts and the hosts themselves.'
+ ),
+ 'label' => $this->translate('All Services'),
+ 'value' => $this->hostDowntimeAllServices
+ )
+ );
+
+ if (! $this->getBackend()->isIcinga2()
+ || version_compare($this->getBackend()->getProgramVersion(), '2.6.0', '>=')
+ ) {
+ $this->addElement(
+ 'select',
+ 'child_hosts',
+ array(
+ 'description' => $this->translate(
+ 'Define what should be done with the child hosts of the hosts.'
+ ),
+ 'label' => $this->translate('Child Hosts'),
+ 'multiOptions' => array(
+ 0 => $this->translate('Do nothing with child hosts'),
+ 1 => $this->translate('Schedule triggered downtime for all child hosts'),
+ 2 => $this->translate('Schedule non-triggered downtime for all child hosts')
+ ),
+ 'value' => 0
+ )
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::onSuccess() For the method documentation.
+ */
+ public function onSuccess()
+ {
+ $end = $this->getValue('end')->getTimestamp();
+ if ($end <= $this->getValue('start')->getTimestamp()) {
+ $endElement = $this->_elements['end'];
+ $endElement->setValue($endElement->getValue()->format($endElement->getFormat()));
+ $endElement->addError($this->translate('The end time must be greater than the start time'));
+ return false;
+ }
+
+ $now = new DateTime;
+ if ($end <= $now->getTimestamp()) {
+ $endElement = $this->_elements['end'];
+ $endElement->setValue($endElement->getValue()->format($endElement->getFormat()));
+ $endElement->addError($this->translate('A downtime must not be in the past'));
+ return false;
+ }
+
+ // Send all_services API parameter if Icinga is equal to or greater than 2.11.0
+ $allServicesNative = version_compare($this->getBackend()->getProgramVersion(), '2.11.0', '>=');
+ // Use ApiScheduleHostDowntimeCommand only when Icinga is equal to or greater than 2.11.0 and
+ // when an API command transport is requested or only API command transports are configured:
+ $useApiDowntime = $allServicesNative;
+ if ($useApiDowntime) {
+ $transport = $this->getTransport($this->getRequest());
+ if ($transport instanceof CommandTransport) {
+ foreach ($transport::getConfig() as $config) {
+ if (strtolower($config->transport) !== 'api') {
+ $useApiDowntime = false;
+ break;
+ }
+ }
+ } elseif (! $transport instanceof ApiCommandTransport) {
+ $useApiDowntime = false;
+ }
+ }
+
+ foreach ($this->objects as $object) {
+ if ($useApiDowntime) {
+ $hostDowntime = (new ApiScheduleHostDowntimeCommand())
+ ->setForAllServices($this->getElement('all_services')->isChecked())
+ ->setChildOptions((int) $this->getElement('child_hosts')->getValue());
+ // Code duplicated for readability and scope
+ $hostDowntime->setObject($object);
+ $this->scheduleDowntime($hostDowntime, $this->request);
+
+ continue;
+ }
+
+ /** @var \Icinga\Module\Monitoring\Object\Host $object */
+ if (($childHostsEl = $this->getElement('child_hosts')) !== null) {
+ $childHosts = (int) $childHostsEl->getValue();
+ } else {
+ $childHosts = 0;
+ }
+ $allServices = $this->getElement('all_services')->isChecked();
+ if ($childHosts === 0) {
+ $hostDowntime = (new ScheduleHostDowntimeCommand())
+ ->setForAllServicesNative($allServicesNative);
+ if ($allServices === true) {
+ $hostDowntime->setForAllServices();
+ };
+ } else {
+ $hostDowntime = new PropagateHostDowntimeCommand();
+ if ($childHosts === 1) {
+ $hostDowntime->setTriggered();
+ }
+ if ($allServices === true) {
+ foreach ($object->services as $service) {
+ $serviceDowntime = new ScheduleServiceDowntimeCommand();
+ $serviceDowntime->setObject($service);
+ $this->scheduleDowntime($serviceDowntime, $this->request);
+ }
+ }
+ }
+ $hostDowntime->setObject($object);
+ $this->scheduleDowntime($hostDowntime, $this->request);
+ }
+ Notification::success($this->translatePlural(
+ 'Scheduling host downtime..',
+ 'Scheduling host downtimes..',
+ count($this->objects)
+ ));
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php b/modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php
new file mode 100644
index 0000000..f65aea8
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php
@@ -0,0 +1,112 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use DateTime;
+use DateInterval;
+use Icinga\Module\Monitoring\Command\Object\ScheduleServiceCheckCommand;
+use Icinga\Web\Notification;
+use Icinga\Web\Request;
+
+/**
+ * Form for scheduling service checks
+ */
+class ScheduleServiceCheckCommandForm extends ObjectsCommandForm
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->addDescription($this->translate(
+ 'This command is used to schedule the next check of hosts or services. Icinga will re-queue the'
+ . ' hosts or services to be checked at the time you specify.'
+ ));
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::getSubmitLabel() For the method documentation.
+ */
+ public function getSubmitLabel()
+ {
+ return $this->translatePlural('Schedule check', 'Schedule checks', count($this->objects));
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::createElements() For the method documentation.
+ */
+ public function createElements(array $formData = array())
+ {
+ $checkTime = new DateTime();
+ $checkTime->add(new DateInterval('PT1H'));
+ $this->addElements(array(
+ array(
+ 'dateTimePicker',
+ 'check_time',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Check Time'),
+ 'description' => $this->translate(
+ 'Set the date and time when the check should be scheduled.'
+ ),
+ 'value' => $checkTime
+ )
+ ),
+ array(
+ 'checkbox',
+ 'force_check',
+ array(
+ 'label' => $this->translate('Force Check'),
+ 'description' => $this->translate(
+ 'If you select this option, Icinga will force a check regardless of both what time the'
+ . ' scheduled check occurs and whether or not checks are enabled.'
+ )
+ )
+ )
+ ));
+ return $this;
+ }
+
+ /**
+ * Schedule a check
+ *
+ * @param ScheduleServiceCheckCommand $check
+ * @param Request $request
+ */
+ public function scheduleCheck(ScheduleServiceCheckCommand $check, Request $request)
+ {
+ $check
+ ->setForced($this->getElement('force_check')->isChecked())
+ ->setCheckTime($this->getElement('check_time')->getValue()->getTimestamp());
+ $this->getTransport($request)->send($check);
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::onSuccess() For the method documentation.
+ */
+ public function onSuccess()
+ {
+ foreach ($this->objects as $object) {
+ /** @var \Icinga\Module\Monitoring\Object\Service $object */
+ if (! $object->active_checks_enabled
+ && ! $this->Auth()->hasPermission('monitoring/command/schedule-check')
+ ) {
+ continue;
+ }
+
+ $check = new ScheduleServiceCheckCommand();
+ $check->setObject($object);
+ $this->scheduleCheck($check, $this->request);
+ }
+ Notification::success($this->translatePlural(
+ 'Scheduling service check..',
+ 'Scheduling service checks..',
+ count($this->objects)
+ ));
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php b/modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php
new file mode 100644
index 0000000..90d50d4
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php
@@ -0,0 +1,263 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use DateTime;
+use DateInterval;
+use Icinga\Application\Config;
+use Icinga\Module\Monitoring\Command\Object\ScheduleServiceDowntimeCommand;
+use Icinga\Web\Notification;
+use Icinga\Web\Request;
+
+/**
+ * Form for scheduling service downtimes
+ */
+class ScheduleServiceDowntimeCommandForm extends ObjectsCommandForm
+{
+ /**
+ * Fixed downtime
+ */
+ const FIXED = 'fixed';
+
+ /**
+ * Flexible downtime
+ */
+ const FLEXIBLE = 'flexible';
+
+ /** @var DateTime downtime start */
+ protected $start;
+
+ /** @var DateTime fixed downtime end */
+ protected $fixedEnd;
+
+ /** @var DateTime flexible downtime end */
+ protected $flexibleEnd;
+
+ /** @var DateInterval flexible downtime duration */
+ protected $flexibleDuration;
+
+ /** @var mixed Comment text */
+ protected $commentText;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->start = new DateTime();
+
+ $config = Config::module('monitoring');
+
+ $this->commentText = $config->get('settings', 'servicedowntime_comment_text');
+ $fixedEnd = clone $this->start;
+ $fixed = $config->get('settings', 'servicedowntime_end_fixed', 'PT1H');
+ $this->fixedEnd = $fixedEnd->add(new DateInterval($fixed));
+
+ $flexibleEnd = clone $this->start;
+ $flexible = $config->get('settings', 'servicedowntime_end_flexible', 'PT1H');
+ $this->flexibleEnd = $flexibleEnd->add(new DateInterval($flexible));
+
+ $flexibleDuration = $config->get('settings', 'servicedowntime_flexible_duration', 'PT2H');
+ $this->flexibleDuration = new DateInterval($flexibleDuration);
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::getSubmitLabel() For the method documentation.
+ */
+ public function getSubmitLabel()
+ {
+ return $this->translatePlural('Schedule downtime', 'Schedule downtimes', count($this->objects));
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::createElements() For the method documentation.
+ */
+ public function createElements(array $formData = array())
+ {
+ $this->addDescription($this->translate(
+ 'This command is used to schedule host and service downtimes. During the specified downtime,'
+ . ' Icinga will not send notifications out about the hosts and services. When the scheduled'
+ . ' downtime expires, Icinga will send out notifications for the hosts and services as it'
+ . ' normally would. Scheduled downtimes are preserved across program shutdowns and'
+ . ' restarts.'
+ ));
+
+ $isFlexible = (bool) isset($formData['type']) && $formData['type'] === self::FLEXIBLE;
+
+ $this->addElements(array(
+ array(
+ 'textarea',
+ 'comment',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Comment'),
+ 'description' => $this->translate(
+ 'If you work with other administrators, you may find it useful to share information about'
+ . ' the host or service that is having problems. Make sure you enter a brief description of'
+ . ' what you are doing.'
+ ),
+ 'attribs' => array('class' => 'autofocus'),
+ 'value' => $this->commentText
+ )
+ ),
+ array(
+ 'dateTimePicker',
+ 'start',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Start Time'),
+ 'description' => $this->translate('Set the start date and time for the downtime.'),
+ 'value' => $this->start
+ )
+ ),
+ array(
+ 'dateTimePicker',
+ 'end',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('End Time'),
+ 'description' => $this->translate('Set the end date and time for the downtime.'),
+ 'preserveDefault' => true,
+ 'value' => $isFlexible ? $this->flexibleEnd : $this->fixedEnd
+ )
+ ),
+ array(
+ 'select',
+ 'type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Type'),
+ 'description' => $this->translate(
+ 'If you select the fixed option, the downtime will be in effect between the start and end'
+ . ' times you specify whereas a flexible downtime starts when the host or service enters a'
+ . ' problem state sometime between the start and end times you specified and lasts as long'
+ . ' as the duration time you enter. The duration fields do not apply for fixed downtimes.'
+ ),
+ 'multiOptions' => array(
+ self::FIXED => $this->translate('Fixed'),
+ self::FLEXIBLE => $this->translate('Flexible')
+ ),
+ 'validators' => array(
+ array(
+ 'InArray',
+ true,
+ array(array(self::FIXED, self::FLEXIBLE))
+ )
+ )
+ )
+ )
+ ));
+ $this->addDisplayGroup(
+ array('start', 'end'),
+ 'start-end',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div'))
+ )
+ )
+ );
+ if ($isFlexible) {
+ $this->addElements(array(
+ array(
+ 'number',
+ 'hours',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Hours'),
+ 'value' => $this->flexibleDuration->h,
+ 'min' => -1
+ )
+ ),
+ array(
+ 'number',
+ 'minutes',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Minutes'),
+ 'value' => $this->flexibleDuration->m,
+ 'min' => -1
+ )
+ )
+ ));
+ $this->addDisplayGroup(
+ array('hours', 'minutes'),
+ 'duration',
+ array(
+ 'legend' => $this->translate('Flexible Duration'),
+ 'description' => $this->translate(
+ 'Enter here the duration of the downtime. The downtime will be automatically deleted after this'
+ . ' time expired.'
+ ),
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div')),
+ array(
+ 'Description',
+ array('tag' => 'span', 'class' => 'description', 'placement' => 'prepend')
+ ),
+ 'Fieldset'
+ )
+ )
+ );
+ }
+ return $this;
+ }
+
+ public function scheduleDowntime(ScheduleServiceDowntimeCommand $downtime, Request $request)
+ {
+ $downtime
+ ->setComment($this->getElement('comment')->getValue())
+ ->setAuthor($request->getUser()->getUsername())
+ ->setStart($this->getElement('start')->getValue()->getTimestamp())
+ ->setEnd($this->getElement('end')->getValue()->getTimestamp());
+ if ($this->getElement('type')->getValue() === self::FLEXIBLE) {
+ $downtime->setFixed(false);
+ $downtime->setDuration(
+ (float) $this->getElement('hours')->getValue() * 3600
+ + (float) $this->getElement('minutes')->getValue() * 60
+ );
+ }
+ $this->getTransport($request)->send($downtime);
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::onSuccess() For the method documentation.
+ */
+ public function onSuccess()
+ {
+ $end = $this->getValue('end')->getTimestamp();
+ if ($end <= $this->getValue('start')->getTimestamp()) {
+ $endElement = $this->_elements['end'];
+ $endElement->setValue($endElement->getValue()->format($endElement->getFormat()));
+ $endElement->addError($this->translate('The end time must be greater than the start time'));
+ return false;
+ }
+
+ $now = new DateTime;
+ if ($end <= $now->getTimestamp()) {
+ $endElement = $this->_elements['end'];
+ $endElement->setValue($endElement->getValue()->format($endElement->getFormat()));
+ $endElement->addError($this->translate('A downtime must not be in the past'));
+ return false;
+ }
+
+ foreach ($this->objects as $object) {
+ /** @var \Icinga\Module\Monitoring\Object\Service $object */
+ $downtime = new ScheduleServiceDowntimeCommand();
+ $downtime->setObject($object);
+ $this->scheduleDowntime($downtime, $this->request);
+ }
+ Notification::success($this->translatePlural(
+ 'Scheduling service downtime..',
+ 'Scheduling service downtimes..',
+ count($this->objects)
+ ));
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php b/modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php
new file mode 100644
index 0000000..0d1c393
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php
@@ -0,0 +1,110 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use Icinga\Application\Config;
+use Icinga\Module\Monitoring\Command\Object\SendCustomNotificationCommand;
+use Icinga\Web\Notification;
+
+/**
+ * Form to send custom notifications
+ */
+class SendCustomNotificationCommandForm extends ObjectsCommandForm
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->addDescription(
+ $this->translate('This command is used to send custom notifications about hosts or services.')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSubmitLabel()
+ {
+ return $this->translatePlural('Send custom notification', 'Send custom notifications', count($this->objects));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData = array())
+ {
+ $config = Config::module('monitoring');
+
+ $this->addElements(array(
+ array(
+ 'textarea',
+ 'comment',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Comment'),
+ 'description' => $this->translate(
+ 'If you work with other administrators, you may find it useful to share information about'
+ . ' the host or service that is having problems. Make sure you enter a brief description of'
+ . ' what you are doing.'
+ )
+ )
+ ),
+ array(
+ 'checkbox',
+ 'forced',
+ array(
+ 'label' => $this->translate('Forced'),
+ 'value' => (bool) $config->get('settings', 'custom_notification_forced', false),
+ 'description' => $this->translate(
+ 'If you check this option, the notification is sent out regardless of time restrictions and'
+ . ' whether or not notifications are enabled.'
+ )
+ )
+ )
+ ));
+
+ if (! $this->getBackend()->isIcinga2()) {
+ $this->addElement(
+ 'checkbox',
+ 'broadcast',
+ array(
+ 'label' => $this->translate('Broadcast'),
+ 'value' => (bool) $config->get('settings', 'custom_notification_broadcast', false),
+ 'description' => $this->translate(
+ 'If you check this option, the notification is sent out to all normal and escalated contacts.'
+ )
+ )
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onSuccess()
+ {
+ foreach ($this->objects as $object) {
+ /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+ $notification = new SendCustomNotificationCommand();
+ $notification
+ ->setObject($object)
+ ->setComment($this->getElement('comment')->getValue())
+ ->setAuthor($this->request->getUser()->getUsername())
+ ->setForced($this->getElement('forced')->isChecked());
+ if (($broadcast = $this->getElement('broadcast')) !== null) {
+ $notification->setBroadcast($broadcast->isChecked());
+ }
+ $this->getTransport($this->request)->send($notification);
+ }
+ Notification::success($this->translatePlural(
+ 'Sending custom notification..',
+ 'Sending custom notifications..',
+ count($this->objects)
+ ));
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Command/Object/ToggleObjectFeaturesCommandForm.php b/modules/monitoring/application/forms/Command/Object/ToggleObjectFeaturesCommandForm.php
new file mode 100644
index 0000000..e4aabb2
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/ToggleObjectFeaturesCommandForm.php
@@ -0,0 +1,187 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Command\Object;
+
+use Icinga\Module\Monitoring\Command\Object\ToggleObjectFeatureCommand;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use Icinga\Web\Notification;
+
+/**
+ * Form for enabling or disabling features of Icinga objects, i.e. hosts or services
+ */
+class ToggleObjectFeaturesCommandForm extends ObjectsCommandForm
+{
+ /**
+ * Feature to feature spec map
+ *
+ * @var string[]
+ */
+ protected $features;
+
+ /**
+ * Feature to feature status map
+ *
+ * @var int[]
+ */
+ protected $featureStatus;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setUseFormAutosubmit();
+ $this->setAttrib('class', self::DEFAULT_CLASSES . ' object-features');
+ $features = array(
+ ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS => array(
+ 'label' => $this->translate('Active Checks'),
+ 'permission' => 'monitoring/command/feature/object/active-checks'
+ ),
+ ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS => array(
+ 'label' => $this->translate('Passive Checks'),
+ 'permission' => 'monitoring/command/feature/object/passive-checks'
+ ),
+ ToggleObjectFeatureCommand::FEATURE_OBSESSING => array(
+ 'label' => $this->translate('Obsessing'),
+ 'permission' => 'monitoring/command/feature/object/obsessing'
+ ),
+ ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS => array(
+ 'label' => $this->translate('Notifications'),
+ 'permission' => 'monitoring/command/feature/object/notifications'
+ ),
+ ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER => array(
+ 'label' => $this->translate('Event Handler'),
+ 'permission' => 'monitoring/command/feature/object/event-handler'
+ ),
+ ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION => array(
+ 'label' => $this->translate('Flap Detection'),
+ 'permission' => 'monitoring/command/feature/object/flap-detection'
+ )
+ );
+ if ($this->getBackend()->isIcinga2()) {
+ unset($features[ToggleObjectFeatureCommand::FEATURE_OBSESSING]);
+ }
+ $this->features = $features;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData = array())
+ {
+ foreach ($this->features as $feature => $spec) {
+ $options = array(
+ 'autosubmit' => true,
+ 'disabled' => $this->hasPermission($spec['permission']) ? null : 'disabled',
+ 'label' => $spec['label']
+ );
+ if ($formData[$feature . '_changed']) {
+ $options['description'] = $this->translate('changed');
+ }
+ if ($formData[$feature] === 2) {
+ $this->addElement('select', $feature, $options + [
+ 'description' => $this->translate('Multiple Values'),
+ 'filters' => [['Null', ['type' => \Zend_Filter_Null::STRING]]],
+ 'multiOptions' => [
+ '' => $this->translate('Leave Unchanged'),
+ $this->translate('Disable All'),
+ $this->translate('Enable All')
+ ],
+ 'decorators' => array_merge(
+ array_slice(static::$defaultElementDecorators, 0, 3),
+ [['Description', ['tag' => 'span']]],
+ array_slice(static::$defaultElementDecorators, 4, 1),
+ [['HtmlTag', ['tag' => 'div', 'class' => 'control-group indeterminate']]]
+ )
+ ]);
+ } else {
+ $options['value'] = $formData[$feature];
+ $this->addElement('checkbox', $feature, $options);
+ }
+ }
+ }
+
+ /**
+ * Load feature status
+ *
+ * @param MonitoredObject|object $object
+ *
+ * @return $this
+ */
+ public function load($object)
+ {
+ $featureStatus = array();
+ foreach (array_keys($this->features) as $feature) {
+ $featureStatus[$feature] = $object->{$feature};
+ if (isset($object->{$feature . '_changed'})) {
+ $featureStatus[$feature . '_changed'] = (bool) $object->{$feature . '_changed'};
+ } else {
+ $featureStatus[$feature . '_changed'] = false;
+ }
+ }
+ $this->create($featureStatus);
+ $this->featureStatus = $featureStatus;
+
+ return $this;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Form::onSuccess() For the method documentation.
+ */
+ public function onSuccess()
+ {
+ $notifications = array(
+ ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS => array(
+ $this->translate('Enabling active checks..'),
+ $this->translate('Disabling active checks..')
+ ),
+ ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS => array(
+ $this->translate('Enabling passive checks..'),
+ $this->translate('Disabling passive checks..')
+ ),
+ ToggleObjectFeatureCommand::FEATURE_OBSESSING => array(
+ $this->translate('Enabling obsessing..'),
+ $this->translate('Disabling obsessing..')
+ ),
+ ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS => array(
+ $this->translate('Enabling notifications..'),
+ $this->translate('Disabling notifications..')
+ ),
+ ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER => array(
+ $this->translate('Enabling event handler..'),
+ $this->translate('Disabling event handler..')
+ ),
+ ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION => array(
+ $this->translate('Enabling flap detection..'),
+ $this->translate('Disabling flap detection..')
+ )
+ );
+
+ foreach ($this->getValues() as $feature => $enabled) {
+ if ($this->getElement($feature)->getAttrib('disabled') !== null
+ || $enabled === null
+ || (int) $enabled === (int) $this->featureStatus[$feature]
+ ) {
+ continue;
+ }
+ foreach ($this->objects as $object) {
+ /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+ if ((bool) $object->{$feature} !== (bool) $enabled) {
+ $toggleFeature = new ToggleObjectFeatureCommand();
+ $toggleFeature
+ ->setFeature($feature)
+ ->setObject($object)
+ ->setEnabled($enabled);
+ $this->getTransport($this->request)->send($toggleFeature);
+ }
+ }
+ Notification::success(
+ $notifications[$feature][$enabled ? 0 : 1]
+ );
+ }
+
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Config/BackendConfigForm.php b/modules/monitoring/application/forms/Config/BackendConfigForm.php
new file mode 100644
index 0000000..5ed42e1
--- /dev/null
+++ b/modules/monitoring/application/forms/Config/BackendConfigForm.php
@@ -0,0 +1,367 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Config;
+
+use Exception;
+use InvalidArgumentException;
+use Icinga\Application\Config;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\ConfigForm;
+use Icinga\Web\Form;
+
+/**
+ * Form for managing monitoring backends
+ */
+class BackendConfigForm extends ConfigForm
+{
+ /**
+ * The available monitoring backend resources split by type
+ *
+ * @var array
+ */
+ protected $resources;
+
+ /**
+ * The backend to load when displaying the form for the first time
+ *
+ * @var string
+ */
+ protected $backendToLoad;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_monitoring_backends');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param Config $resourceConfig The resource configuration
+ *
+ * @return $this
+ *
+ * @throws ConfigurationError In case there are no valid monitoring backend resources
+ */
+ public function setResourceConfig(Config $resourceConfig)
+ {
+ $resources = array();
+ foreach ($resourceConfig as $name => $resource) {
+ if ($resource->type === 'db') {
+ $resources['ido'][$name] = $name;
+ }
+ }
+
+ if (empty($resources)) {
+ throw new ConfigurationError($this->translate(
+ 'Could not find any valid monitoring backend resources. Please configure a database resource first.'
+ ));
+ }
+
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Populate the form with the given backend's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function load($name)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No monitoring backend called "%s" found', $name);
+ }
+
+ $this->backendToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new monitoring backend
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a backend name
+ * @throws IcingaException In case a backend with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ }
+
+ $backendName = $data['name'];
+ if ($this->config->hasSection($backendName)) {
+ throw new IcingaException(
+ $this->translate('A monitoring backend with the name "%s" does already exist'),
+ $backendName
+ );
+ }
+
+ unset($data['name']);
+ $this->config->setSection($backendName, $data);
+ return $this;
+ }
+
+ /**
+ * Edit a monitoring backend
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function edit($name, array $data)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No monitoring backend called "%s" found', $name);
+ }
+
+ $backendConfig = $this->config->getSection($name);
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $this->config->removeSection($name);
+ $name = $data['name'];
+ }
+
+ unset($data['name']);
+ }
+
+ $backendConfig->merge($data);
+ $this->config->setSection($name, $backendConfig);
+ return $this;
+ }
+
+ /**
+ * Remove a monitoring backend
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function delete($name)
+ {
+ $this->config->removeSection($name);
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'checkbox',
+ 'disabled',
+ array(
+ 'label' => $this->translate('Disable This Backend')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this monitoring backend that is used to differentiate it from others'
+ )
+ )
+ );
+
+ $resourceType = isset($formData['type']) ? $formData['type'] : null;
+
+ $resourceTypes = array();
+ if ($resourceType === 'ido' || array_key_exists('ido', $this->resources)) {
+ $resourceTypes['ido'] = 'IDO Backend';
+ }
+
+ if ($resourceType === null) {
+ $resourceType = key($resourceTypes);
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate(
+ 'The type of data source used for retrieving monitoring information'
+ ),
+ 'multiOptions' => $resourceTypes
+ )
+ );
+
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource'),
+ 'description' => $this->translate('The resource to use'),
+ 'multiOptions' => $this->resources[$resourceType],
+ 'value' => current($this->resources[$resourceType]),
+ 'autosubmit' => true
+ )
+ );
+ $resourceName = isset($formData['resource']) ? $formData['resource'] : $this->getValue('resource');
+ $this->addElement(
+ 'note',
+ 'resource_note',
+ array(
+ 'escape' => false,
+ 'value' => sprintf(
+ '<a href="%1$s" data-base-target="_next" title="%2$s" aria-label="%2$s">%3$s</a>',
+ $this->getView()->url('config/editresource', array('resource' => $resourceName)),
+ sprintf($this->translate('Show the configuration of the %s resource'), $resourceName),
+ $this->translate('Show resource configuration')
+ )
+ )
+ );
+
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ // In case another error occured and the checkbox was displayed before
+ $this->addSkipValidationCheckbox();
+ }
+ }
+
+ /**
+ * Populate the configuration of the backend to load
+ */
+ public function onRequest()
+ {
+ if ($this->backendToLoad) {
+ $data = $this->config->getSection($this->backendToLoad)->toArray();
+ $data['name'] = $this->backendToLoad;
+ $this->populate($data);
+ }
+ }
+
+ /**
+ * Return whether the given values are valid
+ *
+ * @param array $formData The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ if (($el = $this->getElement('skip_validation')) === null || false === $el->isChecked()) {
+ $resourceConfig = ResourceFactory::getResourceConfig($this->getValue('resource'));
+ if (! self::isValidIdoSchema($this, $resourceConfig)
+ || (! $this->getElement('disabled')->isChecked()
+ && ! self::isValidIdoInstance($this, $resourceConfig))
+ ) {
+ if ($el === null) {
+ $this->addSkipValidationCheckbox();
+ }
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a checkbox to the form by which the user can skip the schema validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'order' => 0,
+ 'ignore' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate(
+ 'Check this to not to validate the IDO schema of the chosen resource.'
+ )
+ )
+ );
+ }
+
+ /**
+ * Return whether the given resource contains a valid IDO schema
+ *
+ * @param Form $form
+ * @param ConfigObject $resourceConfig
+ *
+ * @return bool
+ */
+ public static function isValidIdoSchema(Form $form, ConfigObject $resourceConfig)
+ {
+ try {
+ $db = ResourceFactory::createResource($resourceConfig);
+ $db->select()->from('icinga_dbversion', array('version'))->fetchOne();
+ } catch (Exception $_) {
+ $form->error($form->translate(
+ 'Cannot find the IDO schema. Please verify that the given database '
+ . 'contains the schema and that the configured user has access to it.'
+ ));
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return whether a single icinga instance is writing to the given resource
+ *
+ * @param Form $form
+ * @param ConfigObject $resourceConfig
+ *
+ * @return bool True if it's a single instance, false if none
+ * or multiple instances are writing to it
+ */
+ public static function isValidIdoInstance(Form $form, ConfigObject $resourceConfig)
+ {
+ $db = ResourceFactory::createResource($resourceConfig);
+ $rowCount = $db->select()->from('icinga_instances')->count();
+
+ if ($rowCount === 0) {
+ $form->warning($form->translate(
+ 'There is currently no icinga instance writing to the IDO. Make sure '
+ . 'that a icinga instance is configured and able to write to the IDO.'
+ ));
+ return false;
+ } elseif ($rowCount > 1) {
+ $form->warning($form->translate(
+ 'There is currently more than one icinga instance writing to the IDO. You\'ll see all objects from all'
+ . ' instances without any differentation. If this is not desired, consider setting up a separate IDO'
+ . ' for each instance.'
+ ));
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Config/SecurityConfigForm.php b/modules/monitoring/application/forms/Config/SecurityConfigForm.php
new file mode 100644
index 0000000..d57f985
--- /dev/null
+++ b/modules/monitoring/application/forms/Config/SecurityConfigForm.php
@@ -0,0 +1,75 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Config;
+
+use Icinga\Web\Notification;
+use Icinga\Forms\ConfigForm;
+
+/**
+ * Form for modifying security relevant settings
+ */
+class SecurityConfigForm extends ConfigForm
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_monitoring_security');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ /**
+ * @see Form::onSuccess()
+ */
+ public function onSuccess()
+ {
+ $this->config->setSection('security', $this->getValues());
+
+ if ($this->save()) {
+ Notification::success($this->translate('New security configuration has successfully been stored'));
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @see Form::onRequest()
+ */
+ public function onRequest()
+ {
+ $this->populate($this->config->getSection('security')->toArray());
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'protected_customvars',
+ array(
+ 'allowEmpty' => true,
+ 'attribs' => array('placeholder' => $this->getDefaultProtectedCustomvars()),
+ 'label' => $this->translate('Protected Custom Variables'),
+ 'description' => $this->translate(
+ 'Comma separated case insensitive list of protected custom variables.'
+ . ' Use * as a placeholder for zero or more wildcard characters.'
+ . ' Existence of those custom variables will be shown, but their values will be masked.'
+ )
+ )
+ );
+ }
+
+ /**
+ * Return the customvars to suggest to protect when none are protected
+ *
+ * @return string
+ */
+ public function getDefaultProtectedCustomvars()
+ {
+ return '*pw*,*pass*,community';
+ }
+}
diff --git a/modules/monitoring/application/forms/Config/Transport/ApiTransportForm.php b/modules/monitoring/application/forms/Config/Transport/ApiTransportForm.php
new file mode 100644
index 0000000..3d501e0
--- /dev/null
+++ b/modules/monitoring/application/forms/Config/Transport/ApiTransportForm.php
@@ -0,0 +1,75 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Config\Transport;
+
+use Icinga\Web\Form;
+
+class ApiTransportForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_command_transport_api');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData = array())
+ {
+ $this->addElements(array(
+ array(
+ 'text',
+ 'host',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Host'),
+ 'description' => $this->translate(
+ 'Hostname or address of the remote Icinga instance'
+ )
+ )
+ ),
+ array(
+ 'number',
+ 'port',
+ array(
+ 'required' => true,
+ 'preserveDefault' => true,
+ 'label' => $this->translate('Port'),
+ 'description' => $this->translate('SSH port to connect to on the remote Icinga instance'),
+ 'value' => 5665
+ )
+ ),
+ array(
+ 'text',
+ 'username',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('API Username'),
+ 'description' => $this->translate(
+ 'User to log in as on the remote Icinga instance. Please note that key-based SSH login must be'
+ . ' possible for this user'
+ )
+ )
+ ),
+ array(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('API Password'),
+ 'description' => $this->translate(
+ 'User to log in as on the remote Icinga instance. Please note that key-based SSH login must be'
+ . ' possible for this user'
+ ),
+ 'renderPassword' => true
+ )
+ )
+ ));
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/application/forms/Config/Transport/LocalTransportForm.php b/modules/monitoring/application/forms/Config/Transport/LocalTransportForm.php
new file mode 100644
index 0000000..15c7357
--- /dev/null
+++ b/modules/monitoring/application/forms/Config/Transport/LocalTransportForm.php
@@ -0,0 +1,37 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Config\Transport;
+
+use Icinga\Web\Form;
+
+class LocalTransportForm extends Form
+{
+ /**
+ * (non-PHPDoc)
+ * @see Form::init() For the method documentation.
+ */
+ public function init()
+ {
+ $this->setName('form_config_command_transport_local');
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see Form::createElements() For the method documentation.
+ */
+ public function createElements(array $formData = array())
+ {
+ $this->addElement(
+ 'text',
+ 'path',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Command File'),
+ 'value' => '/var/run/icinga2/cmd/icinga2.cmd',
+ 'description' => $this->translate('Path to the local Icinga command file')
+ )
+ );
+ return $this;
+ }
+}
diff --git a/modules/monitoring/application/forms/Config/Transport/RemoteTransportForm.php b/modules/monitoring/application/forms/Config/Transport/RemoteTransportForm.php
new file mode 100644
index 0000000..7beeacf
--- /dev/null
+++ b/modules/monitoring/application/forms/Config/Transport/RemoteTransportForm.php
@@ -0,0 +1,185 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Config\Transport;
+
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Web\Form;
+
+class RemoteTransportForm extends Form
+{
+ /**
+ * The available resources split by type
+ *
+ * @var array
+ */
+ protected $resources;
+
+ /**
+ * (non-PHPDoc)
+ * @see Form::init() For the method documentation.
+ */
+ public function init()
+ {
+ $this->setName('form_config_command_transport_remote');
+ }
+
+ /**
+ * Load all available ssh identity resources
+ *
+ * @return $this
+ *
+ * @throws \Icinga\Exception\ConfigurationError
+ */
+ public function loadResources()
+ {
+ $resourceConfig = ResourceFactory::getResourceConfigs();
+
+ $resources = array();
+ foreach ($resourceConfig as $name => $resource) {
+ if ($resource->type === 'ssh') {
+ $resources['ssh'][$name] = $name;
+ }
+ }
+
+ if (empty($resources)) {
+ throw new ConfigurationError($this->translate('Could not find any valid SSH resources'));
+ }
+
+ $this->resources = $resources;
+
+ return $this;
+ }
+
+ /**
+ * Check whether ssh identity resources exists or not
+ *
+ * @return boolean
+ */
+ public function hasResources()
+ {
+ $resourceConfig = ResourceFactory::getResourceConfigs();
+
+ foreach ($resourceConfig as $name => $resource) {
+ if ($resource->type === 'ssh') {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see Form::createElements() For the method documentation.
+ */
+ public function createElements(array $formData = array())
+ {
+ $useResource = false;
+
+ if ($this->hasResources()) {
+ $useResource = isset($formData['use_resource'])
+ ? $formData['use_resource'] : $this->getValue('use_resource');
+
+ $this->addElement(
+ 'checkbox',
+ 'use_resource',
+ array(
+ 'label' => $this->translate('Use SSH Identity'),
+ 'description' => $this->translate('Make use of the ssh identity resource'),
+ 'autosubmit' => true,
+ 'ignore' => true
+ )
+ );
+ }
+
+ if ($useResource) {
+ $this->loadResources();
+
+ $decorators = static::$defaultElementDecorators;
+ array_pop($decorators); // Removes the HtmlTag decorator
+
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('SSH Identity'),
+ 'description' => $this->translate('The resource to use'),
+ 'decorators' => $decorators,
+ 'multiOptions' => $this->resources['ssh'],
+ 'value' => current($this->resources['ssh']),
+ 'autosubmit' => false
+ )
+ );
+ $resourceName = isset($formData['resource']) ? $formData['resource'] : $this->getValue('resource');
+ $this->addElement(
+ 'note',
+ 'resource_note',
+ array(
+ 'escape' => false,
+ 'decorators' => $decorators,
+ 'value' => sprintf(
+ '<a href="%1$s" data-base-target="_next" title="%2$s" aria-label="%2$s">%3$s</a>',
+ $this->getView()->url('config/editresource', array('resource' => $resourceName)),
+ sprintf($this->translate('Show the configuration of the %s resource'), $resourceName),
+ $this->translate('Show resource configuration')
+ )
+ )
+ );
+ }
+
+ $this->addElements(array(
+ array(
+ 'text',
+ 'host',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Host'),
+ 'description' => $this->translate(
+ 'Hostname or address of the remote Icinga instance'
+ )
+ )
+ ),
+ array(
+ 'number',
+ 'port',
+ array(
+ 'required' => true,
+ 'preserveDefault' => true,
+ 'label' => $this->translate('Port'),
+ 'description' => $this->translate('SSH port to connect to on the remote Icinga instance'),
+ 'value' => 22
+ )
+ )
+ ));
+
+ if (! $useResource) {
+ $this->addElement(
+ 'text',
+ 'user',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('User'),
+ 'description' => $this->translate(
+ 'User to log in as on the remote Icinga instance. Please note that key-based SSH login must be'
+ . ' possible for this user'
+ )
+ )
+ );
+ }
+
+ $this->addElement(
+ 'text',
+ 'path',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Command File'),
+ 'value' => '/var/run/icinga2/cmd/icinga2.cmd',
+ 'description' => $this->translate('Path to the Icinga command file on the remote Icinga instance')
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/application/forms/Config/TransportConfigForm.php b/modules/monitoring/application/forms/Config/TransportConfigForm.php
new file mode 100644
index 0000000..c68e63d
--- /dev/null
+++ b/modules/monitoring/application/forms/Config/TransportConfigForm.php
@@ -0,0 +1,392 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Config;
+
+use Icinga\Data\ConfigObject;
+use Icinga\Module\Monitoring\Command\Transport\CommandTransport;
+use Icinga\Module\Monitoring\Exception\CommandTransportException;
+use InvalidArgumentException;
+use Icinga\Application\Platform;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\ConfigForm;
+use Icinga\Module\Monitoring\Command\Transport\ApiCommandTransport;
+use Icinga\Module\Monitoring\Command\Transport\LocalCommandFile;
+use Icinga\Module\Monitoring\Command\Transport\RemoteCommandFile;
+use Icinga\Module\Monitoring\Forms\Config\Transport\ApiTransportForm;
+use Icinga\Module\Monitoring\Forms\Config\Transport\LocalTransportForm;
+use Icinga\Module\Monitoring\Forms\Config\Transport\RemoteTransportForm;
+
+/**
+ * Form for managing command transports
+ */
+class TransportConfigForm extends ConfigForm
+{
+ /**
+ * The transport to load when displaying the form for the first time
+ *
+ * @var string
+ */
+ protected $transportToLoad;
+
+ /**
+ * The names of all available Icinga instances
+ *
+ * @var array
+ */
+ protected $instanceNames;
+
+ /**
+ * @var bool
+ */
+ protected $validatePartial = true;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_command_transports');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ /**
+ * Set the names of all available Icinga instances
+ *
+ * @param array $names
+ *
+ * @return $this
+ */
+ public function setInstanceNames(array $names)
+ {
+ $this->instanceNames = $names;
+ return $this;
+ }
+
+ /**
+ * Return the names of all available Icinga instances
+ *
+ * @return array
+ */
+ public function getInstanceNames()
+ {
+ return $this->instanceNames ?: array();
+ }
+
+ /**
+ * Return a form object for the given transport type
+ *
+ * @param string $type The transport type for which to return a form
+ *
+ * @return \Icinga\Web\Form
+ *
+ * @throws InvalidArgumentException In case the given transport type is invalid
+ */
+ public function getTransportForm($type)
+ {
+ switch (strtolower($type)) {
+ case LocalCommandFile::TRANSPORT:
+ return new LocalTransportForm();
+ case RemoteCommandFile::TRANSPORT:
+ return new RemoteTransportForm();
+ case ApiCommandTransport::TRANSPORT:
+ return new ApiTransportForm();
+ default:
+ throw new InvalidArgumentException(
+ sprintf($this->translate('Invalid command transport type "%s" given'), $type)
+ );
+ }
+ }
+
+ /**
+ * Populate the form with the given transport's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no transport with the given name is found
+ */
+ public function load($name)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No command transport called "%s" found', $name);
+ }
+
+ $this->transportToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new command transport
+ *
+ * The transport to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a transport name
+ * @throws IcingaException In case a transport with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ }
+
+ $transportName = $data['name'];
+ if ($this->config->hasSection($transportName)) {
+ throw new IcingaException(
+ $this->translate('A command transport with the name "%s" does already exist'),
+ $transportName
+ );
+ }
+
+ unset($data['name']);
+ $this->config->setSection($transportName, $data);
+ return $this;
+ }
+
+ /**
+ * Edit an existing command transport
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no transport with the given name is found
+ */
+ public function edit($name, array $data)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No command transport called "%s" found', $name);
+ }
+
+ $transportConfig = $this->config->getSection($name);
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $this->config->removeSection($name);
+ $name = $data['name'];
+ }
+
+ unset($data['name']);
+ }
+
+ $transportConfig->merge($data);
+ $this->config->setSection($name, $transportConfig);
+ return $this;
+ }
+
+ /**
+ * Remove a command transport
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function delete($name)
+ {
+ $this->config->removeSection($name);
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $instanceNames = $this->getInstanceNames();
+ if (count($instanceNames) > 1) {
+ $options = array('none' => $this->translate('None', 'command transport instance association'));
+ $this->addElement(
+ 'select',
+ 'instance',
+ array(
+ 'label' => $this->translate('Instance Link'),
+ 'description' => $this->translate(
+ 'The name of the Icinga instance this transport should exclusively transfer commands to.'
+ ),
+ 'multiOptions' => array_merge($options, array_combine($instanceNames, $instanceNames))
+ )
+ );
+ }
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Transport Name'),
+ 'description' => $this->translate(
+ 'The name of this command transport that is used to differentiate it from others'
+ )
+ )
+ );
+
+ $transportTypes = array(
+ ApiCommandTransport::TRANSPORT => $this->translate('Icinga 2 API'),
+ LocalCommandFile::TRANSPORT => $this->translate('Local Command File'),
+ RemoteCommandFile::TRANSPORT => $this->translate('Remote Command File')
+ );
+ if (! Platform::extensionLoaded('curl')) {
+ unset($transportTypes[ApiCommandTransport::TRANSPORT]);
+ }
+
+ $transportType = isset($formData['transport']) ? $formData['transport'] : null;
+ if ($transportType === null) {
+ $transportType = key($transportTypes);
+ }
+
+ $this->addElements(array(
+ array(
+ 'select',
+ 'transport',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Transport Type'),
+ 'multiOptions' => $transportTypes
+ )
+ )
+ ));
+
+ $this->addSubForm($this->getTransportForm($transportType)->create($formData), 'transport_form');
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton();
+
+ if ($this->getSubForm('transport_form') instanceof ApiTransportForm) {
+ $btnSubmit = $this->getElement('btn_submit');
+
+ if ($btnSubmit !== null) {
+ // In the setup wizard $this is being used as a subform which doesn't have a submit button.
+ $this->addElement(
+ 'submit',
+ 'transport_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+
+ $this->setAttrib('data-progress-element', 'transport-progress');
+ $this->addElement(
+ 'note',
+ 'transport-progress',
+ array(
+ 'decorators' => array(
+ 'ViewHelper',
+ array('Spinner', array('id' => 'transport-progress'))
+ )
+ )
+ );
+
+ $elements = array('transport_validation', 'transport-progress');
+
+ $btnSubmit->setDecorators(array('ViewHelper'));
+ array_unshift($elements, 'btn_submit');
+
+ $this->addDisplayGroup(
+ $elements,
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Populate the configuration of the transport to load
+ */
+ public function onRequest()
+ {
+ if ($this->transportToLoad) {
+ $data = $this->config->getSection($this->transportToLoad)->toArray();
+ $data['name'] = $this->transportToLoad;
+ $this->populate($data);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValidPartial(array $formData)
+ {
+ $isValidPartial = parent::isValidPartial($formData);
+
+ $transportValidation = $this->getElement('transport_validation');
+ if ($transportValidation !== null && $transportValidation->isChecked() && $this->isValid($formData)) {
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ }
+
+ return $isValidPartial;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ if ($this->getSubForm('transport_form') instanceof ApiTransportForm) {
+ if (! isset($formData['transport_validation'])
+ && isset($formData['force_creation']) && $formData['force_creation']
+ ) {
+ // ignore any validation result
+ return true;
+ }
+
+ try {
+ CommandTransport::createTransport(new ConfigObject($this->getValues()))->probe();
+ } catch (CommandTransportException $e) {
+ $this->error(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $e->getMessage()
+ ));
+
+ $this->addElement(
+ 'checkbox',
+ 'force_creation',
+ array(
+ 'order' => 0,
+ 'ignore' => true,
+ 'label' => $this->translate('Force Changes'),
+ 'description' => $this->translate(
+ 'Check this box to enforce changes without connectivity validation'
+ )
+ )
+ );
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Config/TransportReorderForm.php b/modules/monitoring/application/forms/Config/TransportReorderForm.php
new file mode 100644
index 0000000..f3efe4c
--- /dev/null
+++ b/modules/monitoring/application/forms/Config/TransportReorderForm.php
@@ -0,0 +1,87 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Config;
+
+use Icinga\Application\Config;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for reordering command transports
+ */
+class TransportReorderForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_reorder_command_transports');
+ $this->setViewScript('form/reorder-command-transports.phtml');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ // This adds just a dummy element to be able to utilize Form::getValue as part of onSuccess()
+ $this->addElement(
+ 'hidden',
+ 'transport_newpos',
+ array(
+ 'required' => true,
+ 'validators' => array(
+ array(
+ 'validator' => 'regex',
+ 'options' => array(
+ 'pattern' => '/\A\d+\|/'
+ )
+ )
+ )
+ )
+ );
+ }
+
+ /**
+ * Update the command transport order and save the configuration
+ */
+ public function onSuccess()
+ {
+ list($position, $transportName) = explode('|', $this->getValue('transport_newpos'), 2);
+ $config = $this->getConfig();
+ if (! $config->hasSection($transportName)) {
+ Notification::error(sprintf($this->translate('Command transport "%s" not found'), $transportName));
+ return false;
+ }
+
+ if ($config->count() > 1) {
+ $sections = $config->keys();
+ array_splice($sections, array_search($transportName, $sections, true), 1);
+ array_splice($sections, $position, 0, array($transportName));
+
+ $sectionsInNewOrder = array();
+ foreach ($sections as $section) {
+ $sectionsInNewOrder[$section] = $config->getSection($section);
+ $config->removeSection($section);
+ }
+ foreach ($sectionsInNewOrder as $name => $options) {
+ $config->setSection($name, $options);
+ }
+
+ $config->saveIni();
+ Notification::success($this->translate('Command transport order updated'));
+ }
+ }
+
+ /**
+ * Get the command transports config
+ *
+ * @return Config
+ */
+ public function getConfig()
+ {
+ return Config::module('monitoring', 'commandtransports');
+ }
+}
diff --git a/modules/monitoring/application/forms/EventOverviewForm.php b/modules/monitoring/application/forms/EventOverviewForm.php
new file mode 100644
index 0000000..78774f9
--- /dev/null
+++ b/modules/monitoring/application/forms/EventOverviewForm.php
@@ -0,0 +1,157 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms;
+
+use Icinga\Web\Url;
+use Icinga\Web\Form;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Configure the filter for the event overview
+ */
+class EventOverviewForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_event_overview');
+ $this->setDecorators(array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'hbox')),
+ 'Form'
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $decorators = array(
+ array('Label', array('class' => 'optional')),
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'hbox-item optionbox')),
+ );
+
+ $url = Url::fromRequest()->getAbsoluteUrl();
+ $this->addElement(
+ 'checkbox',
+ 'statechange',
+ array(
+ 'label' => $this->translate('State Changes'),
+ 'class' => 'autosubmit',
+ 'decorators' => $decorators,
+ 'value' => strpos($url, $this->stateChangeFilter()->toQueryString()) === false ? 0 : 1
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'downtime',
+ array(
+ 'label' => $this->translate('Downtimes'),
+ 'class' => 'autosubmit',
+ 'decorators' => $decorators,
+ 'value' => strpos($url, $this->downtimeFilter()->toQueryString()) === false ? 0 : 1
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'comment',
+ array(
+ 'label' => $this->translate('Comments'),
+ 'class' => 'autosubmit',
+ 'decorators' => $decorators,
+ 'value' => strpos($url, $this->commentFilter()->toQueryString()) === false ? 0 : 1
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'notification',
+ array(
+ 'label' => $this->translate('Notifications'),
+ 'class' => 'autosubmit',
+ 'decorators' => $decorators,
+ 'value' => strpos($url, $this->notificationFilter()->toQueryString()) === false ? 0 : 1
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'flapping',
+ array(
+ 'label' => $this->translate('Flapping'),
+ 'class' => 'autosubmit',
+ 'decorators' => $decorators,
+ 'value' => strpos($url, $this->flappingFilter()->toQueryString()) === false ? 0 : 1
+ )
+ );
+ }
+
+ /**
+ * Return the corresponding filter-object
+ *
+ * @returns Filter
+ */
+ public function getFilter()
+ {
+ $filters = array();
+ if ($this->getValue('statechange')) {
+ $filters[] = $this->stateChangeFilter();
+ }
+ if ($this->getValue('comment')) {
+ $filters[] = $this->commentFilter();
+ }
+ if ($this->getValue('notification')) {
+ $filters[] = $this->notificationFilter();
+ }
+ if ($this->getValue('downtime')) {
+ $filters[] = $this->downtimeFilter();
+ }
+ if ($this->getValue('flapping')) {
+ $filters[] = $this->flappingFilter();
+ }
+ return Filter::matchAny($filters);
+ }
+
+ public function stateChangeFilter()
+ {
+ return Filter::matchAny(
+ Filter::expression('type', '=', 'hard_state'),
+ Filter::expression('type', '=', 'soft_state')
+ );
+ }
+
+ public function commentFilter()
+ {
+ return Filter::matchAny(
+ Filter::expression('type', '=', 'comment'),
+ Filter::expression('type', '=', 'comment_deleted'),
+ Filter::expression('type', '=', 'dt_comment'),
+ Filter::expression('type', '=', 'dt_comment_deleted'),
+ Filter::expression('type', '=', 'ack')
+ );
+ }
+
+ public function notificationFilter()
+ {
+ return Filter::expression('type', '=', 'notify');
+ }
+
+ public function downtimeFilter()
+ {
+ return Filter::matchAny(
+ Filter::expression('type', '=', 'downtime_start'),
+ Filter::expression('type', '=', 'downtime_end')
+ );
+ }
+
+ public function flappingFilter()
+ {
+ return Filter::matchAny(
+ Filter::expression('type', '=', 'flapping'),
+ Filter::expression('type', '=', 'flapping_deleted')
+ );
+ }
+}
diff --git a/modules/monitoring/application/forms/Navigation/ActionForm.php b/modules/monitoring/application/forms/Navigation/ActionForm.php
new file mode 100644
index 0000000..81d5588
--- /dev/null
+++ b/modules/monitoring/application/forms/Navigation/ActionForm.php
@@ -0,0 +1,79 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Navigation;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\QueryException;
+use Icinga\Forms\Navigation\NavigationItemForm;
+
+class ActionForm extends NavigationItemForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ parent::createElements($formData);
+
+ $this->addElement(
+ 'text',
+ 'filter',
+ array(
+ 'allowEmpty' => true,
+ 'label' => $this->translate('Filter'),
+ 'description' => $this->translate(
+ 'Display this action only for objects matching this filter. Leave it blank'
+ . ' if you want this action being displayed regardless of the object'
+ )
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ if (($filterString = $this->getValue('filter')) !== null) {
+ $filter = Filter::matchAll();
+ $filter->setAllowedFilterColumns(array(
+ 'host_name',
+ 'hostgroup_name',
+ 'instance_name',
+ 'service_description',
+ 'servicegroup_name',
+ 'contact_name',
+ 'contactgroup_name',
+ function ($c) {
+ return preg_match('/^_(?:host|service)_/', $c);
+ }
+ ));
+
+ try {
+ $filter->addFilter(Filter::fromQueryString($filterString));
+ } catch (QueryException $_) {
+ $this->getElement('filter')->addError(sprintf(
+ $this->translate('Invalid filter provided. You can only use the following columns: %s'),
+ implode(', ', array(
+ 'instance_name',
+ 'host_name',
+ 'hostgroup_name',
+ 'service_description',
+ 'servicegroup_name',
+ 'contact_name',
+ 'contactgroup_name',
+ '_(host|service)_<customvar-name>'
+ ))
+ ));
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Navigation/HostActionForm.php b/modules/monitoring/application/forms/Navigation/HostActionForm.php
new file mode 100644
index 0000000..da237d4
--- /dev/null
+++ b/modules/monitoring/application/forms/Navigation/HostActionForm.php
@@ -0,0 +1,8 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Navigation;
+
+class HostActionForm extends ActionForm
+{
+}
diff --git a/modules/monitoring/application/forms/Navigation/ServiceActionForm.php b/modules/monitoring/application/forms/Navigation/ServiceActionForm.php
new file mode 100644
index 0000000..68314d1
--- /dev/null
+++ b/modules/monitoring/application/forms/Navigation/ServiceActionForm.php
@@ -0,0 +1,8 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Navigation;
+
+class ServiceActionForm extends ActionForm
+{
+}
diff --git a/modules/monitoring/application/forms/Setup/BackendPage.php b/modules/monitoring/application/forms/Setup/BackendPage.php
new file mode 100644
index 0000000..d5c7efb
--- /dev/null
+++ b/modules/monitoring/application/forms/Setup/BackendPage.php
@@ -0,0 +1,51 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Setup;
+
+use Icinga\Web\Form;
+use Icinga\Application\Platform;
+
+class BackendPage extends Form
+{
+ public function init()
+ {
+ $this->setName('setup_monitoring_backend');
+ $this->setTitle($this->translate('Monitoring Backend', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'Please configure below how Icinga Web 2 should retrieve monitoring information.'
+ ));
+ }
+
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'value' => 'icinga',
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate('The identifier of this backend')
+ )
+ );
+
+ $resourceTypes = array();
+ if (Platform::hasMysqlSupport() || Platform::hasPostgresqlSupport()) {
+ $resourceTypes['ido'] = 'IDO';
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate(
+ 'The data source used for retrieving monitoring information'
+ ),
+ 'multiOptions' => $resourceTypes
+ )
+ );
+ }
+}
diff --git a/modules/monitoring/application/forms/Setup/IdoResourcePage.php b/modules/monitoring/application/forms/Setup/IdoResourcePage.php
new file mode 100644
index 0000000..d648579
--- /dev/null
+++ b/modules/monitoring/application/forms/Setup/IdoResourcePage.php
@@ -0,0 +1,188 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Setup;
+
+use Icinga\Data\ConfigObject;
+use Icinga\Forms\Config\ResourceConfigForm;
+use Icinga\Forms\Config\Resource\DbResourceForm;
+use Icinga\Web\Form;
+use Icinga\Module\Monitoring\Forms\Config\BackendConfigForm;
+use Icinga\Module\Setup\Utils\DbTool;
+
+class IdoResourcePage extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('setup_monitoring_ido');
+ $this->setTitle($this->translate('Monitoring IDO Resource', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'Please fill out the connection details below to access the IDO database of your monitoring environment.'
+ ));
+ $this->setValidatePartial(true);
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'hidden',
+ 'type',
+ array(
+ 'required' => true,
+ 'value' => 'db'
+ )
+ );
+
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ // In case another error occured and the checkbox was displayed before
+ $this->addSkipValidationCheckbox();
+ } else {
+ $this->addElement(
+ 'hidden',
+ 'skip_validation',
+ array(
+ 'required' => true,
+ 'value' => 0
+ )
+ );
+ }
+
+ $dbResourceForm = new DbResourceForm();
+ $this->addElements($dbResourceForm->createElements($formData)->getElements());
+ $this->getElement('name')->setValue('icinga_ido');
+ }
+
+ /**
+ * Return whether the given values are valid
+ *
+ * @param array $formData The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ if (! isset($formData['skip_validation']) || !$formData['skip_validation']) {
+ if (! $this->validateConfiguration()) {
+ $this->addSkipValidationCheckbox();
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Run the configured backend's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (isset($formData['backend_validation']) && parent::isValid($formData)) {
+ if (! $this->validateConfiguration(true)) {
+ return false;
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ } elseif (! isset($formData['backend_validation'])) {
+ // This is usually done by isValid(Partial), but as we're not calling any of these...
+ $this->populate($formData);
+ }
+
+ return true;
+ }
+
+ /**
+ * Return whether the configuration is valid
+ *
+ * @param bool $showLog Whether to show the validation log
+ *
+ * @return bool
+ */
+ protected function validateConfiguration($showLog = false)
+ {
+ $inspection = ResourceConfigForm::inspectResource($this);
+ if ($inspection !== null) {
+ if ($showLog) {
+ $join = function ($e) use (&$join) {
+ return is_string($e) ? $e : join("\n", array_map($join, $e));
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+ }
+
+ if ($inspection->hasError()) {
+ $this->error(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $configObject = new ConfigObject($this->getValues());
+ if (! BackendConfigForm::isValidIdoSchema($this, $configObject)
+ || !BackendConfigForm::isValidIdoInstance($this, $configObject)
+ ) {
+ return false;
+ }
+
+ if ($this->getValue('db') === 'pgsql') {
+ $db = new DbTool($this->getValues());
+ $version = $db->connectToDb()->getServerVersion();
+ if (version_compare($version, '9.1', '<')) {
+ $this->error(sprintf(
+ $this->translate('The server\'s version %s is too old. The minimum required version is %s.'),
+ $version,
+ '9.1'
+ ));
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a checkbox to the form by which the user can skip the configuration validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate('Check this to not to validate the configuration')
+ )
+ );
+ }
+}
diff --git a/modules/monitoring/application/forms/Setup/SecurityPage.php b/modules/monitoring/application/forms/Setup/SecurityPage.php
new file mode 100644
index 0000000..999103c
--- /dev/null
+++ b/modules/monitoring/application/forms/Setup/SecurityPage.php
@@ -0,0 +1,27 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Setup;
+
+use Icinga\Web\Form;
+use Icinga\Module\Monitoring\Forms\Config\SecurityConfigForm;
+
+class SecurityPage extends Form
+{
+ public function init()
+ {
+ $this->setName('setup_monitoring_security');
+ $this->setTitle($this->translate('Monitoring Security', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'To protect your monitoring environment against prying eyes please fill out the settings below.'
+ ));
+ }
+
+ public function createElements(array $formData)
+ {
+ $securityConfigForm = new SecurityConfigForm();
+ $securityConfigForm->createElements($formData);
+ $this->addElements($securityConfigForm->getElements());
+ $this->getElement('protected_customvars')->setValue($securityConfigForm->getDefaultProtectedCustomvars());
+ }
+}
diff --git a/modules/monitoring/application/forms/Setup/TransportPage.php b/modules/monitoring/application/forms/Setup/TransportPage.php
new file mode 100644
index 0000000..9d0760a
--- /dev/null
+++ b/modules/monitoring/application/forms/Setup/TransportPage.php
@@ -0,0 +1,55 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Setup;
+
+use Icinga\Web\Form;
+use Icinga\Module\Monitoring\Forms\Config\TransportConfigForm;
+
+class TransportPage extends Form
+{
+ public function init()
+ {
+ $this->setName('setup_command_transport');
+ $this->setTitle($this->translate('Command Transport', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'Please define below how you want to send commands to your monitoring instance.'
+ ));
+ $this->setValidatePartial(true);
+ }
+
+ public function createElements(array $formData)
+ {
+ $transportConfigForm = new TransportConfigForm();
+ $this->addSubForm($transportConfigForm, 'transport_form');
+ $transportConfigForm->create($formData);
+ $transportConfigForm->removeElement('instance');
+ $transportConfigForm->getElement('name')->setValue('icinga2');
+ }
+
+ public function getValues($suppressArrayNotation = false)
+ {
+ return $this->getSubForm('transport_form')->getValues($suppressArrayNotation);
+ }
+
+ /**
+ * Run the configured backend's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'transport_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (isset($formData['transport_validation']) && parent::isValid($formData)) {
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ } elseif (! isset($formData['transport_validation'])) {
+ // This is usually done by isValid(Partial), but as we're not calling any of these...
+ $this->populate($formData);
+ }
+
+ return true;
+ }
+}
diff --git a/modules/monitoring/application/forms/Setup/WelcomePage.php b/modules/monitoring/application/forms/Setup/WelcomePage.php
new file mode 100644
index 0000000..aa78db5
--- /dev/null
+++ b/modules/monitoring/application/forms/Setup/WelcomePage.php
@@ -0,0 +1,63 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms\Setup;
+
+use Icinga\Web\Form;
+
+class WelcomePage extends Form
+{
+ public function init()
+ {
+ $this->setName('setup_monitoring_welcome');
+ }
+
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'note',
+ 'welcome',
+ array(
+ 'value' => $this->translate(
+ 'Welcome to the configuration of the monitoring module for Icinga Web 2!'
+ ),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'h2'))
+ )
+ )
+ );
+
+ $this->addElement(
+ 'note',
+ 'core_hint',
+ array(
+ 'value' => $this->translate('This is the core module for Icinga Web 2.'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+
+ $this->addElement(
+ 'note',
+ 'description',
+ array(
+ 'value' => $this->translate(
+ 'It offers various status and reporting views with powerful filter capabilities that allow'
+ . ' you to keep track of the most important events in your monitoring environment.'
+ ),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+
+ $this->addDisplayGroup(
+ array('core_hint', 'description'),
+ 'info',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'info'))
+ )
+ )
+ );
+ }
+}
diff --git a/modules/monitoring/application/forms/StatehistoryForm.php b/modules/monitoring/application/forms/StatehistoryForm.php
new file mode 100644
index 0000000..c28e39c
--- /dev/null
+++ b/modules/monitoring/application/forms/StatehistoryForm.php
@@ -0,0 +1,141 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Forms;
+
+use Icinga\Web\Form;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Configure the filter for the event grid
+ */
+class StatehistoryForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_event_overview');
+ $this->setSubmitLabel($this->translate('Apply'));
+ }
+
+ /**
+ * Return the corresponding filter-object
+ *
+ * @returns Filter
+ */
+ public function getFilter()
+ {
+ $baseFilter = Filter::matchAny(
+ Filter::expression('type', '=', 'hard_state')
+ );
+
+ if ($this->getValue('objecttype') === 'hosts') {
+ $objectTypeFilter = Filter::expression('object_type', '=', 'host');
+ } else {
+ $objectTypeFilter = Filter::expression('object_type', '=', 'service');
+ }
+
+ $states = array(
+ 'cnt_down_hard' => Filter::expression('state', '=', '1'),
+ 'cnt_unreachable_hard' => Filter::expression('state', '=', '2'),
+ 'cnt_up' => Filter::expression('state', '=', '0'),
+ 'cnt_critical_hard' => Filter::expression('state', '=', '2'),
+ 'cnt_warning_hard' => Filter::expression('state', '=', '1'),
+ 'cnt_unknown_hard' => Filter::expression('state', '=', '3'),
+ 'cnt_ok' => Filter::expression('state', '=', '0')
+ );
+ $state = $this->getValue('state');
+ $stateFilter = $states[$state];
+ if (in_array($state, array('cnt_ok', 'cnt_up'))) {
+ return Filter::matchAll($objectTypeFilter, $stateFilter);
+ }
+ return Filter::matchAll($baseFilter, $objectTypeFilter, $stateFilter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'select',
+ 'from',
+ array(
+ 'label' => $this->translate('From'),
+ 'value' => $this->getRequest()->getParam('from', strtotime('3 months ago')),
+ 'multiOptions' => array(
+ strtotime('midnight 3 months ago') => $this->translate('3 Months'),
+ strtotime('midnight 4 months ago') => $this->translate('4 Months'),
+ strtotime('midnight 8 months ago') => $this->translate('8 Months'),
+ strtotime('midnight 12 months ago') => $this->translate('1 Year'),
+ strtotime('midnight 24 months ago') => $this->translate('2 Years')
+ )
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'to',
+ array(
+ 'label' => $this->translate('To'),
+ 'value' => $this->getRequest()->getParam('to', time()),
+ 'multiOptions' => array(
+ time() => $this->translate('Today')
+ )
+ )
+ );
+
+ $objectType = $this->getRequest()->getParam('objecttype', 'services');
+ $this->addElement(
+ 'select',
+ 'objecttype',
+ array(
+ 'label' => $this->translate('Object type'),
+ 'value' => $objectType,
+ 'multiOptions' => array(
+ 'services' => $this->translate('Services'),
+ 'hosts' => $this->translate('Hosts')
+ )
+ )
+ );
+ if ($objectType === 'services') {
+ $serviceState = $this->getRequest()->getParam('state', 'cnt_critical_hard');
+ if (in_array($serviceState, array('cnt_down_hard', 'cnt_unreachable_hard', 'cnt_up'))) {
+ $serviceState = 'cnt_critical_hard';
+ }
+ $this->addElement(
+ 'select',
+ 'state',
+ array(
+ 'label' => $this->translate('State'),
+ 'value' => $serviceState,
+ 'multiOptions' => array(
+ 'cnt_critical_hard' => $this->translate('Critical'),
+ 'cnt_warning_hard' => $this->translate('Warning'),
+ 'cnt_unknown_hard' => $this->translate('Unknown'),
+ 'cnt_ok' => $this->translate('Ok')
+ )
+ )
+ );
+ } else {
+ $hostState = $this->getRequest()->getParam('state', 'cnt_down_hard');
+ if (in_array($hostState, array('cnt_ok', 'cnt_critical_hard', 'cnt_warning', 'cnt_unknown'))) {
+ $hostState = 'cnt_down_hard';
+ }
+ $this->addElement(
+ 'select',
+ 'state',
+ array(
+ 'label' => $this->translate('State'),
+ 'value' => $hostState,
+ 'multiOptions' => array(
+ 'cnt_up' => $this->translate('Up'),
+ 'cnt_down_hard' => $this->translate('Down'),
+ 'cnt_unreachable_hard' => $this->translate('Unreachable')
+ )
+ )
+ );
+ }
+ }
+}
diff --git a/modules/monitoring/application/views/helpers/CheckPerformance.php b/modules/monitoring/application/views/helpers/CheckPerformance.php
new file mode 100644
index 0000000..feac4d8
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/CheckPerformance.php
@@ -0,0 +1,50 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+/**
+ * Convert check summary data into a simple usable stdClass
+ */
+class Zend_View_Helper_CheckPerformance extends Zend_View_Helper_Abstract
+{
+ /**
+ * Create dispatch instance
+ *
+ * @return $this
+ */
+ public function checkPerformance()
+ {
+ return $this;
+ }
+
+ /**
+ * Create a condensed row of object data
+ *
+ * @param array $results Array of stdClass
+ *
+ * @return stdClass Condensed row
+ */
+ public function create(array $results)
+ {
+ $out = new stdClass();
+ $out->host_passive_count = 0;
+ $out->host_passive_latency_avg = 0;
+ $out->host_passive_execution_avg = 0;
+ $out->service_passive_count = 0;
+ $out->service_passive_latency_avg = 0;
+ $out->service_passive_execution_avg = 0;
+ $out->service_active_count = 0;
+ $out->service_active_latency_avg = 0;
+ $out->service_active_execution_avg = 0;
+ $out->host_active_count = 0;
+ $out->host_active_latency_avg = 0;
+ $out->host_active_execution_avg = 0;
+
+ foreach ($results as $row) {
+ $key = $row->object_type . '_' . $row->check_type . '_';
+ $out->{$key . 'count'} = $row->object_count;
+ $out->{$key . 'latency_avg'} = $row->latency / $row->object_count;
+ $out->{$key . 'execution_avg'} = $row->execution_time / $row->object_count;
+ }
+ return $out;
+ }
+}
diff --git a/modules/monitoring/application/views/helpers/ContactFlags.php b/modules/monitoring/application/views/helpers/ContactFlags.php
new file mode 100644
index 0000000..858c726
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/ContactFlags.php
@@ -0,0 +1,46 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+class Zend_View_Helper_ContactFlags extends Zend_View_Helper_Abstract
+{
+ /**
+ * Get the human readable flag name for the given contact notification option
+ *
+ * @param string $tableName The name of the option table
+ *
+ * @return string
+ */
+ public function getNotificationOptionName($tableName)
+ {
+ $exploded = explode('_', $tableName);
+ $name = end($exploded);
+ return ucfirst($name);
+ }
+
+ /**
+ * Build all active notification options to a readable string
+ *
+ * @param object $contact The contact retrieved from a backend
+ * @param string $type Whether to display the flags for 'host' or 'service'
+ * @param string $glue The symbol to use to concatenate the flag names
+ *
+ * @return string A string that contains a human readable list of active options
+ */
+ public function contactFlags($contact, $type, $glue = ', ')
+ {
+ $optionName = 'contact_' . $type . '_notification_options';
+ if (isset($contact->$optionName)) {
+ return $contact->$optionName;
+ }
+ $out = array();
+ foreach ($contact as $key => $value) {
+ if (preg_match('/^contact_notify_' . $type . '_.*/', $key) && $value == true) {
+ $option = $this->getNotificationOptionName($key);
+ if (strtolower($option) != 'timeperiod') {
+ array_push($out, $option);
+ }
+ }
+ }
+ return implode($glue, $out);
+ }
+}
diff --git a/modules/monitoring/application/views/helpers/Customvar.php b/modules/monitoring/application/views/helpers/Customvar.php
new file mode 100644
index 0000000..f015fcd
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/Customvar.php
@@ -0,0 +1,67 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+use Icinga\Web\View;
+
+class Zend_View_Helper_Customvar extends Zend_View_Helper_Abstract
+{
+ /** @var View */
+ public $view;
+
+ /**
+ * Create dispatch instance
+ *
+ * @return $this
+ */
+ public function checkPerformance()
+ {
+ return $this;
+ }
+
+ public function customvar($struct)
+ {
+ if (is_scalar($struct)) {
+ return nl2br($this->view->escape(
+ is_string($struct)
+ ? $struct
+ : var_export($struct, true)
+ ), false);
+ } elseif (is_array($struct)) {
+ return $this->renderArray($struct);
+ } elseif (is_object($struct)) {
+ return $this->renderObject($struct);
+ }
+ }
+
+ protected function renderArray($array)
+ {
+ if (empty($array)) {
+ return '[]';
+ }
+ $out = "<ul>\n";
+
+ foreach ($array as $val) {
+ $out .= '<li>' . $this->customvar($val) . "</li>\n";
+ }
+
+ return $out . "</ul>\n";
+ }
+
+ protected function renderObject($object)
+ {
+ if (0 === count((array) $object)) {
+ return '{}';
+ }
+ $out = "{<ul>\n";
+
+ foreach ($object as $key => $val) {
+ $out .= '<li>'
+ . $this->view->escape($key)
+ . ' => '
+ . $this->customvar($val)
+ . "</li>\n";
+ }
+
+ return $out . "</ul>}";
+ }
+}
diff --git a/modules/monitoring/application/views/helpers/EscapeComment.php b/modules/monitoring/application/views/helpers/EscapeComment.php
new file mode 100644
index 0000000..0afc997
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/EscapeComment.php
@@ -0,0 +1,34 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+/**
+ * Helper for escaping comments, but preserving links
+ */
+class Zend_View_Helper_EscapeComment extends Zend_View_Helper_Abstract
+{
+ /**
+ * The purifier to use for escaping
+ *
+ * @var HTMLPurifier
+ */
+ protected static $purifier;
+
+ /**
+ * Escape any comment for being placed inside HTML, but preserve simple links (<a href="...">).
+ *
+ * @param string $comment
+ *
+ * @return string
+ */
+ public function escapeComment($comment)
+ {
+ if (self::$purifier === null) {
+ $config = HTMLPurifier_Config::createDefault();
+ $config->set('Core.EscapeNonASCIICharacters', true);
+ $config->set('HTML.Allowed', 'a[href]');
+ $config->set('Cache.DefinitionImpl', null);
+ self::$purifier = new HTMLPurifier($config);
+ }
+ return self::$purifier->purify($comment);
+ }
+}
diff --git a/modules/monitoring/application/views/helpers/HostFlags.php b/modules/monitoring/application/views/helpers/HostFlags.php
new file mode 100644
index 0000000..bf2e2f5
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/HostFlags.php
@@ -0,0 +1,38 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+use Icinga\Web\View;
+
+class Zend_View_Helper_HostFlags extends Zend_View_Helper_Abstract
+{
+ /** @var View */
+ public $view;
+
+ public function hostFlags($host)
+ {
+ $icons = array();
+ if (! $host->host_handled && $host->host_state > 0) {
+ $icons[] = $this->view->icon('attention-alt', $this->view->translate('Unhandled'));
+ }
+ if ($host->host_acknowledged) {
+ $icons[] = $this->view->icon('ok', $this->view->translate('Acknowledged'));
+ }
+ if ($host->host_is_flapping) {
+ $icons[] = $this->view->icon('flapping', $this->view->translate('Flapping'));
+ }
+ if (! $host->host_notifications_enabled) {
+ $icons[] = $this->view->icon('bell-off-empty', $this->view->translate('Notifications Disabled'));
+ }
+ if ($host->host_in_downtime) {
+ $icons[] = $this->view->icon('plug', $this->view->translate('In Downtime'));
+ }
+ if (! $host->host_active_checks_enabled) {
+ if (! $host->host_passive_checks_enabled) {
+ $icons[] = $this->view->icon('eye-off', $this->view->translate('Active And Passive Checks Disabled'));
+ } else {
+ $icons[] = $this->view->icon('eye-off', $this->view->translate('Active Checks Disabled'));
+ }
+ }
+ return implode(' ', $icons);
+ }
+}
diff --git a/modules/monitoring/application/views/helpers/IconImage.php b/modules/monitoring/application/views/helpers/IconImage.php
new file mode 100644
index 0000000..0cee7de
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/IconImage.php
@@ -0,0 +1,69 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+use Icinga\Module\Monitoring\Object\Macro;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use Icinga\Web\View;
+
+/**
+ * Generate icons to describe a given hosts state
+ */
+class Zend_View_Helper_IconImage extends Zend_View_Helper_Abstract
+{
+ /** @var View */
+ public $view;
+
+ /**
+ * Create dispatch instance
+ *
+ * @return \Zend_View_Helper_IconImage
+ */
+ public function iconImage()
+ {
+ return $this;
+ }
+
+ /**
+ * Display the image_icon of a MonitoredObject
+ *
+ * @param MonitoredObject|stdClass $object The host or service
+ * @return string
+ */
+ public function host($object)
+ {
+ if ($object->host_icon_image && ! preg_match('/[\'"]/', $object->host_icon_image)) {
+ return $this->view->icon(
+ Macro::resolveMacros($object->host_icon_image, $object),
+ null,
+ array(
+ 'alt' => $object->host_icon_image_alt,
+ 'class' => 'host-icon-image',
+ 'title' => $object->host_icon_image_alt
+ )
+ );
+ }
+ return '';
+ }
+
+ /**
+ * Display the image_icon of a MonitoredObject
+ *
+ * @param MonitoredObject|stdClass $object The host or service
+ * @return string
+ */
+ public function service($object)
+ {
+ if ($object->service_icon_image && ! preg_match('/[\'"]/', $object->service_icon_image)) {
+ return $this->view->icon(
+ Macro::resolveMacros($object->service_icon_image, $object),
+ null,
+ array(
+ 'alt' => $object->service_icon_image_alt,
+ 'class' => 'service-icon-image',
+ 'title' => $object->service_icon_image_alt
+ )
+ );
+ }
+ return '';
+ }
+}
diff --git a/modules/monitoring/application/views/helpers/Link.php b/modules/monitoring/application/views/helpers/Link.php
new file mode 100644
index 0000000..c5443a4
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/Link.php
@@ -0,0 +1,72 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+/**
+ * Helper for generating frequently used jump links
+ *
+ * Most of the monitoring overviews link to detail information, e.g. the full information of the involved monitored
+ * object. Instead of reintroducing link generation and translation in those views, this helper contains most
+ * frequently used jump links.
+ */
+class Zend_View_Helper_Link extends Zend_View_Helper_Abstract
+{
+ /**
+ * Helper entry point
+ *
+ * @return $this
+ */
+ public function link()
+ {
+ return $this;
+ }
+
+ /**
+ * Create a host link
+ *
+ * @param string $host Hostname
+ * @param string $linkText Link text, e.g. the host's display name
+ *
+ * @return string
+ */
+ public function host($host, $linkText)
+ {
+ return $this->view->qlink(
+ $linkText,
+ 'monitoring/host/show',
+ array('host' => $host),
+ array('title' => sprintf($this->view->translate('Show detailed information for host %s'), $linkText))
+ );
+ }
+
+ /**
+ * Create a service link
+ *
+ * @param string $service Service name
+ * @param string $serviceLinkText Text for the service link, e.g. the service's display name
+ * @param string $host Hostname
+ * @param string $hostLinkText Text for the host link, e.g. the host's display name
+ * @param string $class An optional class to use for this link
+ *
+ * @return string
+ */
+ public function service($service, $serviceLinkText, $host, $hostLinkText, $class = null)
+ {
+ return sprintf(
+ '%s&#58; %s',
+ $this->host($host, $hostLinkText),
+ $this->view->qlink(
+ $serviceLinkText,
+ 'monitoring/service/show',
+ array('host' => $host, 'service' => $service),
+ array(
+ 'title' => sprintf(
+ $this->view->translate('Show detailed information for service %s on host %s'),
+ $serviceLinkText,
+ $hostLinkText
+ ),
+ 'class' => $class
+ )
+ )
+ );
+ }
+}
diff --git a/modules/monitoring/application/views/helpers/MonitoringFlags.php b/modules/monitoring/application/views/helpers/MonitoringFlags.php
new file mode 100644
index 0000000..354dc94
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/MonitoringFlags.php
@@ -0,0 +1,40 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+
+/**
+ * Rendering helper for object's properties which may be either enabled or disabled
+ */
+class Zend_View_Helper_MonitoringFlags extends Zend_View_Helper_Abstract
+{
+ /**
+ * Object's properties which may be either enabled or disabled and their human readable description
+ *
+ * @var string[]
+ */
+ private static $flags = array(
+ 'passive_checks_enabled' => 'Passive Checks',
+ 'active_checks_enabled' => 'Active Checks',
+ 'obsessing' => 'Obsessing',
+ 'notifications_enabled' => 'Notifications',
+ 'event_handler_enabled' => 'Event Handler',
+ 'flap_detection_enabled' => 'Flap Detection',
+ );
+
+ /**
+ * Retrieve flags as array with either true or false as value
+ *
+ * @param MonitoredObject $object
+ *
+ * @return array
+ */
+ public function monitoringFlags(/*MonitoredObject*/ $object)
+ {
+ $flags = array();
+ foreach (self::$flags as $column => $description) {
+ $flags[$description] = (bool) $object->{$column};
+ }
+ return $flags;
+ }
+}
diff --git a/modules/monitoring/application/views/helpers/Perfdata.php b/modules/monitoring/application/views/helpers/Perfdata.php
new file mode 100644
index 0000000..82289e2
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/Perfdata.php
@@ -0,0 +1,120 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+use Icinga\Module\Monitoring\Plugin\Perfdata;
+use Icinga\Module\Monitoring\Plugin\PerfdataSet;
+use Icinga\Util\StringHelper;
+use Icinga\Web\View;
+
+class Zend_View_Helper_Perfdata extends Zend_View_Helper_Abstract
+{
+ /** @var View */
+ public $view;
+
+ /**
+ * Display the given perfdata string to the user
+ *
+ * @param string $perfdataStr The perfdata string
+ * @param bool $compact Whether to display the perfdata in compact mode
+ * @param int $limit Max labels to show; 0 for no limit
+ * @param string $color The color indicating the perfdata state
+ *
+ * @return string
+ */
+ public function perfdata($perfdataStr, $compact = false, $limit = 0, $color = Perfdata::PERFDATA_OK)
+ {
+ $pieChartData = PerfdataSet::fromString($perfdataStr)->asArray();
+ uasort(
+ $pieChartData,
+ function ($a, $b) {
+ return $a->worseThan($b) ? -1 : ($b->worseThan($a) ? 1 : 0);
+ }
+ );
+ $results = array();
+ $keys = array('', 'label', 'value', 'min', 'max', 'warn', 'crit');
+ $columns = array();
+ $labels = array_combine(
+ $keys,
+ array(
+ '',
+ $this->view->translate('Label'),
+ $this->view->translate('Value'),
+ $this->view->translate('Min'),
+ $this->view->translate('Max'),
+ $this->view->translate('Warning'),
+ $this->view->translate('Critical')
+ )
+ );
+ foreach ($pieChartData as $perfdata) {
+ if ($perfdata->isVisualizable()) {
+ $columns[''] = '';
+ }
+ foreach ($perfdata->toArray() as $column => $value) {
+ if (empty($value) ||
+ $column === 'min' && floatval($value) === 0.0 ||
+ $column === 'max' && $perfdata->isPercentage() && floatval($value) === 100) {
+ continue;
+ }
+ $columns[$column] = $labels[$column];
+ }
+ }
+ // restore original column array sorting
+ $headers = array();
+ foreach ($keys as $column) {
+ if (isset($columns[$column])) {
+ $headers[$column] = $labels[$column];
+ }
+ }
+ $table = array('<thead><tr><th>' . implode('</th><th>', $headers) . '</th></tr></thead><tbody>');
+ foreach ($pieChartData as $perfdata) {
+ if ($compact && $perfdata->isVisualizable()) {
+ $results[] = $perfdata->asInlinePie($color)->render();
+ } else {
+ $data = array();
+ if ($perfdata->isVisualizable()) {
+ $data []= $perfdata->asInlinePie($color)->render();
+ } elseif (isset($columns[''])) {
+ $data []= '';
+ }
+ if (! $compact) {
+ foreach ($perfdata->toArray() as $column => $value) {
+ if (! isset($columns[$column])) {
+ continue;
+ }
+ $text = $this->view->escape(empty($value) ? '-' : $value);
+ $data []= sprintf(
+ '<span title="%s">%s</span>',
+ $text,
+ $text
+ );
+ }
+ }
+ $table []= '<tr><td class="sparkline-col">' . implode('</td><td>', $data) . '</td></tr>';
+ }
+ }
+ $table[] = '</tbody>';
+ if ($limit > 0) {
+ $count = $compact ? count($results) : count($table);
+ if ($count > $limit) {
+ if ($compact) {
+ $results = array_slice($results, 0, $limit);
+ $title = sprintf($this->view->translate('%d more ...'), $count - $limit);
+ $results[] = '<span aria-hidden="true" title="' . $title . '">...</span>';
+ } else {
+ $table = array_slice($table, 0, $limit);
+ }
+ }
+ }
+ if ($compact) {
+ return join('', $results);
+ } else {
+ if (empty($table)) {
+ return '';
+ }
+ return sprintf(
+ '<table class="performance-data-table collapsible" data-visible-rows="6">%s</table>',
+ implode("\n", $table)
+ );
+ }
+ }
+}
diff --git a/modules/monitoring/application/views/helpers/PluginOutput.php b/modules/monitoring/application/views/helpers/PluginOutput.php
new file mode 100644
index 0000000..fba83d5
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/PluginOutput.php
@@ -0,0 +1,199 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+use Icinga\Web\Dom\DomNodeIterator;
+use Icinga\Web\View;
+use Icinga\Web\Helper\HtmlPurifier;
+
+/**
+ * Plugin output renderer
+ */
+class Zend_View_Helper_PluginOutput extends Zend_View_Helper_Abstract
+{
+ /**
+ * Patterns to be replaced in plain text plugin output
+ *
+ * @var array
+ */
+ protected static $txtPatterns = array(
+ '~\\\t~',
+ '~\\\n~',
+ '~(\[|\()OK(\]|\))~',
+ '~(\[|\()WARNING(\]|\))~',
+ '~(\[|\()CRITICAL(\]|\))~',
+ '~(\[|\()UNKNOWN(\]|\))~',
+ '~(\[|\()UP(\]|\))~',
+ '~(\[|\()DOWN(\]|\))~',
+ '~\@{6,}~'
+ );
+
+ /**
+ * Replacements for $txtPatterns
+ *
+ * @var array
+ */
+ protected static $txtReplacements = array(
+ "\t",
+ "\n",
+ '<span class="state-ok">$1OK$2</span>',
+ '<span class="state-warning">$1WARNING$2</span>',
+ '<span class="state-critical">$1CRITICAL$2</span>',
+ '<span class="state-unknown">$1UNKNOWN$2</span>',
+ '<span class="state-up">$1UP$2</span>',
+ '<span class="state-down">$1DOWN$2</span>',
+ '@@@@@@',
+ );
+
+ /**
+ * Patterns to be replaced in html plugin output
+ *
+ * @var array
+ */
+ protected static $htmlPatterns = array(
+ '~\\\t~',
+ '~\\\n~',
+ '~<table~'
+ );
+
+ /**
+ * Replacements for $htmlPatterns
+ *
+ * @var array
+ */
+ protected static $htmlReplacements = array(
+ "\t",
+ "\n",
+ '<table class="output-table"'
+ );
+
+ /** @var \Icinga\Module\Monitoring\Web\Helper\PluginOutputHookRenderer */
+ protected $hookRenderer;
+
+ public function __construct()
+ {
+ $this->hookRenderer = (new \Icinga\Module\Monitoring\Web\Helper\PluginOutputHookRenderer())->registerHooks();
+ }
+
+ /**
+ * Render plugin output
+ *
+ * @param string $output
+ * @param bool $raw
+ * @param string $command Check command
+ *
+ * @return string
+ */
+ public function pluginOutput($output, $raw = false, $command = null)
+ {
+ if (empty($output)) {
+ return '';
+ }
+
+ if ($command !== null) {
+ $output = $this->hookRenderer->render($command, $output, ! $raw);
+ }
+
+ if (preg_match('~<\w+(?>\s\w+=[^>]*)?>~', $output)) {
+ // HTML
+ $output = HtmlPurifier::process(preg_replace(
+ self::$htmlPatterns,
+ self::$htmlReplacements,
+ $output
+ ));
+ $isHtml = true;
+ } else {
+ // Plaintext
+ $output = preg_replace(
+ self::$txtPatterns,
+ self::$txtReplacements,
+ // Not using the view here to escape this. The view sets `double_encode` to true
+ htmlspecialchars($output, ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, View::CHARSET, false)
+ );
+ $isHtml = false;
+ }
+
+ $output = trim($output);
+ // Add zero-width space after commas which are not followed by a whitespace character
+ // in oder to help browsers to break words in plugin output
+ $output = preg_replace('/,(?=[^\s])/', ',&#8203;', $output);
+ if (! $raw) {
+ if ($isHtml) {
+ $output = $this->processHtml($output);
+ $output = '<div class="plugin-output">' . $output . '</div>';
+ } else {
+ $output = '<div class="plugin-output preformatted">' . $output . '</div>';
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * Replace classic Icinga CGI links with Icinga Web 2 links and color state information, if any
+ *
+ * @param string $html
+ *
+ * @return string
+ */
+ protected function processHtml($html)
+ {
+ $pattern = '/[([](OK|WARNING|CRITICAL|UNKNOWN|UP|DOWN)[)\]]/';
+ $doc = new DOMDocument();
+ $doc->loadXML('<div>' . $html . '</div>', LIBXML_NOERROR | LIBXML_NOWARNING);
+ $dom = new RecursiveIteratorIterator(new DomNodeIterator($doc), RecursiveIteratorIterator::SELF_FIRST);
+ $nodesToRemove = array();
+ foreach ($dom as $node) {
+ /** @var \DOMNode $node */
+ if ($node->nodeType === XML_TEXT_NODE) {
+ $start = 0;
+ while (preg_match($pattern, $node->nodeValue, $match, PREG_OFFSET_CAPTURE, $start)) {
+ $offsetLeft = $match[0][1];
+ $matchLength = strlen($match[0][0]);
+ $leftLength = $offsetLeft - $start;
+ // if there is text before the match
+ if ($leftLength) {
+ // create node for leading text
+ $text = new DOMText(substr($node->nodeValue, $start, $leftLength));
+ $node->parentNode->insertBefore($text, $node);
+ }
+ // create the new element for the match
+ $span = $doc->createElement('span', $match[0][0]);
+ $span->setAttribute('class', 'state-' . strtolower($match[1][0]));
+ $node->parentNode->insertBefore($span, $node);
+
+ // start for next match
+ $start = $offsetLeft + $matchLength;
+ }
+ if ($start) {
+ // is there text left?
+ if (strlen($node->nodeValue) > $start) {
+ // create node for trailing text
+ $text = new DOMText(substr($node->nodeValue, $start));
+ $node->parentNode->insertBefore($text, $node);
+ }
+ // delete the old node later
+ $nodesToRemove[] = $node;
+ }
+ } elseif ($node->nodeType === XML_ELEMENT_NODE) {
+ /** @var \DOMElement $node */
+ if ($node->tagName === 'a'
+ && preg_match('~^/cgi\-bin/status\.cgi\?(.+)$~', $node->getAttribute('href'), $match)
+ ) {
+ parse_str($match[1], $params);
+ if (isset($params['host'])) {
+ $node->setAttribute(
+ 'href',
+ $this->view->baseUrl('/monitoring/host/show?host=' . urlencode($params['host']))
+ );
+ }
+ }
+ }
+ }
+ foreach ($nodesToRemove as $node) {
+ /** @var \DOMNode $node */
+ $node->parentNode->removeChild($node);
+ }
+
+ return substr($doc->saveHTML(), 5, -7);
+ }
+}
diff --git a/modules/monitoring/application/views/helpers/RuntimeVariables.php b/modules/monitoring/application/views/helpers/RuntimeVariables.php
new file mode 100644
index 0000000..e80e8aa
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/RuntimeVariables.php
@@ -0,0 +1,50 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+/**
+ * Convert runtime summary data into a simple usable stdClass
+ */
+class Zend_View_Helper_RuntimeVariables extends Zend_View_Helper_Abstract
+{
+ /**
+ * Create dispatch instance
+ *
+ * @return $this
+ */
+ public function runtimeVariables()
+ {
+ return $this;
+ }
+
+ /**
+ * Create a condensed row of object data
+ *
+ * @param $result stdClass
+ *
+ * @return stdClass Condensed row
+ */
+ public function create(stdClass $result)
+ {
+ $out = new stdClass();
+ $out->total_hosts = isset($result->total_hosts)
+ ? $result->total_hosts
+ : 0;
+ $out->total_scheduled_hosts = isset($result->total_scheduled_hosts)
+ ? $result->total_scheduled_hosts
+ : 0;
+ $out->total_services = isset($result->total_services)
+ ? $result->total_services
+ : 0;
+ $out->total_scheduled_services = isset($result->total_scheduled_services)
+ ? $result->total_scheduled_services
+ : 0;
+ $out->average_services_per_host = $out->total_hosts > 0
+ ? $out->total_services / $out->total_hosts
+ : 0;
+ $out->average_scheduled_services_per_host = $out->total_scheduled_hosts > 0
+ ? $out->total_scheduled_services / $out->total_scheduled_hosts
+ : 0;
+
+ return $out;
+ }
+}
diff --git a/modules/monitoring/application/views/helpers/ServiceFlags.php b/modules/monitoring/application/views/helpers/ServiceFlags.php
new file mode 100644
index 0000000..8fde7bd
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/ServiceFlags.php
@@ -0,0 +1,38 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+use Icinga\Web\View;
+
+class Zend_View_Helper_ServiceFlags extends Zend_View_Helper_Abstract
+{
+ /** @var View */
+ public $view;
+
+ public function serviceFlags($service)
+ {
+ $icons = array();
+ if (! $service->service_handled && $service->service_state > 0) {
+ $icons[] = $this->view->icon('attention-alt', $this->view->translate('Unhandled'));
+ }
+ if ($service->service_acknowledged) {
+ $icons[] = $this->view->icon('ok', $this->view->translate('Acknowledged'));
+ }
+ if ($service->service_is_flapping) {
+ $icons[] = $this->view->icon('flapping', $this->view->translate('Flapping'));
+ }
+ if (! $service->service_notifications_enabled) {
+ $icons[] = $this->view->icon('bell-off-empty', $this->view->translate('Notifications Disabled'));
+ }
+ if ($service->service_in_downtime) {
+ $icons[] = $this->view->icon('plug', $this->view->translate('In Downtime'));
+ }
+ if (! $service->service_active_checks_enabled) {
+ if (! $service->service_passive_checks_enabled) {
+ $icons[] = $this->view->icon('eye-off', $this->view->translate('Active And Passive Checks Disabled'));
+ } else {
+ $icons[] = $this->view->icon('eye-off', $this->view->translate('Active Checks Disabled'));
+ }
+ }
+ return implode(' ', $icons);
+ }
+}
diff --git a/modules/monitoring/application/views/scripts/comment/remove.phtml b/modules/monitoring/application/views/scripts/comment/remove.phtml
new file mode 100644
index 0000000..73f8c68
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/comment/remove.phtml
@@ -0,0 +1,11 @@
+<div class="controls">
+
+ <?php if (! $this->compact): ?>
+ <?= $this->tabs; ?>
+ <?php endif ?>
+
+ <?= $this->render('partials/downtime/downtime-header.phtml'); ?>
+</div>
+<div class="content object-command">
+ <?= $delDowntimeForm; ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/comment/show.phtml b/modules/monitoring/application/views/scripts/comment/show.phtml
new file mode 100644
index 0000000..3cbfb76
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/comment/show.phtml
@@ -0,0 +1,86 @@
+<div class="controls">
+ <?php if (! $this->compact): ?>
+ <?= $this->tabs; ?>
+ <?php endif ?>
+
+ <div data-base-target='_next'>
+ <?= $this->render('partials/comment/comment-header.phtml'); ?>
+ </div>
+</div>
+<div class="content">
+
+<h2><?= $this->translate('Comment detail information') ?></h2>
+<table class="name-value-table">
+ <tbody>
+ <tr>
+ <?php if ($this->comment->objecttype === 'service'): ?>
+ <th> <?= $this->translate('Service') ?> </th>
+ <td>
+ <?= $this->icon('service', $this->translate('Service')); ?>
+ <?= $this->link()->service(
+ $this->comment->service_description,
+ $this->comment->service_display_name,
+ $this->comment->host_name,
+ $this->comment->host_display_name
+ );
+ ?>
+ </td>
+ <?php else: ?>
+ <th> <?= $this->translate('Host') ?> </th>
+ <td>
+ <?= $this->icon('host', $this->translate('Host')); ?>
+ <?= $this->link()->host(
+ $this->comment->host_name,
+ $this->comment->host_display_name
+ );
+ ?>
+ </td>
+ <?php endif ?>
+ </tr>
+
+ <tr>
+ <th><?= $this->translate('Author') ?></th>
+ <td><?= $this->icon('user', $this->translate('User')) ?> <?= $this->escape($this->comment->author) ?></td>
+ </tr>
+
+ <tr>
+ <th><?= $this->translate('Persistent') ?></th>
+ <td><?= $this->escape($this->comment->persistent) ? $this->translate('Yes') : $this->translate('No') ?></td>
+ </tr>
+
+ <tr>
+ <th><?= $this->translate('Created') ?></th>
+ <td><?= $this->formatDateTime($this->comment->timestamp) ?></td>
+ </tr>
+
+ <tr>
+ <th><?= $this->translate('Expires') ?></th>
+ <td>
+ <?= $this->comment->expiration ? sprintf(
+ $this->translate('This comment expires on %s at %s.'),
+ $this->formatDate($this->comment->expiration),
+ $this->formatTime($this->comment->expiration)
+ ) : $this->translate('This comment does not expire.');
+ ?>
+ </td>
+ </tr>
+
+ <tr>
+ <th><?= $this->translate('Comment') ?></th>
+ <td><?= $this->nl2br($this->createTicketLinks($this->markdown($comment->comment))) ?></td>
+ </tr>
+
+ <?php if (isset($delCommentForm)): // Form is unset if the current user lacks the respective permission ?>
+ <tr class="newsection">
+ <th><?= $this->translate('Commands') ?></th>
+ <td>
+ <?= $delCommentForm ?>
+ </td>
+ </tr>
+ <?php endif ?>
+
+ </tbody>
+</table>
+
+</div>
+
diff --git a/modules/monitoring/application/views/scripts/comments/delete-all.phtml b/modules/monitoring/application/views/scripts/comments/delete-all.phtml
new file mode 100644
index 0000000..698c4ee
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/comments/delete-all.phtml
@@ -0,0 +1,12 @@
+<div class="controls">
+
+ <?php if (! $this->compact): ?>
+ <?= $this->tabs; ?>
+ <?php endif ?>
+
+ <?= $this->render('partials/comment/comments-header.phtml'); ?>
+</div>
+
+<div class="content object-command">
+ <?= $delCommentForm ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/comments/show.phtml b/modules/monitoring/application/views/scripts/comments/show.phtml
new file mode 100644
index 0000000..67e1c6b
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/comments/show.phtml
@@ -0,0 +1,19 @@
+<div class="controls">
+<?php if (! $this->compact): ?>
+ <?= $this->tabs ?>
+<?php endif ?>
+ <?= $this->render('partials/comment/comments-header.phtml') ?>
+</div>
+
+<div class="content multi-commands">
+ <h2><?= $this->translate('Commands') ?></h2>
+ <?= $this->qlink(
+ sprintf($this->translate('Remove %d comments'), $comments->count()),
+ $removeAllLink,
+ null,
+ array(
+ 'icon' => 'trash',
+ 'title' => $this->translate('Remove all selected comments')
+ )
+ ) ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/config/form.phtml b/modules/monitoring/application/views/scripts/config/form.phtml
new file mode 100644
index 0000000..cbf0659
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/config/form.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= $tabs->showOnlyCloseButton(); ?>
+</div>
+<div class="content">
+ <?= $form; ?>
+</div> \ No newline at end of file
diff --git a/modules/monitoring/application/views/scripts/config/index.phtml b/modules/monitoring/application/views/scripts/config/index.phtml
new file mode 100644
index 0000000..a1264c2
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/config/index.phtml
@@ -0,0 +1,78 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+
+<div class="content" data-base-target="_next">
+ <div>
+ <h2><?= $this->translate('Monitoring Backends') ?></h2>
+ <?= $this->qlink(
+ $this->translate('Create a New Monitoring Backend') ,
+ 'monitoring/config/createbackend',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new monitoring backend')
+ )
+ ) ?>
+ <table class="table-row-selectable common-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Monitoring Backend') ?></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($this->backendsConfig as $backendName => $config): ?>
+ <tr>
+ <td>
+ <?= $this->qlink(
+ $backendName,
+ 'monitoring/config/editbackend',
+ array('backend-name' => $backendName),
+ array(
+ 'icon' => 'edit',
+ 'title' => sprintf($this->translate('Edit monitoring backend %s'), $backendName)
+ )
+ ) ?>
+ <span class="config-label-meta">&#40;<?= sprintf(
+ $this->translate('Type: %s'),
+ $this->escape($config->type === 'ido' ? 'IDO' : ucfirst($config->type))
+ ) ?>&#41;
+ </span>
+ </td>
+ <td class="text-right">
+ <?= $this->qlink(
+ '',
+ 'monitoring/config/removebackend',
+ array('backend-name' => $backendName),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove monitoring backend %s'), $backendName)
+ )
+ ) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+ </div>
+ <div>
+ <h2><?= $this->translate('Command Transports') ?></h2>
+ <?= $this->qlink(
+ $this->translate('Create a New Command Transport') ,
+ 'monitoring/config/createtransport',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new command transport')
+ )
+ ) ?>
+ <?php
+ /** @var \Icinga\Module\Monitoring\Forms\Config\TransportReorderForm $commandTransportReorderForm */
+ echo $commandTransportReorderForm;
+ ?>
+ </div>
+</div>
diff --git a/modules/monitoring/application/views/scripts/config/security.phtml b/modules/monitoring/application/views/scripts/config/security.phtml
new file mode 100644
index 0000000..3801678
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/config/security.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= $tabs; ?>
+</div>
+<div class="content">
+ <?= $form; ?>
+</div> \ No newline at end of file
diff --git a/modules/monitoring/application/views/scripts/downtime/remove.phtml b/modules/monitoring/application/views/scripts/downtime/remove.phtml
new file mode 100644
index 0000000..34a7dbd
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/downtime/remove.phtml
@@ -0,0 +1,13 @@
+<div class="controls">
+
+ <?php if (! $this->compact): ?>
+ <?= $this->tabs; ?>
+ <?php endif ?>
+
+ <table>
+ <tr> <?= $this->render('partials/downtime/downtime-header.phtml') ?> </tr>
+ </table>
+</div>
+<div class="content object-command">
+ <?= $delDowntimeForm; ?>
+</div> \ No newline at end of file
diff --git a/modules/monitoring/application/views/scripts/downtime/show.phtml b/modules/monitoring/application/views/scripts/downtime/show.phtml
new file mode 100644
index 0000000..4db03cb
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/downtime/show.phtml
@@ -0,0 +1,173 @@
+<div class="controls">
+ <?php if (! $this->compact): ?>
+ <?= $this->tabs; ?>
+ <?php endif ?>
+
+ <table>
+ <tr> <?= $this->render('partials/downtime/downtime-header.phtml'); ?> </tr>
+ </table>
+</div>
+<div class="content"><h2><?= $this->translate('Details') ?></h2>
+ <table class="name-value-table">
+ <tbody>
+ <tr>
+ <th>
+ <?= $this->isService ? $this->translate('Service') : $this->translate('Host') ?>
+ </th>
+ <td data-base-target="_next">
+ <?php if ($this->isService): ?>
+ <?php
+ $link = $this->link()->service(
+ $downtime->service_description,
+ $downtime->service_display_name,
+ $downtime->host_name,
+ $downtime->host_display_name
+ );
+ $icon = $this->icon('service', $this->translate('Service'));
+ ?>
+ <?php else: ?>
+ <?php
+ $icon = $this->icon('host', $this->translate('Host'));
+ $link = $this->link()->host($downtime->host_name, $downtime->host_display_name)
+ ?>
+ <?php endif ?>
+ <?= $icon ?>
+ <?= $link ?>
+ </td>
+ </tr>
+ <tr title="<?= $this->translate('The name of the person who scheduled this downtime'); ?>">
+ <th><?= $this->translate('Author') ?></th>
+ <td><?= $this->icon('user', $this->translate('User')) ?> <?= $this->escape($this->downtime->author_name) ?></td>
+ </tr>
+ <tr title="<?= $this->translate('Date and time this downtime was entered'); ?>">
+ <th><?= $this->translate('Entry Time') ?></th>
+ <td><?= $this->formatDateTime($this->downtime->entry_time) ?></td>
+ </tr>
+ <tr title="<?= $this->translate('A comment, as entered by the author, associated with the scheduled downtime'); ?>">
+ <th><?= $this->translate('Comment') ?></th>
+ <td><?= $this->nl2br($this->createTicketLinks($this->markdown($downtime->comment))) ?></td>
+ </tr>
+ </tbody>
+ </table>
+
+ <h2> <?= $this->translate('Duration') ?> </h2>
+
+ <table class="name-value-table">
+ <tbody>
+ <tr class="newsection">
+ <th><?= $this->escape(
+ $this->downtime->is_flexible ?
+ $this->translate('Flexible') : $this->translate('Fixed')
+ ); ?>
+ <?= $this->icon('info-circled', $this->downtime->is_flexible ?
+ $this->translate('Flexible downtimes have a hard start and end time,'
+ . ' but also an additional restriction on the duration in which '
+ . ' the host or service may actually be down.') :
+ $this->translate('Fixed downtimes have a static start and end time.')) ?>
+ </th>
+ <td>
+ <?php if ($downtime->is_flexible): ?>
+ <?php if ($downtime->is_in_effect): ?>
+ <?= sprintf(
+ $isService
+ ? $this->translate('This flexible service downtime was started on %s at %s and lasts for %s until %s at %s.')
+ : $this->translate('This flexible host downtime was started on %s at %s and lasts for %s until %s at %s.'),
+ $this->formatDate($downtime->start),
+ $this->formatTime($downtime->start),
+ $this->formatDuration($downtime->duration),
+ $this->formatDate($downtime->end),
+ $this->formatTime($downtime->end)
+ ) ?>
+ <?php else: ?>
+ <?= sprintf(
+ $isService
+ ? $this->translate('This flexible service downtime has been scheduled to start between %s - %s and to last for %s.')
+ : $this->translate('This flexible host downtime has been scheduled to start between %s - %s and to last for %s.'),
+ $this->formatDateTime($downtime->scheduled_start),
+ $this->formatDateTime($downtime->scheduled_end),
+ $this->formatDuration($downtime->duration)
+ ) ?>
+ <?php endif ?>
+ <?php else: ?>
+ <?php if ($downtime->is_in_effect): ?>
+ <?= sprintf(
+ $isService
+ ? $this->translate('This fixed service downtime was started on %s at %s and expires on %s at %s.')
+ : $this->translate('This fixed host downtime was started on %s at %s and expires on %s at %s.'),
+ $this->formatDate($downtime->start),
+ $this->formatTime($downtime->start),
+ $this->formatDate($downtime->end),
+ $this->formatTime($downtime->end)
+ ) ?>
+ <?php else: ?>
+ <?= sprintf(
+ $isService
+ ? $this->translate('This fixed service downtime has been scheduled to start on %s at %s and to end on %s at %s.')
+ : $this->translate('This fixed host downtime has been scheduled to start on %s at %s and to end on %s at %s.'),
+ $this->formatDate($downtime->start),
+ $this->formatTime($downtime->start),
+ $this->formatDate($downtime->end),
+ $this->formatTime($downtime->end)
+ ) ?>
+ <?php endif ?>
+ <?php endif ?>
+ </td>
+ </tr>
+ <tr title="<?= $this->translate('The date/time the scheduled downtime is'
+ . ' supposed to start. If this is a flexible (non-fixed) downtime, '
+ . 'this refers to the earliest possible time that the downtime'
+ . ' can start'); ?>">
+ <th><?= $this->translate('Scheduled start') ?></th>
+ <td><?= $this->formatDateTime($this->downtime->scheduled_start) ?></td>
+ </tr>
+ <tr title="<?= $this->translate('The date/time the scheduled downtime is '
+ . 'supposed to end. If this is a flexible (non-fixed) downtime, '
+ . 'this refers to the last possible time that the downtime can '
+ . 'start'); ?>">
+ <th><?= $this->translate('Scheduled end') ?></th>
+ <td><?= $this->formatDateTime($this->downtime->scheduled_end) ?></td>
+ </tr>
+ <?php if ($this->downtime->is_flexible): ?>
+ <tr title="<?= $this->translate('Indicates the number of seconds that the '
+ . 'scheduled downtime should last. This is usually only needed if'
+ . ' this is a flexible downtime, which can start at a variable '
+ . 'time, but lasts for the specified duration'); ?>">
+ <th tit><?= $this->translate('Duration') ?></th>
+ <td><?= $this->formatDuration($this->downtime->duration) ?></td>
+ </tr>
+ <tr title="<?= $this->translate('he date/time the scheduled downtime was'
+ . ' actually started'); ?>">
+ <th><?= $this->translate('Actual start time') ?></th>
+ <td><?= $this->formatDateTime($downtime->start) ?></td>
+ </tr>
+ <tr title="<?= $this->translate('The date/time the scheduled downtime '
+ . 'actually ended'); ?>">
+ <th><?= $this->translate('Actual end time') ?></th>
+ <td><?= $this->formatDateTime($downtime->end) ?></td>
+ </tr>
+ <?php endif; ?>
+
+ <tr class="newsection">
+ <th><?= $this->translate('In effect') ?></th>
+ <td>
+ <?= $this->escape(
+ $this->downtime->is_in_effect ?
+ $this->translate('Yes') : $this->translate('No')
+ );
+ ?>
+ </td>
+ </tr>
+
+ <?php if (isset($delDowntimeForm)): // Form is unset if the current user lacks the respective permission ?>
+ <tr class="newsection">
+ <th><?= $this->translate('Commands') ?></th>
+ <td>
+ <?= $delDowntimeForm ?>
+ </td>
+ </tr>
+ <?php endif ?>
+ </tbody>
+ </table>
+
+</div>
+
diff --git a/modules/monitoring/application/views/scripts/downtimes/delete-all.phtml b/modules/monitoring/application/views/scripts/downtimes/delete-all.phtml
new file mode 100644
index 0000000..e6435fe
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/downtimes/delete-all.phtml
@@ -0,0 +1,12 @@
+<div class="controls">
+
+ <?php if (! $this->compact): ?>
+ <?= $this->tabs; ?>
+ <?php endif ?>
+
+ <?= $this->render('partials/downtime/downtimes-header.phtml'); ?>
+</div>
+
+<div class="content object-command">
+ <?= $delAllDowntimeForm ?>
+</div> \ No newline at end of file
diff --git a/modules/monitoring/application/views/scripts/downtimes/show.phtml b/modules/monitoring/application/views/scripts/downtimes/show.phtml
new file mode 100644
index 0000000..73d9bf6
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/downtimes/show.phtml
@@ -0,0 +1,19 @@
+<div class="controls">
+<?php if (! $this->compact): ?>
+ <?= $this->tabs ?>
+<?php endif ?>
+ <?= $this->render('partials/downtime/downtimes-header.phtml') ?>
+</div>
+
+<div class="content multi-commands">
+ <h2> <?= $this->translate('Commands') ?> </h2>
+ <?= $this->qlink(
+ sprintf($this->translate('Remove all %d scheduled downtimes'), $downtimes->count()),
+ $removeAllLink,
+ null,
+ array(
+ 'icon' => 'trash',
+ 'title' => $this->translate('Remove all selected downtimes')
+ )
+ ) ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/event/show.phtml b/modules/monitoring/application/views/scripts/event/show.phtml
new file mode 100644
index 0000000..c844a6f
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/event/show.phtml
@@ -0,0 +1,34 @@
+<?php
+use Icinga\Module\Monitoring\Object\Service;
+
+/** @var string[][] $details */
+/** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+/** @var \Icinga\Web\View $this */
+?>
+<div class="controls">
+<?php
+if (! $this->compact) {
+ echo $this->tabs;
+}
+
+echo $object instanceof Service
+ ? '<h2>' . $this->translate('Current Service State') . '</h2>' . $this->render('partials/object/service-header.phtml')
+ : '<h2>' . $this->translate('Current Host State') . '</h2>' . $this->render('partials/object/host-header.phtml');
+?>
+</div>
+<div class="content">
+ <?php
+ foreach ($extensionsHtml as $extensionHtml) {
+ echo $extensionHtml;
+ }
+ ?>
+
+ <h2><?= $this->escape($this->translate('Event Details')) ?></h2>
+ <table class="event-details name-value-table" data-base-target="_next">
+ <?php
+ foreach ($details as $detail) {
+ echo '<tr><th>' . $this->escape($detail[0]) . '</th><td>' . $detail[1] . '</td></tr>';
+ }
+ ?>
+ </table>
+</div>
diff --git a/modules/monitoring/application/views/scripts/form/reorder-command-transports.phtml b/modules/monitoring/application/views/scripts/form/reorder-command-transports.phtml
new file mode 100644
index 0000000..2f81610
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/form/reorder-command-transports.phtml
@@ -0,0 +1,93 @@
+<?php
+/** @var \Icinga\Web\View $this */
+/** @var \Icinga\Module\Monitoring\Forms\Config\TransportReorderForm $form */
+?>
+<form id="<?=
+$this->escape($form->getId())
+?>" name="<?=
+$this->escape($form->getName())
+?>" enctype="<?=
+$this->escape($form->getEncType())
+?>" method="<?=
+$this->escape($form->getMethod())
+?>" action="<?=
+$this->escape($form->getAction())
+?>">
+ <table class="table-row-selectable common-table" data-base-target="_next">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Transport') ?></th>
+ <th></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php
+ $i = -1;
+ $transportConfig = $form->getConfig();
+ $total = $transportConfig->count();
+ foreach ($transportConfig as $transportName => $config):
+ ++$i;
+ ?>
+ <tr>
+ <td>
+ <?= $this->qlink(
+ $transportName,
+ 'monitoring/config/edittransport',
+ array('transport' => $transportName),
+ array(
+ 'icon' => 'edit',
+ 'title' => sprintf($this->translate('Edit command transport %s'), $transportName)
+ )
+ ); ?>
+ <span class="config-label-meta">&#40;<?= sprintf(
+ $this->translate('Type: %s'),
+ ucfirst($config->get('transport', 'local'))
+ ) ?>&#41;
+ </span>
+ </td>
+ <td class="text-right">
+ <?= $this->qlink(
+ '',
+ 'monitoring/config/removetransport',
+ array('transport' => $transportName),
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'cancel',
+ 'title' => sprintf($this->translate('Remove command transport %s'), $transportName)
+ )
+ ); ?>
+ </td>
+ <td class="icon-col text-right" data-base-target="_self">
+ <?php if ($i > 0): ?>
+ <button type="submit" name="transport_newpos" class="link-button icon-only animated move-up" value="<?= $this->escape(
+ ($i - 1) . '|' . $transportName
+ ) ?>" title="<?= $this->translate(
+ 'Move up in order'
+ ) ?>" aria-label="<?= $this->escape(sprintf(
+ $this->translate('Move command transport %s upwards'),
+ $transportName
+ )) ?>"><?=
+ $this->icon('up-small')
+ ?></button>
+ <?php endif ?>
+ <?php if ($i + 1 < $total): ?>
+ <button type="submit" name="transport_newpos" class="link-button icon-only animated move-down" value="<?= $this->escape(
+ ($i + 1) . '|' . $transportName
+ ) ?>" title="<?= $this->translate(
+ 'Move down in order'
+ ) ?>" aria-label="<?= $this->escape(sprintf(
+ $this->translate('Move command transport %s downwards'),
+ $transportName
+ )) ?>"><?=
+ $this->icon('down-small')
+ ?></button>
+ <?php endif ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+ <?= $form->getElement($form->getTokenElementName()) ?>
+ <?= $form->getElement($form->getUidElementName()) ?>
+</form>
diff --git a/modules/monitoring/application/views/scripts/health/disable-notifications.phtml b/modules/monitoring/application/views/scripts/health/disable-notifications.phtml
new file mode 100644
index 0000000..e8c75e5
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/health/disable-notifications.phtml
@@ -0,0 +1,20 @@
+<?php if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs->showOnlyCloseButton(); ?>
+</div>
+<?php endif ?>
+<div class="content">
+ <h1><?= $title; ?></h1>
+ <?php if ((bool) $programStatus->notifications_enabled === false): ?>
+ <div>
+ <?= $this->translate('Host and service notifications are already disabled.') ?>
+ <?php if ($this->programStatus->disable_notif_expire_time): ?>
+ <?= sprintf(
+ $this->translate('Notifications will be re-enabled in <strong>%s</strong>.'),
+ $this->timeUntil($this->programStatus->disable_notif_expire_time)); ?>
+ <?php endif; ?>
+ </div>
+ <?php else: ?>
+ <?= $form; ?>
+ <?php endif ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/health/info.phtml b/modules/monitoring/application/views/scripts/health/info.phtml
new file mode 100644
index 0000000..76d9ee3
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/health/info.phtml
@@ -0,0 +1,87 @@
+<?php
+$rv = $this->runtimeVariables()->create($this->runtimevariables);
+$cp = $this->checkPerformance()->create($this->checkperformance);
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs; ?>
+</div>
+<?php endif ?>
+
+<div class="content processinfo">
+ <div class="boxview">
+ <div class="box process">
+ <h2 tabindex="0"><?= $this->translate('Process Info') ?></h2>
+ <table class="name-value-table">
+ <tbody>
+ <tr>
+ <th><?= $this->translate('Program Version') ?></th>
+ <td><?= $this->programStatus->program_version
+ ? $this->programStatus->program_version
+ : $this->translate('N/A') ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Program Start Time') ?></th>
+ <td><?= $this->formatDateTime($this->programStatus->program_start_time) ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Last Status Update'); ?></th>
+ <td><?= $this->timeAgo($this->programStatus->status_update_time); ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Last External Command Check'); ?></th>
+ <td><?= $this->timeAgo($this->programStatus->last_command_check); ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Last Log File Rotation'); ?></th>
+ <td><?= $this->programStatus->last_log_rotation
+ ? $this->timeSince($this->programStatus->last_log_rotation)
+ : $this->translate('N/A') ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Global Service Event Handler'); ?></th>
+ <td><?= $this->programStatus->global_service_event_handler
+ ? $this->programStatus->global_service_event_handler
+ : $this->translate('N/A'); ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Global Host Event Handler'); ?></th>
+ <td><?= $this->programStatus->global_host_event_handler
+ ? $this->programStatus->global_host_event_handler
+ : $this->translate('N/A'); ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Active Endpoint'); ?></th>
+ <td><?= $this->programStatus->endpoint_name
+ ? $this->programStatus->endpoint_name
+ : $this->translate('N/A') ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Active Icinga Web 2 Endpoint'); ?></th>
+ <td><?= gethostname() ?: $this->translate('N/A') ?></td>
+ </tr>
+ </tbody>
+ </table>
+ <?php if ((bool) $this->programStatus->is_currently_running === true): ?>
+ <div class="backend-running">
+ <?= sprintf(
+ $this->translate(
+ '%1$s has been up and running with PID %2$d %3$s',
+ 'Last format parameter represents the time running'
+ ),
+ $this->backendName,
+ $this->programStatus->process_id,
+ $this->timeSince($this->programStatus->program_start_time)) ?>
+ </div>
+ <?php else: ?>
+ <div class="backend-not-running">
+ <?= sprintf($this->translate('Backend %s is not running'), $this->backendName) ?>
+ </div>
+ <?php endif ?>
+ </div>
+ <div class="box features">
+ <h2 tabindex="0"><?= $this->translate('Feature Commands') ?></h2>
+ <?= $this->toggleFeaturesForm ?>
+ </div>
+ </div>
+</div>
diff --git a/modules/monitoring/application/views/scripts/health/not-running.phtml b/modules/monitoring/application/views/scripts/health/not-running.phtml
new file mode 100644
index 0000000..8439fc4
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/health/not-running.phtml
@@ -0,0 +1,8 @@
+<?php if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs; ?>
+</div>
+<?php endif ?>
+<div class="content">
+ <?= sprintf($this->translate('%s is currently not up and running'), $this->backendName) ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/health/stats.phtml b/modules/monitoring/application/views/scripts/health/stats.phtml
new file mode 100644
index 0000000..5cfb8f9
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/health/stats.phtml
@@ -0,0 +1,150 @@
+<?php
+$rv = $this->runtimeVariables()->create($this->runtimevariables);
+$cp = $this->checkPerformance()->create($this->checkperformance);
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<?php endif ?>
+
+<div class="content stats">
+ <div class="boxview">
+ <div class="box stats">
+ <h2 tabindex="0"><?= $this->unhandledProblems ?> <?= $this->translate('Unhandled Problems:') ?></h2>
+ <table class="name-value-table">
+ <thead>
+ <th></th>
+ <th colspan="3"></th>
+ </thead>
+ <tbody>
+ <tr>
+ <th><?= $this->translate('Service Problems:') ?></th>
+ <td colspan="3">
+ <span class="badge state-critical">
+ <?=
+ $this->qlink(
+ $this->unhandledServiceProblems,
+ 'monitoring/list/services?service_problem=1&service_handled=0&sort=service_severity',
+ null,
+ array('data-base-target' => '_next')
+ )
+ ?>
+ </span>
+ </td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Host Problems:') ?></th>
+ <td colspan="3">
+ <span class="badge state-critical">
+ <?=
+ $this->qlink(
+ $this->unhandledhostProblems,
+ 'monitoring/list/hosts?host_problem=1&host_handled=0',
+ null,
+ array('data-base-target' => '_next')
+ )
+ ?>
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <h2 tabindex="0" class="tinystatesummary" data-base-target="_next">
+ <?php $this->stats = $hoststats ?>
+ <?= $this->render('list/components/hostssummary.phtml') ?>
+ </h2>
+ <table class="name-value-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Runtime Variables') ?></th>
+ <th colspan="3"><?= $this->translate('Host Checks') ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th><?= $this->translate('Total') ?></th>
+ <td><?= $rv->total_scheduled_hosts ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Scheduled') ?></th>
+ <td><?= $rv->total_scheduled_hosts ?></td>
+ </tr>
+ </tbody>
+ </table>
+
+ <h2 class="tinystatesummary" data-base-target="_next">
+ <?php $this->stats = $servicestats ?>
+ <?= $this->render('list/components/servicesummary.phtml') ?>
+ </h2>
+ <table class="name-value-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Runtime Variables') ?></th>
+ <th><?= $this->translate('Service Checks') ?></th>
+ <th colspan="2"><?= $this->translate('Per Host') ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th><?= $this->translate('Total') ?></th>
+ <td><?= $rv->total_services ?></td>
+ <td><?= sprintf('%.2f', $rv->average_services_per_host) ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Scheduled') ?></th>
+ <td><?= $rv->total_scheduled_services ?></td>
+ <td><?= sprintf('%.2f', $rv->average_scheduled_services_per_host) ?></td>
+ </tr>
+ </tbody>
+ </table>
+
+ <h2><?= $this->translate('Active checks') ?></h2>
+ <table class="name-value-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Check Performance') ?></th>
+ <th><?= $this->translate('Checks') ?></th>
+ <th><?= $this->translate('Latency') ?></th>
+ <th><?= $this->translate('Execution time') ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th><?= $this->translate('Host Checks') ?></th>
+ <td><?= $cp->host_active_count; ?></td>
+ <td><?= sprintf('%.3f', $cp->host_active_latency_avg) ?>s</td>
+ <td><?= sprintf('%.3f', $cp->host_active_execution_avg) ?>s</td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Service Checks') ?></th>
+ <td><?= $cp->service_active_count; ?></td>
+ <td><?= sprintf('%.3f', $cp->service_active_latency_avg) ?>s</td>
+ <td><?= sprintf('%.3f', $cp->service_active_execution_avg) ?>s</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <h2><?= $this->translate('Passive checks') ?></h2>
+ <table class="name-value-table">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Check Performance') ?></th>
+ <th colspan="3"><?= $this->translate('Passive Checks') ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th><?= $this->translate('Host Checks') ?></th>
+ <td><?= $cp->host_passive_count ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Service Checks') ?></th>
+ <td><?= $cp->service_passive_count ?></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+</div>
diff --git a/modules/monitoring/application/views/scripts/host/services.phtml b/modules/monitoring/application/views/scripts/host/services.phtml
new file mode 100644
index 0000000..ac1dc5b
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/host/services.phtml
@@ -0,0 +1,23 @@
+<?php use Icinga\Data\Filter\Filter; ?>
+
+<div class="controls">
+ <?php if (! $this->compact): ?>
+ <?= $this->tabs; ?>
+ <?php endif ?>
+ <?= $this->render('partials/object/host-header.phtml') ?>
+ <?php
+ $this->baseFilter = Filter::where('host', $object->host_name);
+ $this->stats = $object->stats;
+ echo $this->render('list/components/servicesummary.phtml');
+ ?>
+</div>
+<?= $this->partial(
+ 'list/services.phtml',
+ 'monitoring',
+ array(
+ 'compact' => true,
+ 'showHost' => false,
+ 'services' => $services,
+ 'addColumns' => array()
+ )
+); ?>
diff --git a/modules/monitoring/application/views/scripts/host/show.phtml b/modules/monitoring/application/views/scripts/host/show.phtml
new file mode 100644
index 0000000..72f5af4
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/host/show.phtml
@@ -0,0 +1,14 @@
+<?php use Icinga\Data\Filter\Filter; ?>
+<div class="controls controls-separated">
+<?php if (! $this->compact): ?>
+ <?= $this->tabs ?>
+<?php endif ?>
+ <?= $this->render('partials/object/host-header.phtml') ?>
+<?php
+ $this->stats = $object->stats;
+ $this->baseFilter = Filter::where('host', $object->host_name);
+ echo $this->render('list/components/servicesummary.phtml');
+?>
+ <?= $this->render('partials/object/quick-actions.phtml') ?>
+</div>
+<?= $this->render('partials/object/detail-content.phtml') ?>
diff --git a/modules/monitoring/application/views/scripts/hosts/show.phtml b/modules/monitoring/application/views/scripts/hosts/show.phtml
new file mode 100644
index 0000000..97b8434
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/hosts/show.phtml
@@ -0,0 +1,206 @@
+<div class="controls">
+ <?php if (! $this->compact): ?>
+ <?= $tabs; ?>
+ <?php endif ?>
+ <?= $this->render('list/components/hostssummary.phtml') ?>
+ <?= $this->render('partials/host/objects-header.phtml'); ?>
+ <?php
+ $hostCount = count($objects);
+ $unhandledCount = count($unhandledObjects);
+ $problemCount = count($problemObjects);
+ $unackCount = count($unacknowledgedObjects);
+ $scheduledDowntimeCount = count($objects->getScheduledDowntimes());
+ ?>
+</div>
+
+<div class="content">
+ <?php if ($hostCount === 0): ?>
+ <?= $this->translate('No hosts found matching the filter'); ?>
+ <?php else: ?>
+ <?= $this->render('show/components/extensions.phtml') ?>
+ <h2><?= $this->translate('Problem Handling') ?></h2>
+ <table class="name-value-table">
+ <tbody>
+ <?php
+
+ if ($unackCount > 0): ?>
+ <tr>
+ <th> <?= sprintf($this->translate('%d unhandled problems'), $unackCount) ?> </th>
+ <td>
+ <?= $this->qlink(
+ $this->translate('Acknowledge'),
+ $acknowledgeLink,
+ null,
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'check'
+ )
+ ) ?>
+ </td>
+ </tr>
+ <?php endif; ?>
+
+ <?php if (($acknowledgedCount = count($acknowledgedObjects)) > 0): ?>
+ <tr>
+ <th> <?= sprintf(
+ $this->translatePlural(
+ '%s acknowledgement',
+ '%s acknowledgements',
+ $acknowledgedCount
+ ),
+ '<b>' . $acknowledgedCount . '</b>'
+ ); ?> </th>
+ <td>
+ <?= $removeAckForm->setLabelEnabled(true) ?>
+ </td>
+ </tr>
+ <?php endif ?>
+
+ <tr>
+ <th> <?= $this->translate('Comments') ?> </th>
+ <td>
+ <?= $this->qlink(
+ $this->translate('Add comments'),
+ $addCommentLink,
+ null,
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'comment-empty'
+ )
+ ) ?>
+ </td>
+ </tr>
+
+ <?php if (($commentCount = count($objects->getComments())) > 0): ?>
+ <tr>
+ <th></th>
+ <td>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ '%s comment',
+ '%s comments',
+ $commentCount
+ ),
+ $commentCount
+ ),
+ $commentsLink,
+ null,
+ array('data-base-target' => '_next')
+ ); ?>
+ </td>
+ </tr>
+ <?php endif ?>
+
+ <tr>
+ <th>
+ <?= $this->translate('Downtimes') ?>
+ </th>
+ <td>
+ <?= $this->qlink(
+ $this->translate('Schedule downtimes'),
+ $downtimeAllLink,
+ null,
+ array(
+ 'icon' => 'plug',
+ 'class' => 'action-link'
+ )
+ ) ?>
+ </td>
+ </tr>
+
+ <?php if ($scheduledDowntimeCount > 0): ?>
+ <tr>
+ <th></th>
+ <td>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ '%d scheduled downtime',
+ '%d scheduled downtimes',
+ $scheduledDowntimeCount
+ ),
+ $scheduledDowntimeCount
+ ),
+ $showDowntimesLink,
+ null,
+ array(
+ 'data-base-target' => '_next'
+ )
+ ) ?>
+ </td>
+ </tr>
+ <?php endif ?>
+ </tbody>
+ </table>
+
+ <?php if ($this->hasPermission('monitoring/command/send-custom-notification')): ?>
+ <h2> <?= $this->translate('Notifications') ?> </h2>
+ <table class="name-value-table">
+ <tbody>
+ <tr>
+ <th> <?= $this->translate('Notifications') ?> </th>
+ <td>
+ <?= $this->qlink(
+ $this->translate('Send notifications'),
+ $sendCustomNotificationLink,
+ null,
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'bell'
+ )
+ ) ?>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <?php endif ?>
+
+ <h2> <?= $this->translate('Check Execution') ?> </h2>
+
+ <table class="name-value-table">
+ <tbody>
+ <tr>
+ <th> <?= $this->translate('Command') ?> </th>
+ <td>
+ <?= $this->qlink(
+ $this->translate('Process check result'),
+ $processCheckResultAllLink,
+ null,
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'edit'
+ )
+ ) ?>
+ </td>
+ </tr>
+
+ <?php if (isset($checkNowForm)): // Form is unset if the current user lacks the respective permission ?>
+ <tr>
+ <th> <?= $this->translate('Schedule Check') ?> </th>
+ <td> <?= $checkNowForm ?> </td>
+ </tr>
+ <?php endif ?>
+
+ <?php if (isset($rescheduleAllLink)): ?>
+ <tr>
+ <th></th>
+ <td>
+ <?= $this->qlink(
+ $this->translate('Reschedule'),
+ $rescheduleAllLink,
+ null,
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'calendar-empty'
+ )
+ ) ?>
+ </td>
+ </tr>
+ <?php endif ?>
+ </tbody>
+ </table>
+ <h2><?= $this->translate('Feature Commands') ?></h2>
+ <?= $toggleFeaturesForm ?>
+ <?php endif ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/comments.phtml b/modules/monitoring/application/views/scripts/list/comments.phtml
new file mode 100644
index 0000000..c7fb86a
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/comments.phtml
@@ -0,0 +1,61 @@
+<?php if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->render('list/components/selectioninfo.phtml') ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php if (! $comments->hasResult()): ?>
+ <p><?= $this->translate('No comments found matching the filter') ?></p>
+</div>
+<?php return; endif ?>
+ <table data-base-target="_next"
+ class="table-row-selectable common-table multiselect"
+ data-icinga-multiselect-url="<?= $this->href('monitoring/comments/show') ?>"
+ data-icinga-multiselect-related="<?= $this->href("monitoring/comments") ?>"
+ data-icinga-multiselect-data="comment_id">
+ <thead class="print-only">
+ <tr>
+ <th><?= $this->translate('Type') ?></th>
+ <th><?= $this->translate('Comment') ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($comments->peekAhead($this->compact) as $comment): ?>
+ <tr href="<?= $this->href('monitoring/comment/show', array('comment_id' => $comment->id)) ?>">
+ <td class="icon-col">
+ <?= $this->partial('partials/comment/comment-description.phtml', array('comment' => $comment)) ?>
+ </td>
+ <td>
+ <?= $this->partial(
+ 'partials/comment/comment-detail.phtml',
+ array(
+ 'comment' => $comment,
+ 'delCommentForm' => isset($delCommentForm) ? $delCommentForm : null
+ // Form is unset if the current user lacks the respective permission
+ )) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+<?php if ($comments->hasMore()): ?>
+ <div class="dont-print action-links">
+ <?= $this->qlink(
+ $this->translate('Show More'),
+ $this->url()->without(array('showCompact', 'limit')),
+ null,
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_next'
+ )
+ ) ?>
+ </div>
+<?php endif ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/components/hostssummary.phtml b/modules/monitoring/application/views/scripts/list/components/hostssummary.phtml
new file mode 100644
index 0000000..4b9f1cd
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/components/hostssummary.phtml
@@ -0,0 +1,92 @@
+<?php
+use Icinga\Module\Monitoring\Web\Widget\StateBadges;
+use Icinga\Web\Url;
+
+// Don't fetch rows until they are actually needed to improve dashlet performance
+if (! $stats instanceof stdClass) {
+ $stats = $stats->fetchRow();
+}
+?>
+<div class="hosts-summary dont-print">
+ <span class="hosts-link"><?= $this->qlink(
+ sprintf($this->translatePlural('%u Host', '%u Hosts', $stats->hosts_total), $stats->hosts_total),
+ // @TODO(el): Fix that
+ Url::fromPath('monitoring/list/hosts')->setParams(isset($baseFilter) ? $baseFilter->getUrlParams() : array()),
+ null,
+ array('title' => sprintf(
+ $this->translatePlural('List %u host', 'List all %u hosts', $stats->hosts_total),
+ $stats->hosts_total
+ ))
+ ) ?>&#58;</span>
+<?php
+$stateBadges = new StateBadges();
+$stateBadges
+ ->setBaseFilter(isset($baseFilter) ? $baseFilter : null)
+ ->setUrl('monitoring/list/hosts')
+ ->add(
+ StateBadges::STATE_UP,
+ $stats->hosts_up,
+ array(
+ 'host_state' => 0
+ ),
+ 'List %u host that is currently in state UP',
+ 'List %u hosts which are currently in state UP',
+ array($stats->hosts_up)
+ )
+ ->add(
+ StateBadges::STATE_DOWN,
+ $stats->hosts_down_unhandled,
+ array(
+ 'host_state' => 1,
+ 'host_handled' => 0
+ ),
+ 'List %u host that is currently in state DOWN',
+ 'List %u hosts which are currently in state DOWN',
+ array($stats->hosts_down_unhandled)
+ )
+ ->add(
+ StateBadges::STATE_DOWN_HANDLED,
+ $stats->hosts_down_handled,
+ array(
+ 'host_state' => 1,
+ 'host_handled' => 1
+ ),
+ 'List %u host that is currently in state DOWN (Acknowledged)',
+ 'List %u hosts which are currently in state DOWN (Acknowledged)',
+ array($stats->hosts_down_handled)
+ )
+ ->add(
+ StateBadges::STATE_UNREACHABLE,
+ $stats->hosts_unreachable_unhandled,
+ array(
+ 'host_state' => 2,
+ 'host_handled' => 0
+ ),
+ 'List %u host that is currently in state UNREACHABLE',
+ 'List %u hosts which are currently in state UNREACHABLE',
+ array($stats->hosts_unreachable_unhandled)
+ )
+ ->add(
+ StateBadges::STATE_UNREACHABLE_HANDLED,
+ $stats->hosts_unreachable_handled,
+ array(
+ 'host_state' => 2,
+ 'host_handled' => 1
+ ),
+ 'List %u host that is currently in state UNREACHABLE (Acknowledged)',
+ 'List %u hosts which are currently in state UNREACHABLE (Acknowledged)',
+ array($stats->hosts_unreachable_handled)
+ )
+ ->add(
+ StateBadges::STATE_PENDING,
+ $stats->hosts_pending,
+ array(
+ 'host_state' => 99
+ ),
+ 'List %u host that is currently in state PENDING',
+ 'List %u hosts which are currently in state PENDING',
+ array($stats->hosts_pending)
+ );
+echo $stateBadges->render();
+?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/components/selectioninfo.phtml b/modules/monitoring/application/views/scripts/list/components/selectioninfo.phtml
new file mode 100644
index 0000000..ec0fb85
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/components/selectioninfo.phtml
@@ -0,0 +1,15 @@
+<?php
+$helpMessage = $this->translate(
+ 'Press and hold the Ctrl key while clicking on rows to select multiple rows or press and hold the Shift key to'
+ .' select a range of rows',
+ 'Multi-selection help'
+);
+?>
+<div class="selection-info" title="<?= $this->escape($helpMessage) ?>">
+ <?= sprintf(
+ /// TRANSLATORS: Please leave %s as it is because the selection counter is wrapped in a span tag for updating
+ /// the counter via JavaScript
+ $this->translate('%s row(s) selected', 'Multi-selection count'),
+ '<span class="selection-info-count">0</span>'
+ ) ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/components/servicesummary.phtml b/modules/monitoring/application/views/scripts/list/components/servicesummary.phtml
new file mode 100644
index 0000000..73a3b57
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/components/servicesummary.phtml
@@ -0,0 +1,118 @@
+<?php
+use Icinga\Module\Monitoring\Web\Widget\StateBadges;
+use Icinga\Web\Url;
+
+// Don't fetch rows until they are actually needed, to improve dashlet performance
+if (! $stats instanceof stdClass) {
+ $stats = $stats->fetchRow();
+}
+?>
+<div class="services-summary dont-print">
+ <span class="services-link"><?= $this->qlink(
+ sprintf($this->translatePlural(
+ '%u Service', '%u Services', $stats->services_total),
+ $stats->services_total
+ ),
+ // @TODO(el): Fix that
+ Url::fromPath('monitoring/list/services')->setParams(isset($baseFilter) ? $baseFilter->getUrlParams() : array()),
+ null,
+ array('title' => sprintf(
+ $this->translatePlural('List %u service', 'List all %u services', $stats->services_total),
+ $stats->services_total
+ ))
+ ) ?>&#58;</span>
+<?php
+$stateBadges = new StateBadges();
+$stateBadges
+ ->setBaseFilter(isset($baseFilter) ? $baseFilter : null)
+ ->setUrl('monitoring/list/services')
+ ->add(
+ StateBadges::STATE_OK,
+ $stats->services_ok,
+ array(
+ 'service_state' => 0
+ ),
+ 'List %u service that is currently in state OK',
+ 'List %u services which are currently in state OK',
+ array($stats->services_ok)
+ )
+ ->add(
+ StateBadges::STATE_CRITICAL,
+ $stats->services_critical_unhandled,
+ array(
+ 'service_state' => 2,
+ 'service_handled' => 0
+ ),
+ 'List %u service that is currently in state CRITICAL',
+ 'List %u services which are currently in state CRITICAL',
+ array($stats->services_critical_unhandled)
+ )
+ ->add(
+ StateBadges::STATE_CRITICAL_HANDLED,
+ $stats->services_critical_handled,
+ array(
+ 'service_state' => 2,
+ 'service_handled' => 1
+ ),
+ 'List %u handled service that is currently in state CRITICAL',
+ 'List %u handled services which are currently in state CRITICAL',
+ array($stats->services_critical_handled)
+ )
+ ->add(
+ StateBadges::STATE_UNKNOWN,
+ $stats->services_unknown_unhandled,
+ array(
+ 'service_state' => 3,
+ 'service_handled' => 0
+ ),
+ 'List %u service that is currently in state UNKNOWN',
+ 'List %u services which are currently in state UNKNOWN',
+ array($stats->services_unknown_unhandled)
+ )
+ ->add(
+ StateBadges::STATE_UNKNOWN_HANDLED,
+ $stats->services_unknown_handled,
+ array(
+ 'service_state' => 3,
+ 'service_handled' => 1
+ ),
+ 'List %u handled service that is currently in state UNKNOWN',
+ 'List %u handled services which are currently in state UNKNOWN',
+ array($stats->services_unknown_handled)
+
+ )
+ ->add(
+ StateBadges::STATE_WARNING,
+ $stats->services_warning_unhandled,
+ array(
+ 'service_state' => 1,
+ 'service_handled' => 0
+ ),
+ 'List %u service that is currently in state WARNING',
+ 'List %u services which are currently in state WARNING',
+ array($stats->services_warning_unhandled)
+ )
+ ->add(
+ StateBadges::STATE_WARNING_HANDLED,
+ $stats->services_warning_handled,
+ array(
+ 'service_state' => 1,
+ 'service_handled' => 1
+ ),
+ 'List %u handled service that is currently in state WARNING',
+ 'List %u handled services which are currently in state WARNING',
+ array($stats->services_warning_handled)
+ )
+ ->add(
+ StateBadges::STATE_PENDING,
+ $stats->services_pending,
+ array(
+ 'service_state' => 99
+ ),
+ 'List %u handled service that is currently in state PENDING',
+ 'List %u handled services which are currently in state PENDING',
+ array($stats->services_pending)
+ );
+echo $stateBadges->render();
+?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/contactgroups.phtml b/modules/monitoring/application/views/scripts/list/contactgroups.phtml
new file mode 100644
index 0000000..125aeea
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/contactgroups.phtml
@@ -0,0 +1,53 @@
+<?php
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php if (! $contactGroups->hasResult()): ?>
+ <p><?= $this->translate('No contact groups found matching the filter') ?></p>
+</div>
+<?php return; endif ?>
+ <table class="common-table table-row-selectable" data-base-target="_next">
+ <thead>
+ <tr>
+ <th></th>
+ <th><?= $this->translate('Contact Group') ?></th>
+ <th><?= $this->translate('Alias') ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($contactGroups as $contactGroup): ?>
+ <tr>
+ <td class="count-col">
+ <span class="badge"><?= $contactGroup->contact_count ?></span>
+ </td>
+ <th>
+ <?= $this->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
+ ))
+ ) ?>
+ </th>
+ <td>
+ <?php if ($contactGroup->contactgroup_name !== $contactGroup->contactgroup_alias): ?>
+ <?= $contactGroup->contactgroup_alias ?>
+ <?php endif ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/contacts.phtml b/modules/monitoring/application/views/scripts/list/contacts.phtml
new file mode 100644
index 0000000..42ec778
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/contacts.phtml
@@ -0,0 +1,83 @@
+<?php if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php if (! $contacts->hasResult()): ?>
+ <p><?= $this->translate('No contacts found matching the filter') ?></p>
+</div>
+<?php return; endif ?>
+ <table class="common-table table-row-selectable" data-base-target="_next">
+ <thead>
+ <tr>
+ <th><?= $this->translate('Name') ?></th>
+ <th><?= $this->translate('Email') ?></th>
+ <th><?= $this->translate('Pager') ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($contacts->peekAhead($this->compact) as $contact): ?>
+ <tr>
+ <th>
+ <?= $this->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
+ )
+ )
+ ) ?>
+ </th>
+ <td>
+ <?= $this->translate('Email') ?>:
+ <a href="mailto:<?= $contact->contact_email ?>"
+ title="<?= sprintf($this->translate('Send a mail to %s'), $contact->contact_alias) ?>"
+ aria-label="<?= sprintf($this->translate('Send a mail to %s'), $contact->contact_alias) ?>">
+ <?= $this->escape($contact->contact_email) ?>
+ </a>
+ </td>
+ <td>
+ <?php if ($contact->contact_pager): ?>
+ <?= $this->escape($contact->contact_pager) ?>
+ <?php endif ?>
+ </td>
+
+ <?php if ($contact->contact_notify_service_timeperiod): ?>
+ <td>
+ <?= $this->escape($contact->contact_notify_service_timeperiod) ?>
+ </td>
+ <?php endif ?>
+
+ <?php if ($contact->contact_notify_host_timeperiod): ?>
+ <td>
+ <?= $this->escape($contact->contact_notify_host_timeperiod) ?>
+ </td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+<?php if ($contacts->hasMore()): ?>
+ <div class="dont-print action-links">
+ <?= $this->qlink(
+ $this->translate('Show More'),
+ $this->url()->without(array('showCompact', 'limit')),
+ null,
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_next'
+ )
+ ) ?>
+ </div>
+<?php endif ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/downtimes.phtml b/modules/monitoring/application/views/scripts/list/downtimes.phtml
new file mode 100644
index 0000000..46ce0bb
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/downtimes.phtml
@@ -0,0 +1,64 @@
+<?php
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\Service;
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->render('list/components/selectioninfo.phtml') ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php if (! $downtimes->hasResult()): ?>
+ <p><?= $this->translate('No downtimes found matching the filter.') ?></p>
+</div>
+<?php return; endif ?>
+ <table class="common-table state-table table-row-selectable multiselect"
+ data-base-target="_next"
+ data-icinga-multiselect-url="<?= $this->href('monitoring/downtimes/show') ?>"
+ data-icinga-multiselect-controllers="<?= $this->href("monitoring/downtimes") ?>"
+ data-icinga-multiselect-data="downtime_id">
+ <thead class="print-only">
+ <tr>
+ <th><?= $this->translate('State') ?></th>
+ <th><?= $this->translate('Downtime') ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($downtimes->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);
+ }
+ // Set downtime for partials
+ $this->downtime = $downtime;
+ ?>
+ <tr href="<?= $this->href('monitoring/downtime/show', array('downtime_id' => $downtime->id)) ?>">
+ <?= $this->render('partials/downtime/downtime-header.phtml') ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+<?php if ($downtimes->hasMore()): ?>
+ <div class="dont-print action-links">
+ <?= $this->qlink(
+ $this->translate('Show More'),
+ $this->url()->without(array('showCompact', 'limit')),
+ null,
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_next'
+ )
+ ) ?>
+ </div>
+<?php endif ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/eventgrid.phtml b/modules/monitoring/application/views/scripts/list/eventgrid.phtml
new file mode 100644
index 0000000..ca19123
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/eventgrid.phtml
@@ -0,0 +1,123 @@
+<?php
+use Icinga\Data\Filter\Filter;
+use Icinga\Web\Widget\Chart\HistoryColorGrid;
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->form ?>
+</div>
+<?php endif ?>
+<div class="content" data-base-target="_next">
+<?php
+
+$settings = array(
+ 'cnt_up' => array(
+ 'tooltip' => $this->translate('%d hosts ok on %s'),
+ 'color' => '#49DF96',
+ 'opacity' => '0.55'
+ ),
+ 'cnt_unreachable_hard' => array(
+ 'tooltip' => $this->translate('%d hosts unreachable on %s'),
+ 'color' => '#77AAFF',
+ 'opacity' => '0.55'
+ ),
+ 'cnt_critical_hard' => array(
+ 'tooltip' => $this->translate('%d services critical on %s'),
+ 'color' => '#ff5566',
+ 'opacity' => '0.9'
+ ),
+
+ 'cnt_warning_hard' => array(
+ 'tooltip' => $this->translate('%d services warning on %s'),
+ 'color' => '#ffaa44',
+ 'opacity' => '1.0'
+ ),
+
+ 'cnt_down_hard' => array(
+ 'tooltip' => $this->translate('%d hosts down on %s'),
+ 'color' => '#ff5566',
+ 'opacity' => '0.9'
+ ),
+ 'cnt_unknown_hard' => array(
+ 'tooltip' => $this->translate('%d services unknown on %s'),
+ 'color' => '#cc77ff',
+ 'opacity' => '0.7'
+ ),
+ 'cnt_ok' => array(
+ 'tooltip' => $this->translate('%d services ok on %s'),
+ 'color' => '#49DF96',
+ 'opacity' => '0.55'
+ )
+);
+
+$data = array();
+foreach ($summary as $entry) {
+ $day = $entry->day;
+ $value = $entry->$column;
+ $caption = sprintf(
+ $settings[$column]['tooltip'],
+ $value,
+ $this->formatDate(strtotime($day ?? ''))
+ );
+ $linkFilter = Filter::matchAll(
+ Filter::expression('timestamp', '<', strtotime($day . ' 23:59:59')),
+ Filter::expression('timestamp', '>', strtotime($day . ' 00:00:00')),
+ $form->getFilter(),
+ $filter
+ );
+ $data[$day] = array(
+ 'value' => $value,
+ 'caption' => $caption,
+ 'url' => $this->href('monitoring/list/eventhistory?' . $linkFilter->toQueryString())
+ );
+}
+
+if (! $summary->hasResult()) {
+ echo $this->translate('No state changes in the selected time period.') . '</div>';
+ return;
+}
+
+$from = intval($form->getValue('from', strtotime('3 months ago')));
+$to = intval($form->getValue('to', time()));
+
+// don't display more than ten years, or else this will get really slow
+if ($to - $from > 315360000) {
+ $from = $to - 315360000;
+}
+
+$f = new DateTime();
+$f->setTimestamp($from);
+$t = new DateTime();
+$t->setTimestamp($to);
+$diff = $t->diff($f);
+$step = 124;
+
+for ($i = 0; $i < $diff->days; $i += $step) {
+ $end = clone $f;
+ if ($diff->days - $i > $step) {
+ // full range, move last day to next chunk
+ $end->add(new DateInterval('P' . ($step - 1) . 'D'));
+ } else {
+ // include last day
+ $end->add(new DateInterval('P' . ($diff->days - $i) . 'D'));
+ }
+ $grid = new HistoryColorGrid(null, $f->getTimestamp(), $end->getTimestamp());
+ $grid->setColor($settings[$column]['color']);
+ $grid->opacity = $settings[$column]['opacity'];
+ $grid->orientation = $orientation;
+ $grid->setData($data);
+ $grids[] = $grid;
+
+ $f->add(new DateInterval('P' . $step . 'D'));
+}
+?>
+<div class="event-grid">
+<?php foreach (array_reverse($grids) as $key => $grid): ?>
+ <div class=" <?= $this->orientation === 'horizontal' ? '' : 'vertical' ?>">
+ <?= $grid; ?>
+ <?= $this->orientation === 'horizontal' ? '<br />' : '' ?>
+ </div>
+<?php endforeach ?>
+ </div>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/eventhistory.phtml b/modules/monitoring/application/views/scripts/list/eventhistory.phtml
new file mode 100644
index 0000000..0573e8a
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/eventhistory.phtml
@@ -0,0 +1,22 @@
+<?php
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<?= $this->partial(
+ 'partials/event-history.phtml',
+ array(
+ 'compact' => $this->compact,
+ 'history' => $history,
+ 'isOverview' => true,
+ 'translationDomain' => $this->translationDomain
+ )
+) ?>
+
diff --git a/modules/monitoring/application/views/scripts/list/hostgroup-grid.phtml b/modules/monitoring/application/views/scripts/list/hostgroup-grid.phtml
new file mode 100644
index 0000000..34498d0
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/hostgroup-grid.phtml
@@ -0,0 +1,173 @@
+<?php if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <div class="sort-controls-container">
+ <?= $this->sortBox ?>
+ <a href="<?= $this->href('monitoring/list/hostgroups')->addFilter($this->filterEditor->getFilter()) ?>" class="grid-toggle-link"
+ title="<?= $this->translate('Toogle grid view mode') ?>">
+ <?= $this->icon('th-list', null, ['class' => '-inactive']) ?>
+ <?= $this->icon('th-thumb-empty', null, ['class' => '-active']) ?>
+ </a>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content" data-base-target="_next">
+<?php /** @var \Icinga\Module\Monitoring\DataView\Hostgroup $hostGroups */
+if (! $hostGroups->hasResult()): ?>
+ <p><?= $this->translate('No host groups found matching the filter.') ?></p>
+</div>
+<?php return; endif ?>
+<div class="group-grid">
+<?php foreach ($hostGroups as $hostGroup): ?>
+ <div class="group-grid-cell">
+ <?php if ($hostGroup->hosts_down_unhandled > 0): ?>
+ <?= $this->qlink(
+ $hostGroup->hosts_down_unhandled,
+ $this->url('monitoring/list/hosts')->addFilter($this->filterEditor->getFilter()),
+ [
+ 'hostgroup_name' => $hostGroup->hostgroup_name,
+ 'host_handled' => 0,
+ 'host_state' => 1
+ ],
+ [
+ 'class' => 'state-down',
+ 'title' => sprintf(
+ $this->translatePlural(
+ '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"',
+ $hostGroup->hosts_down_unhandled
+ ),
+ $hostGroup->hosts_down_unhandled,
+ $hostGroup->hostgroup_alias
+ )
+ ]
+ ) ?>
+ <?php elseif ($hostGroup->hosts_unreachable_unhandled > 0): ?>
+ <?= $this->qlink(
+ $hostGroup->hosts_unreachable_unhandled,
+ $this->url('monitoring/list/hosts')->addFilter($this->filterEditor->getFilter()),
+ [
+ 'hostgroup_name' => $hostGroup->hostgroup_name,
+ 'host_handled' => 0,
+ 'host_state' => 2
+ ],
+ [
+ 'class' => 'state-unreachable',
+ 'title' => sprintf(
+ $this->translatePlural(
+ '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"',
+ $hostGroup->hosts_unreachable_unhandled
+ ),
+ $hostGroup->hosts_unreachable_unhandled,
+ $hostGroup->hostgroup_alias
+ )
+ ]
+ ) ?>
+ <?php elseif ($hostGroup->hosts_down_handled > 0): ?>
+ <?= $this->qlink(
+ $hostGroup->hosts_down_handled,
+ $this->url('monitoring/list/hosts')->addFilter($this->filterEditor->getFilter()),
+ [
+ 'hostgroup_name' => $hostGroup->hostgroup_name,
+ 'host_handled' => 1,
+ 'host_state' => 1
+ ],
+ [
+ 'class' => 'state-down handled',
+ 'title' => sprintf(
+ $this->translatePlural(
+ '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"',
+ $hostGroup->hosts_down_handled
+ ),
+ $hostGroup->hosts_down_handled,
+ $hostGroup->hostgroup_alias
+ )
+ ]
+ ) ?>
+ <?php elseif ($hostGroup->hosts_unreachable_handled > 0): ?>
+ <?= $this->qlink(
+ $hostGroup->hosts_unreachable_handled,
+ $this->url('monitoring/list/hosts')->addFilter($this->filterEditor->getFilter()),
+ [
+ 'hostgroup_name' => $hostGroup->hostgroup_name,
+ 'host_handled' => 0,
+ 'host_state' => 2
+ ],
+ [
+ 'class' => 'state-unreachable handled',
+ 'title' => sprintf(
+ $this->translatePlural(
+ '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"',
+ $hostGroup->hosts_unreachable_handled
+ ),
+ $hostGroup->hosts_unreachable_handled,
+ $hostGroup->hostgroup_alias
+ )
+ ]
+ ) ?>
+ <?php elseif ($hostGroup->hosts_pending > 0): ?>
+ <?= $this->qlink(
+ $hostGroup->hosts_pending,
+ $this->url('monitoring/list/hosts')->addFilter($this->filterEditor->getFilter()),
+ [
+ 'hostgroup_name' => $hostGroup->hostgroup_name,
+ 'host_state' => 99
+ ],
+ [
+ 'class' => 'state-pending',
+ 'title' => sprintf(
+ $this->translatePlural(
+ '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"',
+ $hostGroup->hosts_pending
+ ),
+ $hostGroup->hosts_pending,
+ $hostGroup->hostgroup_alias
+ )
+ ]
+ ) ?>
+ <?php elseif ($hostGroup->hosts_up > 0): ?>
+ <?= $this->qlink(
+ $hostGroup->hosts_up,
+ $this->url('monitoring/list/hosts')->addFilter($this->filterEditor->getFilter()),
+ [
+ 'hostgroup_name' => $hostGroup->hostgroup_name,
+ 'host_state' => 0
+ ],
+ [
+ 'class' => 'state-up',
+ 'title' => sprintf(
+ $this->translatePlural(
+ '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"',
+ $hostGroup->hosts_up
+ ),
+ $hostGroup->hosts_up,
+ $hostGroup->hostgroup_alias
+ )
+ ]
+ ) ?>
+ <?php else: ?>
+ <div class="state-none">
+ 0
+ </div>
+ <?php endif ?>
+ <?= $this->qlink(
+ $hostGroup->hostgroup_alias,
+ $this->url('monitoring/list/hosts')->addFilter($this->filterEditor->getFilter()),
+ ['hostgroup_name' => $hostGroup->hostgroup_name],
+ [
+ 'title' => sprintf(
+ $this->translate('List all hosts in the group "%s"'),
+ $hostGroup->hostgroup_alias
+ )
+ ]
+ ) ?>
+ </div>
+<?php endforeach ?>
+</div>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/hostgroups.phtml b/modules/monitoring/application/views/scripts/list/hostgroups.phtml
new file mode 100644
index 0000000..a0592c8
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/hostgroups.phtml
@@ -0,0 +1,296 @@
+<?php
+
+use Icinga\Module\Monitoring\Web\Widget\StateBadges;
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ <a href="<?= $this->href('monitoring/list/hostgroup-grid')->addFilter(clone $this->filterEditor->getFilter()) ?>" class="grid-toggle-link"
+ title="<?= $this->translate('Toogle grid view mode') ?>">
+ <?= $this->icon('th-list', null, ['class' => '-active']) ?>
+ <?= $this->icon('th-thumb-empty', null, ['class' => '-inactive']) ?>
+ </a>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+
+<div class="content">
+<?php /** @var \Icinga\Module\Monitoring\DataView\Hostgroup $hostGroups */ if (! $hostGroups->hasResult()): ?>
+ <p><?= $this->translate('No host groups found matching the filter.') ?></p>
+</div>
+<?php return; endif ?>
+ <table class="common-table table-row-selectable" data-base-target="_next">
+ <thead>
+ <tr>
+ <th></th>
+ <th><?= $this->translate('Host Group') ?></th>
+ <th><?= $this->translate('Host States') ?></th>
+ <th></th>
+ <th><?= $this->translate('Service States') ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($hostGroups->peekAhead($this->compact) as $hostGroup): ?>
+ <tr>
+ <td class="count-col">
+ <span class="badge"><?= $hostGroup->hosts_total ?></span>
+ </td>
+ <th>
+ <?= $this->qlink(
+ $hostGroup->hostgroup_alias,
+ $this
+ ->url('monitoring/list/hosts')
+ ->setParams(['hostgroup_name' => $hostGroup->hostgroup_name])
+ ->addFilter($this->filterEditor->getFilter()),
+ ['sort' => 'host_severity'],
+ ['title' => sprintf(
+ $this->translate('List all hosts in the group "%s"'),
+ $hostGroup->hostgroup_alias
+ )]
+ ) ?>
+ </th>
+ <td>
+ <?php
+ $stateBadges = new StateBadges();
+ $stateBadges
+ ->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();
+ ?>
+ </td>
+ <td class="count-col">
+ <?= $this->qlink(
+ $hostGroup->services_total,
+ $this
+ ->url('monitoring/list/services')
+ ->setParams(['hostgroup_name' => $hostGroup->hostgroup_name])
+ ->addFilter($this->filterEditor->getFilter()),
+ ['sort' => 'service_severity'],
+ [
+ 'title' => sprintf(
+ $this->translate('List all services of all hosts in host group "%s"'),
+ $hostGroup->hostgroup_alias
+ ),
+ 'class' => 'badge'
+ ]
+ ) ?>
+ </td>
+ <td>
+ <?php
+ $stateBadges = new StateBadges();
+ $stateBadges
+ ->setUrl('monitoring/list/services')
+ ->setBaseFilter($this->filterEditor->getFilter())
+ ->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_handled, $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();
+ ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+<?php if ($hostGroups->hasMore()): ?>
+ <div class="dont-print action-links">
+ <?= $this->qlink(
+ $this->translate('Show More'),
+ $this->url()->without(array('showCompact', 'limit')),
+ null,
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_next'
+ )
+ ) ?>
+ </div>
+<?php endif ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/hosts.phtml b/modules/monitoring/application/views/scripts/list/hosts.phtml
new file mode 100644
index 0000000..6d7674e
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/hosts.phtml
@@ -0,0 +1,106 @@
+<?php
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Monitoring\Object\Host;
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php if (! $hosts->hasResult()): ?>
+ <p><?= $this->translate('No hosts found matching the filter.') ?></p>
+</div>
+<?php return; endif ?>
+ <table data-base-target="_next"
+ class="table-row-selectable state-table multiselect"
+ data-icinga-multiselect-url="<?= $this->href('monitoring/hosts/show') ?>"
+ data-icinga-multiselect-controllers="<?= $this->href("monitoring/hosts") ?>"
+ data-icinga-multiselect-data="host">
+ <thead class="print-only">
+ <tr>
+ <th><?= $this->translate('State') ?></th>
+ <th><?= $this->translate('Host') ?></th>
+ <?php foreach($this->addColumns as $col): ?>
+ <th><?= $this->escape($col) ?></th>
+ <?php endforeach ?>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach($hosts->peekAhead($this->compact) as $host):
+ $hostStateName = Host::getStateText($host->host_state);
+ $hostLink = $this->href('monitoring/host/show', array('host' => $host->host_name));
+ $hostCheckOverdue = $host->host_next_update < time();?>
+ <tr<?= $hostCheckOverdue ? ' class="state-outdated"' : '' ?>>
+ <td class="state-col state-<?= $hostStateName ?><?= $host->host_handled ? ' handled' : '' ?>">
+ <div class="state-label">
+ <?php if ($hostCheckOverdue): ?>
+ <?= $this->icon('clock', sprintf($this->translate('Overdue %s'), DateFormatter::timeSince($host->host_next_update))) ?>
+ <?php endif ?>
+ <?= Host::getStateText($host->host_state, true) ?>
+ </div>
+ <?php if ((int) $host->host_state !== 99): ?>
+ <div class="state-meta">
+ <?= $this->timeSince($host->host_last_state_change, $this->compact) ?>
+ <?php if ((int) $host->host_state > 0 && (int) $host->host_state_type === 0): ?>
+ <div><?= $this->translate('Soft', 'Soft state') ?> <?= $host->host_attempt ?></div>
+ <?php endif ?>
+ </div>
+ <?php endif ?>
+ </td>
+ <td>
+ <div class="state-header">
+ <?= $this->iconImage()->host($host) ?>
+ <?= $this->qlink(
+ $host->host_display_name,
+ $hostLink,
+ null,
+ array(
+ 'title' => sprintf(
+ $this->translate('Show detailed information for host %s'),
+ $host->host_display_name
+ ),
+ 'class' => 'rowaction'
+ )
+ ) ?>
+ <span class="state-icons"><?= $this->hostFlags($host) ?></span>
+ </div>
+ <p class="overview-plugin-output"><?= $this->pluginOutput($this->ellipsis($host->host_output, 10000), true, $host->host_check_command) ?></p>
+ </td>
+ <?php foreach($this->addColumns as $col): ?>
+ <?php if ($host->$col && preg_match('~^_(host|service)_([a-zA-Z0-9_]+)$~', $col, $m)): ?>
+ <td><?= $this->escape(\Icinga\Module\Monitoring\Object\MonitoredObject::protectCustomVars([$m[2] => $host->$col])[$m[2]]) ?></td>
+ <?php else: ?>
+ <td><?= $this->escape($host->$col) ?></td>
+ <?php endif ?>
+ <?php endforeach ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+<?php if ($hosts->hasMore()): ?>
+ <div class="dont-print action-links">
+ <?= $this->qlink(
+ $this->translate('Show More'),
+ $this->url()->without(array('showCompact', 'limit')),
+ null,
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_next'
+ )
+ ) ?>
+ </div>
+<?php endif ?>
+</div>
+<?php if (! $this->compact): ?>
+<div class="monitoring-statusbar dont-print">
+ <?= $this->render('list/components/hostssummary.phtml') ?>
+ <?= $this->render('list/components/selectioninfo.phtml') ?>
+</div>
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/list/notifications.phtml b/modules/monitoring/application/views/scripts/list/notifications.phtml
new file mode 100644
index 0000000..51ef432
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/notifications.phtml
@@ -0,0 +1,124 @@
+<?php
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\Service;
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php if (! $notifications->hasResult()): ?>
+ <p><?= $this->translate('No notifications found matching the filter.') ?></p>
+</div>
+<?php return; endif ?>
+ <table data-base-target="_next" class="table-row-selectable state-table">
+ <tbody>
+ <?php foreach ($notifications->peekAhead($this->compact) as $notification):
+ if (isset($notification->service_description)) {
+ $isService = true;
+ $stateLabel = Service::getStateText($notification->notification_state, true);
+ $stateName = Service::getStateText($notification->notification_state);
+ } else {
+ $isService = false;
+ $stateLabel = Host::getStateText($notification->notification_state, true);
+ $stateName = Host::getStateText($notification->notification_state);
+ }
+ ?>
+ <tr href="<?= $this->href('monitoring/event/show', ['id' => $notification->id, 'type' => 'notify']) ?>">
+ <td class="state-col state-<?= $stateName ?>">
+ <div class="state-label"><?= $stateLabel ?></div>
+ <div class="state-meta">
+ <?= $this->formatDateTime($notification->notification_timestamp) ?>
+ </div>
+ </td>
+ <td>
+ <div class="state-header">
+ <?php if ($isService) {
+ echo '<span class="service-on">';
+ echo sprintf(
+ $this->translate('%s on %s', 'service on host'),
+ $this->qlink(
+ $notification->service_display_name,
+ 'monitoring/service/show',
+ [
+ 'host' => $notification->host_name,
+ 'service' => $notification->service_description
+ ],
+ [
+ 'title' => sprintf(
+ $this->translate('Show detailed information for service %s on host %s'),
+ $notification->service_display_name,
+ $notification->host_display_name
+ )
+ ]
+ ),
+ $this->qlink(
+ $notification->host_display_name,
+ 'monitoring/host/show',
+ ['host' => $notification->host_name],
+ [
+ 'title' => sprintf(
+ $this->translate('Show detailed information for host %s'),
+ $notification->host_display_name
+ )
+ ]
+ )
+ );
+ echo '</span>';
+ } else {
+ echo $this->qlink(
+ $notification->host_display_name,
+ 'monitoring/host/show',
+ ['host' => $notification->host_name],
+ [
+ 'title' => sprintf(
+ $this->translate('Show detailed information for host %s'),
+ $notification->host_display_name
+ )
+ ]
+ );
+ } ?>
+ <?php if (! $this->contact): ?>
+ <div class="notification-recipient">
+ <?php if ($notification->notification_contact_name): ?>
+ <?= sprintf(
+ $this->translate('Sent to %s'),
+ $this->qlink(
+ $notification->notification_contact_name,
+ 'monitoring/show/contact',
+ array('contact_name' => $notification->notification_contact_name)
+ )
+ ) ?>
+ <?php else: ?>
+ <?= $this->translate('Not sent out to any contact') ?>
+ <?php endif ?>
+ </div>
+ <?php endif ?>
+ </div>
+ <p class="overview-plugin-output"><?= $this->pluginOutput($this->ellipsis($notification->notification_output, 10000), true) ?></p>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+<?php if ($notifications->hasMore()): ?>
+ <div class="action-links">
+ <?= $this->qlink(
+ $this->translate('Show More'),
+ $this->url(isset($notificationsUrl) ? $notificationsUrl : null)->without(array('showCompact', 'limit')),
+ null,
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_next'
+ )
+ ); ?>
+ </div>
+<?php endif ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/servicegrid-flipped.phtml b/modules/monitoring/application/views/scripts/list/servicegrid-flipped.phtml
new file mode 100644
index 0000000..d7b4c78
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/servicegrid-flipped.phtml
@@ -0,0 +1,144 @@
+<?php
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\Url;
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->problemToggle ?>
+ <div class="sort-controls-container">
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content" data-base-target="_next">
+ <?php if (empty($pivotData)): ?>
+ <p><?= $this->translate('No services found matching the filter.') ?></p>
+</div>
+<?php return; endif;
+$serviceFilter = Filter::matchAny();
+foreach ($pivotData as $serviceDescription => $_) {
+ $serviceFilter->orFilter(Filter::where('service_description', $serviceDescription));
+}
+?>
+<table class="service-grid-table">
+ <thead>
+ <tr>
+ <th><?= $this->partial(
+ 'joystickPagination.phtml',
+ 'default',
+ array(
+ 'flippable' => true,
+ 'xAxisPaginator' => $horizontalPaginator,
+ 'yAxisPaginator' => $verticalPaginator
+ )
+ ) ?></th>
+ <?php foreach ($pivotHeader['cols'] as $hostName => $hostAlias): ?>
+ <th class="rotate-45"><div><span><?= $this->qlink(
+ $this->ellipsis($hostAlias, 24),
+ Url::fromPath('monitoring/list/services')->addFilter(
+ Filter::matchAll($serviceFilter, Filter::where('host_name', $hostName))
+ ),
+ null,
+ array('title' => sprintf($this->translate('List all reported services on host %s'), $hostAlias)),
+ false
+ ) ?></span></div></th>
+ <?php endforeach ?>
+ </tr>
+ </thead>
+ <tbody>
+
+ <?php $i = 0 ?>
+ <?php foreach ($pivotHeader['rows'] as $serviceDescription => $serviceDisplayName): ?>
+ <tr>
+ <th><?php
+ $hostFilter = Filter::matchAny();
+ foreach ($pivotData[$serviceDescription] as $hostName => $_) {
+ $hostFilter->orFilter(Filter::where('host_name', $hostName));
+ }
+ echo $this->qlink(
+ $serviceDisplayName,
+ Url::fromPath('monitoring/list/services')->addFilter(
+ Filter::matchAll($hostFilter, Filter::where('service_description', $serviceDescription))
+ ),
+ null,
+ array('title' => sprintf(
+ $this->translate('List all services with the name "%s" on all reported hosts'),
+ $serviceDisplayName
+ ))
+ );
+ ?></th>
+ <?php foreach (array_keys($pivotHeader['cols']) as $hostName): ?>
+ <td><?php
+ $service = $pivotData[$serviceDescription][$hostName];
+ if ($service === null): ?>
+ <span aria-hidden="true">&middot;</span>
+ <?php continue; endif ?>
+ <?php $ariaDescribedById = $this->protectId($service->host_name . '_' . $service->service_description . '_desc') ?>
+ <span class="sr-only" id="<?= $ariaDescribedById ?>">
+ <?= $this->escape($service->service_output) ?>
+ </span>
+ <?= $this->qlink(
+ '',
+ 'monitoring/service/show',
+ array(
+ 'host' => $hostName,
+ 'service' => $serviceDescription
+ ),
+ array(
+ 'aria-describedby' => $ariaDescribedById,
+ 'aria-label' => sprintf(
+ $this->translate('Show detailed information for service %s on host %s'),
+ $service->service_display_name,
+ $service->host_display_name
+ ),
+ 'class' => 'service-grid-link state-' . Service::getStateText($service->service_state) . ($service->service_handled ? ' handled' : ''),
+ 'title' => $service->service_output
+ )
+ ) ?>
+ </td>
+ <?php endforeach ?>
+ <?php if (! $this->compact && $this->horizontalPaginator->getPages()->pageCount > 1): ?>
+ <td>
+ <?php $expandLink = $this->qlink(
+ $this->translate('Load more'),
+ Url::fromRequest(),
+ array(
+ 'limit' => ($this->horizontalPaginator->getItemCountPerPage() + 20)
+ . ','
+ . $this->verticalPaginator->getItemCountPerPage()
+ ),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self'
+ )
+ ) ?>
+ <?= ++$i === (int) ceil(count($pivotHeader['rows']) / 2) ? $expandLink : '' ?>
+ </td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ <?php if (! $this->compact && $this->verticalPaginator->getPages()->pageCount > 1): ?>
+ <tr>
+ <td colspan="<?= count($pivotHeader['cols']) + 1?>" class="service-grid-table-more">
+ <?php echo $this->qlink(
+ $this->translate('Load more'),
+ Url::fromRequest(),
+ array(
+ 'limit' => $this->horizontalPaginator->getItemCountPerPage()
+ . ','
+ . ($this->verticalPaginator->getItemCountPerPage() + 20)
+ ),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self'
+ )
+ ) ?>
+ </td>
+ </tr>
+ <?php endif ?>
+ </tbody>
+</table>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/servicegrid.phtml b/modules/monitoring/application/views/scripts/list/servicegrid.phtml
new file mode 100644
index 0000000..d0ed4bc
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/servicegrid.phtml
@@ -0,0 +1,144 @@
+<?php
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\Url;
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->problemToggle ?>
+ <div class="sort-controls-container">
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content" data-base-target="_next">
+<?php if (empty($pivotData)): ?>
+ <p><?= $this->translate('No services found matching the filter.') ?></p>
+</div>
+<?php return; endif;
+$hostFilter = Filter::matchAny();
+foreach ($pivotData as $hostName => $_) {
+ $hostFilter->orFilter(Filter::where('host_name', $hostName));
+}
+?>
+ <table class="service-grid-table">
+ <thead>
+ <tr>
+ <th><?= $this->partial(
+ 'joystickPagination.phtml',
+ 'default',
+ array(
+ 'flippable' => true,
+ 'xAxisPaginator' => $horizontalPaginator,
+ 'yAxisPaginator' => $verticalPaginator
+ )
+ ) ?></th>
+ <?php foreach ($pivotHeader['cols'] as $serviceDescription => $serviceDisplayName): ?>
+ <th class="rotate-45"><div><span><?= $this->qlink(
+ $this->ellipsis($serviceDisplayName, 24),
+ Url::fromPath('monitoring/list/services')->addFilter(
+ Filter::matchAll($hostFilter, Filter::where('service_description', $serviceDescription))
+ ),
+ null,
+ array('title' => sprintf(
+ $this->translate('List all services with the name "%s" on all reported hosts'),
+ $serviceDisplayName
+ )),
+ false
+ ) ?></span></div></th>
+ <?php endforeach ?>
+ </tr>
+ </thead>
+ <tbody>
+
+ <?php $i = 0 ?>
+ <?php foreach ($pivotHeader['rows'] as $hostName => $hostDisplayName): ?>
+ <tr>
+ <th><?php
+ $serviceFilter = Filter::matchAny();
+ foreach ($pivotData[$hostName] as $serviceName => $_) {
+ $serviceFilter->orFilter(Filter::where('service_description', $serviceName));
+ }
+ echo $this->qlink(
+ $hostDisplayName,
+ Url::fromPath('monitoring/list/services')->addFilter(
+ Filter::matchAll($serviceFilter, Filter::where('host_name', $hostName))
+ ),
+ null,
+ array('title' => sprintf($this->translate('List all reported services on host %s'), $hostDisplayName))
+ );
+ ?></th>
+ <?php foreach (array_keys($pivotHeader['cols']) as $serviceDescription): ?>
+ <td>
+ <?php
+ $service = $pivotData[$hostName][$serviceDescription];
+ if ($service === null): ?>
+ <span aria-hidden="true">&middot;</span>
+ <?php continue; endif ?>
+ <?php $ariaDescribedById = $this->protectId($service->host_name . '_' . $service->service_description . '_desc') ?>
+ <span class="sr-only" id="<?= $ariaDescribedById ?>">
+ <?= $this->escape($service->service_output) ?>
+ </span>
+ <?= $this->qlink(
+ '',
+ 'monitoring/service/show',
+ array(
+ 'host' => $hostName,
+ 'service' => $serviceDescription
+ ),
+ array(
+ 'aria-describedby' => $ariaDescribedById,
+ 'aria-label' => sprintf(
+ $this->translate('Show detailed information for service %s on host %s'),
+ $service->service_display_name,
+ $service->host_display_name
+ ),
+ 'class' => 'service-grid-link state-' . Service::getStateText($service->service_state) . ($service->service_handled ? ' handled' : ''),
+ 'title' => $service->service_output
+ )
+ ) ?>
+ </td>
+ <?php endforeach ?>
+ <?php if (! $this->compact && $this->horizontalPaginator->getPages()->pageCount > 1): ?>
+ <td>
+ <?php $expandLink = $this->qlink(
+ $this->translate('Load more'),
+ Url::fromRequest(),
+ array(
+ 'limit' => (
+ $this->horizontalPaginator->getItemCountPerPage() + 20) . ','
+ . $this->verticalPaginator->getItemCountPerPage()
+ ),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self'
+ )
+ ) ?>
+ <?= ++$i === (int) (count($pivotHeader['rows']) / 2) ? $expandLink : '' ?>
+ </td>
+ <?php endif ?>
+ </tr>
+ <?php endforeach ?>
+ <?php if (! $this->compact && $this->verticalPaginator->getPages()->pageCount > 1): ?>
+ <tr>
+ <td colspan="<?= count($pivotHeader['cols']) + 1?>" class="service-grid-table-more">
+ <?php echo $this->qlink(
+ $this->translate('Load more'),
+ Url::fromRequest(),
+ array(
+ 'limit' => $this->horizontalPaginator->getItemCountPerPage() . ',' .
+ ($this->verticalPaginator->getItemCountPerPage() + 20)
+ ),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self'
+ )
+ ) ?>
+ </td>
+ </tr>
+ <?php endif ?>
+ </tbody>
+ </table>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/servicegroup-grid.phtml b/modules/monitoring/application/views/scripts/list/servicegroup-grid.phtml
new file mode 100644
index 0000000..5ea6d17
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/servicegroup-grid.phtml
@@ -0,0 +1,217 @@
+<?php if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <div class="sort-controls-container">
+ <?= $this->sortBox ?>
+ <a href="<?= $this->href('monitoring/list/servicegroups')->addFilter($this->filterEditor->getFilter()) ?>" class="grid-toggle-link"
+ title="<?= $this->translate('Toogle grid view mode') ?>">
+ <?= $this->icon('th-list', null, ['class' => '-inactive']) ?>
+ <?= $this->icon('th-thumb-empty', null, ['class' => '-active']) ?>
+ </a>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content" data-base-target="_next">
+<?php /** @var \Icinga\Module\Monitoring\DataView\Servicegroup $serviceGroups */
+if (! $serviceGroups->hasResult()): ?>
+ <p><?= $this->translate('No service groups found matching the filter.') ?></p>
+</div>
+<?php return; endif ?>
+<div class="group-grid">
+<?php foreach ($serviceGroups as $serviceGroup): ?>
+ <div class="group-grid-cell">
+ <?php if ($serviceGroup->services_critical_unhandled > 0): ?>
+ <?= $this->qlink(
+ $serviceGroup->services_critical_unhandled,
+ $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()),
+ [
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'service_handled' => 0,
+ 'service_state' => 2
+ ],
+ [
+ 'class' => 'state-critical',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %s service that is currently in state CRITICAL in service group "%s"',
+ 'List %s services which are currently in state CRITICAL in service group "%s"',
+ $serviceGroup->services_critical_unhandled
+ ),
+ $serviceGroup->services_critical_unhandled,
+ $serviceGroup->servicegroup_alias
+ )
+ ]
+ ) ?>
+ <?php elseif ($serviceGroup->services_warning_unhandled > 0): ?>
+ <?= $this->qlink(
+ $serviceGroup->services_warning_unhandled,
+ $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()),
+ [
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'service_handled' => 0,
+ 'service_state' => 1
+ ],
+ [
+ 'class' => 'state-warning',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %s service that is currently in state WARNING in service group "%s"',
+ 'List %s services which are currently in state WARNING in service group "%s"',
+ $serviceGroup->services_warning_unhandled
+ ),
+ $serviceGroup->services_warning_unhandled,
+ $serviceGroup->servicegroup_alias
+ )
+ ]
+ ) ?>
+ <?php elseif ($serviceGroup->services_unknown_unhandled > 0): ?>
+ <?= $this->qlink(
+ $serviceGroup->services_unknown_unhandled,
+ $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()),
+ [
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'service_handled' => 0,
+ 'service_state' => 3
+ ],
+ [
+ 'class' => 'state-unknown',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %s service that is currently in state UNKNOWN in service group "%s"',
+ 'List %s services which are currently in state UNKNOWN in service group "%s"',
+ $serviceGroup->services_unknown_unhandled
+ ),
+ $serviceGroup->services_unknown_unhandled,
+ $serviceGroup->servicegroup_alias
+ )
+ ]
+ ) ?>
+ <?php elseif ($serviceGroup->services_critical_handled > 0): ?>
+ <?= $this->qlink(
+ $serviceGroup->services_critical_handled,
+ $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()),
+ [
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'service_handled' => 1,
+ 'service_state' => 2
+ ],
+ [
+ 'class' => 'state-critical handled',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %s service that is currently in state CRITICAL (Acknowledged) in service group "%s"',
+ 'List %s services which are currently in state CRITICAL (Acknowledged) in service group "%s"',
+ $serviceGroup->services_critical_handled
+ ),
+ $serviceGroup->services_critical_handled,
+ $serviceGroup->servicegroup_alias
+ )
+ ]
+ ) ?>
+ <?php elseif ($serviceGroup->services_warning_handled > 0): ?>
+ <?= $this->qlink(
+ $serviceGroup->services_warning_handled,
+ $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()),
+ [
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'service_handled' => 1,
+ 'service_state' => 1
+ ],
+ [
+ 'class' => 'state-warning handled',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %s service that is currently in state WARNING (Acknowledged) in service group "%s"',
+ 'List %s services which are currently in state WARNING (Acknowledged) in service group "%s"',
+ $serviceGroup->services_warning_handled
+ ),
+ $serviceGroup->services_warning_handled,
+ $serviceGroup->servicegroup_alias
+ )
+ ]
+ ) ?>
+ <?php elseif ($serviceGroup->services_unknown_handled > 0): ?>
+ <?= $this->qlink(
+ $serviceGroup->services_unknown_handled,
+ $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()),
+ [
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'service_handled' => 1,
+ 'service_state' => 3
+ ],
+ [
+ 'class' => 'state-unknown handled',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %s service that is currently in state UNKNOWN (Acknowledged) in service group "%s"',
+ 'List %s services which are currently in state UNKNOWN (Acknowledged) in service group "%s"',
+ $serviceGroup->services_unknown_handled
+ ),
+ $serviceGroup->services_unknown_handled,
+ $serviceGroup->servicegroup_alias
+ )
+ ]
+ ) ?>
+ <?php elseif ($serviceGroup->services_pending > 0): ?>
+ <?= $this->qlink(
+ $serviceGroup->services_pending,
+ $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()),
+ [
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'service_state' => 99
+ ],
+ [
+ 'class' => 'state-pending',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %s service that is currenlty in state PENDING in service group "%s"',
+ 'List %s services which are currently in state PENDING in service group "%s"',
+ $serviceGroup->services_pending
+ ),
+ $serviceGroup->services_pending,
+ $serviceGroup->servicegroup_alias
+ )
+ ]
+ ) ?>
+ <?php elseif ($serviceGroup->services_ok > 0): ?>
+ <?= $this->qlink(
+ $serviceGroup->services_ok,
+ $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()),
+ [
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'service_state' => 0
+ ],
+ [
+ 'class' => 'state-ok',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %s service that is currently in state OK in service group "%s"',
+ 'List %s services which are currently in state OK in service group "%s"',
+ $serviceGroup->services_ok
+ ),
+ $serviceGroup->services_ok,
+ $serviceGroup->servicegroup_alias
+ )
+ ]
+ ) ?>
+ <?php else: ?>
+ <div class="state-none">
+ 0
+ </div>
+ <?php endif ?>
+ <?= $this->qlink(
+ $serviceGroup->servicegroup_alias,
+ $this->url('monitoring/list/servicegrid')->addFilter($this->filterEditor->getFilter()),
+ ['servicegroup_name' => $serviceGroup->servicegroup_name],
+ [
+ 'title' => sprintf(
+ $this->translate('List all services in the group "%s"'),
+ $serviceGroup->servicegroup_alias
+ )
+ ]
+ ) ?>
+ </div>
+<?php endforeach ?>
+</div>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/servicegroups.phtml b/modules/monitoring/application/views/scripts/list/servicegroups.phtml
new file mode 100644
index 0000000..c915b30
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/servicegroups.phtml
@@ -0,0 +1,184 @@
+<?php use Icinga\Module\Monitoring\Web\Widget\StateBadges;
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ <a href="<?= $this->href('monitoring/list/servicegroup-grid')->addFilter(clone $this->filterEditor->getFilter()) ?>" class="grid-toggle-link"
+ title="<?= $this->translate('Toogle grid view mode') ?>">
+ <?= $this->icon('th-list', null, ['class' => '-active']) ?>
+ <?= $this->icon('th-thumb-empty', null, ['class' => '-inactive']) ?>
+ </a>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php if (! $serviceGroups->hasResult()): ?>
+ <p><?= $this->translate('No service groups found matching the filter.') ?></p>
+</div>
+<?php return; endif ?>
+ <table class="table-row-selectable common-table" data-base-target="_next">
+ <thead>
+ <tr>
+ <th></th>
+ <th><?= $this->translate('Service Group') ?></th>
+ <th><?= $this->translate('Service States') ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($serviceGroups->peekAhead($this->compact) as $serviceGroup): ?>
+ <tr>
+ <td class="count-col">
+ <span class="badge"><?= $serviceGroup->services_total ?></span>
+ </td>
+ <th>
+ <?= $this->qlink(
+ $serviceGroup->servicegroup_alias,
+ $this
+ ->url('monitoring/list/services')
+ ->setParams(['servicegroup_name' => $serviceGroup->servicegroup_name])
+ ->addFilter($this->filterEditor->getFilter()),
+ ['sort' => 'service_severity'],
+ ['title' => sprintf($this->translate('List all services in the group "%s"'), $serviceGroup->servicegroup_alias)]
+ ) ?>
+ </th>
+ <td>
+ <?php
+ $stateBadges = new StateBadges();
+ $stateBadges
+ ->setUrl('monitoring/list/services')
+ ->setBaseFilter($this->filterEditor->getFilter())
+ ->add(
+ StateBadges::STATE_OK,
+ $serviceGroup->services_ok,
+ array(
+ 'service_state' => 0,
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'sort' => 'service_severity'
+ ),
+ 'List %s service that is currently in state OK in service group "%s"',
+ 'List %s services which are currently in state OK in service group "%s"',
+ array($serviceGroup->services_ok, $serviceGroup->servicegroup_alias)
+ )
+ ->add(
+ StateBadges::STATE_CRITICAL,
+ $serviceGroup->services_critical_unhandled,
+ array(
+ 'service_state' => 2,
+ 'service_acknowledged' => 0,
+ 'service_in_downtime' => 0,
+ 'host_problem' => 0,
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'sort' => 'service_severity'
+ ),
+ 'List %s service that is currently in state CRITICAL in service group "%s"',
+ 'List %s services which are currently in state CRITICAL in service group "%s"',
+ array($serviceGroup->services_critical_unhandled, $serviceGroup->servicegroup_alias)
+ )
+ ->add(
+ StateBadges::STATE_CRITICAL_HANDLED,
+ $serviceGroup->services_critical_handled,
+ array(
+ 'service_state' => 2,
+ 'service_handled' => 1,
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'sort' => 'service_severity'
+ ),
+ 'List %s service that is currently in state CRITICAL (Acknowledged) in service group "%s"',
+ 'List %s services which are currently in state CRITICAL (Acknowledged) in service group "%s"',
+ array($serviceGroup->services_critical_handled, $serviceGroup->servicegroup_alias)
+ )
+ ->add(
+ StateBadges::STATE_UNKNOWN,
+ $serviceGroup->services_unknown_unhandled,
+ array(
+ 'service_state' => 3,
+ 'service_acknowledged' => 0,
+ 'service_in_downtime' => 0,
+ 'host_problem' => 0,
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'sort' => 'service_severity'
+ ),
+ 'List %s service that is currently in state UNKNOWN in service group "%s"',
+ 'List %s services which are currently in state UNKNOWN in service group "%s"',
+ array($serviceGroup->services_unknown_unhandled, $serviceGroup->servicegroup_alias)
+ )
+ ->add(
+ StateBadges::STATE_UNKNOWN_HANDLED,
+ $serviceGroup->services_unknown_handled,
+ array(
+ 'service_state' => 3,
+ 'service_handled' => 1,
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'sort' => 'service_severity'
+ ),
+ 'List %s service that is currently in state UNKNOWN (Acknowledged) in service group "%s"',
+ 'List %s services which are currently in state UNKNOWN (Acknowledged) in service group "%s"',
+ array($serviceGroup->services_unknown_handled, $serviceGroup->servicegroup_alias)
+
+ )
+ ->add(
+ StateBadges::STATE_WARNING,
+ $serviceGroup->services_warning_unhandled,
+ array(
+ 'service_state' => 1,
+ 'service_acknowledged' => 0,
+ 'service_in_downtime' => 0,
+ 'host_problem' => 0,
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'sort' => 'service_severity'
+ ),
+ 'List %s service that is currently in state WARNING in service group "%s"',
+ 'List %s services which are currently in state WARNING in service group "%s"',
+ array($serviceGroup->services_warning_unhandled, $serviceGroup->servicegroup_alias)
+ )
+ ->add(
+ StateBadges::STATE_WARNING_HANDLED,
+ $serviceGroup->services_warning_handled,
+ array(
+ 'service_state' => 1,
+ 'service_handled' => 1,
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'sort' => 'service_severity'
+ ),
+ 'List %s service that is currently in state WARNING (Acknowledged) in service group "%s"',
+ 'List %s services which are currently in state WARNING (Acknowledged) in service group "%s"',
+ array($serviceGroup->services_warning_handled, $serviceGroup->servicegroup_alias)
+ )
+ ->add(
+ StateBadges::STATE_PENDING,
+ $serviceGroup->services_pending,
+ array(
+ 'service_state' => 99,
+ 'servicegroup_name' => $serviceGroup->servicegroup_name,
+ 'sort' => 'service_severity'
+ ),
+ 'List %s service that is currenlty in state PENDING in service group "%s"',
+ 'List %s services which are currently in state PENDING in service group "%s"',
+ array($serviceGroup->services_pending, $serviceGroup->servicegroup_alias)
+ );
+ echo $stateBadges->render();
+ ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+<?php if ($serviceGroups->hasMore()): ?>
+<div class="dont-print action-links">
+ <?= $this->qlink(
+ $this->translate('Show More'),
+ $this->url()->without(array('showCompact', 'limit')),
+ null,
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_next'
+ )
+ ) ?>
+</div>
+<?php endif ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/list/services.phtml b/modules/monitoring/application/views/scripts/list/services.phtml
new file mode 100644
index 0000000..b2088e9
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/list/services.phtml
@@ -0,0 +1,161 @@
+<?php
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\Service;
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content">
+<?php if (! $services->hasResult()): ?>
+ <p><?= $this->translate('No services found matching the filter.') ?></p>
+</div>
+<?php return; endif ?>
+ <table data-base-target="_next"
+ class="table-row-selectable state-table multiselect<?php if ($this->compact): ?> compact<?php endif ?>"
+ data-icinga-multiselect-url="<?= $this->href('monitoring/services/show') ?>"
+ data-icinga-multiselect-controllers="<?= $this->href('monitoring/services') ?>"
+ data-icinga-multiselect-data="service,host">
+ <thead class="print-only">
+ <tr>
+ <th><?= $this->translate('State') ?></th>
+ <th><?= $this->translate('Service') ?></th>
+ <?php foreach($this->addColumns as $col): ?>
+ <th><?= $this->escape($col) ?></th>
+ <?php endforeach ?>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($services->peekAhead($this->compact) as $service):
+ $serviceLink = $this->href(
+ 'monitoring/service/show',
+ array(
+ 'host' => $service->host_name,
+ 'service' => $service->service_description
+ )
+ );
+ $hostLink = $this->href(
+ 'monitoring/host/show',
+ array(
+ 'host' => $service->host_name,
+ )
+ );
+ $serviceStateName = Service::getStateText($service->service_state);
+ $serviceCheckOverdue = $service->service_next_update < time(); ?>
+ <tr<?= $serviceCheckOverdue ? ' class="state-outdated"' : '' ?>>
+ <td class="state-col state-<?= $serviceStateName ?><?= $service->service_handled ? ' handled' : '' ?>">
+ <div class="state-label">
+ <?php if ($serviceCheckOverdue): ?>
+ <?= $this->icon('clock', sprintf($this->translate('Overdue %s'), DateFormatter::timeSince($service->service_next_update))) ?>
+ <?php endif ?>
+ <?= Service::getStateText($service->service_state, true) ?>
+ </div>
+ <?php if ((int) $service->service_state !== 99): ?>
+ <div class="state-meta">
+ <?= $this->timeSince($service->service_last_state_change, $this->compact) ?>
+ <?php if ((int) $service->service_state > 0 && (int) $service->service_state_type === 0): ?>
+ <div><?= $this->translate('Soft', 'Soft state') ?> <?= $service->service_attempt ?></div>
+ <?php endif ?>
+ </div>
+ <?php endif ?>
+ </td>
+
+ <td>
+ <div class="state-header">
+ <span class="service-on">
+ <?= $this->iconImage()->service($service) ?>
+ <?php
+ if ($this->showHost) {
+ echo sprintf(
+ $this->translate('%s on %s', 'service on host'),
+ $this->qlink(
+ $service->service_display_name,
+ $serviceLink,
+ null,
+ array(
+ 'title' => sprintf(
+ $this->translate('Show detailed information for service %s on host %s'),
+ $service->service_display_name,
+ $service->host_display_name
+ ),
+ 'class' => 'rowaction'
+ )
+ ),
+ $this->qlink(
+ $service->host_display_name
+ . ($service->host_state != 0 ? ' (' . Host::getStateText($service->host_state, true) . ')' : ''),
+ $hostLink,
+ null,
+ [
+ 'title' => sprintf(
+ $this->translate('Show detailed information for host %s'),
+ $service->host_display_name
+ )
+ ]
+ )
+ );
+ } else {
+ echo $this->qlink(
+ $service->service_display_name,
+ $serviceLink,
+ null,
+ array(
+ 'title' => sprintf(
+ $this->translate('Show detailed information for service %s on host %s'),
+ $service->service_display_name,
+ $service->host_display_name
+ ),
+ 'class' => 'rowaction'
+ )
+ );
+ }
+ ?>
+ </span>
+ <span class="state-icons"><?= $this->serviceFlags($service) ?></span>
+ </div>
+ <div class="overview-plugin-output-container">
+ <div class="overview-performance-data">
+ <?= $this->perfdata($service->service_perfdata, true, 5) ?>
+ </div>
+ <p class="overview-plugin-output"><?= $this->pluginOutput($this->ellipsis($service->service_output, 10000), true, $service->service_check_command) ?></p>
+ </div>
+ </td>
+ <?php foreach($this->addColumns as $col): ?>
+ <?php if ($service->$col && preg_match('~^_(host|service)_([a-zA-Z0-9_]+)$~', $col, $m)): ?>
+ <td><?= $this->escape(\Icinga\Module\Monitoring\Object\MonitoredObject::protectCustomVars([$m[2] => $service->$col])[$m[2]]) ?></td>
+ <?php else: ?>
+ <td><?= $this->escape($service->$col) ?></td>
+ <?php endif ?>
+ <?php endforeach ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+<?php if ($services->hasMore()): ?>
+<div class="dont-print action-links">
+ <?= $this->qlink(
+ $this->translate('Show More'),
+ $this->url()->without(array('showCompact', 'limit')),
+ null,
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_next'
+ )
+ ) ?>
+</div>
+<?php endif ?>
+</div>
+<?php if (! $this->compact): ?>
+<div class="monitoring-statusbar dont-print">
+ <?= $this->render('list/components/servicesummary.phtml') ?>
+ <?= $this->render('list/components/selectioninfo.phtml') ?>
+</div>
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/object/detail-history.phtml b/modules/monitoring/application/views/scripts/object/detail-history.phtml
new file mode 100644
index 0000000..692d3e4
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/object/detail-history.phtml
@@ -0,0 +1,13 @@
+<?php
+
+if (! $this->compact): ?>
+<div class="controls separated">
+ <?= $this->tabs ?>
+<?php if ($object->type === 'service') {
+ echo $this->render('partials/object/service-header.phtml');
+} else {
+ echo $this->render('partials/object/host-header.phtml');
+} ?>
+</div>
+<?php endif ?>
+<?= $this->render('partials/event-history.phtml') ?>
diff --git a/modules/monitoring/application/views/scripts/object/detail-tabhook.phtml b/modules/monitoring/application/views/scripts/object/detail-tabhook.phtml
new file mode 100644
index 0000000..abcfcc1
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/object/detail-tabhook.phtml
@@ -0,0 +1,21 @@
+<?php
+
+if (! $this->compact): ?>
+<div class="controls separated">
+ <?= $this->tabs ?>
+<?php
+if ($this->header === true) {
+ if ($object->type === 'service') {
+ echo $this->render('partials/object/service-header.phtml');
+ } else {
+ echo $this->render('partials/object/host-header.phtml');
+ }
+} elseif ($this->header !== false) {
+ echo $this->header;
+}
+?>
+</div>
+<?php endif ?>
+<div class="content">
+ <?= $this->content ?>
+</div> \ No newline at end of file
diff --git a/modules/monitoring/application/views/scripts/partials/command/object-command-form.phtml b/modules/monitoring/application/views/scripts/partials/command/object-command-form.phtml
new file mode 100644
index 0000000..b4e5a9c
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/command/object-command-form.phtml
@@ -0,0 +1,18 @@
+<?php use Icinga\Data\Filter\Filter; ?>
+<div class="controls">
+<?php if (! $this->compact): ?>
+ <?= $this->tabs ?>
+<?php endif ?>
+<?php if ($object->getType() === $object::TYPE_HOST) {
+ echo $this->render('partials/object/host-header.phtml');
+ $this->baseFilter = Filter::where('host', $object->host_name);
+ $this->stats = $object->stats;
+ echo $this->render('list/components/servicesummary.phtml');
+} else {
+ echo $this->render('partials/object/service-header.phtml');
+} ?>
+<?= $this->render('partials/object/quick-actions.phtml') ?>
+</div>
+<div class="content object-command">
+ <?= $form ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/partials/command/objects-command-form.phtml b/modules/monitoring/application/views/scripts/partials/command/objects-command-form.phtml
new file mode 100644
index 0000000..8d241ee
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/command/objects-command-form.phtml
@@ -0,0 +1,15 @@
+<div class="controls">
+<?php if (! $this->compact): ?>
+ <?= $tabs ?>
+<?php endif ?>
+<?php if (isset($serviceStates)): ?>
+ <?= $this->render('list/components/servicesummary.phtml') ?>
+ <?= $this->render('partials/service/objects-header.phtml') ?>
+<?php else: ?>
+ <?= $this->render('list/components/hostssummary.phtml') ?>
+ <?= $this->render('partials/host/objects-header.phtml') ?>
+<?php endif ?>
+</div>
+<div class="content objects-command">
+ <?= $form ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/partials/comment/comment-description.phtml b/modules/monitoring/application/views/scripts/partials/comment/comment-description.phtml
new file mode 100644
index 0000000..f35680c
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/comment/comment-description.phtml
@@ -0,0 +1,24 @@
+<?php
+switch ($comment->type) {
+ case 'flapping':
+ $icon = 'flapping';
+ $title = $this->translate('Flapping');
+ $tooltip = $this->translate('Comment was caused by a flapping host or service');
+ break;
+ case 'comment':
+ $icon = 'user';
+ $title = $this->translate('User Comment');
+ $tooltip = $this->translate('Comment was created by an user');
+ break;
+ case 'downtime':
+ $icon = 'plug';
+ $title = $this->translate('Downtime');
+ $tooltip = $this->translate('Comment was caused by a downtime');
+ break;
+ case 'ack':
+ $icon = 'ok';
+ $title = $this->translate('Acknowledgement');
+ $tooltip = $this->translate('Comment was caused by an acknowledgement');
+ break;
+}
+echo $this->icon($icon, $tooltip, array('class' => 'large-icon'));
diff --git a/modules/monitoring/application/views/scripts/partials/comment/comment-detail.phtml b/modules/monitoring/application/views/scripts/partials/comment/comment-detail.phtml
new file mode 100644
index 0000000..c603d3c
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/comment/comment-detail.phtml
@@ -0,0 +1,82 @@
+<div class="comment-author">
+<?php if ($comment->objecttype === 'service') {
+ echo '<span class="service-on">';
+ echo sprintf(
+ $this->translate('%s on %s', 'service on host'),
+ $this->qlink(
+ $comment->service_display_name,
+ 'monitoring/service/show',
+ [
+ 'host' => $comment->host_name,
+ 'service' => $comment->service_description
+ ],
+ [
+ 'title' => sprintf(
+ $this->translate('Show detailed information for service %s on host %s'),
+ $comment->service_display_name,
+ $comment->host_display_name
+ )
+ ]
+ ),
+ $this->qlink(
+ $comment->host_display_name,
+ 'monitoring/host/show',
+ ['host' => $comment->host_name],
+ [
+ 'title' => sprintf(
+ $this->translate('Show detailed information for host %s'),
+ $comment->host_display_name
+ )
+ ]
+ )
+ );
+ echo '</span>';
+} else {
+ echo $this->qlink(
+ $comment->host_display_name,
+ 'monitoring/host/show',
+ array('host' => $comment->host_name),
+ array(
+ 'title' => sprintf(
+ $this->translate('Show detailed information for this comment about host %s'),
+ $comment->host_display_name
+ )
+ )
+ );
+} ?>
+ <span class="comment-time">
+ <?= $this->translate('by') ?>
+ <?= $this->escape($comment->author) ?>
+ <?= $this->timeAgo($comment->timestamp) ?>
+ </span>
+ <span class="comment-icons" data-base-target="_self">
+ <?= $comment->persistent ? $this->icon('attach', 'This comment is persistent') : '' ?>
+ <?= $comment->expiration ? $this->icon('clock', sprintf(
+ $this->translate('This comment expires on %s at %s'),
+ $this->formatDate($comment->expiration),
+ $this->formatTime($comment->expiration)
+ )) : '' ?>
+ <?php if (isset($delCommentForm)) {
+ // Form is unset if the current user lacks the respective permission
+ $uniqId = uniqid();
+ $buttonId = 'delete-comment-' . $uniqId;
+ $textId = 'comment-' . $uniqId;
+ $deleteButton = clone $delCommentForm;
+ /** @var \Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentCommandForm $deleteButton */
+ $deleteButton->setAttrib('class', $deleteButton->getAttrib('class') . ' remove-action dont-print');
+ $deleteButton->populate(
+ array(
+ 'comment_id' => $comment->id,
+ 'comment_is_service' => isset($comment->service_description),
+ 'comment_name' => $comment->name
+ )
+ );
+ $deleteButton->getElement('btn_submit')
+ ->setAttrib('aria-label', $this->translate('Delete comment'))
+ ->setAttrib('id', $buttonId)
+ ->setAttrib('aria-describedby', $buttonId . ' ' . $textId);
+ echo $deleteButton;
+ } ?>
+ </span>
+</div>
+<?= $this->nl2br($this->markdownLine($comment->comment, isset($textId) ? ['id' => $textId, 'class' => 'caption'] : [ 'class' => 'caption'])) ?>
diff --git a/modules/monitoring/application/views/scripts/partials/comment/comment-header.phtml b/modules/monitoring/application/views/scripts/partials/comment/comment-header.phtml
new file mode 100644
index 0000000..4472479
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/comment/comment-header.phtml
@@ -0,0 +1,10 @@
+<table>
+ <tr>
+ <td class="icon-col">
+ <?= $this->render('partials/comment/comment-description.phtml') ?>
+ </td>
+ <td>
+ <?= $this->render('partials/comment/comment-detail.phtml') ?>
+ </td>
+ </tr>
+</table>
diff --git a/modules/monitoring/application/views/scripts/partials/comment/comments-header.phtml b/modules/monitoring/application/views/scripts/partials/comment/comments-header.phtml
new file mode 100644
index 0000000..c4c92da
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/comment/comments-header.phtml
@@ -0,0 +1,32 @@
+<table>
+ <tbody>
+ <?php
+ foreach ($comments as $i => $comment):
+ if ($i === 5) {
+ break;
+ }
+ ?>
+ <tr>
+ <td class="icon-col">
+ <?= $this->partial('partials/comment/comment-description.phtml', array('comment' => $comment)) ?>
+ </td>
+ <td>
+ <?= $this->partial('partials/comment/comment-detail.phtml', array('comment' => $comment)) ?>
+ </td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+</table>
+<?php if ($comments->count() > 5): ?>
+<p>
+ <?= $this->qlink(
+ sprintf($this->translate('List all %d comments'), $comments->count()),
+ $listAllLink,
+ null,
+ array(
+ 'data-base-target' => '_next',
+ 'icon' => 'down-open'
+ )
+ ) ?>
+</p>
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/partials/downtime/downtime-header.phtml b/modules/monitoring/application/views/scripts/partials/downtime/downtime-header.phtml
new file mode 100644
index 0000000..dae6caa
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/downtime/downtime-header.phtml
@@ -0,0 +1,101 @@
+<td class="state-col state-<?= $stateName; ?><?= $downtime->is_in_effect ? ' handled' : ''; ?>">
+ <?php if ($downtime->start <= time() && ! $downtime->is_in_effect): ?>
+ <div class="state-label"><?= $this->translate('ENDS', 'Downtime status'); ?></div>
+ <div class="state-meta"><?= $this->timeUntil($downtime->is_flexible ? $downtime->scheduled_end : $downtime->end, $this->compact, true) ?></div>
+ <?php else: ?>
+ <div class="state-label"><?= $downtime->is_in_effect ? $this->translate('EXPIRES', 'Downtime status') : $this->translate('STARTS', 'Downtime status'); ?></div>
+ <div class="state-meta"><?= $this->timeUntil($downtime->is_in_effect ? $downtime->end : $downtime->start, $this->compact, true) ?></div>
+ <?php endif; ?>
+</td>
+<td>
+ <div class="comment-author">
+ <?php if ($isService) {
+ echo '<span class="service-on">';
+ echo sprintf(
+ $this->translate('%s on %s', 'service on host'),
+ $this->qlink(
+ $downtime->service_display_name,
+ 'monitoring/service/show',
+ [
+ 'host' => $downtime->host_name,
+ 'service' => $downtime->service_description
+ ],
+ [
+ 'title' => sprintf(
+ $this->translate('Show detailed information for service %s on host %s'),
+ $downtime->service_display_name,
+ $downtime->host_display_name
+ )
+ ]
+ ),
+ $this->qlink(
+ $downtime->host_display_name,
+ 'monitoring/host/show',
+ ['host' => $downtime->host_name],
+ [
+ 'title' => sprintf(
+ $this->translate('Show detailed information for host %s'),
+ $downtime->host_display_name
+ )
+ ]
+ )
+ );
+ echo '</span>';
+ } else {
+ echo $this->qlink(
+ $downtime->host_display_name,
+ 'monitoring/host/show',
+ array('host' => $downtime->host_name, 'downtime_id' => $downtime->id),
+ array(
+ 'title' => sprintf(
+ $this->translate('Show detailed information for this downtime scheduled for host %s'),
+ $downtime->host_display_name
+ )
+ )
+ );
+ } ?>
+ <span class="comment-time">
+ <?= $this->escape(sprintf(
+ $downtime->is_flexible
+ ? $this->translate('Flexible downtime by %s')
+ : $this->translate('Fixed downtime by %s'),
+ $downtime->author_name
+ )) ?>
+ </span>
+ <?php if (! $downtime->is_in_effect && $downtime->start >= time()): ?>
+ <span><?= sprintf($this->translate('expires %s'), $this->timeUntil($downtime->is_flexible ? $downtime->scheduled_end : $downtime->end, false, true)) ?></span>
+ <?php endif ?>
+ <span class="comment-icons">
+ <?php if ($downtime->is_flexible): ?>
+ <?= $this->icon('magic', $this->translate('This downtime is flexible')); ?>
+ <?php endif ?>
+
+ <?php if ($downtime->is_in_effect): ?>
+ <?= $this->icon('plug', $this->translate('This downtime is in effect')); ?>
+ <?php endif ?>
+
+ <?php if (isset($delDowntimeForm)) {
+ // Form is unset if the current user lacks the respective permission
+ $uniqId = uniqid();
+ $buttonId = 'delete-downtime-' . $uniqId;
+ $textId = 'downtime-' . $uniqId;
+ $deleteButton = clone $delDowntimeForm;
+ /** @var \Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimeCommandForm $deleteButton */
+ $deleteButton->setAttrib('class', $deleteButton->getAttrib('class') . ' remove-action dont-print');
+ $deleteButton->populate(
+ array(
+ 'downtime_id' => $downtime->id,
+ 'downtime_is_service' => isset($downtime->service_description),
+ 'downtime_name' => $downtime->name
+ )
+ );
+ $deleteButton->getElement('btn_submit')
+ ->setAttrib('aria-label', $this->translate('Delete downtime'))
+ ->setAttrib('id', $buttonId)
+ ->setAttrib('aria-describedby', $buttonId . ' ' . $textId);
+ echo $deleteButton;
+ } ?>
+ </span>
+ </div>
+ <?= $this->nl2br($this->markdown($downtime->comment, isset($textId) ? ['id' => $textId] : null)) ?>
+</td>
diff --git a/modules/monitoring/application/views/scripts/partials/downtime/downtimes-header.phtml b/modules/monitoring/application/views/scripts/partials/downtime/downtimes-header.phtml
new file mode 100644
index 0000000..e2582c1
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/downtime/downtimes-header.phtml
@@ -0,0 +1,40 @@
+<?php
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\Service;
+?>
+<table class="state-table common-table" data-base-target="_next">
+ <tbody>
+ <?php
+ foreach ($this->downtimes as $i => $downtime):
+ if ($i > 5) {
+ break;
+ }
+ if ($downtime->objecttype === 'service') {
+ $this->isService = true;
+ $this->stateName = Service::getStateText($downtime->service_state);
+ } else {
+ $this->isService = false;
+ $this->stateName = Host::getStateText($downtime->host_state);
+ }
+ $this->downtime = $downtime;
+ $this->displayComment = false;
+ ?>
+ <tr>
+ <?= $this->render('partials/downtime/downtime-header.phtml') ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+</table>
+<?php if ($downtimes->count() > 5): ?>
+<p>
+ <?= $this->qlink(
+ sprintf($this->translate('List all %d downtimes'), $downtimes->count()),
+ $listAllLink,
+ null,
+ array(
+ 'data-base-target' => '_next',
+ 'icon' => 'down-open'
+ )
+ ) ?>
+</p>
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/partials/event-history.phtml b/modules/monitoring/application/views/scripts/partials/event-history.phtml
new file mode 100644
index 0000000..b81c95d
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/event-history.phtml
@@ -0,0 +1,267 @@
+<?php
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\Url;
+use Icinga\Web\UrlParams;
+
+function contactsLink($match, $view) {
+ $links = array();
+ foreach (preg_split('/,\s/', $match[1]) as $contact) {
+ $links[] = $view->qlink(
+ $contact,
+ 'monitoring/show/contact',
+ array('contact_name' => $contact),
+ array('title' => sprintf($view->translate('Show detailed information about %s'), $contact))
+ );
+ }
+ return '[' . implode(', ', $links) . ']';
+}
+
+$self = $this;
+
+$url = $this->url();
+$limit = (int) $url->getParam('limit', 25);
+if (! $url->hasParam('page') || ($page = (int) $url->getParam('page')) < 1) {
+ $page = 1;
+}
+
+/** @var \Icinga\Module\Monitoring\DataView\EventHistory $history */
+$history->limit($limit * $page);
+?>
+<div class="content">
+<?php
+$dateFormatter = new IntlDateFormatter(setlocale(LC_TIME, 0), IntlDateFormatter::FULL, IntlDateFormatter::NONE);
+$lastDate = null;
+$flappingMsg = $this->translate('Flapping with a %.2f%% state change rate');
+$rowAction = Url::fromPath('monitoring/event/show');
+?>
+ <?php foreach ($history->peekAhead() as $event): ?>
+<?php if ($lastDate === null): ?>
+ <table class="table-row-selectable state-table" data-base-target="_next">
+ <tbody>
+<?php endif;
+ $icon = '';
+ $iconTitle = null;
+ $isService = isset($event->service_description);
+ $msg = $event->output;
+ $stateName = 'no-state';
+
+ $rowAction->setParams(new UrlParams())->addParams(array(
+ 'type' => $event->type,
+ 'id' => $event->id
+ ));
+ switch ($event->type) {
+ case substr($event->type, 0, 13) === 'notification_':
+ $rowAction->setParam('type', 'notify');
+ $icon = 'bell';
+ switch (substr($event->type, 13)) {
+ case 'state':
+ $iconTitle = $this->translate('State notification', 'tooltip');
+ $label = $this->translate('NOTIFICATION');
+ $stateName = $isService ? Service::getStateText($event->state) : Host::getStateText($event->state);
+ break;
+ case 'ack':
+ $iconTitle = $this->translate('Ack Notification', 'tooltip');
+ $label = $this->translate('ACK NOTIFICATION');
+ break;
+ case 'dt_start':
+ $iconTitle = $this->translate('Downtime start notification', 'tooltip');
+ $label = $this->translate('DOWNTIME START NOTIFICATION');
+ break;
+ case 'dt_end':
+ $iconTitle = $this->translate('Downtime end notification', 'tooltip');
+ $label = $this->translate('DOWNTIME END NOTIFICATION');
+ break;
+ case 'flapping':
+ $iconTitle = $this->translate('Flapping notification', 'tooltip');
+ $label = $this->translate('FLAPPING NOTIFICATION');
+ break;
+ case 'flapping_end':
+ $iconTitle = $this->translate('Flapping end notification', 'tooltip');
+ $label = $this->translate('FLAPPING END NOTIFICATION');
+ break;
+ case 'custom':
+ $iconTitle = $this->translate('Custom notification', 'tooltip');
+ $label = $this->translate('CUSTOM NOTIFICATION');
+ break;
+ }
+ $msg = $msg ? preg_replace_callback(
+ '/^\[([^\]]+)\]/',
+ function($match) use ($self) { return contactsLink($match, $self); },
+ $msg
+ ) : $this->translate('This notification was not sent out to any contact.');
+ break;
+ case 'comment':
+ $icon = 'comment-empty';
+ $iconTitle = $this->translate('Comment', 'tooltip');
+ $label = $this->translate('COMMENT');
+ break;
+ case 'comment_deleted':
+ $icon = 'cancel';
+ $iconTitle = $this->translate('Comment removed', 'tooltip');
+ $label = $this->translate('COMMENT DELETED');
+ break;
+ case 'ack':
+ $icon = 'ok';
+ $iconTitle = $this->translate('Acknowledged', 'tooltip');
+ $label = $this->translate('ACKNOWLEDGED');
+ break;
+ case 'ack_deleted':
+ $icon = 'ok';
+ $iconTitle = $this->translate('Acknowledgement removed', 'tooltip');
+ $label = $this->translate('ACKNOWLEDGEMENT REMOVED');
+ break;
+ case 'dt_comment':
+ $icon = 'plug';
+ $iconTitle = $this->translate('Downtime scheduled', 'tooltip');
+ $label = $this->translate('SCHEDULED DOWNTIME');
+ break;
+ case 'dt_comment_deleted':
+ $icon = 'plug';
+ $iconTitle = $this->translate('Downtime removed', 'tooltip');
+ $label = $this->translate('DOWNTIME DELETED');
+ break;
+ case 'flapping':
+ $icon = 'flapping';
+ $iconTitle = $this->translate('Flapping started', 'tooltip');
+ $label = $this->translate('FLAPPING');
+ $msg = sprintf($flappingMsg, $msg);
+ break;
+ case 'flapping_deleted':
+ $icon = 'flapping';
+ $iconTitle = $this->translate('Flapping stopped', 'tooltip');
+ $label = $this->translate('FLAPPING STOPPED');
+ $msg = sprintf($flappingMsg, $msg);
+ break;
+ case 'hard_state':
+ if ((int) $event->state === 0) {
+ $icon = 'thumbs-up';
+ } else {
+ $icon = 'warning-empty';
+ }
+ $iconTitle = $this->translate('Hard state', 'tooltip');
+ $label = $isService ? Service::getStateText($event->state, true) : Host::getStateText($event->state, true);
+ $stateName = $isService ? Service::getStateText($event->state) : Host::getStateText($event->state);
+ break;
+ case 'soft_state':
+ $icon = 'spinner';
+ $iconTitle = $this->translate('Soft state', 'tooltip');
+ $label = $isService ? Service::getStateText($event->state, true) : Host::getStateText($event->state, true);
+ $stateName = $isService ? Service::getStateText($event->state) : Host::getStateText($event->state);
+ break;
+ case 'dt_start':
+ $icon = 'plug';
+ $iconTitle = $this->translate('Downtime started', 'tooltip');
+ $label = $this->translate('DOWNTIME START');
+ break;
+ case 'dt_end':
+ $icon = 'plug';
+ $iconTitle = $this->translate('Downtime ended', 'tooltip');
+ $label = $this->translate('DOWNTIME END');
+ break;
+ } ?>
+ <?php
+ $currentDate = $dateFormatter->format($event->timestamp);
+ if ($currentDate !== $lastDate):
+ $lastDate = $currentDate;
+ ?>
+ <tr>
+ <th colspan="2"><?= $currentDate ?></th>
+ </tr>
+ <?php endif ?>
+ <tr href="<?= $rowAction ?>">
+ <td class="state-col state-<?= $stateName ?>">
+ <?php if ($history->getIteratorPosition() % $limit === 0): ?>
+ <a id="page-<?= $history->getIteratorPosition() / $limit + 1 ?>"></a>
+ <?php endif ?>
+ <div class="state-label"><?= $this->escape($label) ?></div>
+ <div class="state-meta"><?= $this->formatTime($event->timestamp) ?></div>
+ </td>
+ <td>
+ <div class="history-message-container">
+ <?php if ($icon): ?>
+ <div class="history-message-icon">
+ <?= $this->icon($icon, $iconTitle) ?>
+ </div>
+ <?php endif ?>
+ <div class="history-message-output">
+ <?php if ($this->isOverview): ?>
+ <?php if ($isService) {
+ echo '<span class="service-on">';
+ echo sprintf(
+ $this->translate('%s on %s', 'service on host'),
+ $this->qlink(
+ $event->service_display_name,
+ 'monitoring/service/show',
+ [
+ 'host' => $event->host_name,
+ 'service' => $event->service_description
+ ],
+ [
+ 'title' => sprintf(
+ $this->translate('Show detailed information for service %s on host %s'),
+ $event->service_display_name,
+ $event->host_display_name
+ )
+ ]
+ ),
+ $this->qlink(
+ $event->host_display_name,
+ 'monitoring/host/show',
+ ['host' => $event->host_name],
+ [
+ 'title' => sprintf(
+ $this->translate('Show detailed information for host %s'),
+ $event->host_display_name
+ )
+ ]
+ )
+ );
+ echo '</span>';
+ } else {
+ echo $this->qlink(
+ $event->host_display_name,
+ 'monitoring/host/show',
+ ['host' => $event->host_name],
+ [
+ 'title' => sprintf(
+ $this->translate('Show detailed information for host %s'),
+ $event->host_display_name
+ )
+ ]
+ );
+ } ?>
+ <?php endif ?>
+ <?= $this->nl2br($this->createTicketLinks($this->markdown($msg, ['class' => 'overview-plugin-output']))) ?>
+ </div>
+ </div>
+ </td>
+ </tr>
+ <?php endforeach ?>
+<?php if ($lastDate !== null): ?>
+ </tbody>
+ </table>
+<?php endif ?>
+<?php if ($history->hasMore()): ?>
+ <div class="action-links">
+ <?php if ($this->compact) {
+ echo $this->qlink(
+ $this->translate('Show More'),
+ $url->without(array('showCompact', 'limit')),
+ null,
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_next'
+ )
+ );
+ } else {
+ echo $this->qlink(
+ $this->translate('Load More'),
+ $url->setAnchor('page-' . ($page + 1)),
+ array('page' => $page + 1,),
+ array('class' => 'action-link')
+ );
+ } ?>
+ </div>
+<?php endif ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/partials/host/objects-header.phtml b/modules/monitoring/application/views/scripts/partials/host/objects-header.phtml
new file mode 100644
index 0000000..48141e2
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/host/objects-header.phtml
@@ -0,0 +1,41 @@
+<?php
+use Icinga\Module\Monitoring\Object\Host;
+
+if (! ($hostCount = count($objects))): return; endif ?>
+<table class="state-table host-detail-state">
+<tbody>
+<?php foreach ($objects as $i => $host): /** @var Host $host */
+ if ($i === 5) {
+ break;
+ } ?>
+ <tr>
+ <td class="state-col state-<?= Host::getStateText($host->host_state); ?><?= $host->host_handled ? ' handled' : '' ?>">
+ <span class="sr-only"><?= Host::getStateText($host->host_state) ?></span>
+ <div class="state-meta">
+ <?= $this->timeSince($host->host_last_state_change, $this->compact) ?>
+ </div>
+ </td>
+ <td>
+ <?= $this->link()->host(
+ $host->host_name,
+ $host->host_display_name
+ ) ?>
+ <?= $this->hostFlags($host) ?>
+ </td>
+ </tr>
+<?php endforeach ?>
+</tbody>
+</table>
+<?php if ($hostCount > 5): ?>
+<div class="hosts-link">
+ <?= $this->qlink(
+ sprintf($this->translate('List all %d hosts'), $hostCount),
+ $this->url()->setPath('monitoring/list/hosts'),
+ null,
+ array(
+ 'data-base-target' => '_next',
+ 'icon' => 'forward'
+ )
+ ) ?>
+</div>
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/partials/object/detail-content.phtml b/modules/monitoring/application/views/scripts/partials/object/detail-content.phtml
new file mode 100644
index 0000000..62bfd2c
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/object/detail-content.phtml
@@ -0,0 +1,53 @@
+<div class="content" data-base-target="_next">
+ <?= $this->render('show/components/output.phtml') ?>
+ <?= $this->render('show/components/grapher.phtml') ?>
+ <?= $this->render('show/components/extensions.phtml') ?>
+
+ <h2><?= $this->translate('Problem handling') ?></h2>
+ <table class="name-value-table">
+ <tbody>
+ <?= $this->render('show/components/acknowledgement.phtml') ?>
+ <?= $this->render('show/components/comments.phtml') ?>
+ <?= $this->render('show/components/downtime.phtml') ?>
+ <?= $this->render('show/components/notes.phtml') ?>
+ <?= $this->render('show/components/actions.phtml') ?>
+ <?= $this->render('show/components/flapping.phtml') ?>
+ <?php if ($object->type === 'service'): ?>
+ <?= $this->render('show/components/servicegroups.phtml') ?>
+ <?php else: ?>
+ <?= $this->render('show/components/hostgroups.phtml') ?>
+ <?php endif ?>
+ </tbody>
+ </table>
+
+ <?= $this->render('show/components/perfdata.phtml') ?>
+
+ <h2><?= $this->translate('Notifications') ?></h2>
+ <table class="name-value-table">
+ <tbody>
+ <?= $this->render('show/components/notifications.phtml') ?>
+ <?php if ($this->hasPermission('*') || ! $this->hasPermission('no-monitoring/contacts')): ?>
+ <?= $this->render('show/components/contacts.phtml') ?>
+ <?php endif ?>
+ </tbody>
+ </table>
+
+ <h2><?= $this->translate('Check execution') ?></h2>
+ <table class="name-value-table">
+ <tbody>
+ <?= $this->render('show/components/command.phtml') ?>
+ <?= $this->render('show/components/checksource.phtml') ?>
+ <?= $this->render('show/components/reachable.phtml') ?>
+ <?= $this->render('show/components/checkstatistics.phtml') ?>
+ <?= $this->render('show/components/checktimeperiod.phtml') ?>
+ </tbody>
+ </table>
+
+ <?php if (! empty($object->customvars)): ?>
+ <h2><?= $this->translate('Custom Variables') ?></h2>
+ <div id="<?= $object->type ?>-customvars" data-visible-height="200" class="collapsible">
+ <?= (new \Icinga\Module\Monitoring\Web\Widget\CustomVarTable($object->customvarsWithOriginalNames, $object)) ?>
+ </div>
+ <?php endif ?>
+ <?= $this->render('show/components/flags.phtml') ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/partials/object/host-header.phtml b/modules/monitoring/application/views/scripts/partials/object/host-header.phtml
new file mode 100644
index 0000000..4de4a01
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/object/host-header.phtml
@@ -0,0 +1,51 @@
+<?php
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Web\Url;
+
+/** @var Host $object */
+
+$url = Url::fromRequest();
+$linkHostName = ! ($url->getPath() === 'monitoring/host/show' && $url->getParam('host') === $object->host_name);
+?>
+<table class="state-table host-detail-state">
+ <tr>
+ <td class="state-col state-<?= Host::getStateText($object->host_state) ?><?= $object->host_handled ? ' handled' : '' ?>">
+ <div class="state-header"><?= Host::getStateText($object->host_state, true) ?></div>
+ <div class="state-meta">
+ <?= $this->timeSince($object->host_last_state_change) ?>
+ <?php if ((int) $object->host_state > 0 && (int) $object->host_state_type === 0): ?>
+ <div><?= $this->translate('Soft', 'Soft state') ?> <?= $object->host_attempt ?></div>
+ <?php endif ?>
+ </div>
+ </td>
+ <td>
+ <?= $this->iconImage()->host($object) ?>
+ <?php
+ if ($linkHostName) {
+ echo '<a href="' . Url::fromPath('monitoring/host/show', array('host' => $object->host_name)) . '">';
+ }
+ ?>
+ <span class="selectable"><strong><?= $this->escape($object->host_display_name) ?></strong></span>
+ <?php if ($object->host_display_name !== $object->host_name): ?>
+ <span class="selectable host-meta">&#40;<?= $this->escape($object->host_name) ?>&#41;</span>
+ <?php endif ?>
+ <?php
+ if ($linkHostName) {
+ echo '</a>';
+ }
+ ?>
+ <?php if ($object->host_alias !== $object->host_display_name && $object->host_alias !== $object->host_name): ?>
+ <div class="selectable host-meta">
+ <?= $this->escape($this->translate('Alias', 'host') . ': ' . $object->host_alias) ?>
+ </div>
+ <?php endif ?>
+ <?= $this->hostFlags($object) ?>
+ <?php if ($object->host_address6 && $object->host_address6 !== $object->host_name): ?>
+ <div class="selectable host-meta" title="<?= $this->translate('IPv6 address') ?>"><?= $this->escape($object->host_address6) ?></div>
+ <?php endif ?>
+ <?php if ($object->host_address && $object->host_address !== $object->host_name): ?>
+ <div class="selectable host-meta" title="<?= $this->translate('IPv4 address') ?>"><?= $this->escape($object->host_address) ?></div>
+ <?php endif ?>
+ </td>
+ </tr>
+</table>
diff --git a/modules/monitoring/application/views/scripts/partials/object/quick-actions.phtml b/modules/monitoring/application/views/scripts/partials/object/quick-actions.phtml
new file mode 100644
index 0000000..fe05a84
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/object/quick-actions.phtml
@@ -0,0 +1,144 @@
+<div class="quick-actions">
+ <ul class="nav tab-nav">
+ <?php if (isset($removeAckForm)): ?>
+ <li>
+ <?php
+ $removeAckForm = clone $removeAckForm;
+ $removeAckForm->setAttrib('id', 'quickAction_' . $removeAckForm->getName()); // Avoids id duplication
+ $removeAckForm->setLabelEnabled(true);
+ echo $removeAckForm;
+ ?>
+ </li>
+ <?php elseif /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */ ($this->hasPermission('monitoring/command/acknowledge-problem') && ! (in_array((int) $object->state, array(0, 99))) ): ?>
+ <li>
+ <?php if ($object->getType() === $object::TYPE_HOST) {
+ echo $this->qlink(
+ $this->translate('Acknowledge'),
+ 'monitoring/host/acknowledge-problem',
+ array('host' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'edit',
+ 'title' => $this->translate(
+ 'Acknowledge this problem, suppress all future notifications for it and tag it as being handled'
+ )
+ )
+ );
+ } else {
+ echo $this->qlink(
+ $this->translate('Acknowledge'),
+ 'monitoring/service/acknowledge-problem',
+ array('host' => $object->getHost()->getName(), 'service' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'edit',
+ 'title' => $this->translate(
+ 'Acknowledge this problem, suppress all future notifications for it and tag it as being handled'
+ )
+ )
+ );
+ } ?>
+ </li>
+ <?php endif ?>
+ <?php if (isset($checkNowForm)): // Form is unset if the current user lacks the respective permission ?>
+ <?php ($checkNowForm = clone $checkNowForm)->setAttrib('id', 'quickAction_' . $checkNowForm->getName()); // Avoids id duplication ?>
+ <li><?= $checkNowForm ?></li>
+ <?php endif ?>
+ <?php if ($this->hasPermission('monitoring/command/comment/add')): ?>
+ <li>
+ <?php if ($object->getType() === $object::TYPE_HOST) {
+ echo $this->qlink(
+ $this->translate('Comment'),
+ 'monitoring/host/add-comment',
+ array('host' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'comment-empty',
+ 'title' => $this->translate('Add a new comment to this host')
+ )
+ );
+ } else {
+ echo $this->qlink(
+ $this->translate('Comment'),
+ 'monitoring/service/add-comment',
+ array('host' => $object->getHost()->getName(), 'service' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'comment-empty',
+ 'title' => $this->translate('Add a new comment to this service')
+ )
+ );
+ } ?>
+ </li>
+ <?php endif ?>
+ <?php if ($this->hasPermission('monitoring/command/send-custom-notification')): ?>
+ <li>
+ <?php if ($object->getType() === $object::TYPE_HOST) {
+ echo $this->qlink(
+ $this->translate('Notification'),
+ 'monitoring/host/send-custom-notification',
+ array('host' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'bell',
+ 'title' => $this->translate(
+ 'Send a custom notification to contacts responsible for this host'
+ )
+ )
+ );
+ } else {
+ echo $this->qlink(
+ $this->translate('Notification'),
+ 'monitoring/service/send-custom-notification',
+ array('host' => $object->getHost()->getName(), 'service' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'bell',
+ 'title' => $this->translate(
+ 'Send a custom notification to contacts responsible for this service'
+ )
+ )
+ );
+ } ?>
+ </li>
+ <?php endif ?>
+ <?php if ($this->hasPermission('monitoring/command/downtime/schedule')): ?>
+ <li><?php if ($object->getType() === $object::TYPE_HOST) {
+ echo $this->qlink(
+ $this->translate('Downtime'),
+ 'monitoring/host/schedule-downtime',
+ array('host' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'plug',
+ 'title' => $this->translate(
+ 'Schedule a downtime to suppress all problem notifications within a specific period of time'
+ )
+ )
+ );
+ } else {
+ echo $this->qlink(
+ $this->translate('Downtime'),
+ 'monitoring/service/schedule-downtime',
+ array('host' => $object->getHost()->getName(), 'service' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'plug',
+ 'title' => $this->translate(
+ 'Schedule a downtime to suppress all problem notifications within a specific period of time'
+ )
+ )
+ );
+ } ?>
+ </li>
+ <?php endif ?>
+ </ul>
+</div>
diff --git a/modules/monitoring/application/views/scripts/partials/object/service-header.phtml b/modules/monitoring/application/views/scripts/partials/object/service-header.phtml
new file mode 100644
index 0000000..318fe49
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/object/service-header.phtml
@@ -0,0 +1,72 @@
+<?php
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\Url;
+
+/** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+
+$url = Url::fromRequest();
+$linkServiceName = ! ($url->getPath() === 'monitoring/service/show' && $url->getParam('service') === $object->service_description);
+?>
+<table class="state-table service-detail-state">
+ <tr>
+ <td class="state-col state-<?= Host::getStateText($object->host_state) ?><?= $object->host_handled ? ' handled' : '' ?>">
+ <div class="state-label"><?= Host::getStateText($object->host_state, true) ?></div>
+ <div class="state-meta">
+ <?= $this->timeSince($object->host_last_state_change) ?>
+ <?php if ((int) $object->host_state > 0 && (int) $object->host_state_type === 0): ?>
+ <div><?= $this->translate('Soft', 'Soft state') ?> <?= $object->host_attempt ?></div>
+ <?php endif ?>
+ </div>
+ </td>
+ <td>
+ <?= $this->iconImage()->host($object) ?>
+ <a href="<?= Url::fromPath('monitoring/host/show', array('host' => $object->host_name)) ?>">
+ <span class="selectable"><strong><?= $this->escape($object->host_display_name) ?></strong></span>
+ <?php if ($object->host_display_name !== $object->host_name): ?>
+ <span class="selectable host-meta">&#40;<?= $this->escape($object->host_name) ?>&#41;</span>
+ <?php endif ?>
+ </a>
+ <?= $this->hostFlags($object) ?>
+ <?php if ($object->host_address6 && $object->host_address6 !== $object->host_name): ?>
+ <div class="selectable host-meta" title="<?= $this->translate('IPv6 address') ?>"><?= $this->escape($object->host_address6) ?></div>
+ <?php endif ?>
+ <?php if ($object->host_address && $object->host_address !== $object->host_name): ?>
+ <div class="selectable host-meta" title="<?= $this->translate('IPv4 address') ?>"><?= $this->escape($object->host_address) ?></div>
+ <?php endif ?>
+ </td>
+ </tr>
+ <tr>
+ <td class="state-col state-<?= Service::getStateText($object->service_state) ?><?= $object->service_handled ? ' handled' : '' ?>">
+ <div class="state-label"><?= Service::getStateText($object->service_state, true) ?></div>
+ <div class="state-meta">
+ <?= $this->timeSince($object->service_last_state_change) ?>
+ <?php if ((int) $object->service_state > 0 && (int) $object->service_state_type === 0): ?>
+ <div><?= $this->translate('Soft', 'Soft state') ?> <?= $object->service_attempt ?></div>
+ <?php endif ?>
+ </div>
+ </td>
+ <td>
+ <?= $this->iconImage()->service($object) ?>
+ <?= $this->translate('Service') ?>&#58;
+ <?php
+ if ($linkServiceName) {
+ echo '<a href="' . Url::fromPath('monitoring/service/show', array(
+ 'host' => $object->host_name,
+ 'service' => $object->service_description
+ )) . '">';
+ }
+ ?>
+ <span class="selectable"><strong><?= $this->escape($object->service_display_name) ?></strong></span>
+ <?php if ($object->service_display_name !== $object->service_description): ?>
+ <span class="selectable service-meta">&#40;<?= $this->escape($object->service_description) ?>&#41;</span>
+ <?php endif ?>
+ <?php
+ if ($linkServiceName) {
+ echo '</a>';
+ }
+ ?>
+ <?= $this->serviceFlags($object) ?>
+ </td>
+ </tr>
+</table>
diff --git a/modules/monitoring/application/views/scripts/partials/service/objects-header.phtml b/modules/monitoring/application/views/scripts/partials/service/objects-header.phtml
new file mode 100644
index 0000000..d342d87
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/service/objects-header.phtml
@@ -0,0 +1,45 @@
+<?php
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\Service;
+
+if (! ($serviceCount = count($objects))): return; endif ?>
+<table class="state-table service-detail-state">
+<tbody>
+<?php foreach ($objects as $i => $service): /** @var Service $service */
+ if ($i === 5) {
+ break;
+ } ?>
+ <tr>
+ <td class="state-col state-<?= Service::getStateText($service->service_state) ?><?= $service->service_handled ? ' handled' : '' ?>">
+ <span class="sr-only"><?= Service::getStateText($service->service_state) ?></span>
+ <div class="state-meta">
+ <?= $this->timeSince($service->service_last_state_change, $this->compact) ?>
+ </div>
+ </td>
+ <td>
+ <?= $this->link()->service(
+ $service->service_description,
+ $service->service_display_name,
+ $service->host_name,
+ $service->host_display_name
+ . ($service->host_state != 0 ? ' (' . Host::getStateText($service->host_state, true) . ')' : '')
+ ) ?>
+ <?= $this->serviceFlags($service) ?>
+ </td>
+ </tr>
+<?php endforeach ?>
+</tbody>
+</table>
+<?php if ($serviceCount > 5): ?>
+<div class="services-link">
+ <?= $this->qlink(
+ sprintf($this->translate('List all %d services'), $serviceCount),
+ $this->url()->setPath('monitoring/list/services'),
+ null,
+ array(
+ 'data-base-target' => '_next',
+ 'icon' => 'forward'
+ )
+ ) ?>
+</div>
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/partials/show-more.phtml b/modules/monitoring/application/views/scripts/partials/show-more.phtml
new file mode 100644
index 0000000..fd6a99d
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/partials/show-more.phtml
@@ -0,0 +1,15 @@
+<?php
+/** @var \Icinga\Module\Monitoring\DataView\DataView $dataView */
+if ($dataView->hasMore()): ?>
+<div class="text-right">
+ <?= $this->qlink(
+ $this->translate('Show More'),
+ $this->url()->without(array('showCompact', 'limit')),
+ null,
+ array(
+ 'data-base-target' => '_next',
+ 'class' => 'action-link'
+ )
+ ) ?>
+</div>
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/service/show.phtml b/modules/monitoring/application/views/scripts/service/show.phtml
new file mode 100644
index 0000000..bc9c612
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/service/show.phtml
@@ -0,0 +1,8 @@
+<div class="controls controls-separated">
+<?php if (! $this->compact): ?>
+ <?= $this->tabs ?>
+<?php endif ?>
+ <?= $this->render('partials/object/service-header.phtml') ?>
+ <?= $this->render('partials/object/quick-actions.phtml') ?>
+</div>
+<?= $this->render('partials/object/detail-content.phtml') ?>
diff --git a/modules/monitoring/application/views/scripts/services/show.phtml b/modules/monitoring/application/views/scripts/services/show.phtml
new file mode 100644
index 0000000..e9fb56f
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/services/show.phtml
@@ -0,0 +1,208 @@
+<div class="controls">
+
+ <?php if (! $this->compact): ?>
+ <?= $tabs ?>
+ <?php endif ?>
+ <?= $this->render('list/components/servicesummary.phtml') ?>
+ <?= $this->render('partials/service/objects-header.phtml') ?>
+ <?php
+ $serviceCount = count($objects);
+ $unhandledCount = count($unhandledObjects);
+ $problemCount = count($problemObjects);
+ $unackCount = count($unacknowledgedObjects);
+ $scheduledDowntimeCount = count($objects->getScheduledDowntimes());
+ ?>
+</div>
+
+<div class="content">
+
+ <?php if ($serviceCount === 0): ?>
+ <?= $this->translate('No services found matching the filter') ?>
+ <?php else: ?>
+ <?= $this->render('show/components/extensions.phtml') ?>
+ <h2> <?= $this->translate('Problem handling') ?> </h2>
+ <table class="name-value-table">
+ <tbody>
+ <?php if ($unackCount > 0): ?>
+ <tr>
+ <th> <?= sprintf($this->translate('%d unhandled problems'), $unackCount) ?> </th>
+ <td> <?= $this->qlink(
+ $this->translate('Acknowledge'),
+ $acknowledgeLink,
+ null,
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'check'
+ )
+ ) ?> </td>
+ </tr>
+ <?php endif; ?>
+
+ <?php if (($acknowledgedCount = count($acknowledgedObjects)) > 0): ?>
+ <tr>
+ <th> <?= sprintf(
+ $this->translatePlural(
+ '%s acknowledgement',
+ '%s acknowledgements',
+ $acknowledgedCount
+ ),
+ '<b>' . $acknowledgedCount . '</b>'
+ ) ?>
+ </th>
+ <td>
+ <?= $removeAckForm->setLabelEnabled(true) ?>
+ </td>
+ </tr>
+ <?php endif ?>
+
+ <tr>
+ <th> <?= $this->translate('Comments') ?> </th>
+ <td>
+ <?= $this->qlink(
+ $this->translate('Add comments'),
+ $addCommentLink,
+ null,
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'comment-empty'
+ )
+ ) ?>
+ </td>
+ </tr>
+
+ <?php if (($commentCount = count($objects->getComments())) > 0): ?>
+ <tr>
+ <th></th>
+ <td>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ '%s comment',
+ '%s comments',
+ $commentCount
+ ),
+ $commentCount
+ ),
+ $commentsLink,
+ null,
+ array('data-base-target' => '_next')
+ ) ?>
+ </td>
+ </tr>
+ <?php endif ?>
+
+ <tr>
+ <th>
+ <?= $this->translate('Downtimes') ?>
+ </th>
+ <td>
+ <?= $this->qlink(
+ $this->translate('Schedule downtimes'),
+ $downtimeAllLink,
+ null,
+ array(
+ 'icon' => 'plug',
+ 'class' => 'action-link'
+ )
+ ) ?>
+ </td>
+ </tr>
+
+ <?php if ($scheduledDowntimeCount > 0): ?>
+ <tr>
+ <th></th>
+ <td>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ '%d scheduled downtime',
+ '%d scheduled downtimes',
+ $scheduledDowntimeCount
+ ),
+ $scheduledDowntimeCount
+ ),
+ $showDowntimesLink,
+ null,
+ array(
+ 'data-base-target' => '_next'
+ )
+ ) ?>
+ </td>
+ </tr>
+ <?php endif ?>
+
+ </tbody>
+ </table>
+
+ <?php if ($this->hasPermission('monitoring/command/send-custom-notification')): ?>
+
+ <h2> <?= $this->translate('Notifications') ?> </h2>
+
+ <table class="name-value-table">
+ <tbody>
+ <tr>
+ <th> <?= $this->translate('Notifications') ?> </th>
+ <td>
+ <?= $this->qlink(
+ $this->translate('Send notifications'),
+ $sendCustomNotificationLink,
+ null,
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'bell'
+ )
+ ) ?>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <?php endif ?>
+
+ <h2> <?= $this->translate('Check Execution') ?> </h2>
+
+ <table class="name-value-table">
+ <tbody>
+ <tr>
+ <th> <?= $this->translate('Command') ?> </th>
+ <td>
+ <?= $this->qlink(
+ $this->translate('Process check result'),
+ $processCheckResultAllLink,
+ null,
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'edit'
+ )
+ ) ?>
+ </td>
+ </tr>
+
+ <?php if (isset($checkNowForm)): // Form is unset if the current user lacks the respective permission ?>
+ <tr>
+ <th> <?= $this->translate('Schedule Check') ?> </th>
+ <td> <?= $checkNowForm ?> </td>
+ </tr>
+ <?php endif ?>
+
+ <?php if (isset($rescheduleAllLink)): ?>
+ <tr>
+ <th></th>
+ <td>
+ <?= $this->qlink(
+ $this->translate('Reschedule'),
+ $rescheduleAllLink,
+ null,
+ array(
+ 'class' => 'action-link',
+ 'icon' => 'calendar-empty'
+ )
+ ) ?>
+ </td>
+ </tr>
+ <?php endif ?>
+ </tbody>
+ </table>
+ <h2><?= $this->translate('Feature Commands') ?></h2>
+ <?= $toggleFeaturesForm ?>
+ <?php endif ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/show/components/acknowledgement.phtml b/modules/monitoring/application/views/scripts/show/components/acknowledgement.phtml
new file mode 100644
index 0000000..fd7f6bb
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/acknowledgement.phtml
@@ -0,0 +1,94 @@
+<?php
+
+/** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+
+if (in_array((int) $object->state, array(0, 99))) {
+ // Ignore this markup if the object is in a non-problem state or pending
+ return;
+}
+
+if ($object->acknowledged):
+$acknowledgement = $object->acknowledgement;
+/** @var \Icinga\Module\Monitoring\Object\Acknowledgement $acknowledgement */
+?>
+<tr>
+ <th><?= $this->translate('Acknowledged') ?></th>
+ <td data-base-target="_self">
+ <?php if ($acknowledgement): ?>
+ <dl class="comment-list">
+ <dt>
+ <?= $this->escape($acknowledgement->getAuthor()) ?>
+ <span class="comment-time">
+ <?= $this->translate('acknowledged') ?>
+ <?= $this->timeAgo($acknowledgement->getEntryTime()) ?>
+ <?php if ($acknowledgement->expires()): ?>
+ <span aria-hidden="true">&#448;</span>
+ <?= sprintf(
+ $this->translate('Expires %s'),
+ $this->timeUntil($acknowledgement->getExpirationTime())
+ ) ?>
+ <?php endif ?>
+ </span>
+ <?php if ($acknowledgement->getSticky()): ?>
+ <?= $this->icon('pin', sprintf(
+ $this->translate(
+ 'Acknowledgement remains until the %1$s recovers even if the %1$s changes state'
+ ),
+ $object->getType(true)
+ )) ?>
+ <?php endif ?>
+ <?php if (isset($removeAckForm)) {
+ // Form is unset if the current user lacks the respective permission
+ $removeAckForm->setAttrib('class', $removeAckForm->getAttrib('class') . ' remove-action');
+ echo $removeAckForm;
+ } ?>
+ </dt>
+ <dd>
+ <?= $this->nl2br($this->createTicketLinks($this->markdown($acknowledgement->getComment()))) ?>
+ </dd>
+ </dl>
+ <?php elseif (isset($removeAckForm)): ?>
+ <?= $removeAckForm ?>
+ <?php endif ?>
+ </td>
+</tr>
+<?php else: ?>
+<tr>
+ <th><?= $this->translate('Not acknowledged') ?></th>
+ <td>
+ <?php if ($this->hasPermission('monitoring/command/acknowledge-problem')) {
+ if ($object->getType() === $object::TYPE_HOST) {
+ $ackLink = $this->href(
+ 'monitoring/host/acknowledge-problem',
+ array('host' => $object->getName()),
+ null,
+ array('class' => 'action-link')
+ );
+ } else {
+ $ackLink = $this->href(
+ 'monitoring/service/acknowledge-problem',
+ array('host' => $object->getHost()->getName(), 'service' => $object->getName()),
+ null,
+ array('class' => 'action-link')
+ );
+ }
+ ?>
+ <?= $this->qlink(
+ $this->translate('Acknowledge'),
+ $ackLink,
+ null,
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'edit',
+ 'title' => $this->translate(
+ 'Acknowledge this problem, suppress all future notifications for it and tag it as being handled'
+ )
+ )
+ ) ?>
+ <?php } else {
+ echo '&#45;';
+ } // endif ?>
+ </td>
+</tr>
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/show/components/actions.phtml b/modules/monitoring/application/views/scripts/show/components/actions.phtml
new file mode 100644
index 0000000..938ab2a
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/actions.phtml
@@ -0,0 +1,43 @@
+<?php
+
+use Icinga\Web\Navigation\Navigation;
+
+$navigation = new Navigation();
+$navigation->load($object->getType() . '-action');
+foreach ($navigation as $item) {
+ $item->setObject($object);
+}
+
+foreach ($object->getActionUrls() as $i => $link) {
+ $navigation->addItem(
+
+ // add warning to links that open in new tabs to improve accessibility, as recommended by WCAG20 G201
+ $this->icon(
+ 'forward',
+ $this->translate('Link opens in new window'),
+ array('aria-label' => $this->translate('Link opens in new window'))
+ ) . ' Action ' . ($i + 1),
+ array(
+ 'url' => $link,
+ 'target' => '_blank',
+ 'renderer' => array(
+ 'NavigationItemRenderer',
+ 'escape_label' => false
+ )
+ )
+ );
+}
+
+if (isset($this->actions)) {
+ $navigation->merge($this->actions);
+}
+
+if ($navigation->isEmpty() || ! $navigation->hasRenderableItems()) {
+ return;
+}
+
+?>
+<tr>
+ <th><?= $this->translate('Actions'); ?></th>
+ <?= $navigation->getRenderer()->setElementTag('td')->setCssClass('actions go-ahead'); ?>
+</tr>
diff --git a/modules/monitoring/application/views/scripts/show/components/checksource.phtml b/modules/monitoring/application/views/scripts/show/components/checksource.phtml
new file mode 100644
index 0000000..ac9799f
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/checksource.phtml
@@ -0,0 +1,6 @@
+<?php if ($object->check_source !== null): ?>
+<tr>
+ <th><?= $this->translate('Check Source') ?></th>
+ <td><?= $this->escape($object->check_source) ?></td>
+</tr>
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/show/components/checkstatistics.phtml b/modules/monitoring/application/views/scripts/show/components/checkstatistics.phtml
new file mode 100644
index 0000000..e37e30a
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/checkstatistics.phtml
@@ -0,0 +1,85 @@
+<?php
+/** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+$activeChecksEnabled = (bool) $object->active_checks_enabled;
+?>
+
+<tr>
+ <th><?= $activeChecksEnabled ? $this->translate('Last check') : $this->translate('Last update') ?></th>
+ <td data-base-target="_self">
+<?php if ((int) $object->state !== 99): ?>
+ <?= $this->timeAgo($object->last_check) ?>
+ <?php if ($object->next_update < time()): ?>
+ <?= $this->icon('circle', $this->translate('Check result is late'), array('class' => 'icon-stateful state-critical')) ?>
+ <?php endif ?>
+<?php endif ?>
+ <?php if (isset($checkNowForm)) { // Form is unset if the current user lacks the respective permission
+ echo $checkNowForm;
+ } ?>
+ </td>
+</tr>
+
+<tr>
+ <th><?= $activeChecksEnabled ? $this->translate('Next check') : $this->translate('Next update') ?></th>
+ <td>
+ <?php if ((int) $object->state !== 99) {
+ if ($activeChecksEnabled) {
+ echo $this->timeUntil($object->next_check);
+ } else {
+ echo sprintf($this->translate('expected %s'), $this->timeUntil($object->next_update));
+ }
+ } ?>
+ <?php if ($activeChecksEnabled && $this->hasPermission('monitoring/command/schedule-check')) {
+ if ($object->getType() === $object::TYPE_SERVICE) {
+ echo $this->qlink(
+ $this->translate('Reschedule'),
+ 'monitoring/service/reschedule-check',
+ array('host' => $object->getHost()->getName(), 'service' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'calendar-empty',
+ 'title' => $this->translate(
+ 'Schedule the next active check at a different time than the current one'
+ )
+ )
+ );
+ } else {
+ echo $this->qlink(
+ $this->translate('Reschedule'),
+ 'monitoring/host/reschedule-check',
+ array('host' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'calendar-empty',
+ 'title' => $this->translate(
+ 'Schedule the next active check at a different time than the current one'
+ )
+ )
+ );
+ }
+ } ?>
+ </td>
+</tr>
+
+<tr>
+ <th><?= $this->translate('Check attempts') ?></th>
+ <td>
+ <?= $object->attempt ?>
+ (<?= (int) $object->state_type === 0 ? $this->translate('soft state') : $this->translate('hard state') ?>)
+ </td>
+</tr>
+
+<?php if ($object->check_execution_time): ?>
+<tr>
+ <th><?= $this->translate('Check execution time') ?></th>
+ <td><?= round((float) $object->check_execution_time, 3) ?>s</td>
+</tr>
+<?php endif ?>
+
+<?php if ($object->check_latency): ?>
+<tr>
+ <th><?= $this->translate('Check latency') ?></th>
+ <td><?= $object->check_latency ?>s</td>
+</tr>
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/show/components/checktimeperiod.phtml b/modules/monitoring/application/views/scripts/show/components/checktimeperiod.phtml
new file mode 100644
index 0000000..34c4eb9
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/checktimeperiod.phtml
@@ -0,0 +1,21 @@
+<?php if (isset($object->service_check_timeperiod)): ?>
+
+<tr>
+ <th><?= $this->translate('Check Timeperiod') ?></th>
+ <td>
+ <?= $object->service_check_timeperiod ?>
+ </td>
+</tr>
+
+<?php endif ?>
+
+<?php if (isset($object->host_check_timeperiod)): ?>
+
+ <tr>
+ <th><?= $this->translate('Check Timeperiod') ?></th>
+ <td>
+ <?= $object->host_check_timeperiod ?>
+ </td>
+ </tr>
+
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/show/components/command.phtml b/modules/monitoring/application/views/scripts/show/components/command.phtml
new file mode 100644
index 0000000..9b51458
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/command.phtml
@@ -0,0 +1,52 @@
+<?php
+$parts = explode('!', $object->check_command);
+$command = array_shift($parts);
+
+if ($showInstance): ?>
+<tr>
+ <th><?= $this->translate('Instance') ?></th>
+ <td><?= $this->escape($object->instance_name) ?></td>
+</tr>
+<?php endif ?>
+<tr>
+ <th><?= $this->translate('Command') ?></th>
+ <td>
+ <?= $this->escape($command) ?>
+ <?php if ($this->hasPermission('monitoring/command/process-check-result') && $object->passive_checks_enabled) {
+ $title = sprintf(
+ $this->translate('Submit a one time or so called passive result for the %s check'), $command
+ );
+ if ($object->getType() === $object::TYPE_HOST) {
+ echo $this->qlink(
+ $this->translate('Process check result'),
+ 'monitoring/host/process-check-result',
+ array('host' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'edit',
+ 'title' => $title
+ )
+ );
+ } else {
+ echo $this->qlink(
+ $this->translate('Process check result'),
+ 'monitoring/service/process-check-result',
+ array('host' => $object->getHost()->getName(), 'service' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'edit',
+ 'title' => $title
+ )
+ );
+ }
+ } ?>
+ </td>
+</tr>
+
+<?php
+$row = "<tr>\n <th>%s</th>\n <td>%s</td>\n</tr>\n";
+for ($i = 0; $i < count($parts); $i++) {
+ printf($row, '$ARG' . ($i + 1) . '$', $this->escape($parts[$i]));
+}
diff --git a/modules/monitoring/application/views/scripts/show/components/comments.phtml b/modules/monitoring/application/views/scripts/show/components/comments.phtml
new file mode 100644
index 0000000..fd980ee
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/comments.phtml
@@ -0,0 +1,86 @@
+<?php
+$addLink = false;
+if ($this->hasPermission('monitoring/command/comment/add')) {
+ /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+ if ($object->getType() === $object::TYPE_HOST) {
+ $addLink = $this->qlink(
+ $this->translate('Add comment'),
+ 'monitoring/host/add-comment',
+ array('host' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'comment-empty',
+ 'title' => $this->translate('Add a new comment to this host')
+ )
+ );
+ } else {
+ $addLink = $this->qlink(
+ $this->translate('Add comment'),
+ 'monitoring/service/add-comment',
+ array('host' => $object->getHost()->getName(), 'service' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'comment-empty',
+ 'title' => $this->translate('Add a new comment to this service')
+ )
+ );
+ }
+}
+if (empty($object->comments) && ! $addLink) {
+ return;
+}
+?>
+<tr>
+ <th><?php
+ echo $this->translate('Comments');
+ if (! empty($object->comments) && $addLink) {
+ echo '<br>' . $addLink;
+ }
+ ?></th>
+ <td data-base-target="_self">
+ <?php if (empty($object->comments)):
+ echo $addLink;
+ else: ?>
+ <dl class="comment-list">
+ <?php foreach ($object->comments as $comment): ?>
+ <dt>
+ <a data-base-target="_next" href="<?= $this->href('monitoring/comment/show', array('comment_id' => $comment->id)) ?>">
+ <?= $this->escape($comment->author) ?>
+ <span class="comment-time">
+ <?= $this->translate('commented') ?>
+ <?= $this->timeAgo($comment->timestamp) ?>
+ <?php if ($comment->expiration): ?>
+ <span aria-hidden="true">ǀ</span>
+ <?= sprintf(
+ $this->translate('Expires %s'),
+ $this->timeUntil($comment->expiration)
+ ) ?>
+ <?php endif ?>
+ </span>
+ </a>
+ <?= $comment->persistent ? $this->icon('attach', 'This comment is persistent.') : '' ?>
+ <?php if (isset($delCommentForm)) {
+ // Form is unset if the current user lacks the respective permission
+ $deleteButton = clone($delCommentForm);
+ /** @var \Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentCommandForm $deleteButton */
+ $deleteButton->setAttrib('class', $deleteButton->getAttrib('class') . ' remove-action');
+ $deleteButton->populate(
+ array(
+ 'comment_id' => $comment->id,
+ 'comment_is_service' => isset($comment->service_description),
+ 'comment_name' => $comment->name
+ )
+ );
+ echo $deleteButton;
+ } ?>
+ </dt>
+ <dd>
+ <?= $this->nl2br($this->createTicketLinks($this->markdownLine($comment->comment, [ 'class' => 'caption']))) ?>
+ </dd>
+ <?php endforeach ?>
+ </dl>
+ <?php endif ?>
+ </td>
+</tr>
diff --git a/modules/monitoring/application/views/scripts/show/components/contacts.phtml b/modules/monitoring/application/views/scripts/show/components/contacts.phtml
new file mode 100644
index 0000000..5661c1a
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/contacts.phtml
@@ -0,0 +1,38 @@
+<?php
+
+if ($object->contacts->hasResult()) {
+
+ $list = array();
+ foreach ($object->contacts as $contact) {
+ $list[] = $this->qlink(
+ $contact->contact_alias,
+ 'monitoring/show/contact',
+ array('contact_name' => $contact->contact_name),
+ array('title' => sprintf($this->translate('Show detailed information about %s'), $contact->contact_alias))
+ );
+ }
+
+ printf(
+ "<tr><th>%s</th><td class=\"go-ahead\">%s</td></tr>\n",
+ $this->translate('Contacts'),
+ implode(', ', $list)
+ );
+}
+
+if ($object->contactgroups->hasResult()) {
+ $list = array();
+ foreach ($object->contactgroups as $contactgroup) {
+ $list[] = $this->qlink(
+ $contactgroup->contactgroup_alias,
+ 'monitoring/list/contactgroups',
+ array('contactgroup_name' => $contactgroup->contactgroup_name),
+ array('title' => sprintf($this->translate('List contacts in contact-group "%s"'), $contactgroup->contactgroup_alias))
+ );
+ }
+
+ printf(
+ "<tr><th>%s</th><td class=\"go-ahead\">%s</td></tr>\n",
+ $this->translate('Contactgroups'),
+ implode(', ', $list)
+ );
+}
diff --git a/modules/monitoring/application/views/scripts/show/components/downtime.phtml b/modules/monitoring/application/views/scripts/show/components/downtime.phtml
new file mode 100644
index 0000000..618d4d9
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/downtime.phtml
@@ -0,0 +1,109 @@
+<?php
+$addLink = false;
+if ($this->hasPermission('monitoring/command/downtime/schedule')) {
+ /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+ if ($object->getType() === $object::TYPE_HOST) {
+ $addLink = $this->qlink(
+ $this->translate('Schedule downtime'),
+ 'monitoring/host/schedule-downtime',
+ array('host' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'plug',
+ 'title' => $this->translate(
+ 'Schedule a downtime to suppress all problem notifications within a specific period of time'
+ )
+ )
+ );
+ } else {
+ $addLink = $this->qlink(
+ $this->translate('Schedule downtime'),
+ 'monitoring/service/schedule-downtime',
+ array('host' => $object->getHost()->getName(), 'service' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'plug',
+ 'title' => $this->translate(
+ 'Schedule a downtime to suppress all problem notifications within a specific period of time'
+ )
+ )
+ );
+ }
+}
+if (empty($object->downtimes) && ! $addLink) {
+ return;
+}
+?>
+<tr>
+ <th><?php
+ echo $this->translate('Downtimes');
+ if (! empty($object->downtimes) && $addLink) {
+ echo '<br>' . $addLink;
+ }
+ ?></th>
+ <td data-base-target="_self">
+ <?php if (empty($object->downtimes)):
+ echo $addLink;
+ else: ?>
+ <dl class="comment-list">
+ <?php foreach ($object->downtimes as $downtime):
+ if ((bool) $downtime->is_in_effect) {
+ $state = sprintf(
+ $this->translate('expires %s', 'Last format parameter represents the downtime expire time'),
+ $this->timeUntil($downtime->end, false, true)
+ );
+ } else {
+ if ($downtime->start <= time()) {
+ $state = sprintf(
+ $this->translate('ends %s', 'Last format parameter represents the end time'),
+ $this->timeUntil($downtime->is_flexible ? $downtime->scheduled_end : $downtime->end, false, true)
+ );
+ } else {
+ $state = sprintf(
+ $this->translate('scheduled %s', 'Last format parameter represents the time scheduled'),
+ $this->timeUntil($downtime->start, false, true)
+ ) . ' ' . sprintf(
+ $this->translate('expires %s', 'Last format parameter represents the downtime expire time'),
+ $this->timeUntil($downtime->is_flexible ? $downtime->scheduled_end : $downtime->end, false, true)
+ );
+ }
+ }
+ ?>
+ <dt>
+ <?= $this->escape(sprintf(
+ $downtime->is_flexible
+ ? $this->translate('Flexible downtime by %s')
+ : $this->translate('Fixed downtime by %s'),
+ $downtime->author_name
+ )) ?>
+ <span class="comment-time">
+ <?= $state ?>
+ <span aria-hidden="true">&#448;</span>
+ <?= $this->translate('created') ?>
+ <?= $this->timeAgo($downtime->entry_time) ?>
+ </span>
+ <?php if (isset($delDowntimeForm)) {
+ // Form is unset if the current user lacks the respective permission
+ $deleteButton = clone($delDowntimeForm);
+ /** @var \Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimeCommandForm $deleteButton */
+ $deleteButton->setAttrib('class', $deleteButton->getAttrib('class') . ' remove-action');
+ $deleteButton->populate(
+ array(
+ 'downtime_id' => $downtime->id,
+ 'downtime_is_service' => $object->getType() === $object::TYPE_SERVICE,
+ 'downtime_name' => $downtime->name
+ )
+ );
+ echo $deleteButton;
+ } ?>
+ </dt>
+ <dd>
+ <?= $this->nl2br($this->createTicketLinks($this->markdown($downtime->comment))) ?>
+ </dd>
+ <?php endforeach ?>
+ </dl>
+ <?php endif ?>
+ </td>
+</tr>
diff --git a/modules/monitoring/application/views/scripts/show/components/extensions.phtml b/modules/monitoring/application/views/scripts/show/components/extensions.phtml
new file mode 100644
index 0000000..263b7e4
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/extensions.phtml
@@ -0,0 +1,4 @@
+<?php
+foreach ($extensionsHtml as $extensionHtml) {
+ echo $extensionHtml;
+}
diff --git a/modules/monitoring/application/views/scripts/show/components/flags.phtml b/modules/monitoring/application/views/scripts/show/components/flags.phtml
new file mode 100644
index 0000000..871a4dd
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/flags.phtml
@@ -0,0 +1,4 @@
+<div data-base-target="_self">
+ <h2><?= $this->translate('Feature Commands') ?></h2>
+ <?= $toggleFeaturesForm ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/show/components/flapping.phtml b/modules/monitoring/application/views/scripts/show/components/flapping.phtml
new file mode 100644
index 0000000..f09b107
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/flapping.phtml
@@ -0,0 +1,14 @@
+<?php
+
+if ($object->is_flapping) {
+ printf(
+ "<tr><th>%s</th><td>%s %s</td></tr>\n",
+ 'Flapping',
+ $this->icon('flapping', 'Flapping'),
+ sprintf(
+ 'Currently flapping with a %.2f%% state change rate',
+ $object->percent_state_change
+ )
+ );
+}
+
diff --git a/modules/monitoring/application/views/scripts/show/components/grapher.phtml b/modules/monitoring/application/views/scripts/show/components/grapher.phtml
new file mode 100644
index 0000000..0b49e63
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/grapher.phtml
@@ -0,0 +1,6 @@
+<?php if (isset($graphers)) {
+ foreach ($graphers as $grapher) {
+ echo $grapher->getPreviewHtml($object);
+ }
+} ?>
+
diff --git a/modules/monitoring/application/views/scripts/show/components/hostgroups.phtml b/modules/monitoring/application/views/scripts/show/components/hostgroups.phtml
new file mode 100644
index 0000000..377b56f
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/hostgroups.phtml
@@ -0,0 +1,19 @@
+<?php
+
+if (empty($object->hostgroups)) return;
+
+$list = array();
+foreach ($object->hostgroups as $name => $alias) {
+ $list[] = $this->qlink(
+ $alias,
+ 'monitoring/list/hosts',
+ array('hostgroup_name' => $name),
+ array('title' => sprintf($this->translate('List all hosts in the group "%s"'), $alias))
+ );
+}
+printf(
+ "<tr><th>%s</th><td class=\"go-ahead\">%s</td></tr>\n",
+ $this->translate('Hostgroups'),
+ implode(', ', $list)
+);
+
diff --git a/modules/monitoring/application/views/scripts/show/components/notes.phtml b/modules/monitoring/application/views/scripts/show/components/notes.phtml
new file mode 100644
index 0000000..c868c95
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/notes.phtml
@@ -0,0 +1,48 @@
+<?php
+
+use Icinga\Web\Navigation\Navigation;
+
+/** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+
+$navigation = new Navigation();
+$notes = trim($object->notes);
+
+$links = $object->getNotesUrls();
+if (! empty($links)) {
+ foreach ($links as $link) {
+ $navigation->addItem(
+ // add warning to links that open in new tabs to improve accessibility, as recommended by WCAG20 G201
+ $this->icon(
+ 'forward',
+ $this->translate('Link opens in new window'),
+ array('aria-label' => $this->translate('Link opens in new window'))
+ ) . ' ' . $this->escape($link),
+ array(
+ 'url' => $link,
+ 'target' => '_blank',
+ 'renderer' => array(
+ 'NavigationItemRenderer',
+ 'escape_label' => false
+ )
+ )
+ );
+ }
+}
+
+if (($navigation->isEmpty() || ! $navigation->hasRenderableItems()) && $notes === '') {
+ return;
+}
+?>
+<tr>
+ <th><?= $this->translate('Notes') ?></th>
+ <td>
+ <?= $navigation->getRenderer() ?>
+ <?php if ($notes !== ''): ?>
+ <?= $this->markdown($notes, [
+ 'id' => $object->type . '-notes',
+ 'class' => 'collapsible',
+ 'data-visible-height' => 200
+ ]) ?>
+ <?php endif ?>
+ </td>
+</tr> \ No newline at end of file
diff --git a/modules/monitoring/application/views/scripts/show/components/notifications.phtml b/modules/monitoring/application/views/scripts/show/components/notifications.phtml
new file mode 100644
index 0000000..3e8c665
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/notifications.phtml
@@ -0,0 +1,68 @@
+<tr>
+ <th><?= $this->translate('Notifications') ?></th>
+ <td>
+ <?php
+ /** @var \Icinga\Module\Monitoring\Object\MonitoredObject $object */
+ if ($this->hasPermission('monitoring/command/send-custom-notification')) {
+ if ($object->getType() === $object::TYPE_HOST) {
+ /** @var \Icinga\Module\Monitoring\Object\Host $object */
+ echo $this->qlink(
+ $this->translate('Send notification'),
+ 'monitoring/host/send-custom-notification',
+ array('host' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'bell',
+ 'title' => $this->translate(
+ 'Send a custom notification to contacts responsible for this host'
+ )
+ )
+ );
+ } else {
+ /** @var \Icinga\Module\Monitoring\Object\Service $object */
+ echo $this->qlink(
+ $this->translate('Send notification'),
+ 'monitoring/service/send-custom-notification',
+ array('host' => $object->getHost()->getName(), 'service' => $object->getName()),
+ array(
+ 'class' => 'action-link',
+ 'data-base-target' => '_self',
+ 'icon' => 'bell',
+ 'title' => $this->translate(
+ 'Send a custom notification to contacts responsible for this service'
+ )
+ )
+ );
+ }
+ if (! in_array((int) $object->state, array(0, 99))) {
+ echo '<br>';
+ }
+ } elseif (in_array((int) $object->state, array(0, 99))) {
+ echo '&#45;';
+ }
+ // We are not interested in notifications for OK or pending objects
+ if (! in_array((int) $object->state, array(0, 99))) {
+ if ($object->current_notification_number > 0) {
+ if ((int) $object->current_notification_number === 1) {
+ $msg = sprintf(
+ $this->translate('A notification has been sent for this issue %s.'),
+ $this->timeAgo($object->last_notification)
+ );
+ } else {
+ $msg = sprintf(
+ $this->translate('%d notifications have been sent for this issue.'),
+ $object->current_notification_number
+ ) . '<br>' . sprintf(
+ $this->translate('The last one was sent %s.'),
+ $this->timeAgo($object->last_notification)
+ );
+ }
+ } else {
+ $msg = $this->translate('No notification has been sent for this issue.');
+ }
+ echo $msg;
+ }
+ ?>
+ </td>
+</tr>
diff --git a/modules/monitoring/application/views/scripts/show/components/output.phtml b/modules/monitoring/application/views/scripts/show/components/output.phtml
new file mode 100644
index 0000000..34d8268
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/output.phtml
@@ -0,0 +1,5 @@
+<h2><?= $this->translate('Plugin Output') ?></h2>
+<div id="check-output-<?= $this->escape(str_replace(' ', '-', $object->check_command)) ?>" class="collapsible" data-visible-height="100">
+ <?= $this->pluginOutput($object->output, false, $object->check_command) ?>
+ <?= $this->pluginOutput($object->long_output, false, $object->check_command) ?>
+</div>
diff --git a/modules/monitoring/application/views/scripts/show/components/perfdata.phtml b/modules/monitoring/application/views/scripts/show/components/perfdata.phtml
new file mode 100644
index 0000000..78ea6d2
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/perfdata.phtml
@@ -0,0 +1,4 @@
+<?php if ($object->perfdata): ?>
+<h2><?= $this->translate('Performance data') ?></h2>
+<div id="check-perfdata-<?= $this->escape(str_replace(' ', '-', $object->check_command)) ?>"><?= $this->perfdata($object->perfdata) ?></div>
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/show/components/reachable.phtml b/modules/monitoring/application/views/scripts/show/components/reachable.phtml
new file mode 100644
index 0000000..8d55e84
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/reachable.phtml
@@ -0,0 +1,15 @@
+<?php if ($object->is_reachable !== null): ?>
+<tr>
+ <th>
+ <?= $this->translate('Reachable') ?>
+ </th>
+ <td>
+ <span class="check-source-meta"><?= (bool) $object->is_reachable ? $this->translate('yes') : $this->translate('no') ?></span>
+ <?php if ((bool) $object->is_reachable) {
+ echo $this->icon('circle', $this->translate('Is reachable'), array('class' => 'icon-stateful state-ok'));
+ } else {
+ echo $this->icon('circle', $this->translate('Not reachable'), array('class' => 'icon-stateful state-critical'));
+ } ?>
+ </td>
+</tr>
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/show/components/servicegroups.phtml b/modules/monitoring/application/views/scripts/show/components/servicegroups.phtml
new file mode 100644
index 0000000..09ff248
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/servicegroups.phtml
@@ -0,0 +1,20 @@
+<?php
+
+if (empty($object->servicegroups)) return;
+
+$list = array();
+foreach ($object->servicegroups as $name => $alias) {
+ $list[] = $this->qlink(
+ $alias,
+ 'monitoring/list/services',
+ array('servicegroup_name' => $name),
+ array('title' => sprintf($this->translate('List all services in the group "%s"'), $alias))
+ );
+}
+
+printf(
+ "<tr><th>%s</th><td class=\"go-ahead\">%s</td></tr>\n",
+ $this->translate('Servicegroups'),
+ implode(', ', $list)
+);
+
diff --git a/modules/monitoring/application/views/scripts/show/components/status.phtml b/modules/monitoring/application/views/scripts/show/components/status.phtml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/components/status.phtml
diff --git a/modules/monitoring/application/views/scripts/show/contact.phtml b/modules/monitoring/application/views/scripts/show/contact.phtml
new file mode 100644
index 0000000..b0fce72
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/contact.phtml
@@ -0,0 +1,67 @@
+<?php $contactHelper = $this->getHelper('ContactFlags') ?>
+<div class="controls">
+ <?php if (! $this->compact): ?>
+ <?= $this->tabs; ?>
+ <?php endif ?>
+ <h1><?= $this->translate('Contact details') ?></h1>
+
+<?php if (! $contact): ?>
+ <?= $this->translate('No such contact') ?>: <?= $contactName ?>
+</div>
+<?php return; endif ?>
+
+ <table class="name-value-table">
+ <tbody>
+ <tr>
+ <th></th>
+ <td><strong><?= $this->escape($contact->contact_alias) ?></strong> (<?= $contact->contact_name ?>)</td>
+ </tr>
+<?php if ($contact->contact_email): ?>
+ <tr>
+ <th><?= $this->translate('Email') ?></th>
+ <td>
+ <a href="mailto:<?= $contact->contact_email; ?>" title="<?= sprintf($this->translate('Send a mail to %s'), $contact->contact_alias); ?>" aria-label="<?= sprintf($this->translate('Send a mail to %s'), $contact->contact_alias); ?>">
+ <?= $this->escape($contact->contact_email); ?>
+ </a>
+ </td>
+ </tr>
+<?php endif ?>
+<?php if ($contact->contact_pager): ?>
+ <tr>
+ <th><?= $this->translate('Pager') ?></th>
+ <td><?= $this->escape($contact->contact_pager) ?></td>
+ </tr>
+<?php endif ?>
+ <tr>
+ <th><?= $this->translate('Hosts') ?></th>
+ <td><?= $this->escape($contactHelper->contactFlags($contact, 'host')) ?><br />
+ <?= $this->escape($contact->contact_notify_host_timeperiod) ?></td>
+ </tr>
+ <tr>
+ <th><?= $this->translate('Services') ?></th>
+ <td><?= $this->escape($contactHelper->contactFlags($contact, 'service')) ?><br />
+ <?= $this->escape($contact->contact_notify_service_timeperiod) ?></td>
+ </tr>
+ </tbody>
+ </table>
+ <?php if (count($commands)): ?>
+ <h1><?= $this->translate('Commands') ?>:</h1>
+ <ul>
+ <?php foreach ($commands as $command): ?>
+ <li><?= $command->command_name ?></li>
+ <?php endforeach ?>
+ </ul>
+ <?php endif ?>
+ <h1><?= $this->translate('Notifications sent to this contact') ?></h1>
+ <?= $this->limiter; ?>
+ <?= $this->paginator; ?>
+</div>
+
+<?php if (count($notifications)): ?>
+<?= $this->partial('list/notifications.phtml', array(
+ 'notifications' => $notifications,
+ 'compact' => true
+)); ?>
+<?php else: ?>
+<div class="content"><?= $this->translate('No notifications have been sent for this contact') ?></div>
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/tactical/components/hostservicechecks.phtml b/modules/monitoring/application/views/scripts/tactical/components/hostservicechecks.phtml
new file mode 100644
index 0000000..e6dc0be
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/tactical/components/hostservicechecks.phtml
@@ -0,0 +1,131 @@
+<div class="box hostservicechecks col-1-2">
+ <div class="box header">
+ <h2><?= $this->translate('Host and Service Checks'); ?></h2>
+ </div>
+ <div class="box contents">
+ <table>
+ <thead>
+ <tr>
+ <th><?= $this->translate('Hosts'); ?></th>
+ <th><?= $this->translate('Services'); ?></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+<?php if ($this->statusSummary->hosts_active): ?>
+ <div class="box entry"><?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%u Active', '%u Active', $this->statusSummary->hosts_active),
+ $this->statusSummary->hosts_active
+ ),
+ 'monitoring/list/hosts',
+ array('host_active_checks_enabled' => 1),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u actively checked host',
+ 'List %u actively checked hosts',
+ $this->statusSummary->hosts_active
+ ),
+ $this->statusSummary->hosts_active
+ ))
+ ); ?></div>
+<?php endif ?>
+<?php if ($this->statusSummary->hosts_passive): ?>
+ <div class="box entry"><?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%d Passive', '%d Passive', $this->statusSummary->hosts_passive),
+ $this->statusSummary->hosts_passive
+ ),
+ 'monitoring/list/hosts',
+ array('host_active_checks_enabled' => 0, 'host_passive_checks_enabled' => 1),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u passively checked host',
+ 'List %u passively checked hosts',
+ $this->statusSummary->hosts_passive
+ ),
+ $this->statusSummary->hosts_passive
+ ))
+ ); ?></div>
+<?php endif ?>
+<?php if ($this->statusSummary->hosts_not_checked): ?>
+ <div class="box entry"><?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%d Disabled', '%d Disabled', $this->statusSummary->hosts_not_checked),
+ $this->statusSummary->hosts_not_checked
+ ),
+ 'monitoring/list/hosts',
+ array('host_active_checks_enabled' => 0, 'host_passive_checks_enabled' => 0),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u host that is not being checked at all',
+ 'List %u hosts which are not being checked at all',
+ $this->statusSummary->hosts_not_checked
+ ),
+ $this->statusSummary->hosts_not_checked
+ ))
+ ); ?></div>
+<?php endif ?>
+ </td>
+ <td>
+<?php if ($this->statusSummary->services_active): ?>
+ <div class="box entry"><?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%d Active', '%d Active', $this->statusSummary->services_active),
+ $this->statusSummary->services_active
+ ),
+ 'monitoring/list/services',
+ array('service_active_checks_enabled' => 1),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u actively checked service',
+ 'List %u actively checked services',
+ $this->statusSummary->services_active
+ ),
+ $this->statusSummary->services_active
+ ))
+ ); ?></div>
+<?php endif ?>
+<?php if ($this->statusSummary->services_passive): ?>
+ <div class="box entry"><?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%d Passive', '%d Passive', $this->statusSummary->services_passive),
+ $this->statusSummary->services_passive
+ ),
+ 'monitoring/list/services',
+ array('service_active_checks_enabled' => 0, 'service_passive_checks_enabled' => 1),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u passively checked service',
+ 'List %u passively checked services',
+ $this->statusSummary->services_passive
+ ),
+ $this->statusSummary->services_passive
+ ))
+ ); ?></div>
+<?php endif ?>
+<?php if ($this->statusSummary->services_not_checked): ?>
+ <div class="box entry"><?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%d Disabled', '%d Disabled', $this->statusSummary->services_not_checked),
+ $this->statusSummary->services_not_checked
+ ),
+ 'monitoring/list/services',
+ array('service_active_checks_enabled' => 0, 'service_passive_checks_enabled' => 0),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is not being checked at all',
+ 'List %u services which are not being checked at all',
+ $this->statusSummary->services_not_checked
+ ),
+ $this->statusSummary->services_not_checked
+ ))
+ ); ?></div>
+<?php endif ?>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</div>
diff --git a/modules/monitoring/application/views/scripts/tactical/components/monitoringfeatures.phtml b/modules/monitoring/application/views/scripts/tactical/components/monitoringfeatures.phtml
new file mode 100644
index 0000000..eeeec16
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/tactical/components/monitoringfeatures.phtml
@@ -0,0 +1,287 @@
+<div class="box monitoringfeatures col-1-2">
+ <div class="box header">
+ <h2><?= $this->translate('Monitoring Features'); ?></h2>
+ </div>
+ <div class="box contents">
+<?php if ($this->statusSummary->hosts_without_flap_detection || $this->statusSummary->services_without_flap_detection ||
+ $this->statusSummary->hosts_flapping || $this->statusSummary->services_flapping): ?>
+ <div class="box-separator badge feature-highlight"><?= $this->translate('Flap Detection'); ?></div>
+<?php else: ?>
+ <div class="box-separator badge"><?= $this->translate('Flap Detection'); ?></div>
+<?php endif ?>
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ <div class="box entry">
+<?php if ($this->statusSummary->hosts_without_flap_detection): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%u Host Disabled', '%u Hosts Disabled', $this->statusSummary->hosts_without_flap_detection),
+ $this->statusSummary->hosts_without_flap_detection
+ ),
+ 'monitoring/list/hosts',
+ array('host_flap_detection_enabled' => 0),
+ array(
+ 'class' => 'feature-highlight',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %u host for which flap detection has been disabled',
+ 'List %u hosts for which flap detection has been disabled',
+ $this->statusSummary->hosts_without_flap_detection
+ ),
+ $this->statusSummary->hosts_without_flap_detection
+ )
+ )
+ ); ?>
+<?php else: ?>
+ <?= $this->qlink(
+ $this->translate('All Hosts Enabled'),
+ 'monitoring/list/hosts',
+ array('host_flap_detection_enabled' => 1),
+ array('title' => $this->translate(
+ 'List all hosts, for which flap detection is enabled entirely'
+ ))
+ ); ?>
+<?php endif ?>
+<?php if ($this->statusSummary->hosts_flapping): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%u Host Flapping', '%u Hosts Flapping', $this->statusSummary->hosts_flapping),
+ $this->statusSummary->hosts_flapping
+ ),
+ 'monitoring/list/hosts',
+ array('host_is_flapping' => 1),
+ array(
+ 'class' => 'feature-highlight',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %u host that is currently flapping',
+ 'List %u hosts which are currently flapping',
+ $this->statusSummary->hosts_flapping
+ ),
+ $this->statusSummary->hosts_flapping
+ )
+ )
+ ); ?>
+<?php endif ?>
+ </div>
+ </td>
+ <td>
+ <div class="box entry">
+<?php if ($this->statusSummary->services_without_flap_detection): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%u Service Disabled', '%u Services Disabled', $this->statusSummary->services_without_flap_detection),
+ $this->statusSummary->services_without_flap_detection
+ ),
+ 'monitoring/list/services',
+ array('service_flap_detection_enabled' => 0),
+ array(
+ 'class' => 'feature-highlight',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %u service for which flap detection has been disabled',
+ 'List %u services for which flap detection has been disabled',
+ $this->statusSummary->services_without_flap_detection
+ ),
+ $this->statusSummary->services_without_flap_detection
+ )
+ )
+ ); ?>
+<?php else: ?>
+ <?= $this->qlink(
+ $this->translate('All Services Enabled'),
+ 'monitoring/list/services',
+ array('service_flap_detection_enabled' => 1),
+ array('title' => $this->translate(
+ 'List all services, for which flap detection is enabled entirely'
+ ))
+ ); ?>
+<?php endif ?>
+<?php if ($this->statusSummary->services_flapping): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%u Service Flapping', '%u Services Flapping', $this->statusSummary->services_flapping),
+ $this->statusSummary->services_flapping
+ ),
+ 'monitoring/list/services',
+ array('service_is_flapping' => 1),
+ array(
+ 'class' => 'feature-highlight',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently flapping',
+ 'List %u services which are currently flapping',
+ $this->statusSummary->services_flapping
+ ),
+ $this->statusSummary->services_flapping
+ )
+ )
+ ); ?>
+<?php endif ?>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+<?php if ($this->statusSummary->hosts_not_triggering_notifications || $this->statusSummary->services_not_triggering_notifications): ?>
+ <div class="box-separator badge feature-highlight"><?= $this->translate('Notifications'); ?></div>
+<?php else: ?>
+ <div class="box-separator badge"><?= $this->translate('Notifications'); ?></div>
+<?php endif ?>
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ <div class="box entry">
+<?php if ($this->statusSummary->hosts_not_triggering_notifications): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%u Host Disabled', '%u Hosts Disabled', $this->statusSummary->hosts_not_triggering_notifications),
+ $this->statusSummary->hosts_not_triggering_notifications
+ ),
+ 'monitoring/list/hosts',
+ array('host_notifications_enabled' => 0),
+ array(
+ 'class' => 'feature-highlight',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %u host for which notifications are suppressed',
+ 'List %u hosts for which notifications are suppressed',
+ $this->statusSummary->hosts_not_triggering_notifications
+ ),
+ $this->statusSummary->hosts_not_triggering_notifications
+ )
+ )
+ ); ?>
+<?php else: ?>
+ <?= $this->qlink(
+ $this->translate('All Hosts Enabled'),
+ 'monitoring/list/hosts',
+ array('host_notifications_enabled' => 1),
+ array('title' => $this->translate(
+ 'List all hosts, for which notifications are enabled entirely'
+ ))
+ ); ?>
+<?php endif ?>
+ </div>
+ </td>
+ <td>
+ <div class="box entry">
+<?php if ($this->statusSummary->services_not_triggering_notifications): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%u Service Disabled', '%u Services Disabled', $this->statusSummary->services_not_triggering_notifications),
+ $this->statusSummary->services_not_triggering_notifications
+ ),
+ 'monitoring/list/services',
+ array('service_notifications_enabled' => 0),
+ array(
+ 'class' => 'feature-highlight',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %u service for which notifications are suppressed',
+ 'List %u services for which notifications are suppressed',
+ $this->statusSummary->services_not_triggering_notifications
+ ),
+ $this->statusSummary->services_not_triggering_notifications
+ )
+ )
+ ); ?>
+<?php else: ?>
+ <?= $this->qlink(
+ $this->translate('All Services Enabled'),
+ 'monitoring/list/services',
+ array('service_notifications_enabled' => 1),
+ array('title' => $this->translate(
+ 'List all services, for which notifications are enabled entirely'
+ ))
+ ); ?>
+<?php endif ?>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+<?php if ($this->statusSummary->hosts_not_processing_event_handlers || $this->statusSummary->services_not_processing_event_handlers): ?>
+ <div class="box-separator badge feature-highlight"><?= $this->translate('Event Handlers'); ?></div>
+<?php else: ?>
+ <div class="box-separator badge"><?= $this->translate('Event Handlers'); ?></div>
+<?php endif ?>
+ <table>
+ <tbody>
+ <tr>
+ <td>
+ <div class="box entry">
+<?php if ($this->statusSummary->hosts_not_processing_event_handlers): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%u Host Disabled', '%u Hosts Disabled', $this->statusSummary->hosts_not_processing_event_handlers),
+ $this->statusSummary->hosts_not_processing_event_handlers
+ ),
+ 'monitoring/list/hosts',
+ array('host_event_handler_enabled' => 0),
+ array(
+ 'class' => 'feature-highlight',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %u host that is not processing any event handlers',
+ 'List %u hosts which are not processing any event handlers',
+ $this->statusSummary->hosts_not_processing_event_handlers
+ ),
+ $this->statusSummary->hosts_not_processing_event_handlers
+ )
+ )
+ ); ?>
+<?php else: ?>
+ <?= $this->qlink(
+ $this->translate('All Hosts Enabled'),
+ 'monitoring/list/hosts',
+ array('host_event_handler_enabled' => 1),
+ array('title' => $this->translate(
+ 'List all hosts, which are processing event handlers entirely'
+ ))
+ ); ?>
+<?php endif ?>
+ </div>
+ </td>
+ <td>
+ <div class="box entry">
+<?php if ($this->statusSummary->services_not_processing_event_handlers): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%u Service Disabled', '%u Services Disabled', $this->statusSummary->services_not_processing_event_handlers),
+ $this->statusSummary->services_not_processing_event_handlers
+ ),
+ 'monitoring/list/services',
+ array('service_event_handler_enabled' => 0),
+ array(
+ 'class' => 'feature-highlight',
+ 'title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is not processing any event handlers',
+ 'List %u services which are not processing any event handlers',
+ $this->statusSummary->services_not_processing_event_handlers
+ ),
+ $this->statusSummary->services_not_processing_event_handlers
+ )
+ )
+ ); ?>
+<?php else: ?>
+ <?= $this->qlink(
+ $this->translate('All Services Enabled'),
+ 'monitoring/list/services',
+ array('service_event_handler_enabled' => 1),
+ array('title' => $this->translate(
+ 'List all services, which are processing event handlers entirely'
+ ))
+ ); ?>
+<?php endif ?>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</div>
diff --git a/modules/monitoring/application/views/scripts/tactical/components/ok_hosts.phtml b/modules/monitoring/application/views/scripts/tactical/components/ok_hosts.phtml
new file mode 100644
index 0000000..05ffd29
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/tactical/components/ok_hosts.phtml
@@ -0,0 +1,81 @@
+<?php
+$service_problems = (
+ $this->statusSummary->services_warning_handled_on_ok_hosts ||
+ $this->statusSummary->services_warning_unhandled_on_ok_hosts ||
+ $this->statusSummary->services_critical_handled_on_ok_hosts ||
+ $this->statusSummary->services_critical_unhandled_on_ok_hosts ||
+ $this->statusSummary->services_unknown_handled_on_ok_hosts ||
+ $this->statusSummary->services_unknown_unhandled_on_ok_hosts
+);
+?>
+<div class="box ok_hosts state_<?= $this->statusSummary->hosts_up ? 'up' : 'pending'; ?> col-1-2">
+ <div class="box header">
+ <?php if ($this->statusSummary->hosts_up): ?>
+ <h2><?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%u Host UP', '%u Hosts UP', $this->statusSummary->hosts_up),
+ $this->statusSummary->hosts_up
+ ),
+ 'monitoring/list/hosts',
+ array('host_state' => 0),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u host that is currently in state UP',
+ 'List %u hosts which are currently in state UP',
+ $this->statusSummary->hosts_up
+ ),
+ $this->statusSummary->hosts_up
+ ))
+ ); ?></h2>
+ <?php endif ?>
+ <?php if ($this->statusSummary->hosts_pending): ?>
+ <h2><?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%u Host PENDING', '%u Hosts PENDING', $this->statusSummary->hosts_pending),
+ $this->statusSummary->hosts_pending
+ ),
+ 'monitoring/list/hosts',
+ array('host_state' => 99),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u host that is currently in state PENDING',
+ 'List %u hosts which are currently in state PENDING',
+ $this->statusSummary->hosts_pending
+ ),
+ $this->statusSummary->hosts_pending
+ ))
+ ); ?></h2>
+ <?php endif ?>
+ </div>
+<?php if ($service_problems || $this->statusSummary->hosts_down || $this->statusSummary->hosts_unreachable): ?>
+ <div class="box contents">
+ <?= $this->partial(
+ 'tactical/components/parts/servicestatesummarybyhoststate.phtml',
+ array(
+ 'translationDomain' => $this->translationDomain,
+ 'host_problem' => 0,
+ 'services_ok' => $this->statusSummary->services_ok_on_ok_hosts,
+ 'services_ok_not_checked' => $this->statusSummary->services_ok_not_checked_on_ok_hosts,
+ 'services_pending' => $this->statusSummary->services_pending_on_ok_hosts,
+ 'services_pending_not_checked' => $this->statusSummary->services_pending_not_checked_on_ok_hosts,
+ 'services_warning_handled' => $this->statusSummary->services_warning_handled_on_ok_hosts,
+ 'services_warning_unhandled' => $this->statusSummary->services_warning_unhandled_on_ok_hosts,
+ 'services_warning_passive' => $this->statusSummary->services_warning_passive_on_ok_hosts,
+ 'services_warning_not_checked' => $this->statusSummary->services_warning_not_checked_on_ok_hosts,
+ 'services_critical_handled' => $this->statusSummary->services_critical_handled_on_ok_hosts,
+ 'services_critical_unhandled' => $this->statusSummary->services_critical_unhandled_on_ok_hosts,
+ 'services_critical_passive' => $this->statusSummary->services_critical_passive_on_ok_hosts,
+ 'services_critical_not_checked' => $this->statusSummary->services_critical_not_checked_on_ok_hosts,
+ 'services_unknown_handled' => $this->statusSummary->services_unknown_handled_on_ok_hosts,
+ 'services_unknown_unhandled' => $this->statusSummary->services_unknown_unhandled_on_ok_hosts,
+ 'services_unknown_passive' => $this->statusSummary->services_unknown_passive_on_ok_hosts,
+ 'services_unknown_not_checked' => $this->statusSummary->services_unknown_not_checked_on_ok_hosts
+ )
+ ); ?>
+<?php else: ?>
+ <div class="box contents zero">
+ <h3>0</h3>
+ <span><?= $this->translate('Service Problems'); ?></span>
+<?php endif ?>
+ </div>
+</div>
diff --git a/modules/monitoring/application/views/scripts/tactical/components/parts/servicestatesummarybyhoststate.phtml b/modules/monitoring/application/views/scripts/tactical/components/parts/servicestatesummarybyhoststate.phtml
new file mode 100644
index 0000000..4f32daf
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/tactical/components/parts/servicestatesummarybyhoststate.phtml
@@ -0,0 +1,394 @@
+<?php
+
+use Icinga\Module\Monitoring\Object\Service;
+
+?>
+<?php if ($services_critical_handled || $services_critical_unhandled): ?>
+<div class="box badge entry state-<?= Service::getStateText(2); ?> <?= $services_critical_unhandled ? '' : 'handled'; ?>">
+<?php if ($services_critical_unhandled): ?>
+ <?= $this->qlink(
+ $services_critical_unhandled . ' ' . Service::getStateText(2, true),
+ 'monitoring/list/services',
+ array(
+ 'host_problem' => $host_problem,
+ 'service_state' => 2,
+ 'service_acknowledged' => 0,
+ 'service_in_downtime' => 0
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state CRITICAL',
+ 'List %u services which are currently in state CRITICAL',
+ $services_critical_unhandled
+ ),
+ $services_critical_unhandled
+ ))
+ ); ?>
+<?php endif ?>
+<?php if ($services_critical_handled): ?>
+ <?= $this->qlink(
+ $services_critical_handled . ' ' . (
+ $services_critical_unhandled ? $this->translate('Handled') : Service::getStateText(2, true)
+ ),
+ 'monitoring/list/services',
+ array(
+ 'host_problem' => $host_problem,
+ 'service_state' => 2,
+ 'service_handled' => 1
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state CRITICAL (Handled)',
+ 'List %u services which are currently in state CRITICAL (Handled)',
+ $services_critical_handled
+ ),
+ $services_critical_handled
+ ))
+ ); ?>
+<?php endif ?>
+<?php if ($services_critical_passive): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ '%u is passively checked',
+ '%u are passively checked',
+ $services_critical_passive
+ ),
+ $services_critical_passive
+ ),
+ 'monitoring/list/services',
+ array(
+ 'service_state' => 2,
+ 'host_problem' => $host_problem,
+ 'service_active_checks_enabled' => 0,
+ 'service_passive_checks_enabled' => 1
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state CRITICAL and passively checked',
+ 'List %u services which are currently in state CRITICAL and passively checked',
+ $services_critical_passive
+ ),
+ $services_critical_passive
+ ))
+ ); ?>
+<?php endif ?>
+<?php if ($services_critical_not_checked): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ '%u is not checked at all',
+ '%u are not checked at all',
+ $services_critical_not_checked
+ ),
+ $services_critical_not_checked
+ ),
+ 'monitoring/list/services',
+ array(
+ 'service_state' => 2,
+ 'host_problem' => $host_problem,
+ 'service_active_checks_enabled' => 0,
+ 'service_passive_checks_enabled' => 0
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state CRITICAL and not checked at all',
+ 'List %u services which are currently in state CRITICAL and not checked at all',
+ $services_critical_not_checked
+ ),
+ $services_critical_not_checked
+ ))
+ ); ?>
+<?php endif ?>
+</div>
+<?php endif ?>
+<?php if ($services_warning_handled || $services_warning_unhandled): ?>
+<div class="box badge entry state-<?= Service::getStateText(1); ?> <?= $services_warning_unhandled ? '' : 'handled'; ?>">
+<?php if ($services_warning_unhandled): ?>
+ <?= $this->qlink(
+ $services_warning_unhandled . ' ' . Service::getStateText(1, true),
+ 'monitoring/list/services',
+ array(
+ 'host_problem' => $host_problem,
+ 'service_state' => 1,
+ 'service_acknowledged' => 0,
+ 'service_in_downtime' => 0
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state WARNING',
+ 'List %u services which are currently in state WARNING',
+ $services_warning_unhandled
+ ),
+ $services_warning_unhandled
+ ))
+ ); ?>
+<?php endif ?>
+<?php if ($services_warning_handled): ?>
+ <?= $this->qlink(
+ $services_warning_handled . ' ' . (
+ $services_warning_unhandled ? $this->translate('Handled') : Service::getStateText(1, true)
+ ),
+ 'monitoring/list/services',
+ array(
+ 'host_problem' => $host_problem,
+ 'service_state' => 1,
+ 'service_handled' => 1
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state WARNING (Handled)',
+ 'List %u services which are currently in state WARNING (Handled)',
+ $services_warning_handled
+ ),
+ $services_warning_handled
+ ))
+ ); ?>
+<?php endif ?>
+<?php if ($services_warning_passive): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ '%u is passively checked',
+ '%u are passively checked',
+ $services_warning_passive
+ ),
+ $services_warning_passive
+ ),
+ 'monitoring/list/services',
+ array(
+ 'service_state' => 1,
+ 'host_problem' => $host_problem,
+ 'service_active_checks_enabled' => 0,
+ 'service_passive_checks_enabled' => 1
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state WARNING and passively checked',
+ 'List %u services which are currently in state WARNING and passively checked',
+ $services_warning_passive
+ ),
+ $services_warning_passive
+ ))
+ ); ?>
+<?php endif ?>
+<?php if ($services_warning_not_checked): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ '%u is not checked at all',
+ '%u are not checked at all',
+ $services_warning_not_checked
+ ),
+ $services_warning_not_checked
+ ),
+ 'monitoring/list/services',
+ array(
+ 'service_state' => 1,
+ 'host_problem' => $host_problem,
+ 'service_active_checks_enabled' => 0,
+ 'service_passive_checks_enabled' => 0
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state WARNING and not checked at all',
+ 'List %u services which are currently in state WARNING and not checked at all',
+ $services_warning_not_checked
+ ),
+ $services_warning_not_checked
+ ))
+ ); ?>
+<?php endif ?>
+</div>
+<?php endif ?>
+<?php if ($services_unknown_handled || $services_unknown_unhandled): ?>
+<div class="box badge entry state-<?= Service::getStateText(3); ?> <?= $services_unknown_unhandled ? '' : 'handled'; ?>">
+<?php if ($services_unknown_unhandled): ?>
+ <?= $this->qlink(
+ $services_unknown_unhandled . ' ' . Service::getStateText(3, true),
+ 'monitoring/list/services',
+ array(
+ 'host_problem' => $host_problem,
+ 'service_state' => 3,
+ 'service_acknowledged' => 0,
+ 'service_in_downtime' => 0
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state UNKNOWN',
+ 'List %u services which are currently in state UNKNOWN',
+ $services_unknown_unhandled
+ ),
+ $services_unknown_unhandled
+ ))
+ ); ?>
+<?php endif ?>
+<?php if ($services_unknown_handled): ?>
+ <?= $this->qlink(
+ $services_unknown_handled . ' ' . (
+ $services_unknown_unhandled ? $this->translate('Handled') : Service::getStateText(3, true)
+ ),
+ 'monitoring/list/services',
+ array(
+ 'host_problem' => $host_problem,
+ 'service_state' => 3,
+ 'service_handled' => 1
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state UNKNOWN (Handled)',
+ 'List %u services which are currently in state UNKNOWN (Handled)',
+ $services_unknown_handled
+ ),
+ $services_unknown_handled
+ ))
+ ); ?>
+<?php endif ?>
+<?php if ($services_unknown_passive): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ '%u is passively checked',
+ '%u are passively checked',
+ $services_unknown_passive
+ ),
+ $services_unknown_passive
+ ),
+ 'monitoring/list/services',
+ array(
+ 'service_state' => 3,
+ 'host_problem' => $host_problem,
+ 'service_active_checks_enabled' => 0,
+ 'service_passive_checks_enabled' => 1
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state UNKNOWN and passively checked',
+ 'List %u services which are currently in state UNKNOWN and passively checked',
+ $services_unknown_passive
+ ),
+ $services_unknown_passive
+ ))
+ ); ?>
+<?php endif ?>
+<?php if ($services_unknown_not_checked): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ '%u is not checked at all',
+ '%u are not checked at all',
+ $services_unknown_not_checked
+ ),
+ $services_unknown_not_checked
+ ),
+ 'monitoring/list/services',
+ array(
+ 'service_state' => 3,
+ 'host_problem' => $host_problem,
+ 'service_active_checks_enabled' => 0,
+ 'service_passive_checks_enabled' => 0
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state UNKNOWN and not checked at all',
+ 'List %u services which are currently in state UNKNOWN and not checked at all',
+ $services_unknown_not_checked
+ ),
+ $services_unknown_not_checked
+ ))
+ ); ?>
+<?php endif ?>
+</div>
+<?php endif ?>
+<?php if ($services_ok): ?>
+<div class="box badge entry state-<?= Service::getStateText(0); ?>">
+ <?= $this->qlink(
+ $services_ok . ' ' . Service::getStateText(0, true),
+ 'monitoring/list/services',
+ array(
+ 'host_problem' => $host_problem,
+ 'service_state' => 0
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state OK',
+ 'List %u services which are currently in state OK',
+ $services_ok
+ ),
+ $services_ok
+ ))
+ ); ?>
+<?php if ($services_ok_not_checked): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ '%u is not checked at all',
+ '%u are not checked at all',
+ $services_ok_not_checked
+ ),
+ $services_ok_not_checked
+ ),
+ 'monitoring/list/services',
+ array(
+ 'service_state' => 0,
+ 'host_problem' => $host_problem,
+ 'service_active_checks_enabled' => 0,
+ 'service_passive_checks_enabled' => 0
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state OK and not checked at all',
+ 'List %u services which are currently in state OK and not checked at all',
+ $services_ok_not_checked
+ ),
+ $services_ok_not_checked
+ ))
+ ); ?>
+<?php endif ?>
+</div>
+<?php endif ?>
+<?php if ($services_pending): ?>
+<div class="box badge entry state-<?= Service::getStateText(99); ?>">
+ <?= $this->qlink(
+ $services_pending . ' ' . Service::getStateText(99, true),
+ 'monitoring/list/services',
+ array(
+ 'service_state' => 99
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state PENDING',
+ 'List %u services which are currently in state PENDING',
+ $services_pending
+ ),
+ $services_pending
+ ))
+ ); ?>
+<?php if ($services_pending_not_checked): ?>
+ <?= $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ '%u is not checked at all',
+ '%u are not checked at all',
+ $services_pending_not_checked
+ ),
+ $services_pending_not_checked
+ ),
+ 'monitoring/list/services',
+ array(
+ 'service_state' => 99,
+ 'service_active_checks_enabled' => 0,
+ 'service_passive_checks_enabled' => 0
+ ),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u service that is currently in state PENDING and not checked at all',
+ 'List %u services which are currently in state PENDING and not checked at all',
+ $services_pending_not_checked
+ ),
+ $services_pending_not_checked
+ ))
+ ); ?>
+<?php endif ?>
+</div>
+<?php endif ?>
diff --git a/modules/monitoring/application/views/scripts/tactical/components/problem_hosts.phtml b/modules/monitoring/application/views/scripts/tactical/components/problem_hosts.phtml
new file mode 100644
index 0000000..6374ff8
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/tactical/components/problem_hosts.phtml
@@ -0,0 +1,74 @@
+<div class="box problem_hosts <?php
+ echo $this->statusSummary->hosts_down ? 'state_down' : 'state_unreachable';
+ if (!$this->statusSummary->hosts_down_unhandled && !$this->statusSummary->hosts_unreachable_unhandled) {
+ echo ' handled';
+ }
+?> col-1-2">
+ <div class="box header">
+ <?php if ($this->statusSummary->hosts_down): ?>
+ <h2><?= $this->qlink(
+ sprintf(
+ $this->translatePlural('%u Host DOWN', '%u Hosts DOWN', $this->statusSummary->hosts_down),
+ $this->statusSummary->hosts_down
+ ),
+ 'monitoring/list/hosts',
+ array('host_state' => 1),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u host that is currently in state DOWN',
+ 'List %u hosts which are currently in state DOWN',
+ $this->statusSummary->hosts_down
+ ),
+ $this->statusSummary->hosts_down
+ ))
+ ); ?></h2>
+ <?php endif ?>
+ <?php if ($this->statusSummary->hosts_unreachable): ?>
+ <h2><?= $this->qlink(
+ sprintf(
+ $this->translatePlural(
+ '%u Host UNREACHABLE',
+ '%u Hosts UNREACHABLE',
+ $this->statusSummary->hosts_unreachable
+ ),
+ $this->statusSummary->hosts_unreachable
+ ),
+ 'monitoring/list/hosts',
+ array('host_state' => 2),
+ array('title' => sprintf(
+ $this->translatePlural(
+ 'List %u host that is currently in state UNREACHABLE',
+ 'List %u hosts which are currently in state UNREACHABLE',
+ $this->statusSummary->hosts_unreachable
+ ),
+ $this->statusSummary->hosts_unreachable
+ ))
+ ); ?></h2>
+ <?php endif ?>
+ </div>
+ <div class="box contents">
+ <?= $this->partial(
+ 'tactical/components/parts/servicestatesummarybyhoststate.phtml',
+ array(
+ 'translationDomain' => $this->translationDomain,
+ 'host_problem' => 1,
+ 'services_ok' => $this->statusSummary->services_ok_on_problem_hosts,
+ 'services_ok_not_checked' => $this->statusSummary->services_ok_not_checked_on_problem_hosts,
+ 'services_pending' => $this->statusSummary->services_pending_on_problem_hosts,
+ 'services_pending_not_checked' => $this->statusSummary->services_pending_not_checked_on_problem_hosts,
+ 'services_warning_handled' => $this->statusSummary->services_warning_handled_on_problem_hosts,
+ 'services_warning_unhandled' => $this->statusSummary->services_warning_unhandled_on_problem_hosts,
+ 'services_warning_passive' => $this->statusSummary->services_warning_passive_on_problem_hosts,
+ 'services_warning_not_checked' => $this->statusSummary->services_warning_not_checked_on_problem_hosts,
+ 'services_critical_handled' => $this->statusSummary->services_critical_handled_on_problem_hosts,
+ 'services_critical_unhandled' => $this->statusSummary->services_critical_unhandled_on_problem_hosts,
+ 'services_critical_passive' => $this->statusSummary->services_critical_passive_on_problem_hosts,
+ 'services_critical_not_checked' => $this->statusSummary->services_critical_not_checked_on_problem_hosts,
+ 'services_unknown_handled' => $this->statusSummary->services_unknown_handled_on_problem_hosts,
+ 'services_unknown_unhandled' => $this->statusSummary->services_unknown_unhandled_on_problem_hosts,
+ 'services_unknown_passive' => $this->statusSummary->services_unknown_passive_on_problem_hosts,
+ 'services_unknown_not_checked' => $this->statusSummary->services_unknown_not_checked_on_problem_hosts
+ )
+ ); ?>
+ </div>
+</div>
diff --git a/modules/monitoring/application/views/scripts/tactical/index.phtml b/modules/monitoring/application/views/scripts/tactical/index.phtml
new file mode 100644
index 0000000..12f4bc5
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/tactical/index.phtml
@@ -0,0 +1,145 @@
+<?php
+use Icinga\Data\Filter\Filter;
+?>
+<?php if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?= $this->filterEditor ?>
+</div>
+<?php endif ?>
+<div class="content tactical grid">
+<?php if (! count(array_filter((array) $statusSummary))): ?>
+ <p><?= $this->translate('No results found matching the filter.') ?></p>
+</div>
+<?php return; endif ?>
+ <div class="boxview" data-base-target="_next">
+ <div class="donut-container">
+ <h2 aria-label="<?= $this->translate('Host Summary') ?>"><?= $this->translate('Host Summary') ?></h2>
+ <div class="donut">
+ <?= $hostStatusSummaryChart ?>
+ </div>
+ <ul class="donut-legend">
+ <?php if ($statusSummary->hosts_up): ?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/hosts', array('host_state' => 0, 'sort' => 'host_last_check', 'dir' => 'asc')) ?>">
+ <span class="state state-ok badge"><?= $statusSummary->hosts_up ?></span><?= $this->translate('Up') ?>
+ </a>
+ </li>
+ <?php endif ?>
+ <?php if ($statusSummary->hosts_down_handled): ?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/hosts', array('host_state' => 1, 'host_handled' => 1, 'sort' => 'host_last_check', 'dir' => 'asc')) ?>">
+ <span class="state state-critical handled badge"><?= $statusSummary->hosts_down_handled ?></span><?= $this->translate('Down') ?> (<?= $this->translate('Handled') ?>)
+ </a>
+ </li>
+ <?php endif ?>
+ <?php if ($statusSummary->hosts_down_unhandled): ?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/hosts', array('host_state' => 1, 'host_handled' => 0, 'sort' => 'host_last_check', 'dir' => 'asc')) ?>">
+ <span class="state state-critical badge"><?= $statusSummary->hosts_down_unhandled ?></span><?= $this->translate('Down') ?>
+ </a>
+ </li>
+ <?php endif ?>
+ <?php if ($statusSummary->hosts_unreachable_handled): ?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/hosts', array('host_state' => 2, 'host_handled' => 1, 'sort' => 'host_last_check', 'dir' => 'asc')) ?>">
+ <span class="state state-unreachable handled badge"><?= $statusSummary->hosts_unreachable_handled ?></span><?= $this->translate('Unreachable') ?> (<?= $this->translate('Handled') ?>)
+ </a>
+ </li>
+ <?php endif ?>
+ <?php if ($statusSummary->hosts_unreachable_unhandled): ?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/hosts', array('host_state' => 2, 'host_handled' => 0, 'sort' => 'host_last_check', 'dir' => 'asc')) ?>">
+ <span class="state state-unreachable badge"><?= $statusSummary->hosts_unreachable_unhandled ?></span><?= $this->translate('Unreachable') ?>
+ </a>
+ </li>
+ <?php endif ?>
+ <?php if ($statusSummary->hosts_pending): ?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/hosts', array('host_state' => 99, 'sort' => 'host_last_check', 'dir' => 'asc'))->addFilter(Filter::not(Filter::where('host_active_checks_enabled', 0), Filter::where('host_passive_checks_enabled', 0))) ?>">
+ <span class="state state-pending badge"><?= $statusSummary->hosts_pending ?></span><?= $this->translate('Pending') ?>
+ </a>
+ </li>
+ <?php endif ?>
+ <?php if ($statusSummary->hosts_pending_not_checked): ?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/hosts', array('host_state' => 99, 'host_active_checks_enabled' => 0, 'host_passive_checks_enabled' => 0, 'sort' => 'host_last_check', 'dir' => 'asc')) ?>">
+ <span class="state slice-state-not-checked badge"><?= $statusSummary->hosts_pending_not_checked ?></span><?= $this->translate('Not Checked') ?>
+ </a>
+ </li>
+ <?php endif ?>
+ </ul>
+ </div>
+ <div class="donut-container">
+ <h2 aria-label="<?= $this->translate('Service Summary') ?>"><?= $this->translate('Service Summary') ?></h2>
+ <div class="donut">
+ <?= $serviceStatusSummaryChart ?>
+ </div>
+ <ul class="donut-legend">
+ <?php if ($statusSummary->services_ok):?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 0, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>">
+ <span class="state state-ok badge"><?= $statusSummary->services_ok ?></span><?= $this->translate('Ok') ?>
+ </a>
+ </li>
+ <?php endif;
+ if ($statusSummary->services_warning_handled):?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 1, 'service_handled' => 1, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>">
+ <span class="state state-warning handled badge"><?= $statusSummary->services_warning_handled ?></span><?= $this->translate('Warning') ?> (<?= $this->translate('Handled') ?>)
+ </a>
+ </li>
+ <?php endif;
+ if ($statusSummary->services_warning_unhandled):?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 1, 'service_handled' => 0, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>">
+ <span class="state state-warning badge"><?= $statusSummary->services_warning_unhandled ?></span><?= $this->translate('Warning') ?>
+ </a>
+ </li>
+ <?php endif;
+ if ($statusSummary->services_critical_handled):?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 2, 'service_handled' => 1, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>">
+ <span class="state state-critical handled badge"><?= $statusSummary->services_critical_handled ?></span><?= $this->translate('Critical') ?> (<?= $this->translate('Handled') ?>)
+ </a>
+ </li>
+ <?php endif;
+ if ($statusSummary->services_critical_unhandled):?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 2, 'service_handled' => 0, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>">
+ <span class="state state-critical badge"><?= $statusSummary->services_critical_unhandled ?></span><?= $this->translate('Critical') ?>
+ </a>
+ </li>
+ <?php endif;
+ if ($statusSummary->services_unknown_handled):?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 3, 'service_handled' => 1, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>">
+ <span class="state state-unknown handled badge"><?= $statusSummary->services_unknown_handled ?></span><?= $this->translate('Unknown') ?> (<?= $this->translate('Handled') ?>)
+ </a>
+ </li>
+ <?php endif;
+ if ($statusSummary->services_unknown_unhandled):?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 3, 'service_handled' => 0, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>">
+ <span class="state state-unknown badge"><?= $statusSummary->services_unknown_unhandled ?></span><?= $this->translate('Unknown') ?>
+ </a>
+ </li>
+ <?php endif;
+ if ($statusSummary->services_pending):?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 99, 'sort' => 'service_last_check', 'dir' => 'asc'))->addFilter(Filter::not(Filter::where('service_active_checks_enabled', 0), Filter::where('service_passive_checks_enabled', 0))) ?>">
+ <span class="state state-pending badge"><?= $statusSummary->services_pending ?></span><?= $this->translate('Pending') ?>
+ </a>
+ </li>
+ <?php endif;
+ if ($statusSummary->services_pending_not_checked):?>
+ <li>
+ <a href="<?= $this->filteredUrl('monitoring/list/services', array('service_state' => 99, 'service_active_checks_enabled' => 0, 'service_passive_checks_enabled' => 0, 'sort' => 'service_last_check', 'dir' => 'asc')) ?>">
+ <span class="state slice-state-not-checked badge"><?= $statusSummary->services_pending_not_checked ?></span><?= $this->translate('Not Checked') ?>
+ </a>
+ </li>
+ <?php endif?>
+ </ul>
+ </div>
+ </div>
+</div>
diff --git a/modules/monitoring/application/views/scripts/timeline/index.phtml b/modules/monitoring/application/views/scripts/timeline/index.phtml
new file mode 100644
index 0000000..cab18eb
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/timeline/index.phtml
@@ -0,0 +1,196 @@
+<?php
+
+use Icinga\Util\Csp;
+use Icinga\Web\Url;
+use Icinga\Util\Color;
+use ipl\Web\Style;
+
+$groupInfo = $timeline->getGroupInfo();
+$firstRow = ! $beingExtended;
+$timelineStyle = (new Style())
+ ->setNonce(Csp::getStyleNonce())
+ ->setModule('monitoring');
+
+if (! $beingExtended && !$this->compact): ?>
+<div class="controls">
+ <?= $this->tabs; ?>
+ <div class="dontprint">
+ <?= $intervalBox; ?>
+ </div>
+ <div class="timeline-legend">
+ <h2><?= $this->translate('Legend'); ?></h2>
+<?php foreach ($groupInfo as $labelAndClass): ?>
+ <span class="<?= $labelAndClass['class'] ?>">
+ <span><?= $labelAndClass['label']; ?></span>
+ </span>
+<?php endforeach ?>
+ </div>
+</div>
+<?php endif ?>
+<?php if (! $beingExtended): ?>
+<div class="content" data-base-target="_next">
+ <div class="timeline">
+<?php endif ?>
+<?php if ($switchedContext): ?>
+ <hr>
+<?php endif ?>
+<?php foreach ($timeline as $timeInfo):
+ switch ($intervalBox->getInterval()) {
+ case '1d':
+ $titleTime = sprintf(
+ $this->translate('on %s', 'timeline.link.title.time'),
+ $timeInfo[0]->end->format('d/m/Y')
+ );
+ break;
+ case '1w':
+ $titleTime = sprintf(
+ $this->translate('in week %s of %s', 'timeline.link.title.week.and.year'),
+ $timeInfo[0]->end->format('W'),
+ $timeInfo[0]->end->format('Y')
+ );
+ break;
+ case '1m':
+ $titleTime = sprintf(
+ $this->translate('in %s', 'timeline.link.title.month.and.year'),
+ $timeInfo[0]->end->format('F Y')
+ );
+ break;
+ case '1y':
+ $titleTime = sprintf(
+ $this->translate('in %s', 'timeline.link.title.year'),
+ $timeInfo[0]->end->format('Y')
+ );
+ break;
+ default:
+ $titleTime = sprintf(
+ $this->translate('between %s and %s', 'timeline.link.title.datetime.twice'),
+ $timeInfo[0]->end->format('d/m/Y g:i A'),
+ $timeInfo[0]->start->format('d/m/Y g:i A')
+ );
+ } ?>
+ <div class="timeframe">
+ <span><?= $this->qlink(
+ $timeInfo[0]->end->format($intervalFormat),
+ 'monitoring/list/eventhistory',
+ array(
+ 'timestamp<' => $timeInfo[0]->start->getTimestamp(),
+ 'timestamp>' => $timeInfo[0]->end->getTimestamp()
+ ),
+ array('title' => sprintf(
+ $this->translate('List all event records registered %s', 'timeline.link.title'),
+ $titleTime
+ )),
+ false
+ ); ?></span>
+<?php foreach ($groupInfo as $groupName => $labelAndColor): ?>
+<?php if (array_key_exists($groupName, $timeInfo[1])): ?>
+<?php
+$styleId = uniqid();
+$circleWidth = $timeline->calculateCircleWidth($timeInfo[1][$groupName], 2);
+$extrapolatedCircleWidth = $timeline->getExtrapolatedCircleWidth($timeInfo[1][$groupName], 2);
+?>
+<?php if ($firstRow && $extrapolatedCircleWidth !== $circleWidth): ?>
+ <?php
+ $timelineStyle->add(
+ "#circle-box-$styleId",
+ ['width' => $extrapolatedCircleWidth]
+ );
+
+ $timelineStyle->add(
+ "#outer-circle-$styleId",
+ [
+ 'width' => $extrapolatedCircleWidth,
+ 'height' => $extrapolatedCircleWidth,
+ 'margin-top' => sprintf(
+ '-%Fem',
+ (float)substr($extrapolatedCircleWidth, 0, -2) / 2
+ )
+ ]
+ );
+
+ ?>
+ <div id="circle-box-<?= $styleId ?>" class="circle-box">
+ <div id="outer-circle-<?= $styleId ?>" class="outer-circle extrapolated <?= $timeInfo[1][$groupName]->getClass() ?>">
+<?php else: ?>
+ <?php
+ $timelineStyle->add(
+ "#circle-box-$styleId",
+ ['width' => $circleWidth]
+ );
+
+ $timelineStyle->add(
+ "#outer-circle-$styleId",
+ [
+ 'width' => $circleWidth,
+ 'height' => $circleWidth,
+ 'margin-top' => sprintf(
+ '-%Fem',
+ (float)substr($circleWidth, 0, -2) / 2
+ )
+ ]
+ );
+
+ ?>
+ <div id="circle-box-<?= $styleId ?>" class="circle-box">
+ <div id="outer-circle-<?= $styleId ?>" class="outer-circle">
+<?php endif ?>
+<?php
+$timelineStyle->add(
+ "#inner-circle-$styleId",
+ [
+ 'width' => $circleWidth,
+ 'height' => $circleWidth,
+ 'margin-top' => sprintf(
+ '-%Fem',
+ (float)substr($circleWidth, 0, -2) / 2
+ ),
+ 'margin-left' => sprintf(
+ '-%Fem',
+ (float)substr($circleWidth, 0, -2) / 2
+ ),
+ ]
+);
+?>
+ <?= $this->qlink(
+ '',
+ $timeInfo[1][$groupName]->getDetailUrl(),
+ array(
+ 'type' => $groupName,
+ 'timestamp<' => $timeInfo[0]->start->getTimestamp(),
+ 'timestamp>' => $timeInfo[0]->end->getTimestamp()
+ ),
+ array(
+ 'title' => sprintf(
+ $this->translate('List %u %s registered %s', 'timeline.link.title'),
+ $timeInfo[1][$groupName]->getValue(),
+ strtolower($labelAndColor['label']),
+ $titleTime
+ ),
+ 'id' => "inner-circle-$styleId",
+ 'class' => "inner-circle " . $timeInfo[1][$groupName]->getClass()
+ )
+ ); ?>
+ </div>
+ </div>
+<?php endif ?>
+<?php endforeach ?>
+ </div>
+ <?php $firstRow = false; ?>
+<?php endforeach ?>
+ <a aria-hidden="true" id="end" href="<?= Url::fromRequest()->remove(
+ array(
+ 'timestamp<',
+ 'timestamp>'
+ )
+ )->overwriteParams(
+ array(
+ 'start' => $nextRange->getStart()->getTimestamp(),
+ 'end' => $nextRange->getEnd()->getTimestamp(),
+ 'extend' => 1
+ )
+ ); ?>"></a>
+<?php if (!$beingExtended): ?>
+ </div>
+</div>
+<?php endif ?>
+<?= $timelineStyle; ?>
diff --git a/modules/monitoring/configuration.php b/modules/monitoring/configuration.php
new file mode 100644
index 0000000..653b01e
--- /dev/null
+++ b/modules/monitoring/configuration.php
@@ -0,0 +1,432 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+use Icinga\Authentication\Auth;
+
+/** @var $this \Icinga\Application\Modules\Module */
+
+$this->providePermission(
+ 'monitoring/command/*',
+ $this->translate('Allow all commands')
+);
+$this->providePermission(
+ 'monitoring/command/schedule-check',
+ $this->translate('Allow scheduling host and service checks')
+);
+$this->providePermission(
+ 'monitoring/command/schedule-check/active-only',
+ $this->translate('Allow scheduling host and service checks (Only on objects with active checks enabled)')
+);
+$this->providePermission(
+ 'monitoring/command/acknowledge-problem',
+ $this->translate('Allow acknowledging host and service problems')
+);
+$this->providePermission(
+ 'monitoring/command/remove-acknowledgement',
+ $this->translate('Allow removing problem acknowledgements')
+);
+$this->providePermission(
+ 'monitoring/command/comment/*',
+ $this->translate('Allow adding and deleting host and service comments')
+);
+$this->providePermission(
+ 'monitoring/command/comment/add',
+ $this->translate('Allow commenting on hosts and services')
+);
+$this->providePermission(
+ 'monitoring/command/comment/delete',
+ $this->translate('Allow deleting host and service comments')
+);
+$this->providePermission(
+ 'monitoring/command/downtime/*',
+ $this->translate('Allow scheduling and deleting host and service downtimes')
+);
+$this->providePermission(
+ 'monitoring/command/downtime/schedule',
+ $this->translate('Allow scheduling host and service downtimes')
+);
+$this->providePermission(
+ 'monitoring/command/downtime/delete',
+ $this->translate('Allow deleting host and service downtimes')
+);
+$this->providePermission(
+ 'monitoring/command/process-check-result',
+ $this->translate('Allow processing host and service check results')
+);
+$this->providePermission(
+ 'monitoring/command/feature/instance',
+ $this->translate('Allow processing commands for toggling features on an instance-wide basis')
+);
+$this->providePermission(
+ 'monitoring/command/feature/object/*',
+ $this->translate('Allow processing commands for toggling features on host and service objects')
+);
+$this->providePermission(
+ 'monitoring/command/feature/object/active-checks',
+ $this->translate('Allow processing commands for toggling active checks on host and service objects')
+);
+$this->providePermission(
+ 'monitoring/command/feature/object/passive-checks',
+ $this->translate('Allow processing commands for toggling passive checks on host and service objects')
+);
+$this->providePermission(
+ 'monitoring/command/feature/object/notifications',
+ $this->translate('Allow processing commands for toggling notifications on host and service objects')
+);
+$this->providePermission(
+ 'monitoring/command/feature/object/event-handler',
+ $this->translate('Allow processing commands for toggling event handlers on host and service objects')
+);
+$this->providePermission(
+ 'monitoring/command/feature/object/flap-detection',
+ $this->translate('Allow processing commands for toggling flap detection on host and service objects')
+);
+$this->providePermission(
+ 'monitoring/command/send-custom-notification',
+ $this->translate('Allow sending custom notifications for hosts and services')
+);
+$this->providePermission(
+ 'no-monitoring/contacts',
+ $this->translate('Prohibit access to contacts and contactgroups')
+);
+
+$this->provideRestriction(
+ 'monitoring/filter/objects',
+ $this->translate('Restrict views to the Icinga objects that match the filter')
+);
+$this->provideRestriction(
+ 'monitoring/blacklist/properties',
+ $this->translate('Hide the properties of monitored objects that match the filter')
+);
+
+$this->provideConfigTab('backends', array(
+ 'title' => $this->translate('Configure how to retrieve monitoring information'),
+ 'label' => $this->translate('Backends'),
+ 'url' => 'config'
+));
+$this->provideConfigTab('security', array(
+ 'title' => $this->translate('Configure how to protect your monitoring environment against prying eyes'),
+ 'label' => $this->translate('Security'),
+ 'url' => 'config/security'
+));
+$this->provideSetupWizard('Icinga\Module\Monitoring\MonitoringWizard');
+
+/*
+ * Available Search Urls
+ */
+$this->provideSearchUrl($this->translate('Tactical Overview'), 'monitoring/tactical', 100);
+$this->provideSearchUrl($this->translate('Hosts'), 'monitoring/list/hosts?sort=host_severity&limit=10', 99);
+$this->provideSearchUrl($this->translate('Services'), 'monitoring/list/services?sort=service_severity&limit=10', 98);
+$this->provideSearchUrl($this->translate('Hostgroups'), 'monitoring/list/hostgroups?limit=10', 97);
+$this->provideSearchUrl($this->translate('Servicegroups'), 'monitoring/list/servicegroups?limit=10', 96);
+
+/*
+ * Available navigation items
+ */
+$this->provideNavigationItem('host-action', $this->translate('Host Action'));
+$this->provideNavigationItem('service-action', $this->translate('Service Action'));
+// Notes are disabled as we're not sure whether to really make a difference between actions and notes
+//$this->provideNavigationItem('host-note', $this->translate('Host Note'));
+//$this->provideNavigationItem('service-note', $this->translate('Service Note'));
+
+/*
+ * Problems Section
+ */
+$section = $this->menuSection(N_('Problems'), array(
+ 'renderer' => array(
+ 'SummaryNavigationItemRenderer',
+ 'state' => 'critical'
+ ),
+ 'icon' => 'attention-circled',
+ 'priority' => 20
+));
+$section->add(N_('Host Problems'), array(
+ 'icon' => 'host',
+ 'description' => $this->translate('List current host problems'),
+ 'renderer' => array(
+ 'MonitoringBadgeNavigationItemRenderer',
+ 'columns' => array(
+ 'hosts_down_unhandled' => $this->translate('%d unhandled hosts down')
+ ),
+ 'state' => 'critical',
+ 'dataView' => 'unhandledhostproblems'
+ ),
+ 'url' => 'monitoring/list/hosts?host_problem=1&sort=host_severity',
+ 'priority' => 50
+));
+$section->add(N_('Service Problems'), array(
+ 'icon' => 'service',
+ 'description' => $this->translate('List current service problems'),
+ 'renderer' => array(
+ 'MonitoringBadgeNavigationItemRenderer',
+ 'columns' => array(
+ 'services_critical_unhandled' => $this->translate('%d unhandled services critical')
+ ),
+ 'state' => 'critical',
+ 'dataView' => 'unhandledserviceproblems'
+ ),
+ 'url' => 'monitoring/list/services?service_problem=1&sort=service_severity&dir=desc',
+ 'priority' => 60
+));
+$section->add(N_('Service Grid'), array(
+ 'icon' => 'services',
+ 'description' => $this->translate('Display service problems as grid'),
+ 'url' => 'monitoring/list/servicegrid?problems',
+ 'priority' => 70
+));
+$section->add(N_('Current Downtimes'), array(
+ 'icon' => 'plug',
+ 'description' => $this->translate('List current downtimes'),
+ 'url' => 'monitoring/list/downtimes?downtime_is_in_effect=1',
+ 'priority' => 80
+));
+
+/*
+ * Overview Section
+ */
+$section = $this->menuSection(N_('Overview'), array(
+ 'icon' => 'binoculars',
+ 'priority' => 30
+));
+$section->add(N_('Tactical Overview'), array(
+ 'icon' => 'chart-pie',
+ 'description' => $this->translate('Open tactical overview'),
+ 'url' => 'monitoring/tactical',
+ 'priority' => 40
+));
+$section->add(N_('Hosts'), array(
+ 'icon' => 'host',
+ 'description' => $this->translate('List hosts'),
+ 'url' => 'monitoring/list/hosts',
+ 'priority' => 50
+));
+$section->add(N_('Services'), array(
+ 'icon' => 'service',
+ 'description' => $this->translate('List services'),
+ 'url' => 'monitoring/list/services',
+ 'priority' => 50
+));
+$section->add(N_('Servicegroups'), array(
+ 'icon' => 'services',
+ 'description' => $this->translate('List service groups'),
+ 'url' => 'monitoring/list/servicegroups',
+ 'priority' => 60
+));
+$section->add(N_('Hostgroups'), array(
+ 'icon' => 'host',
+ 'description' => $this->translate('List host groups'),
+ 'url' => 'monitoring/list/hostgroups',
+ 'priority' => 60
+));
+
+// Checking the permission here since navigation items don't support negating permissions
+$auth = Auth::getInstance();
+if ($auth->hasPermission('*') || ! $auth->hasPermission('no-monitoring/contacts')) {
+ $section->add(N_('Contacts'), array(
+ 'icon' => 'user',
+ 'description' => $this->translate('List contacts'),
+ 'url' => 'monitoring/list/contacts',
+ 'priority' => 70
+ ));
+ $section->add(N_('Contactgroups'), array(
+ 'icon' => 'users',
+ 'description' => $this->translate('List users'),
+ 'url' => 'monitoring/list/contactgroups',
+ 'priority' => 70
+ ));
+}
+
+$section->add(N_('Comments'), array(
+ 'icon' => 'chat-empty',
+ 'description' => $this->translate('List comments'),
+ 'url' => 'monitoring/list/comments?comment_type=comment|comment_type=ack',
+ 'priority' => 80
+));
+$section->add(N_('Downtimes'), array(
+ 'icon' => 'plug',
+ 'description' => $this->translate('List downtimes'),
+ 'url' => 'monitoring/list/downtimes',
+ 'priority' => 80
+));
+
+/*
+ * History Section
+ */
+$section = $this->menuSection(N_('History'), array(
+ 'icon' => 'history',
+ 'priority' => 90
+));
+$section->add(N_('Event Grid'), array(
+ 'icon' => 'history',
+ 'description' => $this->translate('Open event grid'),
+ 'priority' => 10,
+ 'url' => 'monitoring/list/eventgrid'
+));
+$section->add(N_('Event Overview'), array(
+ 'icon' => 'history',
+ 'description' => $this->translate('Open event overview'),
+ 'priority' => 20,
+ 'url' => 'monitoring/list/eventhistory?timestamp>=-7%20days'
+));
+$section->add(N_('Notifications'), array(
+ 'icon' => 'bell',
+ 'description' => $this->translate('List notifications'),
+ 'priority' => 30,
+ 'url' => 'monitoring/list/notifications?notification_timestamp>=-7%20days',
+));
+$section->add(N_('Timeline'), array(
+ 'icon' => 'clock',
+ 'description' => $this->translate('Open timeline'),
+ 'priority' => 40,
+ 'url' => 'monitoring/timeline'
+));
+
+/*
+ * Reporting Section
+ */
+$section = $this->menuSection(N_('Reporting'), array(
+ 'icon' => 'barchart',
+ 'priority' => 100
+));
+
+/*
+ * Current Incidents
+ */
+$dashboard = $this->dashboard(N_('Current Incidents'), array('priority' => 50));
+$dashboard->add(
+ N_('Service Problems'),
+ 'monitoring/list/services?service_problem=1&limit=10&sort=service_severity',
+ 100
+);
+$dashboard->add(
+ N_('Recently Recovered Services'),
+ 'monitoring/list/services?service_state=0&limit=10&sort=service_last_state_change&dir=desc',
+ 110
+);
+$dashboard->add(
+ N_('Host Problems'),
+ 'monitoring/list/hosts?host_problem=1&sort=host_severity',
+ 120
+);
+
+/*
+ * Overview
+ */
+//$dashboard = $this->dashboard(N_('Overview'), array('priority' => 60));
+//$dashboard->add(
+// N_('Service Grid'),
+// 'monitoring/list/servicegrid?limit=15,18'
+//);
+//$dashboard->add(
+// N_('Service Groups'),
+// 'monitoring/list/servicegroups'
+//);
+//$dashboard->add(
+// N_('Host Groups'),
+// 'monitoring/list/hostgroups'
+//);
+
+/*
+ * Most Overdue
+ */
+$dashboard = $this->dashboard(N_('Overdue'), array('priority' => 70));
+$dashboard->add(
+ N_('Late Host Check Results'),
+ 'monitoring/list/hosts?host_next_update<now',
+ 100
+);
+$dashboard->add(
+ N_('Late Service Check Results'),
+ 'monitoring/list/services?service_next_update<now',
+ 110
+);
+$dashboard->add(
+ N_('Acknowledgements Active For At Least Three Days'),
+ 'monitoring/list/comments?comment_type=Ack&comment_timestamp<-3 days&sort=comment_timestamp&dir=asc',
+ 120
+);
+$dashboard->add(
+ N_('Downtimes Active For More Than Three Days'),
+ 'monitoring/list/downtimes?downtime_is_in_effect=1&downtime_scheduled_start<-3%20days&sort=downtime_start&dir=asc',
+ 130
+);
+
+/*
+ * Muted Objects
+ */
+$dashboard = $this->dashboard(N_('Muted'), array('priority' => 80));
+$dashboard->add(
+ N_('Disabled Service Notifications'),
+ 'monitoring/list/services?service_notifications_enabled=0&limit=10',
+ 100
+);
+$dashboard->add(
+ N_('Disabled Host Notifications'),
+ 'monitoring/list/hosts?host_notifications_enabled=0&limit=10',
+ 110
+);
+$dashboard->add(
+ N_('Disabled Service Checks'),
+ 'monitoring/list/services?service_active_checks_enabled=0&limit=10',
+ 120
+);
+$dashboard->add(
+ N_('Disabled Host Checks'),
+ 'monitoring/list/hosts?host_active_checks_enabled=0&limit=10',
+ 130
+);
+$dashboard->add(
+ N_('Acknowledged Problem Services'),
+ 'monitoring/list/services?service_acknowledgement_type!=0&service_problem=1&sort=service_state&limit=10',
+ 140
+);
+$dashboard->add(
+ N_('Acknowledged Problem Hosts'),
+ 'monitoring/list/hosts?host_acknowledgement_type!=0&host_problem=1&sort=host_severity&limit=10',
+ 150
+);
+
+/*
+ * Activity Stream
+ */
+//$dashboard = $this->dashboard(N_('Activity Stream'), array('priority' => 90));
+//$dashboard->add(
+// N_('Recent Events'),
+// 'monitoring/list/eventhistory?timestamp>=-3%20days&sort=timestamp&dir=desc&limit=8'
+//);
+//$dashboard->add(
+// N_('Recent Hard State Changes'),
+// 'monitoring/list/eventhistory?timestamp>=-3%20days&type=hard_state&sort=timestamp&dir=desc&limit=8'
+//);
+//$dashboard->add(
+// N_('Recent Notifications'),
+// 'monitoring/list/eventhistory?timestamp>=-3%20days&type=notify&sort=timestamp&dir=desc&limit=8'
+//);
+//$dashboard->add(
+// N_('Downtimes Recently Started'),
+// 'monitoring/list/eventhistory?timestamp>=-3%20days&type=dt_start&sort=timestamp&dir=desc&limit=8'
+//);
+//$dashboard->add(
+// N_('Downtimes Recently Ended'),
+// 'monitoring/list/eventhistory?timestamp>=-3%20days&type=dt_end&sort=timestamp&dir=desc&limit=8'
+//);
+
+/*
+ * Stats
+ */
+//$dashboard = $this->dashboard(N_('Stats'), array('priority' => 99));
+//$dashboard->add(
+// N_('Check Stats'),
+// 'monitoring/health/stats'
+//);
+//$dashboard->add(
+// N_('Process Information'),
+// 'monitoring/health/info'
+//);
+
+/*
+ * CSS
+ */
+$this->provideCssFile('event-grid.less');
+$this->provideCssFile('service-grid.less');
+$this->provideCssFile('tables.less');
diff --git a/modules/monitoring/doc/01-About.md b/modules/monitoring/doc/01-About.md
new file mode 100644
index 0000000..deb47bf
--- /dev/null
+++ b/modules/monitoring/doc/01-About.md
@@ -0,0 +1,10 @@
+# About the Monitoring Module <a id="monitoring-module-about"></a>
+
+Please read the following chapters for more insights on this module:
+
+* [Installation](02-Installation.md#monitoring-module-installation)
+* [Configuration](03-Configuration.md#monitoring-module-configuration)
+* [Security](06-Security.md#monitoring-module-security)
+* [Restrict Custom Variables](10-Restrict-Custom-Variables.md#monitoring-module-restrict-access-custom-variables)
+* [Hooks](20-Hooks.md#monitoring-module-hooks)
+* [Add Columns to List Views](11-Add-Columns-List-Views.md#monitoring-module-add-columns-list-views)
diff --git a/modules/monitoring/doc/02-Installation.md b/modules/monitoring/doc/02-Installation.md
new file mode 100644
index 0000000..43a7cd0
--- /dev/null
+++ b/modules/monitoring/doc/02-Installation.md
@@ -0,0 +1,15 @@
+# Monitoring Module Installation <a id="monitoring-module-installation"></a>
+
+This module is provided with the Icinga Web 2 package and does
+not need any extra installation step.
+
+## Enable the Module <a id="monitoring-module-enable"></a>
+
+Navigate to `Configuration` -> `Modules` -> `monitoring` and enable
+the module.
+
+You can also enable the module during the setup wizard, or on the CLI:
+
+```
+icingacli module enable monitoring
+```
diff --git a/modules/monitoring/doc/03-Configuration.md b/modules/monitoring/doc/03-Configuration.md
new file mode 100644
index 0000000..9adacc1
--- /dev/null
+++ b/modules/monitoring/doc/03-Configuration.md
@@ -0,0 +1,69 @@
+# Monitoring Module Configuration <a id="monitoring-module-configuration"></a>
+
+## Overview <a id="monitoring-module-configuration-overview"></a>
+
+The module specific configuration is stored in `/etc/icingaweb2/modules/monitoring`.
+
+File/Directory | Description
+----------------------------------------------------------------------|---------------------------------
+config.ini | Security settings (e.g. protected custom vars) for the `monitoring` module |
+[backends.ini](04-Backends.md#monitoring-module-backends) | Data backend (e.g. the IDO database [resource](../../../doc/04-Resources.md#resources-configuration-database) name).
+[commandtransports.ini](05-Command-Transports.md) | Command transports for specific Icinga instances
+
+
+## General Configuration <a id="monitoring-module-configuration-general"></a>
+
+Navigate into `Configuration` -> `Modules` -> `Monitoring`. This allows
+you to see the provided [permissions and restrictions](06-Security.md#monitoring-security)
+by this module.
+
+### Default Settings <a id="monitoring-module-configuration-settings"></a>
+
+Option | Description
+----------------------------------|-----------------------------------------------
+acknowledge_expire | **Optional.** Check "Use Expire Time" in Acknowledgement dialog by default. Defaults to **0 (false)**.
+acknowledge_expire_time | **Optional.** Set default value for "Expire Time" in Acknowledgement dialog, its calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **1 hour (PT1H)**.
+acknowledge_notify | **Optional.** Check "Send Notification" in Acknowledgement dialog by default. Defaults to **1 (true)**.
+acknowledge_persistent | **Optional.** Check "Persistent Comment" in Acknowledgement dialog by default. Defaults to **0 (false)**.
+acknowledge_sticky | **Optional.** Check "Sticky Acknowledgement" in Acknowledgement dialog by default. Defaults to **0 (false)**.
+comment_expire | **Optional.** Check "Use Expire Time" in Comment dialog by default. Defaults to **0 (false)**.
+hostdowntime_comment_text | **Optional.** Set default text for "Comment" in Host Downtime dialog by default.
+servicedowntime_comment_text | **Optional.** Set default text for "Comment" in Service Downtime dialog by default.
+comment_expire_time | **Optional.** Set default value for "Expire Time" in Comment dialog, its calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **1 hour (PT1H)**.
+custom_notification_forced | **Optional.** Check "Forced" in Custom Notification dialog by default. Defaults to **0 (false)**.
+hostcheck_all_services | **Optional.** Check "All Services" in Schedule Host Check dialog by default. Defaults to **0 (false)**.
+hostdowntime_all_services | **Optional.** Check "All Services" in Schedule Host Downtime dialog by default. Defaults to **0 (false)**.
+hostdowntime_end_fixed | **Optional.** Set default value for "End Time" in Schedule Host Downtime dialog for **Fixed** downtime, its calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **1 hour (PT1H)**.
+hostdowntime_end_flexible | **Optional.** Set default value for "End Time" in Schedule Host Downtime dialog for **Flexible** downtime, its calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **1 hour (PT1H)**.
+hostdowntime_flexible_duration | **Optional.** Set default value for "Flexible Duration" in Schedule Host Downtime dialog for **Flexible** downtime. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **2 hour (PT2H)**.
+servicedowntime_end_fixed | **Optional.** Set default value for "End Time" in Schedule Service Downtime dialog for **Fixed** downtime, its calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **1 hour (PT1H)**.
+servicedowntime_end_flexible | **Optional.** Set default value for "End Time" in Schedule Service Downtime dialog for **Flexible** downtime, its calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **1 hour (PT1H)**.
+servicedowntime_flexible_duration | **Optional.** Set default value for "Flexible Duration" in Schedule Service Downtime dialog for **Flexible** downtime. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). Defaults to **2 hour (PT2H)**.
+
+Example for having acknowledgements with 2 hours expire time by default.
+
+```
+# vim /etc/icingaweb2/modules/monitoring/config.ini
+
+[settings]
+acknowledge_expire = 1
+acknowledge_expire_time = PT2H
+
+```
+
+### Security Configuration <a id="monitoring-module-configuration-security"></a>
+
+Option | Description
+-------------------------|-----------------------------------------------
+protected\_customvars | **Optional.** Comma separated list of string patterns for custom variables which should be excluded from user's view.
+
+
+Example for custom variable names which match `*pw*` or `*pass*` or `community`.
+
+```
+# vim /etc/icingaweb2/modules/monitoring/config.ini
+
+[security]
+protected_customvars = "*pw*,*pass*,community"
+```
+
diff --git a/modules/monitoring/doc/04-Backends.md b/modules/monitoring/doc/04-Backends.md
new file mode 100644
index 0000000..2681109
--- /dev/null
+++ b/modules/monitoring/doc/04-Backends.md
@@ -0,0 +1,30 @@
+# Backends <a id="monitoring-module-backends"></a>
+
+The configuration file `backends.ini` contains information about data sources which are
+used to fetch monitoring objects presented to the user.
+
+The required [resources](../../../doc/04-Resources.md#resources-configuration-database) must be globally defined beforehand.
+
+## Configuration <a id="monitoring-module-backends-configuration"></a>
+
+Navigate into `Configuration` -> `Modules` -> `Monitoring` -> `Backends`.
+You can select a specified global resource here, and also update its details.
+
+Each section in `backends.ini` references a resource. By default you should only have one backend enabled.
+
+### IDO Backend <a id="monitoring-module-backends-ido"></a>
+
+Option | Description
+-------------------------|-----------------------------------------------
+type | **Required.** Specify the backend type. Must be set to `ido`.
+resource | **Required.** Specify a defined [resource](../../../doc/04-Resources.md#resources-configuration-database) name which provides details about the IDO database resource.
+
+
+Example for using the database resource `icinga2_ido_mysql`:
+
+```
+[icinga2_ido_mysql]
+type = "ido"
+resource = "icinga2_ido_mysql"
+```
+
diff --git a/modules/monitoring/doc/05-Command-Transports.md b/modules/monitoring/doc/05-Command-Transports.md
new file mode 100644
index 0000000..bdf9f56
--- /dev/null
+++ b/modules/monitoring/doc/05-Command-Transports.md
@@ -0,0 +1,185 @@
+# External Command Transport Configuration <a id="monitoring-module-commandtransports"></a>
+
+## Configuration <a id="monitoring-module-commandtransports-configuration"></a>
+
+Navigate into `Configuration` -> `Modules` -> `Monitoring` -> `Backends`.
+You can create/edit command transports here.
+
+The `commandtransports.ini` configuration file defines how Icinga Web 2
+transports commands to your Icinga instance in order to submit
+external commands. By default, this file is located at `/etc/icingaweb2/modules/monitoring/commandtransports.ini`.
+
+You can define multiple command transports in the `commandtransports.ini` file. Every transport starts with a section header
+containing its name, followed by the config directives for this transport in the standard INI-format.
+
+Icinga Web 2 will try one transport after another to send a command until the command is successfully sent.
+If [configured](05-Command-Transports.md#commandtransports-multiple-instances), Icinga Web 2 will take different instances into account.
+The order in which Icinga Web 2 processes the configured transports is defined by the order of sections in
+`commandtransports.ini`.
+
+## Use the Icinga 2 API <a id="commandtransports-icinga2-api"></a>
+
+If you're running Icinga 2 it's best to use the [Icinga 2 API](https://icinga.com/docs/icinga2/latest/doc/12-icinga2-api/)
+for transmitting external commands.
+
+### Icinga 2 Preparations <a id="commandtransports-icinga2-api-preparations"></a>
+
+You have to run the `api` setup on the Icinga 2 host where you want to send the commands to:
+
+```
+icinga2 api setup
+```
+
+Next, you have to create an ApiUser object for authenticating against the Icinga 2 API. This configuration also applies
+to the host where you want to send the commands to. We recommend to create/edit the file
+`/etc/icinga2/conf.d/api-users.conf`:
+
+```
+object ApiUser "icingaweb2" {
+ password = "bea11beb7b810ea9ce6ea" // Change this!
+ permissions = [ "status/query", "actions/*", "objects/modify/*", "objects/query/*" ]
+}
+```
+
+The permissions are mandatory in order to submit all external commands from within Icinga Web 2.
+
+**Restart Icinga 2** for the changes to take effect.
+
+```
+systemctl restart icinga2
+```
+
+### Configuration in Icinga Web 2 <a id="commandtransports-icinga2-api-configuration"></a>
+
+> **Note**
+>
+> Please make sure that your server running Icinga Web 2 has the `PHP cURL` extension installed and enabled.
+
+The Icinga 2 API requires the following settings:
+
+Option | Description
+-------------------------|-----------------------------------------------
+transport | **Required.** The transport type. Must be set to `api`.
+host | **Required.** The host address where the Icinga 2 API is listening on.
+port | **Required.** The port where the Icinga 2 API is listening on. Defaults to `5665`.
+username | **Required.** Basic auth username.
+password | **Required.** Basic auth password.
+
+Example:
+
+```
+# vim /etc/icingaweb2/modules/monitoring/commandtransports.ini
+
+[icinga2]
+transport = "api"
+host = "127.0.0.1" ; Icinga 2 host
+port = "5665"
+username = "icingaweb2"
+password = "bea11beb7b810ea9ce6ea" ; Change this!
+```
+
+## Use a Local Command Pipe <a id="commandtransports-local-command-pipe"></a>
+
+A local Icinga instance requires the following settings:
+
+Option | Description
+-------------------------|-----------------------------------------------
+transport | **Required.** The transport type. Must be set to `local`.
+path | **Required.** The absolute path to the local command pipe.
+
+Example:
+
+```
+# vim /etc/icingaweb2/modules/monitoring/commandtransports.ini
+
+[icinga2]
+transport = local
+path = /var/run/icinga2/cmd/icinga2.cmd
+```
+
+When commands are being sent to the Icinga instance, Icinga Web 2 opens the file found
+on the local filesystem underneath `path` and writes the external command to it.
+
+Please note that errors are not returned using this method. The Icinga 2 API sends
+error feedback.
+
+## Use SSH For a Remote Command Pipe <a id="commandtransports-ssh-remote-command-pipe"></a>
+
+A command pipe on a remote host's filesystem can be accessed by configuring a
+SSH based command transport and requires the following settings:
+
+Option | Description
+-------------------------|-----------------------------------------------
+transport | **Required.** The transport type. Must be set to `remote`.
+path | **Required.** The path on the remote server to its local command pipe.
+host | **Required.** The SSH host.
+port | **Optional.** The SSH port. Defaults to `22`.
+user | **Required.** The SSH auth user.
+resource | **Optional.** The SSH [resource](../../../doc/04-Resources.md#resources-configuration-ssh)
+instance | **Optional.** The Icinga instance name. Only required for multiple instances.
+
+Example:
+
+```
+# vim /etc/icingaweb2/modules/monitoring/commandtransports.ini
+
+[icinga2]
+transport = remote
+path = /var/run/icinga2/cmd/icinga2.cmd
+host = example.tld
+user = icinga
+;port = 22 ; Optional. The default is 22
+```
+
+To make this example work, you'll need to permit your web-server's user
+public-key based access to the defined remote host so that Icinga Web 2 can
+connect to it and login as the defined user.
+
+You can also make use of a dedicated SSH resource to permit access for a
+different user than the web-server's one. This way, you can provide a private
+key file on the local filesystem that is used to access the remote host.
+
+To accomplish this, a new resource is required that is defined in your
+transport's configuration instead of a user:
+
+```
+# vim /etc/icingaweb2/modules/monitoring/commandtransports.ini
+
+[icinga2]
+transport = remote
+path = /var/run/icinga2/cmd/icinga2.cmd
+host = example.tld
+resource = example.tld-icinga2
+;port = 22 ; Optional. The default is 22
+```
+
+The resource's configuration needs to be put into the resources.ini file:
+
+```
+# vim /etc/icingaweb2/resources.ini
+
+[example.tld-icinga2]
+type = ssh
+user = icinga
+private_key = /etc/icingaweb2/ssh/icinga
+```
+
+## Configure Transports for Different Icinga Instances <a id="commandtransports-multiple-instances"></a>
+
+If there are multiple but different Icinga instances writing to your IDO database,
+you can define which transport belongs to which Icinga instance by providing the
+`instance` setting. This setting must specify the name of the Icinga
+instance you want to assign to the transport:
+
+```
+[icinga1]
+...
+instance = icinga1
+
+[icinga2]
+...
+instance = icinga2
+```
+
+Associating a transport to a specific Icinga instance causes this transport to be used to send commands to the linked
+instance only. Transports without a linked Icinga instance are used to send commands to all instances.
diff --git a/modules/monitoring/doc/06-Security.md b/modules/monitoring/doc/06-Security.md
new file mode 100644
index 0000000..750eaef
--- /dev/null
+++ b/modules/monitoring/doc/06-Security.md
@@ -0,0 +1,66 @@
+# Security <a id="monitoring-module-security"></a>
+
+The monitoring module provides an additional set of restrictions and permissions
+that can be used for access control. The following sections will list those
+restrictions and permissions in detail:
+
+
+## Permissions <a id="monitoring-module-security-permissions"></a>
+
+The monitoring module allows to send commands to an Icinga 2 instance.
+A user needs specific permissions to be able to send those commands
+when using the monitoring module.
+
+
+Name | Permits
+-------------------------------------------------|-----------------------------------------------
+monitoring/command/* | Allow all commands.
+monitoring/command/schedule-check | Allow scheduling host and service checks.
+monitoring/command/schedule-check/active-only | Allow scheduling host and service checks. (Only on objects with active checks enabled)
+monitoring/command/acknowledge-problem | Allow acknowledging host and service problems.
+monitoring/command/remove-acknowledgement | Allow removing problem acknowledgements.
+monitoring/command/comment/* | Allow adding and deleting host and service comments.
+monitoring/command/comment/add | Allow commenting on hosts and services.
+monitoring/command/comment/delete | Allow deleting host and service comments.
+monitoring/command/downtime/* | Allow scheduling and deleting host and service downtimes.
+monitoring/command/downtime/schedule | Allow scheduling host and service downtimes.
+monitoring/command/downtime/delete | Allow deleting host and service downtimes.
+monitoring/command/process-check-result | Allow processing host and service check results.
+monitoring/command/feature/instance | Allow processing commands for toggling features on an instance-wide basis.
+monitoring/command/feature/object/* | Allow processing commands for toggling features on host and service objects.
+monitoring/command/feature/object/active-checks | Allow processing commands for toggling active checks on host and service objects.
+monitoring/command/feature/object/passive-checks | Allow processing commands for toggling passive checks on host and service objects.
+monitoring/command/feature/object/notifications | Allow processing commands for toggling notifications on host and service objects.
+monitoring/command/feature/object/event-handler | Allow processing commands for toggling event handlers on host and service objects.
+monitoring/command/feature/object/flap-detection | Allow processing commands for toggling flap detection on host and service objects.
+monitoring/command/send-custom-notification | Allow sending custom notifications for hosts and services.
+
+
+## Restrictions <a id="monitoring-module-security-restrictions"></a>
+
+The monitoring module allows filtering objects:
+
+
+Keys | Restricts
+--------------------------------------------|-----------------------------------------------
+monitoring/filter/objects | Applies a filter to all hosts and services.
+
+
+This filter will affect all hosts and services. Furthermore, it will also
+affect all related objects, like notifications, downtimes and events. If a
+service is hidden, all notifications, downtimes on that service will be hidden too.
+
+
+### Filter Column Names <a id="monitoring-module-security-restrictions-filter-column-names"></a>
+
+The following filter column names are available in filter expressions:
+
+
+Column | Description
+-----------------------------------------------------------|-----------------------------------------------
+instance\_name | Filter on an Icinga 2 instance.
+host\_name | Filter on host object names.
+hostgroup\_name | Filter on hostgroup object names.
+service\_description | Filter on service object names.
+servicegroup\_name | Filter on servicegroup object names.
+all custom variables prefixed with `_host_` or `_service_` | Filter on specified custom variables.
diff --git a/modules/monitoring/doc/10-Restrict-Custom-Variables.md b/modules/monitoring/doc/10-Restrict-Custom-Variables.md
new file mode 100644
index 0000000..8d3a3b1
--- /dev/null
+++ b/modules/monitoring/doc/10-Restrict-Custom-Variables.md
@@ -0,0 +1,77 @@
+# Restrict Access to Custom Variables <a id="monitoring-module-restrict-access-custom-variables"></a>
+
+* Restriction name: monitoring/blacklist/properties
+* Restriction value: Comma separated list of GLOB like filters
+
+Imagine the following host custom variable structure.
+
+```
+host.vars.
+|-- cmdb_name
+|-- cmdb_id
+|-- cmdb_location
+|-- wiki_id
+|-- passwords.
+| |-- mysql_password
+| |-- ldap_password
+| `-- mongodb_password
+|-- legacy.
+| |-- cmdb_name
+| |-- mysql_password
+| `-- wiki_id
+`-- backup.
+ `-- passwords.
+ |-- mysql_password
+ `-- ldap_password
+```
+
+`host.vars.cmdb_name`
+
+Blacklists `cmdb_name` in the first level of the custom variable structure only.
+`host.vars.legacy.cmdb_name` is not blacklisted.
+
+
+`host.vars.cmdb_*`
+
+All custom variables in the first level of the structure which begin with `cmdb_` become blacklisted.
+Deeper custom variables are ignored. `host.vars.legacy.cmdb_name` is not blacklisted.
+
+`host.vars.*id`
+
+All custom variables in the first level of the structure which end with `id` become blacklisted.
+Deeper custom variables are ignored. `host.vars.legacy.wiki_id` is not blacklisted.
+
+`host.vars.*.mysql_password`
+
+Matches all custom variables on the second level which are equal to `mysql_password`.
+
+`host.vars.*.*password`
+
+Matches all custom variables on the second level which end with `password`.
+
+`host.vars.*.mysql_password,host.vars.*.ldap_password`
+
+Matches all custorm variables on the second level which equal `mysql_password` or `ldap_password`.
+
+`host.vars.**.*password`
+
+Matches all custom variables on all levels which end with `password`.
+
+Please note the two asterisks, `**`, here for crossing level boundaries. This syntax is used for matching the complete
+custom variable structure.
+
+If you want to restrict all custom variables that end with password for both hosts and services, you have to define
+the following restriction.
+
+`host.vars.**.*password,service.vars.**.*password`
+
+## Escape Meta Characters <a id="restrict-access-custom-variables-escape-meta-chars"></a>
+
+Use backslash to escape the meta characters
+
+* *
+* ,
+
+`host.vars.\*fall`
+
+Matches all custom variables in the first level which equal `*fall`.
diff --git a/modules/monitoring/doc/11-Add-Columns-List-Views.md b/modules/monitoring/doc/11-Add-Columns-List-Views.md
new file mode 100644
index 0000000..2567ead
--- /dev/null
+++ b/modules/monitoring/doc/11-Add-Columns-List-Views.md
@@ -0,0 +1,32 @@
+# Add Columns to List Views <a id="monitoring-module-add-columns-list-views"></a>
+
+The monitoring module provides list views for hosts and services.
+These lists only provide the most common columns to reduce the backend
+query load.
+
+If you want to add more columns to the list view e.g. in order to use the URL in
+your dashboards or as external iframe integration, you need the `addColumns` URL
+parameter.
+
+
+
+Example for adding the host `address` attribute in a host list:
+
+```
+http://localhost/icingaweb2/monitoring/list/hosts?addColumns=host_address
+```
+
+![Screenshot](img/list_hosts_add_columns.png)
+
+
+
+
+Example for multiple columns as comma separated parameter string. This
+includes a reference to the Icinga 2 host object custom attribute `os` using
+`_host_` as custom variable identifier.
+
+```
+http://localhost/icingaweb2/monitoring/list/services?addColumns=host_address,_host_os
+```
+
+![Screenshot](img/list_services_add_columns.png)
diff --git a/modules/monitoring/doc/20-Hooks.md b/modules/monitoring/doc/20-Hooks.md
new file mode 100644
index 0000000..5d38843
--- /dev/null
+++ b/modules/monitoring/doc/20-Hooks.md
@@ -0,0 +1,161 @@
+# Monitoring Module Hooks <a id="monitoring-module-hooks"></a>
+
+## Detail View Extension Hook <a id="monitoring-module-hooks-detailviewextension"></a>
+
+This hook can be used to easily extend the detail view of monitored objects (hosts and services).
+
+### How it works <a id="monitoring-module-hooks-detailviewextension-how-it-works"></a>
+
+#### Directory structure <a id="monitoring-module-hooks-detailviewextension-directory-structure"></a>
+
+* `icingaweb2/modules/example`
+ * `library/Example/ProvidedHook/Monitoring/DetailviewExtension/Simple.php`
+ * `run.php`
+
+#### Files <a id="monitoring-module-hooks-detailviewextension-files"></a>
+
+##### run.php <a id="monitoring-module-hooks-detailviewextension-files-run-php"></a>
+
+```php
+<?php
+/** @var \Icinga\Application\Modules\Module $this */
+
+$this->provideHook(
+ 'monitoring/DetailviewExtension',
+ 'Icinga\Module\Example\ProvidedHook\Monitoring\DetailviewExtension\Simple'
+);
+```
+
+##### Simple.php <a id="monitoring-module-hooks-detailviewextension-files-simple-php"></a>
+
+```php
+<?php
+namespace Icinga\Module\Example\ProvidedHook\Monitoring\DetailviewExtension;
+
+use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+
+class Simple extends DetailviewExtensionHook
+{
+ public function getHtmlForObject(MonitoredObject $object)
+ {
+ $stats = array();
+ foreach (str_split($object->name) as $c) {
+ if (isset($stats[$c])) {
+ ++$stats[$c];
+ } else {
+ $stats[$c] = 1;
+ }
+ }
+
+ ksort($stats);
+
+ $view = $this->getView();
+
+ $thead = '';
+ $tbody = '';
+ foreach ($stats as $c => $amount) {
+ $thead .= '<th>' . $view->escape($c) . '</th>';
+ $tbody .= '<td>' . $amount . '</td>';
+ }
+
+ return '<h2>'
+ . $view->escape(sprintf($view->translate('A %s named "%s"'), $object->getType(), $object->name))
+ . '</h2>'
+ . '<h3>Character stats</h3>'
+ . '<table>'
+ . '<thead>' . $thead . '</thead>'
+ . '<tbody>' . $tbody . '</tbody>'
+ . '</table>';
+ }
+}
+```
+
+### How it looks <a id="monitoring-module-hooks-detailviewextension-how-it-looks"></a>
+
+![Screenshot](img/hooks-detailviewextension-01.png)
+
+## Plugin Output Hook <a id="monitoring-module-hooks-pluginoutput"></a>
+
+The Plugin Output Hook allows you to rewrite the plugin output based on check commands. You have to implement the
+following methods:
+
+* `getCommands()`
+* and `render()`
+
+With `getCommands()` you specify for which commands the provided hook is responsible for. You may return a single
+command as string or a list of commands as array. If you want your hook to be responsible for every command, you have to
+specify the `*`.
+
+In `render()` you rewrite the plugin output based on check commands. The parameter `$command` specifies the check
+command of the host or service and `$output` specifies the plugin output. The parameter `$detail` tells you
+whether the output is requested from the detail area of the host or service.
+
+Do not use complex logic for rewriting plugin output in list views because of the performance impact!
+
+You have to return the rewritten plugin output as string. It is also possible to return a HTML string here.
+Please refer to `\Icinga\Module\Monitoring\Web\Helper\PluginOutputPurifier` for a list of allowed tags.
+
+Please also have a look at the following examples.
+
+**Example hook which is responsible for disk checks:**
+
+```php
+<?php
+
+namespace Icinga\Module\Example\ProvidedHook\Monitoring;
+
+use Icinga\Module\Monitoring\Hook\PluginOutputHook;
+
+class PluginOutput extends PluginOutputHook
+{
+ public function getCommands()
+ {
+ return ['disk'];
+ }
+
+ public function render($command, $output, $detail)
+ {
+ if (! $detail) {
+ // Don't rewrite plugin output in list views
+ return $output;
+ }
+ return implode('<br>', explode(';', $output));
+ }
+}
+```
+
+**Example hook which is responsible for disk and procs checks:**
+
+```php
+<?php
+
+namespace Icinga\Module\Example\ProvidedHook\Monitoring;
+
+use Icinga\Module\Monitoring\Hook\PluginOutputHook;
+
+class PluginOutput extends PluginOutputHook
+{
+ public function getCommands()
+ {
+ return ['disk', 'procs'];
+ }
+
+ public function render($command, $output, $detail)
+ {
+ switch ($command) {
+ case 'disk':
+ if ($detail) {
+ // Only rewrite plugin output in the detail area
+ $output = implode('<br>', explode(';', $output));
+ }
+ break;
+ case 'procs':
+ $output = preg_replace('/(\d)+/', '<b>$1</b>', $output);
+ break;
+ }
+
+ return $output;
+ }
+}
+```
diff --git a/modules/monitoring/doc/img/hooks-detailviewextension-01.png b/modules/monitoring/doc/img/hooks-detailviewextension-01.png
new file mode 100644
index 0000000..a5ddaf1
--- /dev/null
+++ b/modules/monitoring/doc/img/hooks-detailviewextension-01.png
Binary files differ
diff --git a/modules/monitoring/doc/img/list_hosts_add_columns.png b/modules/monitoring/doc/img/list_hosts_add_columns.png
new file mode 100644
index 0000000..874a8f1
--- /dev/null
+++ b/modules/monitoring/doc/img/list_hosts_add_columns.png
Binary files differ
diff --git a/modules/monitoring/doc/img/list_services_add_columns.png b/modules/monitoring/doc/img/list_services_add_columns.png
new file mode 100644
index 0000000..dd0db82
--- /dev/null
+++ b/modules/monitoring/doc/img/list_services_add_columns.png
Binary files differ
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/IdoBackend.php b/modules/monitoring/library/Monitoring/Backend/Ido/IdoBackend.php
new file mode 100644
index 0000000..71fc6a1
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/IdoBackend.php
@@ -0,0 +1,10 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido;
+
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+
+class IdoBackend extends MonitoringBackend
+{
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/AllcontactsQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/AllcontactsQuery.php
new file mode 100644
index 0000000..3598726
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/AllcontactsQuery.php
@@ -0,0 +1,75 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Zend_Db_Select;
+
+class AllcontactsQuery extends IdoQuery
+{
+ protected $columnMap = array(
+ 'contacts' => array(
+ 'contact_name' => 'c.contact_name',
+ 'host_object_id' => 'c.host_object_id',
+ 'host_name' => 'c.host_name',
+ 'service_object_id' => 'c.service_object_id',
+ 'service_host_name' => 'c.service_host_name',
+ 'service_description' => 'c.service_description',
+
+ 'contact_alias' => 'c.contact_alias',
+ 'contact_email' => 'c.contact_email',
+ 'contact_pager' => 'c.contact_pager',
+ 'contact_has_host_notfications' => 'c.contact_has_host_notfications',
+ 'contact_has_service_notfications' => 'c.contact_has_service_notfications',
+ 'contact_can_submit_commands' => 'c.contact_can_submit_commands',
+ 'contact_notify_service_recovery' => 'c.notify_service_recovery',
+ 'contact_notify_service_warning' => 'c.notify_service_warning',
+ 'contact_notify_service_critical' => 'c.notify_service_critical',
+ 'contact_notify_service_unknown' => 'c.notify_service_unknown',
+ 'contact_notify_service_flapping' => 'c.notify_service_flapping',
+ 'contact_notify_service_downtime' => 'c.notify_service_downtime',
+ 'contact_notify_host_recovery' => 'c.notify_host_recovery',
+ 'contact_notify_host_down' => 'c.notify_host_down',
+ 'contact_notify_host_unreachable' => 'c.notify_host_unreachable',
+ 'contact_notify_host_flapping' => 'c.notify_host_flapping',
+ 'contact_notify_host_downtime' => 'c.notify_host_downtime',
+
+
+ )
+ );
+
+ protected $contacts;
+ protected $contactgroups;
+ protected $baseQuery;
+ protected $useSubqueryCount = true;
+
+ public function requireColumn($alias)
+ {
+ $this->contacts->addColumn($alias);
+ $this->contactgroups->addColumn($alias);
+ return parent::requireColumn($alias);
+ }
+
+ protected function joinBaseTables()
+ {
+ $this->contacts = $this->createSubQuery(
+ 'contact',
+ array('contact_name')
+ );
+ $this->contactgroups = $this->createSubQuery(
+ 'contactgroup',
+ array('contact_name')
+ );
+ $sub = $this->db->select()->union(
+ array($this->contacts, $this->contactgroups),
+ Zend_Db_Select::SQL_UNION_ALL
+ );
+
+ $this->baseQuery = $this->db->select()->distinct()->from(
+ array('c' => $sub),
+ array()
+ );
+
+ $this->joinedVirtualTables = array('contacts' => true);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommandQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommandQuery.php
new file mode 100644
index 0000000..59a4ccb
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommandQuery.php
@@ -0,0 +1,61 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for commands
+ */
+class CommandQuery extends IdoQuery
+{
+ /**
+ * @var array
+ */
+ protected $columnMap = array(
+ 'commands' => array(
+ 'command_id' => 'c.command_id',
+ 'command_instance_id' => 'c.instance_id',
+ 'command_config_type' => 'c.config_type',
+ 'command_line' => 'c.command_line',
+ 'command_name' => 'co.name1'
+ ),
+
+ 'contacts' => array(
+ 'contact_id' => 'con.contact_id',
+ 'contact_alias' => 'con.contact_alias'
+ )
+ );
+
+ /**
+ * Fetch basic information about commands
+ */
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ array('c' => $this->prefix . 'commands'),
+ array()
+ )->join(
+ array('co' => $this->prefix . 'objects'),
+ 'co.object_id = c.object_id',
+ array()
+ );
+
+ $this->joinedVirtualTables = array('commands' => true);
+ }
+
+ /**
+ * Join contacts
+ */
+ protected function joinContacts()
+ {
+ $this->select->join(
+ array('cnc' => $this->prefix . 'contact_notificationcommands'),
+ 'cnc.command_object_id = co.object_id',
+ array()
+ )->join(
+ array('con' => $this->prefix . 'contacts'),
+ 'con.contact_id = cnc.contact_id',
+ array()
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentQuery.php
new file mode 100644
index 0000000..6c01931
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentQuery.php
@@ -0,0 +1,158 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\FilterExpression;
+use Zend_Db_Expr;
+use Zend_Db_Select;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for host and service comments
+ */
+class CommentQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'comments' => array(
+ 'comment_author' => 'c.comment_author',
+ 'comment_author_name' => 'c.comment_author_name',
+ 'comment_data' => 'c.comment_data',
+ 'comment_expiration' => 'c.comment_expiration',
+ 'comment_internal_id' => 'c.comment_internal_id',
+ 'comment_is_persistent' => 'c.comment_is_persistent',
+ 'comment_name' => 'c.comment_name',
+ 'comment_timestamp' => 'c.comment_timestamp',
+ 'comment_type' => 'c.comment_type',
+ 'instance_name' => 'c.instance_name',
+ 'object_type' => 'c.object_type'
+ ),
+ 'hosts' => array(
+ 'host_display_name' => 'c.host_display_name',
+ 'host_name' => 'c.host_name',
+ 'host_state' => 'c.host_state'
+ ),
+ 'services' => array(
+ 'service_description' => 'c.service_description',
+ 'service_display_name' => 'c.service_display_name',
+ 'service_host_name' => 'c.service_host_name',
+ 'service_state' => 'c.service_state'
+ )
+ );
+
+ /**
+ * The union
+ *
+ * @var Zend_Db_Select
+ */
+ protected $commentQuery;
+
+ /**
+ * Subqueries used for the comment query
+ *
+ * @var IdoQuery[]
+ */
+ protected $subQueries = array();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function allowsCustomVars()
+ {
+ foreach ($this->subQueries as $query) {
+ if (! $query->allowsCustomVars()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ if (version_compare($this->getIdoVersion(), '1.14.0', '<')) {
+ $this->columnMap['comments']['comment_name'] = '(NULL)';
+ }
+ $this->commentQuery = $this->db->select();
+ $this->select->from(
+ array('c' => $this->commentQuery),
+ array()
+ );
+ $this->joinedVirtualTables['comments'] = true;
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $columns = array_keys($this->columnMap['comments'] + $this->columnMap['hosts']);
+ foreach (array_keys($this->columnMap['services']) as $column) {
+ $columns[$column] = new Zend_Db_Expr('NULL');
+ }
+ $hosts = $this->createSubQuery('hostcomment', $columns);
+ $this->subQueries[] = $hosts;
+ $this->commentQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $columns = array_keys($this->columnMap['comments'] + $this->columnMap['hosts'] + $this->columnMap['services']);
+ $services = $this->createSubQuery('servicecomment', $columns);
+ $this->subQueries[] = $services;
+ $this->commentQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order($columnOrAlias, $dir = null)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->requireColumn($columnOrAlias);
+ }
+ return parent::order($columnOrAlias, $dir);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ $this->requireColumn($condition);
+ foreach ($this->subQueries as $sub) {
+ $sub->where($condition, $value);
+ }
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->requireColumn($ex->getColumn());
+ foreach ($this->subQueries as $sub) {
+ $sub->whereEx($ex);
+ }
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php
new file mode 100644
index 0000000..8cb4ddb
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php
@@ -0,0 +1,179 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\FilterExpression;
+use Zend_Db_Expr;
+use Zend_Db_Select;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for host and service comment removal records
+ */
+class CommentdeletionhistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'commenthistory' => array(
+ 'id' => 'cdh.id',
+ 'object_type' => 'cdh.object_type'
+ ),
+ 'history' => array(
+ 'type' => 'cdh.type',
+ 'timestamp' => 'cdh.timestamp',
+ 'object_id' => 'cdh.object_id',
+ 'state' => 'cdh.state',
+ 'output' => 'cdh.output'
+ ),
+ 'hosts' => array(
+ 'host_display_name' => 'cdh.host_display_name',
+ 'host_name' => 'cdh.host_name'
+ ),
+ 'services' => array(
+ 'service_description' => 'cdh.service_description',
+ 'service_display_name' => 'cdh.service_display_name',
+ 'service_host_name' => 'cdh.service_host_name'
+ )
+ );
+
+ /**
+ * The union
+ *
+ * @var Zend_Db_Select
+ */
+ protected $commentDeletionHistoryQuery;
+
+ /**
+ * Subqueries used for the comment history query
+ *
+ * @var IdoQuery[]
+ */
+ protected $subQueries = array();
+
+ /**
+ * Whether to additionally select all history columns
+ *
+ * @var bool
+ */
+ protected $fetchHistoryColumns = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function allowsCustomVars()
+ {
+ foreach ($this->subQueries as $query) {
+ if (! $query->allowsCustomVars()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->commentDeletionHistoryQuery = $this->db->select();
+ $this->select->from(
+ array('cdh' => $this->commentDeletionHistoryQuery),
+ array()
+ );
+ $this->joinedVirtualTables['commenthistory'] = true;
+ }
+
+ /**
+ * Join history related columns and tables
+ */
+ protected function joinHistory()
+ {
+ // TODO: Ensure that one is selecting the history columns first...
+ $this->fetchHistoryColumns = true;
+ $this->requireVirtualTable('hosts');
+ $this->requireVirtualTable('services');
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $columns = array_keys(
+ $this->columnMap['commenthistory'] + $this->columnMap['hosts']
+ );
+ foreach ($this->columnMap['services'] as $column => $_) {
+ $columns[$column] = new Zend_Db_Expr('NULL');
+ }
+ if ($this->fetchHistoryColumns) {
+ $columns = array_merge($columns, array_keys($this->columnMap['history']));
+ }
+ $hosts = $this->createSubQuery('Hostcommentdeletionhistory', $columns);
+ $this->subQueries[] = $hosts;
+ $this->commentDeletionHistoryQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $columns = array_keys(
+ $this->columnMap['commenthistory'] + $this->columnMap['hosts'] + $this->columnMap['services']
+ );
+ if ($this->fetchHistoryColumns) {
+ $columns = array_merge($columns, array_keys($this->columnMap['history']));
+ }
+ $services = $this->createSubQuery('Servicecommentdeletionhistory', $columns);
+ $this->subQueries[] = $services;
+ $this->commentDeletionHistoryQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order($columnOrAlias, $dir = null)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->requireColumn($columnOrAlias);
+ }
+ return parent::order($columnOrAlias, $dir);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ $this->requireColumn($condition);
+ foreach ($this->subQueries as $sub) {
+ $sub->where($condition, $value);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->requireColumn($ex->getColumn());
+ foreach ($this->subQueries as $sub) {
+ $sub->whereEx($ex);
+ }
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenteventQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenteventQuery.php
new file mode 100644
index 0000000..c85adff
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenteventQuery.php
@@ -0,0 +1,39 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for host and service comment entry and deletion events
+ */
+class CommenteventQuery extends IdoQuery
+{
+ protected $columnMap = array(
+ 'commentevent' => array(
+ 'commentevent_id' => 'ch.commenthistory_id',
+ 'commentevent_entry_type' => "(CASE ch.entry_type WHEN 1 THEN 'comment' WHEN 2 THEN 'downtime' WHEN 3 THEN 'flapping' WHEN 4 THEN 'ack' ELSE NULL END)",
+ 'commentevent_comment_time' => 'UNIX_TIMESTAMP(ch.comment_time)',
+ 'commentevent_author_name' => 'ch.author_name',
+ 'commentevent_comment_data' => 'ch.comment_data',
+ 'commentevent_is_persistent' => 'ch.is_persistent',
+ 'commentevent_comment_source' => "(CASE ch.comment_source WHEN 0 THEN 'icinga' WHEN 1 THEN 'user' ELSE NULL END)",
+ 'commentevent_expires' => 'ch.expires',
+ 'commentevent_expiration_time' => 'UNIX_TIMESTAMP(ch.expiration_time)',
+ 'commentevent_deletion_time' => 'UNIX_TIMESTAMP(ch.deletion_time)'
+ ),
+ 'object' => array(
+ 'host_name' => 'o.name1',
+ 'service_description' => 'o.name2'
+ )
+ );
+
+ protected function joinBaseTables()
+ {
+ $this->select()
+ ->from(array('ch' => $this->prefix . 'commenthistory'), array())
+ ->join(array('o' => $this->prefix . 'objects'), 'ch.object_id = o.object_id', array());
+
+ $this->joinedVirtualTables['commentevent'] = true;
+ $this->joinedVirtualTables['object'] = true;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php
new file mode 100644
index 0000000..47dd97c
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php
@@ -0,0 +1,179 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\FilterExpression;
+use Zend_Db_Expr;
+use Zend_Db_Select;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for host and service comment history records
+ */
+class CommenthistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'commenthistory' => array(
+ 'id' => 'ch.id',
+ 'object_type' => 'ch.object_type'
+ ),
+ 'history' => array(
+ 'type' => 'ch.type',
+ 'timestamp' => 'ch.timestamp',
+ 'object_id' => 'ch.object_id',
+ 'state' => 'ch.state',
+ 'output' => 'ch.output'
+ ),
+ 'hosts' => array(
+ 'host_display_name' => 'ch.host_display_name',
+ 'host_name' => 'ch.host_name'
+ ),
+ 'services' => array(
+ 'service_description' => 'ch.service_description',
+ 'service_display_name' => 'ch.service_display_name',
+ 'service_host_name' => 'ch.service_host_name'
+ )
+ );
+
+ /**
+ * The union
+ *
+ * @var Zend_Db_Select
+ */
+ protected $commentHistoryQuery;
+
+ /**
+ * Subqueries used for the comment history query
+ *
+ * @var IdoQuery[]
+ */
+ protected $subQueries = array();
+
+ /**
+ * Whether to additionally select all history columns
+ *
+ * @var bool
+ */
+ protected $fetchHistoryColumns = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function allowsCustomVars()
+ {
+ foreach ($this->subQueries as $query) {
+ if (! $query->allowsCustomVars()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->commentHistoryQuery = $this->db->select();
+ $this->select->from(
+ array('ch' => $this->commentHistoryQuery),
+ array()
+ );
+ $this->joinedVirtualTables['commenthistory'] = true;
+ }
+
+ /**
+ * Join history related columns and tables
+ */
+ protected function joinHistory()
+ {
+ // TODO: Ensure that one is selecting the history columns first...
+ $this->fetchHistoryColumns = true;
+ $this->requireVirtualTable('hosts');
+ $this->requireVirtualTable('services');
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $columns = array_keys(
+ $this->columnMap['commenthistory'] + $this->columnMap['hosts']
+ );
+ foreach ($this->columnMap['services'] as $column => $_) {
+ $columns[$column] = new Zend_Db_Expr('NULL');
+ }
+ if ($this->fetchHistoryColumns) {
+ $columns = array_merge($columns, array_keys($this->columnMap['history']));
+ }
+ $hosts = $this->createSubQuery('Hostcommenthistory', $columns);
+ $this->subQueries[] = $hosts;
+ $this->commentHistoryQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $columns = array_keys(
+ $this->columnMap['commenthistory'] + $this->columnMap['hosts'] + $this->columnMap['services']
+ );
+ if ($this->fetchHistoryColumns) {
+ $columns = array_merge($columns, array_keys($this->columnMap['history']));
+ }
+ $services = $this->createSubQuery('Servicecommenthistory', $columns);
+ $this->subQueries[] = $services;
+ $this->commentHistoryQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order($columnOrAlias, $dir = null)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->requireColumn($columnOrAlias);
+ }
+ return parent::order($columnOrAlias, $dir);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ $this->requireColumn($condition);
+ foreach ($this->subQueries as $sub) {
+ $sub->where($condition, $value);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->requireColumn($ex->getColumn());
+ foreach ($this->subQueries as $sub) {
+ $sub->whereEx($ex);
+ }
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactQuery.php
new file mode 100644
index 0000000..ca10323
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactQuery.php
@@ -0,0 +1,139 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\FilterExpression;
+use Zend_Db_Select;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for contacts
+ */
+class ContactQuery extends IdoQuery
+{
+ protected $columnMap = [
+ 'contacts' => [
+ 'contact_id' => 'c.contact_id',
+ 'contact' => 'c.contact',
+ 'contact_name' => 'c.contact_name',
+ 'contact_alias' => 'c.contact_alias',
+ 'contact_email' => 'c.contact_email',
+ 'contact_pager' => 'c.contact_pager',
+ 'contact_object_id' => 'c.contact_object_id',
+ 'contact_has_host_notfications' => 'c.contact_has_host_notfications',
+ 'contact_has_service_notfications' => 'c.contact_has_service_notfications',
+ 'contact_can_submit_commands' => 'c.contact_can_submit_commands',
+ 'contact_notify_service_recovery' => 'c.contact_notify_service_recovery',
+ 'contact_notify_service_warning' => 'c.contact_notify_service_warning',
+ 'contact_notify_service_critical' => 'c.contact_notify_service_critical',
+ 'contact_notify_service_unknown' => 'c.contact_notify_service_unknown',
+ 'contact_notify_service_flapping' => 'c.contact_notify_service_flapping',
+ 'contact_notify_service_downtime' => 'c.contact_notify_service_downtime',
+ 'contact_notify_host_recovery' => 'c.contact_notify_host_recovery',
+ 'contact_notify_host_down' => 'c.contact_notify_host_down',
+ 'contact_notify_host_unreachable' => 'c.contact_notify_host_unreachable',
+ 'contact_notify_host_flapping' => 'c.contact_notify_host_flapping',
+ 'contact_notify_host_downtime' => 'c.contact_notify_host_downtime',
+ 'contact_notify_host_timeperiod' => 'c.contact_notify_host_timeperiod',
+ 'contact_notify_service_timeperiod' => 'c.contact_notify_service_timeperiod'
+ ]
+ ];
+
+ /** @var Zend_Db_Select The union */
+ protected $contactQuery;
+
+ /** @var IdoQuery[] Subqueries used for the contact query */
+ protected $subQueries = [];
+
+ public function allowsCustomVars()
+ {
+ foreach ($this->subQueries as $query) {
+ if (! $query->allowsCustomVars()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public function addFilter(Filter $filter)
+ {
+ $strangers = array_diff(
+ $filter->listFilteredColumns(),
+ array_keys($this->columnMap['contacts'])
+ );
+ if (! empty($strangers)) {
+ $this->transformToUnion();
+ }
+
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+
+ return $this;
+ }
+
+ protected function joinBaseTables()
+ {
+ $this->contactQuery = $this->createSubQuery('Hostcontact', array_keys($this->columnMap['contacts']));
+ $this->contactQuery->setIsSubQuery();
+ $this->subQueries[] = $this->contactQuery;
+
+ $this->select->from(
+ ['c' => $this->contactQuery],
+ []
+ );
+
+ $this->joinedVirtualTables['contacts'] = true;
+ }
+
+ public function order($columnOrAlias, $dir = null)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->requireColumn($columnOrAlias);
+ }
+
+ return parent::order($columnOrAlias, $dir);
+ }
+
+ public function where($condition, $value = null)
+ {
+ $this->requireColumn($condition);
+ foreach ($this->subQueries as $sub) {
+ $sub->where($condition, $value);
+ }
+
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->requireColumn($ex->getColumn());
+ foreach ($this->subQueries as $sub) {
+ $sub->whereEx($ex);
+ }
+
+ return $this;
+ }
+
+ public function transformToUnion()
+ {
+ $this->contactQuery = $this->db->select();
+ $this->select->reset();
+ $this->subQueries = [];
+
+ $this->select->distinct()->from(
+ ['c' => $this->contactQuery],
+ []
+ );
+
+ $hosts = $this->createSubQuery('Hostcontact', array_keys($this->columnMap['contacts']));
+ $this->subQueries[] = $hosts;
+ $this->contactQuery->union([$hosts], Zend_Db_Select::SQL_UNION_ALL);
+
+ $services = $this->createSubQuery('Servicecontact', array_keys($this->columnMap['contacts']));
+ $this->subQueries[] = $services;
+ $this->contactQuery->union([$services], Zend_Db_Select::SQL_UNION_ALL);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php
new file mode 100644
index 0000000..7d4cbc1
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php
@@ -0,0 +1,214 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for contact groups
+ */
+class ContactgroupQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('contactgroups' => array('cg.contactgroup_id', 'cgo.object_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hosts', 'members', 'services');
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'contactgroups' => array(
+ 'contactgroup' => 'cgo.name1 COLLATE latin1_general_ci',
+ 'contactgroup_name' => 'cgo.name1',
+ 'contactgroup_alias' => 'cg.alias COLLATE latin1_general_ci'
+ ),
+ 'members' => array(
+ 'contact_count' => 'SUM(CASE WHEN cgmo.object_id IS NOT NULL THEN 1 ELSE 0 END)'
+ ),
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host' => 'ho.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'ho.name1',
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci',
+ 'service_host_name' => 'so.name1'
+ )
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ array('cg' => $this->prefix . 'contactgroups'),
+ array()
+ )->join(
+ array('cgo' => $this->prefix . 'objects'),
+ 'cgo.object_id = cg.contactgroup_object_id AND cgo.is_active = 1 AND cgo.objecttype_id = 11',
+ array()
+ );
+ $this->joinedVirtualTables['contactgroups'] = true;
+ }
+
+ /**
+ * Join contact group members
+ */
+ protected function joinMembers()
+ {
+ $this->select->joinLeft(
+ array('cgm' => $this->prefix . 'contactgroup_members'),
+ 'cgm.contactgroup_id = cg.contactgroup_id',
+ array()
+ )->joinLeft(
+ array('cgmo' => $this->prefix . 'objects'),
+ 'cgmo.object_id = cgm.contact_object_id AND cgmo.is_active = 1 AND cgmo.objecttype_id = 10',
+ array()
+ );
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->requireVirtualTable('hosts');
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->select->joinLeft(
+ array('hcg' => $this->prefix . 'host_contactgroups'),
+ 'hcg.contactgroup_object_id = cg.contactgroup_object_id',
+ array()
+ )->joinLeft(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_id = hcg.host_id',
+ array()
+ )->joinLeft(
+ array('ho' => $this->prefix . 'objects'),
+ 'ho.object_id = h.host_object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = cg.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = s.service_object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sg.servicegroup_id = sgm.servicegroup_id',
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->joinLeft(
+ array('scg' => $this->prefix . 'service_contactgroups'),
+ 'scg.contactgroup_object_id = cg.contactgroup_object_id',
+ array()
+ )->joinLeft(
+ array('s' => $this->prefix . 'services'),
+ 's.service_id = scg.service_id',
+ array()
+ )->joinLeft(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $this->requireVirtualTable('hosts');
+
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 'ho.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $this->requireVirtualTable('services');
+
+ $query->joinVirtualTable('members');
+
+ return ['sgm.service_object_id', 'so.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/CustomvarQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CustomvarQuery.php
new file mode 100644
index 0000000..1492894
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/CustomvarQuery.php
@@ -0,0 +1,116 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Application\Config;
+use Icinga\Data\Filter\FilterExpression;
+
+class CustomvarQuery extends IdoQuery
+{
+ protected $columnMap = array(
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'customvariablestatus' => array(
+ 'varname' => 'cvs.varname',
+ 'varvalue' => 'cvs.varvalue',
+ 'is_json' => 'cvs.is_json',
+ ),
+ 'objects' => array(
+ 'host' => 'cvo.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'cvo.name1',
+ 'service' => 'cvo.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'cvo.name2',
+ 'contact' => 'cvo.name1 COLLATE latin1_general_ci',
+ 'contact_name' => 'cvo.name1',
+ 'object_type' => "CASE cvo.objecttype_id WHEN 1 THEN 'host' WHEN 2 THEN 'service' WHEN 10 THEN 'contact' ELSE 'invalid' END",
+ 'object_type_id' => 'cvo.objecttype_id'
+// 'object_type' => "CASE cvo.objecttype_id WHEN 1 THEN 'host' WHEN 2 THEN 'service' WHEN 3 THEN 'hostgroup' WHEN 4 THEN 'servicegroup' WHEN 5 THEN 'hostescalation' WHEN 6 THEN 'serviceescalation' WHEN 7 THEN 'hostdependency' WHEN 8 THEN 'servicedependency' WHEN 9 THEN 'timeperiod' WHEN 10 THEN 'contact' WHEN 11 THEN 'contactgroup' WHEN 12 THEN 'command' ELSE 'other' END"
+ ),
+ );
+
+ public function where($expression, $parameters = null)
+ {
+ $types = array('host' => 1, 'service' => 2, 'contact' => 10);
+ if ($expression === 'object_type') {
+ parent::where('object_type_id', $types[$parameters]);
+ } else {
+ parent::where($expression, $parameters);
+ }
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $types = ['host' => 1, 'service' => 2, 'contact' => 10];
+ if ($ex->getColumn() === 'object_type') {
+ $ex = clone $ex;
+ $ex->setColumn('object_type_id');
+ $ex->setExpression($types[$ex->getExpression()]);
+ }
+
+ parent::whereEx($ex);
+
+ return $this;
+ }
+
+ protected function joinBaseTables()
+ {
+ if (version_compare($this->getIdoVersion(), '1.12.0', '<')) {
+ $this->columnMap['customvariablestatus']['is_json'] = '(0)';
+ }
+
+ if (! (bool) Config::module('monitoring')->get('ido', 'use_customvar_status_table', true)) {
+ $table = 'customvariables';
+ } else {
+ $table = 'customvariablestatus';
+ }
+
+ $this->select->from(
+ array('cvs' => $this->prefix . $table),
+ array()
+ )->join(
+ array('cvo' => $this->prefix . 'objects'),
+ 'cvs.object_id = cvo.object_id AND cvo.is_active = 1',
+ array()
+ );
+ $this->joinedVirtualTables = array(
+ 'customvariablestatus' => true,
+ 'objects' => true
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = cvs.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getGroup()
+ {
+ $group = parent::getGroup();
+ if (! empty($group) && $this->ds->getDbType() === 'pgsql') {
+ foreach ($this->columnMap as $table => $columns) {
+ $pk = ($table === 'objects' ? 'cvo.' : 'cvs.') . $this->getPrimaryKeyColumn($table);
+ foreach ($columns as $alias => $_) {
+ if (! in_array($pk, $group, true) && in_array($alias, $group, true)) {
+ $group[] = $pk;
+ break;
+ }
+ }
+ }
+ }
+
+ return $group;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php
new file mode 100644
index 0000000..9bc1d88
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php
@@ -0,0 +1,163 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\FilterExpression;
+use Zend_Db_Expr;
+use Zend_Db_Select;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for host and service downtimes
+ */
+class DowntimeQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'downtimes' => array(
+ 'downtime_author' => 'd.downtime_author',
+ 'downtime_author_name' => 'd.downtime_author_name',
+ 'downtime_comment' => 'd.downtime_comment',
+ 'downtime_duration' => 'd.downtime_duration',
+ 'downtime_end' => 'd.downtime_end',
+ 'downtime_entry_time' => 'd.downtime_entry_time',
+ 'downtime_internal_id' => 'd.downtime_internal_id',
+ 'downtime_is_fixed' => 'd.downtime_is_fixed',
+ 'downtime_is_flexible' => 'd.downtime_is_flexible',
+ 'downtime_is_in_effect' => 'd.downtime_is_in_effect',
+ 'downtime_name' => 'd.downtime_name',
+ 'downtime_scheduled_end' => 'd.downtime_scheduled_end',
+ 'downtime_scheduled_start' => 'd.downtime_scheduled_start',
+ 'downtime_start' => 'd.downtime_start',
+ 'object_type' => 'd.object_type',
+ 'instance_name' => 'd.instance_name'
+ ),
+ 'hosts' => array(
+ 'host_display_name' => 'd.host_display_name',
+ 'host_name' => 'd.host_name',
+ 'host_state' => 'd.host_state'
+ ),
+ 'services' => array(
+ 'service_description' => 'd.service_description',
+ 'service_display_name' => 'd.service_display_name',
+ 'service_host_name' => 'd.service_host_name',
+ 'service_state' => 'd.service_state'
+ )
+ );
+
+ /**
+ * The union
+ *
+ * @var Zend_Db_Select
+ */
+ protected $downtimeQuery;
+
+ /**
+ * Subqueries used for the downtime query
+ *
+ * @var IdoQuery[]
+ */
+ protected $subQueries = array();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function allowsCustomVars()
+ {
+ foreach ($this->subQueries as $query) {
+ if (! $query->allowsCustomVars()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ if (version_compare($this->getIdoVersion(), '1.14.0', '<')) {
+ $this->columnMap['downtimes']['downtime_name'] = '(NULL)';
+ }
+ $this->downtimeQuery = $this->db->select();
+ $this->select->from(
+ array('d' => $this->downtimeQuery),
+ array()
+ );
+ $this->joinedVirtualTables['downtimes'] = true;
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $columns = array_keys($this->columnMap['downtimes'] + $this->columnMap['hosts']);
+ foreach (array_keys($this->columnMap['services']) as $column) {
+ $columns[$column] = new Zend_Db_Expr('NULL');
+ }
+ $hosts = $this->createSubQuery('hostdowntime', $columns);
+ $this->subQueries[] = $hosts;
+ $this->downtimeQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $columns = array_keys($this->columnMap['downtimes'] + $this->columnMap['hosts'] + $this->columnMap['services']);
+ $services = $this->createSubQuery('servicedowntime', $columns);
+ $this->subQueries[] = $services;
+ $this->downtimeQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order($columnOrAlias, $dir = null)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->requireColumn($columnOrAlias);
+ }
+ return parent::order($columnOrAlias, $dir);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ $this->requireColumn($condition);
+ foreach ($this->subQueries as $sub) {
+ $sub->where($condition, $value);
+ }
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->requireColumn($ex->getColumn());
+ foreach ($this->subQueries as $sub) {
+ $sub->whereEx($ex);
+ }
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php
new file mode 100644
index 0000000..de47418
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php
@@ -0,0 +1,179 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\FilterExpression;
+use Zend_Db_Expr;
+use Zend_Db_Select;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for host and service downtime end history records
+ */
+class DowntimeendhistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'downtimehistory' => array(
+ 'id' => 'deh.id',
+ 'object_type' => 'deh.object_type'
+ ),
+ 'history' => array(
+ 'type' => 'deh.type',
+ 'timestamp' => 'deh.timestamp',
+ 'object_id' => 'deh.object_id',
+ 'state' => 'deh.state',
+ 'output' => 'deh.output'
+ ),
+ 'hosts' => array(
+ 'host_display_name' => 'deh.host_display_name',
+ 'host_name' => 'deh.host_name'
+ ),
+ 'services' => array(
+ 'service_description' => 'deh.service_description',
+ 'service_display_name' => 'deh.service_display_name',
+ 'service_host_name' => 'deh.service_host_name'
+ )
+ );
+
+ /**
+ * The union
+ *
+ * @var Zend_Db_Select
+ */
+ protected $downtimeEndHistoryQuery;
+
+ /**
+ * Subqueries used for the downtime end history query
+ *
+ * @var IdoQuery[]
+ */
+ protected $subQueries = array();
+
+ /**
+ * Whether to additionally select all history columns
+ *
+ * @var bool
+ */
+ protected $fetchHistoryColumns = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function allowsCustomVars()
+ {
+ foreach ($this->subQueries as $query) {
+ if (! $query->allowsCustomVars()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->downtimeEndHistoryQuery = $this->db->select();
+ $this->select->from(
+ array('deh' => $this->downtimeEndHistoryQuery),
+ array()
+ );
+ $this->joinedVirtualTables['downtimehistory'] = true;
+ }
+
+ /**
+ * Join history related columns and tables
+ */
+ protected function joinHistory()
+ {
+ // TODO: Ensure that one is selecting the history columns first...
+ $this->fetchHistoryColumns = true;
+ $this->requireVirtualTable('hosts');
+ $this->requireVirtualTable('services');
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $columns = array_keys(
+ $this->columnMap['downtimehistory'] + $this->columnMap['hosts']
+ );
+ foreach ($this->columnMap['services'] as $column => $_) {
+ $columns[$column] = new Zend_Db_Expr('NULL');
+ }
+ if ($this->fetchHistoryColumns) {
+ $columns = array_merge($columns, array_keys($this->columnMap['history']));
+ }
+ $hosts = $this->createSubQuery('Hostdowntimeendhistory', $columns);
+ $this->subQueries[] = $hosts;
+ $this->downtimeEndHistoryQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $columns = array_keys(
+ $this->columnMap['downtimehistory'] + $this->columnMap['hosts'] + $this->columnMap['services']
+ );
+ if ($this->fetchHistoryColumns) {
+ $columns = array_merge($columns, array_keys($this->columnMap['history']));
+ }
+ $services = $this->createSubQuery('Servicedowntimeendhistory', $columns);
+ $this->subQueries[] = $services;
+ $this->downtimeEndHistoryQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order($columnOrAlias, $dir = null)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->requireColumn($columnOrAlias);
+ }
+ return parent::order($columnOrAlias, $dir);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ $this->requireColumn($condition);
+ foreach ($this->subQueries as $sub) {
+ $sub->where($condition, $value);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->requireColumn($ex->getColumn());
+ foreach ($this->subQueries as $sub) {
+ $sub->whereEx($ex);
+ }
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeeventQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeeventQuery.php
new file mode 100644
index 0000000..04e6aa5
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeeventQuery.php
@@ -0,0 +1,42 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for host and service downtime events
+ */
+class DowntimeeventQuery extends IdoQuery
+{
+ protected $columnMap = array(
+ 'downtimeevent' => array(
+ 'downtimeevent_id' => 'dth.downtimehistory_id',
+ 'downtimeevent_entry_time' => 'UNIX_TIMESTAMP(dth.entry_time)',
+ 'downtimeevent_author_name' => 'dth.author_name',
+ 'downtimeevent_comment_data' => 'dth.comment_data',
+ 'downtimeevent_is_fixed' => 'dth.is_fixed',
+ 'downtimeevent_scheduled_start_time' => 'UNIX_TIMESTAMP(dth.scheduled_start_time)',
+ 'downtimeevent_scheduled_end_time' => 'UNIX_TIMESTAMP(dth.scheduled_end_time)',
+ 'downtimeevent_was_started' => 'dth.was_started',
+ 'downtimeevent_actual_start_time' => 'UNIX_TIMESTAMP(dth.actual_start_time)',
+ 'downtimeevent_actual_end_time' => 'UNIX_TIMESTAMP(dth.actual_end_time)',
+ 'downtimeevent_was_cancelled' => 'dth.was_cancelled',
+ 'downtimeevent_is_in_effect' => 'dth.is_in_effect',
+ 'downtimeevent_trigger_time' => 'UNIX_TIMESTAMP(dth.trigger_time)'
+ ),
+ 'object' => array(
+ 'host_name' => 'o.name1',
+ 'service_description' => 'o.name2'
+ )
+ );
+
+ protected function joinBaseTables()
+ {
+ $this->select()
+ ->from(array('dth' => $this->prefix . 'downtimehistory'), array())
+ ->join(array('o' => $this->prefix . 'objects'), 'dth.object_id = o.object_id', array());
+
+ $this->joinedVirtualTables['downtimeevent'] = true;
+ $this->joinedVirtualTables['object'] = true;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php
new file mode 100644
index 0000000..3ba600d
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php
@@ -0,0 +1,179 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\FilterExpression;
+use Zend_Db_Expr;
+use Zend_Db_Select;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for host and service downtime start history records
+ */
+class DowntimestarthistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'downtimehistory' => array(
+ 'id' => 'dsh.id',
+ 'object_type' => 'dsh.object_type'
+ ),
+ 'history' => array(
+ 'type' => 'dsh.type',
+ 'timestamp' => 'dsh.timestamp',
+ 'object_id' => 'dsh.object_id',
+ 'state' => 'dsh.state',
+ 'output' => 'dsh.output'
+ ),
+ 'hosts' => array(
+ 'host_display_name' => 'dsh.host_display_name',
+ 'host_name' => 'dsh.host_name'
+ ),
+ 'services' => array(
+ 'service_description' => 'dsh.service_description',
+ 'service_display_name' => 'dsh.service_display_name',
+ 'service_host_name' => 'dsh.service_host_name'
+ )
+ );
+
+ /**
+ * The union
+ *
+ * @var Zend_Db_Select
+ */
+ protected $downtimeStartHistoryQuery;
+
+ /**
+ * Subqueries used for the downtime start history query
+ *
+ * @var IdoQuery[]
+ */
+ protected $subQueries = array();
+
+ /**
+ * Whether to additionally select all history columns
+ *
+ * @var bool
+ */
+ protected $fetchHistoryColumns = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function allowsCustomVars()
+ {
+ foreach ($this->subQueries as $query) {
+ if (! $query->allowsCustomVars()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->downtimeStartHistoryQuery = $this->db->select();
+ $this->select->from(
+ array('dsh' => $this->downtimeStartHistoryQuery),
+ array()
+ );
+ $this->joinedVirtualTables['downtimehistory'] = true;
+ }
+
+ /**
+ * Join history related columns and tables
+ */
+ protected function joinHistory()
+ {
+ // TODO: Ensure that one is selecting the history columns first...
+ $this->fetchHistoryColumns = true;
+ $this->requireVirtualTable('hosts');
+ $this->requireVirtualTable('services');
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $columns = array_keys(
+ $this->columnMap['downtimehistory'] + $this->columnMap['hosts']
+ );
+ foreach ($this->columnMap['services'] as $column => $_) {
+ $columns[$column] = new Zend_Db_Expr('NULL');
+ }
+ if ($this->fetchHistoryColumns) {
+ $columns = array_merge($columns, array_keys($this->columnMap['history']));
+ }
+ $hosts = $this->createSubQuery('Hostdowntimestarthistory', $columns);
+ $this->subQueries[] = $hosts;
+ $this->downtimeStartHistoryQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $columns = array_keys(
+ $this->columnMap['downtimehistory'] + $this->columnMap['hosts'] + $this->columnMap['services']
+ );
+ if ($this->fetchHistoryColumns) {
+ $columns = array_merge($columns, array_keys($this->columnMap['history']));
+ }
+ $services = $this->createSubQuery('Servicedowntimestarthistory', $columns);
+ $this->subQueries[] = $services;
+ $this->downtimeStartHistoryQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order($columnOrAlias, $dir = null)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->requireColumn($columnOrAlias);
+ }
+ return parent::order($columnOrAlias, $dir);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ $this->requireColumn($condition);
+ foreach ($this->subQueries as $sub) {
+ $sub->where($condition, $value);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->requireColumn($ex->getColumn());
+ foreach ($this->subQueries as $sub) {
+ $sub->whereEx($ex);
+ }
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyhostgroupQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyhostgroupQuery.php
new file mode 100644
index 0000000..a99d6b7
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyhostgroupQuery.php
@@ -0,0 +1,38 @@
+<?php
+/* Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+class EmptyhostgroupQuery extends HostgroupQuery
+{
+ protected $subQueryTargets = [];
+
+ protected $columnMap = [
+ 'hostgroups' => [
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1',
+ 'host_name' => '(NULL)',
+ 'service_description' => '(NULL)',
+ 'servicegroup_name' => '(NULL)',
+ 'host_contact' => '(NULL)',
+ 'host_contactgroup' => '(NULL)'
+ ],
+ 'instances' => [
+ 'instance_name' => 'i.instance_name'
+ ]
+ ];
+
+ protected function joinBaseTables()
+ {
+ parent::joinBaseTables();
+
+ $this->select->joinLeft(
+ ['ehgm' => $this->prefix . 'hostgroup_members'],
+ 'ehgm.hostgroup_id = hg.hostgroup_id',
+ []
+ );
+ $this->select->group(['hgo.object_id', 'hg.hostgroup_id']);
+ $this->select->having('COUNT(ehgm.hostgroup_member_id) = ?', 0);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyservicegroupQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyservicegroupQuery.php
new file mode 100644
index 0000000..88ee4c3
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyservicegroupQuery.php
@@ -0,0 +1,51 @@
+<?php
+/* Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+class EmptyservicegroupQuery extends ServicegroupQuery
+{
+ protected $subQueryTargets = [];
+
+ protected $columnMap = [
+ 'servicegroups' => [
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'host_name' => '(NULL)',
+ 'hostgroup_name' => '(NULL)',
+ 'service_description' => '(NULL)',
+ 'host_contact' => '(NULL)',
+ 'host_contactgroup' => '(NULL)',
+ 'service_contact' => '(NULL)',
+ 'service_contactgroup' => '(NULL)'
+ ],
+ 'instances' => [
+ 'instance_name' => 'i.instance_name'
+ ]
+ ];
+
+ protected function joinBaseTables()
+ {
+ parent::joinBaseTables();
+
+ $this->select->joinLeft(
+ ['esgm' => $this->prefix . 'servicegroup_members'],
+ 'esgm.servicegroup_id = sg.servicegroup_id',
+ []
+ );
+ $this->select->group(['sgo.object_id', 'sg.servicegroup_id']);
+ $this->select->having('COUNT(esgm.servicegroup_member_id) = ?', 0);
+ }
+
+ protected function joinHosts()
+ {
+ parent::joinHosts();
+
+ $this->select->joinLeft(
+ ['h' => 'icinga_hosts'],
+ 'h.host_object_id = s.host_object_id',
+ []
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridQuery.php
new file mode 100644
index 0000000..4a75bf2
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridQuery.php
@@ -0,0 +1,56 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+abstract class EventgridQuery extends StatehistoryQuery
+{
+ /**
+ * The columns additionally provided by this query
+ *
+ * @var array
+ */
+ protected $additionalColumns = array(
+ 'day' => 'DATE(FROM_UNIXTIME(sth.timestamp))',
+ 'cnt_up' => "SUM(CASE WHEN sth.state = 0 THEN 1 ELSE 0 END)",
+ 'cnt_down_hard' => "SUM(CASE WHEN sth.state = 1 AND sth.type = 'hard_state' THEN 1 ELSE 0 END)",
+ 'cnt_down' => "SUM(CASE WHEN sth.state = 1 THEN 1 ELSE 0 END)",
+ 'cnt_unreachable_hard' => "SUM(CASE WHEN sth.state = 2 AND sth.type = 'hard_state' THEN 1 ELSE 0 END)",
+ 'cnt_unreachable' => "SUM(CASE WHEN sth.state = 2 THEN 1 ELSE 0 END)",
+ 'cnt_unknown' => "SUM(CASE WHEN sth.state = 3 THEN 1 ELSE 0 END)",
+ 'cnt_unknown_hard' => "SUM(CASE WHEN sth.state = 3 AND sth.type = 'hard_state' THEN 1 ELSE 0 END)",
+ 'cnt_critical' => "SUM(CASE WHEN sth.state = 2 THEN 1 ELSE 0 END)",
+ 'cnt_critical_hard' => "SUM(CASE WHEN sth.state = 2 AND sth.type = 'hard_state' THEN 1 ELSE 0 END)",
+ 'cnt_warning' => "SUM(CASE WHEN sth.state = 1 THEN 1 ELSE 0 END)",
+ 'cnt_warning_hard' => "SUM(CASE WHEN sth.state = 1 AND sth.type = 'hard_state' THEN 1 ELSE 0 END)",
+ 'cnt_ok' => "SUM(CASE WHEN sth.state = 0 THEN 1 ELSE 0 END)"
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ parent::joinBaseTables();
+ $this->requireVirtualTable('history');
+ $this->columnMap['statehistory'] += $this->additionalColumns;
+ $this->select->group(array('DATE(FROM_UNIXTIME(sth.timestamp))'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order($columnOrAlias, $dir = null)
+ {
+ if (array_key_exists($columnOrAlias, $this->additionalColumns)) {
+ $subQueries = $this->subQueries;
+ $this->subQueries = array();
+ parent::order($columnOrAlias, $dir);
+ $this->subQueries = $subQueries;
+ } else {
+ parent::order($columnOrAlias, $dir);
+ }
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridhostsQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridhostsQuery.php
new file mode 100644
index 0000000..62d92e4
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridhostsQuery.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+class EventgridhostsQuery extends EventgridQuery
+{
+
+ /**
+ * Join history related columns and tables, hosts only
+ */
+ protected function joinHistory()
+ {
+ $this->fetchHistoryColumns = true;
+ $this->requireVirtualTable('hosts');
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridservicesQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridservicesQuery.php
new file mode 100644
index 0000000..424de45
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridservicesQuery.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+class EventgridservicesQuery extends EventgridQuery
+{
+ /**
+ * Join history related columns and tables, services only
+ */
+ protected function joinHistory()
+ {
+ $this->fetchHistoryColumns = true;
+ $this->requireVirtualTable('services');
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventhistoryQuery.php
new file mode 100644
index 0000000..680e2ca
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventhistoryQuery.php
@@ -0,0 +1,134 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\FilterExpression;
+use Zend_Db_Select;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for event history records
+ */
+class EventhistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $useSubqueryCount = true;
+
+ /**
+ * Subqueries used for the event history query
+ *
+ * @type IdoQuery[]
+ */
+ protected $subQueries = array();
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'eventhistory' => array(
+ 'id' => 'eh.id',
+ 'host_name' => 'eh.host_name',
+ 'service_description' => 'eh.service_description',
+ 'object_type' => 'eh.object_type',
+ 'timestamp' => 'eh.timestamp',
+ 'state' => 'eh.state',
+ 'output' => 'eh.output',
+ 'type' => 'eh.type',
+ 'host_display_name' => 'eh.host_display_name',
+ 'service_display_name' => 'eh.service_display_name'
+ )
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $columns = array(
+ 'id',
+ 'timestamp',
+ 'output',
+ 'type',
+ 'state',
+ 'object_type',
+ 'host_name',
+ 'service_description',
+ 'host_display_name',
+ 'service_display_name'
+ );
+ $this->subQueries = array(
+ $this->createSubQuery('Notificationhistory', $columns),
+ $this->createSubQuery('Statehistory', $columns),
+ $this->createSubQuery('Downtimestarthistory', $columns),
+ $this->createSubQuery('Downtimeendhistory', $columns),
+ $this->createSubQuery('Commenthistory', $columns),
+ $this->createSubQuery('Commentdeletionhistory', $columns),
+ $this->createSubQuery('Flappingstarthistory', $columns),
+ $this->createSubQuery('Flappingendhistory', $columns)
+ );
+ $sub = $this->db->select()->union($this->subQueries, Zend_Db_Select::SQL_UNION_ALL);
+ $this->select->from(array('eh' => $sub), array());
+ $this->joinedVirtualTables['eventhistory'] = true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function allowsCustomVars()
+ {
+ foreach ($this->subQueries as $query) {
+ if (! $query->allowsCustomVars()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order($columnOrAlias, $dir = null)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->requireColumn($columnOrAlias);
+ }
+ return parent::order($columnOrAlias, $dir);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ $this->requireColumn($condition);
+ foreach ($this->subQueries as $sub) {
+ $sub->where($condition, $value);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->requireColumn($ex->getColumn());
+ foreach ($this->subQueries as $sub) {
+ $sub->whereEx($ex);
+ }
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingendhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingendhistoryQuery.php
new file mode 100644
index 0000000..7bdf332
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingendhistoryQuery.php
@@ -0,0 +1,49 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Zend_Db_Expr;
+use Zend_Db_Select;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for host and service flapping end history records
+ */
+class FlappingendhistoryQuery extends FlappingstarthistoryQuery
+{
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $columns = array_keys(
+ $this->columnMap['flappinghistory'] + $this->columnMap['hosts']
+ );
+ foreach ($this->columnMap['services'] as $column => $_) {
+ $columns[$column] = new Zend_Db_Expr('NULL');
+ }
+ if ($this->fetchHistoryColumns) {
+ $columns = array_merge($columns, array_keys($this->columnMap['history']));
+ }
+ $hosts = $this->createSubQuery('Hostflappingendhistory', $columns);
+ $this->subQueries[] = $hosts;
+ $this->flappingStartHistoryQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $columns = array_keys(
+ $this->columnMap['flappinghistory'] + $this->columnMap['hosts'] + $this->columnMap['services']
+ );
+ if ($this->fetchHistoryColumns) {
+ $columns = array_merge($columns, array_keys($this->columnMap['history']));
+ }
+ $services = $this->createSubQuery('Serviceflappingendhistory', $columns);
+ $this->subQueries[] = $services;
+ $this->flappingStartHistoryQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingeventQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingeventQuery.php
new file mode 100644
index 0000000..d993467
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingeventQuery.php
@@ -0,0 +1,36 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for host and service flapping events
+ */
+class FlappingeventQuery extends IdoQuery
+{
+ protected $columnMap = array(
+ 'flappingevent' => array(
+ 'flappingevent_id' => 'fh.flappinghistory_id',
+ 'flappingevent_event_time' => 'UNIX_TIMESTAMP(fh.event_time)',
+ 'flappingevent_event_type' => "(CASE fh.event_type WHEN 1000 THEN 'flapping' WHEN 1001 THEN 'flapping_deleted' ELSE NULL END)",
+ 'flappingevent_reason_type' => "(CASE fh.reason_type WHEN 1 THEN 'stopped' WHEN 2 THEN 'disabled' ELSE NULL END)",
+ 'flappingevent_percent_state_change' => 'fh.percent_state_change',
+ 'flappingevent_low_threshold' => 'fh.low_threshold',
+ 'flappingevent_high_threshold' => 'fh.high_threshold'
+ ),
+ 'object' => array(
+ 'host_name' => 'o.name1',
+ 'service_description' => 'o.name2'
+ )
+ );
+
+ protected function joinBaseTables()
+ {
+ $this->select()
+ ->from(array('fh' => $this->prefix . 'flappinghistory'), array())
+ ->join(array('o' => $this->prefix . 'objects'), 'fh.object_id = o.object_id', array());
+
+ $this->joinedVirtualTables['flappingevent'] = true;
+ $this->joinedVirtualTables['object'] = true;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php
new file mode 100644
index 0000000..5c8bec5
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php
@@ -0,0 +1,179 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\FilterExpression;
+use Zend_Db_Expr;
+use Zend_Db_Select;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for host and service flapping start history records
+ */
+class FlappingstarthistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'flappinghistory' => array(
+ 'id' => 'fsh.id',
+ 'object_type' => 'fsh.object_type'
+ ),
+ 'history' => array(
+ 'type' => 'fsh.type',
+ 'timestamp' => 'fsh.timestamp',
+ 'object_id' => 'fsh.object_id',
+ 'state' => 'fsh.state',
+ 'output' => 'fsh.output'
+ ),
+ 'hosts' => array(
+ 'host_display_name' => 'fsh.host_display_name',
+ 'host_name' => 'fsh.host_name'
+ ),
+ 'services' => array(
+ 'service_description' => 'fsh.service_description',
+ 'service_display_name' => 'fsh.service_display_name',
+ 'service_host_name' => 'fsh.service_host_name'
+ )
+ );
+
+ /**
+ * The union
+ *
+ * @var Zend_Db_Select
+ */
+ protected $flappingStartHistoryQuery;
+
+ /**
+ * Subqueries used for the flapping start history query
+ *
+ * @var IdoQuery[]
+ */
+ protected $subQueries = array();
+
+ /**
+ * Whether to additionally select all history columns
+ *
+ * @var bool
+ */
+ protected $fetchHistoryColumns = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function allowsCustomVars()
+ {
+ foreach ($this->subQueries as $query) {
+ if (! $query->allowsCustomVars()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->flappingStartHistoryQuery = $this->db->select();
+ $this->select->from(
+ array('fsh' => $this->flappingStartHistoryQuery),
+ array()
+ );
+ $this->joinedVirtualTables['flappinghistory'] = true;
+ }
+
+ /**
+ * Join history related columns and tables
+ */
+ protected function joinHistory()
+ {
+ // TODO: Ensure that one is selecting the history columns first...
+ $this->fetchHistoryColumns = true;
+ $this->requireVirtualTable('hosts');
+ $this->requireVirtualTable('services');
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $columns = array_keys(
+ $this->columnMap['flappinghistory'] + $this->columnMap['hosts']
+ );
+ foreach ($this->columnMap['services'] as $column => $_) {
+ $columns[$column] = new Zend_Db_Expr('NULL');
+ }
+ if ($this->fetchHistoryColumns) {
+ $columns = array_merge($columns, array_keys($this->columnMap['history']));
+ }
+ $hosts = $this->createSubQuery('Hostflappingstarthistory', $columns);
+ $this->subQueries[] = $hosts;
+ $this->flappingStartHistoryQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $columns = array_keys(
+ $this->columnMap['flappinghistory'] + $this->columnMap['hosts'] + $this->columnMap['services']
+ );
+ if ($this->fetchHistoryColumns) {
+ $columns = array_merge($columns, array_keys($this->columnMap['history']));
+ }
+ $services = $this->createSubQuery('Serviceflappingstarthistory', $columns);
+ $this->subQueries[] = $services;
+ $this->flappingStartHistoryQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order($columnOrAlias, $dir = null)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->requireColumn($columnOrAlias);
+ }
+ return parent::order($columnOrAlias, $dir);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ $this->requireColumn($condition);
+ foreach ($this->subQueries as $sub) {
+ $sub->where($condition, $value);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->requireColumn($ex->getColumn());
+ foreach ($this->subQueries as $sub) {
+ $sub->whereEx($ex);
+ }
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/GroupsummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/GroupsummaryQuery.php
new file mode 100644
index 0000000..60ea5ef
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/GroupsummaryQuery.php
@@ -0,0 +1,131 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Zend_Db_Select;
+
+/**
+ * Query for host and service group summaries
+ */
+class GroupsummaryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'hoststatussummary' => array(
+ 'hostgroup' => 'hostgroup COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hostgroup_alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hostgroup_name',
+ 'hosts_up' => 'SUM(CASE WHEN object_type = \'host\' AND state = 0 THEN 1 ELSE 0 END)',
+ 'hosts_unreachable' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 THEN 1 ELSE 0 END)',
+ 'hosts_unreachable_handled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 AND acknowledged + in_downtime != 0 THEN 1 ELSE 0 END)',
+ 'hosts_unreachable_unhandled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 AND acknowledged + in_downtime = 0 THEN 1 ELSE 0 END)',
+ 'hosts_down' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 THEN 1 ELSE 0 END)',
+ 'hosts_down_handled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 AND acknowledged + in_downtime != 0 THEN 1 ELSE 0 END)',
+ 'hosts_down_last_state_change_handled' => 'MAX(CASE WHEN object_type = \'host\' AND state = 1 AND acknowledged + in_downtime != 0 THEN state_change ELSE 0 END)',
+ 'hosts_down_last_state_change_unhandled' => 'MAX(CASE WHEN object_type = \'host\' AND state = 1 AND acknowledged + in_downtime = 0 THEN state_change ELSE 0 END)',
+ 'hosts_down_unhandled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 AND acknowledged + in_downtime = 0 THEN 1 ELSE 0 END)',
+ 'hosts_pending' => 'SUM(CASE WHEN object_type = \'host\' AND state = 99 THEN 1 ELSE 0 END)',
+ 'hosts_pending_last_state_change' => 'MAX(CASE WHEN object_type = \'host\' AND state = 99 THEN state_change ELSE 0 END)',
+ 'hosts_severity' => 'MAX(CASE WHEN object_type = \'host\' THEN severity ELSE 0 END)',
+ 'hosts_total' => 'SUM(CASE WHEN object_type = \'host\' THEN 1 ELSE 0 END)',
+ 'hosts_unreachable_last_state_change_handled' => 'MAX(CASE WHEN object_type = \'host\' AND state = 2 AND acknowledged + in_downtime != 0 THEN state_change ELSE 0 END)',
+ 'hosts_unreachable_last_state_change_unhandled' => 'MAX(CASE WHEN object_type = \'host\' AND state = 2 AND acknowledged + in_downtime = 0 THEN state_change ELSE 0 END)',
+ 'hosts_up_last_state_change' => 'MAX(CASE WHEN object_type = \'host\' AND state = 0 THEN state_change ELSE 0 END)'
+ ),
+ 'servicestatussummary' => array(
+ 'servicegroup' => 'servicegroup COLLATE latin1_general_ci',
+ 'servicegroup_alias' => 'servicegroup_alias COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'servicegroup_name',
+ 'services_critical' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 THEN 1 ELSE 0 END)',
+ 'services_critical_handled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 AND acknowledged + in_downtime + host_state > 0 THEN 1 ELSE 0 END)',
+ 'services_critical_last_state_change_handled' => 'MAX(CASE WHEN object_type = \'service\' AND state = 2 AND acknowledged + in_downtime + host_state > 0 THEN state_change ELSE 0 END)',
+ 'services_critical_last_state_change_unhandled' => 'MAX(CASE WHEN object_type = \'service\' AND state = 2 AND acknowledged + in_downtime + host_state = 0 THEN state_change ELSE 0 END)',
+ 'services_critical_unhandled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 AND acknowledged + in_downtime + host_state = 0 THEN 1 ELSE 0 END)',
+ 'services_ok' => 'SUM(CASE WHEN object_type = \'service\' AND state = 0 THEN 1 ELSE 0 END)',
+ 'services_ok_last_state_change' => 'MAX(CASE WHEN object_type = \'service\' AND state = 0 THEN state_change ELSE 0 END)',
+ 'services_pending' => 'SUM(CASE WHEN object_type = \'service\' AND state = 99 THEN 1 ELSE 0 END)',
+ 'services_pending_last_state_change' => 'MAX(CASE WHEN object_type = \'service\' AND state = 99 THEN state_change ELSE 0 END)',
+ 'services_severity' => 'MAX(CASE WHEN object_type = \'service\' THEN severity ELSE 0 END)',
+ 'services_total' => 'SUM(CASE WHEN object_type = \'service\' THEN 1 ELSE 0 END)',
+ 'services_unknown' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 THEN 1 ELSE 0 END)',
+ 'services_unknown_handled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 AND acknowledged + in_downtime + host_state > 0 THEN 1 ELSE 0 END)',
+ 'services_unknown_last_state_change_handled' => 'MAX(CASE WHEN object_type = \'service\' AND state = 3 AND acknowledged + in_downtime + host_state > 0 THEN state_change ELSE 0 END)',
+ 'services_unknown_last_state_change_unhandled' => 'MAX(CASE WHEN object_type = \'service\' AND state = 3 AND acknowledged + in_downtime + host_state = 0 THEN state_change ELSE 0 END)',
+ 'services_unknown_unhandled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 AND acknowledged + in_downtime + host_state = 0 THEN 1 ELSE 0 END)',
+ 'services_warning' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 THEN 1 ELSE 0 END)',
+ 'services_warning_handled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 AND acknowledged + in_downtime + host_state > 0 THEN 1 ELSE 0 END)',
+ 'services_warning_last_state_change_handled' => 'MAX(CASE WHEN object_type = \'service\' AND state = 1 AND acknowledged + in_downtime + host_state > 0 THEN state_change ELSE 0 END)',
+ 'services_warning_last_state_change_unhandled' => 'MAX(CASE WHEN object_type = \'service\' AND state = 1 AND acknowledged + in_downtime + host_state = 0 THEN state_change ELSE 0 END)',
+ 'services_warning_unhandled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 AND acknowledged + in_downtime + host_state = 0 THEN 1 ELSE 0 END)'
+ )
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $useSubqueryCount = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $columns = array(
+ 'object_type',
+ 'host_state'
+ );
+
+ if (in_array('servicegroup', $this->desiredColumns) || in_array('servicegroup_name', $this->desiredColumns)) {
+ $columns[] = 'servicegroup';
+ $columns[] = 'servicegroup_name';
+ $columns[] = 'servicegroup_alias';
+ $groupColumns = array('servicegroup_name', 'servicegroup_alias');
+ } else {
+ $columns[] = 'hostgroup';
+ $columns[] = 'hostgroup_name';
+ $columns[] = 'hostgroup_alias';
+ $groupColumns = array('hostgroup_name', 'hostgroup_alias');
+ }
+ $hosts = $this->createSubQuery(
+ 'Hoststatus',
+ $columns + array(
+ 'state' => 'host_state',
+ 'acknowledged' => 'host_acknowledged',
+ 'in_downtime' => 'host_in_downtime',
+ 'state_change' => 'host_last_state_change',
+ 'severity' => 'host_severity'
+ )
+ );
+ if (in_array('servicegroup_name', $this->desiredColumns)) {
+ $hosts->group(array(
+ 'sgo.name1',
+ 'ho.object_id',
+ 'sg.alias',
+ 'state',
+ 'acknowledged',
+ 'in_downtime',
+ 'state_change',
+ 'severity'
+ ));
+ }
+ $services = $this->createSubQuery(
+ 'Status',
+ $columns + array(
+ 'state' => 'service_state',
+ 'acknowledged' => 'service_acknowledged',
+ 'in_downtime' => 'service_in_downtime',
+ 'state_change' => 'service_last_state_change',
+ 'severity' => 'service_severity'
+ )
+ );
+ $union = $this->db->select()->union(array($hosts, $services), Zend_Db_Select::SQL_UNION_ALL);
+ $this->select->from(array('statussummary' => $union), array())->group($groupColumns);
+ $this->joinedVirtualTables = array(
+ 'servicestatussummary' => true,
+ 'hoststatussummary' => true
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php
new file mode 100644
index 0000000..b388204
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php
@@ -0,0 +1,202 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for host comments
+ */
+class HostcommentQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('comments' => array('c.comment_id', 'ho.object_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hostgroups', 'services');
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'comments' => array(
+ 'comment_author' => 'c.author_name COLLATE latin1_general_ci',
+ 'comment_author_name' => 'c.author_name',
+ 'comment_data' => 'c.comment_data',
+ 'comment_expiration' => 'CASE c.expires WHEN 1 THEN UNIX_TIMESTAMP(c.expiration_time) ELSE NULL END',
+ 'comment_internal_id' => 'c.internal_comment_id',
+ 'comment_is_persistent' => 'c.is_persistent',
+ 'comment_name' => 'c.name',
+ 'comment_timestamp' => 'UNIX_TIMESTAMP(c.comment_time)',
+ 'comment_type' => "CASE c.entry_type WHEN 1 THEN 'comment' WHEN 2 THEN 'downtime' WHEN 3 THEN 'flapping' WHEN 4 THEN 'ack' END",
+ 'host' => 'ho.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'ho.name1',
+ 'object_type' => '(\'host\')'
+ ),
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ),
+ 'hoststatus' => array(
+ 'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci',
+ )
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ if (version_compare($this->getIdoVersion(), '1.14.0', '<')) {
+ $this->columnMap['comments']['comment_name'] = '(NULL)';
+ }
+ $this->select->from(
+ array('c' => $this->prefix . 'comments'),
+ array()
+ )->join(
+ array('ho' => $this->prefix . 'objects'),
+ 'ho.object_id = c.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
+ array()
+ );
+ $this->joinedVirtualTables['comments'] = true;
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = ho.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join host status
+ */
+ protected function joinHoststatus()
+ {
+ $this->select->join(
+ array('hs' => $this->prefix . 'hoststatus'),
+ 'hs.host_object_id = ho.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = c.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = s.service_object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sgm.servicegroup_id = sg.' . $this->servicegroup_id,
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->joinLeft(
+ array('s' => $this->prefix . 'services'),
+ 's.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 'ho.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('services');
+
+ return ['s.host_object_id', 'ho.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentdeletionhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentdeletionhistoryQuery.php
new file mode 100644
index 0000000..d798d56
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentdeletionhistoryQuery.php
@@ -0,0 +1,44 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for host comment removal records
+ */
+class HostcommentdeletionhistoryQuery extends HostcommenthistoryQuery
+{
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') {
+ $this->requireColumn('timestamp');
+ $filter->setColumn('hch.deletion_time');
+ $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression())));
+ return null;
+ }
+
+ return parent::requireFilterColumns($filter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ parent::joinBaseTables();
+ $this->select->where("hch.deletion_time > '1970-01-02 00:00:00'");
+ $this->columnMap['commenthistory']['timestamp'] = str_replace(
+ 'comment_time',
+ 'deletion_time',
+ $this->columnMap['commenthistory']['timestamp']
+ );
+ $this->columnMap['commenthistory']['type'] = str_replace(
+ 'END)',
+ "END || '_deleted')",
+ $this->columnMap['commenthistory']['type']
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php
new file mode 100644
index 0000000..b8f166a
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php
@@ -0,0 +1,197 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for host comment history records
+ */
+class HostcommenthistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('commenthistory' => array('hch.commenthistory_id', 'ho.object_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hostgroups', 'services');
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'commenthistory' => array(
+ 'id' => 'hch.commenthistory_id',
+ 'host' => 'ho.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'ho.name1',
+ 'object_id' => 'hch.object_id',
+ 'object_type' => '(\'host\')',
+ 'output' => "('[' || hch.author_name || '] ' || hch.comment_data)",
+ 'state' => '(-1)',
+ 'timestamp' => 'UNIX_TIMESTAMP(hch.comment_time)',
+ 'type' => "(CASE hch.entry_type WHEN 1 THEN 'comment' WHEN 2 THEN 'dt_comment' WHEN 3 THEN 'flapping' WHEN 4 THEN 'ack' END)"
+ ),
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci',
+ 'service_host_name' => 'so.name1'
+ )
+ );
+
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') {
+ $this->requireColumn('timestamp');
+ $filter->setColumn('hch.comment_time');
+ $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression())));
+ return null;
+ }
+
+ return parent::requireFilterColumns($filter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ array('hch' => $this->prefix . 'commenthistory'),
+ array()
+ )->join(
+ array('ho' => $this->prefix . 'objects'),
+ 'ho.object_id = hch.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
+ array()
+ );
+ $this->joinedVirtualTables['commenthistory'] = true;
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = ho.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = hch.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = s.service_object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id',
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->joinLeft(
+ array('s' => $this->prefix . 'services'),
+ 's.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 'ho.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('services');
+
+ return ['s.host_object_id', 'ho.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
new file mode 100644
index 0000000..23b0e90
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
@@ -0,0 +1,247 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for host contacts
+ */
+class HostcontactQuery extends IdoQuery
+{
+ protected $allowCustomVars = true;
+
+ protected $groupBase = [
+ 'contacts' => ['co.object_id', 'c.contact_id'],
+ 'timeperiods' => ['ht.timeperiod_id', 'st.timeperiod_id']
+ ];
+
+ protected $groupOrigin = ['contactgroups', 'hosts', 'services'];
+
+ protected $subQueryTargets = [
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ ];
+
+ protected $columnMap = [
+ 'contactgroups' => [
+ 'contactgroup' => 'cgo.name1 COLLATE latin1_general_ci',
+ 'contactgroup_name' => 'cgo.name1',
+ 'contactgroup_alias' => 'cg.alias COLLATE latin1_general_ci'
+ ],
+ 'contacts' => [
+ 'contact_id' => 'c.contact_id',
+ 'contact' => 'co.name1 COLLATE latin1_general_ci',
+ 'contact_name' => 'co.name1',
+ 'contact_alias' => 'c.alias COLLATE latin1_general_ci',
+ 'contact_email' => 'c.email_address COLLATE latin1_general_ci',
+ 'contact_pager' => 'c.pager_address',
+ 'contact_object_id' => 'c.contact_object_id',
+ 'contact_has_host_notfications' => 'c.host_notifications_enabled',
+ 'contact_has_service_notfications' => 'c.service_notifications_enabled',
+ 'contact_can_submit_commands' => 'c.can_submit_commands',
+ 'contact_notify_service_recovery' => 'c.notify_service_recovery',
+ 'contact_notify_service_warning' => 'c.notify_service_warning',
+ 'contact_notify_service_critical' => 'c.notify_service_critical',
+ 'contact_notify_service_unknown' => 'c.notify_service_unknown',
+ 'contact_notify_service_flapping' => 'c.notify_service_flapping',
+ 'contact_notify_service_downtime' => 'c.notify_service_downtime',
+ 'contact_notify_host_recovery' => 'c.notify_host_recovery',
+ 'contact_notify_host_down' => 'c.notify_host_down',
+ 'contact_notify_host_unreachable' => 'c.notify_host_unreachable',
+ 'contact_notify_host_flapping' => 'c.notify_host_flapping',
+ 'contact_notify_host_downtime' => 'c.notify_host_downtime'
+ ],
+ 'hostgroups' => [
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ],
+ 'hosts' => [
+ 'host' => 'ho.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'ho.name1',
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ],
+ 'instances' => [
+ 'instance_name' => 'i.instance_name'
+ ],
+ 'servicegroups' => [
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ],
+ 'services' => [
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci',
+ 'service_host_name' => 'so.name1'
+ ],
+ 'timeperiods' => [
+ 'contact_notify_host_timeperiod' => 'ht.alias COLLATE latin1_general_ci',
+ 'contact_notify_service_timeperiod' => 'st.alias COLLATE latin1_general_ci'
+ ]
+ ];
+
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ ['c' => $this->prefix . 'contacts'],
+ []
+ )->join(
+ ['co' => $this->prefix . 'objects'],
+ 'co.object_id = c.contact_object_id AND co.is_active = 1 AND co.objecttype_id = 10',
+ []
+ );
+
+ $this->joinedVirtualTables = array('contacts' => true);
+ }
+
+ /**
+ * Join contact groups
+ */
+ protected function joinContactgroups()
+ {
+ $this->select->joinLeft(
+ ['cgm' => $this->prefix . 'contactgroup_members'],
+ 'co.object_id = cgm.contact_object_id',
+ []
+ )->joinLeft(
+ ['cg' => $this->prefix . 'contactgroups'],
+ 'cgm.contactgroup_id = cg.contactgroup_id',
+ []
+ )->joinLeft(
+ ['cgo' => $this->prefix . 'objects'],
+ 'cg.contactgroup_object_id = cgo.object_id AND cgo.is_active = 1 AND cgo.objecttype_id = 11',
+ []
+ );
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->requireVirtualTable('hosts');
+
+ $this->select->joinLeft(
+ ['hgm' => $this->prefix . 'hostgroup_members'],
+ 'hgm.host_object_id = ho.object_id',
+ []
+ )->joinLeft(
+ ['hg' => $this->prefix . 'hostgroups'],
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ []
+ )->joinLeft(
+ ['hgo' => $this->prefix . 'objects'],
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ []
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->select->joinLeft(
+ ['hc' => $this->prefix . 'host_contacts'],
+ 'hc.contact_object_id = c.contact_object_id',
+ []
+ )->joinLeft(
+ ['h' => $this->prefix . 'hosts'],
+ 'h.host_id = hc.host_id',
+ []
+ )->joinLeft(
+ ['ho' => $this->prefix . 'objects'],
+ 'ho.object_id = h.host_object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
+ []
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ ['i' => $this->prefix . 'instances'],
+ 'i.instance_id = c.instance_id',
+ []
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ ['sgm' => $this->prefix . 'servicegroup_members'],
+ 'sgm.service_object_id = s.service_object_id',
+ []
+ )->joinLeft(
+ ['sg' => $this->prefix . 'servicegroups'],
+ 'sg.servicegroup_id = sgm.servicegroup_id',
+ []
+ )->joinLeft(
+ ['sgo' => $this->prefix . 'objects'],
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ []
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->requireVirtualTable('hosts');
+
+ $this->select->joinLeft(
+ ['s' => $this->prefix . 'services'],
+ 's.host_object_id = ho.object_id',
+ []
+ )->joinLeft(
+ ['so' => $this->prefix . 'objects'],
+ 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ []
+ );
+ }
+
+ /**
+ * Join time periods
+ */
+ protected function joinTimeperiods()
+ {
+ $this->select->joinLeft(
+ ['ht' => $this->prefix . 'timeperiods'],
+ 'ht.timeperiod_object_id = c.host_timeperiod_object_id',
+ []
+ );
+ $this->select->joinLeft(
+ ['st' => $this->prefix . 'timeperiods'],
+ 'st.timeperiod_object_id = c.service_timeperiod_object_id',
+ []
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $this->requireVirtualTable('hosts');
+
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 'ho.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $this->requireVirtualTable('services');
+
+ $query->joinVirtualTable('members');
+
+ return ['sgm.service_object_id', 'so.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php
new file mode 100644
index 0000000..62f5ceb
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php
@@ -0,0 +1,208 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for host downtimes
+ */
+class HostdowntimeQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('downtimes' => array('sd.scheduleddowntime_id', 'ho.object_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hostgroups', 'servicegroups', 'services');
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'downtimes' => array(
+ 'downtime_author' => 'sd.author_name COLLATE latin1_general_ci',
+ 'downtime_author_name' => 'sd.author_name',
+ 'downtime_comment' => 'sd.comment_data',
+ 'downtime_duration' => 'sd.duration',
+ 'downtime_end' => 'CASE WHEN sd.is_fixed > 0 THEN UNIX_TIMESTAMP(sd.scheduled_end_time) ELSE UNIX_TIMESTAMP(sd.trigger_time) + sd.duration END',
+ 'downtime_entry_time' => 'UNIX_TIMESTAMP(sd.entry_time)',
+ 'downtime_internal_id' => 'sd.internal_downtime_id',
+ 'downtime_is_fixed' => 'sd.is_fixed',
+ 'downtime_is_flexible' => 'CASE WHEN sd.is_fixed = 0 THEN 1 ELSE 0 END',
+ 'downtime_is_in_effect' => 'sd.is_in_effect',
+ 'downtime_name' => 'sd.name',
+ 'downtime_scheduled_end' => 'UNIX_TIMESTAMP(sd.scheduled_end_time)',
+ 'downtime_scheduled_start' => 'UNIX_TIMESTAMP(sd.scheduled_start_time)',
+ 'downtime_start' => 'UNIX_TIMESTAMP(CASE WHEN UNIX_TIMESTAMP(sd.trigger_time) > 0 then sd.trigger_time ELSE sd.scheduled_start_time END)',
+ 'downtime_triggered_by_id' => 'sd.triggered_by_id',
+ 'host' => 'ho.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'ho.name1',
+ 'object_type' => '(\'host\')'
+ ),
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ),
+ 'hoststatus' => array(
+ 'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci',
+ )
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ if (version_compare($this->getIdoVersion(), '1.14.0', '<')) {
+ $this->columnMap['downtimes']['downtime_name'] = '(NULL)';
+ }
+ $this->select->from(
+ array('sd' => $this->prefix . 'scheduleddowntime'),
+ array()
+ )->join(
+ array('ho' => $this->prefix . 'objects'),
+ 'sd.object_id = ho.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
+ array()
+ );
+ $this->joinedVirtualTables['downtimes'] = true;
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = ho.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join host status
+ */
+ protected function joinHoststatus()
+ {
+ $this->select->join(
+ array('hs' => $this->prefix . 'hoststatus'),
+ 'hs.host_object_id = ho.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = sd.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = s.service_object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sgm.servicegroup_id = sg.' . $this->servicegroup_id,
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->joinLeft(
+ array('s' => $this->prefix . 'services'),
+ 's.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 'ho.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('services');
+
+ return ['s.host_object_id', 'ho.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeendhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeendhistoryQuery.php
new file mode 100644
index 0000000..808b3f2
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeendhistoryQuery.php
@@ -0,0 +1,42 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for host downtime end history records
+ */
+class HostdowntimeendhistoryQuery extends HostdowntimestarthistoryQuery
+{
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') {
+ $this->requireColumn('timestamp');
+ $filter->setColumn('hdh.actual_end_time');
+ $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression())));
+ return null;
+ }
+
+ return parent::requireFilterColumns($filter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ parent::joinBaseTables(true);
+ $this->select->where(
+ "hdh.actual_end_time > '1970-01-02 00:00:00' AND hdh.was_started = 1 AND hdh.was_cancelled = 0"
+ );
+ $this->columnMap['downtimehistory']['type'] = "('dt_end')";
+ $this->columnMap['downtimehistory']['timestamp'] = str_replace(
+ 'actual_start_time',
+ 'actual_end_time',
+ $this->columnMap['downtimehistory']['timestamp']
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
new file mode 100644
index 0000000..a6f97e7
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
@@ -0,0 +1,206 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for host downtime start history records
+ */
+class HostdowntimestarthistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('downtimehistory' => array('hdh.downtimehistory_id', 'ho.object_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hostgroups', 'services');
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'downtimehistory' => array(
+ 'id' => 'hdh.downtimehistory_id',
+ 'host' => 'ho.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'ho.name1',
+ 'object_id' => 'hdh.object_id',
+ 'object_type' => '(\'host\')',
+ 'output' => "('[' || hdh.author_name || '] ' || hdh.comment_data)",
+ 'state' => '(-1)',
+ 'timestamp' => 'UNIX_TIMESTAMP(hdh.actual_start_time)',
+ 'type' => "('dt_start')"
+ ),
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci',
+ 'service_host_name' => 'so.name1'
+ )
+ );
+
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') {
+ $this->requireColumn('timestamp');
+ $filter->setColumn('hdh.actual_start_time');
+ $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression())));
+ return null;
+ }
+
+ return parent::requireFilterColumns($filter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ array('hdh' => $this->prefix . 'downtimehistory'),
+ array()
+ )->join(
+ array('ho' => $this->prefix . 'objects'),
+ 'ho.object_id = hdh.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
+ array()
+ );
+
+ if (func_num_args() === 0 || func_get_arg(0) === false) {
+ $this->select->where(
+ "hdh.actual_start_time > '1970-01-02 00:00:00'"
+ );
+ }
+ $this->select->where(
+ "hdh.was_started = 1 AND hdh.was_cancelled = 0"
+ );
+ $this->joinedVirtualTables['downtimehistory'] = true;
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = ho.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = hdh.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = s.service_object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id',
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->joinLeft(
+ array('s' => $this->prefix . 'services'),
+ 's.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 'ho.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('services');
+
+ return ['s.host_object_id', 'ho.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingendhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingendhistoryQuery.php
new file mode 100644
index 0000000..ebc346b
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingendhistoryQuery.php
@@ -0,0 +1,31 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for host flapping end history records
+ */
+class HostflappingendhistoryQuery extends HostflappingstarthistoryQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ array('hfh' => $this->prefix . 'flappinghistory'),
+ array()
+ )->join(
+ array('ho' => $this->prefix . 'objects'),
+ 'ho.object_id = hfh.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
+ array()
+ );
+
+ $this->select->where('hfh.event_type = 1001');
+
+ $this->joinedVirtualTables['flappinghistory'] = true;
+
+ $this->columnMap['flappinghistory']['type'] = '(\'flapping_deleted\')';
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php
new file mode 100644
index 0000000..497a493
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php
@@ -0,0 +1,200 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for host flapping start history records
+ */
+class HostflappingstarthistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('flappinghistory' => array('hfh.flappinghistory_id', 'ho.object_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hostgroups', 'services');
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'flappinghistory' => array(
+ 'id' => 'hfh.flappinghistory_id',
+ 'host' => 'ho.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'ho.name1',
+ 'object_id' => 'hfh.object_id',
+ 'object_type' => '(\'host\')',
+ 'output' => '(hfh.percent_state_change || \'\')',
+ 'state' => '(-1)',
+ 'timestamp' => 'UNIX_TIMESTAMP(hfh.event_time)',
+ 'type' => '(\'flapping\')'
+ ),
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci',
+ 'service_host_name' => 'so.name1'
+ )
+ );
+
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') {
+ $this->requireColumn('timestamp');
+ $filter->setColumn('hfh.event_time');
+ $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression())));
+ return null;
+ }
+
+ return parent::requireFilterColumns($filter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ array('hfh' => $this->prefix . 'flappinghistory'),
+ array()
+ )->join(
+ array('ho' => $this->prefix . 'objects'),
+ 'ho.object_id = hfh.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
+ array()
+ );
+
+ $this->select->where('hfh.event_type = 1000');
+
+ $this->joinedVirtualTables['flappinghistory'] = true;
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = ho.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = hfh.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = s.service_object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id',
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->joinLeft(
+ array('s' => $this->prefix . 'services'),
+ 's.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 'ho.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('services');
+
+ return ['s.host_object_id', 'ho.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
new file mode 100644
index 0000000..463fba9
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
@@ -0,0 +1,295 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for host groups
+ */
+class HostgroupQuery extends IdoQuery
+{
+ protected $allowCustomVars = true;
+
+ protected $groupBase = array(
+ 'hostgroups' => array('hgo.object_id', 'hg.hostgroup_id'),
+ 'hoststatus' => array('hs.hoststatus_id'),
+ 'servicestatus' => array('ss.servicestatus_id')
+ );
+
+ protected $groupOrigin = array('members');
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ protected $columnMap = array(
+ 'contacts' => [
+ 'host_contact' => 'hco.name1'
+ ],
+ 'contactgroups' => [
+ 'host_contactgroup' => 'hcgo.name1'
+ ],
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hoststatus' => array(
+ 'host_handled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) > 0 THEN 1 ELSE 0 END',
+ 'host_severity' => '
+ CASE
+ WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL
+ THEN 16
+ ELSE
+ CASE
+ WHEN hs.current_state = 0
+ THEN 1
+ ELSE
+ CASE
+ WHEN hs.current_state = 1 THEN 64
+ WHEN hs.current_state = 2 THEN 32
+ ELSE 256
+ END
+ +
+ CASE
+ WHEN hs.problem_has_been_acknowledged = 1 THEN 2
+ WHEN hs.scheduled_downtime_depth > 0 THEN 1
+ ELSE 256
+ END
+ END
+ END',
+ 'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'members' => array(
+ 'host_name' => 'ho.name1'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup_name' => 'sgo.name1'
+ ),
+ 'services' => array(
+ 'service_description' => 'so.name2'
+ ),
+ 'servicestatus' => array(
+ 'service_handled' => 'CASE WHEN (ss.problem_has_been_acknowledged + ss.scheduled_downtime_depth + COALESCE(hs.current_state, 0)) > 0 THEN 1 ELSE 0 END',
+ 'service_severity' => '
+ CASE WHEN ss.current_state = 0
+ THEN
+ CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL
+ THEN 16
+ ELSE 0
+ END
+ +
+ CASE WHEN ss.problem_has_been_acknowledged = 1
+ THEN 2
+ ELSE
+ CASE WHEN ss.scheduled_downtime_depth > 0
+ THEN 1
+ ELSE 4
+ END
+ END
+ ELSE
+ CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 16
+ WHEN ss.current_state = 1 THEN 32
+ WHEN ss.current_state = 2 THEN 128
+ WHEN ss.current_state = 3 THEN 64
+ ELSE 256
+ END
+ +
+ CASE WHEN hs.current_state > 0
+ THEN 1024
+ ELSE
+ CASE WHEN ss.problem_has_been_acknowledged = 1
+ THEN 512
+ ELSE
+ CASE WHEN ss.scheduled_downtime_depth > 0
+ THEN 256
+ ELSE 2048
+ END
+ END
+ END
+ END',
+ 'service_state' => 'CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 99 ELSE ss.current_state END'
+ )
+ );
+
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ array('hgo' => $this->prefix . 'objects'),
+ array()
+ )->join(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_object_id = hgo.object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ $this->joinedVirtualTables['hostgroups'] = true;
+ }
+
+ /**
+ * Join contacts
+ */
+ protected function joinContacts()
+ {
+ $this->requireVirtualTable('hosts');
+
+ $this->select->joinLeft(
+ ['hc' => 'icinga_host_contacts'],
+ 'hc.host_id = h.host_id',
+ []
+ )->joinLeft(
+ ['hco' => 'icinga_objects'],
+ 'hco.object_id = hc.contact_object_id AND hco.is_active = 1 AND hco.objecttype_id = 10',
+ []
+ );
+ }
+
+ /**
+ * Join contact groups
+ */
+ protected function joinContactgroups()
+ {
+ $this->requireVirtualTable('hosts');
+
+ $this->select->joinLeft(
+ ['hcg' => 'icinga_host_contactgroups'],
+ 'hcg.host_id = h.host_id',
+ []
+ )->joinLeft(
+ ['hcgo' => 'icinga_objects'],
+ 'hcgo.object_id = hcg.contactgroup_object_id AND hcgo.is_active = 1 AND hcgo.objecttype_id = 11',
+ []
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->requireVirtualTable('members');
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = ho.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join host status
+ */
+ protected function joinHoststatus()
+ {
+ $this->requireVirtualTable('members');
+ $this->select->join(
+ array('hs' => $this->prefix . 'hoststatus'),
+ 'hs.host_object_id = ho.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = hg.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join members
+ */
+ protected function joinMembers()
+ {
+ $this->select->join(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.hostgroup_id = hg.hostgroup_id',
+ array()
+ )->join(
+ array('ho' => $this->prefix . 'objects'),
+ 'hgm.host_object_id = ho.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = s.service_object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sgm.servicegroup_id = sg.servicegroup_id',
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->requireVirtualTable('hosts');
+ $this->select->joinLeft(
+ array('s' => $this->prefix . 'services'),
+ 's.host_object_id = h.host_object_id',
+ array()
+ )->joinLeft(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ }
+
+ /**
+ * Join service status
+ */
+ protected function joinServicestatus()
+ {
+ $this->requireVirtualTable('services');
+ $this->requireVirtualTable('hoststatus');
+ $this->select->join(
+ array('ss' => $this->prefix . 'servicestatus'),
+ 'ss.service_object_id = so.object_id',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ // Propagate that the "parent" query has to be filtered as well
+ $additionalFilter = clone $filter;
+
+ $this->requireVirtualTable('members');
+
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 'ho.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $this->requireVirtualTable('members');
+
+ $query->joinVirtualTable('services');
+
+ return ['s.host_object_id', 'ho.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupsummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupsummaryQuery.php
new file mode 100644
index 0000000..a1b7182
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupsummaryQuery.php
@@ -0,0 +1,142 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Zend_Db_Expr;
+use Zend_Db_Select;
+
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for host group summary
+ */
+class HostgroupsummaryQuery extends IdoQuery
+{
+ protected $allowCustomVars = true;
+
+ protected $columnMap = array(
+ 'hostgroupsummary' => array(
+ 'hostgroup_alias' => 'hostgroup_alias',
+ 'hostgroup_name' => 'hostgroup_name',
+ 'hosts_down' => 'SUM(CASE WHEN host_state = 1 THEN 1 ELSE 0 END)',
+ 'hosts_down_handled' => 'SUM(CASE WHEN host_state = 1 AND host_handled = 1 THEN 1 ELSE 0 END)',
+ 'hosts_down_unhandled' => 'SUM(CASE WHEN host_state = 1 AND host_handled = 0 THEN 1 ELSE 0 END)',
+ 'hosts_pending' => 'SUM(CASE WHEN host_state = 99 THEN 1 ELSE 0 END)',
+ 'hosts_severity' => 'MAX(host_severity)',
+ 'hosts_total' => 'SUM(CASE WHEN host_state IS NOT NULL THEN 1 ELSE 0 END)',
+ 'hosts_unreachable' => 'SUM(CASE WHEN host_state = 2 THEN 1 ELSE 0 END)',
+ 'hosts_unreachable_handled' => 'SUM(CASE WHEN host_state = 2 AND host_handled = 1 THEN 1 ELSE 0 END)',
+ 'hosts_unreachable_unhandled' => 'SUM(CASE WHEN host_state = 2 AND host_handled = 0 THEN 1 ELSE 0 END)',
+ 'hosts_up' => 'SUM(CASE WHEN host_state = 0 THEN 1 ELSE 0 END)',
+ 'services_critical' => 'SUM(CASE WHEN service_state = 2 THEN 1 ELSE 0 END)',
+ 'services_critical_handled' => 'SUM(CASE WHEN service_state = 2 AND service_handled = 1 THEN 1 ELSE 0 END)',
+ 'services_critical_unhandled' => 'SUM(CASE WHEN service_state = 2 AND service_handled = 0 THEN 1 ELSE 0 END)',
+ 'services_ok' => 'SUM(CASE WHEN service_state = 0 THEN 1 ELSE 0 END)',
+ 'services_pending' => 'SUM(CASE WHEN service_state = 99 THEN 1 ELSE 0 END)',
+ 'services_total' => 'SUM(CASE WHEN service_state IS NOT NULL THEN 1 ELSE 0 END)',
+ 'services_unknown' => 'SUM(CASE WHEN service_state = 3 THEN 1 ELSE 0 END)',
+ 'services_unknown_handled' => 'SUM(CASE WHEN service_state = 3 AND service_handled = 1 THEN 1 ELSE 0 END)',
+ 'services_unknown_unhandled' => 'SUM(CASE WHEN service_state = 3 AND service_handled = 0 THEN 1 ELSE 0 END)',
+ 'services_warning' => 'SUM(CASE WHEN service_state = 1 THEN 1 ELSE 0 END)',
+ 'services_warning_handled' => 'SUM(CASE WHEN service_state = 1 AND service_handled = 1 THEN 1 ELSE 0 END)',
+ 'services_warning_unhandled' => 'SUM(CASE WHEN service_state = 1 AND service_handled = 0 THEN 1 ELSE 0 END)',
+ )
+ );
+
+ /**
+ * The union
+ *
+ * @var Zend_Db_Select
+ */
+ protected $summaryQuery;
+
+ /**
+ * Subqueries used for the summary query
+ *
+ * @var IdoQuery[]
+ */
+ protected $subQueries = array();
+
+ /**
+ * Count query
+ *
+ * @var IdoQuery
+ */
+ protected $countQuery;
+
+ public function addFilter(Filter $filter)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+ $this->countQuery->applyFilter(clone $filter);
+ return $this;
+ }
+
+ protected function joinBaseTables()
+ {
+ $this->countQuery = $this->createSubQuery(
+ 'Hostgroup',
+ array()
+ );
+ $hosts = $this->createSubQuery(
+ 'Hostgroup',
+ array(
+ 'hostgroup_alias',
+ 'hostgroup_name',
+ 'host_handled',
+ 'host_severity',
+ 'host_state',
+ 'service_handled' => new Zend_Db_Expr('NULL'),
+ 'service_severity' => new Zend_Db_Expr('0'),
+ 'service_state' => new Zend_Db_Expr('NULL'),
+ )
+ );
+ $this->subQueries[] = $hosts;
+ $services = $this->createSubQuery(
+ 'Hostgroup',
+ array(
+ 'hostgroup_alias',
+ 'hostgroup_name',
+ 'host_handled' => new Zend_Db_Expr('NULL'),
+ 'host_severity' => new Zend_Db_Expr('0'),
+ 'host_state' => new Zend_Db_Expr('NULL'),
+ 'service_handled',
+ 'service_severity',
+ 'service_state'
+ )
+ );
+ $this->subQueries[] = $services;
+ $emptyGroups = $this->createSubQuery(
+ 'Emptyhostgroup',
+ [
+ 'hostgroup_alias',
+ 'hostgroup_name',
+ 'host_handled' => new Zend_Db_Expr('NULL'),
+ 'host_severity' => new Zend_Db_Expr('0'),
+ 'host_state' => new Zend_Db_Expr('NULL'),
+ 'service_handled' => new Zend_Db_Expr('NULL'),
+ 'service_severity' => new Zend_Db_Expr('0'),
+ 'service_state' => new Zend_Db_Expr('NULL'),
+ ]
+ );
+ $this->subQueries[] = $emptyGroups;
+ $this->summaryQuery = $this->db->select()->union(
+ [$hosts, $services, $emptyGroups],
+ Zend_Db_Select::SQL_UNION_ALL
+ );
+ $this->select->from(array('hostgroupsummary' => $this->summaryQuery), array());
+ $this->group(array('hostgroup_name', 'hostgroup_alias'));
+ $this->joinedVirtualTables['hostgroupsummary'] = true;
+ }
+
+ public function getCountQuery()
+ {
+ $count = $this->countQuery->select();
+ $this->countQuery->applyFilterSql($count);
+ $count->columns(array('hgo.object_id'));
+ $count->group(array('hgo.object_id'));
+ return $this->db->select()->from($count, array('cnt' => 'COUNT(*)'));
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
new file mode 100644
index 0000000..f252a7e
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
@@ -0,0 +1,284 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for host notifications
+ */
+class HostnotificationQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'contactnotifications' => array(
+ 'notification_contact_name' => 'co.name1'
+ ),
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci',
+ 'host_alias' => 'h.alias COLLATE latin1_general_ci',
+ ),
+ 'history' => array(
+ 'output' => null,
+ 'state' => 'hn.state',
+ 'timestamp' => 'UNIX_TIMESTAMP(hn.start_time)',
+ 'type' => '
+ CASE hn.notification_reason
+ WHEN 1 THEN \'notification_ack\'
+ WHEN 2 THEN \'notification_flapping\'
+ WHEN 3 THEN \'notification_flapping_end\'
+ WHEN 5 THEN \'notification_dt_start\'
+ WHEN 6 THEN \'notification_dt_end\'
+ WHEN 7 THEN \'notification_dt_end\'
+ WHEN 8 THEN \'notification_custom\'
+ ELSE \'notification_state\'
+ END',
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'notifications' => array(
+ 'id' => 'hn.notification_id',
+ 'host' => 'ho.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'ho.name1',
+ 'notification_output' => 'hn.output',
+ 'notification_reason' => 'hn.notification_reason',
+ 'notification_state' => 'hn.state',
+ 'notification_timestamp' => 'UNIX_TIMESTAMP(hn.start_time)',
+ 'object_type' => '(\'host\')'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci',
+ 'service_host_name' => 'so.name1'
+ )
+ );
+
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression) {
+ switch ($filter->getColumn()) {
+ case 'output':
+ $this->requireColumn('output');
+ $filter->setColumn('hn.output');
+ return null;
+ case 'timestamp':
+ case 'notification_timestamp':
+ $this->requireColumn($filter->getColumn());
+ $filter->setColumn('hn.start_time');
+ $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression())));
+ return null;
+ }
+ }
+
+ return parent::requireFilterColumns($filter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $concattedContacts = null;
+ switch ($this->ds->getDbType()) {
+ case 'mysql':
+ $concattedContacts = "GROUP_CONCAT("
+ . "DISTINCT co.name1 ORDER BY co.name1 SEPARATOR ', '"
+ . ") COLLATE latin1_general_ci";
+ break;
+ case 'pgsql':
+ // TODO: Find a way to order the contact alias list:
+ $concattedContacts = "ARRAY_TO_STRING(ARRAY_AGG(DISTINCT co.name1), ', ')";
+ break;
+ }
+ $this->columnMap['history']['output'] = "('[' || $concattedContacts || '] ' || hn.output)";
+
+ $this->select->from(
+ array('hn' => $this->prefix . 'notifications'),
+ array()
+ )->join(
+ array('ho' => $this->prefix . 'objects'),
+ 'ho.object_id = hn.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
+ array()
+ );
+ $this->joinedVirtualTables['notifications'] = true;
+ }
+
+ /**
+ * Join virtual table history
+ */
+ protected function joinHistory()
+ {
+ $this->requireVirtualTable('contactnotifications');
+ }
+
+ /**
+ * Join contact notifications
+ */
+ protected function joinContactnotifications()
+ {
+ $this->select->joinLeft(
+ array('cn' => $this->prefix . 'contactnotifications'),
+ 'cn.notification_id = hn.notification_id',
+ array()
+ );
+ $this->select->joinLeft(
+ array('co' => $this->prefix . 'objects'),
+ 'co.object_id = cn.contact_object_id AND co.is_active = 1 AND co.objecttype_id = 10',
+ array()
+ );
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = ho.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = s.service_object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id',
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->joinLeft(
+ array('s' => $this->prefix . 'services'),
+ 's.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = hn.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getGroup()
+ {
+ $group = array();
+
+ if ($this->hasJoinedVirtualTable('history')
+ || $this->hasJoinedVirtualTable('services')
+ || $this->hasJoinedVirtualTable('hostgroups')
+ ) {
+ $group = array('hn.notification_id', 'ho.object_id');
+ if ($this->hasJoinedVirtualTable('contactnotifications') && !$this->hasJoinedVirtualTable('history')) {
+ $group[] = 'co.object_id';
+ }
+ } elseif ($this->hasJoinedVirtualTable('contactnotifications')) {
+ $group = array('hn.notification_id', 'co.object_id', 'ho.object_id');
+ }
+
+ if (! empty($group)) {
+ if ($this->hasJoinedVirtualTable('hosts')) {
+ $group[] = 'h.host_id';
+ }
+
+ if ($this->hasJoinedVirtualTable('instances')) {
+ $group[] = 'i.instance_id';
+ }
+ }
+
+ return $group;
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 'ho.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('services');
+
+ return ['s.host_object_id', 'ho.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
new file mode 100644
index 0000000..ac85c1f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
@@ -0,0 +1,222 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for host state history records
+ */
+class HoststatehistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('statehistory' => array('hh.statehistory_id', 'ho.object_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hostgroups', 'services');
+
+ /**
+ * Array to map type names to type ids for query optimization
+ *
+ * @var array
+ */
+ protected $types = array(
+ 'soft_state' => 0,
+ 'hard_state' => 1
+ );
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci',
+ 'service_host_name' => 'so.name1'
+ ),
+ 'statehistory' => array(
+ 'id' => 'hh.statehistory_id',
+ 'host' => 'ho.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'ho.name1',
+ 'object_id' => 'hh.object_id',
+ 'object_type' => '(\'host\')',
+ 'output' => '(CASE WHEN hh.state_type = 1 THEN hh.output ELSE \'[ \' || hh.current_check_attempt || \'/\' || hh.max_check_attempts || \' ] \' || hh.output END)',
+ 'state' => 'hh.state',
+ 'timestamp' => 'UNIX_TIMESTAMP(hh.state_time)',
+ 'type' => "(CASE WHEN hh.state_type = 1 THEN 'hard_state' ELSE 'soft_state' END)"
+ ),
+ );
+
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression) {
+ switch ($filter->getColumn()) {
+ case 'timestamp':
+ $this->requireColumn('timestamp');
+ $filter->setColumn('hh.state_time');
+ $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression())));
+ return null;
+ case 'type':
+ if (! is_array($filter->getExpression())) {
+ $this->requireColumn('type');
+ $filter->setColumn('hh.state_type');
+ if (isset($this->types[$filter->getExpression()])) {
+ $filter->setExpression($this->types[$filter->getExpression()]);
+ } else {
+ $filter->setExpression(-1);
+ }
+
+ return null;
+ }
+ }
+ }
+
+ return parent::requireFilterColumns($filter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ array('hh' => $this->prefix . 'statehistory'),
+ array()
+ )->join(
+ array('ho' => $this->prefix . 'objects'),
+ 'ho.object_id = hh.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
+ array()
+ );
+ $this->joinedVirtualTables['statehistory'] = true;
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = ho.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = hh.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = s.service_object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id',
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->joinLeft(
+ array('s' => $this->prefix . 'services'),
+ 's.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 'ho.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('services');
+
+ return ['s.host_object_id', 'ho.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
new file mode 100644
index 0000000..e1b5480
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
@@ -0,0 +1,338 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+class HoststatusQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('hosts' => array('ho.object_id', 'h.host_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hostgroups', 'servicegroups', 'services');
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'checktimeperiods' => array(
+ 'host_check_timeperiod' => 'ctp.alias COLLATE latin1_general_ci'
+ ),
+ 'contacts' => [
+ 'host_contact' => 'hco.name1'
+ ],
+ 'contactgroups' => [
+ 'host_contactgroup' => 'hcgo.name1'
+ ],
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host' => 'ho.name1 COLLATE latin1_general_ci',
+ 'host_action_url' => 'h.action_url',
+ 'host_address' => 'h.address',
+ 'host_address6' => 'h.address6',
+ 'host_alias' => 'h.alias',
+ 'host_check_interval' => '(h.check_interval * 60)',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci',
+ 'host_icon_image' => 'h.icon_image',
+ 'host_icon_image_alt' => 'h.icon_image_alt',
+ 'host_ipv4' => 'INET_ATON(h.address)',
+ 'host_name' => 'ho.name1',
+ 'host_notes' => 'h.notes',
+ 'host_notes_url' => 'h.notes_url',
+ 'object_type' => '(\'host\')',
+ 'object_id' => 'ho.object_id'
+ ),
+ 'hoststatus' => array(
+ 'host_acknowledged' => 'hs.problem_has_been_acknowledged',
+ 'host_acknowledgement_type' => 'hs.acknowledgement_type',
+ 'host_active_checks_enabled' => 'hs.active_checks_enabled',
+ 'host_active_checks_enabled_changed' => 'CASE WHEN hs.active_checks_enabled = h.active_checks_enabled THEN 0 ELSE 1 END',
+ 'host_attempt' => 'hs.current_check_attempt || \'/\' || hs.max_check_attempts',
+ 'host_check_command' => 'hs.check_command',
+ 'host_check_execution_time' => 'hs.execution_time',
+ 'host_check_latency' => 'hs.latency',
+ 'host_check_source' => 'hs.check_source',
+ 'host_check_type' => 'hs.check_type',
+ 'host_current_check_attempt' => 'hs.current_check_attempt',
+ 'host_current_notification_number' => 'hs.current_notification_number',
+ 'host_event_handler' => 'hs.event_handler',
+ 'host_event_handler_enabled' => 'hs.event_handler_enabled',
+ 'host_event_handler_enabled_changed' => 'CASE WHEN hs.event_handler_enabled = h.event_handler_enabled THEN 0 ELSE 1 END',
+ 'host_failure_prediction_enabled' => 'hs.failure_prediction_enabled',
+ 'host_flap_detection_enabled' => 'hs.flap_detection_enabled',
+ 'host_flap_detection_enabled_changed' => 'CASE WHEN hs.flap_detection_enabled = h.flap_detection_enabled THEN 0 ELSE 1 END',
+ 'host_handled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) > 0 THEN 1 ELSE 0 END',
+ 'host_hard_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE CASE WHEN hs.state_type = 1 THEN hs.current_state ELSE hs.last_hard_state END END',
+ 'host_in_downtime' => 'CASE WHEN (hs.scheduled_downtime_depth = 0) THEN 0 ELSE 1 END',
+ 'host_is_flapping' => 'hs.is_flapping',
+ 'host_is_passive_checked' => 'CASE WHEN hs.active_checks_enabled = 0 AND hs.passive_checks_enabled = 1 THEN 1 ELSE 0 END',
+ 'host_is_reachable' => 'hs.is_reachable',
+ 'host_last_check' => 'UNIX_TIMESTAMP(hs.last_check)',
+ 'host_last_hard_state' => 'hs.last_hard_state',
+ 'host_last_hard_state_change' => 'UNIX_TIMESTAMP(hs.last_hard_state_change)',
+ 'host_last_notification' => 'UNIX_TIMESTAMP(hs.last_notification)',
+ 'host_last_state_change' => 'UNIX_TIMESTAMP(hs.last_state_change)',
+ 'host_last_state_change_ts' => 'hs.last_state_change',
+ 'host_last_time_down' => 'UNIX_TIMESTAMP(hs.last_time_down)',
+ 'host_last_time_unreachable' => 'UNIX_TIMESTAMP(hs.last_time_unreachable)',
+ 'host_last_time_up' => 'UNIX_TIMESTAMP(hs.last_time_up)',
+ 'host_long_output' => 'hs.long_output',
+ 'host_max_check_attempts' => 'hs.max_check_attempts',
+ 'host_modified_host_attributes' => 'hs.modified_host_attributes',
+ 'host_next_check' => 'CASE hs.should_be_scheduled WHEN 1 THEN UNIX_TIMESTAMP(hs.next_check) ELSE NULL END',
+ 'host_next_notification' => 'UNIX_TIMESTAMP(hs.next_notification)',
+ 'host_next_update' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL
+ THEN
+ CASE hs.should_be_scheduled WHEN 1 THEN UNIX_TIMESTAMP(hs.next_check) + (hs.normal_check_interval * 60) ELSE NULL END
+ ELSE
+ UNIX_TIMESTAMP(hs.next_check)
+ + (CASE WHEN
+ COALESCE(hs.current_state, 0) > 0 AND hs.state_type = 0
+ THEN
+ hs.retry_check_interval
+ ELSE
+ hs.normal_check_interval
+ END * 60)
+ + (CEIL(hs.execution_time + hs.latency) * 2)
+ END',
+ 'host_no_more_notifications' => 'hs.no_more_notifications',
+ 'host_normal_check_interval' => 'hs.normal_check_interval',
+ 'host_notifications_enabled' => 'hs.notifications_enabled',
+ 'host_notifications_enabled_changed' => 'CASE WHEN hs.notifications_enabled = h.notifications_enabled THEN 0 ELSE 1 END',
+ 'host_obsessing' => 'hs.obsess_over_host',
+ 'host_obsessing_changed' => 'CASE WHEN hs.obsess_over_host = h.obsess_over_host THEN 0 ELSE 1 END',
+ 'host_output' => 'hs.output',
+ 'host_passive_checks_enabled' => 'hs.passive_checks_enabled',
+ 'host_passive_checks_enabled_changed' => 'CASE WHEN hs.passive_checks_enabled = h.passive_checks_enabled THEN 0 ELSE 1 END',
+ 'host_percent_state_change' => 'hs.percent_state_change',
+ 'host_perfdata' => 'hs.perfdata',
+ 'host_problem' => 'CASE WHEN COALESCE(hs.current_state, 0) = 0 THEN 0 ELSE 1 END',
+ 'host_problem_has_been_acknowledged' => 'hs.problem_has_been_acknowledged',
+ 'host_process_performance_data' => 'hs.process_performance_data',
+ 'host_retry_check_interval' => 'hs.retry_check_interval',
+ 'host_scheduled_downtime_depth' => 'hs.scheduled_downtime_depth',
+ 'host_severity' => '
+ CASE
+ WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL
+ THEN 16
+ ELSE
+ CASE
+ WHEN hs.current_state = 0
+ THEN 1
+ ELSE
+ CASE
+ WHEN hs.current_state = 1 THEN 64
+ WHEN hs.current_state = 2 THEN 32
+ ELSE 256
+ END
+ +
+ CASE
+ WHEN hs.problem_has_been_acknowledged = 1 THEN 2
+ WHEN hs.scheduled_downtime_depth > 0 THEN 1
+ ELSE 256
+ END
+ END
+ END',
+ 'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END',
+ 'host_state_type' => 'hs.state_type',
+ 'host_status_update_time' => 'hs.status_update_time',
+ 'host_unhandled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) = 0 THEN 1 ELSE 0 END',
+ 'problems' => 'CASE WHEN COALESCE(hs.current_state, 0) = 0 THEN 0 ELSE 1 END'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci',
+ )
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ if (version_compare($this->getIdoVersion(), '1.10.0', '<')) {
+ $this->columnMap['hoststatus']['host_check_source'] = '(NULL)';
+ }
+ if (version_compare($this->getIdoVersion(), '1.13.0', '<')) {
+ $this->columnMap['hoststatus']['host_is_reachable'] = '(NULL)';
+ }
+
+ $this->select->from(
+ array('ho' => $this->prefix . 'objects'),
+ array()
+ )->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = ho.object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
+ array()
+ );
+ $this->joinedVirtualTables['hosts'] = true;
+ }
+
+ /**
+ * Join check time periods
+ */
+ protected function joinChecktimeperiods()
+ {
+ $this->select->joinLeft(
+ array('ctp' => $this->prefix . 'timeperiods'),
+ 'ctp.timeperiod_object_id = h.check_timeperiod_object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join contacts
+ */
+ protected function joinContacts()
+ {
+ $this->select->joinLeft(
+ ['hc' => 'icinga_host_contacts'],
+ 'hc.host_id = h.host_id',
+ []
+ )->joinLeft(
+ ['hco' => 'icinga_objects'],
+ 'hco.object_id = hc.contact_object_id AND hco.is_active = 1 AND hco.objecttype_id = 10',
+ []
+ );
+ }
+
+ /**
+ * Join contact groups
+ */
+ protected function joinContactgroups()
+ {
+ $this->select->joinLeft(
+ ['hcg' => 'icinga_host_contactgroups'],
+ 'hcg.host_id = h.host_id',
+ []
+ )->joinLeft(
+ ['hcgo' => 'icinga_objects'],
+ 'hcgo.object_id = hcg.contactgroup_object_id AND hcgo.is_active = 1 AND hcgo.objecttype_id = 11',
+ []
+ );
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = ho.object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join host status
+ */
+ protected function joinHoststatus()
+ {
+ $this->select->join(
+ array('hs' => $this->prefix . 'hoststatus'),
+ 'hs.host_object_id = ho.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = ho.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = s.service_object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sgm.servicegroup_id = sg.' . $this->servicegroup_id,
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->requireVirtualTable('hosts');
+ $this->select->joinLeft(
+ array('s' => $this->prefix . 'services'),
+ 's.host_object_id = h.host_object_id',
+ array()
+ )->joinLeft(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 'ho.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('services');
+
+ return ['s.host_object_id', 'ho.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatussummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatussummaryQuery.php
new file mode 100644
index 0000000..b4a91f3
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatussummaryQuery.php
@@ -0,0 +1,91 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for host group summaries
+ */
+class HoststatussummaryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'hoststatussummary' => array(
+ 'hosts_down' => 'SUM(CASE WHEN state = 1 THEN 1 ELSE 0 END)',
+ 'hosts_down_handled' => 'SUM(CASE WHEN state = 1 AND handled = 1 THEN 1 ELSE 0 END)',
+ 'hosts_down_unhandled' => 'SUM(CASE WHEN state = 1 AND handled = 0 THEN 1 ELSE 0 END)',
+ 'hosts_pending' => 'SUM(CASE WHEN state = 99 THEN 1 ELSE 0 END)',
+ 'hosts_total' => 'SUM(1)',
+ 'hosts_unreachable' => 'SUM(CASE WHEN state = 2 THEN 1 ELSE 0 END)',
+ 'hosts_unreachable_handled' => 'SUM(CASE WHEN state = 2 AND handled = 1 THEN 1 ELSE 0 END)',
+ 'hosts_unreachable_unhandled' => 'SUM(CASE WHEN state = 2 AND handled = 0 THEN 1 ELSE 0 END)',
+ 'hosts_up' => 'SUM(CASE WHEN state = 0 THEN 1 ELSE 0 END)'
+ )
+ );
+
+ /**
+ * The host status sub select
+ *
+ * @var HoststatusQuery
+ */
+ protected $subSelect;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function allowsCustomVars()
+ {
+ return $this->subSelect->allowsCustomVars();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ $this->subSelect->applyFilter(clone $filter);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ // TODO(el): Allow to switch between hard and soft states
+ $this->subSelect = $this->createSubQuery(
+ 'Hoststatus',
+ array(
+ 'handled' => 'host_handled',
+ 'state' => 'host_state',
+ 'state_change' => 'host_last_state_change'
+ )
+ );
+ $this->select->from(
+ array('hoststatussummary' => $this->subSelect->setIsSubQuery(true)),
+ array()
+ );
+ $this->joinedVirtualTables['hoststatussummary'] = true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ $this->subSelect->where($condition, $value);
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->subSelect->whereEx($ex);
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
new file mode 100644
index 0000000..5785092
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
@@ -0,0 +1,1599 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\FilterNot;
+use Zend_Db_Expr;
+use Icinga\Application\Icinga;
+use Icinga\Application\Hook;
+use Icinga\Application\Logger;
+use Icinga\Data\Db\DbQuery;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotImplementedError;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\QueryException;
+use Icinga\Web\Session;
+use Icinga\Module\Monitoring\Data\ColumnFilterIterator;
+use Zend_Db_Select;
+
+/**
+ * Base class for Ido Queries
+ *
+ * This is the base class for all Ido queries and should be extended for new queries
+ * The starting point for implementations is the columnMap attribute. This is an asscociative array in the
+ * following form:
+ *
+ * <pre>
+ * <code>
+ * array(
+ * 'virtualTable' => array(
+ * 'fieldalias1' => 'queryColumn1',
+ * 'fieldalias2' => 'queryColumn2',
+ * ....
+ * ),
+ * 'virtualTable2' => array(
+ * 'host' => 'host_name1'
+ * )
+ * )
+ * </code>
+ * </pre>
+ *
+ * This allows you to select e.g. fieldalias1, which automatically calls the query code for joining 'virtualTable'. If
+ * you afterwards select 'host', 'virtualTable2' will be joined. The joining logic is up to you, in order to make the
+ * above example work you need to implement the joinVirtualTable() method which contain your
+ * custom (Zend_Db) logic for joining, filtering and querying the data you want.
+ *
+ */
+abstract class IdoQuery extends DbQuery
+{
+ /**
+ * The prefix to use
+ *
+ * @var string
+ */
+ protected $prefix;
+
+ /**
+ * An array to map aliases to column names
+ *
+ * @var array
+ */
+ protected $idxAliasColumn;
+
+ /**
+ * An array to map aliases to table names
+ *
+ * @var array
+ */
+ protected $idxAliasTable;
+
+ /**
+ * An array to map custom aliases to aliases
+ *
+ * @var array
+ */
+ protected $idxCustomAliases;
+
+ /**
+ * The column map containing all filterable columns
+ *
+ * This must be overwritten by child classes, in the format
+ * array(
+ * 'virtualTable' => array(
+ * 'fieldalias1' => 'queryColumn1',
+ * 'fieldalias2' => 'queryColumn2',
+ * ....
+ * )
+ * )
+ *
+ * @var array
+ */
+ protected $columnMap = array();
+
+ /**
+ * Custom vars available for this query
+ *
+ * @var array
+ */
+ protected $customVars = array();
+
+ /**
+ * Printf compatible string to joins custom vars
+ *
+ * - %1$s Source field, contain the object_id
+ * - %2$s Alias used for the relation
+ * - %3$s Name of the CustomVariable
+ *
+ * @var string
+ */
+ private $customVarsJoinTemplate = '%1$s = %2$s.object_id AND %2$s.varname = %3$s';
+
+ /**
+ * An array with all 'virtual' tables that are already joined
+ *
+ * Virtual tables are the keys of the columnMap array and require a
+ * join%VirtualTableName%() method to be defined in the concrete
+ * query
+ *
+ * @var array
+ */
+ protected $joinedVirtualTables = array();
+
+ /**
+ * A map of virtual table names and corresponding hook instances
+ *
+ * Joins for those tables will be delegated to them
+ *
+ * @var array
+ */
+ protected $hookedVirtualTables = array();
+
+ /**
+ * List of column aliases used for sorting the result
+ *
+ * @var array
+ */
+ protected $orderColumns = array();
+
+ /**
+ * Table to columns map which have to be added to the GROUP BY list if the query is grouped
+ *
+ * @var array
+ */
+ protected $groupBase = array();
+
+ /**
+ * List of table names which initiate grouping if one of them is joined
+ *
+ * @var array
+ */
+ protected $groupOrigin = array();
+
+ /**
+ * Map of table names to query names for which to create subquery filters
+ *
+ * @var array
+ */
+ protected $subQueryTargets = array();
+
+ /**
+ * The primary key column for the instances table
+ *
+ * @var string
+ */
+ protected $instance_id = 'instance_id';
+
+ /**
+ * The primary key column for the objects table
+ *
+ * @var string
+ */
+ protected $object_id = 'object_id';
+
+ /**
+ * The primary key column for the acknowledgements table
+ *
+ * @var string
+ */
+ protected $acknowledgement_id = 'acknowledgement_id';
+
+ /**
+ * The primary key column for the commenthistory table
+ *
+ * @var string
+ */
+ protected $commenthistory_id = 'commenthistory_id';
+
+ /**
+ * The primary key column for the contactnotifications table
+ *
+ * @var string
+ */
+ protected $contactnotification_id = 'contactnotification_id';
+
+ /**
+ * The primary key column for the downtimehistory table
+ *
+ * @var string
+ */
+ protected $downtimehistory_id = 'downtimehistory_id';
+
+ /**
+ * The primary key column for the flappinghistory table
+ *
+ * @var string
+ */
+ protected $flappinghistory_id = 'flappinghistory_id';
+
+ /**
+ * The primary key column for the notifications table
+ *
+ * @var string
+ */
+ protected $notification_id = 'notification_id';
+
+ /**
+ * The primary key column for the statehistory table
+ *
+ * @var string
+ */
+ protected $statehistory_id = 'statehistory_id';
+
+ /**
+ * The primary key column for the comments table
+ *
+ * @var string
+ */
+ protected $comment_id = 'comment_id';
+
+ /**
+ * The primary key column for the customvariablestatus table
+ *
+ * @var string
+ */
+ protected $customvariablestatus_id = 'customvariablestatus_id';
+
+ /**
+ * The primary key column for the hoststatus table
+ *
+ * @var string
+ */
+ protected $hoststatus_id = 'hoststatus_id';
+
+ /**
+ * The primary key column for the programstatus table
+ *
+ * @var string
+ */
+ protected $programstatus_id = 'programstatus_id';
+
+ /**
+ * The primary key column for the runtimevariables table
+ *
+ * @var string
+ */
+ protected $runtimevariable_id = 'runtimevariable_id';
+
+ /**
+ * The primary key column for the scheduleddowntime table
+ *
+ * @var string
+ */
+ protected $scheduleddowntime_id = 'scheduleddowntime_id';
+
+ /**
+ * The primary key column for the servicestatus table
+ *
+ * @var string
+ */
+ protected $servicestatus_id = 'servicestatus_id';
+
+ /**
+ * The primary key column for the contactstatus table
+ *
+ * @var string
+ */
+ protected $contactstatus_id = 'contactstatus_id';
+
+ /**
+ * The primary key column for the commands table
+ *
+ * @var string
+ */
+ protected $command_id = 'command_id';
+
+ /**
+ * The primary key column for the contactgroup_members table
+ *
+ * @var string
+ */
+ protected $contactgroup_member_id = 'contactgroup_member_id';
+
+ /**
+ * The primary key column for the contactgroups table
+ *
+ * @var string
+ */
+ protected $contactgroup_id = 'contactgroup_id';
+
+ /**
+ * The primary key column for the contacts table
+ *
+ * @var string
+ */
+ protected $contact_id = 'contact_id';
+
+ /**
+ * The primary key column for the customvariables table
+ *
+ * @var string
+ */
+ protected $customvariable_id = 'customvariable_id';
+
+ /**
+ * The primary key column for the host_contactgroups table
+ *
+ * @var string
+ */
+ protected $host_contactgroup_id = 'host_contactgroup_id';
+
+ /**
+ * The primary key column for the host_contacts table
+ *
+ * @var string
+ */
+ protected $host_contact_id = 'host_contact_id';
+
+ /**
+ * The primary key column for the hostgroup_members table
+ *
+ * @var string
+ */
+ protected $hostgroup_member_id = 'hostgroup_member_id';
+
+ /**
+ * The primary key column for the hostgroups table
+ *
+ * @var string
+ */
+ protected $hostgroup_id = 'hostgroup_id';
+
+ /**
+ * The primary key column for the hosts table
+ *
+ * @var string
+ */
+ protected $host_id = 'host_id';
+
+ /**
+ * The primary key column for the service_contactgroup table
+ *
+ * @var string
+ */
+ protected $service_contactgroup_id = 'service_contactgroup_id';
+
+ /**
+ * The primary key column for the service_contact table
+ *
+ * @var string
+ */
+ protected $service_contact_id = 'service_contact_id';
+
+ /**
+ * The primary key column for the servicegroup_members table
+ *
+ * @var string
+ */
+ protected $servicegroup_member_id = 'servicegroup_member_id';
+
+ /**
+ * The primary key column for the servicegroups table
+ *
+ * @var string
+ */
+ protected $servicegroup_id = 'servicegroup_id';
+
+ /**
+ * The primary key column for the services table
+ *
+ * @var string
+ */
+ protected $service_id = 'service_id';
+
+ /**
+ * The primary key column for the timeperiods table
+ *
+ * @var string
+ */
+ protected $timeperiod_id = 'timeperiod_id';
+
+ /**
+ * An array containing Column names that cause an aggregation of the query
+ *
+ * @var array
+ */
+ protected $aggregateColumnIdx = array();
+
+ /**
+ * True to allow customvar filters and queries
+ *
+ * @var bool
+ */
+ protected $allowCustomVars = false;
+
+ /**
+ * Current IDO version. This is bullshit and needs to be moved somewhere
+ * else. As someone decided that we need no Backend-specific connection
+ * class unfortunately there is no better place right now. And as of the
+ * 'check_source' patch we need a quick fix immediately. So here you go.
+ *
+ * TODO: Fix this.
+ *
+ * @var string
+ */
+ protected static $idoVersion;
+
+ /**
+ * List of column aliases mapped to their table where the COLLATE SQL-instruction has been removed
+ *
+ * This list is being populated in case of a PostgreSQL backend only,
+ * to ensure case-insensitive string comparison in WHERE clauses.
+ *
+ * @var array
+ */
+ protected $caseInsensitiveColumns;
+
+ /**
+ * Return true when the column is an aggregate column
+ *
+ * @param String $column The column to test
+ * @return bool True when the column is an aggregate column
+ */
+ public function isAggregateColumn($column)
+ {
+ return array_key_exists($column, $this->aggregateColumnIdx);
+ }
+
+ /**
+ * Order the result by the given alias
+ *
+ * @param string $alias The column alias to order by
+ * @param int $dir The sort direction or null to use the default direction
+ *
+ * @return $this
+ */
+ public function order($alias, $dir = null)
+ {
+ $this->requireColumn($alias);
+
+ if ($this->isCustomVar($alias)) {
+ $column = $this->getCustomvarColumnName($alias);
+ } elseif ($this->hasAliasName($alias)) {
+ $column = $this->aliasToColumnName($alias);
+ $table = $this->aliasToTableName($alias);
+ if (isset($this->caseInsensitiveColumns[$table][$alias])) {
+ $column = 'LOWER(' . $column . ')';
+ }
+ } else {
+ Logger::info('Can\'t order by column ' . $alias);
+ return $this;
+ }
+
+ $this->orderColumns[] = $alias;
+ return parent::order($column, $dir);
+ }
+
+ /**
+ * Return true when the given field can be used for filtering
+ *
+ * @param String $field The field to test
+ * @return bool True when the field can be used for querying, otherwise false
+ */
+ public function isValidFilterTarget($field)
+ {
+ return $this->getMappedField($field) !== null;
+ }
+
+ /**
+ * Return the resolved field for an alias
+ *
+ * @param String $field The alias to resolve
+ * @return String The resolved alias or null if unknown
+ */
+ public function getMappedField($field)
+ {
+ foreach ($this->columnMap as $columnSource => $columnSet) {
+ if (isset($columnSet[$field])) {
+ return $columnSet[$field];
+ }
+ }
+ if ($this->isCustomVar($field)) {
+ return $this->getCustomvarColumnName($field);
+ }
+ return null;
+ }
+
+ public function distinct()
+ {
+ $this->select->distinct();
+ return $this;
+ }
+
+ /**
+ * Prepare the given query so that it can be linked to the parent
+ *
+ * @param IdoQuery $query
+ * @param string $name
+ * @param FilterExpression $filter The filter which initiated the sub query
+ * @param bool $and Whether it's an AND filter
+ * @param bool $negate Whether it's an != filter
+ * @param FilterExpression $additionalFilter Filters which should be applied to the "parent" query
+ *
+ * @return array The first value is their, the second our key column
+ *
+ * @throws NotImplementedError In case the given query is unknown
+ */
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ throw new NotImplementedError('Query "%s" is unknown', $name);
+ }
+
+ /**
+ * Create and return a sub-query filter for the given filter expression
+ *
+ * @param FilterExpression $filter
+ * @param string $queryName
+ *
+ * @return Filter
+ *
+ * @throws QueryException
+ */
+ protected function createSubQueryFilter(FilterExpression $filter, $queryName)
+ {
+ $expr = $filter->getExpression();
+ $op = $filter->getSign();
+
+ if ($op === '=' && ! is_array($expr) && $op !== '!=') {
+ // We're joining a subquery only if the filter is enclosed in parentheses or if it's a != filter,
+ // e.g. hostgroup_name=(linux...), hostgroup_name!=linux, hostgroup_name!=(linux...)
+ throw new NotImplementedError('');
+ }
+
+ $subQuery = $this->createSubQuery($queryName);
+ $subQuery->setIsSubQuery();
+
+ $subQueryFilter = clone $filter;
+
+ if ($op === '!=') {
+ $negate = true;
+ if (! is_array($expr)) {
+ // We assume that expression is an array later on but we'll support subquery joins for != filters
+ // which are not enclosed in parentheses
+ $expr = [$expr];
+ }
+ } else {
+ $negate = false;
+ }
+
+ if (count($expr) === 1 && strpos($expr[0], '&') !== false) {
+ // Our current filter implementation does not specify & as a control character so the count of the
+ // expression array is always one in this case
+ $expr = array_unique(explode('&', $expr[0]));
+ $subQueryFilter->setExpression($expr);
+ $and = true;
+ } else {
+ // Or filters are respected by our filter implementation. No special handling needed here
+ $and = false;
+ }
+
+ $alias = $filter->getColumn();
+ $column = $subQuery->aliasToColumnName($alias);
+ if (isset($this->caseInsensitiveColumns[$subQuery->aliasToTableName($alias)][$alias])) {
+ $column = 'LOWER( ' . $column . ' )';
+ $subQueryFilter->setColumn($column);
+ $subQueryFilter->setExpression(array_map('strtolower', (array) $subQueryFilter->getExpression()));
+ } else {
+ $subQueryFilter->setColumn($column);
+ }
+
+ $additional = null;
+
+ list($theirs, $ours) = $this->joinSubQuery($subQuery, $queryName, $subQueryFilter, $and, $negate, $additional);
+
+ $zendSelect = $subQuery->select();
+ $fromPart = $zendSelect->getPart($zendSelect::FROM);
+ $zendSelect->reset($zendSelect::FROM);
+
+ foreach ($fromPart as $correlationName => $joinOptions) {
+ if (isset($joinOptions['joinCondition'])) {
+ $joinOptions['joinCondition'] = preg_replace(
+ '/(?<=^|\s)\w+(?=\.)/',
+ 'sub_$0',
+ $joinOptions['joinCondition']
+ );
+ }
+
+ $name = ['sub_' . $correlationName => $joinOptions['tableName']];
+ switch ($joinOptions['joinType']) {
+ case $zendSelect::FROM:
+ $zendSelect->from($name);
+ break;
+ case $zendSelect::INNER_JOIN:
+ $zendSelect->joinInner($name, $joinOptions['joinCondition'], null);
+ break;
+ case $zendSelect::LEFT_JOIN:
+ $zendSelect->joinLeft($name, $joinOptions['joinCondition'], null);
+ break;
+ default:
+ // TODO: Add support for other join types if required?
+ throw new QueryException(
+ 'Unsupported join type %s. Cannot create subquery filter.',
+ $joinOptions['joinType']
+ );
+ }
+ }
+
+ if ($and || $negate) {
+ // Having is only required for AND and != filters,
+ // e.g. hostgroup_name=(ping&linux), hostgroup_name!=ping, hostgroup_name!=(ping|linux)
+ $groups = $subQuery->getGroup();
+ if (! empty($groups)) {
+ $group = $groups[0];
+ $group = preg_replace('/(?<=^|\s)\w+(?=\.)/', 'sub_$0', $group);
+
+ $cnt = count($expr);
+
+ $subQuery->select()->having("COUNT(DISTINCT $group) >= $cnt");
+ }
+ }
+
+ $subQueryFilter->setColumn(preg_replace(
+ '/(?<=^|\s)\w+(?=\.)/',
+ 'sub_$0',
+ $column
+ ));
+
+ if ($negate) {
+ // != will be NOT EXISTS later
+ $subQueryFilter = $subQueryFilter->setSign('=');
+ }
+
+ $subQueryFilter = $subQueryFilter->andFilter(Filter::where(
+ preg_replace('/(?<=^|\s)\w+(?=\.)/', 'sub_$0', $theirs),
+ new Zend_Db_Expr($ours)
+ ));
+
+ $subQuery
+ ->setFilter($subQueryFilter)
+ ->clearGroupingRules()
+ ->select()
+ ->reset('columns')
+ ->columns([new Zend_Db_Expr('1')]);
+
+ // EXISTS is the column name because without any column $this->isCustomVar() fails badly otherwise.
+ // Additionally it bypasses the non-required optimizations made by our filter rendering implementation.
+ $exists = new FilterExpression($negate ? 'NOT EXISTS' : 'EXISTS', '', new Zend_Db_Expr($subQuery));
+
+ if ($additional !== null) {
+ return Filter::matchAll($exists, $additional);
+ }
+
+ return $exists;
+ }
+
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression) {
+ $alias = $filter->getColumn();
+
+ $virtualTable = $this->aliasToTableName($alias);
+ if (isset($this->subQueryTargets[$virtualTable])) {
+ try {
+ return $this->createSubQueryFilter($filter, $this->subQueryTargets[$virtualTable]);
+ } catch (NotImplementedError $e) {
+ // We don't want to create subquery filters in all cases
+ }
+ }
+
+ $this->requireColumn($alias);
+
+ if ($this->isCustomVar($alias)) {
+ $column = $this->getCustomvarColumnName($alias);
+ } else {
+ $column = $this->aliasToColumnName($alias);
+ if (isset($this->caseInsensitiveColumns[$this->aliasToTableName($alias)][$alias])) {
+ $column = 'LOWER(' . $column . ')';
+ $expression = $filter->getExpression();
+ if (is_array($expression)) {
+ $filter->setExpression(array_map('strtolower', $expression));
+ } else {
+ $filter->setExpression(strtolower($expression));
+ }
+ }
+ }
+
+ $filter->setColumn($column);
+ } else {
+ if (! $filter instanceof FilterNot) {
+ // Allow subquery filters in a filter chain
+ $columns = $filter->listFilteredColumns();
+ if (count($columns) === 1) {
+ $column = $columns[0];
+ $virtualTable = $this->aliasToTableName($column);
+ if (isset($this->subQueryTargets[$virtualTable])) {
+ $lastSign = null;
+ $filters = [];
+ $expressions = [];
+ foreach ($filter->filters() as $child) {
+ switch (true) {
+ case $child instanceof FilterExpression:
+ $expression = $child->getExpression();
+ if (! is_array($expression)) {
+ break;
+ }
+ // Move to default
+ default:
+ $filters[] = $child;
+ continue 2;
+ }
+ if ($lastSign === null) {
+ $lastSign = $child->getSign();
+ } else {
+ $sign = $child->getSign();
+ if ($sign !== $lastSign) {
+ $filters[] = new FilterExpression(
+ $column,
+ $lastSign,
+ $filter->getOperatorSymbol() === '&'
+ ? [implode('&', $expressions)]
+ : $expressions
+ );
+ $expressions = [];
+ $lastSign = $sign;
+ }
+ }
+ $expressions[] = $expression;
+ }
+ if (! empty($expressions)) {
+ $filters[] = new FilterExpression(
+ $column,
+ $lastSign,
+ $filter->getOperatorSymbol() === '&'
+ ? [implode('&', $expressions)]
+ : $expressions
+ );
+ }
+ $filter->setFilters($filters);
+ }
+ }
+ }
+
+ foreach ($filter->filters() as $child) {
+ $replacement = $this->requireFilterColumns($child);
+ if ($replacement !== null) {
+ // setId($child->getId()) is performed because replaceById() doesn't already do it
+ $filter->replaceById($child->getId(), $replacement->setId($child->getId()));
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ $filter = clone $filter;
+ return parent::addFilter($this->requireFilterColumns($filter) ?: $filter);
+ }
+
+ public function where($condition, $value = null)
+ {
+ $this->requireColumn($condition);
+ $col = $this->getMappedField($condition);
+ if ($col === null) {
+ throw new IcingaException(
+ 'No such field: %s',
+ $condition
+ );
+ }
+ return parent::where($col, $value);
+ }
+
+ /**
+ * Add a filter expression, with as less validation as possible
+ *
+ * @param FilterExpression $ex
+ *
+ * @internal If you use this outside the monitoring module, it's your fault if something breaks
+ * @return $this
+ */
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->requireColumn($ex->getColumn());
+ $col = $this->getMappedField($ex->getColumn());
+ if ($col === null) {
+ throw new IcingaException(
+ 'No such field: %s',
+ $ex->getColumn()
+ );
+ }
+
+ parent::addFilter((clone $ex)->setColumn($col));
+
+ return $this;
+ }
+
+ /**
+ * Return true if an field contains an explicit timestamp
+ *
+ * @param string $field The field to test for containing an timestamp
+ *
+ * @return bool True when the field represents an timestamp
+ */
+ public function isTimestamp($field)
+ {
+ if ($this->isCustomVar($field)) {
+ return false;
+ }
+
+ return stripos($this->getMappedField($field) ?: $field, 'UNIX_TIMESTAMP') !== false;
+ }
+
+ /**
+ * Return whether the given alias provides case insensitive value comparison
+ *
+ * @param string $alias
+ *
+ * @return bool
+ */
+ public function isCaseInsensitive($alias)
+ {
+ if ($this->isCustomVar($alias)) {
+ return false;
+ }
+
+ $column = $this->getMappedField($alias);
+ if (! $column) {
+ return false;
+ }
+
+ if (empty($this->caseInsensitiveColumns)) {
+ return preg_match('/ COLLATE .+$/', $column) === 1;
+ }
+
+ if (strpos($column, 'LOWER') === 0) {
+ return true;
+ }
+
+ $table = $this->aliasToTableName($alias);
+ if (! $table) {
+ return false;
+ }
+
+ return isset($this->caseInsensitiveColumns[$table][$alias]);
+ }
+
+ /**
+ * Return our column map
+ *
+ * Might be useful for hooks
+ *
+ * @return array
+ */
+ public function getColumnMap()
+ {
+ return $this->columnMap;
+ }
+
+ /**
+ * Apply oracle specific query initialization
+ */
+ private function initializeForOracle()
+ {
+ // Oracle uses the reserved field 'id' for primary keys, so
+ // these must be used instead of the normally defined ids
+ $this->object_id = $this->host_id = $this->service_id
+ = $this->hostgroup_id = $this->servicegroup_id
+ = $this->contact_id = $this->contactgroup_id = 'id';
+ $this->customVarsJoinTemplate =
+ '%1$s = %2$s.object_id AND LOWER(%2$s.varname) = %3$s';
+ foreach ($this->columnMap as &$columns) {
+ foreach ($columns as &$value) {
+ $value = preg_replace('/UNIX_TIMESTAMP/', 'localts2unixts', $value);
+ $value = preg_replace('/ COLLATE .+$/', '', $value);
+ }
+ }
+ }
+
+ /**
+ * Apply PostgreSQL specific query initialization
+ */
+ private function initializeForPostgres()
+ {
+ $this->customVarsJoinTemplate =
+ '%1$s = %2$s.object_id AND LOWER(%2$s.varname) = %3$s';
+ foreach ($this->columnMap as $table => & $columns) {
+ foreach ($columns as $alias => & $column) {
+ if ($column === null) {
+ continue;
+ }
+
+ // Using a regex here because COLLATE may occur anywhere in the string
+ $column = preg_replace('/ COLLATE .+$/', '', $column, -1, $count);
+ if ($count > 0) {
+ $this->caseInsensitiveColumns[$table][$alias] = true;
+ }
+
+ $column = preg_replace(
+ '/inet_aton\(([[:word:].]+)\)/i',
+ '(CASE WHEN $1 ~ \'(?:[0-9]{1,3}\\\\.){3}[0-9]{1,3}\' THEN $1::inet - \'0.0.0.0\' ELSE NULL END)',
+ $column
+ );
+ if (version_compare($this->getIdoVersion(), '1.14.2', '>=')) {
+ $column = str_replace('NOW()', 'NOW() AT TIME ZONE \'UTC\'', $column);
+ } else {
+ $column = preg_replace(
+ '/UNIX_TIMESTAMP(\((?>[^()]|(?-1))*\))/i',
+ 'CASE WHEN ($1 < \'1970-01-03 00:00:00+00\'::timestamp with time zone) THEN 0 ELSE UNIX_TIMESTAMP($1) END',
+ $column
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * Set up this query and join the initial tables
+ *
+ * @see IdoQuery::initializeForPostgres For postgresql specific setup
+ */
+ protected function init()
+ {
+ parent::init();
+ $this->prefix = $this->ds->getTablePrefix();
+
+ foreach (Hook::all('monitoring/idoQueryExtension') as $hook) {
+ $extensions = $hook->extendColumnMap($this);
+ if (! is_array($extensions)) {
+ continue;
+ }
+
+ foreach ($extensions as $vTable => $cols) {
+ if (! array_key_exists($vTable, $this->columnMap)) {
+ $this->hookedVirtualTables[$vTable] = $hook;
+ $this->columnMap[$vTable] = array();
+ }
+
+ foreach ($cols as $k => $v) {
+ $this->columnMap[$vTable][$k] = $v;
+ }
+ }
+ }
+
+ $dbType = $this->ds->getDbType();
+ if ($dbType === 'oracle') {
+ $this->initializeForOracle();
+ } elseif ($dbType === 'pgsql') {
+ $this->initializeForPostgres();
+ } else {
+ $charset = $this->ds->getConfig()->get('charset') ?: 'latin1';
+ $this->customVarsJoinTemplate .= " COLLATE {$charset}_general_ci";
+ }
+ $this->joinBaseTables();
+ $this->select->columns($this->columns);
+ $this->prepareAliasIndexes();
+ }
+
+ /**
+ * Join the base tables for this query
+ */
+ protected function joinBaseTables()
+ {
+ reset($this->columnMap);
+ $table = key($this->columnMap);
+
+ $this->select->from(
+ array($table => $this->prefix . $table),
+ array()
+ );
+
+ $this->joinedVirtualTables = array($table => true);
+ }
+
+ /**
+ * Populates the idxAliasTAble and idxAliasColumn properties
+ */
+ protected function prepareAliasIndexes()
+ {
+ foreach ($this->columnMap as $tbl => & $cols) {
+ foreach ($cols as $alias => $col) {
+ $this->idxAliasTable[$alias] = $tbl;
+ $this->idxAliasColumn[$alias] = preg_replace('~\n\s*~', ' ', $col);
+ }
+ }
+ }
+
+ /**
+ * Resolve columns aliases to their database field using the columnMap
+ *
+ * @param array $columns
+ *
+ * @return array
+ */
+ public function resolveColumns($columns)
+ {
+ $resolvedColumns = array();
+
+ foreach ($columns as $alias => $col) {
+ if ($col instanceof Zend_Db_Expr) {
+ // Support selecting NULL as column for example
+ $resolvedColumns[$alias] = $col;
+ continue;
+ }
+ $this->requireColumn($col);
+ if ($this->isCustomVar($col)) {
+ $name = $this->getCustomvarColumnName($col);
+ } else {
+ $name = $this->aliasToColumnName($col);
+ }
+ if (is_int($alias)) {
+ $alias = $col;
+ } else {
+ $this->idxCustomAliases[$alias] = $col;
+ }
+
+ $resolvedColumns[$alias] = preg_replace('|\n|', ' ', $name);
+ }
+
+ return $resolvedColumns;
+ }
+
+ /**
+ * Return all columns that will be selected when no columns are given in the constructor or from
+ *
+ * @return array An array of column aliases
+ */
+ public function getDefaultColumns()
+ {
+ reset($this->columnMap);
+ $table = key($this->columnMap);
+ return array_keys($this->columnMap[$table]);
+ }
+
+ /**
+ * Modify the query to the given alias can be used in the result set or queries
+ *
+ * This calls requireVirtualTable if needed
+ *
+ * @param string $alias The alias of the column to require
+ *
+ * @return $this Fluent interface
+ * @see IdoQuery::requireVirtualTable The method initializing required joins
+ * @throws \Icinga\Exception\ProgrammingError When an unknown column is requested
+ */
+ public function requireColumn($alias)
+ {
+ if ($this->hasAliasName($alias)) {
+ $this->requireVirtualTable($this->aliasToTableName($alias));
+ } elseif ($this->isCustomVar($alias)) {
+ $this->requireCustomvar($alias);
+ } else {
+ throw new ProgrammingError(
+ '%s : Got invalid column: %s',
+ get_called_class(),
+ $alias
+ );
+ }
+ return $this;
+ }
+
+ /**
+ * Return true if the given alias exists
+ *
+ * @param String $alias The alias to test for
+ * @return bool True when the alias exists, otherwise false
+ */
+ protected function hasAliasName($alias)
+ {
+ return array_key_exists($alias, $this->idxAliasColumn);
+ }
+
+ /**
+ * Require a virtual table for the given table name if not already required
+ *
+ * @param String $name The table name to require
+ * @return $this Fluent interface
+ */
+ protected function requireVirtualTable($name)
+ {
+ if ($this->hasJoinedVirtualTable($name)) {
+ return $this;
+ }
+
+ if ($this->virtualTableIsHooked($name)) {
+ return $this->joinHookedVirtualTable($name);
+ } else {
+ return $this->joinVirtualTable($name);
+ }
+ }
+
+ /**
+ * Whether a given virtual table name has been provided by a hook
+ *
+ * @param string $name Virtual table name
+ *
+ * @return boolean
+ */
+ protected function virtualTableIsHooked($name)
+ {
+ return array_key_exists($name, $this->hookedVirtualTables);
+ }
+
+ protected function conflictsWithVirtualTable($name)
+ {
+ if ($this->hasJoinedVirtualTable($name)) {
+ throw new ProgrammingError(
+ 'IDO query virtual table conflict with "%s"',
+ $name
+ );
+ }
+ return $this;
+ }
+
+ /**
+ * Call the method for joining a virtual table
+ *
+ * This requires a join$Table() method to exist
+ *
+ * @param String $table The table to join by calling join$Table() in the concrete implementation
+ * @return $this Fluent interface
+ *
+ * @throws \Icinga\Exception\ProgrammingError If the join method for this table does not exist
+ */
+ protected function joinVirtualTable($table)
+ {
+ $func = 'join' . ucfirst($table);
+ if (method_exists($this, $func)) {
+ $this->$func();
+ } else {
+ throw new ProgrammingError(
+ 'Cannot join "%s", no such table found',
+ $table
+ );
+ }
+ $this->joinedVirtualTables[$table] = true;
+ return $this;
+ }
+
+ /**
+ * Tell a hook to join a virtual table
+ *
+ * @param String $table
+ * @return $this
+ */
+ protected function joinHookedVirtualTable($table)
+ {
+ $this->hookedVirtualTables[$table]->joinVirtualTable($this, $table);
+ $this->joinedVirtualTables[$table] = true;
+ return $this;
+ }
+
+ /**
+ * Get the table for a specific alias
+ *
+ * @param String $alias The alias to request the table for
+ * @return String The table for the alias or null if it doesn't exist
+ */
+ protected function aliasToTableName($alias)
+ {
+ return isset($this->idxAliasTable[$alias]) ? $this->idxAliasTable[$alias] : null;
+ }
+
+ /**
+ * Return whether this query allows to join custom variables
+ *
+ * @return bool
+ */
+ public function allowsCustomVars()
+ {
+ return $this->allowCustomVars;
+ }
+
+ /**
+ * Return true if the given alias denotes a custom variable
+ *
+ * @param String $alias The alias to test for being a customvariable
+ * @return bool True if the alias is a customvariable, otherwise false
+ */
+ protected function isCustomVar($alias)
+ {
+ return $this->allowCustomVars && $alias[0] === '_';
+ }
+
+ protected function requireCustomvar($customvar)
+ {
+ if (! $this->hasCustomvar($customvar)) {
+ $this->joinCustomvar($customvar);
+ }
+ return $this;
+ }
+
+ protected function hasCustomvar($customvar)
+ {
+ return array_key_exists(strtolower($customvar), $this->customVars);
+ }
+
+ protected function joinCustomvar($customvar)
+ {
+ // TODO: This is not generic enough yet
+ list($type, $name) = $this->customvarNameToTypeName($customvar);
+ $alias = ($type === 'host' ? 'hcv_' : 'scv_') . preg_replace('~[^a-zA-Z0-9_]~', '_', $name);
+
+ // We're replacing any problematic char with an underscore, which will lead to duplicates, this avoids them
+ $from = $this->select->getPart(Zend_Db_Select::FROM);
+ for ($i = 2; array_key_exists($alias, $from); $i++) {
+ $alias = $alias . '_' . $i;
+ }
+
+ $this->customVars[strtolower($customvar)] = $alias;
+
+ if ($type === 'host') {
+ if ($this instanceof ServicecommentQuery
+ || $this instanceof ServicedowntimeQuery
+ || $this instanceof ServicecommenthistoryQuery
+ || $this instanceof ServicedowntimestarthistoryQuery
+ || $this instanceof ServiceflappingstarthistoryQuery
+ || $this instanceof ServicegroupQuery
+ || $this instanceof ServicenotificationQuery
+ || $this instanceof ServicestatehistoryQuery
+ || $this instanceof ServicestatusQuery
+ ) {
+ $this->requireVirtualTable('services');
+ $leftcol = 's.host_object_id';
+ } else {
+ $leftcol = 'ho.object_id';
+ if (! $this->hasJoinedTable('ho')) {
+ $this->requireVirtualTable('hosts');
+ }
+ }
+ } else { // $type === 'service'
+ $leftcol = 'so.object_id';
+ if (! $this->hasJoinedTable('so')) {
+ $this->requireVirtualTable('services');
+ }
+ }
+
+ $mapped = $this->getMappedField($leftcol);
+ if ($mapped !== null) {
+ $this->requireColumn($leftcol);
+ $leftcol = $mapped;
+ }
+
+ $joinOn = sprintf(
+ $this->customVarsJoinTemplate,
+ $leftcol,
+ $alias,
+ $this->db->quote($name)
+ );
+
+ $this->select->joinLeft(
+ array($alias => $this->prefix . 'customvariablestatus'),
+ $joinOn,
+ array()
+ );
+
+ return $this;
+ }
+
+ protected function customvarNameToTypeName($customvar)
+ {
+ $customvar = strtolower($customvar);
+ if (! preg_match('~^_(host|service)_(.+)$~', $customvar, $m)) {
+ throw new ProgrammingError(
+ 'Got invalid custom var: "%s"',
+ $customvar
+ );
+ }
+ return array($m[1], $m[2]);
+ }
+
+ protected function hasJoinedVirtualTable($name)
+ {
+ return array_key_exists($name, $this->joinedVirtualTables);
+ }
+
+ /**
+ * Get the query column of a already joined custom variable
+ *
+ * @param string $customvar
+ *
+ * @return string
+ * @throws QueryException If the custom variable has not been joined
+ */
+ protected function getCustomvarColumnName($customvar)
+ {
+ if (! isset($this->customVars[($customvar = strtolower($customvar))])) {
+ throw new QueryException('Custom variable %s has not been joined', $customvar);
+ }
+ return $this->customVars[$customvar] . '.varvalue';
+ }
+
+ public function aliasToColumnName($alias)
+ {
+ return $this->idxAliasColumn[$alias];
+ }
+
+ /**
+ * Get the alias of a column expression as defined in the {@link $columnMap} property.
+ *
+ * @param string $alias Potential custom alias
+ *
+ * @return string
+ */
+ public function customAliasToAlias($alias)
+ {
+ if (isset($this->idxCustomAliases[$alias])) {
+ return $this->idxCustomAliases[$alias];
+ }
+ return $alias;
+ }
+
+ /**
+ * Create a sub query
+ *
+ * @param string $queryName
+ * @param array $columns
+ *
+ * @return static
+ */
+ protected function createSubQuery($queryName, $columns = array())
+ {
+ $class = '\\'
+ . substr(__CLASS__, 0, strrpos(__CLASS__, '\\') + 1)
+ . ucfirst($queryName) . 'Query';
+ $query = new $class($this->ds, $columns);
+ return $query;
+ }
+
+ /**
+ * Set columns to select
+ *
+ * @param array $columns
+ *
+ * @return $this
+ */
+ public function columns(array $columns)
+ {
+ $this->idxCustomAliases = array();
+ $this->columns = $this->resolveColumns($columns);
+ // TODO: we need to refresh our select!
+ // $this->select->columns($columns);
+ return $this;
+ }
+
+ public function clearGroupingRules()
+ {
+ $this->groupBase = array();
+ $this->groupOrigin = array();
+ return $this;
+ }
+
+ /**
+ * Register the GROUP BY columns required for the given alias
+ *
+ * @param string $alias The alias to register columns for
+ * @param string $table The table the given alias is associated with
+ * @param array $groupedColumns The grouping columns registered so far
+ * @param array $groupedTables The tables for which columns were registered so far
+ */
+ protected function registerGroupColumns($alias, $table, array &$groupedColumns, array &$groupedTables)
+ {
+ switch ($table) {
+ case 'checktimeperiods':
+ $groupedColumns[] = 'ctp.timeperiod_id';
+ break;
+ case 'contacts':
+ $groupedColumns[] = 'co.object_id';
+ $groupedColumns[] = 'c.contact_id';
+ break;
+ case 'hostobjects':
+ $groupedColumns[] = 'ho.object_id';
+ break;
+ case 'hosts':
+ $groupedColumns[] = 'h.host_id';
+ break;
+ case 'hostgroups':
+ $groupedColumns[] = 'hgo.object_id';
+ $groupedColumns[] = 'hg.hostgroup_id';
+ break;
+ case 'hoststatus':
+ $groupedColumns[] = 'hs.hoststatus_id';
+ break;
+ case 'instances':
+ $groupedColumns[] = 'i.instance_id';
+ break;
+ case 'servicegroups':
+ $groupedColumns[] = 'sgo.object_id';
+ $groupedColumns[] = 'sg.servicegroup_id';
+ break;
+ case 'serviceobjects':
+ $groupedColumns[] = 'so.object_id';
+ break;
+ case 'serviceproblemsummary':
+ $groupedColumns[] = 'sps.unhandled_services_count';
+ break;
+ case 'services':
+ $groupedColumns[] = 'so.object_id';
+ $groupedColumns[] = 's.service_id';
+ break;
+ case 'servicestatus':
+ $groupedColumns[] = 'ss.servicestatus_id';
+ break;
+ default:
+ return;
+ }
+
+ $groupedTables[$table] = true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getGroup()
+ {
+ $group = parent::getGroup() ?: array();
+ if (! is_array($group)) {
+ $group = array($group);
+ }
+
+ $joinedOrigins = array_filter($this->groupOrigin, array($this, 'hasJoinedVirtualTable'));
+ if (empty($joinedOrigins)) {
+ return $group;
+ }
+
+ $groupedTables = array();
+ foreach ($this->groupBase as $baseTable => $aliasedPks) {
+ if (! $this->hasJoinedVirtualTable($baseTable)) {
+ continue;
+ }
+ $groupedTables[$baseTable] = true;
+ foreach ($aliasedPks as $aliasedPk) {
+ $group[] = $aliasedPk;
+ }
+ }
+
+ foreach (new ColumnFilterIterator($this->columns) as $desiredAlias => $desiredColumn) {
+ $alias = is_string($desiredAlias) ? $this->customAliasToAlias($desiredAlias) : $desiredColumn;
+ if ($this->isCustomVar($alias) && $this->getDatasource()->getDbType() === 'pgsql') {
+ $table = $this->customVars[$alias];
+ if (! isset($groupedTables[$table])) {
+ $group[] = $this->getCustomvarColumnName($alias);
+ $groupedTables[$table] = true;
+ }
+ continue;
+ }
+ $table = $this->aliasToTableName($alias);
+ if ($table && !isset($groupedTables[$table]) && (
+ in_array($table, $joinedOrigins, true) || $this->getDatasource()->getDbType() === 'pgsql')
+ ) {
+ $this->registerGroupColumns($alias, $table, $group, $groupedTables);
+ }
+ }
+
+ if (! empty($group) && $this->getDatasource()->getDbType() === 'pgsql') {
+ foreach (new ColumnFilterIterator($this->orderColumns) as $alias) {
+ if ($this->isCustomVar($alias)) {
+ $table = $this->customVars[$alias];
+ if (! isset($groupedTables[$table])) {
+ $group[] = $this->getCustomvarColumnName($alias);
+ $groupedTables[$table] = true;
+ }
+ continue;
+ }
+ $table = $this->aliasToTableName($alias);
+ if ($table && !isset($groupedTables[$table])
+ && !in_array($this->getMappedField($alias), $this->columns, true)
+ ) {
+ $this->registerGroupColumns($alias, $table, $group, $groupedTables);
+ }
+ }
+ }
+
+ return array_unique($group);
+ }
+
+ // TODO: Move this away, see note related to $idoVersion var
+ protected function getIdoVersion()
+ {
+ if (self::$idoVersion === null) {
+ $dbconf = $this->db->getConfig();
+ $id = $dbconf['host'] . '/' . $dbconf['dbname'];
+ $session = null;
+ if (Icinga::app()->isWeb()) {
+ // TODO: Once we have version per connection we should choose a
+ // namespace based on resource name
+ $session = Session::getSession()->getNamespace('monitoring/ido/' . $id);
+ if (isset($session->version)) {
+ self::$idoVersion = $session->version;
+ return self::$idoVersion;
+ }
+ }
+ self::$idoVersion = $this->db->fetchOne(
+ $this->db->select()->from($this->prefix . 'dbversion', 'version')
+ );
+ if ($session !== null) {
+ $session->version = self::$idoVersion;
+ }
+ }
+ return self::$idoVersion;
+ }
+
+ /**
+ * Return the name of the primary key column for the given table name
+ *
+ * @param string $table
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case $table is unknown
+ */
+ protected function getPrimaryKeyColumn($table)
+ {
+ // TODO: For god's sake, make this being a mapping
+ // (instead of matching a ton of properties using a ridiculous long switch case)
+ switch ($table) {
+ case 'instances':
+ return $this->instance_id;
+ case 'objects':
+ return $this->object_id;
+ case 'acknowledgements':
+ return $this->acknowledgement_id;
+ case 'commenthistory':
+ return $this->commenthistory_id;
+ case 'contactnotifiations':
+ return $this->contactnotification_id;
+ case 'downtimehistory':
+ return $this->downtimehistory_id;
+ case 'flappinghistory':
+ return $this->flappinghistory_id;
+ case 'notifications':
+ return $this->notification_id;
+ case 'statehistory':
+ return $this->statehistory_id;
+ case 'comments':
+ return $this->comment_id;
+ case 'customvariablestatus':
+ return $this->customvariablestatus_id;
+ case 'hoststatus':
+ return $this->hoststatus_id;
+ case 'programstatus':
+ return $this->programstatus_id;
+ case 'runtimevariables':
+ return $this->runtimevariable_id;
+ case 'scheduleddowntime':
+ return $this->scheduleddowntime_id;
+ case 'servicestatus':
+ return $this->servicestatus_id;
+ case 'contactstatus':
+ return $this->contactstatus_id;
+ case 'commands':
+ return $this->command_id;
+ case 'contactgroup_members':
+ return $this->contactgroup_member_id;
+ case 'contactgroups':
+ return $this->contactgroup_id;
+ case 'contacts':
+ return $this->contact_id;
+ case 'customvariables':
+ return $this->customvariable_id;
+ case 'host_contactgroups':
+ return $this->host_contactgroup_id;
+ case 'host_contacts':
+ return $this->host_contact_id;
+ case 'hostgroup_members':
+ return $this->hostgroup_member_id;
+ case 'hostgroups':
+ return $this->hostgroup_id;
+ case 'hosts':
+ return $this->host_id;
+ case 'service_contactgroups':
+ return $this->service_contactgroup_id;
+ case 'service_contacts':
+ return $this->service_contact_id;
+ case 'servicegroup_members':
+ return $this->servicegroup_member_id;
+ case 'servicegroups':
+ return $this->servicegroup_id;
+ case 'services':
+ return $this->service_id;
+ case 'timeperiods':
+ return $this->timeperiod_id;
+ default:
+ throw new ProgrammingError('Cannot provide a primary key column. Table "%s" is unknown', $table);
+ }
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/InstanceQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/InstanceQuery.php
new file mode 100644
index 0000000..ac538ec
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/InstanceQuery.php
@@ -0,0 +1,26 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+class InstanceQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'instances' => array(
+ 'instance_id' => 'i.instance_id',
+ 'instance_name' => 'i.instance_name'
+ )
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->select()->from(array('i' => $this->prefix . 'instances'), array());
+ $this->joinedVirtualTables['instances'] = true;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationQuery.php
new file mode 100644
index 0000000..8bfb725
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationQuery.php
@@ -0,0 +1,144 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\FilterExpression;
+use Zend_Db_Expr;
+use Zend_Db_Select;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for host and service notifications
+ */
+class NotificationQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'notifications' => array(
+ 'id' => 'n.id',
+ 'instance_name' => 'n.instance_name',
+ 'notification_contact_name' => 'n.notification_contact_name',
+ 'notification_output' => 'n.notification_output',
+ 'notification_reason' => 'n.notification_reason',
+ 'notification_state' => 'n.notification_state',
+ 'notification_timestamp' => 'n.notification_timestamp'
+ ),
+ 'hosts' => array(
+ 'host_display_name' => 'n.host_display_name',
+ 'host_name' => 'n.host_name'
+ ),
+ 'services' => array(
+ 'service_description' => 'n.service_description',
+ 'service_display_name' => 'n.service_display_name',
+ 'service_host_name' => 'n.service_host_name'
+ )
+ );
+
+ /**
+ * The union
+ *
+ * @var Zend_Db_Select
+ */
+ protected $notificationQuery;
+
+ /**
+ * Subqueries used for the notification query
+ *
+ * @var IdoQuery[]
+ */
+ protected $subQueries = array();
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->notificationQuery = $this->db->select();
+ $this->select->from(
+ array('n' => $this->notificationQuery),
+ array()
+ );
+ $this->joinedVirtualTables['notifications'] = true;
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $columns = $this->desiredColumns;
+ $columns = array_combine($columns, $columns);
+ foreach ($this->columnMap['services'] as $column => $_) {
+ if (isset($columns[$column])) {
+ $columns[$column] = new Zend_Db_Expr('NULL');
+ }
+ }
+ $hosts = $this->createSubQuery('hostnotification', $columns);
+ $hosts->setIsSubQuery(true);
+ $this->subQueries[] = $hosts;
+ $this->notificationQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $services = $this->createSubQuery('servicenotification', $this->desiredColumns);
+ $services->setIsSubQuery(true);
+ $this->subQueries[] = $services;
+ $this->notificationQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order($columnOrAlias, $dir = null)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->requireColumn($columnOrAlias);
+ }
+ return parent::order($columnOrAlias, $dir);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ $this->requireColumn($condition);
+ foreach ($this->subQueries as $sub) {
+ $sub->where($condition, $value);
+ }
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->requireColumn($ex->getColumn());
+ foreach ($this->subQueries as $sub) {
+ $sub->whereEx($ex);
+ }
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationeventQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationeventQuery.php
new file mode 100644
index 0000000..87a71f6
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationeventQuery.php
@@ -0,0 +1,52 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for host and service notification events
+ */
+class NotificationeventQuery extends IdoQuery
+{
+ protected $columnMap = array(
+ 'notificationevent' => array(
+ 'notificationevent_id' => 'n.notification_id',
+ 'notificationevent_reason' => <<<EOF
+(CASE n.notification_reason
+ WHEN 0 THEN 'normal_notification'
+ WHEN 1 THEN 'ack'
+ WHEN 2 THEN 'flapping_started'
+ WHEN 3 THEN 'flapping_stopped'
+ WHEN 4 THEN 'flapping_disabled'
+ WHEN 5 THEN 'dt_start'
+ WHEN 6 THEN 'dt_end'
+ WHEN 7 THEN 'dt_cancel'
+ WHEN 99 THEN 'custom_notification'
+ ELSE NULL
+END)
+EOF
+ ,
+ 'notificationevent_start_time' => 'UNIX_TIMESTAMP(n.start_time)',
+ 'notificationevent_end_time' => 'UNIX_TIMESTAMP(n.end_time)',
+ 'notificationevent_state' => 'n.state',
+ 'notificationevent_output' => 'n.output',
+ 'notificationevent_long_output' => 'n.long_output',
+ 'notificationevent_escalated' => 'n.escalated',
+ 'notificationevent_contacts_notified' => 'n.contacts_notified'
+ ),
+ 'object' => array(
+ 'host_name' => 'o.name1',
+ 'service_description' => 'o.name2'
+ )
+ );
+
+ protected function joinBaseTables()
+ {
+ $this->select()
+ ->from(array('n' => $this->prefix . 'notifications'), array())
+ ->join(array('o' => $this->prefix . 'objects'), 'n.object_id = o.object_id', array());
+
+ $this->joinedVirtualTables['notificationevent'] = true;
+ $this->joinedVirtualTables['object'] = true;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationhistoryQuery.php
new file mode 100644
index 0000000..f629115
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationhistoryQuery.php
@@ -0,0 +1,142 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\FilterExpression;
+use Zend_Db_Expr;
+use Zend_Db_Select;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for host and service notification history
+ */
+class NotificationhistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'history' => array(
+ 'id' => 'n.id',
+ 'object_type' => 'n.object_type',
+ 'output' => 'n.output',
+ 'state' => 'n.state',
+ 'timestamp' => 'n.timestamp',
+ 'type' => 'n.type'
+ ),
+ 'hosts' => array(
+ 'host_display_name' => 'n.host_display_name',
+ 'host_name' => 'n.host_name'
+ ),
+ 'services' => array(
+ 'service_description' => 'n.service_description',
+ 'service_display_name' => 'n.service_display_name',
+ 'service_host_name' => 'n.service_host_name'
+ )
+ );
+
+ /**
+ * The union
+ *
+ * @var Zend_Db_Select
+ */
+ protected $notificationQuery;
+
+ /**
+ * Subqueries used for the notification query
+ *
+ * @var IdoQuery[]
+ */
+ protected $subQueries = array();
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->notificationQuery = $this->db->select();
+ $this->select->from(
+ array('n' => $this->notificationQuery),
+ array()
+ );
+ $this->joinedVirtualTables['history'] = true;
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $columns = $this->desiredColumns;
+ $columns = array_combine($columns, $columns);
+ foreach ($this->columnMap['services'] as $column => $_) {
+ if (isset($columns[$column])) {
+ $columns[$column] = new Zend_Db_Expr('NULL');
+ }
+ }
+ $hosts = $this->createSubQuery('hostnotification', $columns);
+ $this->subQueries[] = $hosts;
+ $this->notificationQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $columns = array_flip($this->desiredColumns);
+ $services = $this->createSubQuery('servicenotification', array_flip($columns));
+ $this->subQueries[] = $services;
+ $this->notificationQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order($columnOrAlias, $dir = null)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->requireColumn($columnOrAlias);
+ }
+ return parent::order($columnOrAlias, $dir);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ $this->requireColumn($condition);
+ foreach ($this->subQueries as $sub) {
+ $sub->where($condition, $value);
+ }
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->requireColumn($ex->getColumn());
+ foreach ($this->subQueries as $sub) {
+ $sub->whereEx($ex);
+ }
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ProgramstatusQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ProgramstatusQuery.php
new file mode 100644
index 0000000..9e9f5f6
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ProgramstatusQuery.php
@@ -0,0 +1,68 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Program status query
+ */
+class ProgramstatusQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'programstatus' => array(
+ 'id' => 'programstatus_id',
+ 'status_update_time' => 'UNIX_TIMESTAMP(programstatus.status_update_time)',
+ 'program_version' => 'program_version',
+ 'program_start_time' => 'UNIX_TIMESTAMP(programstatus.program_start_time)',
+ 'program_end_time' => 'UNIX_TIMESTAMP(programstatus.program_end_time)',
+ 'is_currently_running' => 'CASE WHEN (UNIX_TIMESTAMP(programstatus.status_update_time) + 60 > UNIX_TIMESTAMP(NOW()))
+ THEN
+ 1
+ ELSE
+ 0
+ END',
+ 'process_id' => 'process_id',
+ 'endpoint_name' => 'endpoint_name',
+ 'daemon_mode' => 'daemon_mode',
+ 'last_command_check' => 'UNIX_TIMESTAMP(programstatus.last_command_check)',
+ 'last_log_rotation' => 'UNIX_TIMESTAMP(programstatus.last_log_rotation)',
+ 'notifications_enabled' => 'notifications_enabled',
+ 'disable_notif_expire_time' => 'UNIX_TIMESTAMP(programstatus.disable_notif_expire_time)',
+ 'active_service_checks_enabled' => 'active_service_checks_enabled',
+ 'passive_service_checks_enabled' => 'passive_service_checks_enabled',
+ 'active_host_checks_enabled' => 'active_host_checks_enabled',
+ 'passive_host_checks_enabled' => 'passive_host_checks_enabled',
+ 'event_handlers_enabled' => 'event_handlers_enabled',
+ 'flap_detection_enabled' => 'flap_detection_enabled',
+ 'failure_prediction_enabled' => 'failure_prediction_enabled',
+ 'process_performance_data' => 'process_performance_data',
+ 'obsess_over_hosts' => 'obsess_over_hosts',
+ 'obsess_over_services' => 'obsess_over_services',
+ 'modified_host_attributes' => 'modified_host_attributes',
+ 'modified_service_attributes' => 'modified_service_attributes',
+ 'global_host_event_handler' => 'global_host_event_handler',
+ 'global_service_event_handler' => 'global_service_event_handler',
+ )
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ parent::joinBaseTables();
+
+ if (version_compare($this->getIdoVersion(), '1.11.7', '<')) {
+ $this->columnMap['programstatus']['endpoint_name'] = '(0)';
+ }
+ if (version_compare($this->getIdoVersion(), '1.11.8', '<')) {
+ $this->columnMap['programstatus']['program_version'] = '(NULL)';
+ }
+ if (version_compare($this->getIdoVersion(), '1.8', '<')) {
+ $this->columnMap['programstatus']['disable_notif_expire_time'] = '(NULL)';
+ }
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimesummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimesummaryQuery.php
new file mode 100644
index 0000000..1aa2257
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimesummaryQuery.php
@@ -0,0 +1,80 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Zend_Db_Select;
+
+/**
+ * Query check summaries out of database
+ */
+class RuntimesummaryQuery extends IdoQuery
+{
+ protected $columnMap = array(
+ 'runtimesummary' => array(
+ 'check_type' => 'check_type',
+ 'active_checks_enabled' => 'active_checks_enabled',
+ 'passive_checks_enabled' => 'passive_checks_enabled',
+ 'execution_time' => 'execution_time',
+ 'latency' => 'latency',
+ 'object_count' => 'object_count',
+ 'object_type' => 'object_type'
+ )
+ );
+
+ protected function joinBaseTables()
+ {
+ $hosts = $this->db->select()->from(
+ array('ho' => $this->prefix . 'objects'),
+ array()
+ )->join(
+ array('hs' => $this->prefix . 'hoststatus'),
+ 'ho.object_id = hs.host_object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
+ array()
+ )->columns(
+ array(
+ 'check_type' => 'CASE '
+ . 'WHEN hs.active_checks_enabled = 0 AND hs.passive_checks_enabled = 1 THEN \'passive\' '
+ . 'WHEN hs.active_checks_enabled = 1 THEN \'active\' '
+ . 'END',
+ 'active_checks_enabled' => 'hs.active_checks_enabled',
+ 'passive_checks_enabled' => 'hs.passive_checks_enabled',
+ 'execution_time' => 'SUM(hs.execution_time)',
+ 'latency' => 'SUM(hs.latency)',
+ 'object_count' => 'COUNT(*)',
+ 'object_type' => "('host')"
+ )
+ )->group('check_type')->group('active_checks_enabled')->group('passive_checks_enabled');
+
+ $services = $this->db->select()->from(
+ array('so' => $this->prefix . 'objects'),
+ array()
+ )->join(
+ array('ss' => $this->prefix . 'servicestatus'),
+ 'so.object_id = ss.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ )->columns(
+ array(
+ 'check_type' => 'CASE '
+ . 'WHEN ss.active_checks_enabled = 0 AND ss.passive_checks_enabled = 1 THEN \'passive\' '
+ . 'WHEN ss.active_checks_enabled = 1 THEN \'active\' '
+ . 'END',
+ 'active_checks_enabled' => 'ss.active_checks_enabled',
+ 'passive_checks_enabled' => 'ss.passive_checks_enabled',
+ 'execution_time' => 'SUM(ss.execution_time)',
+ 'latency' => 'SUM(ss.latency)',
+ 'object_count' => 'COUNT(*)',
+ 'object_type' => "('service')"
+ )
+ )->group('check_type')->group('active_checks_enabled')->group('passive_checks_enabled');
+
+ $union = $this->db->select()->union(
+ array('s' => $services, 'h' => $hosts),
+ Zend_Db_Select::SQL_UNION_ALL
+ );
+
+ $this->select->from(array('hs' => $union));
+
+ $this->joinedVirtualTables = array('runtimesummary' => true);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimevariablesQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimevariablesQuery.php
new file mode 100644
index 0000000..494744a
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimevariablesQuery.php
@@ -0,0 +1,18 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for runtimevariables table
+ */
+class RuntimevariablesQuery extends IdoQuery
+{
+ protected $columnMap = array(
+ 'runtimevariables' => array(
+ 'id' => 'runtimevariable_id',
+ 'varname' => 'varname',
+ 'varvalue' => 'varvalue'
+ )
+ );
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
new file mode 100644
index 0000000..cae11bc
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
@@ -0,0 +1,218 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for service comments
+ */
+class ServicecommentQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('comments' => array('c.comment_id', 'so.object_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hostgroups', 'servicegroups');
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'comments' => array(
+ 'comment_author' => 'c.author_name COLLATE latin1_general_ci',
+ 'comment_author_name' => 'c.author_name',
+ 'comment_data' => 'c.comment_data',
+ 'comment_expiration' => 'CASE c.expires WHEN 1 THEN UNIX_TIMESTAMP(c.expiration_time) ELSE NULL END',
+ 'comment_internal_id' => 'c.internal_comment_id',
+ 'comment_is_persistent' => 'c.is_persistent',
+ 'comment_name' => 'c.name',
+ 'comment_timestamp' => 'UNIX_TIMESTAMP(c.comment_time)',
+ 'comment_type' => "CASE c.entry_type WHEN 1 THEN 'comment' WHEN 2 THEN 'downtime' WHEN 3 THEN 'flapping' WHEN 4 THEN 'ack' END",
+ 'host' => 'so.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'so.name1',
+ 'object_type' => '(\'service\')',
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_host' => 'so.name1 COLLATE latin1_general_ci',
+ 'service_host_name' => 'so.name1'
+ ),
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'hoststatus' => array(
+ 'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci'
+ ),
+ 'servicestatus' => array(
+ 'service_state' => 'CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 99 ELSE ss.current_state END'
+ )
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ if (version_compare($this->getIdoVersion(), '1.14.0', '<')) {
+ $this->columnMap['comments']['comment_name'] = '(NULL)';
+ }
+ $this->select->from(
+ array('c' => $this->prefix . 'comments'),
+ array()
+ )->join(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = c.object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ $this->joinedVirtualTables['comments'] = true;
+ }
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = s.host_object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = s.host_object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join host status
+ */
+ protected function joinHoststatus()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->join(
+ array('hs' => $this->prefix . 'hoststatus'),
+ 'hs.host_object_id = s.host_object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = so.object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sgm.servicegroup_id = sg.' . $this->servicegroup_id,
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = c.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->join(
+ array('s' => $this->prefix . 'services'),
+ 's.service_object_id = so.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service status
+ */
+ protected function joinServicestatus()
+ {
+ $this->select->join(
+ array('ss' => $this->prefix . 'servicestatus'),
+ 'ss.service_object_id = so.object_id',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $this->requireVirtualTable('services');
+
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 's.host_object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('members');
+
+ return ['sgm.service_object_id', 'so.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentdeletionhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentdeletionhistoryQuery.php
new file mode 100644
index 0000000..33aaa25
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentdeletionhistoryQuery.php
@@ -0,0 +1,44 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for service comment removal records
+ */
+class ServicecommentdeletionhistoryQuery extends ServicecommenthistoryQuery
+{
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') {
+ $this->requireColumn('timestamp');
+ $filter->setColumn('sch.deletion_time');
+ $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression())));
+ return null;
+ }
+
+ return parent::requireFilterColumns($filter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ parent::joinBaseTables();
+ $this->select->where("sch.deletion_time > '1970-01-02 00:00:00'");
+ $this->columnMap['commenthistory']['timestamp'] = str_replace(
+ 'comment_time',
+ 'deletion_time',
+ $this->columnMap['commenthistory']['timestamp']
+ );
+ $this->columnMap['commenthistory']['type'] = str_replace(
+ 'END)',
+ "END || '_deleted')",
+ $this->columnMap['commenthistory']['type']
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php
new file mode 100644
index 0000000..b3e9c16
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php
@@ -0,0 +1,195 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for service comment history records
+ */
+class ServicecommenthistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('commenthistory' => array('sch.commenthistory_id', 'so.object_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hostgroups', 'servicegroups', 'services');
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'commenthistory' => array(
+ 'id' => 'sch.commenthistory_id',
+ 'host' => 'so.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'so.name1',
+ 'object_id' => 'sch.object_id',
+ 'object_type' => '(\'service\')',
+ 'output' => "('[' || sch.author_name || '] ' || sch.comment_data)",
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_host' => 'so.name1 COLLATE latin1_general_ci',
+ 'service_host_name' => 'so.name1',
+ 'state' => '(-1)',
+ 'timestamp' => 'UNIX_TIMESTAMP(sch.comment_time)',
+ 'type' => "(CASE sch.entry_type WHEN 1 THEN 'comment' WHEN 2 THEN 'dt_comment' WHEN 3 THEN 'flapping' WHEN 4 THEN 'ack' END)"
+ ),
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci'
+ )
+ );
+
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') {
+ $this->requireColumn('timestamp');
+ $filter->setColumn('sch.comment_time');
+ $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression())));
+ return null;
+ }
+
+ return parent::requireFilterColumns($filter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ array('sch' => $this->prefix . 'commenthistory'),
+ array()
+ )->join(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = sch.object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ $this->joinedVirtualTables['commenthistory'] = true;
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = s.host_object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = s.host_object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = sch.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = so.object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id',
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->join(
+ array('s' => $this->prefix . 'services'),
+ 's.service_object_id = so.object_id',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('services');
+
+ return ['so.object_id', 'so.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('members');
+
+ return ['sgm.service_object_id', 'so.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php
new file mode 100644
index 0000000..0a46709
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php
@@ -0,0 +1,235 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for service contacts
+ */
+class ServicecontactQuery extends IdoQuery
+{
+ protected $allowCustomVars = true;
+
+ protected $groupBase = [
+ 'contacts' => ['co.object_id', 'c.contact_id'],
+ 'timeperiods' => ['ht.timeperiod_id', 'st.timeperiod_id']
+ ];
+
+ protected $groupOrigin = ['contactgroups', 'hosts', 'services'];
+
+ protected $subQueryTargets = [
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ ];
+
+ protected $columnMap = [
+ 'contactgroups' => [
+ 'contactgroup' => 'cgo.name1 COLLATE latin1_general_ci',
+ 'contactgroup_name' => 'cgo.name1',
+ 'contactgroup_alias' => 'cg.alias COLLATE latin1_general_ci'
+ ],
+ 'contacts' => [
+ 'contact_id' => 'c.contact_id',
+ 'contact' => 'co.name1 COLLATE latin1_general_ci',
+ 'contact_name' => 'co.name1',
+ 'contact_alias' => 'c.alias COLLATE latin1_general_ci',
+ 'contact_email' => 'c.email_address COLLATE latin1_general_ci',
+ 'contact_pager' => 'c.pager_address',
+ 'contact_object_id' => 'c.contact_object_id',
+ 'contact_has_host_notfications' => 'c.host_notifications_enabled',
+ 'contact_has_service_notfications' => 'c.service_notifications_enabled',
+ 'contact_can_submit_commands' => 'c.can_submit_commands',
+ 'contact_notify_service_recovery' => 'c.notify_service_recovery',
+ 'contact_notify_service_warning' => 'c.notify_service_warning',
+ 'contact_notify_service_critical' => 'c.notify_service_critical',
+ 'contact_notify_service_unknown' => 'c.notify_service_unknown',
+ 'contact_notify_service_flapping' => 'c.notify_service_flapping',
+ 'contact_notify_service_downtime' => 'c.notify_service_downtime',
+ 'contact_notify_host_recovery' => 'c.notify_host_recovery',
+ 'contact_notify_host_down' => 'c.notify_host_down',
+ 'contact_notify_host_unreachable' => 'c.notify_host_unreachable',
+ 'contact_notify_host_flapping' => 'c.notify_host_flapping',
+ 'contact_notify_host_downtime' => 'c.notify_host_downtime'
+ ],
+ 'hostgroups' => [
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ],
+ 'hosts' => [
+ 'host' => 'ho.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'ho.name1',
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ],
+ 'instances' => [
+ 'instance_name' => 'i.instance_name'
+ ],
+ 'servicegroups' => [
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ],
+ 'services' => [
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci',
+ 'service_host_name' => 'so.name1'
+ ],
+ 'timeperiods' => [
+ 'contact_notify_host_timeperiod' => 'ht.alias COLLATE latin1_general_ci',
+ 'contact_notify_service_timeperiod' => 'st.alias COLLATE latin1_general_ci'
+ ]
+ ];
+
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ ['c' => $this->prefix . 'contacts'],
+ []
+ )->join(
+ ['co' => $this->prefix . 'objects'],
+ 'co.object_id = c.contact_object_id AND co.is_active = 1 AND co.objecttype_id = 10',
+ []
+ );
+
+ $this->select->joinLeft(
+ ['sc' => $this->prefix . 'service_contacts'],
+ 'sc.contact_object_id = c.contact_object_id',
+ []
+ )->joinLeft(
+ ['s' => $this->prefix . 'services'],
+ 's.service_id = sc.service_id',
+ []
+ )->joinLeft(
+ ['so' => $this->prefix . 'objects'],
+ 'so.object_id = s.service_object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ []
+ );
+
+ $this->joinedVirtualTables['contacts'] = true;
+ $this->joinedVirtualTables['services'] = true;
+ }
+
+ /**
+ * Join contact groups
+ */
+ protected function joinContactgroups()
+ {
+ $this->select->joinLeft(
+ ['cgm' => $this->prefix . 'contactgroup_members'],
+ 'co.object_id = cgm.contact_object_id',
+ []
+ )->joinLeft(
+ ['cg' => $this->prefix . 'contactgroups'],
+ 'cgm.contactgroup_id = cg.contactgroup_id',
+ []
+ )->joinLeft(
+ ['cgo' => $this->prefix . 'objects'],
+ 'cg.contactgroup_object_id = cgo.object_id AND cgo.is_active = 1 AND cgo.objecttype_id = 11',
+ []
+ );
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->requireVirtualTable('hosts');
+ $this->select->joinLeft(
+ ['hgm' => $this->prefix . 'hostgroup_members'],
+ 'hgm.host_object_id = ho.object_id',
+ []
+ )->joinLeft(
+ ['hg' => $this->prefix . 'hostgroups'],
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ []
+ )->joinLeft(
+ ['hgo' => $this->prefix . 'objects'],
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ []
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->select->joinLeft(
+ ['h' => $this->prefix . 'hosts'],
+ 'h.host_object_id = s.host_object_id',
+ []
+ )->joinLeft(
+ ['ho' => $this->prefix . 'objects'],
+ 'ho.object_id = h.host_object_id AND ho.is_active = 1 AND ho.objecttype_id = 1',
+ []
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ ['i' => $this->prefix . 'instances'],
+ 'i.instance_id = c.instance_id',
+ []
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ ['sgm' => $this->prefix . 'servicegroup_members'],
+ 'sgm.service_object_id = s.service_object_id',
+ []
+ )->joinLeft(
+ ['sg' => $this->prefix . 'servicegroups'],
+ 'sg.servicegroup_id = sgm.servicegroup_id',
+ []
+ )->joinLeft(
+ ['sgo' => $this->prefix . 'objects'],
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ []
+ );
+ }
+
+ /**
+ * Join time periods
+ */
+ protected function joinTimeperiods()
+ {
+ $this->select->joinLeft(
+ ['ht' => $this->prefix . 'timeperiods'],
+ 'ht.timeperiod_object_id = c.host_timeperiod_object_id',
+ []
+ );
+ $this->select->joinLeft(
+ ['st' => $this->prefix . 'timeperiods'],
+ 'st.timeperiod_object_id = c.service_timeperiod_object_id',
+ []
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 's.host_object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('members');
+
+ return ['sgm.service_object_id', 'so.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
new file mode 100644
index 0000000..feea061
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
@@ -0,0 +1,222 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for service downtimes
+ */
+class ServicedowntimeQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('downtimes' => array('sd.scheduleddowntime_id', 'so.object_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hostgroups', 'servicegroups');
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'downtimes' => array(
+ 'downtime_author' => 'sd.author_name COLLATE latin1_general_ci',
+ 'downtime_author_name' => 'sd.author_name',
+ 'downtime_comment' => 'sd.comment_data',
+ 'downtime_duration' => 'sd.duration',
+ 'downtime_end' => 'CASE WHEN sd.is_fixed > 0 THEN UNIX_TIMESTAMP(sd.scheduled_end_time) ELSE UNIX_TIMESTAMP(sd.trigger_time) + sd.duration END',
+ 'downtime_entry_time' => 'UNIX_TIMESTAMP(sd.entry_time)',
+ 'downtime_internal_id' => 'sd.internal_downtime_id',
+ 'downtime_is_fixed' => 'sd.is_fixed',
+ 'downtime_is_flexible' => 'CASE WHEN sd.is_fixed = 0 THEN 1 ELSE 0 END',
+ 'downtime_is_in_effect' => 'sd.is_in_effect',
+ 'downtime_name' => 'sd.name',
+ 'downtime_scheduled_end' => 'UNIX_TIMESTAMP(sd.scheduled_end_time)',
+ 'downtime_scheduled_start' => 'UNIX_TIMESTAMP(sd.scheduled_start_time)',
+ 'downtime_start' => 'UNIX_TIMESTAMP(CASE WHEN UNIX_TIMESTAMP(sd.trigger_time) > 0 then sd.trigger_time ELSE sd.scheduled_start_time END)',
+ 'downtime_triggered_by_id' => 'sd.triggered_by_id',
+ 'host' => 'so.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'so.name1',
+ 'object_type' => '(\'service\')',
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_host' => 'so.name1 COLLATE latin1_general_ci',
+ 'service_host_name' => 'so.name1'
+ ),
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ),
+ 'hoststatus' => array(
+ 'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci'
+ ),
+ 'servicestatus' => array(
+ 'service_state' => 'CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 99 ELSE ss.current_state END'
+ )
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ if (version_compare($this->getIdoVersion(), '1.14.0', '<')) {
+ $this->columnMap['downtimes']['downtime_name'] = '(NULL)';
+ }
+ $this->select->from(
+ array('sd' => $this->prefix . 'scheduleddowntime'),
+ array()
+ )->join(
+ array('so' => $this->prefix . 'objects'),
+ 'sd.object_id = so.object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ $this->joinedVirtualTables['downtimes'] = true;
+ }
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = s.host_object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = s.host_object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join host status
+ */
+ protected function joinHoststatus()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->join(
+ array('hs' => $this->prefix . 'hoststatus'),
+ 'hs.host_object_id = s.host_object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = sd.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = so.object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sgm.servicegroup_id = sg.' . $this->servicegroup_id,
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->join(
+ array('s' => $this->prefix . 'services'),
+ 's.service_object_id = so.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service status
+ */
+ protected function joinServicestatus()
+ {
+ $this->select->join(
+ array('ss' => $this->prefix . 'servicestatus'),
+ 'ss.service_object_id = so.object_id',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('services');
+
+ return ['so.object_id', 'so.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('members');
+
+ return ['sgm.service_object_id', 'so.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeendhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeendhistoryQuery.php
new file mode 100644
index 0000000..6243829
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeendhistoryQuery.php
@@ -0,0 +1,42 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for host downtime end history records
+ */
+class ServicedowntimeendhistoryQuery extends ServicedowntimestarthistoryQuery
+{
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') {
+ $this->requireColumn('timestamp');
+ $filter->setColumn('sdh.actual_end_time');
+ $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression())));
+ return null;
+ }
+
+ return parent::requireFilterColumns($filter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ parent::joinBaseTables(true);
+ $this->select->where(
+ "sdh.actual_end_time > '1970-01-02 00:00:00' AND sdh.was_started = 1 AND sdh.was_cancelled = 0"
+ );
+ $this->columnMap['downtimehistory']['type'] = "('dt_end')";
+ $this->columnMap['downtimehistory']['timestamp'] = str_replace(
+ 'actual_start_time',
+ 'actual_end_time',
+ $this->columnMap['downtimehistory']['timestamp']
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
new file mode 100644
index 0000000..b8805fe
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
@@ -0,0 +1,205 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for service downtime start history records
+ */
+class ServicedowntimestarthistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('downtimehistory' => array('sdh.downtimehistory_id', 'so.object_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hostgroups', 'servicegroups');
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'downtimehistory' => array(
+ 'id' => 'sdh.downtimehistory_id',
+ 'host' => 'so.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'so.name1',
+ 'object_id' => 'sdh.object_id',
+ 'object_type' => '(\'service\')',
+ 'output' => "('[' || sdh.author_name || '] ' || sdh.comment_data)",
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_host' => 'so.name1 COLLATE latin1_general_ci',
+ 'service_host_name' => 'so.name1',
+ 'state' => '(-1)',
+ 'timestamp' => 'UNIX_TIMESTAMP(sdh.actual_start_time)',
+ 'type' => "('dt_start')"
+ ),
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci'
+ )
+ );
+
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') {
+ $this->requireColumn('timestamp');
+ $filter->setColumn('sdh.actual_start_time');
+ $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression())));
+ return null;
+ }
+
+ return parent::requireFilterColumns($filter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ array('sdh' => $this->prefix . 'downtimehistory'),
+ array()
+ )->join(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = sdh.object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+
+ if (func_num_args() === 0 || func_get_arg(0) === false) {
+ $this->select->where(
+ "sdh.actual_start_time > '1970-01-02 00:00:00'"
+ );
+ }
+ $this->select->where(
+ "sdh.was_started = 1 AND sdh.was_cancelled = 0"
+ );
+
+ $this->joinedVirtualTables['downtimehistory'] = true;
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = s.host_object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = s.host_object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = sdh.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = so.object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id',
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->join(
+ array('s' => $this->prefix . 'services'),
+ 's.service_object_id = so.object_id',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('services');
+
+ return ['so.object_id', 'so.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('members');
+
+ return ['sgm.service_object_id', 'so.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingendhistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingendhistoryQuery.php
new file mode 100644
index 0000000..48fb0bc
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingendhistoryQuery.php
@@ -0,0 +1,31 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for service flapping end history records
+ */
+class ServiceflappingendhistoryQuery extends ServiceflappingstarthistoryQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ array('sfh' => $this->prefix . 'flappinghistory'),
+ array()
+ )->join(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = sfh.object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+
+ $this->select->where('sfh.event_type = 1001');
+
+ $this->joinedVirtualTables['flappinghistory'] = true;
+
+ $this->columnMap['flappinghistory']['type'] = '(\'flapping_deleted\')';
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php
new file mode 100644
index 0000000..f068681
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php
@@ -0,0 +1,197 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for service flapping start history records
+ */
+class ServiceflappingstarthistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('flappinghistory' => array('sfh.flappinghistory_id', 'so.object_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hostgroups', 'servicegroups');
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'flappinghistory' => array(
+ 'id' => 'sfh.flappinghistory_id',
+ 'host' => 'so.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'so.name1',
+ 'object_id' => 'sfh.object_id',
+ 'object_type' => '(\'service\')',
+ 'output' => '(sfh.percent_state_change || \'\')',
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_host_name' => 'so.name1',
+ 'state' => '(-1)',
+ 'timestamp' => 'UNIX_TIMESTAMP(sfh.event_time)',
+ 'type' => "('flapping')"
+ ),
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci'
+ )
+ );
+
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression && $filter->getColumn() === 'timestamp') {
+ $this->requireColumn('timestamp');
+ $filter->setColumn('sfh.event_time');
+ $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression())));
+ return null;
+ }
+
+ return parent::requireFilterColumns($filter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ array('sfh' => $this->prefix . 'flappinghistory'),
+ array()
+ )->join(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = sfh.object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+
+ $this->select->where('sfh.event_type = 1000');
+
+ $this->joinedVirtualTables['flappinghistory'] = true;
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = s.host_object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = s.host_object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = sfh.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = so.object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id',
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->join(
+ array('s' => $this->prefix . 'services'),
+ 's.service_object_id = so.object_id',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('services');
+
+ return ['so.object_id', 'so.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('members');
+
+ return ['sgm.service_object_id', 'so.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
new file mode 100644
index 0000000..7f7be50
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
@@ -0,0 +1,303 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+class ServicegroupQuery extends IdoQuery
+{
+ protected $groupBase = array(
+ 'servicegroups' => array('sgo.object_id', 'sg.servicegroup_id'),
+ 'servicestatus' => array('ss.servicestatus_id', 'hs.hoststatus_id')
+ );
+
+ protected $groupOrigin = array('members');
+
+ protected $allowCustomVars = true;
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ protected $columnMap = array(
+ 'contacts' => [
+ 'service_contact' => 'sco.name1'
+ ],
+ 'contactgroups' => [
+ 'service_contactgroup' => 'scgo.name1'
+ ],
+ 'hostcontacts' => [
+ 'host_contact' => 'hco.name1'
+ ],
+ 'hostcontactgroups' => [
+ 'host_contactgroup' => 'hcgo.name1'
+ ],
+ 'hostgroups' => array(
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'h.host_object_id' => 's.host_object_id'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'members' => array(
+ 'host_name' => 'so.name1',
+ 'service_description' => 'so.name2'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1'
+ ),
+ 'servicestatus' => array(
+ 'service_handled' => 'CASE WHEN (ss.problem_has_been_acknowledged + ss.scheduled_downtime_depth + COALESCE(hs.current_state, 0)) > 0 THEN 1 ELSE 0 END',
+ 'service_severity' => '
+ CASE WHEN ss.current_state = 0
+ THEN
+ CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL
+ THEN 16
+ ELSE 0
+ END
+ +
+ CASE WHEN ss.problem_has_been_acknowledged = 1
+ THEN 2
+ ELSE
+ CASE WHEN ss.scheduled_downtime_depth > 0
+ THEN 1
+ ELSE 4
+ END
+ END
+ ELSE
+ CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 16
+ WHEN ss.current_state = 1 THEN 32
+ WHEN ss.current_state = 2 THEN 128
+ WHEN ss.current_state = 3 THEN 64
+ ELSE 256
+ END
+ +
+ CASE WHEN hs.current_state > 0
+ THEN 1024
+ ELSE
+ CASE WHEN ss.problem_has_been_acknowledged = 1
+ THEN 512
+ ELSE
+ CASE WHEN ss.scheduled_downtime_depth > 0
+ THEN 256
+ ELSE 2048
+ END
+ END
+ END
+ END',
+ 'service_state' => 'CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 99 ELSE ss.current_state END'
+ )
+ );
+
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ array('sgo' => $this->prefix . 'objects'),
+ array()
+ )->join(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sg.servicegroup_object_id = sgo.object_id AND sgo.objecttype_id = 4 AND sgo.is_active = 1',
+ array()
+ );
+ $this->joinedVirtualTables = array('servicegroups' => true);
+ }
+
+ /**
+ * Join contacts
+ */
+ protected function joinContacts()
+ {
+ $this->requireVirtualTable('services');
+
+ $this->select->joinLeft(
+ ['sc' => 'icinga_service_contacts'],
+ 'sc.service_id = s.service_id',
+ []
+ )->joinLeft(
+ ['sco' => 'icinga_objects'],
+ 'sco.object_id = sc.contact_object_id AND sco.is_active = 1 AND sco.objecttype_id = 10',
+ []
+ );
+ }
+
+ /**
+ * Join contact groups
+ */
+ protected function joinContactgroups()
+ {
+ $this->requireVirtualTable('services');
+
+ $this->select->joinLeft(
+ ['scg' => 'icinga_service_contactgroups'],
+ 'scg.service_id = s.service_id',
+ []
+ )->joinLeft(
+ ['scgo' => 'icinga_objects'],
+ 'scgo.object_id = scg.contactgroup_object_id AND scgo.is_active = 1 AND scgo.objecttype_id = 11',
+ []
+ );
+ }
+
+ /**
+ * Join host contacts
+ */
+ protected function joinHostcontacts()
+ {
+ $this->requireVirtualTable('services');
+
+ $this->select->joinLeft(
+ ['h' => 'icinga_hosts'],
+ 'h.host_object_id = s.host_object_id',
+ []
+ )->joinLeft(
+ ['hc' => 'icinga_host_contacts'],
+ 'hc.host_id = h.host_id',
+ []
+ )->joinLeft(
+ ['hco' => 'icinga_objects'],
+ 'hco.object_id = hc.contact_object_id AND hco.is_active = 1 AND hco.objecttype_id = 10',
+ []
+ );
+ }
+
+ /**
+ * Join host contact groups
+ */
+ protected function joinHostcontactgroups()
+ {
+ $this->requireVirtualTable('services');
+
+ $this->select->joinLeft(
+ ['h' => 'icinga_hosts'],
+ 'h.host_object_id = s.host_object_id',
+ []
+ )->joinLeft(
+ ['hcg' => 'icinga_host_contactgroups'],
+ 'hcg.host_id = h.host_id',
+ []
+ )->joinLeft(
+ ['hcgo' => 'icinga_objects'],
+ 'hcgo.object_id = hcg.contactgroup_object_id AND hcgo.is_active = 1 AND hcgo.objecttype_id = 11',
+ []
+ );
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = s.host_object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.objecttype_id = 3 AND hgo.is_active = 1',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ *
+ * This is required to make filters work which filter by host custom variables.
+ */
+ protected function joinHosts()
+ {
+ $this->requireVirtualTable('services');
+
+ // Host custom var filters work w/o any host related table. If a host table join is necessary here some day,
+ // please adjust `joinHostcontact*()` where we explicitly do this already
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = sg.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service objects
+ */
+ protected function joinMembers()
+ {
+ $this->select->join(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.servicegroup_id = sg.servicegroup_id',
+ array()
+ )->join(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = sgm.service_object_id AND so.objecttype_id = 2 AND so.is_active = 1',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->requireVirtualTable('members');
+ $this->select->join(
+ array('s' => $this->prefix . 'services'),
+ 's.service_object_id = so.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service status
+ */
+ protected function joinServicestatus()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->join(
+ array('hs' => $this->prefix . 'hoststatus'),
+ 'hs.host_object_id = s.host_object_id',
+ array()
+ );
+ $this->select->join(
+ array('ss' => $this->prefix . 'servicestatus'),
+ 'ss.service_object_id = so.object_id',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $this->requireVirtualTable('members');
+
+ $query->joinVirtualTable('services');
+
+ return ['so.object_id', 'so.object_id'];
+ } elseif ($name === 'servicegroup') {
+ // Propagate that the "parent" query has to be filtered as well
+ $additionalFilter = clone $filter;
+
+ $this->requireVirtualTable('members');
+
+ $query->joinVirtualTable('members');
+
+ return ['sgm.service_object_id', 'so.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupsummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupsummaryQuery.php
new file mode 100644
index 0000000..11b62d0
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupsummaryQuery.php
@@ -0,0 +1,113 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Zend_Db_Expr;
+use Zend_Db_Select;
+
+/**
+ * Query for service group summary
+ */
+class ServicegroupsummaryQuery extends IdoQuery
+{
+
+ protected $allowCustomVars = true;
+
+ protected $columnMap = array(
+ 'servicegroupsummary' => array(
+ 'servicegroup_alias' => 'servicegroup_alias',
+ 'servicegroup_name' => 'servicegroup_name',
+ 'services_critical' => 'SUM(CASE WHEN service_state = 2 THEN 1 ELSE 0 END)',
+ 'services_critical_handled' => 'SUM(CASE WHEN service_state = 2 AND service_handled = 1 THEN 1 ELSE 0 END)',
+ 'services_critical_unhandled' => 'SUM(CASE WHEN service_state = 2 AND service_handled = 0 THEN 1 ELSE 0 END)',
+ 'services_ok' => 'SUM(CASE WHEN service_state = 0 THEN 1 ELSE 0 END)',
+ 'services_pending' => 'SUM(CASE WHEN service_state = 99 THEN 1 ELSE 0 END)',
+ 'services_severity' => 'MAX(service_severity)',
+ 'services_total' => 'SUM(1)',
+ 'services_unknown' => 'SUM(CASE WHEN service_state = 3 THEN 1 ELSE 0 END)',
+ 'services_unknown_handled' => 'SUM(CASE WHEN service_state = 3 AND service_handled = 1 THEN 1 ELSE 0 END)',
+ 'services_unknown_unhandled' => 'SUM(CASE WHEN service_state = 3 AND service_handled = 0 THEN 1 ELSE 0 END)',
+ 'services_warning' => 'SUM(CASE WHEN service_state = 1 THEN 1 ELSE 0 END)',
+ 'services_warning_handled' => 'SUM(CASE WHEN service_state = 1 AND service_handled = 1 THEN 1 ELSE 0 END)',
+ 'services_warning_unhandled' => 'SUM(CASE WHEN service_state = 1 AND service_handled = 0 THEN 1 ELSE 0 END)',
+ )
+ );
+
+ /**
+ * The union
+ *
+ * @var Zend_Db_Select
+ */
+ protected $summaryQuery;
+
+ /**
+ * Subqueries used for the summary query
+ *
+ * @var IdoQuery[]
+ */
+ protected $subQueries = [];
+
+ /**
+ * Count query
+ *
+ * @var IdoQuery
+ */
+ protected $countQuery;
+
+ public function addFilter(Filter $filter)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+ $this->countQuery->applyFilter(clone $filter);
+ return $this;
+ }
+
+ protected function joinBaseTables()
+ {
+ $this->countQuery = $this->createSubQuery(
+ 'Servicegroup',
+ array()
+ );
+ $subQuery = $this->createSubQuery(
+ 'Servicegroup',
+ array(
+ 'servicegroup_alias',
+ 'servicegroup_name',
+ 'service_handled',
+ 'service_severity',
+ 'service_state'
+ )
+ );
+ $this->subQueries[] = $subQuery;
+ $emptyGroups = $this->createSubQuery(
+ 'Emptyservicegroup',
+ [
+ 'servicegroup_alias',
+ 'servicegroup_name',
+ 'service_handled' => new Zend_Db_Expr('NULL'),
+ 'service_severity' => new Zend_Db_Expr('0'),
+ 'service_state' => new Zend_Db_Expr('NULL'),
+ ]
+ );
+ $this->subQueries[] = $emptyGroups;
+ $this->summaryQuery = $this->db->select()->union(
+ [$subQuery, $emptyGroups],
+ Zend_Db_Select::SQL_UNION_ALL
+ );
+ $this->select->from(['servicesgroupsummary' => $this->summaryQuery], []);
+ $this->group(['servicegroup_name', 'servicegroup_alias']);
+ $this->joinedVirtualTables['servicegroupsummary'] = true;
+ }
+
+ public function getCountQuery()
+ {
+ $count = $this->countQuery->select();
+ $this->countQuery->applyFilterSql($count);
+ $count->columns(array('sgo.object_id'));
+ $count->group(array('sgo.object_id'));
+ return $this->db->select()->from($count, array('cnt' => 'COUNT(*)'));
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
new file mode 100644
index 0000000..1159e0c
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
@@ -0,0 +1,287 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for service notifications
+ */
+class ServicenotificationQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'contactnotifications' => array(
+ 'notification_contact_name' => 'co.name1'
+ ),
+ 'history' => array(
+ 'output' => null,
+ 'state' => 'sn.state',
+ 'timestamp' => 'UNIX_TIMESTAMP(sn.start_time)',
+ 'type' => '
+ CASE sn.notification_reason
+ WHEN 1 THEN \'notification_ack\'
+ WHEN 2 THEN \'notification_flapping\'
+ WHEN 3 THEN \'notification_flapping_end\'
+ WHEN 5 THEN \'notification_dt_start\'
+ WHEN 6 THEN \'notification_dt_end\'
+ WHEN 7 THEN \'notification_dt_end\'
+ WHEN 8 THEN \'notification_custom\'
+ ELSE \'notification_state\'
+ END',
+ ),
+ 'hostgroups' => array(
+ 'hostgroup_name' => 'hgo.name1',
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ ),
+ 'hosts' => array(
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci',
+ 'host_alias' => 'h.alias COLLATE latin1_general_ci',
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'notifications' => array(
+ 'id' => 'sn.notification_id',
+ 'host' => 'so.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'so.name1',
+ 'notification_output' => 'sn.output',
+ 'notification_reason' => 'sn.notification_reason',
+ 'notification_state' => 'sn.state',
+ 'notification_timestamp' => 'UNIX_TIMESTAMP(sn.start_time)',
+ 'object_type' => '(\'service\')',
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_host_name' => 'so.name1'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci'
+ )
+ );
+
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression) {
+ switch ($filter->getColumn()) {
+ case 'output':
+ $this->requireColumn('output');
+ $filter->setColumn('sn.output');
+ return null;
+ case 'timestamp':
+ case 'notification_timestamp':
+ $this->requireColumn($filter->getColumn());
+ $filter->setColumn('sn.start_time');
+ $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression())));
+ return null;
+ }
+ }
+
+ return parent::requireFilterColumns($filter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $concattedContacts = null;
+ switch ($this->ds->getDbType()) {
+ case 'mysql':
+ $concattedContacts = "GROUP_CONCAT("
+ . "DISTINCT co.name1 ORDER BY co.name1 SEPARATOR ', '"
+ . ") COLLATE latin1_general_ci";
+ break;
+ case 'pgsql':
+ // TODO: Find a way to order the contact alias list:
+ $concattedContacts = "ARRAY_TO_STRING(ARRAY_AGG(DISTINCT co.name1), ', ')";
+ break;
+ }
+ $this->columnMap['history']['output'] = "('[' || $concattedContacts || '] ' || sn.output)";
+
+ $this->select->from(
+ array('sn' => $this->prefix . 'notifications'),
+ array()
+ )->join(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = sn.object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ $this->joinedVirtualTables['notifications'] = true;
+ }
+
+ /**
+ * Join virtual table history
+ */
+ protected function joinHistory()
+ {
+ $this->requireVirtualTable('contactnotifications');
+ }
+
+ /**
+ * Join contact notifications
+ */
+ protected function joinContactnotifications()
+ {
+ $this->select->joinLeft(
+ array('cn' => $this->prefix . 'contactnotifications'),
+ 'cn.notification_id = sn.notification_id',
+ array()
+ );
+ $this->select->joinLeft(
+ array('co' => $this->prefix . 'objects'),
+ 'co.object_id = cn.contact_object_id AND co.is_active = 1 AND co.objecttype_id = 10',
+ array()
+ );
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = s.host_object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = s.host_object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = so.object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id',
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->join(
+ array('s' => $this->prefix . 'services'),
+ 's.service_object_id = so.object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = sn.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getGroup()
+ {
+ $group = array();
+
+ if ($this->hasJoinedVirtualTable('history')
+ || $this->hasJoinedVirtualTable('hostgroups')
+ || $this->hasJoinedVirtualTable('servicegroups')
+ ) {
+ $group = array('sn.notification_id', 'so.object_id');
+ if ($this->hasJoinedVirtualTable('contactnotifications') && !$this->hasJoinedVirtualTable('history')) {
+ $group[] = 'co.object_id';
+ }
+ } elseif ($this->hasJoinedVirtualTable('contactnotifications')) {
+ $group = array('sn.notification_id', 'co.object_id', 'so.object_id');
+ }
+
+ if (! empty($group)) {
+ if ($this->hasJoinedVirtualTable('hosts')) {
+ $group[] = 'h.host_id';
+ }
+
+ if ($this->hasJoinedVirtualTable('services')) {
+ $group[] = 's.service_id';
+ }
+
+ if ($this->hasJoinedVirtualTable('instances')) {
+ $group[] = 'i.instance_id';
+ }
+ }
+
+ return $group;
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $this->requireVirtualTable('services');
+
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 's.host_object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('members');
+
+ return ['sgm.service_object_id', 'so.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
new file mode 100644
index 0000000..f93ca8a
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
@@ -0,0 +1,220 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for service state history records
+ */
+class ServicestatehistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('statehistory' => array('sh.statehistory_id', 'so.object_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hostgroups', 'servicegroups');
+
+ /**
+ * Array to map type names to type ids for query optimization
+ *
+ * @var array
+ */
+ protected $types = array(
+ 'soft_state' => 0,
+ 'hard_state' => 1
+ );
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host_alias' => 'h.alias',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci'
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'services' => array(
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci'
+ ),
+ 'statehistory' => array(
+ 'id' => 'sh.statehistory_id',
+ 'host' => 'so.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'so.name1',
+ 'object_id' => 'sh.object_id',
+ 'object_type' => '(\'service\')',
+ 'output' => '(CASE WHEN sh.state_type = 1 THEN sh.output ELSE \'[ \' || sh.current_check_attempt || \'/\' || sh.max_check_attempts || \' ] \' || sh.output END)',
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_description' => 'so.name2',
+ 'service_host' => 'so.name1 COLLATE latin1_general_ci',
+ 'service_host_name' => 'so.name1',
+ 'state' => 'sh.state',
+ 'timestamp' => 'UNIX_TIMESTAMP(sh.state_time)',
+ 'type' => "(CASE WHEN sh.state_type = 1 THEN 'hard_state' ELSE 'soft_state' END)"
+ ),
+ );
+
+ protected function requireFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterExpression) {
+ switch ($filter->getColumn()) {
+ case 'timestamp':
+ $this->requireColumn('timestamp');
+ $filter->setColumn('sh.state_time');
+ $filter->setExpression($this->timestampForSql($this->valueToTimestamp($filter->getExpression())));
+ return null;
+ case 'type':
+ if (! is_array($filter->getExpression())) {
+ $this->requireColumn('type');
+ $filter->setColumn('sh.state_type');
+ if (isset($this->types[$filter->getExpression()])) {
+ $filter->setExpression($this->types[$filter->getExpression()]);
+ } else {
+ $filter->setExpression(-1);
+ }
+
+ return null;
+ }
+ }
+ }
+
+ return parent::requireFilterColumns($filter);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->select->from(
+ array('sh' => $this->prefix . 'statehistory'),
+ array()
+ )->join(
+ array('so' => $this->prefix . 'objects'),
+ 'so.object_id = sh.object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ $this->joinedVirtualTables['statehistory'] = true;
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = s.host_object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->requireVirtualTable('services');
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = s.host_object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = sh.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = so.object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sg.' . $this->servicegroup_id . ' = sgm.servicegroup_id',
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $this->select->join(
+ array('s' => $this->prefix . 'services'),
+ 's.service_object_id = so.object_id',
+ array()
+ );
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('services');
+
+ return ['so.object_id', 'so.object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('members');
+
+ return ['sgm.service_object_id', 'so.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
new file mode 100644
index 0000000..fafa03b
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
@@ -0,0 +1,524 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for service status
+ */
+class ServicestatusQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowCustomVars = true;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupBase = array('services' => array('so.object_id', 's.service_id'));
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $groupOrigin = array('hostgroups', 'servicegroups', 'contacts', 'contactgroups');
+
+ protected $subQueryTargets = array(
+ 'hostgroups' => 'hostgroup',
+ 'servicegroups' => 'servicegroup'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'checktimeperiods' => array(
+ 'service_check_timeperiod' => 'ctp.alias COLLATE latin1_general_ci'
+ ),
+ 'contacts' => [
+ 'service_contact' => 'sco.name1'
+ ],
+ 'contactgroups' => [
+ 'service_contactgroup' => 'scgo.name1'
+ ],
+ 'hostcontacts' => [
+ 'host_contact' => 'hco.name1'
+ ],
+ 'hostcontactgroups' => [
+ 'host_contactgroup' => 'hcgo.name1'
+ ],
+ 'hostgroups' => array(
+ 'hostgroup' => 'hgo.name1 COLLATE latin1_general_ci',
+ 'hostgroup_alias' => 'hg.alias COLLATE latin1_general_ci',
+ 'hostgroup_name' => 'hgo.name1'
+ ),
+ 'hosts' => array(
+ 'host_action_url' => 'h.action_url',
+ 'host_address' => 'h.address',
+ 'host_address6' => 'h.address6',
+ 'host_alias' => 'h.alias COLLATE latin1_general_ci',
+ 'host_display_name' => 'h.display_name COLLATE latin1_general_ci',
+ 'host_icon_image' => 'h.icon_image',
+ 'host_icon_image_alt' => 'h.icon_image_alt',
+ 'host_ipv4' => 'INET_ATON(h.address)',
+ 'host_notes' => 'h.notes',
+ 'host_notes_url' => 'h.notes_url'
+ ),
+ 'hoststatus' => array(
+ 'host_acknowledged' => 'hs.problem_has_been_acknowledged',
+ 'host_acknowledgement_type' => 'hs.acknowledgement_type',
+ 'host_active_checks_enabled' => 'hs.active_checks_enabled',
+ 'host_active_checks_enabled_changed' => 'CASE WHEN hs.active_checks_enabled = h.active_checks_enabled THEN 0 ELSE 1 END',
+ 'host_attempt' => 'hs.current_check_attempt || \'/\' || hs.max_check_attempts',
+ 'host_check_command' => 'hs.check_command',
+ 'host_check_execution_time' => 'hs.execution_time',
+ 'host_check_latency' => 'hs.latency',
+ 'host_check_source' => 'hs.check_source',
+ 'host_check_timeperiod_object_id' => 'hs.check_timeperiod_object_id',
+ 'host_check_type' => 'hs.check_type',
+ 'host_current_check_attempt' => 'hs.current_check_attempt',
+ 'host_current_notification_number' => 'hs.current_notification_number',
+ 'host_event_handler' => 'hs.event_handler',
+ 'host_event_handler_enabled' => 'hs.event_handler_enabled',
+ 'host_event_handler_enabled_changed' => 'CASE WHEN hs.event_handler_enabled = h.event_handler_enabled THEN 0 ELSE 1 END',
+ 'host_failure_prediction_enabled' => 'hs.failure_prediction_enabled',
+ 'host_flap_detection_enabled' => 'hs.flap_detection_enabled',
+ 'host_flap_detection_enabled_changed' => 'CASE WHEN hs.flap_detection_enabled = h.flap_detection_enabled THEN 0 ELSE 1 END',
+ 'host_handled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) > 0 THEN 1 ELSE 0 END',
+ 'host_hard_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE CASE WHEN hs.state_type = 1 THEN hs.current_state ELSE hs.last_hard_state END END',
+ 'host_in_downtime' => 'CASE WHEN (hs.scheduled_downtime_depth = 0) THEN 0 ELSE 1 END',
+ 'host_is_flapping' => 'hs.is_flapping',
+ 'host_is_reachable' => 'hs.is_reachable',
+ 'host_last_check' => 'UNIX_TIMESTAMP(hs.last_check)',
+ 'host_last_hard_state' => 'hs.last_hard_state',
+ 'host_last_hard_state_change' => 'UNIX_TIMESTAMP(hs.last_hard_state_change)',
+ 'host_last_notification' => 'UNIX_TIMESTAMP(hs.last_notification)',
+ 'host_last_state_change' => 'UNIX_TIMESTAMP(hs.last_state_change)',
+ 'host_last_time_down' => 'UNIX_TIMESTAMP(hs.last_time_down)',
+ 'host_last_time_unreachable' => 'UNIX_TIMESTAMP(hs.last_time_unreachable)',
+ 'host_last_time_up' => 'UNIX_TIMESTAMP(hs.last_time_up)',
+ 'host_long_output' => 'hs.long_output',
+ 'host_max_check_attempts' => 'hs.max_check_attempts',
+ 'host_modified_host_attributes' => 'hs.modified_host_attributes',
+ 'host_next_check' => 'CASE hs.should_be_scheduled WHEN 1 THEN UNIX_TIMESTAMP(hs.next_check) ELSE NULL END',
+ 'host_next_notification' => 'UNIX_TIMESTAMP(hs.next_notification)',
+ 'host_no_more_notifications' => 'hs.no_more_notifications',
+ 'host_normal_check_interval' => 'hs.normal_check_interval',
+ 'host_notifications_enabled' => 'hs.notifications_enabled',
+ 'host_notifications_enabled_changed' => 'CASE WHEN hs.notifications_enabled = h.notifications_enabled THEN 0 ELSE 1 END',
+ 'host_obsessing' => 'hs.obsess_over_host',
+ 'host_obsessing_changed' => 'CASE WHEN hs.obsess_over_host = h.obsess_over_host THEN 0 ELSE 1 END',
+ 'host_output' => 'hs.output',
+ 'host_passive_checks_enabled' => 'hs.passive_checks_enabled',
+ 'host_passive_checks_enabled_changed' => 'CASE WHEN hs.passive_checks_enabled = h.passive_checks_enabled THEN 0 ELSE 1 END',
+ 'host_percent_state_change' => 'hs.percent_state_change',
+ 'host_perfdata' => 'hs.perfdata',
+ 'host_problem' => 'CASE WHEN COALESCE(hs.current_state, 0) = 0 THEN 0 ELSE 1 END',
+ 'host_problem_has_been_acknowledged' => 'hs.problem_has_been_acknowledged',
+ 'host_process_performance_data' => 'hs.process_performance_data',
+ 'host_retry_check_interval' => 'hs.retry_check_interval',
+ 'host_scheduled_downtime_depth' => 'hs.scheduled_downtime_depth',
+ 'host_severity' => '
+ CASE
+ WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL
+ THEN 16
+ ELSE
+ CASE
+ WHEN hs.current_state = 0
+ THEN 1
+ ELSE
+ CASE
+ WHEN hs.current_state = 1 THEN 64
+ WHEN hs.current_state = 2 THEN 32
+ ELSE 256
+ END
+ +
+ CASE
+ WHEN hs.problem_has_been_acknowledged = 1 THEN 2
+ WHEN hs.scheduled_downtime_depth > 0 THEN 1
+ ELSE 256
+ END
+ END
+ END',
+ 'host_state' => 'CASE WHEN hs.has_been_checked = 0 OR hs.has_been_checked IS NULL THEN 99 ELSE hs.current_state END',
+ 'host_state_type' => 'hs.state_type',
+ 'host_status_update_time' => 'hs.status_update_time',
+ 'host_unhandled' => 'CASE WHEN (hs.problem_has_been_acknowledged + hs.scheduled_downtime_depth) = 0 THEN 1 ELSE 0 END'
+
+ ),
+ 'instances' => array(
+ 'instance_name' => 'i.instance_name'
+ ),
+ 'services' => array(
+ 'host' => 'so.name1 COLLATE latin1_general_ci',
+ 'host_name' => 'so.name1',
+ 'object_type' => '(\'service\')',
+ 'service' => 'so.name2 COLLATE latin1_general_ci',
+ 'service_action_url' => 's.action_url',
+ 'service_check_interval' => '(s.check_interval * 60)',
+ 'service_description' => 'so.name2',
+ 'service_display_name' => 's.display_name COLLATE latin1_general_ci',
+ 'service_host' => 'so.name1 COLLATE latin1_general_ci',
+ 'service_host_name' => 'so.name1',
+ 'service_icon_image' => 's.icon_image',
+ 'service_icon_image_alt' => 's.icon_image_alt',
+ 'service_notes_url' => 's.notes_url',
+ 'service_notes' => 's.notes'
+ ),
+ 'servicegroups' => array(
+ 'servicegroup' => 'sgo.name1 COLLATE latin1_general_ci',
+ 'servicegroup_name' => 'sgo.name1',
+ 'servicegroup_alias' => 'sg.alias COLLATE latin1_general_ci'
+ ),
+ 'servicestatus' => array(
+ 'service_acknowledged' => 'ss.problem_has_been_acknowledged',
+ 'service_acknowledgement_type' => 'ss.acknowledgement_type',
+ 'service_active_checks_enabled' => 'ss.active_checks_enabled',
+ 'service_active_checks_enabled_changed' => 'CASE WHEN ss.active_checks_enabled=s.active_checks_enabled THEN 0 ELSE 1 END',
+ 'service_attempt' => 'ss.current_check_attempt || \'/\' || ss.max_check_attempts',
+ 'service_check_command' => 'ss.check_command',
+ 'service_check_execution_time' => 'ss.execution_time',
+ 'service_check_latency' => 'ss.latency',
+ 'service_check_source' => 'ss.check_source',
+ 'service_check_timeperiod_object_id' => 'ss.check_timeperiod_object_id',
+ 'service_check_type' => 'ss.check_type',
+ 'service_current_check_attempt' => 'ss.current_check_attempt',
+ 'service_current_notification_number' => 'ss.current_notification_number',
+ 'service_event_handler' => 'ss.event_handler',
+ 'service_event_handler_enabled' => 'ss.event_handler_enabled',
+ 'service_event_handler_enabled_changed' => 'CASE WHEN ss.event_handler_enabled=s.event_handler_enabled THEN 0 ELSE 1 END',
+ 'service_failure_prediction_enabled' => 'ss.failure_prediction_enabled',
+ 'service_flap_detection_enabled' => 'ss.flap_detection_enabled',
+ 'service_flap_detection_enabled_changed' => 'CASE WHEN ss.flap_detection_enabled=s.flap_detection_enabled THEN 0 ELSE 1 END',
+ 'service_handled' => 'CASE WHEN (ss.problem_has_been_acknowledged + ss.scheduled_downtime_depth + COALESCE(hs.current_state, 0)) > 0 THEN 1 ELSE 0 END',
+ 'service_hard_state' => 'CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 99 ELSE CASE WHEN ss.state_type = 1 THEN ss.current_state ELSE ss.last_hard_state END END',
+ 'service_in_downtime' => 'CASE WHEN (ss.scheduled_downtime_depth = 0 OR ss.scheduled_downtime_depth IS NULL) THEN 0 ELSE 1 END',
+ 'service_is_flapping' => 'ss.is_flapping',
+ 'service_is_passive_checked' => 'CASE WHEN ss.active_checks_enabled = 0 AND ss.passive_checks_enabled = 1 THEN 1 ELSE 0 END',
+ 'service_is_reachable' => 'ss.is_reachable',
+ 'service_last_check' => 'UNIX_TIMESTAMP(ss.last_check)',
+ 'service_last_hard_state' => 'ss.last_hard_state',
+ 'service_last_hard_state_change' => 'UNIX_TIMESTAMP(ss.last_hard_state_change)',
+ 'service_last_notification' => 'UNIX_TIMESTAMP(ss.last_notification)',
+ 'service_last_state_change' => 'UNIX_TIMESTAMP(ss.last_state_change)',
+ 'service_last_state_change_ts' => 'ss.last_state_change',
+ 'service_last_time_critical' => 'ss.last_time_critical',
+ 'service_last_time_ok' => 'ss.last_time_ok',
+ 'service_last_time_unknown' => 'ss.last_time_unknown',
+ 'service_last_time_warning' => 'ss.last_time_warning',
+ 'service_long_output' => 'ss.long_output',
+ 'service_max_check_attempts' => 'ss.max_check_attempts',
+ 'service_modified_service_attributes' => 'ss.modified_service_attributes',
+ 'service_next_check' => 'UNIX_TIMESTAMP(ss.next_check)',
+ 'service_next_notification' => 'UNIX_TIMESTAMP(ss.next_notification)',
+ 'service_next_update' => 'CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL
+ THEN
+ CASE ss.should_be_scheduled WHEN 1 THEN UNIX_TIMESTAMP(ss.next_check) + (ss.normal_check_interval * 60) ELSE NULL END
+ ELSE
+ UNIX_TIMESTAMP(ss.next_check)
+ + (CASE WHEN
+ COALESCE(ss.current_state, 0) > 0 AND ss.state_type = 0
+ THEN
+ ss.retry_check_interval
+ ELSE
+ ss.normal_check_interval
+ END * 60)
+ + (CEIL(ss.execution_time + ss.latency) * 2)
+ END',
+ 'service_no_more_notifications' => 'ss.no_more_notifications',
+ 'service_normal_check_interval' => 'ss.normal_check_interval',
+ 'service_notifications_enabled' => 'ss.notifications_enabled',
+ 'service_notifications_enabled_changed' => 'CASE WHEN ss.notifications_enabled=s.notifications_enabled THEN 0 ELSE 1 END',
+ 'service_obsessing' => 'ss.obsess_over_service',
+ 'service_obsessing_changed' => 'CASE WHEN ss.obsess_over_service=s.obsess_over_service THEN 0 ELSE 1 END',
+ 'service_output' => 'ss.output',
+ 'service_passive_checks_enabled' => 'ss.passive_checks_enabled',
+ 'service_passive_checks_enabled_changed' => 'CASE WHEN ss.passive_checks_enabled=s.passive_checks_enabled THEN 0 ELSE 1 END',
+ 'service_percent_state_change' => 'ss.percent_state_change',
+ 'service_perfdata' => 'ss.perfdata',
+ 'service_problem' => 'CASE WHEN COALESCE(ss.current_state, 0) = 0 THEN 0 ELSE 1 END',
+ 'service_problem_has_been_acknowledged' => 'ss.problem_has_been_acknowledged',
+ 'service_process_performance_data' => 'ss.process_performance_data',
+ 'service_retry_check_interval' => 'ss.retry_check_interval',
+ 'service_scheduled_downtime_depth' => 'ss.scheduled_downtime_depth',
+ 'service_severity' => 'CASE WHEN ss.current_state = 0
+ THEN
+ CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL
+ THEN 16
+ ELSE 0
+ END
+ +
+ CASE WHEN ss.problem_has_been_acknowledged = 1
+ THEN 2
+ ELSE
+ CASE WHEN ss.scheduled_downtime_depth > 0
+ THEN 1
+ ELSE 4
+ END
+ END
+ ELSE
+ CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 16
+ WHEN ss.current_state = 1 THEN 32
+ WHEN ss.current_state = 2 THEN 128
+ WHEN ss.current_state = 3 THEN 64
+ ELSE 256
+ END
+ +
+ CASE WHEN hs.current_state > 0
+ THEN 1024
+ ELSE
+ CASE WHEN ss.problem_has_been_acknowledged = 1
+ THEN 512
+ ELSE
+ CASE WHEN ss.scheduled_downtime_depth > 0
+ THEN 256
+ ELSE 2048
+ END
+ END
+ END
+ END',
+ 'service_state' => 'CASE WHEN ss.has_been_checked = 0 OR ss.has_been_checked IS NULL THEN 99 ELSE ss.current_state END',
+ 'service_state_type' => 'ss.state_type',
+ 'service_status_update_time' => 'ss.status_update_time',
+ 'service_unhandled' => 'CASE WHEN (ss.problem_has_been_acknowledged + ss.scheduled_downtime_depth + COALESCE(hs.current_state, 0)) = 0 THEN 1 ELSE 0 END',
+ 'problems' => 'CASE WHEN COALESCE(ss.current_state, 0) = 0 THEN 0 ELSE 1 END'
+ )
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ if (version_compare($this->getIdoVersion(), '1.10.0', '<')) {
+ $this->columnMap['hoststatus']['host_check_source'] = '(NULL)';
+ $this->columnMap['servicestatus']['service_check_source'] = '(NULL)';
+ }
+ if (version_compare($this->getIdoVersion(), '1.13.0', '<')) {
+ $this->columnMap['hoststatus']['host_is_reachable'] = '(NULL)';
+ $this->columnMap['servicestatus']['service_is_reachable'] = '(NULL)';
+ }
+
+ $this->select->from(
+ array('so' => $this->prefix . 'objects'),
+ array()
+ )->join(
+ array('s' => $this->prefix . 'services'),
+ 's.service_object_id = so.object_id AND so.is_active = 1 AND so.objecttype_id = 2',
+ array()
+ );
+ $this->joinedVirtualTables['services'] = true;
+ }
+
+ /**
+ * Join check time periods
+ */
+ protected function joinChecktimeperiods()
+ {
+ $this->select->joinLeft(
+ array('ctp' => $this->prefix . 'timeperiods'),
+ 'ctp.timeperiod_object_id = s.check_timeperiod_object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join contacts
+ */
+ protected function joinContacts()
+ {
+ $this->select->joinLeft(
+ ['sc' => 'icinga_service_contacts'],
+ 'sc.service_id = s.service_id',
+ []
+ )->joinLeft(
+ ['sco' => 'icinga_objects'],
+ 'sco.object_id = sc.contact_object_id AND sco.is_active = 1 AND sco.objecttype_id = 10',
+ []
+ );
+ }
+
+ /**
+ * Join contact groups
+ */
+ protected function joinContactgroups()
+ {
+ $this->select->joinLeft(
+ ['scg' => 'icinga_service_contactgroups'],
+ 'scg.service_id = s.service_id',
+ []
+ )->joinLeft(
+ ['scgo' => 'icinga_objects'],
+ 'scgo.object_id = scg.contactgroup_object_id AND scgo.is_active = 1 AND scgo.objecttype_id = 11',
+ []
+ );
+ }
+
+ /**
+ * Join host contacts
+ */
+ protected function joinHostcontacts()
+ {
+ $this->requireVirtualTable('hosts');
+
+ $this->select->joinLeft(
+ ['hc' => 'icinga_host_contacts'],
+ 'hc.host_id = h.host_id',
+ []
+ )->joinLeft(
+ ['hco' => 'icinga_objects'],
+ 'hco.object_id = hc.contact_object_id AND hco.is_active = 1 AND hco.objecttype_id = 10',
+ []
+ );
+ }
+
+ /**
+ * Join host contact groups
+ */
+ protected function joinHostcontactgroups()
+ {
+ $this->requireVirtualTable('hosts');
+
+ $this->select->joinLeft(
+ ['hcg' => 'icinga_host_contactgroups'],
+ 'hcg.host_id = h.host_id',
+ []
+ )->joinLeft(
+ ['hcgo' => 'icinga_objects'],
+ 'hcgo.object_id = hcg.contactgroup_object_id AND hcgo.is_active = 1 AND hcgo.objecttype_id = 11',
+ []
+ );
+ }
+
+ /**
+ * Join host groups
+ */
+ protected function joinHostgroups()
+ {
+ $this->select->joinLeft(
+ array('hgm' => $this->prefix . 'hostgroup_members'),
+ 'hgm.host_object_id = s.host_object_id',
+ array()
+ )->joinLeft(
+ array('hg' => $this->prefix . 'hostgroups'),
+ 'hg.hostgroup_id = hgm.hostgroup_id',
+ array()
+ )->joinLeft(
+ array('hgo' => $this->prefix . 'objects'),
+ 'hgo.object_id = hg.hostgroup_object_id AND hgo.is_active = 1 AND hgo.objecttype_id = 3',
+ array()
+ );
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $this->select->join(
+ array('h' => $this->prefix . 'hosts'),
+ 'h.host_object_id = s.host_object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join host status
+ */
+ protected function joinHoststatus()
+ {
+ $this->select->join(
+ array('hs' => $this->prefix . 'hoststatus'),
+ 'hs.host_object_id = s.host_object_id',
+ array()
+ );
+ }
+
+ /**
+ * Join instances
+ */
+ protected function joinInstances()
+ {
+ $this->select->join(
+ array('i' => $this->prefix . 'instances'),
+ 'i.instance_id = so.instance_id',
+ array()
+ );
+ }
+
+ /**
+ * Join service groups
+ */
+ protected function joinServicegroups()
+ {
+ $this->select->joinLeft(
+ array('sgm' => $this->prefix . 'servicegroup_members'),
+ 'sgm.service_object_id = so.object_id',
+ array()
+ )->joinLeft(
+ array('sg' => $this->prefix . 'servicegroups'),
+ 'sg.servicegroup_id = sgm.servicegroup_id',
+ array()
+ )->joinLeft(
+ array('sgo' => $this->prefix . 'objects'),
+ 'sgo.object_id = sg.servicegroup_object_id AND sgo.is_active = 1 AND sgo.objecttype_id = 4',
+ array()
+ );
+ }
+
+ /**
+ * Join service status
+ */
+ protected function joinServicestatus()
+ {
+ $this->requireVirtualTable('hoststatus');
+ $this->select->join(
+ array('ss' => $this->prefix . 'servicestatus'),
+ 'ss.service_object_id = so.object_id',
+ array()
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function registerGroupColumns($alias, $table, array &$groupedColumns, array &$groupedTables)
+ {
+ if ($alias === 'service_handled' || $alias === 'service_severity' || $alias === 'service_unhandled') {
+ if (! isset($groupedTables['hoststatus'])) {
+ $groupedColumns[] = 'hs.hoststatus_id';
+ $groupedTables['hoststatus'] = true;
+ }
+
+ if (! isset($groupedTables['servicestatus'])) {
+ $groupedColumns[] = 'ss.servicestatus_id';
+ $groupedTables['servicestatus'] = true;
+ }
+ } elseif ($table === 'contacts') {
+ $groupedColumns[] = 'sc.service_contact_id';
+ $groupedColumns[] = 'sco.object_id';
+ $groupedTables[$table] = true;
+ } elseif ($table === 'contactgroups') {
+ $groupedColumns[] = 'scg.service_contactgroup_id';
+ $groupedColumns[] = 'scgo.object_id';
+ $groupedTables[$table] = true;
+ } else {
+ parent::registerGroupColumns($alias, $table, $groupedColumns, $groupedTables);
+ }
+ }
+
+ protected function joinSubQuery(IdoQuery $query, $name, $filter, $and, $negate, &$additionalFilter)
+ {
+ if ($name === 'hostgroup') {
+ $query->joinVirtualTable('members');
+
+ return ['hgm.host_object_id', 's.host_object_id'];
+ } elseif ($name === 'servicegroup') {
+ $query->joinVirtualTable('members');
+
+ return ['sgm.service_object_id', 'so.object_id'];
+ }
+
+ return parent::joinSubQuery($query, $name, $filter, $and, $negate, $additionalFilter);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatussummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatussummaryQuery.php
new file mode 100644
index 0000000..4455c3f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatussummaryQuery.php
@@ -0,0 +1,104 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+
+/**
+ * Query for service status summary
+ *
+ * TODO(el): Allow to switch between hard and soft states
+ */
+class ServicestatussummaryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'servicestatussummary' => array(
+ 'services_critical' => 'SUM(CASE WHEN state = 2 THEN 1 ELSE 0 END)',
+ 'services_critical_handled' => 'SUM(CASE WHEN state = 2 AND handled = 1 THEN 1 ELSE 0 END)',
+// 'services_critical_handled_last_state_change' => 'MAX(CASE WHEN state = 2 AND handled = 1 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)',
+ 'services_critical_unhandled' => 'SUM(CASE WHEN state = 2 AND handled = 0 THEN 1 ELSE 0 END)',
+// 'services_critical_unhandled_last_state_change' => 'MAX(CASE WHEN state = 2 AND handled = 0 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)',
+ 'services_ok' => 'SUM(CASE WHEN state = 0 THEN 1 ELSE 0 END)',
+// 'services_ok_last_state_change' => 'MAX(CASE WHEN state = 0 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)',
+ 'services_pending' => 'SUM(CASE WHEN state = 99 THEN 1 ELSE 0 END)',
+// 'services_pending_last_state_change' => 'MAX(CASE WHEN state = 99 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)',
+ 'services_total' => 'SUM(1)',
+ 'services_unknown' => 'SUM(CASE WHEN state = 3 THEN 1 ELSE 0 END)',
+ 'services_unknown_handled' => 'SUM(CASE WHEN state = 3 AND handled = 1 THEN 1 ELSE 0 END)',
+// 'services_unknown_handled_last_state_change' => 'MAX(CASE WHEN state = 3 AND handled = 1 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)',
+ 'services_unknown_unhandled' => 'SUM(CASE WHEN state = 3 AND handled = 0 THEN 1 ELSE 0 END)',
+// 'services_unknown_unhandled_last_state_change' => 'MAX(CASE WHEN state = 3 AND handled = 0 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)',
+ 'services_warning' => 'SUM(CASE WHEN state = 1 THEN 1 ELSE 0 END)',
+ 'services_warning_handled' => 'SUM(CASE WHEN state = 1 AND handled = 1 THEN 1 ELSE 0 END)',
+// 'services_warning_handled_last_state_change' => 'MAX(CASE WHEN state = 1 AND handled = 1 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)',
+ 'services_warning_unhandled' => 'SUM(CASE WHEN state = 1 AND handled = 0 THEN 1 ELSE 0 END)',
+// 'services_warning_unhandled_last_state_change' => 'MAX(CASE WHEN state = 1 AND handled = 0 THEN UNIX_TIMESTAMP(last_state_change) ELSE NULL END)'
+ )
+ );
+
+ /**
+ * The service status sub select
+ *
+ * @var ServicestatusQuery
+ */
+ protected $subSelect;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function allowsCustomVars()
+ {
+ return $this->subSelect->allowsCustomVars();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ $this->subSelect->applyFilter(clone $filter);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ // TODO(el): Allow to switch between hard and soft states
+ $this->subSelect = $this->createSubQuery(
+ 'servicestatus',
+ array(
+ 'handled' => 'service_handled',
+ 'state' => 'service_state',
+ 'state_change' => 'service_last_state_change'
+ )
+ );
+ $this->select->from(
+ array('servicestatussummary' => $this->subSelect->setIsSubQuery(true)),
+ array()
+ );
+ $this->joinedVirtualTables['servicestatussummary'] = true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ $this->subSelect->where($condition, $value);
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->subSelect->whereEx($ex);
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatechangeeventQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatechangeeventQuery.php
new file mode 100644
index 0000000..18d893f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatechangeeventQuery.php
@@ -0,0 +1,41 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+/**
+ * Query for host and service state change events
+ */
+class StatechangeeventQuery extends IdoQuery
+{
+ protected $columnMap = array(
+ 'statechangeevent' => array(
+ 'statechangeevent_id' => 'sh.statehistory_id',
+ 'statechangeevent_state_time' => 'UNIX_TIMESTAMP(sh.state_time)',
+ 'statechangeevent_state_change' => 'sh.state_change',
+ 'statechangeevent_state' => 'sh.state',
+ 'statechangeevent_state_type' => "(CASE sh.state_type WHEN 0 THEN 'soft_state' WHEN 1 THEN 'hard_state' ELSE NULL END)",
+ 'statechangeevent_current_check_attempt' => 'sh.current_check_attempt',
+ 'statechangeevent_max_check_attempts' => 'sh.max_check_attempts',
+ 'statechangeevent_last_state' => 'sh.last_state',
+ 'statechangeevent_last_hard_state' => 'sh.last_hard_state',
+ 'statechangeevent_output' => 'sh.output',
+ 'statechangeevent_long_output' => 'sh.long_output',
+ 'statechangeevent_check_source' => 'sh.check_source'
+ ),
+ 'object' => array(
+ 'host_name' => 'o.name1',
+ 'service_description' => 'o.name2'
+ )
+ );
+
+ protected function joinBaseTables()
+ {
+ $this->select()
+ ->from(array('sh' => $this->prefix . 'statehistory'), array())
+ ->join(array('o' => $this->prefix . 'objects'), 'sh.object_id = o.object_id', array());
+
+ $this->joinedVirtualTables['statechangeevent'] = true;
+ $this->joinedVirtualTables['object'] = true;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php
new file mode 100644
index 0000000..56d1e3b
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php
@@ -0,0 +1,179 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\FilterExpression;
+use Zend_Db_Expr;
+use Zend_Db_Select;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for host and service state history records
+ */
+class StatehistoryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'statehistory' => array(
+ 'id' => 'sth.id',
+ 'object_type' => 'sth.object_type'
+ ),
+ 'history' => array(
+ 'type' => 'sth.type',
+ 'timestamp' => 'sth.timestamp',
+ 'object_id' => 'sth.object_id',
+ 'state' => 'sth.state',
+ 'output' => 'sth.output'
+ ),
+ 'hosts' => array(
+ 'host_display_name' => 'sth.host_display_name',
+ 'host_name' => 'sth.host_name'
+ ),
+ 'services' => array(
+ 'service_description' => 'sth.service_description',
+ 'service_display_name' => 'sth.service_display_name',
+ 'service_host_name' => 'sth.service_host_name'
+ )
+ );
+
+ /**
+ * The union
+ *
+ * @var Zend_Db_Select
+ */
+ protected $stateHistoryQuery;
+
+ /**
+ * Subqueries used for the state history query
+ *
+ * @var IdoQuery[]
+ */
+ protected $subQueries = array();
+
+ /**
+ * Whether to additionally select all history columns
+ *
+ * @var bool
+ */
+ protected $fetchHistoryColumns = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function allowsCustomVars()
+ {
+ foreach ($this->subQueries as $query) {
+ if (! $query->allowsCustomVars()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ $this->stateHistoryQuery = $this->db->select();
+ $this->select->from(
+ array('sth' => $this->stateHistoryQuery),
+ array()
+ );
+ $this->joinedVirtualTables['statehistory'] = true;
+ }
+
+ /**
+ * Join history related columns and tables
+ */
+ protected function joinHistory()
+ {
+ // TODO: Ensure that one is selecting the history columns first...
+ $this->fetchHistoryColumns = true;
+ $this->requireVirtualTable('hosts');
+ $this->requireVirtualTable('services');
+ }
+
+ /**
+ * Join hosts
+ */
+ protected function joinHosts()
+ {
+ $columns = array_keys(
+ $this->columnMap['statehistory'] + $this->columnMap['hosts']
+ );
+ foreach ($this->columnMap['services'] as $column => $_) {
+ $columns[$column] = new Zend_Db_Expr('NULL');
+ }
+ if ($this->fetchHistoryColumns) {
+ $columns = array_merge($columns, array_keys($this->columnMap['history']));
+ }
+ $hosts = $this->createSubQuery('Hoststatehistory', $columns);
+ $this->subQueries[] = $hosts;
+ $this->stateHistoryQuery->union(array($hosts), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * Join services
+ */
+ protected function joinServices()
+ {
+ $columns = array_keys(
+ $this->columnMap['statehistory'] + $this->columnMap['hosts'] + $this->columnMap['services']
+ );
+ if ($this->fetchHistoryColumns) {
+ $columns = array_merge($columns, array_keys($this->columnMap['history']));
+ }
+ $services = $this->createSubQuery('Servicestatehistory', $columns);
+ $this->subQueries[] = $services;
+ $this->stateHistoryQuery->union(array($services), Zend_Db_Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order($columnOrAlias, $dir = null)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->requireColumn($columnOrAlias);
+ }
+ return parent::order($columnOrAlias, $dir);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ $this->requireColumn($condition);
+ foreach ($this->subQueries as $sub) {
+ $sub->where($condition, $value);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->requireColumn($ex->getColumn());
+ foreach ($this->subQueries as $sub) {
+ $sub->whereEx($ex);
+ }
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatussummaryQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatussummaryQuery.php
new file mode 100644
index 0000000..b1ee9e2
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/StatussummaryQuery.php
@@ -0,0 +1,243 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\FilterExpression;
+use Zend_Db_Expr;
+use Zend_Db_Select;
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for host and service status summary
+ */
+class StatussummaryQuery extends IdoQuery
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnMap = array(
+ 'hoststatussummary' => array(
+ 'hosts_total' => 'SUM(CASE WHEN object_type = \'host\' THEN 1 ELSE 0 END)',
+ 'hosts_up' => 'SUM(CASE WHEN object_type = \'host\' AND state = 0 THEN 1 ELSE 0 END)',
+ 'hosts_up_not_checked' => 'SUM(CASE WHEN object_type = \'host\' AND state = 0 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'hosts_pending' => 'SUM(CASE WHEN object_type = \'host\' AND state = 99 THEN 1 ELSE 0 END)',
+ 'hosts_pending_not_checked' => 'SUM(CASE WHEN object_type = \'host\' AND state = 99 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'hosts_down' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 THEN 1 ELSE 0 END)',
+ 'hosts_down_handled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 AND handled > 0 THEN 1 ELSE 0 END)',
+ 'hosts_down_unhandled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 AND handled = 0 THEN 1 ELSE 0 END)',
+ 'hosts_down_passive' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 AND is_passive_checked = 1 THEN 1 ELSE 0 END)',
+ 'hosts_down_not_checked' => 'SUM(CASE WHEN object_type = \'host\' AND state = 1 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'hosts_unreachable' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 THEN 1 ELSE 0 END)',
+ 'hosts_unreachable_handled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 AND handled > 0 THEN 1 ELSE 0 END)',
+ 'hosts_unreachable_unhandled' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 AND handled = 0 THEN 1 ELSE 0 END)',
+ 'hosts_unreachable_passive' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 AND is_passive_checked = 1 THEN 1 ELSE 0 END)',
+ 'hosts_unreachable_not_checked' => 'SUM(CASE WHEN object_type = \'host\' AND state = 2 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'hosts_active' => 'SUM(CASE WHEN object_type = \'host\' AND is_active_checked = 1 THEN 1 ELSE 0 END)',
+ 'hosts_passive' => 'SUM(CASE WHEN object_type = \'host\' AND is_passive_checked = 1 THEN 1 ELSE 0 END)',
+ 'hosts_not_checked' => 'SUM(CASE WHEN object_type = \'host\' AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'hosts_not_processing_event_handlers' => 'SUM(CASE WHEN object_type = \'host\' AND is_processing_events = 0 THEN 1 ELSE 0 END)',
+ 'hosts_not_triggering_notifications' => 'SUM(CASE WHEN object_type = \'host\' AND is_triggering_notifications = 0 THEN 1 ELSE 0 END)',
+ 'hosts_without_flap_detection' => 'SUM(CASE WHEN object_type = \'host\' AND is_allowed_to_flap = 0 THEN 1 ELSE 0 END)',
+ 'hosts_flapping' => 'SUM(CASE WHEN object_type = \'host\' AND is_flapping = 1 THEN 1 ELSE 0 END)'
+ ),
+ 'servicestatussummary' => array(
+ 'services_total' => 'SUM(CASE WHEN object_type = \'service\' THEN 1 ELSE 0 END)',
+ 'services_problem' => 'SUM(CASE WHEN object_type = \'service\' AND state > 0 THEN 1 ELSE 0 END)',
+ 'services_problem_handled' => 'SUM(CASE WHEN object_type = \'service\' AND state > 0 AND handled + host_problem > 0 THEN 1 ELSE 0 END)',
+ 'services_problem_unhandled' => 'SUM(CASE WHEN object_type = \'service\' AND state > 0 AND handled + host_problem = 0 THEN 1 ELSE 0 END)',
+ 'services_ok' => 'SUM(CASE WHEN object_type = \'service\' AND state = 0 THEN 1 ELSE 0 END)',
+ 'services_ok_not_checked' => 'SUM(CASE WHEN object_type = \'service\' AND state = 0 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_pending' => 'SUM(CASE WHEN object_type = \'service\' AND state = 99 THEN 1 ELSE 0 END)',
+ 'services_pending_not_checked' => 'SUM(CASE WHEN object_type = \'service\' AND state = 99 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_warning' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 THEN 1 ELSE 0 END)',
+ 'services_warning_handled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 AND handled + host_problem > 0 THEN 1 ELSE 0 END)',
+ 'services_warning_unhandled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 AND handled + host_problem = 0 THEN 1 ELSE 0 END)',
+ 'services_warning_passive' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 AND is_passive_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_warning_not_checked' => 'SUM(CASE WHEN object_type = \'service\' AND state = 1 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_critical' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 THEN 1 ELSE 0 END)',
+ 'services_critical_handled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 AND handled + host_problem > 0 THEN 1 ELSE 0 END)',
+ 'services_critical_unhandled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 AND handled + host_problem = 0 THEN 1 ELSE 0 END)',
+ 'services_critical_passive' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 AND is_passive_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_critical_not_checked' => 'SUM(CASE WHEN object_type = \'service\' AND state = 2 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_unknown' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 THEN 1 ELSE 0 END)',
+ 'services_unknown_handled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 AND handled + host_problem > 0 THEN 1 ELSE 0 END)',
+ 'services_unknown_unhandled' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 AND handled + host_problem = 0 THEN 1 ELSE 0 END)',
+ 'services_unknown_passive' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 AND is_passive_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_unknown_not_checked' => 'SUM(CASE WHEN object_type = \'service\' AND state = 3 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_active' => 'SUM(CASE WHEN object_type = \'service\' AND is_active_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_passive' => 'SUM(CASE WHEN object_type = \'service\' AND is_passive_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_not_checked' => 'SUM(CASE WHEN object_type = \'service\' AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_not_processing_event_handlers' => 'SUM(CASE WHEN object_type = \'service\' AND is_processing_events = 0 THEN 1 ELSE 0 END)',
+ 'services_not_triggering_notifications' => 'SUM(CASE WHEN object_type = \'service\' AND is_triggering_notifications = 0 THEN 1 ELSE 0 END)',
+ 'services_without_flap_detection' => 'SUM(CASE WHEN object_type = \'service\' AND is_allowed_to_flap = 0 THEN 1 ELSE 0 END)',
+ 'services_flapping' => 'SUM(CASE WHEN object_type = \'service\' AND is_flapping = 1 THEN 1 ELSE 0 END)',
+
+/*
+NOTE: in case you might wonder, please see #7303. As a quickfix I did:
+
+:%s/(host_state = 0 OR host_state = 99)/host_state != 1 AND host_state != 2/g
+:%s/(host_state = 1 OR host_state = 2)/host_state != 0 AND host_state != 99/g
+
+We have to find a better solution here.
+
+*/
+ 'services_ok_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 0 THEN 1 ELSE 0 END)',
+ 'services_ok_not_checked_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 0 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_pending_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 99 THEN 1 ELSE 0 END)',
+ 'services_pending_not_checked_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 99 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_warning_handled_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 1 AND handled > 0 THEN 1 ELSE 0 END)',
+ 'services_warning_unhandled_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 1 AND handled = 0 THEN 1 ELSE 0 END)',
+ 'services_warning_passive_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 1 AND is_passive_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_warning_not_checked_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 1 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_critical_handled_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 2 AND handled > 0 THEN 1 ELSE 0 END)',
+ 'services_critical_unhandled_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 2 AND handled = 0 THEN 1 ELSE 0 END)',
+ 'services_critical_passive_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 2 AND is_passive_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_critical_not_checked_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 2 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_unknown_handled_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 3 AND handled > 0 THEN 1 ELSE 0 END)',
+ 'services_unknown_unhandled_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 3 AND handled = 0 THEN 1 ELSE 0 END)',
+ 'services_unknown_passive_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 3 AND is_passive_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_unknown_not_checked_on_ok_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 1 AND host_state != 2 AND state = 3 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_ok_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 0 THEN 1 ELSE 0 END)',
+ 'services_ok_not_checked_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 0 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_pending_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 99 THEN 1 ELSE 0 END)',
+ 'services_pending_not_checked_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 99 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_warning_handled_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 1 AND handled > 0 THEN 1 ELSE 0 END)',
+ 'services_warning_unhandled_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 1 AND handled = 0 THEN 1 ELSE 0 END)',
+ 'services_warning_passive_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 1 AND is_passive_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_warning_not_checked_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 1 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_critical_handled_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 2 AND handled > 0 THEN 1 ELSE 0 END)',
+ 'services_critical_unhandled_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 2 AND handled = 0 THEN 1 ELSE 0 END)',
+ 'services_critical_passive_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 2 AND is_passive_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_critical_not_checked_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 2 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)',
+ 'services_unknown_handled_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 3 AND handled > 0 THEN 1 ELSE 0 END)',
+ 'services_unknown_unhandled_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 3 AND handled = 0 THEN 1 ELSE 0 END)',
+ 'services_unknown_passive_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 3 AND is_passive_checked = 1 THEN 1 ELSE 0 END)',
+ 'services_unknown_not_checked_on_problem_hosts' => 'SUM(CASE WHEN object_type = \'service\' AND host_state != 0 AND host_state != 99 AND state = 3 AND is_active_checked = 0 AND is_passive_checked = 0 THEN 1 ELSE 0 END)'
+ )
+ );
+
+ /**
+ * The union
+ *
+ * @var Zend_Db_Select
+ */
+ protected $summaryQuery;
+
+ /**
+ * Subqueries used for the summary query
+ *
+ * @var IdoQuery[]
+ */
+ protected $subQueries = array();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function allowsCustomVars()
+ {
+ foreach ($this->subQueries as $query) {
+ if (! $query->allowsCustomVars()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ foreach ($this->subQueries as $sub) {
+ $sub->applyFilter(clone $filter);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function joinBaseTables()
+ {
+ // TODO(el): Allow to switch between hard and soft states
+ $hosts = $this->createSubQuery(
+ 'Hoststatus',
+ array(
+ 'handled' => 'host_handled',
+ 'host_problem',
+ 'host_state' => new Zend_Db_Expr('NULL'),
+ 'is_active_checked' => 'host_active_checks_enabled',
+ 'is_allowed_to_flap' => 'host_flap_detection_enabled',
+ 'is_flapping' => 'host_is_flapping',
+ 'is_passive_checked' => 'host_is_passive_checked',
+ 'is_processing_events' => 'host_event_handler_enabled',
+ 'is_triggering_notifications' => 'host_notifications_enabled',
+ 'object_type',
+ 'severity' => 'host_severity',
+ 'state_change' => 'host_last_state_change',
+ 'state' => 'host_state'
+ )
+ );
+ $this->subQueries[] = $hosts;
+ $services = $this->createSubQuery(
+ 'Servicestatus',
+ array(
+ 'handled' => 'service_handled',
+ 'host_problem',
+ 'host_state' => 'host_hard_state',
+ 'is_active_checked' => 'service_active_checks_enabled',
+ 'is_allowed_to_flap' => 'service_flap_detection_enabled',
+ 'is_flapping' => 'service_is_flapping',
+ 'is_passive_checked' => 'service_is_passive_checked',
+ 'is_processing_events' => 'service_event_handler_enabled',
+ 'is_triggering_notifications' => 'service_notifications_enabled',
+ 'object_type',
+ 'severity' => 'service_severity',
+ 'state_change' => 'service_last_state_change',
+ 'state' => 'service_state'
+ )
+ );
+ $this->subQueries[] = $services;
+ $this->summaryQuery = $this->db->select()->union(array($hosts, $services), Zend_Db_Select::SQL_UNION_ALL);
+ $this->select->from(array('statussummary' => $this->summaryQuery), array());
+ $this->joinedVirtualTables['hoststatussummary'] = true;
+ $this->joinedVirtualTables['servicestatussummary'] = true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function order($columnOrAlias, $dir = null)
+ {
+ if (! $this->hasAliasName($columnOrAlias)) {
+ foreach ($this->subQueries as $sub) {
+ $sub->requireColumn($columnOrAlias);
+ }
+ }
+ return parent::order($columnOrAlias, $dir);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ $this->requireColumn($condition);
+ foreach ($this->subQueries as $sub) {
+ $sub->where($condition, $value);
+ }
+ return $this;
+ }
+
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->requireColumn($ex->getColumn());
+ foreach ($this->subQueries as $sub) {
+ $sub->whereEx($ex);
+ }
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledhostproblemsQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledhostproblemsQuery.php
new file mode 100644
index 0000000..f4c4e07
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledhostproblemsQuery.php
@@ -0,0 +1,48 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for unhandled host problems
+ */
+class UnhandledhostproblemsQuery extends IdoQuery
+{
+ protected $allowCustomVars = true;
+
+ protected $columnMap = array(
+ 'problems' => array(
+ 'hosts_down_unhandled' => 'COUNT(*)',
+ )
+ );
+
+ /**
+ * The service status sub select
+ *
+ * @var HoststatusQuery
+ */
+ protected $subSelect;
+
+ public function addFilter(Filter $filter)
+ {
+ $this->subSelect->applyFilter(clone $filter);
+ return $this;
+ }
+
+ protected function joinBaseTables()
+ {
+ $this->subSelect = $this->createSubQuery(
+ 'Hoststatus',
+ array('host_name')
+ );
+ $this->subSelect->where('host_handled', 0);
+ $this->subSelect->where('host_state', 1);
+ $this->select->from(
+ array('problems' => $this->subSelect->setIsSubQuery(true)),
+ array()
+ );
+ $this->joinedVirtualTables['problems'] = true;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledserviceproblemsQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledserviceproblemsQuery.php
new file mode 100644
index 0000000..a218caf
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledserviceproblemsQuery.php
@@ -0,0 +1,48 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend\Ido\Query;
+
+use Icinga\Data\Filter\Filter;
+
+/**
+ * Query for unhandled service problems
+ */
+class UnhandledserviceproblemsQuery extends IdoQuery
+{
+ protected $allowCustomVars = true;
+
+ protected $columnMap = array(
+ 'problems' => array(
+ 'services_critical_unhandled' => 'COUNT(*)',
+ )
+ );
+
+ /**
+ * The service status sub select
+ *
+ * @var ServicestatusQuery
+ */
+ protected $subSelect;
+
+ public function addFilter(Filter $filter)
+ {
+ $this->subSelect->applyFilter(clone $filter);
+ return $this;
+ }
+
+ protected function joinBaseTables()
+ {
+ $this->subSelect = $this->createSubQuery(
+ 'Servicestatus',
+ array('service_description')
+ );
+ $this->subSelect->where('service_handled', 0);
+ $this->subSelect->where('service_state', 2);
+ $this->select->from(
+ array('problems' => $this->subSelect->setIsSubQuery(true)),
+ array()
+ );
+ $this->joinedVirtualTables['problems'] = true;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php b/modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
new file mode 100644
index 0000000..440ffa4
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
@@ -0,0 +1,349 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Backend;
+
+use Icinga\Application\Config;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\QueryInterface;
+use Icinga\Data\ResourceFactory;
+use Icinga\Data\ConnectionInterface;
+use Icinga\Data\Queryable;
+use Icinga\Data\Selectable;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\ProgrammingError;
+
+class MonitoringBackend implements Selectable, Queryable, ConnectionInterface
+{
+ /**
+ * Backend configuration
+ *
+ * @var ConfigObject
+ */
+ protected $config;
+
+ /**
+ * Resource
+ *
+ * @var mixed
+ */
+ protected $resource;
+
+ /**
+ * Type
+ *
+ * @var string
+ */
+ protected $type;
+
+ /**
+ * The configured name of this backend
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * Already created instances
+ *
+ * @var array
+ */
+ protected static $instances = array();
+
+ /**
+ * Create a new backend
+ *
+ * @param string $name
+ * @param ConfigObject $config
+ */
+ protected function __construct($name, ConfigObject $config)
+ {
+ $this->name = $name;
+ $this->config = $config;
+ }
+
+ /**
+ * Get a backend instance
+ *
+ * You may ask for a specific backend name or get the default one otherwise
+ *
+ * @param string $name Backend name
+ *
+ * @return MonitoringBackend
+ */
+ public static function instance($name = null)
+ {
+ if (! array_key_exists($name, self::$instances)) {
+ list($foundName, $config) = static::loadConfig($name);
+ $type = $config->get('type');
+ $class = implode(
+ '\\',
+ array(
+ __NAMESPACE__,
+ ucfirst($type),
+ ucfirst($type) . 'Backend'
+ )
+ );
+
+ if (!class_exists($class)) {
+ throw new ConfigurationError(
+ mt('monitoring', 'There is no "%s" monitoring backend'),
+ $class
+ );
+ }
+
+ self::$instances[$name] = new $class($foundName, $config);
+ if ($name === null) {
+ self::$instances[$foundName] = self::$instances[$name];
+ }
+ }
+
+ return self::$instances[$name];
+ }
+
+ /**
+ * Clear all cached instances. Mostly for testing purposes.
+ */
+ public static function clearInstances()
+ {
+ self::$instances = array();
+ }
+
+ /**
+ * Whether this backend is of a specific type
+ *
+ * @param string $type Backend type
+ *
+ * @return boolean
+ */
+ public function is($type)
+ {
+ return $this->getType() === $type;
+ }
+
+ /**
+ * Get the configured name of this backend
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Get the backend type name
+ *
+ * @return string
+ */
+ public function getType()
+ {
+ if ($this->type === null) {
+ $parts = preg_split('~\\\~', get_class($this));
+ $class = array_pop($parts);
+ if (substr($class, -7) === 'Backend') {
+ $this->type = lcfirst(substr($class, 0, -7));
+ } else {
+ throw new ProgrammingError(
+ '%s is not a valid monitoring backend class name',
+ $class
+ );
+ }
+ }
+ return $this->type;
+ }
+
+ /**
+ * Return the configuration for the first enabled or the given backend
+ */
+ protected static function loadConfig($name = null)
+ {
+ $backends = Config::module('monitoring', 'backends');
+
+ if ($name === null) {
+ $count = 0;
+
+ foreach ($backends as $name => $config) {
+ $count++;
+ if ((bool) $config->get('disabled', false) === false) {
+ return array($name, $config);
+ }
+ }
+
+ if ($count === 0) {
+ $message = mt('monitoring', 'No backend has been configured');
+ } else {
+ $message = mt('monitoring', 'All backends are disabled');
+ }
+
+ throw new ConfigurationError($message);
+ } else {
+ $config = $backends->getSection($name);
+
+ if ($config->isEmpty()) {
+ throw new ConfigurationError(
+ mt('monitoring', 'No configuration for backend %s'),
+ $name
+ );
+ }
+
+ if ((bool) $config->get('disabled', false) === true) {
+ throw new ConfigurationError(
+ mt('monitoring', 'Configuration for backend %s is disabled'),
+ $name
+ );
+ }
+
+ return array($name, $config);
+ }
+ }
+
+ /**
+ * Get this backend's internal resource
+ *
+ * @return mixed
+ */
+ public function getResource()
+ {
+ if ($this->resource === null) {
+ $config = ResourceFactory::getResourceConfig($this->config->get('resource'));
+ if ($this->is('ido') && $config->type === 'db' && $config->db === 'mysql' && $config->charset === null) {
+ $config->charset = 'latin1';
+ }
+ $this->resource = ResourceFactory::createResource($config);
+ if ($this->is('ido') && $this->resource->getDbType() !== 'oracle') {
+ // TODO(el): The resource should set the table prefix
+ $this->resource->setTablePrefix('icinga_');
+ }
+ }
+ return $this->resource;
+ }
+
+ /**
+ * Backend entry point
+ *
+ * @return $this
+ */
+ public function select()
+ {
+ return $this;
+ }
+
+ /**
+ * Create a data view to fetch data from
+ *
+ * @param string $name
+ * @param array $columns
+ *
+ * @return \Icinga\Module\Monitoring\DataView\DataView
+ */
+ public function from($name, array $columns = null)
+ {
+ $class = $this->buildViewClassName($name);
+ return new $class($this, $columns);
+ }
+
+ /**
+ * View name to class name resolution
+ *
+ * @param string $view
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case the view does not exist
+ */
+ protected function buildViewClassName($view)
+ {
+ $class = ucfirst(strtolower($view));
+ $classPath = '\\Icinga\\Module\\Monitoring\\DataView\\' . $class;
+ if (! class_exists($classPath)) {
+ throw new ProgrammingError('DataView %s does not exist', $class);
+ }
+
+ return $classPath;
+ }
+
+ /**
+ * Get a specific query class instance
+ *
+ * @param string $name Query name
+ * @param array $columns Optional column list
+ *
+ * @return QueryInterface
+ *
+ * @throws ProgrammingError When the query does not exist for this backend
+ */
+ public function query($name, $columns = null)
+ {
+ $class = $this->buildQueryClassName($name);
+
+ if (!class_exists($class)) {
+ throw new ProgrammingError(
+ 'Query "%s" does not exist for backend %s',
+ $name,
+ $this->getType()
+ );
+ }
+
+ return new $class($this->getResource(), $columns);
+ }
+
+ /**
+ * Whether this backend supports the given query
+ *
+ * @param string $name Query name to check for
+ *
+ * @return bool
+ */
+ public function hasQuery($name)
+ {
+ return class_exists($this->buildQueryClassName($name));
+ }
+
+ /**
+ * Query name to class name resolution
+ *
+ * @param string $query
+ *
+ * @return string
+ */
+ protected function buildQueryClassName($query)
+ {
+ $parts = preg_split('~\\\~', get_class($this));
+ array_pop($parts);
+ array_push($parts, 'Query', ucfirst(strtolower($query)) . 'Query');
+ return implode('\\', $parts);
+ }
+
+ /**
+ * Fetch and return the program version of the current instance
+ *
+ * @return string
+ */
+ public function getProgramVersion()
+ {
+ return preg_replace(
+ '/^[vr]/',
+ '',
+ $this->select()->from('programstatus', array('program_version'))->fetchOne()
+ );
+ }
+
+ /**
+ * Get whether the backend is Icinga 2
+ *
+ * @param string $programVersion
+ *
+ * @return bool
+ */
+ public function isIcinga2($programVersion = null)
+ {
+ if ($programVersion === null) {
+ $programVersion = $this->select()->from('programstatus', array('program_version'))->fetchOne();
+ }
+ return (bool) preg_match(
+ '/^[vr]?2\.\d+\.\d+.*$/',
+ $programVersion
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/BackendStep.php b/modules/monitoring/library/Monitoring/BackendStep.php
new file mode 100644
index 0000000..9683392
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/BackendStep.php
@@ -0,0 +1,208 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring;
+
+use Exception;
+use Icinga\Module\Setup\Step;
+use Icinga\Application\Config;
+use Icinga\Exception\IcingaException;
+
+class BackendStep extends Step
+{
+ protected $data;
+
+ protected $backendIniError;
+
+ protected $resourcesIniError;
+
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ }
+
+ public function apply()
+ {
+ $success = $this->createBackendsIni();
+ $success &= $this->createResourcesIni();
+ return $success;
+ }
+
+ protected function createBackendsIni()
+ {
+ $config = array();
+ $config[$this->data['backendConfig']['name']] = array(
+ 'type' => $this->data['backendConfig']['type'],
+ 'resource' => $this->data['resourceConfig']['name']
+ );
+
+ try {
+ Config::fromArray($config)
+ ->setConfigFile(Config::resolvePath('modules/monitoring/backends.ini'))
+ ->saveIni();
+ } catch (Exception $e) {
+ $this->backendIniError = $e;
+ return false;
+ }
+
+ $this->backendIniError = false;
+ return true;
+ }
+
+ protected function createResourcesIni()
+ {
+ $resourceConfig = $this->data['resourceConfig'];
+ $resourceName = $resourceConfig['name'];
+ unset($resourceConfig['name']);
+
+ try {
+ $config = Config::app('resources', true);
+ $config->setSection($resourceName, $resourceConfig);
+ $config->saveIni();
+ } catch (Exception $e) {
+ $this->resourcesIniError = $e;
+ return false;
+ }
+
+ $this->resourcesIniError = false;
+ return true;
+ }
+
+ public function getSummary()
+ {
+ $pageTitle = '<h2>' . mt('monitoring', 'Monitoring Backend', 'setup.page.title') . '</h2>';
+ $backendDescription = '<p>' . sprintf(
+ mt(
+ 'monitoring',
+ 'Icinga Web 2 will retrieve information from your monitoring environment'
+ . ' using a backend called "%s" and the specified resource below:'
+ ),
+ $this->data['backendConfig']['name']
+ ) . '</p>';
+
+ $resourceTitle = null;
+ $resourceHtml = null;
+ if ($this->data['resourceConfig']['type'] === 'db') {
+ $resourceTitle = '<h3>' . mt('monitoring', 'Database Resource') . '</h3>';
+ $resourceHtml = ''
+ . '<table>'
+ . '<tbody>'
+ . '<tr>'
+ . '<td><strong>' . t('Resource Name') . '</strong></td>'
+ . '<td>' . $this->data['resourceConfig']['name'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Database Type') . '</strong></td>'
+ . '<td>' . $this->data['resourceConfig']['db'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Host') . '</strong></td>'
+ . '<td>' . $this->data['resourceConfig']['host'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Port') . '</strong></td>'
+ . '<td>' . $this->data['resourceConfig']['port'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Database Name') . '</strong></td>'
+ . '<td>' . $this->data['resourceConfig']['dbname'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Username') . '</strong></td>'
+ . '<td>' . $this->data['resourceConfig']['username'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Password') . '</strong></td>'
+ . '<td>' . str_repeat('*', strlen($this->data['resourceConfig']['password'])) . '</td>'
+ . '</tr>';
+
+ if (defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')
+ && isset($this->data['resourceConfig']['ssl_do_not_verify_server_cert'])
+ && $this->data['resourceConfig']['ssl_do_not_verify_server_cert']
+ ) {
+ $resourceHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('SSL Do Not Verify Server Certificate') . '</strong></td>'
+ . '<td>' . $this->data['resourceConfig']['ssl_do_not_verify_server_cert'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['resourceConfig']['ssl_key']) && $this->data['resourceConfig']['ssl_key']) {
+ $resourceHtml .= ''
+ .'<tr>'
+ . '<td><strong>' . t('SSL Key') . '</strong></td>'
+ . '<td>' . $this->data['resourceConfig']['ssl_key'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['resourceConfig']['ssl_cert']) && $this->data['resourceConfig']['ssl_cert']) {
+ $resourceHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('SSL Cert') . '</strong></td>'
+ . '<td>' . $this->data['resourceConfig']['ssl_cert'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['resourceConfig']['ssl_ca']) && $this->data['resourceConfig']['ssl_ca']) {
+ $resourceHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('CA') . '</strong></td>'
+ . '<td>' . $this->data['resourceConfig']['ssl_ca'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['resourceConfig']['ssl_capath']) && $this->data['resourceConfig']['ssl_capath']) {
+ $resourceHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('CA Path') . '</strong></td>'
+ . '<td>' . $this->data['resourceConfig']['ssl_capath'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['resourceConfig']['ssl_cipher']) && $this->data['resourceConfig']['ssl_cipher']) {
+ $resourceHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('Cipher') . '</strong></td>'
+ . '<td>' . $this->data['resourceConfig']['ssl_cipher'] . '</td>'
+ . '</tr>';
+ }
+
+ $resourceHtml .= ''
+ . '</tbody>'
+ . '</table>';
+ }
+
+ return $pageTitle . '<div class="topic">' . $backendDescription . $resourceTitle . $resourceHtml . '</div>';
+ }
+
+ public function getReport()
+ {
+ $report = array();
+
+ if ($this->backendIniError === false) {
+ $report[] = sprintf(
+ mt('monitoring', 'Monitoring backend configuration has been successfully written to: %s'),
+ Config::resolvePath('modules/monitoring/backends.ini')
+ );
+ } elseif ($this->backendIniError !== null) {
+ $report[] = sprintf(
+ mt(
+ 'monitoring',
+ 'Monitoring backend configuration could not be written to: %s. An error occured:'
+ ),
+ Config::resolvePath('modules/monitoring/backends.ini')
+ );
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->backendIniError));
+ }
+
+ if ($this->resourcesIniError === false) {
+ $report[] = sprintf(
+ mt('monitoring', 'Resource configuration has been successfully updated: %s'),
+ Config::resolvePath('resources.ini')
+ );
+ } elseif ($this->resourcesIniError !== null) {
+ $report[] = sprintf(
+ mt('monitoring', 'Resource configuration could not be udpated: %s. An error occured:'),
+ Config::resolvePath('resources.ini')
+ );
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->resourcesIniError));
+ }
+
+ return $report;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Cli/CliUtils.php b/modules/monitoring/library/Monitoring/Cli/CliUtils.php
new file mode 100644
index 0000000..3d7d3ee
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Cli/CliUtils.php
@@ -0,0 +1,122 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Cli;
+
+use Icinga\Cli\Screen;
+
+class CliUtils
+{
+ protected $hostColors = array(
+ 0 => array('black', 'lightgreen'),
+ 1 => array('black', 'lightred'),
+ 2 => array('black', 'brown'),
+ 99 => array('black', 'lightgray'),
+ );
+ protected $serviceColors = array(
+ 0 => array('black', 'lightgreen'),
+ 1 => array('black', 'yellow'),
+ 2 => array('black', 'lightred'),
+ 3 => array('black', 'lightpurple'),
+ 99 => array('black', 'lightgray'),
+ );
+ protected $hostStates = array(
+ 0 => 'UP',
+ 1 => 'DOWN',
+ 2 => 'UNREACHABLE',
+ 99 => 'PENDING',
+ );
+
+ protected $serviceStates = array(
+ 0 => 'OK',
+ 1 => 'WARNING',
+ 2 => 'CRITICAL',
+ 3 => 'UNKNOWN',
+ 99 => 'PENDING',
+ );
+
+ protected $screen;
+ protected $hostState;
+ protected $serviceState;
+
+ public function __construct(Screen $screen)
+ {
+ $this->screen = $screen;
+ }
+
+ public function setHostState($state)
+ {
+ $this->hostState = $state;
+ }
+
+ public function setServiceState($state)
+ {
+ $this->serviceState = $state;
+ }
+
+ public function shortHostState($state = null)
+ {
+ if ($state === null) {
+ $state = $this->hostState;
+ }
+ return sprintf('%-4s', substr($this->hostStates[$state], 0, 4));
+ }
+
+ public function shortServiceState($state = null)
+ {
+ if ($state === null) {
+ $state = $this->serviceState;
+ }
+ return sprintf('%-4s', substr($this->serviceStates[$state], 0, 4));
+ }
+
+ public function hostStateBackground($text, $state = null)
+ {
+ if ($state === null) {
+ $state = $this->hostState;
+ }
+ return $this->screen->colorize(
+ $text,
+ $this->hostColors[$state][0],
+ $this->hostColors[$state][1]
+ );
+ }
+
+ public function serviceStateBackground($text, $state = null)
+ {
+ if ($state === null) {
+ $state = $this->serviceState;
+ }
+ return $this->screen->colorize(
+ $text,
+ $this->serviceColors[$state][0],
+ $this->serviceColors[$state][1]
+ );
+ }
+
+ public function objectStateFlags($type, &$row)
+ {
+ $extra = array();
+ if ($row->{$type . '_in_downtime'}) {
+ if ($this->screen->hasUtf8()) {
+ $extra[] = 'DOWNTIME ⌚';
+ } else {
+ $extra[] = 'DOWNTIME';
+ }
+ }
+ if ($row->{$type . '_acknowledged'}) {
+ if ($this->screen->hasUtf8()) {
+ $extra[] = 'ACK ✓';
+ } else {
+ $extra[] = 'ACK';
+ }
+ }
+
+ if (empty($extra)) {
+ $extra = '';
+ } else {
+ $extra = sprintf(' [ %s ]', implode(', ', $extra));
+ }
+ return $extra;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/IcingaApiCommand.php b/modules/monitoring/library/Monitoring/Command/IcingaApiCommand.php
new file mode 100644
index 0000000..c33157f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/IcingaApiCommand.php
@@ -0,0 +1,126 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command;
+
+class IcingaApiCommand
+{
+ /**
+ * Command data
+ *
+ * @var array
+ */
+ protected $data;
+
+ /**
+ * Name of the endpoint
+ *
+ * @var string
+ */
+ protected $endpoint;
+
+ /**
+ * Next Icinga API command to be sent, if any
+ *
+ * @var static
+ */
+ protected $next;
+
+ /**
+ * Create a new Icinga 2 API command
+ *
+ * @param string $endpoint
+ * @param array $data
+ *
+ * @return static
+ */
+ public static function create($endpoint, array $data)
+ {
+ $command = new static();
+ $command
+ ->setEndpoint($endpoint)
+ ->setData($data);
+ return $command;
+ }
+
+ /**
+ * Get the command data
+ *
+ * @return array
+ */
+ public function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * Set the command data
+ *
+ * @param array $data
+ *
+ * @return $this
+ */
+ public function setData($data)
+ {
+ $this->data = $data;
+
+ return $this;
+ }
+
+ /**
+ * Get the name of the endpoint
+ *
+ * @return string
+ */
+ public function getEndpoint()
+ {
+ return $this->endpoint;
+ }
+
+ /**
+ * Set the name of the endpoint
+ *
+ * @param string $endpoint
+ *
+ * @return $this
+ */
+ public function setEndpoint($endpoint)
+ {
+ $this->endpoint = $endpoint;
+
+ return $this;
+ }
+
+ /**
+ * Get whether another Icinga API command should be sent after this one
+ *
+ * @return bool
+ */
+ public function hasNext()
+ {
+ return $this->next !== null;
+ }
+
+ /**
+ * Get the next Icinga API command
+ *
+ * @return IcingaApiCommand
+ */
+ public function getNext()
+ {
+ return $this->next;
+ }
+
+ /**
+ * Set the next Icinga API command
+ *
+ * @param IcingaApiCommand $next
+ *
+ * @return IcingaApiCommand
+ */
+ public function setNext(IcingaApiCommand $next)
+ {
+ $this->next = $next;
+ return $next;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/IcingaCommand.php b/modules/monitoring/library/Monitoring/Command/IcingaCommand.php
new file mode 100644
index 0000000..49ce586
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/IcingaCommand.php
@@ -0,0 +1,21 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command;
+
+/**
+ * Base class for commands sent to an Icinga instance
+ */
+abstract class IcingaCommand
+{
+ /**
+ * Get the name of the command
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ $nsParts = explode('\\', get_called_class());
+ return substr_replace(end($nsParts), '', -7); // Remove 'Command' Suffix
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Instance/DisableNotificationsExpireCommand.php b/modules/monitoring/library/Monitoring/Command/Instance/DisableNotificationsExpireCommand.php
new file mode 100644
index 0000000..1d3ce9d
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Instance/DisableNotificationsExpireCommand.php
@@ -0,0 +1,42 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Instance;
+
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+
+/**
+ * Disable host and service notifications w/ expire time on an Icinga instance
+ */
+class DisableNotificationsExpireCommand extends IcingaCommand
+{
+ /**
+ * The time when notifications should be re-enabled after disabling
+ *
+ * @var int|null Unix timestamp
+ */
+ protected $expireTime;
+
+ /**
+ * Set time when notifications should be re-enabled after disabling
+ *
+ * @param $expireTime int Unix timestamp
+ *
+ * @return $this
+ */
+ public function setExpireTime($expireTime)
+ {
+ $this->expireTime = (int) $expireTime;
+ return $this;
+ }
+
+ /**
+ * Get the date and time when notifications should be re-enabled after disabling
+ *
+ * @return int|null Unix timestamp
+ */
+ public function getExpireTime()
+ {
+ return $this->expireTime;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Instance/ToggleInstanceFeatureCommand.php b/modules/monitoring/library/Monitoring/Command/Instance/ToggleInstanceFeatureCommand.php
new file mode 100644
index 0000000..8a8a8ca
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Instance/ToggleInstanceFeatureCommand.php
@@ -0,0 +1,122 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Instance;
+
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+
+/**
+ * Enable or disable a feature of an Icinga instance
+ */
+class ToggleInstanceFeatureCommand extends IcingaCommand
+{
+ /**
+ * Feature for enabling or disabling active host checks on an Icinga instance
+ */
+ const FEATURE_ACTIVE_HOST_CHECKS = 'active_host_checks_enabled';
+
+ /**
+ * Feature for enabling or disabling active service checks on an Icinga instance
+ */
+ const FEATURE_ACTIVE_SERVICE_CHECKS = 'active_service_checks_enabled';
+
+ /**
+ * Feature for enabling or disabling host and service event handlers on an Icinga instance
+ */
+ const FEATURE_EVENT_HANDLERS = 'event_handlers_enabled';
+
+ /**
+ * Feature for enabling or disabling host and service flap detection on an Icinga instance
+ */
+ const FEATURE_FLAP_DETECTION = 'flap_detection_enabled';
+
+ /**
+ * Feature for enabling or disabling host and service notifications on an Icinga instance
+ */
+ const FEATURE_NOTIFICATIONS = 'notifications_enabled';
+
+ /**
+ * Feature for enabling or disabling processing of host checks via the OCHP command on an Icinga instance
+ */
+ const FEATURE_HOST_OBSESSING = 'obsess_over_hosts';
+
+ /**
+ * Feature for enabling or disabling processing of service checks via the OCHP command on an Icinga instance
+ */
+ const FEATURE_SERVICE_OBSESSING = 'obsess_over_services';
+
+ /**
+ * Feature for enabling or disabling passive host checks on an Icinga instance
+ */
+ const FEATURE_PASSIVE_HOST_CHECKS = 'passive_host_checks_enabled';
+
+ /**
+ * Feature for enabling or disabling passive service checks on an Icinga instance
+ */
+ const FEATURE_PASSIVE_SERVICE_CHECKS = 'passive_service_checks_enabled';
+
+ /**
+ * Feature for enabling or disabling the processing of host and service performance data on an Icinga instance
+ */
+ const FEATURE_PERFORMANCE_DATA = 'process_performance_data';
+
+ /**
+ * Feature that is to be enabled or disabled
+ *
+ * @var string
+ */
+ protected $feature;
+
+ /**
+ * Whether the feature should be enabled or disabled
+ *
+ * @var bool
+ */
+ protected $enabled;
+
+ /**
+ * Set the feature that is to be enabled or disabled
+ *
+ * @param string $feature
+ *
+ * @return $this
+ */
+ public function setFeature($feature)
+ {
+ $this->feature = (string) $feature;
+ return $this;
+ }
+
+ /**
+ * Get the feature that is to be enabled or disabled
+ *
+ * @return string
+ */
+ public function getFeature()
+ {
+ return $this->feature;
+ }
+
+ /**
+ * Set whether the feature should be enabled or disabled
+ *
+ * @param bool $enabled
+ *
+ * @return $this
+ */
+ public function setEnabled($enabled = true)
+ {
+ $this->enabled = (bool) $enabled;
+ return $this;
+ }
+
+ /**
+ * Get whether the feature should be enabled or disabled
+ *
+ * @return bool
+ */
+ public function getEnabled()
+ {
+ return $this->enabled;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/AcknowledgeProblemCommand.php b/modules/monitoring/library/Monitoring/Command/Object/AcknowledgeProblemCommand.php
new file mode 100644
index 0000000..2001e78
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/AcknowledgeProblemCommand.php
@@ -0,0 +1,144 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+/**
+ * Acknowledge a host or service problem
+ */
+class AcknowledgeProblemCommand extends WithCommentCommand
+{
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation.
+ */
+ protected $allowedObjects = array(
+ self::TYPE_HOST,
+ self::TYPE_SERVICE
+ );
+
+ /**
+ * Whether the acknowledgement is sticky
+ *
+ * Sticky acknowledgements remain until the host or service recovers. Non-sticky acknowledgements will be
+ * automatically removed when the host or service state changes.
+ *
+ * @var bool
+ */
+ protected $sticky = false;
+
+ /**
+ * Whether to send a notification about the acknowledgement
+
+ * @var bool
+ */
+ protected $notify = false;
+
+ /**
+ * Whether the comment associated with the acknowledgement is persistent
+ *
+ * Persistent comments are not lost the next time the monitoring host restarts.
+ *
+ * @var bool
+ */
+ protected $persistent = false;
+
+ /**
+ * Optional time when the acknowledgement should expire
+ *
+ * @var int|null
+ */
+ protected $expireTime;
+
+ /**
+ * Set whether the acknowledgement is sticky
+ *
+ * @param bool $sticky
+ *
+ * @return $this
+ */
+ public function setSticky($sticky = true)
+ {
+ $this->sticky = (bool) $sticky;
+ return $this;
+ }
+
+ /**
+ * Is the acknowledgement sticky?
+ *
+ * @return bool
+ */
+ public function getSticky()
+ {
+ return $this->sticky;
+ }
+
+ /**
+ * Set whether to send a notification about the acknowledgement
+ *
+ * @param bool $notify
+ *
+ * @return $this
+ */
+ public function setNotify($notify = true)
+ {
+ $this->notify = (bool) $notify;
+ return $this;
+ }
+
+ /**
+ * Get whether to send a notification about the acknowledgement
+ *
+ * @return bool
+ */
+ public function getNotify()
+ {
+ return $this->notify;
+ }
+
+ /**
+ * Set whether the comment associated with the acknowledgement is persistent
+ *
+ * @param bool $persistent
+ *
+ * @return $this
+ */
+ public function setPersistent($persistent = true)
+ {
+ $this->persistent = (bool) $persistent;
+ return $this;
+ }
+
+ /**
+ * Is the comment associated with the acknowledgement is persistent?
+ *
+ * @return bool
+ */
+ public function getPersistent()
+ {
+ return $this->persistent;
+ }
+
+ /**
+ * Set the time when the acknowledgement should expire
+ *
+ * @param int $expireTime
+ *
+ * @return $this
+ */
+ public function setExpireTime($expireTime)
+ {
+ $this->expireTime = (int) $expireTime;
+ return $this;
+ }
+
+ /**
+ * Get the time when the acknowledgement should expire
+ *
+ * @return int|null
+ */
+ public function getExpireTime()
+ {
+ return $this->expireTime;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/AddCommentCommand.php b/modules/monitoring/library/Monitoring/Command/Object/AddCommentCommand.php
new file mode 100644
index 0000000..9e3151f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/AddCommentCommand.php
@@ -0,0 +1,80 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+/**
+ * Add a comment to a host or service
+ */
+class AddCommentCommand extends WithCommentCommand
+{
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation.
+ */
+ protected $allowedObjects = array(
+ self::TYPE_HOST,
+ self::TYPE_SERVICE
+ );
+
+ /**
+ * Whether the comment is persistent
+ *
+ * Persistent comments are not lost the next time the monitoring host restarts.
+ */
+ protected $persistent;
+
+ /**
+ * Optional time when the acknowledgement should expire
+ *
+ * @var int|null
+ */
+ protected $expireTime;
+
+ /**
+ * Set whether the comment is persistent
+ *
+ * @param bool $persistent
+ *
+ * @return $this
+ */
+ public function setPersistent($persistent = true)
+ {
+ $this->persistent = $persistent;
+ return $this;
+ }
+
+ /**
+ * Is the comment persistent?
+ *
+ * @return bool
+ */
+ public function getPersistent()
+ {
+ return $this->persistent;
+ }
+
+ /**
+ * Set the time when the acknowledgement should expire
+ *
+ * @param int $expireTime
+ *
+ * @return $this
+ */
+ public function setExpireTime($expireTime)
+ {
+ $this->expireTime = (int) $expireTime;
+
+ return $this;
+ }
+
+ /**
+ * Get the time when the acknowledgement should expire
+ *
+ * @return int|null
+ */
+ public function getExpireTime()
+ {
+ return $this->expireTime;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/ApiScheduleHostDowntimeCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ApiScheduleHostDowntimeCommand.php
new file mode 100644
index 0000000..6495375
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/ApiScheduleHostDowntimeCommand.php
@@ -0,0 +1,40 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+/**
+ * Schedule host downtime command for API command transport and Icinga >= 2.11.0 that
+ * sends all_services and child_options in a single request
+ */
+class ApiScheduleHostDowntimeCommand extends ScheduleHostDowntimeCommand
+{
+ /** @var int Whether no, triggered, or non-triggered child downtimes should be scheduled */
+ protected $childOptions;
+
+ protected $forAllServicesNative = true;
+
+ /**
+ * Get child options, i.e. whether no, triggered, or non-triggered child downtimes should be scheduled
+ *
+ * @return int
+ */
+ public function getChildOptions()
+ {
+ return $this->childOptions;
+ }
+
+ /**
+ * Set child options, i.e. whether no, triggered, or non-triggered child downtimes should be scheduled
+ *
+ * @param int $childOptions
+ *
+ * @return $this
+ */
+ public function setChildOptions($childOptions)
+ {
+ $this->childOptions = $childOptions;
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/CommandAuthor.php b/modules/monitoring/library/Monitoring/Command/Object/CommandAuthor.php
new file mode 100644
index 0000000..577e3df
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/CommandAuthor.php
@@ -0,0 +1,38 @@
+<?php
+
+/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+trait CommandAuthor
+{
+ /**
+ * Author of the command
+ *
+ * @var string
+ */
+ protected $author;
+
+ /**
+ * Set the author
+ *
+ * @param string $author
+ *
+ * @return $this
+ */
+ public function setAuthor($author)
+ {
+ $this->author = (string) $author;
+ return $this;
+ }
+
+ /**
+ * Get the author
+ *
+ * @return string
+ */
+ public function getAuthor()
+ {
+ return $this->author;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/DeleteCommentCommand.php b/modules/monitoring/library/Monitoring/Command/Object/DeleteCommentCommand.php
new file mode 100644
index 0000000..348175a
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/DeleteCommentCommand.php
@@ -0,0 +1,110 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+
+/**
+ * Delete a host or service comment
+ */
+class DeleteCommentCommand extends IcingaCommand
+{
+ use CommandAuthor;
+
+ /**
+ * ID of the comment that is to be deleted
+ *
+ * @var int
+ */
+ protected $commentId;
+
+ /**
+ * Name of the comment (Icinga 2.4+)
+ *
+ * Required for removing the comment via Icinga 2's API.
+ *
+ * @var string
+ */
+ protected $commentName;
+
+ /**
+ * Whether the command affects a service comment
+ *
+ * @var boolean
+ */
+ protected $isService = false;
+
+ /**
+ * Get the ID of the comment that is to be deleted
+ *
+ * @return int
+ */
+ public function getCommentId()
+ {
+ return $this->commentId;
+ }
+
+ /**
+ * Set the ID of the comment that is to be deleted
+ *
+ * @param int $commentId
+ *
+ * @return $this
+ */
+ public function setCommentId($commentId)
+ {
+ $this->commentId = (int) $commentId;
+ return $this;
+ }
+
+ /**
+ * Get the name of the comment (Icinga 2.4+)
+ *
+ * Required for removing the comment via Icinga 2's API.
+ *
+ * @return string
+ */
+ public function getCommentName()
+ {
+ return $this->commentName;
+ }
+
+ /**
+ * Set the name of the comment (Icinga 2.4+)
+ *
+ * Required for removing the comment via Icinga 2's API.
+ *
+ * @param string $commentName
+ *
+ * @return $this
+ */
+ public function setCommentName($commentName)
+ {
+ $this->commentName = $commentName;
+ return $this;
+ }
+
+ /**
+ * Get whether the command affects a service comment
+ *
+ * @return boolean
+ */
+ public function getIsService()
+ {
+ return $this->isService;
+ }
+
+ /**
+ * Set whether the command affects a service comment
+ *
+ * @param bool $isService
+ *
+ * @return $this
+ */
+ public function setIsService($isService = true)
+ {
+ $this->isService = (bool) $isService;
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/DeleteDowntimeCommand.php b/modules/monitoring/library/Monitoring/Command/Object/DeleteDowntimeCommand.php
new file mode 100644
index 0000000..a314864
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/DeleteDowntimeCommand.php
@@ -0,0 +1,110 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+
+/**
+ * Delete a host or service downtime
+ */
+class DeleteDowntimeCommand extends IcingaCommand
+{
+ use CommandAuthor;
+
+ /**
+ * ID of the downtime that is to be deleted
+ *
+ * @var int
+ */
+ protected $downtimeId;
+
+ /**
+ * Name of the downtime (Icinga 2.4+)
+ *
+ * Required for removing the downtime via Icinga 2's API.
+ *
+ * @var string
+ */
+ protected $downtimeName;
+
+ /**
+ * Whether the command affects a service downtime
+ *
+ * @var boolean
+ */
+ protected $isService = false;
+
+ /**
+ * Get the ID of the downtime that is to be deleted
+ *
+ * @return int
+ */
+ public function getDowntimeId()
+ {
+ return $this->downtimeId;
+ }
+
+ /**
+ * Set the ID of the downtime that is to be deleted
+ *
+ * @param int $downtimeId
+ *
+ * @return $this
+ */
+ public function setDowntimeId($downtimeId)
+ {
+ $this->downtimeId = (int) $downtimeId;
+ return $this;
+ }
+
+ /**
+ * Get the name of the downtime (Icinga 2.4+)
+ *
+ * Required for removing the downtime via Icinga 2's API.
+ *
+ * @return string
+ */
+ public function getDowntimeName()
+ {
+ return $this->downtimeName;
+ }
+
+ /**
+ * Set the name of the downtime (Icinga 2.4+)
+ *
+ * Required for removing the downtime via Icinga 2's API.
+ *
+ * @param string $downtimeName
+ *
+ * @return $this
+ */
+ public function setDowntimeName($downtimeName)
+ {
+ $this->downtimeName = $downtimeName;
+ return $this;
+ }
+
+ /**
+ * Get whether the command affects a service
+ *
+ * @return bool
+ */
+ public function getIsService()
+ {
+ return $this->isService;
+ }
+
+ /**
+ * Set whether the command affects a service
+ *
+ * @param bool $isService
+ *
+ * @return $this
+ */
+ public function setIsService($isService = true)
+ {
+ $this->isService = (bool) $isService;
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/ObjectCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ObjectCommand.php
new file mode 100644
index 0000000..43ab645
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/ObjectCommand.php
@@ -0,0 +1,61 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+
+/**
+ * Base class for commands that involve a monitored object, i.e. a host or service
+ */
+abstract class ObjectCommand extends IcingaCommand
+{
+ /**
+ * Type host
+ */
+ const TYPE_HOST = MonitoredObject::TYPE_HOST;
+
+ /**
+ * Type service
+ */
+ const TYPE_SERVICE = MonitoredObject::TYPE_SERVICE;
+
+ /**
+ * Allowed Icinga object types for the command
+ *
+ * @var string[]
+ */
+ protected $allowedObjects = array();
+
+ /**
+ * Involved object
+ *
+ * @var MonitoredObject
+ */
+ protected $object;
+
+ /**
+ * Set the involved object
+ *
+ * @param MonitoredObject $object
+ *
+ * @return $this
+ */
+ public function setObject(MonitoredObject $object)
+ {
+ $object->assertOneOf($this->allowedObjects);
+ $this->object = $object;
+ return $this;
+ }
+
+ /**
+ * Get the involved object
+ *
+ * @return MonitoredObject
+ */
+ public function getObject()
+ {
+ return $this->object;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/ProcessCheckResultCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ProcessCheckResultCommand.php
new file mode 100644
index 0000000..cd2db33
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/ProcessCheckResultCommand.php
@@ -0,0 +1,176 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+use InvalidArgumentException;
+use LogicException;
+
+/**
+ * Submit a passive check result for a host or service
+ */
+class ProcessCheckResultCommand extends ObjectCommand
+{
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation.
+ */
+ protected $allowedObjects = array(
+ self::TYPE_HOST,
+ self::TYPE_SERVICE
+ );
+
+ /**
+ * Host up
+ */
+ const HOST_UP = 0;
+
+ /**
+ * Host down
+ */
+ const HOST_DOWN = 1;
+
+ /**
+ * Host unreachable
+ */
+ const HOST_UNREACHABLE = 2; // TODO: Icinga 2.x does not support submitting results with this state, yet
+
+ /**
+ * Service ok
+ */
+ const SERVICE_OK = 0;
+
+ /**
+ * Service warning
+ */
+ const SERVICE_WARNING = 1;
+
+ /**
+ * Service critical
+ */
+ const SERVICE_CRITICAL = 2;
+
+ /**
+ * Service unknown
+ */
+ const SERVICE_UNKNOWN = 3;
+
+ /**
+ * Possible status codes for passive host and service checks
+ *
+ * @var array
+ */
+ public static $statusCodes = array(
+ self::TYPE_HOST => array(
+ self::HOST_UP, self::HOST_DOWN, self::HOST_UNREACHABLE
+ ),
+ self::TYPE_SERVICE => array(
+ self::SERVICE_OK, self::SERVICE_WARNING, self::SERVICE_CRITICAL, self::SERVICE_UNKNOWN
+ )
+ );
+
+ /**
+ * Status code of the host or service check result
+ *
+ * @var int
+ */
+ protected $status;
+
+ /**
+ * Text output of the host or service check result
+ *
+ * @var string
+ */
+ protected $output;
+
+ /**
+ * Optional performance data of the host or service check result
+ *
+ * @var string
+ */
+ protected $performanceData;
+
+
+ /**
+ * Set the status code of the host or service check result
+ *
+ * @param int $status
+ *
+ * @return $this
+ *
+ * @throws LogicException If the object is null
+ * @throws InvalidArgumentException If status is not one of the valid status codes for the object's type
+ */
+ public function setStatus($status)
+ {
+ if ($this->object === null) {
+ throw new LogicException('You\'re required to call setObject() before calling setStatus()');
+ }
+ $status = (int) $status;
+ if (! in_array($status, self::$statusCodes[$this->object->getType()])) {
+ throw new InvalidArgumentException(sprintf(
+ 'The status code %u you provided is not one of the valid status codes for type %s',
+ $status,
+ $this->object->getType()
+ ));
+ }
+ $this->status = $status;
+ return $this;
+ }
+
+ /**
+ * Get the status code of the host or service check result
+ *
+ * @return int
+ */
+ public function getStatus()
+ {
+ return $this->status;
+ }
+
+ /**
+ * Set the text output of the host or service check result
+ *
+ * @param string $output
+ *
+ * @return $this
+ */
+ public function setOutput($output)
+ {
+ $this->output = (string) $output;
+ return $this;
+ }
+
+ /**
+ * Get the text output of the host or service check result
+ *
+ * @return string
+ */
+ public function getOutput()
+ {
+ return $this->output;
+ }
+
+ /**
+ * Set the performance data of the host or service check result
+ *
+ * @param string $performanceData
+ *
+ * @return $this
+ */
+ public function setPerformanceData($performanceData)
+ {
+ $this->performanceData = (string) $performanceData;
+ return $this;
+ }
+
+ /**
+ * Get the performance data of the host or service check result
+ *
+ * @return string
+ */
+ public function getPerformanceData()
+ {
+ return $this->performanceData;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/PropagateHostDowntimeCommand.php b/modules/monitoring/library/Monitoring/Command/Object/PropagateHostDowntimeCommand.php
new file mode 100644
index 0000000..3fd350c
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/PropagateHostDowntimeCommand.php
@@ -0,0 +1,48 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+/**
+ * Schedule and propagate host downtime
+ */
+class PropagateHostDowntimeCommand extends ScheduleServiceDowntimeCommand
+{
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation.
+ */
+ protected $allowedObjects = array(
+ self::TYPE_HOST
+ );
+
+ /**
+ * Whether the downtime for child hosts are all set to be triggered by this' host downtime
+ *
+ * @var bool
+ */
+ protected $triggered = false;
+
+ /**
+ * Set whether the downtime for child hosts are all set to be triggered by this' host downtime
+ *
+ * @param bool $triggered
+ *
+ * @return $this
+ */
+ public function setTriggered($triggered = true)
+ {
+ $this->triggered = (bool) $triggered;
+ return $this;
+ }
+
+ /**
+ * Get whether the downtime for child hosts are all set to be triggered by this' host downtime
+ *
+ * @return bool
+ */
+ public function getTriggered()
+ {
+ return $this->triggered;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/RemoveAcknowledgementCommand.php b/modules/monitoring/library/Monitoring/Command/Object/RemoveAcknowledgementCommand.php
new file mode 100644
index 0000000..31c8180
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/RemoveAcknowledgementCommand.php
@@ -0,0 +1,21 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+/**
+ * Remove a problem acknowledgement from a host or service
+ */
+class RemoveAcknowledgementCommand extends ObjectCommand
+{
+ use CommandAuthor;
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation.
+ */
+ protected $allowedObjects = array(
+ self::TYPE_HOST,
+ self::TYPE_SERVICE
+ );
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/ScheduleHostCheckCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ScheduleHostCheckCommand.php
new file mode 100644
index 0000000..8a0a2cb
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/ScheduleHostCheckCommand.php
@@ -0,0 +1,48 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+/**
+ * Schedule a host check
+ */
+class ScheduleHostCheckCommand extends ScheduleServiceCheckCommand
+{
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation.
+ */
+ protected $allowedObjects = array(
+ self::TYPE_HOST
+ );
+
+ /**
+ * Whether to schedule a check of all services associated with a particular host
+ *
+ * @var bool
+ */
+ protected $ofAllServices = false;
+
+ /**
+ * Set whether to schedule a check of all services associated with a particular host
+ *
+ * @param bool $ofAllServices
+ *
+ * @return $this
+ */
+ public function setOfAllServices($ofAllServices = true)
+ {
+ $this->ofAllServices = (bool) $ofAllServices;
+ return $this;
+ }
+
+ /**
+ * Get whether to schedule a check of all services associated with a particular host
+ *
+ * @return bool
+ */
+ public function getOfAllServices()
+ {
+ return $this->ofAllServices;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/ScheduleHostDowntimeCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ScheduleHostDowntimeCommand.php
new file mode 100644
index 0000000..3ac37d3
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/ScheduleHostDowntimeCommand.php
@@ -0,0 +1,75 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+/**
+ * Schedule a host downtime
+ */
+class ScheduleHostDowntimeCommand extends ScheduleServiceDowntimeCommand
+{
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation.
+ */
+ protected $allowedObjects = array(
+ self::TYPE_HOST
+ );
+
+ /**
+ * Whether to schedule a downtime for all services associated with a particular host
+ *
+ * @var bool
+ */
+ protected $forAllServices = false;
+
+ /** @var bool Whether to send the all_services API parameter */
+ protected $forAllServicesNative;
+
+ /**
+ * Set whether to schedule a downtime for all services associated with a particular host
+ *
+ * @param bool $forAllServices
+ *
+ * @return $this
+ */
+ public function setForAllServices($forAllServices = true)
+ {
+ $this->forAllServices = (bool) $forAllServices;
+ return $this;
+ }
+
+ /**
+ * Get whether to schedule a downtime for all services associated with a particular host
+ *
+ * @return bool
+ */
+ public function getForAllServices()
+ {
+ return $this->forAllServices;
+ }
+
+ /**
+ * Get whether to send the all_services API parameter
+ *
+ * @return bool
+ */
+ public function isForAllServicesNative()
+ {
+ return $this->forAllServicesNative;
+ }
+
+ /**
+ * Get whether to send the all_services API parameter
+ *
+ * @param bool $forAllServicesNative
+ *
+ * @return $this
+ */
+ public function setForAllServicesNative($forAllServicesNative = true)
+ {
+ $this->forAllServicesNative = (bool) $forAllServicesNative;
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/ScheduleServiceCheckCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ScheduleServiceCheckCommand.php
new file mode 100644
index 0000000..8880984
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/ScheduleServiceCheckCommand.php
@@ -0,0 +1,92 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+/**
+ * Schedule a service check
+ */
+class ScheduleServiceCheckCommand extends ObjectCommand
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowedObjects = array(
+ self::TYPE_SERVICE
+ );
+
+ /**
+ * Time when the next check of a host or service is to be scheduled
+ *
+ * If active checks are disabled on a host- or service-specific or program-wide basis or the host or service is
+ * already scheduled to be checked at an earlier time, etc. The check may not actually be scheduled at the time
+ * specified. This behaviour can be overridden by setting `ScheduledCheck::$forced' to true.
+ *
+ * @var int Unix timestamp
+ */
+ protected $checkTime;
+
+ /**
+ * Whether the check is forced
+ *
+ * Forced checks are performed regardless of what time it is (e.g. time period restrictions are ignored) and whether
+ * or not active checks are enabled on a host- or service-specific or program-wide basis.
+ *
+ * @var bool
+ */
+ protected $forced = false;
+
+ /**
+ * Set the time when the next check of a host or service is to be scheduled
+ *
+ * @param int $checkTime Unix timestamp
+ *
+ * @return $this
+ */
+ public function setCheckTime($checkTime)
+ {
+ $this->checkTime = (int) $checkTime;
+ return $this;
+ }
+
+ /**
+ * Get the time when the next check of a host or service is to be scheduled
+ *
+ * @return int Unix timestamp
+ */
+ public function getCheckTime()
+ {
+ return $this->checkTime;
+ }
+
+ /**
+ * Set whether the check is forced
+ *
+ * @param bool $forced
+ *
+ * @return $this
+ */
+ public function setForced($forced = true)
+ {
+ $this->forced = (bool) $forced;
+ return $this;
+ }
+
+ /**
+ * Get whether the check is forced
+ *
+ * @return bool
+ */
+ public function getForced()
+ {
+ return $this->forced;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName()
+ {
+ return 'ScheduleCheck';
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/ScheduleServiceDowntimeCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ScheduleServiceDowntimeCommand.php
new file mode 100644
index 0000000..a023ab5
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/ScheduleServiceDowntimeCommand.php
@@ -0,0 +1,190 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+/**
+ * Schedule a service downtime
+ */
+class ScheduleServiceDowntimeCommand extends AddCommentCommand
+{
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation.
+ */
+ protected $allowedObjects = array(
+ self::TYPE_SERVICE
+ );
+
+ /**
+ * Downtime starts at the exact time specified
+ *
+ * If `Downtime::$fixed' is set to false, the time between `Downtime::$start' and `Downtime::$end' at which a
+ * host or service transitions to a problem state determines the time at which the downtime actually starts.
+ * The downtime will then last for `Downtime::$duration' seconds.
+ *
+ * @var int Unix timestamp
+ */
+ protected $start;
+
+ /**
+ * Downtime ends at the exact time specified
+ *
+ * If `Downtime::$fixed' is set to false, the time between `Downtime::$start' and `Downtime::$end' at which a
+ * host or service transitions to a problem state determines the time at which the downtime actually starts.
+ * The downtime will then last for `Downtime::$duration' seconds.
+ *
+ * @var int Unix timestamp
+ */
+ protected $end;
+
+ /**
+ * Whether it's a fixed or flexible downtime
+ *
+ * @var bool
+ */
+ protected $fixed = true;
+
+ /**
+ * ID of the downtime which triggers this downtime
+ *
+ * The start of this downtime is triggered by the start of the other scheduled host or service downtime.
+ *
+ * @var int|null
+ */
+ protected $triggerId;
+
+ /**
+ * The duration in seconds the downtime must last if it's a flexible downtime
+ *
+ * If `Downtime::$fixed' is set to false, the downtime will last for the duration in seconds specified, even
+ * if the host or service recovers before the downtime expires.
+ *
+ * @var int|null
+ */
+ protected $duration;
+
+ /**
+ * Set the time when the downtime should start
+ *
+ * @param int $start Unix timestamp
+ *
+ * @return $this
+ */
+ public function setStart($start)
+ {
+ $this->start = (int) $start;
+ return $this;
+ }
+
+ /**
+ * Get the time when the downtime should start
+ *
+ * @return int Unix timestamp
+ */
+ public function getStart()
+ {
+ return $this->start;
+ }
+
+ /**
+ * Set the time when the downtime should end
+ *
+ * @param int $end Unix timestamp
+ *
+ * @return $this
+ */
+ public function setEnd($end)
+ {
+ $this->end = (int) $end;
+ return $this;
+ }
+
+ /**
+ * Get the time when the downtime should end
+ *
+ * @return int Unix timestamp
+ */
+ public function getEnd()
+ {
+ return $this->end;
+ }
+
+ /**
+ * Set whether it's a fixed or flexible downtime
+ *
+ * @param boolean $fixed
+ *
+ * @return $this
+ */
+ public function setFixed($fixed = true)
+ {
+ $this->fixed = (bool) $fixed;
+ return $this;
+ }
+
+ /**
+ * Is the downtime fixed?
+ *
+ * @return boolean
+ */
+ public function getFixed()
+ {
+ return $this->fixed;
+ }
+
+ /**
+ * Set the ID of the downtime which triggers this downtime
+ *
+ * @param int $triggerId
+ *
+ * @return $this
+ */
+ public function setTriggerId($triggerId)
+ {
+ $this->triggerId = (int) $triggerId;
+ return $this;
+ }
+
+ /**
+ * Get the ID of the downtime which triggers this downtime
+ *
+ * @return int|null
+ */
+ public function getTriggerId()
+ {
+ return $this->triggerId;
+ }
+
+ /**
+ * Set the duration in seconds the downtime must last if it's a flexible downtime
+ *
+ * @param int $duration
+ *
+ * @return $this
+ */
+ public function setDuration($duration)
+ {
+ $this->duration = (int) $duration;
+ return $this;
+ }
+
+ /**
+ * Get the duration in seconds the downtime must last if it's a flexible downtime
+ *
+ * @return int|null
+ */
+ public function getDuration()
+ {
+ return $this->duration;
+ }
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Module\Monitoring\Command\Object\IcingaCommand::getName() For the method documentation.
+ */
+ public function getName()
+ {
+ return 'ScheduleDowntime';
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/SendCustomNotificationCommand.php b/modules/monitoring/library/Monitoring/Command/Object/SendCustomNotificationCommand.php
new file mode 100644
index 0000000..ac8889c
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/SendCustomNotificationCommand.php
@@ -0,0 +1,82 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+/**
+ * Send custom notifications for a host or service
+ */
+class SendCustomNotificationCommand extends WithCommentCommand
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected $allowedObjects = array(
+ self::TYPE_HOST,
+ self::TYPE_SERVICE
+ );
+
+ /**
+ * Whether the notification is forced
+ *
+ * Forced notifications are sent out regardless of time restrictions and whether or not notifications are enabled.
+ *
+ * @var bool
+ */
+ protected $forced;
+
+ /**
+ * Whether to broadcast the notification
+ *
+ * Broadcast notifications are sent out to all normal and escalated contacts.
+ *
+ * @var bool
+ */
+ protected $broadcast;
+
+ /**
+ * Get whether to force the notification
+ *
+ * @return bool
+ */
+ public function getForced()
+ {
+ return $this->forced;
+ }
+
+ /**
+ * Set whether to force the notification
+ *
+ * @param bool $forced
+ *
+ * @return $this
+ */
+ public function setForced($forced = true)
+ {
+ $this->forced = $forced;
+ return $this;
+ }
+
+ /**
+ * Get whether to broadcast the notification
+ *
+ * @return bool
+ */
+ public function getBroadcast()
+ {
+ return $this->broadcast;
+ }
+
+ /**
+ * Set whether to broadcast the notification
+ *
+ * @param bool $broadcast
+ *
+ * @return $this
+ */
+ public function setBroadcast($broadcast = true)
+ {
+ $this->broadcast = $broadcast;
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/ToggleObjectFeatureCommand.php b/modules/monitoring/library/Monitoring/Command/Object/ToggleObjectFeatureCommand.php
new file mode 100644
index 0000000..e3ba8a2
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/ToggleObjectFeatureCommand.php
@@ -0,0 +1,113 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+/**
+ * Enable or disable a feature of an Icinga object, i.e. host or service
+ */
+class ToggleObjectFeatureCommand extends ObjectCommand
+{
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Module\Monitoring\Command\Object\ObjectCommand::$allowedObjects For the property documentation.
+ */
+ protected $allowedObjects = array(
+ self::TYPE_HOST,
+ self::TYPE_SERVICE
+ );
+
+ /**
+ * Feature for enabling or disabling active checks of a host or service
+ */
+ const FEATURE_ACTIVE_CHECKS = 'active_checks_enabled';
+
+ /**
+ * Feature for enabling or disabling passive checks of a host or service
+ */
+ const FEATURE_PASSIVE_CHECKS = 'passive_checks_enabled';
+
+ /**
+ * Feature for enabling or disabling processing of host or service checks via the OCHP command for a host or service
+ */
+ const FEATURE_OBSESSING = 'obsessing';
+
+ /**
+ * Feature for enabling or disabling notifications for a host or service
+ *
+ * Notifications will be sent out only if notifications are enabled on a program-wide basis as well.
+ */
+ const FEATURE_NOTIFICATIONS = 'notifications_enabled';
+
+ /**
+ * Feature for enabling or disabling event handler for a host or service
+ */
+ const FEATURE_EVENT_HANDLER = 'event_handler_enabled';
+
+ /**
+ * Feature for enabling or disabling flap detection for a host or service.
+ *
+ * In order to enable flap detection flap detection must be enabled on a program-wide basis as well.
+ */
+ const FEATURE_FLAP_DETECTION = 'flap_detection_enabled';
+
+ /**
+ * Feature that is to be enabled or disabled
+ *
+ * @var string
+ */
+ protected $feature;
+
+ /**
+ * Whether the feature should be enabled or disabled
+ *
+ * @var bool
+ */
+ protected $enabled;
+
+ /**
+ * Set the feature that is to be enabled or disabled
+ *
+ * @param string $feature
+ *
+ * @return $this
+ */
+ public function setFeature($feature)
+ {
+ $this->feature = (string) $feature;
+ return $this;
+ }
+
+ /**
+ * Get the feature that is to be enabled or disabled
+ *
+ * @return string
+ */
+ public function getFeature()
+ {
+ return $this->feature;
+ }
+
+ /**
+ * Set whether the feature should be enabled or disabled
+ *
+ * @param bool $enabled
+ *
+ * @return $this
+ */
+ public function setEnabled($enabled = true)
+ {
+ $this->enabled = (bool) $enabled;
+ return $this;
+ }
+
+ /**
+ * Get whether the feature should be enabled or disabled
+ *
+ * @return bool
+ */
+ public function getEnabled()
+ {
+ return $this->enabled;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Object/WithCommentCommand.php b/modules/monitoring/library/Monitoring/Command/Object/WithCommentCommand.php
new file mode 100644
index 0000000..aa2e439
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Object/WithCommentCommand.php
@@ -0,0 +1,42 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Object;
+
+/**
+ * Base class for commands adding comments
+ */
+abstract class WithCommentCommand extends ObjectCommand
+{
+ use CommandAuthor;
+
+ /**
+ * Comment
+ *
+ * @var string
+ */
+ protected $comment;
+
+ /**
+ * Set the comment
+ *
+ * @param string $comment
+ *
+ * @return $this
+ */
+ public function setComment($comment)
+ {
+ $this->comment = (string) $comment;
+ return $this;
+ }
+
+ /**
+ * Get the comment
+ *
+ * @return string
+ */
+ public function getComment()
+ {
+ return $this->comment;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php b/modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
new file mode 100644
index 0000000..3fcda6d
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
@@ -0,0 +1,322 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Renderer;
+
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Module\Monitoring\Command\IcingaApiCommand;
+use Icinga\Module\Monitoring\Command\Instance\ToggleInstanceFeatureCommand;
+use Icinga\Module\Monitoring\Command\Object\AcknowledgeProblemCommand;
+use Icinga\Module\Monitoring\Command\Object\AddCommentCommand;
+use Icinga\Module\Monitoring\Command\Object\ApiScheduleHostDowntimeCommand;
+use Icinga\Module\Monitoring\Command\Object\DeleteCommentCommand;
+use Icinga\Module\Monitoring\Command\Object\DeleteDowntimeCommand;
+use Icinga\Module\Monitoring\Command\Object\ProcessCheckResultCommand;
+use Icinga\Module\Monitoring\Command\Object\PropagateHostDowntimeCommand;
+use Icinga\Module\Monitoring\Command\Object\RemoveAcknowledgementCommand;
+use Icinga\Module\Monitoring\Command\Object\ScheduleHostDowntimeCommand;
+use Icinga\Module\Monitoring\Command\Object\ScheduleServiceCheckCommand;
+use Icinga\Module\Monitoring\Command\Object\ScheduleServiceDowntimeCommand;
+use Icinga\Module\Monitoring\Command\Object\SendCustomNotificationCommand;
+use Icinga\Module\Monitoring\Command\Object\ToggleObjectFeatureCommand;
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use InvalidArgumentException;
+
+/**
+ * Icinga command renderer for the Icinga command file
+ */
+class IcingaApiCommandRenderer implements IcingaCommandRendererInterface
+{
+ /**
+ * Name of the Icinga application object
+ *
+ * @var string
+ */
+ protected $app = 'app';
+
+ /**
+ * Get the name of the Icinga application object
+ *
+ * @return string
+ */
+ public function getApp()
+ {
+ return $this->app;
+ }
+
+ /**
+ * Set the name of the Icinga application object
+ *
+ * @param string $app
+ *
+ * @return $this
+ */
+ public function setApp($app)
+ {
+ $this->app = $app;
+
+ return $this;
+ }
+
+ /**
+ * Apply filter to query data
+ *
+ * @param array $data
+ * @param MonitoredObject $object
+ */
+ protected function applyFilter(array &$data, MonitoredObject $object)
+ {
+ if ($object->getType() === $object::TYPE_HOST) {
+ /** @var \Icinga\Module\Monitoring\Object\Host $object */
+ $data['host'] = $object->getName();
+ } else {
+ /** @var \Icinga\Module\Monitoring\Object\Service $object */
+ $data['service'] = sprintf('%s!%s', $object->getHost()->getName(), $object->getName());
+ }
+ }
+
+ /**
+ * Render a command
+ *
+ * @param IcingaCommand $command
+ *
+ * @return IcingaApiCommand
+ */
+ public function render(IcingaCommand $command)
+ {
+ $renderMethod = 'render' . $command->getName();
+ if (! method_exists($this, $renderMethod)) {
+ die($renderMethod);
+ }
+ return $this->$renderMethod($command);
+ }
+
+ public function renderAddComment(AddCommentCommand $command)
+ {
+ $endpoint = 'actions/add-comment';
+ $data = array(
+ 'author' => $command->getAuthor(),
+ 'comment' => $command->getComment()
+ );
+
+ if ($command->getExpireTime() !== null) {
+ $data['expiry'] = $command->getExpireTime();
+ }
+
+ $this->applyFilter($data, $command->getObject());
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderSendCustomNotification(SendCustomNotificationCommand $command)
+ {
+ $endpoint = 'actions/send-custom-notification';
+ $data = array(
+ 'author' => $command->getAuthor(),
+ 'comment' => $command->getComment(),
+ 'force' => $command->getForced()
+ );
+ $this->applyFilter($data, $command->getObject());
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderProcessCheckResult(ProcessCheckResultCommand $command)
+ {
+ $endpoint = 'actions/process-check-result';
+ $data = array(
+ 'exit_status' => $command->getStatus(),
+ 'plugin_output' => $command->getOutput(),
+ 'performance_data' => $command->getPerformanceData()
+ );
+ $this->applyFilter($data, $command->getObject());
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderScheduleCheck(ScheduleServiceCheckCommand $command)
+ {
+ $endpoint = 'actions/reschedule-check';
+ $data = array(
+ 'next_check' => $command->getCheckTime(),
+ 'force' => $command->getForced()
+ );
+ $this->applyFilter($data, $command->getObject());
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderScheduleDowntime(ScheduleServiceDowntimeCommand $command)
+ {
+ $endpoint = 'actions/schedule-downtime';
+ $data = array(
+ 'author' => $command->getAuthor(),
+ 'comment' => $command->getComment(),
+ 'start_time' => $command->getStart(),
+ 'end_time' => $command->getEnd(),
+ 'duration' => $command->getDuration(),
+ 'fixed' => $command->getFixed(),
+ 'trigger_name' => $command->getTriggerId()
+ );
+ $commandData = $data;
+
+ if ($command instanceof PropagateHostDowntimeCommand) {
+ $commandData['child_options'] = $command->getTriggered() ? 1 : 2;
+ } elseif ($command instanceof ApiScheduleHostDowntimeCommand) {
+ // We assume that it has previously been verified that the Icinga version is
+ // equal to or greater than 2.11.0
+ $commandData['child_options'] = $command->getChildOptions();
+ }
+
+ $allServicesCompat = false;
+ if ($command instanceof ScheduleHostDowntimeCommand) {
+ if ($command->isForAllServicesNative()) {
+ // We assume that it has previously been verified that the Icinga version is
+ // equal to or greater than 2.11.0
+ $commandData['all_services'] = $command->getForAllServices();
+ } else {
+ $allServicesCompat = $command->getForAllServices();
+ }
+ }
+
+ $this->applyFilter($commandData, $command->getObject());
+ $apiCommand = IcingaApiCommand::create($endpoint, $commandData);
+
+ if ($allServicesCompat) {
+ $commandData = $data + [
+ 'type' => 'Service',
+ 'filter' => 'host.name == host_name',
+ 'filter_vars' => [
+ 'host_name' => $command->getObject()->getName()
+ ]
+ ];
+ $apiCommand->setNext(IcingaApiCommand::create($endpoint, $commandData));
+ }
+
+ return $apiCommand;
+ }
+
+ public function renderAcknowledgeProblem(AcknowledgeProblemCommand $command)
+ {
+ $endpoint = 'actions/acknowledge-problem';
+ $data = array(
+ 'author' => $command->getAuthor(),
+ 'comment' => $command->getComment(),
+ 'sticky' => $command->getSticky(),
+ 'notify' => $command->getNotify(),
+ 'persistent' => $command->getPersistent()
+ );
+ if ($command->getExpireTime() !== null) {
+ $data['expiry'] = $command->getExpireTime();
+ }
+ $this->applyFilter($data, $command->getObject());
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderToggleObjectFeature(ToggleObjectFeatureCommand $command)
+ {
+ if ($command->getEnabled() === true) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+ switch ($command->getFeature()) {
+ case ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS:
+ $attr = 'enable_active_checks';
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS:
+ $attr = 'enable_passive_checks';
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS:
+ $attr = 'enable_notifications';
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER:
+ $attr = 'enable_event_handler';
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION:
+ $attr = 'enable_flapping';
+ break;
+ default:
+ throw new InvalidArgumentException($command->getFeature());
+ }
+ $endpoint = 'objects/';
+ $object = $command->getObject();
+ if ($object->getType() === ToggleObjectFeatureCommand::TYPE_HOST) {
+ /** @var \Icinga\Module\Monitoring\Object\Host $object */
+ $endpoint .= 'hosts';
+ } else {
+ /** @var \Icinga\Module\Monitoring\Object\Service $object */
+ $endpoint .= 'services';
+ }
+ $data = array(
+ 'attrs' => array(
+ $attr => $enabled
+ )
+ );
+ $this->applyFilter($data, $object);
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderDeleteComment(DeleteCommentCommand $command)
+ {
+ $endpoint = 'actions/remove-comment';
+ $data = [
+ 'author' => $command->getAuthor(),
+ 'comment' => $command->getCommentName()
+ ];
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderDeleteDowntime(DeleteDowntimeCommand $command)
+ {
+ $endpoint = 'actions/remove-downtime';
+ $data = [
+ 'author' => $command->getAuthor(),
+ 'downtime' => $command->getDowntimeName()
+ ];
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderRemoveAcknowledgement(RemoveAcknowledgementCommand $command)
+ {
+ $endpoint = 'actions/remove-acknowledgement';
+ $data = ['author' => $command->getAuthor()];
+ $this->applyFilter($data, $command->getObject());
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+
+ public function renderToggleInstanceFeature(ToggleInstanceFeatureCommand $command)
+ {
+ $endpoint = 'objects/icingaapplications/' . $this->getApp();
+ if ($command->getEnabled() === true) {
+ $enabled = true;
+ } else {
+ $enabled = false;
+ }
+ switch ($command->getFeature()) {
+ case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS:
+ $attr = 'enable_host_checks';
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS:
+ $attr = 'enable_service_checks';
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS:
+ $attr = 'enable_event_handlers';
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION:
+ $attr = 'enable_flapping';
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS:
+ $attr = 'enable_notifications';
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA:
+ $attr = 'enable_perfdata';
+ break;
+ default:
+ throw new InvalidArgumentException($command->getFeature());
+ }
+ $data = array(
+ 'attrs' => array(
+ $attr => $enabled
+ )
+ );
+ return IcingaApiCommand::create($endpoint, $data);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php b/modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php
new file mode 100644
index 0000000..97d1314
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php
@@ -0,0 +1,478 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Renderer;
+
+use Icinga\Module\Monitoring\Command\Instance\DisableNotificationsExpireCommand;
+use Icinga\Module\Monitoring\Command\Instance\ToggleInstanceFeatureCommand;
+use Icinga\Module\Monitoring\Command\Object\AcknowledgeProblemCommand;
+use Icinga\Module\Monitoring\Command\Object\AddCommentCommand;
+use Icinga\Module\Monitoring\Command\Object\DeleteCommentCommand;
+use Icinga\Module\Monitoring\Command\Object\DeleteDowntimeCommand;
+use Icinga\Module\Monitoring\Command\Object\ProcessCheckResultCommand;
+use Icinga\Module\Monitoring\Command\Object\PropagateHostDowntimeCommand;
+use Icinga\Module\Monitoring\Command\Object\RemoveAcknowledgementCommand;
+use Icinga\Module\Monitoring\Command\Object\ScheduleServiceCheckCommand;
+use Icinga\Module\Monitoring\Command\Object\ScheduleServiceDowntimeCommand;
+use Icinga\Module\Monitoring\Command\Object\SendCustomNotificationCommand;
+use Icinga\Module\Monitoring\Command\Object\ToggleObjectFeatureCommand;
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+use InvalidArgumentException;
+
+/**
+ * Icinga command renderer for the Icinga command file
+ */
+class IcingaCommandFileCommandRenderer implements IcingaCommandRendererInterface
+{
+ /**
+ * Escape a command string
+ *
+ * @param string $commandString
+ *
+ * @return string
+ */
+ protected function escape($commandString)
+ {
+ return str_replace(array("\r", "\n"), array('\r', '\n'), $commandString);
+ }
+
+ /**
+ * Render a command
+ *
+ * @param IcingaCommand $command
+ * @param int|null $now
+ *
+ * @return string
+ */
+ public function render(IcingaCommand $command, $now = null)
+ {
+ $renderMethod = 'render' . $command->getName();
+ if (! method_exists($this, $renderMethod)) {
+ die($renderMethod);
+ }
+ if ($now === null) {
+ $now = time();
+ }
+ return sprintf('[%u] %s', $now, $this->escape($this->$renderMethod($command)));
+ }
+
+ public function renderAddComment(AddCommentCommand $command)
+ {
+ $object = $command->getObject();
+ if ($command->getObject()->getType() === $command::TYPE_HOST) {
+ /** @var \Icinga\Module\Monitoring\Object\Host $object */
+ $commandString = sprintf(
+ 'ADD_HOST_COMMENT;%s',
+ $object->getName()
+ );
+ } else {
+ /** @var \Icinga\Module\Monitoring\Object\Service $object */
+ $commandString = sprintf(
+ 'ADD_SVC_COMMENT;%s;%s',
+ $object->getHost()->getName(),
+ $object->getName()
+ );
+ }
+ return sprintf(
+ '%s;%u;%s;%s',
+ $commandString,
+ $command->getPersistent(),
+ $command->getAuthor(),
+ $command->getComment()
+ );
+ }
+
+ public function renderSendCustomNotification(SendCustomNotificationCommand $command)
+ {
+ $object = $command->getObject();
+ if ($command->getObject()->getType() === $command::TYPE_HOST) {
+ /** @var \Icinga\Module\Monitoring\Object\Host $object */
+ $commandString = sprintf(
+ 'SEND_CUSTOM_HOST_NOTIFICATION;%s',
+ $object->getName()
+ );
+ } else {
+ /** @var \Icinga\Module\Monitoring\Object\Service $object */
+ $commandString = sprintf(
+ 'SEND_CUSTOM_SVC_NOTIFICATION;%s;%s',
+ $object->getHost()->getName(),
+ $object->getName()
+ );
+ }
+ $options = 0; // 0 for no options
+ if ($command->getBroadcast() === true) {
+ $options |= 1;
+ }
+ if ($command->getForced() === true) {
+ $options |= 2;
+ }
+ return sprintf(
+ '%s;%u;%s;%s',
+ $commandString,
+ $options,
+ $command->getAuthor(),
+ $command->getComment()
+ );
+ }
+
+ public function renderProcessCheckResult(ProcessCheckResultCommand $command)
+ {
+ $object = $command->getObject();
+ if ($command->getObject()->getType() === $command::TYPE_HOST) {
+ /** @var \Icinga\Module\Monitoring\Object\Host $object */
+ $commandString = sprintf(
+ 'PROCESS_HOST_CHECK_RESULT;%s',
+ $object->getName()
+ );
+ } else {
+ /** @var \Icinga\Module\Monitoring\Object\Service $object */
+ $commandString = sprintf(
+ 'PROCESS_SERVICE_CHECK_RESULT;%s;%s',
+ $object->getHost()->getName(),
+ $object->getName()
+ );
+ }
+ $output = $command->getOutput();
+ if ($command->getPerformanceData() !== null) {
+ $output .= '|' . $command->getPerformanceData();
+ }
+ return sprintf(
+ '%s;%u;%s',
+ $commandString,
+ $command->getStatus(),
+ $output
+ );
+ }
+
+ public function renderScheduleCheck(ScheduleServiceCheckCommand $command)
+ {
+ $object = $command->getObject();
+ if ($command->getObject()->getType() === $command::TYPE_HOST) {
+ /** @var \Icinga\Module\Monitoring\Object\Host $object */
+ /** @var \Icinga\Module\Monitoring\Command\Object\ScheduleHostCheckCommand $command */
+ if ($command->getOfAllServices() === true) {
+ if ($command->getForced() === true) {
+ $commandName = 'SCHEDULE_FORCED_HOST_SVC_CHECKS';
+ } else {
+ $commandName = 'SCHEDULE_HOST_SVC_CHECKS';
+ }
+ } else {
+ if ($command->getForced() === true) {
+ $commandName = 'SCHEDULE_FORCED_HOST_CHECK';
+ } else {
+ $commandName = 'SCHEDULE_HOST_CHECK';
+ }
+ }
+ $commandString = sprintf(
+ '%s;%s',
+ $commandName,
+ $object->getName()
+ );
+ } else {
+ /** @var \Icinga\Module\Monitoring\Object\Service $object */
+ $commandString = sprintf(
+ '%s;%s;%s',
+ $command->getForced() === true ? 'SCHEDULE_FORCED_SVC_CHECK' : 'SCHEDULE_SVC_CHECK',
+ $object->getHost()->getName(),
+ $object->getName()
+ );
+ }
+ return sprintf(
+ '%s;%u',
+ $commandString,
+ $command->getCheckTime()
+ );
+ }
+
+ public function renderScheduleDowntime(ScheduleServiceDowntimeCommand $command)
+ {
+ $object = $command->getObject();
+ if ($command->getObject()->getType() === $command::TYPE_HOST) {
+ /** @var \Icinga\Module\Monitoring\Object\Host $object */
+ /** @var \Icinga\Module\Monitoring\Command\Object\ScheduleHostDowntimeCommand $command */
+ if ($command instanceof PropagateHostDowntimeCommand) {
+ /** @var \Icinga\Module\Monitoring\Command\Object\PropagateHostDowntimeCommand $command */
+ $commandName = $command->getTriggered() === true ? 'SCHEDULE_AND_PROPAGATE_TRIGGERED_HOST_DOWNTIME'
+ : 'SCHEDULE_AND_PROPAGATE_HOST_DOWNTIME';
+ } elseif ($command->getForAllServices() === true) {
+ $commandName = 'SCHEDULE_HOST_SVC_DOWNTIME';
+ } else {
+ $commandName = 'SCHEDULE_HOST_DOWNTIME';
+ }
+ $commandString = sprintf(
+ '%s;%s',
+ $commandName,
+ $object->getName()
+ );
+ } else {
+ /** @var \Icinga\Module\Monitoring\Object\Service $object */
+ $commandString = sprintf(
+ '%s;%s;%s',
+ 'SCHEDULE_SVC_DOWNTIME',
+ $object->getHost()->getName(),
+ $object->getName()
+ );
+ }
+ return sprintf(
+ '%s;%u;%u;%u;%u;%u;%s;%s',
+ $commandString,
+ $command->getStart(),
+ $command->getEnd(),
+ $command->getFixed(),
+ $command->getTriggerId(),
+ $command->getDuration(),
+ $command->getAuthor(),
+ $command->getComment()
+ );
+ }
+
+ public function renderAcknowledgeProblem(AcknowledgeProblemCommand $command)
+ {
+ $object = $command->getObject();
+ if ($command->getObject()->getType() === $command::TYPE_HOST) {
+ /** @var \Icinga\Module\Monitoring\Object\Host $object */
+ $commandString = sprintf(
+ '%s;%s',
+ $command->getExpireTime() !== null ? 'ACKNOWLEDGE_HOST_PROBLEM_EXPIRE' : 'ACKNOWLEDGE_HOST_PROBLEM',
+ $object->getName()
+ );
+ } else {
+ /** @var \Icinga\Module\Monitoring\Object\Service $object */
+ $commandString = sprintf(
+ '%s;%s;%s',
+ $command->getExpireTime() !== null ? 'ACKNOWLEDGE_SVC_PROBLEM_EXPIRE' : 'ACKNOWLEDGE_SVC_PROBLEM',
+ $object->getHost()->getName(),
+ $object->getName()
+ );
+ }
+ $commandString = sprintf(
+ '%s;%u;%u;%u',
+ $commandString,
+ $command->getSticky() ? 2 : 0,
+ $command->getNotify(),
+ $command->getPersistent()
+ );
+ if ($command->getExpireTime() !== null) {
+ $commandString = sprintf(
+ '%s;%u',
+ $commandString,
+ $command->getExpireTime()
+ );
+ }
+ return sprintf(
+ '%s;%s;%s',
+ $commandString,
+ $command->getAuthor(),
+ $command->getComment()
+ );
+ }
+
+ public function renderToggleObjectFeature(ToggleObjectFeatureCommand $command)
+ {
+ if ($command->getEnabled() === true) {
+ $commandPrefix = 'ENABLE';
+ } else {
+ $commandPrefix = 'DISABLE';
+ }
+ switch ($command->getFeature()) {
+ case ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS:
+ $commandFormat = sprintf('%s_%%s_CHECK', $commandPrefix);
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS:
+ $commandFormat = sprintf('%s_PASSIVE_%%s_CHECKS', $commandPrefix);
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_OBSESSING:
+ if ($command->getEnabled() === true) {
+ $commandPrefix = 'START';
+ } else {
+ $commandPrefix = 'STOP';
+ }
+ $commandFormat = sprintf('%s_OBSESSING_OVER_%%s', $commandPrefix);
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS:
+ $commandFormat = sprintf('%s_%%s_NOTIFICATIONS', $commandPrefix);
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER:
+ $commandFormat = sprintf('%s_%%s_EVENT_HANDLER', $commandPrefix);
+ break;
+ case ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION:
+ $commandFormat = sprintf('%s_%%s_FLAP_DETECTION', $commandPrefix);
+ break;
+ default:
+ throw new InvalidArgumentException($command->getFeature());
+ }
+ $object = $command->getObject();
+ if ($object->getType() === ToggleObjectFeatureCommand::TYPE_HOST) {
+ /** @var \Icinga\Module\Monitoring\Object\Host $object */
+ $commandString = sprintf(
+ $commandFormat . ';%s',
+ 'HOST',
+ $object->getName()
+ );
+ } else {
+ /** @var \Icinga\Module\Monitoring\Object\Service $object */
+ $commandString = sprintf(
+ $commandFormat . ';%s;%s',
+ 'SVC',
+ $object->getHost()->getName(),
+ $object->getName()
+ );
+ }
+ return $commandString;
+ }
+
+ public function renderDeleteComment(DeleteCommentCommand $command)
+ {
+ return sprintf(
+ '%s;%u',
+ $command->getIsService() ? 'DEL_SVC_COMMENT' : 'DEL_HOST_COMMENT',
+ $command->getCommentId()
+ );
+ }
+
+ public function renderDeleteDowntime(DeleteDowntimeCommand $command)
+ {
+ return sprintf(
+ '%s;%u',
+ $command->getIsService() ? 'DEL_SVC_DOWNTIME' : 'DEL_HOST_DOWNTIME',
+ $command->getDowntimeId()
+ );
+ }
+
+ public function renderRemoveAcknowledgement(RemoveAcknowledgementCommand $command)
+ {
+ $object = $command->getObject();
+ if ($command->getObject()->getType() === $command::TYPE_HOST) {
+ /** @var \Icinga\Module\Monitoring\Object\Host $object */
+ $commandString = sprintf(
+ '%s;%s',
+ 'REMOVE_HOST_ACKNOWLEDGEMENT',
+ $object->getName()
+ );
+ } else {
+ /** @var \Icinga\Module\Monitoring\Object\Service $object */
+ $commandString = sprintf(
+ '%s;%s;%s',
+ 'REMOVE_SVC_ACKNOWLEDGEMENT',
+ $object->getHost()->getName(),
+ $object->getName()
+ );
+ }
+ return $commandString;
+ }
+
+ public function renderDisableNotificationsExpire(DisableNotificationsExpireCommand $command)
+ {
+ return sprintf(
+ '%s;%u;%u',
+ 'DISABLE_NOTIFICATIONS_EXPIRE_TIME',
+ time(),
+ $command->getExpireTime()
+ );
+ }
+
+ public function renderToggleInstanceFeature(ToggleInstanceFeatureCommand $command)
+ {
+ switch ($command->getFeature()) {
+ case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS:
+ case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS:
+ case ToggleInstanceFeatureCommand::FEATURE_HOST_OBSESSING:
+ case ToggleInstanceFeatureCommand::FEATURE_SERVICE_OBSESSING:
+ case ToggleInstanceFeatureCommand::FEATURE_PASSIVE_HOST_CHECKS:
+ case ToggleInstanceFeatureCommand::FEATURE_PASSIVE_SERVICE_CHECKS:
+ if ($command->getEnabled() === true) {
+ $commandPrefix = 'START';
+ } else {
+ $commandPrefix = 'STOP';
+ }
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS:
+ case ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION:
+ case ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS:
+ case ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA:
+ if ($command->getEnabled() === true) {
+ $commandPrefix = 'ENABLE';
+ } else {
+ $commandPrefix = 'DISABLE';
+ }
+ break;
+ default:
+ throw new InvalidArgumentException($command->getFeature());
+ }
+ switch ($command->getFeature()) {
+ case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS:
+ $commandString = sprintf(
+ '%s_%s',
+ $commandPrefix,
+ 'EXECUTING_HOST_CHECKS'
+ );
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS:
+ $commandString = sprintf(
+ '%s_%s',
+ $commandPrefix,
+ 'EXECUTING_SVC_CHECKS'
+ );
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS:
+ $commandString = sprintf(
+ '%s_%s',
+ $commandPrefix,
+ 'EVENT_HANDLERS'
+ );
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION:
+ $commandString = sprintf(
+ '%s_%s',
+ $commandPrefix,
+ 'FLAP_DETECTION'
+ );
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS:
+ $commandString = sprintf(
+ '%s_%s',
+ $commandPrefix,
+ 'NOTIFICATIONS'
+ );
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_HOST_OBSESSING:
+ $commandString = sprintf(
+ '%s_%s',
+ $commandPrefix,
+ 'OBSESSING_OVER_HOST_CHECKS'
+ );
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_SERVICE_OBSESSING:
+ $commandString = sprintf(
+ '%s_%s',
+ $commandPrefix,
+ 'OBSESSING_OVER_SVC_CHECKS'
+ );
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_PASSIVE_HOST_CHECKS:
+ $commandString = sprintf(
+ '%s_%s',
+ $commandPrefix,
+ 'ACCEPTING_PASSIVE_HOST_CHECKS'
+ );
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_PASSIVE_SERVICE_CHECKS:
+ $commandString = sprintf(
+ '%s_%s',
+ $commandPrefix,
+ 'ACCEPTING_PASSIVE_SVC_CHECKS'
+ );
+ break;
+ case ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA:
+ $commandString = sprintf(
+ '%s_%s',
+ $commandPrefix,
+ 'PERFORMANCE_DATA'
+ );
+ break;
+ default:
+ throw new InvalidArgumentException($command->getFeature());
+ }
+ return $commandString;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandRendererInterface.php b/modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandRendererInterface.php
new file mode 100644
index 0000000..e3ef6ba
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandRendererInterface.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Renderer;
+
+/**
+ * Interface for Icinga command renderer
+ */
+interface IcingaCommandRendererInterface
+{
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php b/modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php
new file mode 100644
index 0000000..06e6afd
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php
@@ -0,0 +1,291 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Transport;
+
+use Icinga\Application\Hook\AuditHook;
+use Icinga\Application\Logger;
+use Icinga\Exception\Json\JsonDecodeException;
+use Icinga\Module\Monitoring\Command\IcingaApiCommand;
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+use Icinga\Module\Monitoring\Command\Renderer\IcingaApiCommandRenderer;
+use Icinga\Module\Monitoring\Exception\CommandTransportException;
+use Icinga\Module\Monitoring\Exception\CurlException;
+use Icinga\Module\Monitoring\Web\Rest\RestRequest;
+use Icinga\Util\Json;
+
+/**
+ * Command transport over Icinga 2's REST API
+ */
+class ApiCommandTransport implements CommandTransportInterface
+{
+ /**
+ * Transport identifier
+ */
+ const TRANSPORT = 'api';
+
+ /**
+ * API host
+ *
+ * @var string
+ */
+ protected $host;
+
+ /**
+ * API password
+ *
+ * @var string
+ */
+ protected $password;
+
+ /**
+ * API port
+ *
+ * @var int
+ */
+ protected $port = 5665;
+
+ /**
+ * Command renderer
+ *
+ * @var IcingaApiCommandRenderer
+ */
+ protected $renderer;
+
+ /**
+ * API username
+ *
+ * @var string
+ */
+ protected $username;
+
+ /**
+ * Create a new API command transport
+ */
+ public function __construct()
+ {
+ $this->renderer = new IcingaApiCommandRenderer();
+ }
+
+ /**
+ * Set the name of the Icinga application object
+ *
+ * @param string $app
+ *
+ * @return $this
+ */
+ public function setApp($app)
+ {
+ $this->renderer->setApp($app);
+
+ return $this;
+ }
+
+ /**
+ * Get the API host
+ *
+ * @return string
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * Set the API host
+ *
+ * @param string $host
+ *
+ * @return $this
+ */
+ public function setHost($host)
+ {
+ $this->host = $host;
+
+ return $this;
+ }
+
+ /**
+ * Get the API password
+ *
+ * @return string
+ */
+ public function getPassword()
+ {
+ return $this->password;
+ }
+
+ /**
+ * Set the API password
+ *
+ * @param string $password
+ *
+ * @return $this
+ */
+ public function setPassword($password)
+ {
+ $this->password = $password;
+
+ return $this;
+ }
+
+ /**
+ * Get the API port
+ *
+ * @return int
+ */
+ public function getPort()
+ {
+ return $this->port;
+ }
+
+ /**
+ * Set the API port
+ *
+ * @param int $port
+ *
+ * @return $this
+ */
+ public function setPort($port)
+ {
+ $this->port = (int) $port;
+
+ return $this;
+ }
+
+ /**
+ * Get the API username
+ *
+ * @return string
+ */
+ public function getUsername()
+ {
+ return $this->username;
+ }
+
+ /**
+ * Set the API username
+ *
+ * @param string $username
+ *
+ * @return $this
+ */
+ public function setUsername($username)
+ {
+ $this->username = $username;
+
+ return $this;
+ }
+
+ /**
+ * Get URI for endpoint
+ *
+ * @param string $endpoint
+ *
+ * @return string
+ */
+ protected function getUriFor($endpoint)
+ {
+ return sprintf('https://%s:%u/v1/%s', $this->getHost(), $this->getPort(), $endpoint);
+ }
+
+ protected function sendCommand(IcingaApiCommand $command)
+ {
+ Logger::debug(
+ 'Sending Icinga command "%s" to the API "%s:%u"',
+ $command->getEndpoint(),
+ $this->getHost(),
+ $this->getPort()
+ );
+
+ $data = $command->getData();
+ $payload = Json::encode($data);
+ AuditHook::logActivity(
+ 'monitoring/command',
+ "Issued command {$command->getEndpoint()} with the following payload: $payload",
+ $data
+ );
+
+ try {
+ $response = RestRequest::post($this->getUriFor($command->getEndpoint()))
+ ->authenticateWith($this->getUsername(), $this->getPassword())
+ ->sendJson()
+ ->noStrictSsl()
+ ->setPayload($command->getData())
+ ->send();
+ } catch (JsonDecodeException $e) {
+ throw new CommandTransportException(
+ 'Got invalid JSON response from the Icinga 2 API: %s',
+ $e->getMessage()
+ );
+ }
+
+ if (isset($response['error'])) {
+ throw new CommandTransportException(
+ 'Can\'t send external Icinga command: %u %s',
+ $response['error'],
+ $response['status']
+ );
+ }
+ $result = array_pop($response['results']);
+ if (! empty($result)
+ && ($result['code'] < 200 || $result['code'] >= 300)
+ ) {
+ throw new CommandTransportException(
+ 'Can\'t send external Icinga command: %u %s',
+ $result['code'],
+ $result['status']
+ );
+ }
+ if ($command->hasNext()) {
+ $this->sendCommand($command->getNext());
+ }
+ }
+
+ /**
+ * Send the Icinga command over the Icinga 2 API
+ *
+ * @param IcingaCommand $command
+ * @param int|null $now
+ *
+ * @throws CommandTransportException
+ */
+ public function send(IcingaCommand $command, $now = null)
+ {
+ $this->sendCommand($this->renderer->render($command));
+ }
+
+ /**
+ * Try to connect to the API
+ *
+ * @throws CommandTransportException In case of failure
+ */
+ public function probe()
+ {
+ $request = RestRequest::get($this->getUriFor(null))
+ ->authenticateWith($this->getUsername(), $this->getPassword())
+ ->noStrictSsl();
+
+ try {
+ $response = $request->send();
+ } catch (CurlException $e) {
+ throw new CommandTransportException(
+ 'Couldn\'t connect to the Icinga 2 API: %s',
+ $e->getMessage()
+ );
+ } catch (JsonDecodeException $e) {
+ throw new CommandTransportException(
+ 'Got invalid JSON response from the Icinga 2 API: %s',
+ $e->getMessage()
+ );
+ }
+
+ if (isset($response['error'])) {
+ throw new CommandTransportException(
+ 'Can\'t connect to the Icinga 2 API: %u %s',
+ $response['error'],
+ $response['status']
+ );
+ }
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php
new file mode 100644
index 0000000..4086dec
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php
@@ -0,0 +1,170 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Transport;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+use Icinga\Module\Monitoring\Command\Object\ObjectCommand;
+use Icinga\Module\Monitoring\Exception\CommandTransportException;
+
+/**
+ * Command transport
+ *
+ * This class is subject to change as we do not have environments yet (#4471).
+ */
+class CommandTransport implements CommandTransportInterface
+{
+ /**
+ * Transport configuration
+ *
+ * @var Config
+ */
+ protected static $config;
+
+ /**
+ * Get transport configuration
+ *
+ * @return Config
+ *
+ * @throws ConfigurationError
+ */
+ public static function getConfig()
+ {
+ if (static::$config === null) {
+ $config = Config::module('monitoring', 'commandtransports');
+ if ($config->isEmpty()) {
+ throw new ConfigurationError(
+ mt('monitoring', 'No command transports have been configured in "%s".'),
+ $config->getConfigFile()
+ );
+ }
+
+ static::$config = $config;
+ }
+
+ return static::$config;
+ }
+
+ /**
+ * Create a transport from config
+ *
+ * @param ConfigObject $config
+ *
+ * @return LocalCommandFile|RemoteCommandFile|ApiCommandTransport
+ *
+ * @throws ConfigurationError
+ */
+ public static function createTransport(ConfigObject $config)
+ {
+ $config = clone $config;
+ switch (strtolower($config->transport)) {
+ case RemoteCommandFile::TRANSPORT:
+ $transport = new RemoteCommandFile();
+ break;
+ case ApiCommandTransport::TRANSPORT:
+ $transport = new ApiCommandTransport();
+ break;
+ case LocalCommandFile::TRANSPORT:
+ case '': // Casting null to string is the empty string
+ $transport = new LocalCommandFile();
+ break;
+ default:
+ throw new ConfigurationError(
+ mt(
+ 'monitoring',
+ 'Cannot create command transport "%s". Invalid transport'
+ . ' defined in "%s". Use one of "%s", "%s" or "%s".'
+ ),
+ $config->transport,
+ static::getConfig()->getConfigFile(),
+ LocalCommandFile::TRANSPORT,
+ RemoteCommandFile::TRANSPORT,
+ ApiCommandTransport::TRANSPORT
+ );
+ }
+
+ unset($config->transport);
+ foreach ($config as $key => $value) {
+ $method = 'set' . ucfirst($key);
+ if (! method_exists($transport, $method)) {
+ // Ignore settings from config that don't have a setter on the transport instead of throwing an
+ // exception here because the transport should throw an exception if it's not fully set up
+ // when being about to send a command
+ continue;
+ }
+
+ $transport->$method($value);
+ }
+
+ return $transport;
+ }
+
+ /**
+ * Send the given command over an appropriate Icinga command transport
+ *
+ * This will try one configured transport after another until the command has been successfully sent.
+ *
+ * @param IcingaCommand $command The command to send
+ * @param int|null $now Timestamp of the command or null for now
+ *
+ * @throws CommandTransportException If sending the Icinga command failed
+ */
+ public function send(IcingaCommand $command, $now = null)
+ {
+ $errors = array();
+
+ foreach (static::getConfig() as $name => $transportConfig) {
+ $transport = static::createTransport($transportConfig);
+ if ($this->transferPossible($command, $transport)) {
+ try {
+ $transport->send($command, $now);
+ } catch (Exception $e) {
+ Logger::error($e);
+ $errors[] = sprintf('%s: %s.', $name, rtrim($e->getMessage(), '.'));
+ continue; // Try the next transport
+ }
+
+ return; // The command was successfully sent
+ }
+ }
+
+ if (! empty($errors)) {
+ throw new CommandTransportException(implode("\n", $errors));
+ }
+
+ throw new CommandTransportException(
+ mt(
+ 'monitoring',
+ 'Failed to send external Icinga command. No transport has been configured'
+ . ' for this instance. Please contact your Icinga Web administrator.'
+ )
+ );
+ }
+
+ /**
+ * Return whether it is possible to send the given command using the given transport
+ *
+ * @param IcingaCommand $command
+ * @param CommandTransportInterface $transport
+ *
+ * @return bool
+ */
+ protected function transferPossible($command, $transport)
+ {
+ if (! method_exists($transport, 'getInstance') || !$command instanceof ObjectCommand) {
+ return true;
+ }
+
+ $transportInstance = $transport->getInstance();
+ if (! $transportInstance || $transportInstance === 'none') {
+ return true;
+ }
+
+ return strtolower($transportInstance) === strtolower($command->getObject()->instance_name);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Transport/CommandTransportInterface.php b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransportInterface.php
new file mode 100644
index 0000000..e9cb086
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransportInterface.php
@@ -0,0 +1,22 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Transport;
+
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+
+/**
+ * Interface for Icinga command transports
+ */
+interface CommandTransportInterface
+{
+ /**
+ * Send an Icinga command over the Icinga command transport
+ *
+ * @param IcingaCommand $command The command to send
+ * @param int|null $now Timestamp of the command or null for now
+ *
+ * @throws \Icinga\Module\Monitoring\Exception\CommandTransportException If sending the Icinga command failed
+ */
+ public function send(IcingaCommand $command, $now = null);
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php b/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php
new file mode 100644
index 0000000..891a46f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php
@@ -0,0 +1,168 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Transport;
+
+use Exception;
+use RuntimeException;
+use Icinga\Application\Logger;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+use Icinga\Module\Monitoring\Command\Renderer\IcingaCommandFileCommandRenderer;
+use Icinga\Module\Monitoring\Exception\CommandTransportException;
+use Icinga\Util\File;
+
+/**
+ * A local Icinga command file
+ */
+class LocalCommandFile implements CommandTransportInterface
+{
+ /**
+ * Transport identifier
+ */
+ const TRANSPORT = 'local';
+
+ /**
+ * The name of the Icinga instance this transport will transfer commands to
+ *
+ * @var string
+ */
+ protected $instanceName;
+
+ /**
+ * Path to the icinga command file
+ *
+ * @var String
+ */
+ protected $path;
+
+ /**
+ * Mode used to open the icinga command file
+ *
+ * @var string
+ */
+ protected $openMode = 'wn';
+
+ /**
+ * Command renderer
+ *
+ * @var IcingaCommandFileCommandRenderer
+ */
+ protected $renderer;
+
+ /**
+ * Create a new local command file command transport
+ */
+ public function __construct()
+ {
+ $this->renderer = new IcingaCommandFileCommandRenderer();
+ }
+
+ /**
+ * Set the name of the Icinga instance this transport will transfer commands to
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setInstance($name)
+ {
+ $this->instanceName = $name;
+ return $this;
+ }
+
+ /**
+ * Return the name of the Icinga instance this transport will transfer commands to
+ *
+ * @return string
+ */
+ public function getInstance()
+ {
+ return $this->instanceName;
+ }
+
+ /**
+ * Set the path to the local Icinga command file
+ *
+ * @param string $path
+ *
+ * @return $this
+ */
+ public function setPath($path)
+ {
+ $this->path = (string) $path;
+ return $this;
+ }
+
+ /**
+ * Get the path to the local Icinga command file
+ *
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * Set the mode used to open the icinga command file
+ *
+ * @param string $openMode
+ *
+ * @return $this
+ */
+ public function setOpenMode($openMode)
+ {
+ $this->openMode = (string) $openMode;
+ return $this;
+ }
+
+ /**
+ * Get the mode used to open the icinga command file
+ *
+ * @return string
+ */
+ public function getOpenMode()
+ {
+ return $this->openMode;
+ }
+
+ /**
+ * Write the command to the local Icinga command file
+ *
+ * @param IcingaCommand $command
+ * @param int|null $now
+ *
+ * @throws ConfigurationError
+ * @throws CommandTransportException
+ */
+ public function send(IcingaCommand $command, $now = null)
+ {
+ if (! isset($this->path)) {
+ throw new ConfigurationError(
+ 'Can\'t send external Icinga Command. Path to the local command file is missing'
+ );
+ }
+ $commandString = $this->renderer->render($command, $now);
+ Logger::debug(
+ 'Sending external Icinga command "%s" to the local command file "%s"',
+ $commandString,
+ $this->path
+ );
+ try {
+ $file = new File($this->path, $this->openMode);
+ $file->fwrite($commandString . "\n");
+ } catch (Exception $e) {
+ $message = $e->getMessage();
+ if ($e instanceof RuntimeException && ($pos = strrpos($message, ':')) !== false) {
+ // Assume RuntimeException thrown by SplFileObject in the format: __METHOD__ . "({$filename}): Message"
+ $message = substr($message, $pos + 1);
+ }
+ throw new CommandTransportException(
+ 'Can\'t send external Icinga command to the local command file "%s": %s',
+ $this->path,
+ $message
+ );
+ }
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php b/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
new file mode 100644
index 0000000..8619e87
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
@@ -0,0 +1,465 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Command\Transport;
+
+use Icinga\Application\Logger;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Module\Monitoring\Command\IcingaCommand;
+use Icinga\Module\Monitoring\Command\Renderer\IcingaCommandFileCommandRenderer;
+use Icinga\Module\Monitoring\Exception\CommandTransportException;
+
+/**
+ * A remote Icinga command file
+ *
+ * Key-based SSH login must be possible for the user to log in as on the remote host
+ */
+class RemoteCommandFile implements CommandTransportInterface
+{
+ /**
+ * Transport identifier
+ */
+ const TRANSPORT = 'remote';
+
+ /**
+ * The name of the Icinga instance this transport will transfer commands to
+ *
+ * @var string
+ */
+ protected $instanceName;
+
+ /**
+ * Remote host
+ *
+ * @var string
+ */
+ protected $host;
+
+ /**
+ * Port to connect to on the remote host
+ *
+ * @var int
+ */
+ protected $port = 22;
+
+ /**
+ * User to log in as on the remote host
+ *
+ * Defaults to current PHP process' user
+ *
+ * @var string
+ */
+ protected $user;
+
+ /**
+ * Path to the private key file for the key-based authentication
+ *
+ * @var string
+ */
+ protected $privateKey;
+
+ /**
+ * Path to the Icinga command file on the remote host
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * Command renderer
+ *
+ * @var IcingaCommandFileCommandRenderer
+ */
+ protected $renderer;
+
+ /**
+ * SSH subprocess pipes
+ *
+ * @var array
+ */
+ protected $sshPipes;
+
+ /**
+ * SSH subprocess
+ *
+ * @var resource
+ */
+ protected $sshProcess;
+
+ /**
+ * Create a new remote command file command transport
+ */
+ public function __construct()
+ {
+ $this->renderer = new IcingaCommandFileCommandRenderer();
+ }
+
+ /**
+ * Set the name of the Icinga instance this transport will transfer commands to
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setInstance($name)
+ {
+ $this->instanceName = $name;
+ return $this;
+ }
+
+ /**
+ * Return the name of the Icinga instance this transport will transfer commands to
+ *
+ * @return string
+ */
+ public function getInstance()
+ {
+ return $this->instanceName;
+ }
+
+ /**
+ * Set the remote host
+ *
+ * @param string $host
+ *
+ * @return $this
+ */
+ public function setHost($host)
+ {
+ $this->host = (string) $host;
+ return $this;
+ }
+
+ /**
+ * Get the remote host
+ *
+ * @return string
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * Set the port to connect to on the remote host
+ *
+ * @param int $port
+ *
+ * @return $this
+ */
+ public function setPort($port)
+ {
+ $this->port = (int) $port;
+ return $this;
+ }
+
+ /**
+ * Get the port to connect on the remote host
+ *
+ * @return int
+ */
+ public function getPort()
+ {
+ return $this->port;
+ }
+
+ /**
+ * Set the user to log in as on the remote host
+ *
+ * @param string $user
+ *
+ * @return $this
+ */
+ public function setUser($user)
+ {
+ $this->user = (string) $user;
+ return $this;
+ }
+
+ /**
+ * Get the user to log in as on the remote host
+ *
+ * Defaults to current PHP process' user
+ *
+ * @return string|null
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * Set the path to the private key file
+ *
+ * @param string $privateKey
+ *
+ * @return $this
+ */
+ public function setPrivateKey($privateKey)
+ {
+ $this->privateKey = (string) $privateKey;
+ return $this;
+ }
+
+ /**
+ * Get the path to the private key
+ *
+ * @return string
+ */
+ public function getPrivateKey()
+ {
+ return $this->privateKey;
+ }
+
+ /**
+ * Use a given resource to set the user and the key
+ *
+ * @param ?string $resource
+ *
+ * @throws ConfigurationError
+ */
+ public function setResource($resource = null)
+ {
+ $config = ResourceFactory::getResourceConfig($resource);
+
+ if (! isset($config->user)) {
+ throw new ConfigurationError(
+ t("Can't send external Icinga Command. Remote user is missing")
+ );
+ }
+ if (! isset($config->private_key)) {
+ throw new ConfigurationError(
+ t("Can't send external Icinga Command. The private key for the remote user is missing")
+ );
+ }
+
+ $this->setUser($config->user);
+ $this->setPrivateKey($config->private_key);
+ }
+
+ /**
+ * Set the path to the Icinga command file on the remote host
+ *
+ * @param string $path
+ *
+ * @return $this
+ */
+ public function setPath($path)
+ {
+ $this->path = (string) $path;
+ return $this;
+ }
+
+ /**
+ * Get the path to the Icinga command file on the remote host
+ *
+ * @return string
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ /**
+ * Write the command to the Icinga command file on the remote host
+ *
+ * @param IcingaCommand $command
+ * @param int|null $now
+ *
+ * @throws ConfigurationError
+ * @throws CommandTransportException
+ */
+ public function send(IcingaCommand $command, $now = null)
+ {
+ if (! isset($this->path)) {
+ throw new ConfigurationError(
+ 'Can\'t send external Icinga Command. Path to the remote command file is missing'
+ );
+ }
+ if (! isset($this->host)) {
+ throw new ConfigurationError('Can\'t send external Icinga Command. Remote host is missing');
+ }
+ $commandString = $this->renderer->render($command, $now);
+ Logger::debug(
+ 'Sending external Icinga command "%s" to the remote command file "%s:%u%s"',
+ $commandString,
+ $this->host,
+ $this->port,
+ $this->path
+ );
+ return $this->sendCommandString($commandString);
+ }
+
+ /**
+ * Get the SSH command
+ *
+ * @return string
+ */
+ protected function sshCommand()
+ {
+ $cmd = sprintf(
+ 'exec ssh -o BatchMode=yes -p %u',
+ $this->port
+ );
+ // -o BatchMode=yes for disabling interactive authentication methods
+
+ if (isset($this->user)) {
+ $cmd .= ' -l ' . escapeshellarg($this->user);
+ }
+
+ if (isset($this->privateKey)) {
+ // TODO: StrictHostKeyChecking=no for compat only, must be removed
+ $cmd .= ' -o StrictHostKeyChecking=no'
+ . ' -i ' . escapeshellarg($this->privateKey);
+ }
+
+ $cmd .= sprintf(
+ ' %s "cat > %s"',
+ escapeshellarg($this->host),
+ escapeshellarg($this->path)
+ );
+
+ return $cmd;
+ }
+
+ /**
+ * Send the command over SSH
+ *
+ * @param string $commandString
+ *
+ * @throws CommandTransportException
+ */
+ protected function sendCommandString($commandString)
+ {
+ if ($this->isSshAlive()) {
+ $ret = fwrite($this->sshPipes[0], $commandString . "\n");
+ if ($ret === false) {
+ $this->throwSshFailure('Cannot write to the remote command pipe');
+ } elseif ($ret !== strlen($commandString) + 1) {
+ $this->throwSshFailure(
+ 'Failed to write the whole command to the remote command pipe'
+ );
+ }
+ } else {
+ $this->throwSshFailure();
+ }
+ }
+
+ /**
+ * Get the pipes of the SSH subprocess
+ *
+ * @return array
+ */
+ protected function getSshPipes()
+ {
+ if ($this->sshPipes === null) {
+ $this->forkSsh();
+ }
+
+ return $this->sshPipes;
+ }
+
+ /**
+ * Get the SSH subprocess
+ *
+ * @return resource
+ */
+ protected function getSshProcess()
+ {
+ if ($this->sshProcess === null) {
+ $this->forkSsh();
+ }
+
+ return $this->sshProcess;
+ }
+
+ /**
+ * Get the status of the SSH subprocess
+ *
+ * @param string $what
+ *
+ * @return mixed
+ */
+ protected function getSshProcessStatus($what = null)
+ {
+ $status = proc_get_status($this->getSshProcess());
+ if ($what === null) {
+ return $status;
+ } else {
+ return $status[$what];
+ }
+ }
+
+ /**
+ * Get whether the SSH subprocess is alive
+ *
+ * @return bool
+ */
+ protected function isSshAlive()
+ {
+ return $this->getSshProcessStatus('running');
+ }
+
+ /**
+ * Fork SSH subprocess
+ *
+ * @throws CommandTransportException If fork fails
+ */
+ protected function forkSsh()
+ {
+ $descriptors = array(
+ 0 => array('pipe', 'r'),
+ 1 => array('pipe', 'w'),
+ 2 => array('pipe', 'w')
+ );
+
+ $this->sshProcess = proc_open($this->sshCommand(), $descriptors, $this->sshPipes);
+
+ if (! is_resource($this->sshProcess)) {
+ throw new CommandTransportException(
+ 'Can\'t send external Icinga command: Failed to fork SSH'
+ );
+ }
+ }
+
+ /**
+ * Read from STDERR
+ *
+ * @return string
+ */
+ protected function readStderr()
+ {
+ return stream_get_contents($this->sshPipes[2]);
+ }
+
+ /**
+ * Throw SSH failure
+ *
+ * @param string $msg
+ *
+ * @throws CommandTransportException
+ */
+ protected function throwSshFailure($msg = 'Can\'t send external Icinga command')
+ {
+ throw new CommandTransportException(
+ '%s: %s',
+ $msg,
+ $this->readStderr() . var_export($this->getSshProcessStatus(), true)
+ );
+ }
+
+ /**
+ * Close SSH pipes and SSH subprocess
+ */
+ public function __destruct()
+ {
+ if (is_resource($this->sshProcess)) {
+ fclose($this->sshPipes[0]);
+ fclose($this->sshPipes[1]);
+ fclose($this->sshPipes[2]);
+
+ proc_close($this->sshProcess);
+ }
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Controller.php b/modules/monitoring/library/Monitoring/Controller.php
new file mode 100644
index 0000000..2628935
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Controller.php
@@ -0,0 +1,159 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring;
+
+use ArrayIterator;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\QueryException;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filterable;
+use Icinga\File\Csv;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Module\Monitoring\Data\CustomvarProtectionIterator;
+use Icinga\Util\Json;
+use Icinga\Web\Controller as IcingaWebController;
+use Icinga\Web\Url;
+
+/**
+ * Base class for all monitoring action controller
+ */
+class Controller extends IcingaWebController
+{
+ /**
+ * The backend used for this controller
+ *
+ * @var MonitoringBackend
+ */
+ protected $backend;
+
+ protected function moduleInit()
+ {
+ $this->backend = MonitoringBackend::instance($this->_getParam('backend'));
+ $this->view->url = Url::fromRequest();
+ }
+
+ protected function handleFormatRequest($query)
+ {
+ $desiredContentType = $this->getRequest()->getHeader('Accept');
+ if ($desiredContentType === 'application/json') {
+ $desiredFormat = 'json';
+ } elseif ($desiredContentType === 'text/csv') {
+ $desiredFormat = 'csv';
+ } else {
+ $desiredFormat = strtolower($this->params->get('format', 'html'));
+ }
+
+ if ($desiredFormat !== 'html' && ! $this->params->has('limit')) {
+ $query->limit(); // Resets any default limit and offset
+ }
+
+ switch ($desiredFormat) {
+ case 'sql':
+ echo '<pre>'
+ . htmlspecialchars(wordwrap($query->dump()))
+ . '</pre>';
+ exit;
+ case 'json':
+ $response = $this->getResponse();
+ $response
+ ->setHeader('Content-Type', 'application/json')
+ ->setHeader('Cache-Control', 'no-store')
+ ->setHeader(
+ 'Content-Disposition',
+ 'inline; filename=' . $this->getRequest()->getActionName() . '.json'
+ )
+ ->appendBody(
+ Json::sanitize(
+ iterator_to_array(
+ new CustomvarProtectionIterator(
+ new ArrayIterator($query->fetchAll())
+ )
+ )
+ )
+ )
+ ->sendResponse();
+ exit;
+ case 'csv':
+ $response = $this->getResponse();
+ $response
+ ->setHeader('Content-Type', 'text/csv')
+ ->setHeader('Cache-Control', 'no-store')
+ ->setHeader(
+ 'Content-Disposition',
+ 'attachment; filename=' . $this->getRequest()->getActionName() . '.csv'
+ )
+ ->appendBody((string) Csv::fromQuery(new CustomvarProtectionIterator($query)))
+ ->sendResponse();
+ exit;
+ }
+ }
+
+ /**
+ * Apply a restriction of the authenticated on the given filterable
+ *
+ * @param string $name Name of the restriction
+ * @param Filterable $filterable Filterable to restrict
+ *
+ * @return Filterable The filterable having the restriction applied
+ */
+ protected function applyRestriction($name, Filterable $filterable)
+ {
+ $filterable->applyFilter($this->getRestriction($name));
+ return $filterable;
+ }
+
+ /**
+ * Get a restriction of the authenticated
+ *
+ * @param string $name Name of the restriction
+ *
+ * @return Filter Filter object
+ * @throws ConfigurationError If the restriction contains invalid filter columns
+ */
+ protected function getRestriction($name)
+ {
+ $restriction = Filter::matchAny();
+ $restriction->setAllowedFilterColumns(array(
+ 'host_name',
+ 'hostgroup_name',
+ 'instance_name',
+ 'service_description',
+ 'servicegroup_name',
+ function ($c) {
+ return preg_match('/^_(?:host|service)_/i', $c);
+ }
+ ));
+ foreach ($this->getRestrictions($name) as $filter) {
+ if ($filter === '*') {
+ return Filter::matchAll();
+ }
+ try {
+ $restriction->addFilter(Filter::fromQueryString($filter));
+ } catch (QueryException $e) {
+ throw new ConfigurationError(
+ $this->translate(
+ 'Cannot apply restriction %s using the filter %s. You can only use the following columns: %s'
+ ),
+ $name,
+ $filter,
+ implode(', ', array(
+ 'instance_name',
+ 'host_name',
+ 'hostgroup_name',
+ 'service_description',
+ 'servicegroup_name',
+ '_(host|service)_<customvar-name>'
+ )),
+ $e
+ );
+ }
+ }
+
+ if ($restriction->isEmpty()) {
+ return Filter::matchAll();
+ }
+
+ return $restriction;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Data/ColumnFilterIterator.php b/modules/monitoring/library/Monitoring/Data/ColumnFilterIterator.php
new file mode 100644
index 0000000..0ad051b
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Data/ColumnFilterIterator.php
@@ -0,0 +1,30 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Data;
+
+use ArrayIterator;
+use FilterIterator;
+use Zend_Db_Expr;
+
+/**
+ * Iterator over non-pseudo monitoring query columns
+ */
+class ColumnFilterIterator extends FilterIterator
+{
+ /**
+ * Create a new ColumnFilterIterator
+ *
+ * @param array $columns
+ */
+ public function __construct(array $columns)
+ {
+ parent::__construct(new ArrayIterator($columns));
+ }
+
+ public function accept(): bool
+ {
+ $column = $this->current();
+ return ! ($column instanceof Zend_Db_Expr || $column === '(NULL)');
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Data/CustomvarProtectionIterator.php b/modules/monitoring/library/Monitoring/Data/CustomvarProtectionIterator.php
new file mode 100644
index 0000000..c3cc01a
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Data/CustomvarProtectionIterator.php
@@ -0,0 +1,25 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Data;
+
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use IteratorIterator;
+
+class CustomvarProtectionIterator extends IteratorIterator
+{
+ const IS_CV_RE = '~^_(host|service)_([a-zA-Z0-9_]+)$~';
+
+ public function current(): object
+ {
+ $row = parent::current();
+
+ foreach ($row as $col => $val) {
+ if (preg_match(self::IS_CV_RE, $col, $m)) {
+ $row->$col = MonitoredObject::protectCustomVars([$m[2] => $val])[$m[2]];
+ }
+ }
+
+ return $row;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Command.php b/modules/monitoring/library/Monitoring/DataView/Command.php
new file mode 100644
index 0000000..6beb8bc
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Command.php
@@ -0,0 +1,24 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * View representation for commands
+ */
+class Command extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'command_id',
+ 'command_instance_id',
+ 'command_config_type',
+ 'command_line',
+ 'command_name'
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Comment.php b/modules/monitoring/library/Monitoring/DataView/Comment.php
new file mode 100644
index 0000000..3a035bc
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Comment.php
@@ -0,0 +1,82 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * Host and service comments view
+ */
+class Comment extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'comment_author_name',
+ 'comment_data',
+ 'comment_expiration',
+ 'comment_internal_id',
+ 'comment_is_persistent',
+ 'comment_name',
+ 'comment_timestamp',
+ 'comment_type',
+ 'host_display_name',
+ 'host_name',
+ 'object_type',
+ 'service_description',
+ 'service_display_name',
+ 'service_host_name'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'comment_author',
+ 'host', 'host_alias',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'instance_name',
+ 'service',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSearchColumns()
+ {
+ return array('host_display_name', 'service_display_name');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSortRules()
+ {
+ return array(
+ 'comment_timestamp' => array(
+ 'order' => self::SORT_DESC
+ ),
+ 'host_display_name' => array(
+ 'columns' => array(
+ 'host_display_name',
+ 'service_display_name'
+ ),
+ 'order' => self::SORT_ASC
+ ),
+ 'service_display_name' => array(
+ 'columns' => array(
+ 'service_display_name',
+ 'host_display_name'
+ ),
+ 'order' => self::SORT_ASC
+ )
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Commentevent.php b/modules/monitoring/library/Monitoring/DataView/Commentevent.php
new file mode 100644
index 0000000..316700a
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Commentevent.php
@@ -0,0 +1,30 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Commentevent extends DataView
+{
+ public function getColumns()
+ {
+ return array(
+ 'commentevent_id',
+ 'commentevent_entry_type',
+ 'commentevent_comment_time',
+ 'commentevent_author_name',
+ 'commentevent_comment_data',
+ 'commentevent_is_persistent',
+ 'commentevent_comment_source',
+ 'commentevent_expires',
+ 'commentevent_expiration_time',
+ 'commentevent_deletion_time',
+ 'host_name',
+ 'service_description'
+ );
+ }
+
+ public function getStaticFilterColumns()
+ {
+ return array('commentevent_id');
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Contact.php b/modules/monitoring/library/Monitoring/DataView/Contact.php
new file mode 100644
index 0000000..986acab
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Contact.php
@@ -0,0 +1,73 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Contact extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'contact_object_id',
+ 'contact_id',
+ 'contact_name',
+ 'contact_alias',
+ 'contact_email',
+ 'contact_pager',
+ 'contact_has_host_notfications',
+ 'contact_has_service_notfications',
+ 'contact_can_submit_commands',
+ 'contact_notify_service_recovery',
+ 'contact_notify_service_warning',
+ 'contact_notify_service_critical',
+ 'contact_notify_service_unknown',
+ 'contact_notify_service_flapping',
+ 'contact_notify_service_downtime',
+ 'contact_notify_host_recovery',
+ 'contact_notify_host_down',
+ 'contact_notify_host_unreachable',
+ 'contact_notify_host_flapping',
+ 'contact_notify_host_downtime',
+ 'contact_notify_host_timeperiod',
+ 'contact_notify_service_timeperiod'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSortRules()
+ {
+ return array(
+ 'contact_name' => array(
+ 'order' => self::SORT_ASC
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'contact', 'instance_name',
+ 'contactgroup', 'contactgroup_name', 'contactgroup_alias',
+ 'host', 'host_name', 'host_display_name', 'host_alias',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'service', 'service_description', 'service_display_name',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSearchColumns()
+ {
+ return array('contact_alias');
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Contactgroup.php b/modules/monitoring/library/Monitoring/DataView/Contactgroup.php
new file mode 100644
index 0000000..84eecd1
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Contactgroup.php
@@ -0,0 +1,57 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Contactgroup extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'contactgroup_name',
+ 'contactgroup_alias',
+ 'contact_count'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSortRules()
+ {
+ return array(
+ 'contactgroup_name' => array(
+ 'order' => self::SORT_ASC
+ ),
+ 'contactgroup_alias' => array(
+ 'order' => self::SORT_ASC
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'contactgroup',
+ 'host', 'host_name', 'host_display_name', 'host_alias',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'instance_name',
+ 'service', 'service_description', 'service_display_name',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSearchColumns()
+ {
+ return array('contactgroup_alias');
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Customvar.php b/modules/monitoring/library/Monitoring/DataView/Customvar.php
new file mode 100644
index 0000000..c02d52f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Customvar.php
@@ -0,0 +1,47 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Customvar extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'varname',
+ 'varvalue',
+ 'is_json',
+ 'host_name',
+ 'service_description',
+ 'contact_name',
+ 'object_type',
+ 'object_type_id'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSortRules()
+ {
+ return array(
+ 'varname' => array(
+ 'columns' => array(
+ 'varname',
+ 'varvalue'
+ )
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array('host', 'service', 'contact');
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/DataView.php b/modules/monitoring/library/Monitoring/DataView/DataView.php
new file mode 100644
index 0000000..5b16e28
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/DataView.php
@@ -0,0 +1,608 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\Filter\FilterMatch;
+use IteratorAggregate;
+use Icinga\Application\Hook;
+use Icinga\Data\ConnectionInterface;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\FilterColumns;
+use Icinga\Data\PivotTable;
+use Icinga\Data\QueryInterface;
+use Icinga\Data\SortRules;
+use Icinga\Exception\QueryException;
+use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Web\Request;
+use Icinga\Web\Url;
+use Traversable;
+
+/**
+ * A read-only view of an underlying query
+ */
+abstract class DataView implements QueryInterface, SortRules, FilterColumns, IteratorAggregate
+{
+ /**
+ * The query used to populate the view
+ *
+ * @var IdoQuery
+ */
+ protected $query;
+
+ protected $connection;
+
+ protected $isSorted = false;
+
+ /**
+ * The cache for all filter columns
+ *
+ * @var array
+ */
+ protected $filterColumns;
+
+ /**
+ * Create a new view
+ *
+ * @param ConnectionInterface $connection
+ * @param array $columns
+ */
+ public function __construct(ConnectionInterface $connection, array $columns = null)
+ {
+ $this->connection = $connection;
+ $this->query = $connection->query($this->getQueryName(), $columns);
+ }
+
+ /**
+ * Return a iterator for all rows of the result set
+ *
+ * @return IdoQuery
+ */
+ public function getIterator(): Traversable
+ {
+ return $this->getQuery();
+ }
+
+ /**
+ * Return the current position of the result set's iterator
+ *
+ * @return int
+ */
+ public function getIteratorPosition()
+ {
+ return $this->query->getIteratorPosition();
+ }
+
+ /**
+ * Get the query name this data view relies on
+ *
+ * By default this is this class' name without its namespace
+ *
+ * @return string
+ */
+ public static function getQueryName()
+ {
+ $tableName = explode('\\', get_called_class());
+ $tableName = end($tableName);
+ return $tableName;
+ }
+
+ public function where($condition, $value = null)
+ {
+ $this->query->where($condition, $value);
+ return $this;
+ }
+
+ /**
+ * Add a filter expression, with as less validation as possible
+ *
+ * @param FilterExpression $ex
+ *
+ * @internal If you use this outside the monitoring module, it's your fault if something breaks
+ * @return $this
+ */
+ public function whereEx(FilterExpression $ex)
+ {
+ $this->query->whereEx($ex);
+ return $this;
+ }
+
+ public function dump()
+ {
+ if (! $this->isSorted) {
+ $this->order();
+ }
+ return $this->query->dump();
+ }
+
+ /**
+ * Retrieve columns provided by this view
+ *
+ * @return array
+ */
+ abstract public function getColumns();
+
+ protected function getHookedColumns()
+ {
+ $columns = array();
+ foreach (Hook::all('monitoring/dataviewExtension') as $hook) {
+ foreach ($hook->getAdditionalQueryColumns($this->getQueryName()) as $col) {
+ $columns[] = $col;
+ }
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Create view from params
+ *
+ * @param array $params
+ * @param array $columns
+ *
+ * @return static
+ */
+ public static function fromParams(array $params, array $columns = null)
+ {
+ $view = new static(MonitoringBackend::instance($params['backend']), $columns);
+
+ foreach ($params as $key => $value) {
+ if ($view->isValidFilterTarget($key)) {
+ $view->where($key, $value);
+ }
+ }
+
+ if (isset($params['sort'])) {
+ $order = isset($params['order']) ? $params['order'] : null;
+ if ($order !== null) {
+ if (strtolower($order) === 'desc') {
+ $order = self::SORT_DESC;
+ } else {
+ $order = self::SORT_ASC;
+ }
+ }
+
+ $view->order($params['sort'], $order);
+ }
+ return $view;
+ }
+
+ /**
+ * Check whether the given column is a valid filter column
+ *
+ * @param string $column
+ *
+ * @return bool
+ */
+ public function isValidFilterTarget($column)
+ {
+ // Customvar
+ if ($column[0] === '_' && preg_match('/^_(?:host|service)_/i', $column)) {
+ return true;
+ }
+ return in_array($column, $this->getColumns()) || in_array($column, $this->getStaticFilterColumns());
+ }
+
+ /**
+ * Return all filter columns with their optional label as key
+ *
+ * This will merge the results of self::getColumns(), self::getStaticFilterColumns() and
+ * self::getDynamicFilterColumns() *once*. (i.e. subsequent calls of this function will
+ * return the same result.)
+ *
+ * @return array
+ */
+ public function getFilterColumns()
+ {
+ if ($this->filterColumns === null) {
+ $columns = array_merge(
+ $this->getColumns(),
+ $this->getStaticFilterColumns(),
+ $this->getDynamicFilterColumns()
+ );
+
+ $this->filterColumns = array();
+ foreach ($columns as $label => $column) {
+ if (is_int($label)) {
+ $label = ucwords(str_replace('_', ' ', $column));
+ }
+
+ if ($this->query->isCaseInsensitive($column)) {
+ $label .= ' ' . t('(Case insensitive)');
+ }
+
+ $this->filterColumns[$label] = $column;
+ }
+ }
+
+ return $this->filterColumns;
+ }
+
+ /**
+ * Return all static filter columns
+ *
+ * @return array
+ */
+ public function getStaticFilterColumns()
+ {
+ return array();
+ }
+
+ /**
+ * Return all dynamic filter columns such as custom variables
+ *
+ * @return array
+ */
+ public function getDynamicFilterColumns()
+ {
+ $columns = array();
+ if (! $this->query->allowsCustomVars()) {
+ return $columns;
+ }
+
+ $query = MonitoringBackend::instance()
+ ->select()
+ ->from('customvar', array('varname', 'object_type'))
+ ->where('is_json', 0)
+ ->where('object_type_id', array(1, 2))
+ ->getQuery()->group(array('varname', 'object_type'));
+ foreach ($query as $row) {
+ if ($row->object_type === 'host') {
+ $label = t('Host') . ' ' . ucwords(str_replace('_', ' ', $row->varname));
+ $columns[$label] = '_host_' . $row->varname;
+ } else { // $row->object_type === 'service'
+ $label = t('Service') . ' ' . ucwords(str_replace('_', ' ', $row->varname));
+ $columns[$label] = '_service_' . $row->varname;
+ }
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Return the current filter
+ *
+ * @return Filter
+ */
+ public function getFilter()
+ {
+ return $this->query->getFilter();
+ }
+
+ /**
+ * Return a pivot table for the given columns based on the current query
+ *
+ * @param string $xAxisColumn The column to use for the x axis
+ * @param string $yAxisColumn The column to use for the y axis
+ * @param Filter $xAxisFilter The filter to apply on a query for the x axis
+ * @param Filter $yAxisFilter The filter to apply on a query for the y axis
+ *
+ * @return PivotTable
+ */
+ public function pivot($xAxisColumn, $yAxisColumn, Filter $xAxisFilter = null, Filter $yAxisFilter = null)
+ {
+ $pivot = new PivotTable($this->query, $xAxisColumn, $yAxisColumn);
+ return $pivot->setXAxisFilter($xAxisFilter)->setYAxisFilter($yAxisFilter);
+ }
+
+ /**
+ * Sort result set either by the given column (and direction) or the sort defaults
+ *
+ * @param string $column
+ * @param string $direction
+ *
+ * @return $this
+ */
+ public function order($column = null, $direction = null)
+ {
+ $sortRules = $this->getSortRules();
+ if ($column === null) {
+ // Use first available sort rule as default
+ if (empty($sortRules)) {
+ return $this;
+ }
+ $sortColumns = reset($sortRules);
+ if (! isset($sortColumns['columns'])) {
+ $sortColumns['columns'] = array(key($sortRules));
+ }
+ } else {
+ if (isset($sortRules[$column])) {
+ $sortColumns = $sortRules[$column];
+ if (! isset($sortColumns['columns'])) {
+ $sortColumns['columns'] = array($column);
+ }
+ } else {
+ $sortColumns = array(
+ 'columns' => array($column),
+ 'order' => $direction
+ );
+ };
+ }
+
+ $direction = $direction === null ? ($sortColumns['order'] ?? static::SORT_ASC) : $direction;
+ $direction = (strtoupper($direction) === static::SORT_ASC) ? 'ASC' : 'DESC';
+
+ foreach ($sortColumns['columns'] as $column) {
+ list($column, $order) = $this->query->splitOrder($column);
+ if (! $this->isValidFilterTarget($column)) {
+ throw new QueryException(
+ mt('monitoring', 'The sort column "%s" is not allowed in "%s".'),
+ $column,
+ get_class($this)
+ );
+ }
+ $this->query->order($column, $order !== null ? $order : $direction);
+ }
+ $this->isSorted = true;
+ return $this;
+ }
+
+ /**
+ * Retrieve default sorting rules for particular columns. These involve sort order and potential additional to sort
+ *
+ * @return array
+ */
+ public function getSortRules()
+ {
+ return array();
+ }
+
+ /**
+ * Whether an order is set
+ *
+ * @return bool
+ */
+ public function hasOrder()
+ {
+ return $this->query->hasOrder();
+ }
+
+ /**
+ * Get the order if any
+ *
+ * @return array|null
+ */
+ public function getOrder()
+ {
+ return $this->query->getOrder();
+ }
+
+ public function getMappedField($field)
+ {
+ return $this->query->getMappedField($field);
+ }
+
+ /**
+ * Return the query which was created in the constructor
+ *
+ * @return \Icinga\Data\SimpleQuery
+ */
+ public function getQuery()
+ {
+ if (! $this->isSorted) {
+ $this->order();
+ }
+ return $this->query;
+ }
+
+ public function applyFilter(Filter $filter)
+ {
+ $this->validateFilterColumns($filter);
+
+ return $this->addFilter($filter);
+ }
+
+ /**
+ * Validates recursive the Filter columns against the isValidFilterTarget() method
+ *
+ * @param Filter $filter
+ *
+ * @throws \Icinga\Data\Filter\FilterException
+ */
+ public function validateFilterColumns(Filter $filter)
+ {
+ if ($filter instanceof FilterMatch) {
+ if (! $this->isValidFilterTarget($filter->getColumn())) {
+ throw new QueryException(
+ mt('monitoring', 'The filter column "%s" is not allowed here.'),
+ $filter->getColumn()
+ );
+ }
+ }
+
+ if (method_exists($filter, 'filters')) {
+ foreach ($filter->filters() as $filter) {
+ $this->validateFilterColumns($filter);
+ }
+ }
+ }
+
+ public function clearFilter()
+ {
+ $this->query->clearFilter();
+ return $this;
+ }
+
+ /**
+ * @deprecated(EL): Only use DataView::applyFilter() for applying filter because all other functions are missing
+ * column validation. Filter::matchAny() for the IdoQuery (or the DbQuery or the SimpleQuery I didn't have a look)
+ * is required for the filter to work properly.
+ */
+ public function setFilter(Filter $filter)
+ {
+ $this->query->setFilter($filter);
+ return $this;
+ }
+
+ /**
+ * Get the view's search columns
+ *
+ * @return string[]
+ */
+ public function getSearchColumns()
+ {
+ return array();
+ }
+
+ /**
+ * @deprecated(EL): Only use DataView::applyFilter() for applying filter because all other functions are missing
+ * column validation.
+ */
+ public function addFilter(Filter $filter)
+ {
+ $this->query->addFilter($filter);
+ return $this;
+ }
+
+ /**
+ * Count result set
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return $this->query->count();
+ }
+
+ /**
+ * Set whether the query should peek ahead for more results
+ *
+ * Enabling this causes the current query limit to be increased by one. The potential extra row being yielded will
+ * be removed from the result set. Note that this only applies when fetching multiple results of limited queries.
+ *
+ * @return $this
+ */
+ public function peekAhead($state = true)
+ {
+ $this->query->peekAhead($state);
+ return $this;
+ }
+
+ /**
+ * Return whether the query did not yield all available results
+ *
+ * @return bool
+ */
+ public function hasMore()
+ {
+ return $this->query->hasMore();
+ }
+
+ /**
+ * Return whether this query will or has yielded any result
+ *
+ * @return bool
+ */
+ public function hasResult()
+ {
+ return $this->query->hasResult();
+ }
+
+ /**
+ * Set a limit count and offset
+ *
+ * @param int $count Number of rows to return
+ * @param int $offset Start returning after this many rows
+ *
+ * @return self
+ */
+ public function limit($count = null, $offset = null)
+ {
+ $this->query->limit($count, $offset);
+ return $this;
+ }
+
+ /**
+ * Whether a limit is set
+ *
+ * @return bool
+ */
+ public function hasLimit()
+ {
+ return $this->query->hasLimit();
+ }
+
+ /**
+ * Get the limit if any
+ *
+ * @return int|null
+ */
+ public function getLimit()
+ {
+ return $this->query->getLimit();
+ }
+
+ /**
+ * Whether an offset is set
+ *
+ * @return bool
+ */
+ public function hasOffset()
+ {
+ return $this->query->hasOffset();
+ }
+
+ /**
+ * Get the offset if any
+ *
+ * @return int|null
+ */
+ public function getOffset()
+ {
+ return $this->query->getOffset();
+ }
+
+ /**
+ * Retrieve an array containing all rows of the result set
+ *
+ * @return array
+ */
+ public function fetchAll()
+ {
+ return $this->getQuery()->fetchAll();
+ }
+
+ /**
+ * Fetch the first row of the result set
+ *
+ * @return mixed
+ */
+ public function fetchRow()
+ {
+ return $this->getQuery()->fetchRow();
+ }
+
+ /**
+ * Fetch the first column of all rows of the result set as an array
+ *
+ * @return array
+ */
+ public function fetchColumn()
+ {
+ return $this->getQuery()->fetchColumn();
+ }
+
+ /**
+ * Fetch the first column of the first row of the result set
+ *
+ * @return string
+ */
+ public function fetchOne()
+ {
+ return $this->getQuery()->fetchOne();
+ }
+
+ /**
+ * Fetch all rows of the result set as an array of key-value pairs
+ *
+ * The first column is the key, the second column is the value.
+ *
+ * @return array
+ */
+ public function fetchPairs()
+ {
+ return $this->getQuery()->fetchPairs();
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Downtime.php b/modules/monitoring/library/Monitoring/DataView/Downtime.php
new file mode 100644
index 0000000..ca42e2d
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Downtime.php
@@ -0,0 +1,96 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * Host and service downtimes view
+ */
+class Downtime extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'downtime_author_name',
+ 'downtime_comment',
+ 'downtime_duration',
+ 'downtime_end',
+ 'downtime_entry_time',
+ 'downtime_internal_id',
+ 'downtime_is_fixed',
+ 'downtime_is_flexible',
+ 'downtime_is_in_effect',
+ 'downtime_name',
+ 'downtime_scheduled_end',
+ 'downtime_scheduled_start',
+ 'downtime_start',
+ 'host_display_name',
+ 'host_name',
+ 'host_state',
+ 'object_type',
+ 'service_description',
+ 'service_display_name',
+ 'service_host_name',
+ 'service_state'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'downtime_author',
+ 'host', 'host_alias',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'instance_name',
+ 'service',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSearchColumns()
+ {
+ return array('host_display_name', 'service_display_name');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSortRules()
+ {
+ return array(
+ 'downtime_is_in_effect' => array(
+ 'columns' => array(
+ 'downtime_is_in_effect',
+ 'downtime_scheduled_start'
+ ),
+ 'order' => self::SORT_DESC
+ ),
+ 'downtime_start' => array(
+ 'order' => self::SORT_DESC
+ ),
+ 'host_display_name' => array(
+ 'columns' => array(
+ 'host_display_name',
+ 'service_display_name'
+ ),
+ 'order' => self::SORT_ASC
+ ),
+ 'service_display_name' => array(
+ 'columns' => array(
+ 'service_display_name',
+ 'host_display_name'
+ ),
+ 'order' => self::SORT_ASC
+ )
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Downtimeevent.php b/modules/monitoring/library/Monitoring/DataView/Downtimeevent.php
new file mode 100644
index 0000000..a1fc0f6
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Downtimeevent.php
@@ -0,0 +1,33 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Downtimeevent extends DataView
+{
+ public function getColumns()
+ {
+ return array(
+ 'downtimeevent_id',
+ 'downtimeevent_entry_time',
+ 'downtimeevent_author_name',
+ 'downtimeevent_comment_data',
+ 'downtimeevent_is_fixed',
+ 'downtimeevent_scheduled_start_time',
+ 'downtimeevent_scheduled_end_time',
+ 'downtimeevent_was_started',
+ 'downtimeevent_actual_start_time',
+ 'downtimeevent_actual_end_time',
+ 'downtimeevent_was_cancelled',
+ 'downtimeevent_is_in_effect',
+ 'downtimeevent_trigger_time',
+ 'host_name',
+ 'service_description'
+ );
+ }
+
+ public function getStaticFilterColumns()
+ {
+ return array('downtimeevent_id');
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Eventgrid.php b/modules/monitoring/library/Monitoring/DataView/Eventgrid.php
new file mode 100644
index 0000000..1639e6b
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Eventgrid.php
@@ -0,0 +1,60 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Eventgrid extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'day',
+ 'cnt_up',
+ 'cnt_down_hard',
+ 'cnt_down',
+ 'cnt_unreachable_hard',
+ 'cnt_unreachable',
+ 'cnt_unknown_hard',
+ 'cnt_unknown',
+ 'cnt_critical',
+ 'cnt_critical_hard',
+ 'cnt_warning',
+ 'cnt_warning_hard',
+ 'cnt_ok',
+ 'host_name',
+ 'host_display_name',
+ 'service_description',
+ 'service_display_name',
+ 'timestamp'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSortRules()
+ {
+ return array(
+ 'day' => array(
+ 'order' => self::SORT_DESC
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'instance_name',
+ 'host', 'host_alias',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'service', 'service_host_name',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Eventgridhosts.php b/modules/monitoring/library/Monitoring/DataView/Eventgridhosts.php
new file mode 100644
index 0000000..9d9acc9
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Eventgridhosts.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Eventgridhosts extends Eventgrid
+{
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Eventgridservices.php b/modules/monitoring/library/Monitoring/DataView/Eventgridservices.php
new file mode 100644
index 0000000..faa1065
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Eventgridservices.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Eventgridservices extends Eventgrid
+{
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Eventhistory.php b/modules/monitoring/library/Monitoring/DataView/Eventhistory.php
new file mode 100644
index 0000000..cd947f5
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Eventhistory.php
@@ -0,0 +1,60 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class EventHistory extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'id',
+ 'instance_name',
+ 'host_name',
+ 'host_display_name',
+ 'service_description',
+ 'service_display_name',
+ 'object_type',
+ 'timestamp',
+ 'state',
+ 'output',
+ 'type'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSortRules()
+ {
+ return array(
+ 'timestamp' => array(
+ 'order' => self::SORT_DESC
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'host', 'host_alias',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'service',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSearchColumns()
+ {
+ return array('host_display_name', 'service_display_name');
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Flappingevent.php b/modules/monitoring/library/Monitoring/DataView/Flappingevent.php
new file mode 100644
index 0000000..bc79497
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Flappingevent.php
@@ -0,0 +1,27 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Flappingevent extends DataView
+{
+ public function getColumns()
+ {
+ return array(
+ 'flappingevent_id',
+ 'flappingevent_event_time',
+ 'flappingevent_event_type',
+ 'flappingevent_reason_type',
+ 'flappingevent_percent_state_change',
+ 'flappingevent_low_threshold',
+ 'flappingevent_high_threshold',
+ 'host_name',
+ 'service_description'
+ );
+ }
+
+ public function getStaticFilterColumns()
+ {
+ return array('flappingevent_id');
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Hostcomment.php b/modules/monitoring/library/Monitoring/DataView/Hostcomment.php
new file mode 100644
index 0000000..74fc2ef
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Hostcomment.php
@@ -0,0 +1,45 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * Host comment view
+ */
+class Hostcomment extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'comment_author',
+ 'comment_author_name',
+ 'comment_data',
+ 'comment_expiration',
+ 'comment_internal_id',
+ 'comment_is_persistent',
+ 'comment_name',
+ 'comment_timestamp',
+ 'comment_type',
+ 'host_display_name',
+ 'host_name',
+ 'object_type'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'host', 'host_alias',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'instance_name',
+ 'service', 'service_description', 'service_display_name',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Hostcontact.php b/modules/monitoring/library/Monitoring/DataView/Hostcontact.php
new file mode 100644
index 0000000..ecfed2f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Hostcontact.php
@@ -0,0 +1,17 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Hostcontact extends Contact
+{
+ public function getColumns()
+ {
+ return [
+ 'contact_name',
+ 'contact_alias',
+ 'contact_email',
+ 'contact_pager'
+ ];
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Hostdowntime.php b/modules/monitoring/library/Monitoring/DataView/Hostdowntime.php
new file mode 100644
index 0000000..f5e4e80
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Hostdowntime.php
@@ -0,0 +1,50 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * Host downtime view
+ */
+class Hostdowntime extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'downtime_author',
+ 'downtime_author_name',
+ 'downtime_comment',
+ 'downtime_duration',
+ 'downtime_end',
+ 'downtime_entry_time',
+ 'downtime_internal_id',
+ 'downtime_is_fixed',
+ 'downtime_is_flexible',
+ 'downtime_is_in_effect',
+ 'downtime_name',
+ 'downtime_scheduled_end',
+ 'downtime_scheduled_start',
+ 'downtime_start',
+ 'host_display_name',
+ 'host_name',
+ 'object_type'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'host', 'host_alias',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'instance_name',
+ 'service', 'service_description', 'service_display_name',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Hostgroup.php b/modules/monitoring/library/Monitoring/DataView/Hostgroup.php
new file mode 100644
index 0000000..b204fcd
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Hostgroup.php
@@ -0,0 +1,34 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * Host group data view
+ */
+class Hostgroup extends DataView
+{
+ public function getColumns()
+ {
+ return array(
+ 'hostgroup_alias',
+ 'hostgroup_name'
+ );
+ }
+
+ public function getSortRules()
+ {
+ return array(
+ 'hostgroup_alias' => array(
+ 'order' => self::SORT_ASC
+ )
+ );
+ }
+
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'instance_name', 'host_name', 'service_description', 'servicegroup_name'
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Hostgroupsummary.php b/modules/monitoring/library/Monitoring/DataView/Hostgroupsummary.php
new file mode 100644
index 0000000..9ed2eb9
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Hostgroupsummary.php
@@ -0,0 +1,81 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * Data view for the host group summary
+ */
+class Hostgroupsummary extends DataView
+{
+ public function getColumns()
+ {
+ return array(
+ 'hostgroup_alias',
+ 'hostgroup_name',
+ 'hosts_down_handled',
+ 'hosts_down_unhandled',
+ 'hosts_pending',
+ 'hosts_severity',
+ 'hosts_total',
+ 'hosts_unreachable_handled',
+ 'hosts_unreachable_unhandled',
+ 'hosts_up',
+ 'services_critical_handled',
+ 'services_critical_unhandled',
+ 'services_ok',
+ 'services_pending',
+ 'services_total',
+ 'services_unknown_handled',
+ 'services_unknown_unhandled',
+ 'services_warning_handled',
+ 'services_warning_unhandled'
+ );
+ }
+
+ public function getSearchColumns()
+ {
+ return array('hostgroup', 'hostgroup_alias');
+ }
+
+ public function getSortRules()
+ {
+ return array(
+ 'hostgroup_alias' => array(
+ 'order' => self::SORT_ASC
+ ),
+ 'hosts_severity' => array(
+ 'columns' => array(
+ 'hosts_severity',
+ 'hostgroup_alias ASC'
+ ),
+ 'order' => self::SORT_DESC
+ )
+ );
+ }
+
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'instance_name',
+ 'host_contact', 'host_contactgroup', 'host_name',
+ 'hostgroup',
+ 'service_description',
+ 'servicegroup_name'
+ );
+ }
+
+ public function getFilterColumns()
+ {
+ if ($this->filterColumns === null) {
+ $filterColumns = parent::getFilterColumns();
+ $diff = array_diff($filterColumns, $this->getColumns());
+ $this->filterColumns = array_merge($diff, [
+ 'Hostgroup Name' => 'hostgroup_name',
+ 'Hostgroup Alias' => 'hostgroup_alias'
+ ]);
+ }
+
+ return $this->filterColumns;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Hoststatus.php b/modules/monitoring/library/Monitoring/DataView/Hoststatus.php
new file mode 100644
index 0000000..cd9b31f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Hoststatus.php
@@ -0,0 +1,129 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Hoststatus extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array_merge($this->getHookedColumns(), array(
+ 'host_acknowledged',
+ 'host_acknowledgement_type',
+ 'host_action_url',
+ 'host_active_checks_enabled',
+ 'host_active_checks_enabled_changed',
+ 'host_address',
+ 'host_address6',
+ 'host_alias',
+ 'host_check_command',
+ 'host_check_execution_time',
+ 'host_check_latency',
+ 'host_check_source',
+ 'host_check_timeperiod',
+ 'host_current_check_attempt',
+ 'host_current_notification_number',
+ 'host_display_name',
+ 'host_event_handler_enabled',
+ 'host_event_handler_enabled_changed',
+ 'host_flap_detection_enabled',
+ 'host_flap_detection_enabled_changed',
+ 'host_handled',
+ 'host_hard_state',
+ 'host_in_downtime',
+ 'host_ipv4',
+ 'host_is_flapping',
+ 'host_is_reachable',
+ 'host_last_check',
+ 'host_last_notification',
+ 'host_last_state_change',
+ 'host_last_state_change_ts',
+ 'host_long_output',
+ 'host_max_check_attempts',
+ 'host_modified_host_attributes',
+ 'host_name',
+ 'host_next_check',
+ 'host_notes_url',
+ 'host_notifications_enabled',
+ 'host_notifications_enabled_changed',
+ 'host_obsessing',
+ 'host_obsessing_changed',
+ 'host_output',
+ 'host_passive_checks_enabled',
+ 'host_passive_checks_enabled_changed',
+ 'host_percent_state_change',
+ 'host_perfdata',
+ 'host_problem',
+ 'host_severity',
+ 'host_state',
+ 'host_state_type',
+ 'host_unhandled',
+ 'instance_name'
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'host', 'host_contact', 'host_contactgroup',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'service', 'service_description', 'service_display_name',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSearchColumns($search = null)
+ {
+ if ($search !== null
+ && (@inet_pton($search) !== false || preg_match('/^\d{1,3}\.\d{1,3}\./', $search))
+ ) {
+ return array('host', 'host_address', 'host_address6');
+ } else {
+ if ($this->connection->isIcinga2()) {
+ return array('host', 'host_display_name');
+ } else {
+ return array('host', 'host_display_name', 'host_alias');
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSortRules()
+ {
+ return array(
+ 'host_display_name' => array(
+ 'order' => self::SORT_ASC
+ ),
+ 'host_severity' => array(
+ 'columns' => array(
+ 'host_severity',
+ 'host_last_state_change_ts DESC'
+ ),
+ 'order' => self::SORT_DESC
+ ),
+ 'host_address' => array(
+ 'columns' => array(
+ 'host_ipv4'
+ ),
+ 'order' => self::SORT_ASC
+ ),
+ 'host_last_state_change' => array(
+ 'columns' => array(
+ 'host_last_state_change_ts'
+ ),
+ 'order' => self::SORT_DESC
+ )
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Hoststatussummary.php b/modules/monitoring/library/Monitoring/DataView/Hoststatussummary.php
new file mode 100644
index 0000000..a857466
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Hoststatussummary.php
@@ -0,0 +1,40 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * Data view for host status summaries
+ */
+class Hoststatussummary extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'hosts_down_handled',
+ 'hosts_down_unhandled',
+ 'hosts_pending',
+ 'hosts_total',
+ 'hosts_unreachable_handled',
+ 'hosts_unreachable_unhandled',
+ 'hosts_up',
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'instance_name',
+ 'host', 'host_alias', 'host_display_name', 'host_name',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'service', 'service_description', 'service_display_name',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Instance.php b/modules/monitoring/library/Monitoring/DataView/Instance.php
new file mode 100644
index 0000000..98ef1d6
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Instance.php
@@ -0,0 +1,33 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * View representation for instances
+ */
+class Instance extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'instance_id',
+ 'instance_name'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSortRules()
+ {
+ return array(
+ 'instance_name' => array(
+ 'order' => self::SORT_ASC
+ )
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Notification.php b/modules/monitoring/library/Monitoring/DataView/Notification.php
new file mode 100644
index 0000000..90755de
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Notification.php
@@ -0,0 +1,59 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Notification extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'host_display_name',
+ 'host_name',
+ 'notification_contact_name',
+ 'notification_output',
+ 'notification_reason',
+ 'notification_state',
+ 'notification_timestamp',
+ 'object_type',
+ 'service_description',
+ 'service_display_name',
+ 'service_host_name'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSortRules()
+ {
+ return array(
+ 'notification_timestamp' => array(
+ 'order' => self::SORT_DESC
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'hostgroup_name',
+ 'instance_name',
+ 'servicegroup_name'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSearchColumns()
+ {
+ return array('host_display_name', 'service_display_name');
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Notificationevent.php b/modules/monitoring/library/Monitoring/DataView/Notificationevent.php
new file mode 100644
index 0000000..82dd212
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Notificationevent.php
@@ -0,0 +1,29 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Notificationevent extends DataView
+{
+ public function getColumns()
+ {
+ return array(
+ 'notificationevent_id',
+ 'notificationevent_reason',
+ 'notificationevent_start_time',
+ 'notificationevent_end_time',
+ 'notificationevent_state',
+ 'notificationevent_output',
+ 'notificationevent_long_output',
+ 'notificationevent_escalated',
+ 'notificationevent_contacts_notified',
+ 'host_name',
+ 'service_description'
+ );
+ }
+
+ public function getStaticFilterColumns()
+ {
+ return array('notificationevent_id');
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Programstatus.php b/modules/monitoring/library/Monitoring/DataView/Programstatus.php
new file mode 100644
index 0000000..d611c72
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Programstatus.php
@@ -0,0 +1,44 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * View for programstatus query
+ */
+class Programstatus extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'id',
+ 'status_update_time',
+ 'program_start_time',
+ 'program_end_time',
+ 'is_currently_running',
+ 'process_id',
+ 'daemon_mode',
+ 'last_command_check',
+ 'last_log_rotation',
+ 'notifications_enabled',
+ 'disable_notif_expire_time',
+ 'active_service_checks_enabled',
+ 'passive_service_checks_enabled',
+ 'active_host_checks_enabled',
+ 'passive_host_checks_enabled',
+ 'event_handlers_enabled',
+ 'flap_detection_enabled',
+ 'failure_prediction_enabled',
+ 'process_performance_data',
+ 'obsess_over_hosts',
+ 'obsess_over_services',
+ 'modified_host_attributes',
+ 'modified_service_attributes',
+ 'global_host_event_handler',
+ 'global_service_event_handler',
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Runtimesummary.php b/modules/monitoring/library/Monitoring/DataView/Runtimesummary.php
new file mode 100644
index 0000000..bf80226
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Runtimesummary.php
@@ -0,0 +1,38 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * View for runtimesummary query
+ */
+class Runtimesummary extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'check_type',
+ 'active_checks_enabled',
+ 'passive_checks_enabled',
+ 'execution_time',
+ 'latency',
+ 'object_count',
+ 'object_type'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSortRules()
+ {
+ return array(
+ 'active_checks_enabled' => array(
+ 'order' => self::SORT_ASC
+ )
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Runtimevariables.php b/modules/monitoring/library/Monitoring/DataView/Runtimevariables.php
new file mode 100644
index 0000000..b3624b7
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Runtimevariables.php
@@ -0,0 +1,34 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * View for runtimevariables query
+ */
+class Runtimevariables extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'id',
+ 'varname',
+ 'varvalue'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSortRules()
+ {
+ return array(
+ 'id' => array(
+ 'order' => self::SORT_ASC
+ )
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Servicecomment.php b/modules/monitoring/library/Monitoring/DataView/Servicecomment.php
new file mode 100644
index 0000000..78c1333
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Servicecomment.php
@@ -0,0 +1,48 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * Service comment view
+ */
+class Servicecomment extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'comment_author',
+ 'comment_author_name',
+ 'comment_data',
+ 'comment_expiration',
+ 'comment_internal_id',
+ 'comment_is_persistent',
+ 'comment_name',
+ 'comment_timestamp',
+ 'comment_type',
+ 'host_display_name',
+ 'host_name',
+ 'object_type',
+ 'service_description',
+ 'service_display_name',
+ 'service_host_name'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'host', 'host_alias',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'instance_name',
+ 'service',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Servicecontact.php b/modules/monitoring/library/Monitoring/DataView/Servicecontact.php
new file mode 100644
index 0000000..55c9950
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Servicecontact.php
@@ -0,0 +1,8 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Servicecontact extends Hostcontact
+{
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Servicedowntime.php b/modules/monitoring/library/Monitoring/DataView/Servicedowntime.php
new file mode 100644
index 0000000..43d895e
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Servicedowntime.php
@@ -0,0 +1,50 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Servicedowntime extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'downtime_author',
+ 'downtime_author_name',
+ 'downtime_comment',
+ 'downtime_duration',
+ 'downtime_end',
+ 'downtime_entry_time',
+ 'downtime_internal_id',
+ 'downtime_is_fixed',
+ 'downtime_is_flexible',
+ 'downtime_is_in_effect',
+ 'downtime_name',
+ 'downtime_scheduled_end',
+ 'downtime_scheduled_start',
+ 'downtime_start',
+ 'host_display_name',
+ 'host_name',
+ 'object_type',
+ 'service_description',
+ 'service_display_name',
+ 'service_host_name'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'host', 'host_alias',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'instance_name',
+ 'service',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Servicegroup.php b/modules/monitoring/library/Monitoring/DataView/Servicegroup.php
new file mode 100644
index 0000000..9909a68
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Servicegroup.php
@@ -0,0 +1,31 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Servicegroup extends DataView
+{
+ public function getColumns()
+ {
+ return array(
+ 'servicegroup_alias',
+ 'servicegroup_name'
+ );
+ }
+
+ public function getSortRules()
+ {
+ return array(
+ 'servicegroup_alias' => array(
+ 'order' => self::SORT_ASC
+ )
+ );
+ }
+
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'instance_name', 'host_name', 'hostgroup_name', 'service_description'
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Servicegroupsummary.php b/modules/monitoring/library/Monitoring/DataView/Servicegroupsummary.php
new file mode 100644
index 0000000..9dc3ee0
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Servicegroupsummary.php
@@ -0,0 +1,75 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * Data view for service group summaries
+ */
+class Servicegroupsummary extends DataView
+{
+ public function getColumns()
+ {
+ return array(
+ 'servicegroup_alias',
+ 'servicegroup_name',
+ 'services_critical_handled',
+ 'services_critical_unhandled',
+ 'services_ok',
+ 'services_pending',
+ 'services_severity',
+ 'services_total',
+ 'services_unknown_handled',
+ 'services_unknown_unhandled',
+ 'services_warning_handled',
+ 'services_warning_unhandled'
+ );
+ }
+
+ public function getSearchColumns()
+ {
+ return array('servicegroup', 'servicegroup_alias');
+ }
+
+ public function getSortRules()
+ {
+ return array(
+ 'servicegroup_alias' => array(
+ 'order' => self::SORT_ASC
+ ),
+ 'services_severity' => array(
+ 'columns' => array(
+ 'services_severity',
+ 'servicegroup_alias ASC'
+ ),
+ 'order' => self::SORT_DESC
+ )
+ );
+ }
+
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'instance_name',
+ 'services_severity',
+ 'host_contact', 'host_contactgroup', 'host_name',
+ 'hostgroup_name',
+ 'service_contact', 'service_contactgroup', 'service_description',
+ 'servicegroup'
+ );
+ }
+
+ public function getFilterColumns()
+ {
+ if ($this->filterColumns === null) {
+ $filterColumns = parent::getFilterColumns();
+ $diff = array_diff($filterColumns, $this->getColumns());
+ $this->filterColumns = array_merge($diff, [
+ 'Servicegroup Name' => 'servicegroup_name',
+ 'Servicegroup Alias' => 'servicegroup_alias'
+ ]);
+ }
+
+ return $this->filterColumns;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Servicestatus.php b/modules/monitoring/library/Monitoring/DataView/Servicestatus.php
new file mode 100644
index 0000000..86da272
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Servicestatus.php
@@ -0,0 +1,180 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Servicestatus extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array_merge($this->getHookedColumns(), array(
+ 'host_acknowledged',
+ 'host_action_url',
+ 'host_active_checks_enabled',
+ 'host_address',
+ 'host_address6',
+ 'host_alias',
+ 'host_check_source',
+ 'host_display_name',
+ 'host_handled',
+ 'host_hard_state',
+ 'host_in_downtime',
+ 'host_ipv4',
+ 'host_is_flapping',
+ 'host_last_check',
+ 'host_last_hard_state',
+ 'host_last_hard_state_change',
+ 'host_last_state_change',
+ 'host_last_time_down',
+ 'host_last_time_unreachable',
+ 'host_last_time_up',
+ 'host_long_output',
+ 'host_modified_host_attributes',
+ 'host_name',
+ 'host_notes_url',
+ 'host_notifications_enabled',
+ 'host_output',
+ 'host_passive_checks_enabled',
+ 'host_perfdata',
+ 'host_problem',
+ 'host_severity',
+ 'host_state',
+ 'host_state_type',
+ 'host_unhandled_service_count',
+ 'instance_name',
+ 'service_acknowledged',
+ 'service_acknowledgement_type',
+ 'service_action_url',
+ 'service_active_checks_enabled',
+ 'service_active_checks_enabled_changed',
+ 'service_attempt',
+ 'service_check_command',
+ 'service_check_source',
+ 'service_check_timeperiod',
+ 'service_current_check_attempt',
+ 'service_current_notification_number',
+ 'service_description',
+ 'service_display_name',
+ 'service_event_handler_enabled',
+ 'service_event_handler_enabled_changed',
+ 'service_flap_detection_enabled',
+ 'service_flap_detection_enabled_changed',
+ 'service_handled',
+ 'service_hard_state',
+ 'service_host_name',
+ 'service_in_downtime',
+ 'service_is_flapping',
+ 'service_is_reachable',
+ 'service_last_check',
+ 'service_last_hard_state',
+ 'service_last_hard_state_change',
+ 'service_last_notification',
+ 'service_last_state_change',
+ 'service_last_state_change_ts',
+ 'service_last_time_critical',
+ 'service_last_time_ok',
+ 'service_last_time_unknown',
+ 'service_last_time_warning',
+ 'service_long_output',
+ 'service_max_check_attempts',
+ 'service_modified_service_attributes',
+ 'service_next_check',
+ 'service_notes',
+ 'service_notes_url',
+ 'service_notifications_enabled',
+ 'service_notifications_enabled_changed',
+ 'service_obsessing',
+ 'service_obsessing_changed',
+ 'service_output',
+ 'service_passive_checks_enabled',
+ 'service_passive_checks_enabled_changed',
+ 'service_perfdata',
+ 'service_problem',
+ 'service_severity',
+ 'service_state',
+ 'service_state_type',
+ 'service_unhandled'
+ ));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSortRules()
+ {
+ return array(
+ 'service_display_name' => array(
+ 'order' => self::SORT_ASC
+ ),
+ 'service_severity' => array(
+ 'columns' => array(
+ 'service_severity',
+ 'service_last_state_change_ts DESC'
+ ),
+ 'order' => self::SORT_DESC
+ ),
+ 'service_last_state_change' => array(
+ 'columns' => array(
+ 'service_last_state_change_ts'
+ ),
+ 'order' => self::SORT_DESC
+ ),
+ 'host_severity' => array(
+ 'columns' => array(
+ 'host_severity',
+ 'host_last_state_change DESC',
+ 'host_display_name ASC',
+ 'service_display_name ASC'
+ ),
+ 'order' => self::SORT_DESC
+ ),
+ 'host_display_name' => array(
+ 'columns' => array(
+ 'host_display_name',
+ 'service_display_name'
+ ),
+ 'order' => self::SORT_ASC
+ ),
+ 'host_address' => array(
+ 'columns' => array(
+ 'host_ipv4',
+ 'service_display_name'
+ ),
+ 'order' => self::SORT_ASC
+ )
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'host',
+ 'host_contact',
+ 'host_contactgroup',
+ 'hostgroup',
+ 'hostgroup_alias',
+ 'hostgroup_name',
+ 'service',
+ 'service_contact',
+ 'service_contactgroup',
+ 'service_host',
+ 'servicegroup',
+ 'servicegroup_alias',
+ 'servicegroup_name'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSearchColumns()
+ {
+ return array('service', 'service_display_name');
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Servicestatussummary.php b/modules/monitoring/library/Monitoring/DataView/Servicestatussummary.php
new file mode 100644
index 0000000..abd3593
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Servicestatussummary.php
@@ -0,0 +1,45 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * Data view for service status summaries
+ */
+class Servicestatussummary extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'services_critical',
+ 'services_critical_handled',
+ 'services_critical_unhandled',
+ 'services_ok',
+ 'services_pending',
+ 'services_total',
+ 'services_unknown',
+ 'services_unknown_handled',
+ 'services_unknown_unhandled',
+ 'services_warning',
+ 'services_warning_handled',
+ 'services_warning_unhandled'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'instance_name',
+ 'host', 'host_alias', 'host_display_name', 'host_name',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'service', 'service_description', 'service_display_name',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Statechangeevent.php b/modules/monitoring/library/Monitoring/DataView/Statechangeevent.php
new file mode 100644
index 0000000..0b01aff
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Statechangeevent.php
@@ -0,0 +1,32 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class Statechangeevent extends DataView
+{
+ public function getColumns()
+ {
+ return array(
+ 'statechangeevent_id',
+ 'statechangeevent_state_time',
+ 'statechangeevent_state_change',
+ 'statechangeevent_state',
+ 'statechangeevent_state_type',
+ 'statechangeevent_current_check_attempt',
+ 'statechangeevent_max_check_attempts',
+ 'statechangeevent_last_state',
+ 'statechangeevent_last_hard_state',
+ 'statechangeevent_output',
+ 'statechangeevent_long_output',
+ 'statechangeevent_check_source',
+ 'host_name',
+ 'service_description'
+ );
+ }
+
+ public function getStaticFilterColumns()
+ {
+ return array('statechangeevent_id');
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Statussummary.php b/modules/monitoring/library/Monitoring/DataView/Statussummary.php
new file mode 100644
index 0000000..36efccb
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Statussummary.php
@@ -0,0 +1,111 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+class StatusSummary extends DataView
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function getColumns()
+ {
+ return array(
+ 'hosts_up',
+ 'hosts_up_not_checked',
+ 'hosts_pending',
+ 'hosts_pending_not_checked',
+ 'hosts_down',
+ 'hosts_down_handled',
+ 'hosts_down_unhandled',
+ 'hosts_down_passive',
+ 'hosts_down_not_checked',
+ 'hosts_unreachable',
+ 'hosts_unreachable_handled',
+ 'hosts_unreachable_unhandled',
+ 'hosts_unreachable_passive',
+ 'hosts_unreachable_not_checked',
+ 'hosts_active',
+ 'hosts_passive',
+ 'hosts_not_checked',
+ 'hosts_not_processing_event_handlers',
+ 'hosts_not_triggering_notifications',
+ 'hosts_without_flap_detection',
+ 'hosts_flapping',
+ 'services_ok',
+ 'services_ok_not_checked',
+ 'services_pending',
+ 'services_pending_not_checked',
+ 'services_warning',
+ 'services_warning_handled',
+ 'services_warning_unhandled',
+ 'services_warning_passive',
+ 'services_warning_not_checked',
+ 'services_critical',
+ 'services_critical_handled',
+ 'services_critical_unhandled',
+ 'services_critical_passive',
+ 'services_critical_not_checked',
+ 'services_unknown',
+ 'services_unknown_handled',
+ 'services_unknown_unhandled',
+ 'services_unknown_passive',
+ 'services_unknown_not_checked',
+ 'services_active',
+ 'services_passive',
+ 'services_not_checked',
+ 'services_not_processing_event_handlers',
+ 'services_not_triggering_notifications',
+ 'services_without_flap_detection',
+ 'services_flapping',
+
+
+ 'services_ok_on_ok_hosts',
+ 'services_ok_not_checked_on_ok_hosts',
+ 'services_pending_on_ok_hosts',
+ 'services_pending_not_checked_on_ok_hosts',
+ 'services_warning_handled_on_ok_hosts',
+ 'services_warning_unhandled_on_ok_hosts',
+ 'services_warning_passive_on_ok_hosts',
+ 'services_warning_not_checked_on_ok_hosts',
+ 'services_critical_handled_on_ok_hosts',
+ 'services_critical_unhandled_on_ok_hosts',
+ 'services_critical_passive_on_ok_hosts',
+ 'services_critical_not_checked_on_ok_hosts',
+ 'services_unknown_handled_on_ok_hosts',
+ 'services_unknown_unhandled_on_ok_hosts',
+ 'services_unknown_passive_on_ok_hosts',
+ 'services_unknown_not_checked_on_ok_hosts',
+ 'services_ok_on_problem_hosts',
+ 'services_ok_not_checked_on_problem_hosts',
+ 'services_pending_on_problem_hosts',
+ 'services_pending_not_checked_on_problem_hosts',
+ 'services_warning_handled_on_problem_hosts',
+ 'services_warning_unhandled_on_problem_hosts',
+ 'services_warning_passive_on_problem_hosts',
+ 'services_warning_not_checked_on_problem_hosts',
+ 'services_critical_handled_on_problem_hosts',
+ 'services_critical_unhandled_on_problem_hosts',
+ 'services_critical_passive_on_problem_hosts',
+ 'services_critical_not_checked_on_problem_hosts',
+ 'services_unknown_handled_on_problem_hosts',
+ 'services_unknown_unhandled_on_problem_hosts',
+ 'services_unknown_passive_on_problem_hosts',
+ 'services_unknown_not_checked_on_problem_hosts'
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'instance_name',
+ 'host', 'host_alias', 'host_display_name', 'host_name',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'service', 'service_description', 'service_display_name',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Unhandledhostproblems.php b/modules/monitoring/library/Monitoring/DataView/Unhandledhostproblems.php
new file mode 100644
index 0000000..4f5f392
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Unhandledhostproblems.php
@@ -0,0 +1,28 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * Data view for unhandled host problems
+ */
+class Unhandledhostproblems extends DataView
+{
+ public function getColumns()
+ {
+ return array(
+ 'hosts_down_unhandled'
+ );
+ }
+
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'instance_name',
+ 'host', 'host_alias', 'host_display_name', 'host_name',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'service', 'service_description', 'service_display_name',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/DataView/Unhandledserviceproblems.php b/modules/monitoring/library/Monitoring/DataView/Unhandledserviceproblems.php
new file mode 100644
index 0000000..3af4502
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/DataView/Unhandledserviceproblems.php
@@ -0,0 +1,28 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\DataView;
+
+/**
+ * Data view for unhandled service problems
+ */
+class Unhandledserviceproblems extends DataView
+{
+ public function getColumns()
+ {
+ return array(
+ 'services_critical_unhandled'
+ );
+ }
+
+ public function getStaticFilterColumns()
+ {
+ return array(
+ 'instance_name',
+ 'host', 'host_alias', 'host_display_name', 'host_name',
+ 'hostgroup', 'hostgroup_alias', 'hostgroup_name',
+ 'service', 'service_description', 'service_display_name',
+ 'servicegroup', 'servicegroup_alias', 'servicegroup_name'
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Exception/CommandTransportException.php b/modules/monitoring/library/Monitoring/Exception/CommandTransportException.php
new file mode 100644
index 0000000..5c08351
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Exception/CommandTransportException.php
@@ -0,0 +1,13 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Exception;
+
+use Icinga\Exception\IcingaException;
+
+/**
+ * Exception thrown if a command was not sent
+ */
+class CommandTransportException extends IcingaException
+{
+}
diff --git a/modules/monitoring/library/Monitoring/Exception/CurlException.php b/modules/monitoring/library/Monitoring/Exception/CurlException.php
new file mode 100644
index 0000000..01757af
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Exception/CurlException.php
@@ -0,0 +1,13 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Exception;
+
+use Icinga\Exception\IcingaException;
+
+/**
+ * Exception thrown if {@link curl_exec()} fails
+ */
+class CurlException extends IcingaException
+{
+}
diff --git a/modules/monitoring/library/Monitoring/Exception/UnsupportedBackendException.php b/modules/monitoring/library/Monitoring/Exception/UnsupportedBackendException.php
new file mode 100644
index 0000000..94d1af2
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Exception/UnsupportedBackendException.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+
+namespace Icinga\Module\Monitoring\Exception;
+
+use Icinga\Exception\IcingaException;
+
+class UnsupportedBackendException extends IcingaException
+{
+}
diff --git a/modules/monitoring/library/Monitoring/Hook/CustomVarRendererHook.php b/modules/monitoring/library/Monitoring/Hook/CustomVarRendererHook.php
new file mode 100644
index 0000000..6895337
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Hook/CustomVarRendererHook.php
@@ -0,0 +1,98 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Hook;
+
+use Closure;
+use Exception;
+use Icinga\Application\Hook;
+use Icinga\Application\Logger;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+
+abstract class CustomVarRendererHook
+{
+ /**
+ * Prefetch the data the hook needs to render custom variables
+ *
+ * @param MonitoredObject $object The object for which they'll be rendered
+ *
+ * @return bool Return true if the hook can render variables for the given object, false otherwise
+ */
+ abstract public function prefetchForObject(MonitoredObject $object);
+
+ /**
+ * Render the given variable name
+ *
+ * @param string $key
+ *
+ * @return ?mixed
+ */
+ abstract public function renderCustomVarKey($key);
+
+ /**
+ * Render the given variable value
+ *
+ * @param string $key
+ * @param mixed $value
+ *
+ * @return ?mixed
+ */
+ abstract public function renderCustomVarValue($key, $value);
+
+ /**
+ * Return a group name for the given variable name
+ *
+ * @param string $key
+ *
+ * @return ?string
+ */
+ abstract public function identifyCustomVarGroup($key);
+
+ /**
+ * Prepare available hooks to render custom variables of the given object
+ *
+ * @param MonitoredObject $object
+ *
+ * @return Closure A callback ($key, $value) which returns an array [$newKey, $newValue, $group]
+ */
+ final public static function prepareForObject(MonitoredObject $object)
+ {
+ $hooks = [];
+ foreach (Hook::all('Monitoring/CustomVarRenderer') as $hook) {
+ /** @var self $hook */
+ try {
+ if ($hook->prefetchForObject($object)) {
+ $hooks[] = $hook;
+ }
+ } catch (Exception $e) {
+ Logger::error('Failed to load hook %s:', get_class($hook), $e);
+ }
+ }
+
+ return function ($key, $value) use ($hooks) {
+ $newKey = $key;
+ $newValue = $value;
+ $group = null;
+ foreach ($hooks as $hook) {
+ /** @var self $hook */
+
+ try {
+ $renderedKey = $hook->renderCustomVarKey($key);
+ $renderedValue = $hook->renderCustomVarValue($key, $value);
+ $group = $hook->identifyCustomVarGroup($key);
+ } catch (Exception $e) {
+ Logger::error('Failed to use hook %s:', get_class($hook), $e);
+ continue;
+ }
+
+ if ($renderedKey !== null || $renderedValue !== null) {
+ $newKey = $renderedKey !== null ? $renderedKey : $key;
+ $newValue = $renderedValue !== null ? $renderedValue : $value;
+ break;
+ }
+ }
+
+ return [$newKey, $newValue, $group];
+ };
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Hook/DataviewExtensionHook.php b/modules/monitoring/library/Monitoring/Hook/DataviewExtensionHook.php
new file mode 100644
index 0000000..24b97c5
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Hook/DataviewExtensionHook.php
@@ -0,0 +1,20 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Hook;
+
+abstract class DataviewExtensionHook
+{
+ public function getAdditionalQueryColumns($queryName)
+ {
+ $cols = $this->provideAdditionalQueryColumns($queryName);
+
+ if (! is_array($cols)) {
+ return array();
+ }
+
+ return $cols;
+ }
+
+ abstract public function provideAdditionalQueryColumns($queryName);
+}
diff --git a/modules/monitoring/library/Monitoring/Hook/DetailviewExtensionHook.php b/modules/monitoring/library/Monitoring/Hook/DetailviewExtensionHook.php
new file mode 100644
index 0000000..9eb5ca3
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Hook/DetailviewExtensionHook.php
@@ -0,0 +1,126 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Hook;
+
+use Icinga\Application\ClassLoader;
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use Icinga\Module\Monitoring\Object\ObjectList;
+use Icinga\Web\View;
+
+/**
+ * Base class for hooks extending the detail view of monitored objects
+ *
+ * Extend this class if you want to extend the detail view of monitored objects with custom HTML.
+ */
+abstract class DetailviewExtensionHook
+{
+ /**
+ * The view the generated HTML will be included in
+ *
+ * @var View
+ */
+ private $view;
+
+ /**
+ * The module of the derived class
+ *
+ * @var Module
+ */
+ private $module;
+
+ /**
+ * Create a new hook
+ *
+ * @see init() For hook initialization.
+ */
+ final public function __construct()
+ {
+ $this->init();
+ }
+
+ /**
+ * Overwrite this function for hook initialization, e.g. loading the hook's config
+ */
+ protected function init()
+ {
+ }
+
+ /**
+ * Shall return valid HTML to include in the detail view
+ *
+ * @param MonitoredObject $object The object to generate HTML for
+ *
+ * @return string
+ */
+ abstract public function getHtmlForObject(MonitoredObject $object);
+
+ /**
+ * Shall return valid HTML to include in the detail view of a multi-select view
+ *
+ * @param ObjectList $objects A list of objects shown in the multi-select view
+ *
+ * @return string
+ */
+ public function getHtmlForObjects($objects)
+ {
+ // For compatibility empty by default
+ return '';
+ }
+
+ /**
+ * Get {@link view}
+ *
+ * @return View
+ */
+ public function getView()
+ {
+ return $this->view;
+ }
+
+ /**
+ * Set {@link view}
+ *
+ * @param View $view
+ *
+ * @return $this
+ */
+ public function setView($view)
+ {
+ $this->view = $view;
+ return $this;
+ }
+
+ /**
+ * Get the module of the derived class
+ *
+ * @return Module
+ */
+ public function getModule()
+ {
+ if ($this->module === null) {
+ $class = get_class($this);
+ if (ClassLoader::classBelongsToModule($class)) {
+ $this->module = Icinga::app()->getModuleManager()->getModule(ClassLoader::extractModuleName($class));
+ }
+ }
+
+ return $this->module;
+ }
+
+ /**
+ * Set the module of the derived class
+ *
+ * @param Module $module
+ *
+ * @return $this
+ */
+ public function setModule(Module $module)
+ {
+ $this->module = $module;
+
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Hook/EventDetailsExtensionHook.php b/modules/monitoring/library/Monitoring/Hook/EventDetailsExtensionHook.php
new file mode 100644
index 0000000..e0375d5
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Hook/EventDetailsExtensionHook.php
@@ -0,0 +1,79 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Hook;
+
+use Icinga\Application\ClassLoader;
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+
+/**
+ * Base class for hooks extending the event view of monitored objects
+ *
+ * Extend this class if you want to extend the event view of monitored objects with custom HTML.
+ */
+abstract class EventDetailsExtensionHook
+{
+ /**
+ * The module of the derived class
+ *
+ * @var Module
+ */
+ private $module;
+
+ /**
+ * Create a new hook
+ *
+ * @see init() For hook initialization.
+ */
+ final public function __construct()
+ {
+ $this->init();
+ }
+ /**
+ * Overwrite this function for hook initialization, e.g. loading the hook's config
+ */
+ protected function init()
+ {
+ }
+
+
+ /**
+ * Shall return valid HTML to include in the detail view
+ *
+ * @param object $event The object to generate HTML for
+ *
+ * @return string
+ */
+ abstract public function getHtmlForEvent($event);
+
+ /**
+ * Get the module of the derived class
+ *
+ * @return Module
+ * @throws \Icinga\Exception\ProgrammingError
+ */
+ public function getModule()
+ {
+ if ($this->module === null) {
+ $class = get_class($this);
+ if (ClassLoader::classBelongsToModule($class)) {
+ $this->module = Icinga::app()->getModuleManager()->getModule(ClassLoader::extractModuleName($class));
+ }
+ }
+ return $this->module;
+ }
+
+ /**
+ * Set the module of the derived class
+ *
+ * @param Module $module
+ *
+ * @return $this
+ */
+ public function setModule(Module $module)
+ {
+ $this->module = $module;
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Hook/HostActionsHook.php b/modules/monitoring/library/Monitoring/Hook/HostActionsHook.php
new file mode 100644
index 0000000..def0090
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Hook/HostActionsHook.php
@@ -0,0 +1,52 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Hook;
+
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+
+/**
+ * Base class for host action hooks
+ */
+abstract class HostActionsHook extends ObjectActionsHook
+{
+ /**
+ * Implementors of this method should return an array containing
+ * additional action links for a specific host. You get a full Host
+ * object, which allows you to return specific links only for nodes
+ * with specific properties.
+ *
+ * The result array should be in the form title => url, where title will
+ * be used as link caption. Url should be an Icinga\Web\Url object when
+ * the link should point to an Icinga Web url - otherwise a string would
+ * be fine.
+ *
+ * Mixed example:
+ * <code>
+ * return array(
+ * 'Wiki' => 'http://my.wiki/host=' . rawurlencode($host->host_name),
+ * 'Logstash' => Url::fromPath(
+ * 'logstash/search/syslog',
+ * array('host' => $host->host_name)
+ * )
+ * );
+ * </code>
+ *
+ * One might also provide ssh:// or rdp:// urls if equipped with fitting
+ * (safe) URL handlers for his browser(s).
+ *
+ * TODO: I'd love to see some kind of a Link/LinkSet object implemented
+ * for this and similar hooks.
+ *
+ * @param Host $host Monitoring host object
+ *
+ * @return array An array containing a list of host action links
+ */
+ abstract public function getActionsForHost(Host $host);
+
+ public function getActionsForObject(MonitoredObject $object)
+ {
+ return $this->getActionsForHost($object);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Hook/IdoQueryExtensionHook.php b/modules/monitoring/library/Monitoring/Hook/IdoQueryExtensionHook.php
new file mode 100644
index 0000000..64ac65c
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Hook/IdoQueryExtensionHook.php
@@ -0,0 +1,15 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Hook;
+
+use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery;
+
+abstract class IdoQueryExtensionHook
+{
+ abstract public function extendColumnMap(IdoQuery $query);
+
+ public function joinVirtualTable(IdoQuery $query, $virtualTable)
+ {
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Hook/ObjectActionsHook.php b/modules/monitoring/library/Monitoring/Hook/ObjectActionsHook.php
new file mode 100644
index 0000000..eb2d910
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Hook/ObjectActionsHook.php
@@ -0,0 +1,47 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Hook;
+
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+
+/**
+ * Base class for object action hooks
+ */
+abstract class ObjectActionsHook
+{
+ /**
+ * Return the action navigation for the given object
+ *
+ * @return Navigation
+ */
+ public function getNavigation(MonitoredObject $object)
+ {
+ $urls = $this->getActionsForObject($object);
+ if (is_array($urls)) {
+ $navigation = new Navigation();
+ foreach ($urls as $label => $url) {
+ $navigation->addItem($label, array('url' => $url));
+ }
+ } else {
+ $navigation = $urls;
+ }
+
+ return $navigation;
+ }
+
+ /**
+ * Create and return a new Navigation object
+ *
+ * @param array $actions Optional array of actions to add to the returned object
+ *
+ * @return Navigation
+ */
+ protected function createNavigation(array $actions = null)
+ {
+ return empty($actions) ? new Navigation() : Navigation::fromArray($actions);
+ }
+
+ abstract public function getActionsForObject(MonitoredObject $object);
+}
diff --git a/modules/monitoring/library/Monitoring/Hook/ObjectDetailsTabHook.php b/modules/monitoring/library/Monitoring/Hook/ObjectDetailsTabHook.php
new file mode 100644
index 0000000..15fa9bb
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Hook/ObjectDetailsTabHook.php
@@ -0,0 +1,60 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Hook;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use Icinga\Web\Request;
+
+/**
+ * Base class for object host details custom tab hooks
+ */
+abstract class ObjectDetailsTabHook
+{
+ /**
+ * Return the tab name - it must be unique
+ *
+ * @return string
+ */
+ abstract public function getName();
+
+ /**
+ * Return the tab label
+ *
+ * @return string
+ */
+ abstract public function getLabel();
+
+ /**
+ * Return the tab header
+ *
+ * @param MonitoredObject $monitoredObject The monitored object related to that page
+ * @param Request $request
+ * @return string/bool The HTML string that compose the tab header,
+ * bool True if the default header should be shown, False to display nothing
+ */
+ public function getHeader(MonitoredObject $monitoredObject, Request $request)
+ {
+ return true;
+ }
+
+ /**
+ * Return the tab content
+ *
+ * @param MonitoredObject $monitoredObject The monitored object related to that page
+ * @param Request $request
+ * @return string The HTML string that compose the tab content
+ */
+ abstract public function getContent(MonitoredObject $monitoredObject, Request $request);
+
+ /**
+ * This method returns true if the tab is visible for the logged user, otherwise false
+ *
+ * @return bool True if the tab is visible for the logged user, otherwise false
+ */
+ public function shouldBeShown(MonitoredObject $monitoredObject, Auth $auth)
+ {
+ return true;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Hook/PluginOutputHook.php b/modules/monitoring/library/Monitoring/Hook/PluginOutputHook.php
new file mode 100644
index 0000000..52ecd09
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Hook/PluginOutputHook.php
@@ -0,0 +1,46 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Hook;
+
+/**
+ * Base class for plugin output hooks
+ *
+ * The Plugin Output Hook allows you to rewrite the plugin output based on check commands.
+ * You have to implement the following methods:
+ * * {@link getCommands()}
+ * * and {@link render()}
+ */
+abstract class PluginOutputHook
+{
+ /**
+ * Get the command or list of commands the hook is responsible for
+ *
+ * With this method you specify for which commands the provided hook is responsible for. You may return a single
+ * command as string or a list of commands as array.
+ * If you want your hook to be responsible for every command, you have to return the asterisk `'*'`.
+ *
+ * @return string|array
+ */
+ abstract public function getCommands();
+
+ /**
+ * Render the given plugin output based on the specified check command
+ *
+ * With this method you rewrite the plugin output based on check commands. The parameter `$command` specifies the
+ * check command of the host or service and `$output` specifies the plugin output. The parameter `$detail` tells you
+ * whether the output is requested from the detail area of the host or service.
+ *
+ * Do not use complex logic for rewriting plugin output in list views because of the performance impact!
+ *
+ * You have to return the rewritten plugin output as string. It is also possible to return a HTML string here.
+ * Please refer to {@link \Icinga\Module\Monitoring\Web\Helper\PluginOutputPurifier} for a list of allowed tags.
+ *
+ * @param string $command Check command
+ * @param string $output Plugin output
+ * @param bool $detail Whether the output is requested from the detail area
+ *
+ * @return string Rewritten plugin output
+ */
+ abstract public function render($command, $output, $detail);
+}
diff --git a/modules/monitoring/library/Monitoring/Hook/ServiceActionsHook.php b/modules/monitoring/library/Monitoring/Hook/ServiceActionsHook.php
new file mode 100644
index 0000000..c6cf5f5
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Hook/ServiceActionsHook.php
@@ -0,0 +1,52 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Hook;
+
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+
+/**
+ * Base class for host action hooks
+ */
+abstract class ServiceActionsHook extends ObjectActionsHook
+{
+ /**
+ * Implementors of this method should return an array containing
+ * additional action links for a specific host. You get a full Service
+ * object, which allows you to return specific links only for nodes
+ * with specific properties.
+ *
+ * The result array should be in the form title => url, where title will
+ * be used as link caption. Url should be an Icinga\Web\Url object when
+ * the link should point to an Icinga Web url - otherwise a string would
+ * be fine.
+ *
+ * Mixed example:
+ * <code>
+ * return array(
+ * 'Wiki' => 'http://my.wiki/host=' . rawurlencode($service->service_name),
+ * 'Logstash' => Url::fromPath(
+ * 'logstash/search/syslog',
+ * array('service' => $service->host_name)
+ * )
+ * );
+ * </code>
+ *
+ * One might also provide ssh:// or rdp:// urls if equipped with fitting
+ * (safe) URL handlers for his browser(s).
+ *
+ * TODO: I'd love to see some kind of a Link/LinkSet object implemented
+ * for this and similar hooks.
+ *
+ * @param Service $service Monitoring service object
+ *
+ * @return array An array containing a list of service action links
+ */
+ abstract public function getActionsForService(Service $service);
+
+ public function getActionsForObject(MonitoredObject $object)
+ {
+ return $this->getActionsForService($object);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Hook/TimelineProviderHook.php b/modules/monitoring/library/Monitoring/Hook/TimelineProviderHook.php
new file mode 100644
index 0000000..d302d12
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Hook/TimelineProviderHook.php
@@ -0,0 +1,37 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Hook;
+
+use Icinga\Module\Monitoring\Timeline\TimeRange;
+
+/**
+ * Base class for TimeLine providers
+ */
+abstract class TimelineProviderHook
+{
+ /**
+ * Return the names by which to group entries
+ *
+ * @return array An array with the names as keys and their attribute-lists as values
+ */
+ abstract public function getIdentifiers();
+
+ /**
+ * Return the visible entries supposed to be shown on the timeline
+ *
+ * @param TimeRange $range The range of time for which to fetch entries
+ *
+ * @return array The entries to display on the timeline
+ */
+ abstract public function fetchEntries(TimeRange $range);
+
+ /**
+ * Return the entries supposed to be used to calculate forecasts
+ *
+ * @param TimeRange $range The range of time for which to fetch forecasts
+ *
+ * @return array The entries to calculate forecasts with
+ */
+ abstract public function fetchForecasts(TimeRange $range);
+}
diff --git a/modules/monitoring/library/Monitoring/MonitoringWizard.php b/modules/monitoring/library/Monitoring/MonitoringWizard.php
new file mode 100644
index 0000000..d19650a
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/MonitoringWizard.php
@@ -0,0 +1,160 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring;
+
+use Icinga\Module\Setup\Forms\RequirementsPage;
+use Icinga\Web\Form;
+use Icinga\Web\Wizard;
+use Icinga\Web\Request;
+use Icinga\Module\Setup\Setup;
+use Icinga\Module\Setup\SetupWizard;
+use Icinga\Module\Setup\RequirementSet;
+use Icinga\Module\Setup\Forms\SummaryPage;
+use Icinga\Module\Monitoring\Forms\Setup\WelcomePage;
+use Icinga\Module\Monitoring\Forms\Setup\SecurityPage;
+use Icinga\Module\Monitoring\Forms\Setup\TransportPage;
+use Icinga\Module\Monitoring\Forms\Setup\IdoResourcePage;
+use Icinga\Module\Setup\Requirement\PhpModuleRequirement;
+
+/**
+ * Monitoring Module Setup Wizard
+ */
+class MonitoringWizard extends Wizard implements SetupWizard
+{
+ /**
+ * Register all pages for this wizard
+ */
+ public function init()
+ {
+ $this->addPage(new WelcomePage());
+ $this->addPage(new IdoResourcePage());
+ $this->addPage(new TransportPage());
+ $this->addPage(new SecurityPage());
+ $this->addPage(new SummaryPage(array('name' => 'setup_monitoring_summary')));
+ }
+
+ /**
+ * Setup the given page that is either going to be displayed or validated
+ *
+ * @param Form|RequirementsPage|SummaryPage $page The page to setup
+ * @param Request $request The current request
+ */
+ public function setupPage(Form $page, Request $request)
+ {
+ if ($page->getName() === 'setup_requirements') {
+ $page->setRequirements($this->getRequirements());
+ } elseif ($page->getName() === 'setup_monitoring_summary') {
+ $page->setSummary($this->getSetup()->getSummary());
+ $page->setSubjectTitle(mt('monitoring', 'the monitoring module', 'setup.summary.subject'));
+ } elseif ($this->getDirection() === static::FORWARD
+ && ($page->getName() === 'setup_monitoring_ido')
+ ) {
+ if ((($authDbResourceData = $this->getPageData('setup_auth_db_resource')) !== null
+ && $authDbResourceData['name'] === $request->getPost('name'))
+ || (($configDbResourceData = $this->getPageData('setup_config_db_resource')) !== null
+ && $configDbResourceData['name'] === $request->getPost('name'))
+ || (($ldapResourceData = $this->getPageData('setup_ldap_resource')) !== null
+ && $ldapResourceData['name'] === $request->getPost('name'))
+ ) {
+ $page->error(mt('monitoring', 'The given resource name is already in use.'));
+ }
+ }
+ }
+
+ /**
+ * Add buttons to the given page based on its position in the page-chain
+ *
+ * @param Form $page The page to add the buttons to
+ *
+ * @todo This is never called, because its a sub-wizard only
+ * @todo This is missing the ´transport_validation´ case
+ * @see WebWizard::addButtons which does some of the needed work
+ */
+ protected function addButtons(Form $page)
+ {
+ parent::addButtons($page);
+
+ $pages = $this->getPages();
+ $index = array_search($page, $pages, true);
+ if ($index === 0) {
+ // Used t() here as "Start" is too generic and already translated in the icinga domain
+ $page->getElement(static::BTN_NEXT)->setLabel(t('Start', 'setup.welcome.btn.next'));
+ } elseif ($index === count($pages) - 1) {
+ $page->getElement(static::BTN_NEXT)->setLabel(
+ mt('monitoring', 'Setup the monitoring module for Icinga Web 2', 'setup.summary.btn.finish')
+ );
+ }
+
+ if ($page->getName() === 'setup_monitoring_ido') {
+ $page->addElement(
+ 'submit',
+ 'backend_validation',
+ array(
+ 'ignore' => true,
+ 'label' => t('Validate Configuration'),
+ 'data-progress-label' => t('Validation In Progress'),
+ 'decorators' => array('ViewHelper'),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ $page->getDisplayGroup('buttons')->addElement($page->getElement('backend_validation'));
+ }
+ }
+
+ /**
+ * Return the setup for this wizard
+ *
+ * @return Setup
+ */
+ public function getSetup()
+ {
+ $pageData = $this->getPageData();
+ $setup = new Setup();
+
+ $setup->addStep(
+ new BackendStep(array(
+ 'backendConfig' => ['name' => 'icinga', 'type' => 'ido'],
+ 'resourceConfig' => array_diff_key(
+ $pageData['setup_monitoring_ido'], //TODO: Prefer a new backend once implemented.
+ array('skip_validation' => null)
+ )
+ ))
+ );
+
+ $setup->addStep(
+ new TransportStep(array(
+ 'transportConfig' => $pageData['setup_command_transport']
+ ))
+ );
+
+ $setup->addStep(
+ new SecurityStep(array(
+ 'securityConfig' => $pageData['setup_monitoring_security']
+ ))
+ );
+
+ return $setup;
+ }
+
+ /**
+ * Return the requirements of this wizard
+ *
+ * @return RequirementSet
+ */
+ public function getRequirements()
+ {
+ $set = new RequirementSet();
+ $set->add(new PhpModuleRequirement(array(
+ 'optional' => true,
+ 'condition' => 'curl',
+ 'alias' => 'cURL',
+ 'description' => mt(
+ 'monitoring',
+ 'To send external commands over Icinga 2\'s API the cURL module for PHP is required.'
+ )
+ )));
+
+ return $set;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Object/Acknowledgement.php b/modules/monitoring/library/Monitoring/Object/Acknowledgement.php
new file mode 100644
index 0000000..3cd0d20
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Object/Acknowledgement.php
@@ -0,0 +1,215 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Object;
+
+use InvalidArgumentException;
+use Traversable;
+use Icinga\Util\StringHelper;
+
+/**
+ * Acknowledgement of a host or service incident
+ */
+class Acknowledgement
+{
+ /**
+ * Author of the acknowledgement
+ *
+ * @var string
+ */
+ protected $author;
+
+ /**
+ * Comment of the acknowledgement
+ *
+ * @var string
+ */
+ protected $comment;
+
+ /**
+ * Entry time of the acknowledgement
+ *
+ * @var int
+ */
+ protected $entryTime;
+
+ /**
+ * Expiration time of the acknowledgment
+ *
+ * @var int|null
+ */
+ protected $expirationTime;
+
+ /**
+ * Whether the acknowledgement is sticky
+ *
+ * Sticky acknowledgements suppress notifications until the host or service recovers
+ *
+ * @var bool
+ */
+ protected $sticky = false;
+
+ /**
+ * Create a new acknowledgement of a host or service incident
+ *
+ * @param array|object|Traversable $properties
+ *
+ * @throws InvalidArgumentException If the type of the given properties is invalid
+ */
+ public function __construct($properties = null)
+ {
+ if ($properties !== null) {
+ $this->setProperties($properties);
+ }
+ }
+
+ /**
+ * Get the author of the acknowledgement
+ *
+ * @return string
+ */
+ public function getAuthor()
+ {
+ return $this->author;
+ }
+
+ /**
+ * Set the author of the acknowledgement
+ *
+ * @param string $author
+ *
+ * @return $this
+ */
+ public function setAuthor($author)
+ {
+ $this->author = (string) $author;
+ return $this;
+ }
+
+ /**
+ * Get the comment of the acknowledgement
+ *
+ * @return string
+ */
+ public function getComment()
+ {
+ return $this->comment;
+ }
+
+ /**
+ * Set the comment of the acknowledgement
+ *
+ * @param string $comment
+ *
+ * @return $this
+ */
+ public function setComment($comment)
+ {
+ $this->comment = (string) $comment;
+
+ return $this;
+ }
+
+ /**
+ * Get the entry time of the acknowledgement
+ *
+ * @return int
+ */
+ public function getEntryTime()
+ {
+ return $this->entryTime;
+ }
+
+ /**
+ * Set the entry time of the acknowledgement
+ *
+ * @param int $entryTime
+ *
+ * @return $this
+ */
+ public function setEntryTime($entryTime)
+ {
+ $this->entryTime = (int) $entryTime;
+
+ return $this;
+ }
+
+ /**
+ * Get the expiration time of the acknowledgement
+ *
+ * @return int|null
+ */
+ public function getExpirationTime()
+ {
+ return $this->expirationTime;
+ }
+
+ /**
+ * Set the expiration time of the acknowledgement
+ *
+ * @param int|null $expirationTime Unix timestamp
+ *
+ * @return $this
+ */
+ public function setExpirationTime($expirationTime = null)
+ {
+ $this->expirationTime = $expirationTime !== null ? (int) $expirationTime : null;
+
+ return $this;
+ }
+
+ /**
+ * Get whether the acknowledgement is sticky
+ *
+ * @return bool
+ */
+ public function getSticky()
+ {
+ return $this->sticky;
+ }
+
+ /**
+ * Set whether the acknowledgement is sticky
+ *
+ * @param bool $sticky
+ *
+ * @return $this
+ */
+ public function setSticky($sticky = true)
+ {
+ $this->sticky = (bool) $sticky;
+ return $this;
+ }
+
+ /**
+ * Get whether the acknowledgement expires
+ *
+ * @return bool
+ */
+ public function expires()
+ {
+ return $this->expirationTime !== null;
+ }
+
+ /**
+ * Set the properties of the acknowledgement
+ *
+ * @param array|object|Traversable $properties
+ *
+ * @return $this
+ * @throws InvalidArgumentException If the type of the given properties is invalid
+ */
+ public function setProperties($properties)
+ {
+ if (! is_array($properties) && ! is_object($properties) && ! $properties instanceof Traversable) {
+ throw new InvalidArgumentException('Properties must be either an array or an instance of Traversable');
+ }
+ foreach ($properties as $name => $value) {
+ $setter = 'set' . ucfirst(StringHelper::cname($name));
+ if (method_exists($this, $setter)) {
+ $this->$setter($value);
+ }
+ }
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Object/Host.php b/modules/monitoring/library/Monitoring/Object/Host.php
new file mode 100644
index 0000000..dd8538b
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Object/Host.php
@@ -0,0 +1,205 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Object;
+
+use Icinga\Data\Filter\FilterEqual;
+use Icinga\Module\Monitoring\DataView\Hoststatus;
+use InvalidArgumentException;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+
+/**
+ * An Icinga host
+ */
+class Host extends MonitoredObject
+{
+ /**
+ * Host state 'UP'
+ */
+ const STATE_UP = 0;
+
+ /**
+ * Host state 'DOWN'
+ */
+ const STATE_DOWN = 1;
+
+ /**
+ * Host state 'UNREACHABLE'
+ */
+ const STATE_UNREACHABLE = 2;
+
+ /**
+ * Host state 'PENDING'
+ */
+ const STATE_PENDING = 99;
+
+ /**
+ * Type of the Icinga host
+ *
+ * @var string
+ */
+ public $type = self::TYPE_HOST;
+
+ /**
+ * Prefix of the Icinga host
+ *
+ * @var string
+ */
+ public $prefix = 'host_';
+
+ /**
+ * Hostname
+ *
+ * @var string
+ */
+ protected $host;
+
+ /**
+ * The services running on the hosts
+ *
+ * @var \Icinga\Module\Monitoring\Object\Service[]
+ */
+ protected $services;
+
+ /**
+ * Create a new host
+ *
+ * @param MonitoringBackend $backend Backend to fetch host information from
+ * @param string $host Hostname
+ */
+ public function __construct(MonitoringBackend $backend, $host)
+ {
+ parent::__construct($backend);
+ $this->host = $host;
+ }
+
+ /**
+ * Get the hostname
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->host;
+ }
+
+ /**
+ * Get the data view to fetch the host information from
+ *
+ * @return Hoststatus
+ */
+ protected function getDataView()
+ {
+ $columns = array(
+ 'host_acknowledged',
+ 'host_acknowledgement_type',
+ 'host_action_url',
+ 'host_active_checks_enabled',
+ 'host_active_checks_enabled_changed',
+ 'host_address',
+ 'host_address6',
+ 'host_alias',
+ 'host_attempt',
+ 'host_check_command',
+ 'host_check_execution_time',
+ 'host_check_interval',
+ 'host_check_latency',
+ 'host_check_source',
+ 'host_check_timeperiod',
+ 'host_current_check_attempt',
+ 'host_current_notification_number',
+ 'host_display_name',
+ 'host_event_handler_enabled',
+ 'host_event_handler_enabled_changed',
+ 'host_flap_detection_enabled',
+ 'host_flap_detection_enabled_changed',
+ 'host_handled',
+ 'host_icon_image',
+ 'host_icon_image_alt',
+ 'host_in_downtime',
+ 'host_is_flapping',
+ 'host_is_reachable',
+ 'host_last_check',
+ 'host_last_notification',
+ 'host_last_state_change',
+ 'host_long_output',
+ 'host_max_check_attempts',
+ 'host_name',
+ 'host_next_check',
+ 'host_next_update',
+ 'host_notes',
+ 'host_notes_url',
+ 'host_notifications_enabled',
+ 'host_notifications_enabled_changed',
+ 'host_obsessing',
+ 'host_obsessing_changed',
+ 'host_output',
+ 'host_passive_checks_enabled',
+ 'host_passive_checks_enabled_changed',
+ 'host_percent_state_change',
+ 'host_perfdata',
+ 'host_process_perfdata' => 'host_process_performance_data',
+ 'host_state',
+ 'host_state_type',
+ 'instance_name'
+ );
+ return $this->backend->select()->from('hoststatus', $columns)
+ ->whereEx(new FilterEqual('host_name', '=', $this->host));
+ }
+
+ /**
+ * Fetch the services running on the host
+ *
+ * @return $this
+ */
+ public function fetchServices()
+ {
+ $services = array();
+ foreach ($this->backend->select()->from('servicestatus', array('service_description'))
+ ->where('host_name', $this->host)
+ ->applyFilter($this->getFilter())
+ ->getQuery() as $service) {
+ $services[] = new Service($this->backend, $this->host, $service->service_description);
+ }
+ $this->services = $services;
+ return $this;
+ }
+
+ /**
+ * Get the optional translated textual representation of a host state
+ *
+ * @param int $state
+ * @param bool $translate
+ *
+ * @return string
+ * @throws InvalidArgumentException If the host state is not valid
+ */
+ public static function getStateText($state, $translate = false)
+ {
+ $translate = (bool) $translate;
+ switch ((int) $state) {
+ case self::STATE_UP:
+ $text = $translate ? mt('monitoring', 'UP') : 'up';
+ break;
+ case self::STATE_DOWN:
+ $text = $translate ? mt('monitoring', 'DOWN') : 'down';
+ break;
+ case self::STATE_UNREACHABLE:
+ $text = $translate ? mt('monitoring', 'UNREACHABLE') : 'unreachable';
+ break;
+ case self::STATE_PENDING:
+ $text = $translate ? mt('monitoring', 'PENDING') : 'pending';
+ break;
+ default:
+ throw new InvalidArgumentException(sprintf('Invalid host state \'%s\'', $state));
+ }
+ return $text;
+ }
+
+ public function getNotesUrls()
+ {
+ return $this->resolveAllStrings(
+ MonitoredObject::parseAttributeUrls($this->host_notes_url)
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Object/HostList.php b/modules/monitoring/library/Monitoring/Object/HostList.php
new file mode 100644
index 0000000..8b1947d
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Object/HostList.php
@@ -0,0 +1,133 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Object;
+
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterOr;
+use Icinga\Data\SimpleQuery;
+use Icinga\Util\StringHelper;
+
+/**
+ * A host list
+ */
+class HostList extends ObjectList
+{
+ protected $dataViewName = 'hoststatus';
+
+ protected $columns = array('host_name');
+
+ protected function fetchObjects()
+ {
+ $hosts = array();
+ $query = $this->backend->select()->from($this->dataViewName, $this->columns)->applyFilter($this->filter)
+ ->getQuery()->getSelectQuery()->query();
+ foreach ($query as $row) {
+ /** @var object $row */
+ $host = new Host($this->backend, $row->host_name);
+ $host->setProperties($row);
+ $hosts[] = $host;
+ }
+ return $hosts;
+ }
+
+ /**
+ * Create a state summary of all hosts that can be consumed by hostssummary.phtml
+ *
+ * @return SimpleQuery
+ */
+ public function getStateSummary()
+ {
+ $hostStates = array_fill_keys(self::getHostStatesSummaryEmpty(), 0);
+ foreach ($this as $host) {
+ $unhandled = (bool) $host->problem === true && (bool) $host->handled === false;
+
+ $stateName = 'hosts_' . $host::getStateText($host->state);
+ ++$hostStates[$stateName];
+ ++$hostStates[$stateName. ($unhandled ? '_unhandled' : '_handled')];
+ }
+
+ $hostStates['hosts_total'] = count($this);
+
+ $ds = new ArrayDatasource(array((object) $hostStates));
+ return $ds->select();
+ }
+
+ /**
+ * Return an empty array with all possible host state names
+ *
+ * @return array An array containing all possible host states as keys and 0 as values.
+ */
+ public static function getHostStatesSummaryEmpty()
+ {
+ return StringHelper::cartesianProduct(
+ array(
+ array('hosts'),
+ array(
+ Host::getStateText(Host::STATE_UP),
+ Host::getStateText(Host::STATE_DOWN),
+ Host::getStateText(Host::STATE_UNREACHABLE),
+ Host::getStateText(Host::STATE_PENDING)
+ ),
+ array(null, 'handled', 'unhandled')
+ ),
+ '_'
+ );
+ }
+
+ /**
+ * Returns a Filter that matches all hosts in this list
+ *
+ * @return Filter
+ */
+ public function objectsFilter($columns = array('host' => 'host'))
+ {
+ $filterExpression = array();
+ foreach ($this as $host) {
+ $filterExpression[] = Filter::where($columns['host'], $host->getName());
+ }
+ return FilterOr::matchAny($filterExpression);
+ }
+
+ /**
+ * Get the comments
+ *
+ * @return \Icinga\Module\Monitoring\DataView\Hostcomment
+ */
+ public function getComments()
+ {
+ return $this->backend
+ ->select()
+ ->from('hostcomment', array('host_name'))
+ ->applyFilter(clone $this->filter);
+ }
+
+ /**
+ * Get the scheduled downtimes
+ *
+ * @return \Icinga\Module\Monitoring\DataView\Hostdowntime
+ */
+ public function getScheduledDowntimes()
+ {
+ return $this->backend
+ ->select()
+ ->from('hostdowntime', array('host_name'))
+ ->applyFilter(clone $this->filter);
+ }
+
+ /**
+ * @return ObjectList
+ */
+ public function getUnacknowledgedObjects()
+ {
+ $unhandledObjects = array();
+ foreach ($this as $object) {
+ if (! in_array((int) $object->state, array(0, 99)) &&
+ (bool) $object->host_acknowledged === false) {
+ $unhandledObjects[] = $object;
+ }
+ }
+ return $this->newFromArray($unhandledObjects);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Object/Macro.php b/modules/monitoring/library/Monitoring/Object/Macro.php
new file mode 100644
index 0000000..18a1855
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Object/Macro.php
@@ -0,0 +1,83 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Object;
+
+use Exception;
+use Icinga\Application\Logger;
+use stdClass;
+
+/**
+ * Expand macros in string in the context of MonitoredObjects
+ */
+class Macro
+{
+ /**
+ * Known icinga macros
+ *
+ * @var array
+ */
+ private static $icingaMacros = array(
+ 'HOSTNAME' => 'host_name',
+ 'HOSTADDRESS' => 'host_address',
+ 'HOSTADDRESS6' => 'host_address6',
+ 'SERVICEDESC' => 'service_description',
+ 'host.name' => 'host_name',
+ 'host.address' => 'host_address',
+ 'host.address6' => 'host_address6',
+ 'service.description' => 'service_description',
+ 'service.name' => 'service_description'
+ );
+
+ /**
+ * Return the given string with macros being resolved
+ *
+ * @param string $input The string in which to look for macros
+ * @param MonitoredObject|stdClass $object The host or service used to resolve macros
+ *
+ * @return string The substituted or unchanged string
+ */
+ public static function resolveMacros($input, $object)
+ {
+ $matches = array();
+ if (preg_match_all('@\$([^\$\s]+)\$@', $input, $matches)) {
+ foreach ($matches[1] as $key => $value) {
+ $newValue = self::resolveMacro($value, $object);
+ if ($newValue !== $value) {
+ $input = str_replace($matches[0][$key], $newValue, $input);
+ }
+ }
+ }
+
+ return $input;
+ }
+
+ /**
+ * Resolve a macro based on the given object
+ *
+ * @param string $macro The macro to resolve
+ * @param MonitoredObject|stdClass $object The object used to resolve the macro
+ *
+ * @return string The new value or the macro if it cannot be resolved
+ */
+ public static function resolveMacro($macro, $object)
+ {
+ if (isset(self::$icingaMacros[$macro]) && isset($object->{self::$icingaMacros[$macro]})) {
+ return $object->{self::$icingaMacros[$macro]};
+ }
+
+ try {
+ $value = $object->$macro;
+ } catch (Exception $e) {
+ $objectName = $object->getName();
+ if ($object instanceof Service) {
+ $objectName = $object->getHost()->getName() . '!' . $objectName;
+ }
+
+ $value = null;
+ Logger::debug('Unable to resolve macro "%s" on object "%s". An error occured: %s', $macro, $objectName, $e);
+ }
+
+ return $value !== null ? $value : $macro;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Object/MonitoredObject.php b/modules/monitoring/library/Monitoring/Object/MonitoredObject.php
new file mode 100644
index 0000000..91fd9e7
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Object/MonitoredObject.php
@@ -0,0 +1,930 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Object;
+
+use Icinga\Data\Filter\FilterEqual;
+use stdClass;
+use InvalidArgumentException;
+use Icinga\Authentication\Auth;
+use Icinga\Application\Config;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filterable;
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Util\GlobFilter;
+use Icinga\Web\UrlParams;
+
+/**
+ * A monitored Icinga object, i.e. host or service
+ */
+abstract class MonitoredObject implements Filterable
+{
+ /**
+ * Type host
+ */
+ const TYPE_HOST = 'host';
+
+ /**
+ * Type service
+ */
+ const TYPE_SERVICE = 'service';
+
+ /**
+ * Acknowledgement of the host or service if any
+ *
+ * @var object
+ */
+ protected $acknowledgement;
+
+ /**
+ * Backend to fetch object information from
+ *
+ * @var MonitoringBackend
+ */
+ protected $backend;
+
+ /**
+ * Comments
+ *
+ * @var array
+ */
+ protected $comments;
+
+ /**
+ * This object's obfuscated custom variables
+ *
+ * @var array
+ */
+ protected $customvars;
+
+ /**
+ * This object's obfuscated custom variables, names not lower case
+ *
+ * @var array
+ */
+ protected $customvarsWithOriginalNames;
+
+ /**
+ * The host custom variables
+ *
+ * @var array
+ */
+ protected $hostVariables;
+
+ /**
+ * The service custom variables
+ *
+ * @var array
+ */
+ protected $serviceVariables;
+
+ /**
+ * Contact groups
+ *
+ * @var array
+ */
+ protected $contactgroups;
+
+ /**
+ * Contacts
+ *
+ * @var array
+ */
+ protected $contacts;
+
+ /**
+ * Downtimes
+ *
+ * @var array
+ */
+ protected $downtimes;
+
+ /**
+ * Event history
+ *
+ * @var \Icinga\Module\Monitoring\DataView\EventHistory
+ */
+ protected $eventhistory;
+
+ /**
+ * Filter
+ *
+ * @var Filter
+ */
+ protected $filter;
+
+ /**
+ * Host groups
+ *
+ * @var array
+ */
+ protected $hostgroups;
+
+ /**
+ * Prefix of the Icinga object, i.e. 'host_' or 'service_'
+ *
+ * @var string
+ */
+ protected $prefix;
+
+ /**
+ * Properties
+ *
+ * @var object
+ */
+ protected $properties;
+
+ /**
+ * Service groups
+ *
+ * @var array
+ */
+ protected $servicegroups;
+
+ /**
+ * Type of the Icinga object, i.e. 'host' or 'service'
+ *
+ * @var string
+ */
+ protected $type;
+
+ /**
+ * Stats
+ *
+ * @var object
+ */
+ protected $stats;
+
+ /**
+ * The properties to hide from the user
+ *
+ * @var GlobFilter
+ */
+ protected $blacklistedProperties = null;
+
+ /**
+ * Create a monitored object, i.e. host or service
+ *
+ * @param MonitoringBackend $backend Backend to fetch object information from
+ */
+ public function __construct(MonitoringBackend $backend)
+ {
+ $this->backend = $backend;
+ }
+
+ /**
+ * Get the object's data view
+ *
+ * @return \Icinga\Module\Monitoring\DataView\DataView
+ */
+ abstract protected function getDataView();
+
+ /**
+ * Get all note urls configured for this monitored object
+ *
+ * @return array All note urls as a string
+ */
+ abstract public function getNotesUrls();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFilter(Filter $filter)
+ {
+ // Left out on purpose. Interface is deprecated.
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applyFilter(Filter $filter)
+ {
+ $this->getFilter()->addFilter($filter);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFilter()
+ {
+ if ($this->filter === null) {
+ $this->filter = Filter::matchAll();
+ }
+
+ return $this->filter;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFilter(Filter $filter)
+ {
+ // Left out on purpose. Interface is deprecated.
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function where($condition, $value = null)
+ {
+ // Left out on purpose. Interface is deprecated.
+ }
+
+ /**
+ * Require the object's type to be one of the given types
+ *
+ * @param array $oneOf
+ *
+ * @return bool
+ * @throws InvalidArgumentException If the object's type is not one of the given types.
+ */
+ public function assertOneOf(array $oneOf)
+ {
+ if (! in_array($this->type, $oneOf)) {
+ throw new InvalidArgumentException;
+ }
+ return true;
+ }
+
+ /**
+ * Fetch the object's properties
+ *
+ * @return bool
+ */
+ public function fetch()
+ {
+ $properties = $this->getDataView()->applyFilter($this->getFilter())->getQuery()->fetchRow();
+
+ if ($properties === false) {
+ return false;
+ }
+
+ if (isset($properties->host_contacts)) {
+ $this->contacts = array();
+ foreach (preg_split('~,~', $properties->host_contacts) as $contact) {
+ $this->contacts[] = (object) array(
+ 'contact_name' => $contact,
+ 'contact_alias' => $contact,
+ 'contact_email' => null,
+ 'contact_pager' => null,
+ );
+ }
+ }
+
+ $this->properties = $properties;
+
+ return true;
+ }
+
+ /**
+ * Fetch the object's acknowledgement
+ */
+ public function fetchAcknowledgement()
+ {
+ if ($this->comments === null) {
+ $this->fetchComments();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Fetch the object's comments
+ *
+ * @return $this
+ */
+ public function fetchComments()
+ {
+ $commentsView = $this->backend->select()->from('comment', array(
+ 'author' => 'comment_author_name',
+ 'comment' => 'comment_data',
+ 'expiration' => 'comment_expiration',
+ 'id' => 'comment_internal_id',
+ 'name' => 'comment_name',
+ 'persistent' => 'comment_is_persistent',
+ 'timestamp' => 'comment_timestamp',
+ 'type' => 'comment_type'
+ ));
+ if ($this->type === self::TYPE_SERVICE) {
+ $commentsView
+ ->whereEx(new FilterEqual('service_host_name', '=', $this->host_name))
+ ->whereEx(new FilterEqual('service_description', '=', $this->service_description));
+ } else {
+ $commentsView->whereEx(new FilterEqual('host_name', '=', $this->host_name));
+ }
+ $commentsView
+ ->whereEx(new FilterEqual('comment_type', '=', ['ack', 'comment']))
+ ->whereEx(new FilterEqual('object_type', '=', $this->type));
+
+ $comments = $commentsView->fetchAll();
+
+ if ((bool) $this->properties->{$this->prefix . 'acknowledged'}) {
+ $ackCommentIdx = null;
+
+ foreach ($comments as $i => $comment) {
+ if ($comment->type === 'ack') {
+ $this->acknowledgement = new Acknowledgement(array(
+ 'author' => $comment->author,
+ 'comment' => $comment->comment,
+ 'entry_time' => $comment->timestamp,
+ 'expiration_time' => $comment->expiration,
+ 'sticky' => (int) $this->properties->{$this->prefix . 'acknowledgement_type'} === 2
+ ));
+ $ackCommentIdx = $i;
+ break;
+ }
+ }
+
+ if ($ackCommentIdx !== null) {
+ unset($comments[$ackCommentIdx]);
+ }
+ }
+
+ $this->comments = $comments;
+
+ return $this;
+ }
+
+ /**
+ * Fetch the object's contact groups
+ *
+ * @return $this
+ */
+ public function fetchContactgroups()
+ {
+ $contactsGroups = $this->backend->select()->from('contactgroup', array(
+ 'contactgroup_name',
+ 'contactgroup_alias'
+ ));
+ if ($this->type === self::TYPE_SERVICE) {
+ $contactsGroups
+ ->whereEx(new FilterEqual('service_host_name', '=', $this->host_name))
+ ->whereEx(new FilterEqual('service_description', '=', $this->service_description));
+ } else {
+ $contactsGroups->whereEx(new FilterEqual('host_name', '=', $this->host_name));
+ }
+ $this->contactgroups = $contactsGroups;
+ return $this;
+ }
+
+ /**
+ * Fetch the object's contacts
+ *
+ * @return $this
+ */
+ public function fetchContacts()
+ {
+ $contacts = $this->backend->select()->from("{$this->type}contact", array(
+ 'contact_name',
+ 'contact_alias',
+ 'contact_email',
+ 'contact_pager',
+ ));
+ if ($this->type === self::TYPE_SERVICE) {
+ $contacts
+ ->whereEx(new FilterEqual('service_host_name', '=', $this->host_name))
+ ->whereEx(new FilterEqual('service_description', '=', $this->service_description));
+ } else {
+ $contacts->whereEx(new FilterEqual('host_name', '=', $this->host_name));
+ }
+ $this->contacts = $contacts;
+ return $this;
+ }
+
+ /**
+ * Fetch this object's obfuscated custom variables
+ *
+ * @return $this
+ */
+ public function fetchCustomvars()
+ {
+
+ if ($this->type === self::TYPE_SERVICE) {
+ $this->fetchServiceVariables();
+ $customvars = $this->serviceVariables;
+ } else {
+ $this->fetchHostVariables();
+ $customvars = $this->hostVariables;
+ }
+
+ $this->customvars = $customvars;
+ $this->hideBlacklistedProperties();
+ $this->customvars = $this->obfuscateCustomVars($this->customvars, null);
+ $this->customvarsWithOriginalNames = $this->obfuscateCustomVars($this->customvarsWithOriginalNames, null);
+
+ return $this;
+ }
+
+ /**
+ * Obfuscate custom variables recursively
+ *
+ * @param stdClass|array $customvars The custom variables to obfuscate
+ *
+ * @return stdClass|array The obfuscated custom variables
+ */
+ protected function obfuscateCustomVars($customvars, $_)
+ {
+ return self::protectCustomVars($customvars);
+ }
+
+ public static function protectCustomVars($customvars)
+ {
+ $blacklist = [];
+ $blacklistPattern = '';
+
+ if (($blacklistConfig = Config::module('monitoring')->get('security', 'protected_customvars', '')) !== '') {
+ foreach (explode(',', $blacklistConfig) as $customvar) {
+ $nonWildcards = array();
+ foreach (explode('*', $customvar) as $nonWildcard) {
+ $nonWildcards[] = preg_quote($nonWildcard, '/');
+ }
+ $blacklist[] = implode('.*', $nonWildcards);
+ }
+ $blacklistPattern = '/^(' . implode('|', $blacklist) . ')$/i';
+ }
+
+ if (! $blacklistPattern) {
+ return $customvars;
+ }
+
+ $obfuscator = function ($vars) use ($blacklistPattern, &$obfuscator) {
+ $result = [];
+ foreach ($vars as $name => $value) {
+ if ($blacklistPattern && preg_match($blacklistPattern, $name)) {
+ $result[$name] = '***';
+ } elseif ($value instanceof stdClass || is_array($value)) {
+ $obfuscated = $obfuscator($value);
+ $result[$name] = $value instanceof stdClass ? (object) $obfuscated : $obfuscated;
+ } else {
+ $result[$name] = $value;
+ }
+ }
+
+ return $result;
+ };
+ $obfuscatedCustomVars = $obfuscator($customvars);
+
+ return $customvars instanceof stdClass ? (object) $obfuscatedCustomVars : $obfuscatedCustomVars;
+ }
+
+ /**
+ * Hide all blacklisted properties from the user as restricted by monitoring/blacklist/properties
+ *
+ * Currently this only affects the custom variables
+ */
+ protected function hideBlacklistedProperties()
+ {
+ if ($this->blacklistedProperties === null) {
+ $this->blacklistedProperties = new GlobFilter(
+ Auth::getInstance()->getRestrictions('monitoring/blacklist/properties')
+ );
+ }
+
+ $allProperties = $this->blacklistedProperties->removeMatching(
+ [$this->type => ['vars' => $this->customvars]]
+ );
+ $this->customvars = isset($allProperties[$this->type]['vars'])
+ ? $allProperties[$this->type]['vars']
+ : [];
+
+ $allProperties = $this->blacklistedProperties->removeMatching(
+ [$this->type => ['vars' => $this->customvarsWithOriginalNames]]
+ );
+ $this->customvarsWithOriginalNames = isset($allProperties[$this->type]['vars'])
+ ? $allProperties[$this->type]['vars']
+ : [];
+ }
+
+ /**
+ * Fetch the host custom variables related to this object
+ *
+ * @return $this
+ */
+ public function fetchHostVariables()
+ {
+ $query = $this->backend->select()->from('customvar', array(
+ 'varname',
+ 'varvalue',
+ 'is_json'
+ ))
+ ->whereEx(new FilterEqual('object_type', '=', static::TYPE_HOST))
+ ->whereEx(new FilterEqual('host_name', '=', $this->host_name));
+
+ $this->hostVariables = [];
+
+ if ($this->type === static::TYPE_HOST) {
+ $this->customvarsWithOriginalNames = [];
+ }
+
+ foreach ($query as $row) {
+ if ($row->is_json) {
+ $this->hostVariables[strtolower($row->varname)] = json_decode($row->varvalue);
+ } else {
+ $this->hostVariables[strtolower($row->varname)] = $row->varvalue;
+ }
+
+ if ($this->type === static::TYPE_HOST) {
+ $this->customvarsWithOriginalNames[$row->varname] = $this->hostVariables[strtolower($row->varname)];
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Fetch the service custom variables related to this object
+ *
+ * @return $this
+ *
+ * @throws ProgrammingError In case this object is not a service
+ */
+ public function fetchServiceVariables()
+ {
+ if ($this->type !== static::TYPE_SERVICE) {
+ throw new ProgrammingError('Cannot fetch service custom variables for non-service objects');
+ }
+
+ $query = $this->backend->select()->from('customvar', array(
+ 'varname',
+ 'varvalue',
+ 'is_json'
+ ))
+ ->whereEx(new FilterEqual('object_type', '=', static::TYPE_SERVICE))
+ ->whereEx(new FilterEqual('host_name', '=', $this->host_name))
+ ->whereEx(new FilterEqual('service_description', '=', $this->service_description));
+
+ $this->serviceVariables = [];
+ $this->customvarsWithOriginalNames = [];
+ foreach ($query as $row) {
+ if ($row->is_json) {
+ $this->customvarsWithOriginalNames[$row->varname] = json_decode($row->varvalue);
+ $this->serviceVariables[strtolower($row->varname)] = $this->customvarsWithOriginalNames[$row->varname];
+ } else {
+ $this->serviceVariables[strtolower($row->varname)] = $row->varvalue;
+ $this->customvarsWithOriginalNames[$row->varname] = $row->varvalue;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Fetch the object's downtimes
+ *
+ * @return $this
+ */
+ public function fetchDowntimes()
+ {
+ $downtimes = $this->backend->select()->from('downtime', array(
+ 'author_name' => 'downtime_author_name',
+ 'comment' => 'downtime_comment',
+ 'duration' => 'downtime_duration',
+ 'end' => 'downtime_end',
+ 'entry_time' => 'downtime_entry_time',
+ 'id' => 'downtime_internal_id',
+ 'is_fixed' => 'downtime_is_fixed',
+ 'is_flexible' => 'downtime_is_flexible',
+ 'is_in_effect' => 'downtime_is_in_effect',
+ 'name' => 'downtime_name',
+ 'objecttype' => 'object_type',
+ 'scheduled_end' => 'downtime_scheduled_end',
+ 'scheduled_start' => 'downtime_scheduled_start',
+ 'start' => 'downtime_start'
+ ))
+ ->whereEx(new FilterEqual('object_type', '=', $this->type))
+ ->order('downtime_is_in_effect', 'DESC')
+ ->order('downtime_scheduled_start', 'ASC');
+ if ($this->type === self::TYPE_SERVICE) {
+ $downtimes
+ ->whereEx(new FilterEqual('service_host_name', '=', $this->host_name))
+ ->whereEx(new FilterEqual('service_description', '=', $this->service_description));
+ } else {
+ $downtimes
+ ->whereEx(new FilterEqual('host_name', '=', $this->host_name));
+ }
+ $this->downtimes = $downtimes->getQuery()->fetchAll();
+ return $this;
+ }
+
+ /**
+ * Fetch the object's event history
+ *
+ * @return $this
+ */
+ public function fetchEventhistory()
+ {
+ $eventHistory = $this->backend
+ ->select()
+ ->from(
+ 'eventhistory',
+ array(
+ 'id',
+ 'object_type',
+ 'host_name',
+ 'host_display_name',
+ 'service_description',
+ 'service_display_name',
+ 'timestamp',
+ 'state',
+ 'output',
+ 'type'
+ )
+ )
+ ->whereEx(new FilterEqual('object_type', '=', $this->type))
+ ->whereEx(new FilterEqual('host_name', '=', $this->host_name));
+
+ if ($this->type === self::TYPE_SERVICE) {
+ $eventHistory->whereEx(
+ new FilterEqual('service_description', '=', $this->service_description)
+ );
+ }
+
+ $this->eventhistory = $eventHistory;
+ return $this;
+ }
+
+ /**
+ * Fetch the object's host groups
+ *
+ * @return $this
+ */
+ public function fetchHostgroups()
+ {
+ $this->hostgroups = $this->backend->select()
+ ->from('hostgroup', array('hostgroup_name', 'hostgroup_alias'))
+ ->whereEx(new FilterEqual('host_name', '=', $this->host_name))
+ ->applyFilter($this->getFilter())
+ ->fetchPairs();
+ return $this;
+ }
+
+ /**
+ * Fetch the object's service groups
+ *
+ * @return $this
+ */
+ public function fetchServicegroups()
+ {
+ $query = $this->backend->select()
+ ->from('servicegroup', array('servicegroup_name', 'servicegroup_alias'))
+ ->whereEx(new FilterEqual('host_name', '=', $this->host_name));
+
+ if ($this->type === self::TYPE_SERVICE) {
+ $query->whereEx(
+ new FilterEqual('service_description', '=', $this->service_description)
+ );
+ }
+
+ $this->servicegroups = $query->applyFilter($this->getFilter())->fetchPairs();
+ return $this;
+ }
+
+ /**
+ * Fetch stats
+ *
+ * @return $this
+ */
+ public function fetchStats()
+ {
+ $this->stats = $this->backend->select()->from('servicestatussummary', array(
+ 'services_total',
+ 'services_ok',
+ 'services_critical',
+ 'services_critical_unhandled',
+ 'services_critical_handled',
+ 'services_warning',
+ 'services_warning_unhandled',
+ 'services_warning_handled',
+ 'services_unknown',
+ 'services_unknown_unhandled',
+ 'services_unknown_handled',
+ 'services_pending',
+ ))
+ ->whereEx(new FilterEqual('service_host_name', '=', $this->host_name))
+ ->applyFilter($this->getFilter())
+ ->fetchRow();
+ return $this;
+ }
+
+ /**
+ * Get all action urls configured for this monitored object
+ *
+ * @return array All note urls as a string
+ */
+ public function getActionUrls()
+ {
+ return $this->resolveAllStrings(
+ MonitoredObject::parseAttributeUrls($this->action_url)
+ );
+ }
+
+ /**
+ * Get the type of the object
+ *
+ * @param bool $translate
+ *
+ * @return string
+ */
+ public function getType($translate = false)
+ {
+ if ($translate !== false) {
+ switch ($this->type) {
+ case self::TYPE_HOST:
+ $type = mt('montiroing', 'host');
+ break;
+ case self::TYPE_SERVICE:
+ $type = mt('monitoring', 'service');
+ break;
+ default:
+ throw new InvalidArgumentException('Invalid type ' . $this->type);
+ }
+ } else {
+ $type = $this->type;
+ }
+ return $type;
+ }
+
+ /**
+ * Parse the content of the action_url or notes_url attributes
+ *
+ * Find all occurences of http links, separated by whitespaces and quoted
+ * by single or double-ticks.
+ *
+ * @link http://docs.icinga.com/latest/de/objectdefinitions.html
+ *
+ * @param string $urlString A string containing one or more urls
+ * @return array Array of urls as strings
+ */
+ public static function parseAttributeUrls($urlString)
+ {
+ if (empty($urlString)) {
+ return array();
+ }
+ $links = array();
+ if (strpos($urlString, "' ") === false) {
+ $links[] = $urlString;
+ } else {
+ // parse notes-url format
+ foreach (explode("' ", $urlString) as $url) {
+ $url = strpos($url, "'") === 0 ? substr($url, 1) : $url;
+ $url = strrpos($url, "'") === strlen($url) - 1 ? substr($url, 0, strlen($url) - 1) : $url;
+ $links[] = $url;
+ }
+ }
+ return $links;
+ }
+
+ /**
+ * Fetch all available data of the object
+ *
+ * @return $this
+ */
+ public function populate()
+ {
+ $this
+ ->fetchComments()
+ ->fetchContactgroups()
+ ->fetchContacts()
+ ->fetchCustomvars()
+ ->fetchDowntimes();
+
+ // Call fetchHostgroups or fetchServicegroups depending on the object's type
+ $fetchGroups = 'fetch' . ucfirst($this->type) . 'groups';
+ $this->$fetchGroups();
+
+ return $this;
+ }
+
+ /**
+ * Resolve macros in all given strings in the current object context
+ *
+ * @param array $strs An array of urls as string
+ *
+ * @return array
+ */
+ protected function resolveAllStrings(array $strs)
+ {
+ foreach ($strs as $i => $str) {
+ $strs[$i] = Macro::resolveMacros($str, $this);
+ }
+ return $strs;
+ }
+
+ /**
+ * Set the object's properties
+ *
+ * @param object $properties
+ *
+ * @return $this
+ */
+ public function setProperties($properties)
+ {
+ $this->properties = (object) $properties;
+ return $this;
+ }
+
+ public function __isset($name)
+ {
+ if (property_exists($this->properties, $name)) {
+ return isset($this->properties->$name);
+ } elseif (property_exists($this, $name)) {
+ return isset($this->$name);
+ }
+ return false;
+ }
+
+ public function __get($name)
+ {
+ if (property_exists($this->properties, $name)) {
+ return $this->properties->$name;
+ } elseif (property_exists($this, $name)) {
+ if ($this->$name === null) {
+ $fetchMethod = 'fetch' . ucfirst($name);
+ $this->$fetchMethod();
+ }
+
+ return $this->$name;
+ } elseif (preg_match('/^_(host|service)_(.+)/i', $name, $matches)) {
+ if (strtolower($matches[1]) === static::TYPE_HOST) {
+ if ($this->hostVariables === null) {
+ $this->fetchHostVariables();
+ }
+
+ $customvars = $this->hostVariables;
+ } else {
+ if ($this->serviceVariables === null) {
+ $this->fetchServiceVariables();
+ }
+
+ $customvars = $this->serviceVariables;
+ }
+
+ $variableName = strtolower($matches[2]);
+ if (isset($customvars[$variableName])) {
+ return $customvars[$variableName];
+ }
+
+ return null; // Unknown custom variables MUST NOT throw an error
+ } elseif (in_array($name, array('contact_name', 'contactgroup_name', 'hostgroup_name', 'servicegroup_name'))) {
+ if ($name === 'contact_name') {
+ if ($this->contacts === null) {
+ $this->fetchContacts();
+ }
+
+ return array_map(function ($el) {
+ return $el->contact_name;
+ }, $this->contacts);
+ } elseif ($name === 'contactgroup_name') {
+ if ($this->contactgroups === null) {
+ $this->fetchContactgroups();
+ }
+
+ return array_map(function ($el) {
+ return $el->contactgroup_name;
+ }, $this->contactgroups);
+ } elseif ($name === 'hostgroup_name') {
+ if ($this->hostgroups === null) {
+ $this->fetchHostgroups();
+ }
+
+ return array_keys($this->hostgroups);
+ } else { // $name === 'servicegroup_name'
+ if ($this->servicegroups === null) {
+ $this->fetchServicegroups();
+ }
+
+ return array_keys($this->servicegroups);
+ }
+ } elseif (strpos($name, $this->prefix) !== 0) {
+ $propertyName = strtolower($name);
+ $prefixedName = $this->prefix . $propertyName;
+ if (property_exists($this->properties, $prefixedName)) {
+ return $this->properties->$prefixedName;
+ }
+
+ if ($this->type === static::TYPE_HOST) {
+ if ($this->hostVariables === null) {
+ $this->fetchHostVariables();
+ }
+
+ $customvars = $this->hostVariables;
+ } else { // $this->type === static::TYPE_SERVICE
+ if ($this->serviceVariables === null) {
+ $this->fetchServiceVariables();
+ }
+
+ $customvars = $this->serviceVariables;
+ }
+
+ if (isset($customvars[$propertyName])) {
+ return $customvars[$propertyName];
+ }
+ }
+
+ throw new InvalidPropertyException('Can\'t access property \'%s\'. Property does not exist.', $name);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Object/ObjectList.php b/modules/monitoring/library/Monitoring/Object/ObjectList.php
new file mode 100644
index 0000000..5237c56
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Object/ObjectList.php
@@ -0,0 +1,295 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Object;
+
+use ArrayIterator;
+use Countable;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filterable;
+use Icinga\Module\Monitoring\DataView\Downtime;
+use IteratorAggregate;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Traversable;
+
+abstract class ObjectList implements Countable, IteratorAggregate, Filterable
+{
+ /**
+ * @var string
+ */
+ protected $dataViewName;
+
+ /**
+ * @var MonitoringBackend
+ */
+ protected $backend;
+
+ /**
+ * @var array
+ */
+ protected $columns;
+
+ /**
+ * @var Filter
+ */
+ protected $filter;
+
+ /**
+ * @var array
+ */
+ protected $objects;
+
+ /**
+ * @var int
+ */
+ protected $count;
+
+ public function __construct(MonitoringBackend $backend)
+ {
+ $this->backend = $backend;
+ }
+
+ /**
+ * @param array $columns
+ *
+ * @return $this
+ */
+ public function setColumns(array $columns)
+ {
+ $this->columns = $columns;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getColumns()
+ {
+ return $this->columns;
+ }
+
+ /**
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function setFilter(Filter $filter)
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ /**
+ * @return Filter|FilterChain
+ */
+ public function getFilter()
+ {
+ if ($this->filter === null) {
+ $this->filter = Filter::matchAll();
+ }
+
+ return $this->filter;
+ }
+
+ public function applyFilter(Filter $filter)
+ {
+ $this->getFilter()->addFilter($filter);
+ return $this;
+ }
+
+ public function addFilter(Filter $filter)
+ {
+ $this->getFilter()->addFilter($filter);
+ }
+
+ public function where($condition, $value = null)
+ {
+ $this->getFilter()->addFilter(Filter::where($condition, $value));
+ }
+
+ abstract protected function fetchObjects();
+
+ /**
+ * @return array
+ */
+ public function fetch()
+ {
+ if ($this->objects === null) {
+ $this->objects = $this->fetchObjects();
+ }
+ return $this->objects;
+ }
+
+ public function count(): int
+ {
+ if ($this->count === null) {
+ $this->count = (int) $this->backend
+ ->select()
+ ->from($this->dataViewName, $this->columns)
+ ->applyFilter($this->filter)
+ ->getQuery()
+ ->count();
+ }
+
+ return $this->count;
+ }
+
+ public function getIterator(): Traversable
+ {
+ if ($this->objects === null) {
+ $this->fetch();
+ }
+ return new ArrayIterator($this->objects);
+ }
+
+ /**
+ * Get the comments
+ *
+ * @return \Icinga\Module\Monitoring\DataView\Comment
+ */
+ public function getComments()
+ {
+ return $this->backend->select()->from('comment')->applyFilter($this->filter);
+ }
+
+ /**
+ * Get the scheduled downtimes
+ *
+ * @return Downtime
+ */
+ public function getScheduledDowntimes()
+ {
+ return $this->backend->select()->from('downtime')->applyFilter($this->filter);
+ }
+
+ /**
+ * @return ObjectList
+ */
+ public function getAcknowledgedObjects()
+ {
+ $acknowledgedObjects = array();
+ foreach ($this as $object) {
+ if ((bool) $object->acknowledged === true) {
+ $acknowledgedObjects[] = $object;
+ }
+ }
+ return $this->newFromArray($acknowledgedObjects);
+ }
+
+ /**
+ * @return ObjectList
+ */
+ public function getObjectsInDowntime()
+ {
+ $objectsInDowntime = array();
+ foreach ($this as $object) {
+ if ((bool) $object->in_downtime === true) {
+ $objectsInDowntime[] = $object;
+ }
+ }
+ return $this->newFromArray($objectsInDowntime);
+ }
+
+ /**
+ * @return ObjectList
+ */
+ public function getUnhandledObjects()
+ {
+ $unhandledObjects = array();
+ foreach ($this as $object) {
+ if ((bool) $object->problem === true && (bool) $object->handled === false) {
+ $unhandledObjects[] = $object;
+ }
+ }
+ return $this->newFromArray($unhandledObjects);
+ }
+
+ /**
+ * @return ObjectList
+ */
+ public function getProblemObjects()
+ {
+ $handledObjects = array();
+ foreach ($this as $object) {
+ if ((bool) $object->problem === true) {
+ $handledObjects[] = $object;
+ }
+ }
+ return $this->newFromArray($handledObjects);
+ }
+
+ /**
+ * @return ObjectList
+ */
+ abstract public function getUnacknowledgedObjects();
+
+ /**
+ * Create a ObjectList from an array of hosts without querying a backend
+ *
+ * @return ObjectList
+ */
+ protected function newFromArray(array $objects)
+ {
+ $class = get_called_class();
+ $list = new $class($this->backend);
+ $list->objects = $objects;
+ $list->count = count($objects);
+ $list->filter = $list->objectsFilter();
+ return $list;
+ }
+
+ /**
+ * Create a filter that matches exactly the elements of this object list
+ *
+ * @param array $columns Override default column names.
+ *
+ * @return Filter
+ */
+ abstract public function objectsFilter($columns = array());
+
+ /**
+ * Get the feature status
+ *
+ * @return array
+ */
+ public function getFeatureStatus()
+ {
+ // null - init
+ // 0 - disabled
+ // 1 - enabled
+ // 2 - enabled & disabled
+ $featureStatus = array(
+ 'active_checks_enabled' => null,
+ 'passive_checks_enabled' => null,
+ 'obsessing' => null,
+ 'notifications_enabled' => null,
+ 'event_handler_enabled' => null,
+ 'flap_detection_enabled' => null
+ );
+
+ $features = array();
+
+ foreach ($featureStatus as $feature => &$status) {
+ $features[$feature] = &$status;
+ }
+
+ foreach ($this as $object) {
+ foreach ($features as $feature => &$status) {
+ $enabled = (int) $object->{$feature};
+ if (! isset($status)) {
+ $status = $enabled;
+ } elseif ($status !== $enabled) {
+ unset($features[$feature]);
+ if (empty($features)) {
+ break 2;
+ }
+
+ break;
+ }
+ }
+ }
+
+ return $featureStatus;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Object/Service.php b/modules/monitoring/library/Monitoring/Object/Service.php
new file mode 100644
index 0000000..a63db6f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Object/Service.php
@@ -0,0 +1,220 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Object;
+
+use Icinga\Data\Filter\FilterEqual;
+use Icinga\Module\Monitoring\DataView\Servicestatus;
+use InvalidArgumentException;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+
+/**
+ * An Icinga service
+ */
+class Service extends MonitoredObject
+{
+ /**
+ * Service state 'OK'
+ */
+ const STATE_OK = 0;
+
+ /**
+ * Service state 'WARNING'
+ */
+ const STATE_WARNING = 1;
+
+ /**
+ * Service state 'CRITICAL'
+ */
+ const STATE_CRITICAL = 2;
+
+ /**
+ * Service state 'UNKNOWN'
+ */
+ const STATE_UNKNOWN = 3;
+
+ /**
+ * Service state 'PENDING'
+ */
+ const STATE_PENDING = 99;
+
+ /**
+ * Type of the Icinga service
+ *
+ * @var string
+ */
+ public $type = self::TYPE_SERVICE;
+
+ /**
+ * Prefix of the Icinga service
+ *
+ * @var string
+ */
+ public $prefix = 'service_';
+
+ /**
+ * Host the service is running on
+ *
+ * @var Host
+ */
+ protected $host;
+
+ /**
+ * Service name
+ *
+ * @var string
+ */
+ protected $service;
+
+ /**
+ * Create a new service
+ *
+ * @param MonitoringBackend $backend Backend to fetch service information from
+ * @param string $host Hostname the service is running on
+ * @param string $service Service name
+ */
+ public function __construct(MonitoringBackend $backend, $host, $service)
+ {
+ parent::__construct($backend);
+ $this->host = new Host($backend, $host);
+ $this->service = $service;
+ }
+
+ /**
+ * Get the host the service is running on
+ *
+ * @return Host
+ */
+ public function getHost()
+ {
+ return $this->host;
+ }
+
+ /**
+ * Get the service name
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->service;
+ }
+
+ /**
+ * Get the data view
+ *
+ * @return Servicestatus
+ */
+ protected function getDataView()
+ {
+ return $this->backend->select()->from('servicestatus', array(
+ 'instance_name',
+ 'host_attempt',
+ 'host_icon_image',
+ 'host_icon_image_alt',
+ 'host_acknowledged',
+ 'host_active_checks_enabled',
+ 'host_address',
+ 'host_address6',
+ 'host_alias',
+ 'host_display_name',
+ 'host_handled',
+ 'host_in_downtime',
+ 'host_is_flapping',
+ 'host_last_state_change',
+ 'host_name',
+ 'host_notifications_enabled',
+ 'host_passive_checks_enabled',
+ 'host_state',
+ 'host_state_type',
+ 'service_icon_image',
+ 'service_icon_image_alt',
+ 'service_acknowledged',
+ 'service_acknowledgement_type',
+ 'service_action_url',
+ 'service_active_checks_enabled',
+ 'service_active_checks_enabled_changed',
+ 'service_attempt',
+ 'service_check_command',
+ 'service_check_execution_time',
+ 'service_check_interval',
+ 'service_check_latency',
+ 'service_check_source',
+ 'service_check_timeperiod',
+ 'service_current_notification_number',
+ 'service_description',
+ 'service_display_name',
+ 'service_event_handler_enabled',
+ 'service_event_handler_enabled_changed',
+ 'service_flap_detection_enabled',
+ 'service_flap_detection_enabled_changed',
+ 'service_handled',
+ 'service_in_downtime',
+ 'service_is_flapping',
+ 'service_is_reachable',
+ 'service_last_check',
+ 'service_last_notification',
+ 'service_last_state_change',
+ 'service_long_output',
+ 'service_next_check',
+ 'service_next_update',
+ 'service_notes',
+ 'service_notes_url',
+ 'service_notifications_enabled',
+ 'service_notifications_enabled_changed',
+ 'service_obsessing',
+ 'service_obsessing_changed',
+ 'service_output',
+ 'service_passive_checks_enabled',
+ 'service_passive_checks_enabled_changed',
+ 'service_percent_state_change',
+ 'service_perfdata',
+ 'service_process_perfdata' => 'service_process_performance_data',
+ 'service_state',
+ 'service_state_type'
+ ))
+ ->whereEx(new FilterEqual('host_name', '=', $this->host->getName()))
+ ->whereEx(new FilterEqual('service_description', '=', $this->service));
+ }
+
+ /**
+ * Get the optional translated textual representation of a service state
+ *
+ * @param int $state
+ * @param bool $translate
+ *
+ * @return string
+ * @throws InvalidArgumentException If the service state is not valid
+ */
+ public static function getStateText($state, $translate = false)
+ {
+ $translate = (bool) $translate;
+ switch ((int) $state) {
+ case self::STATE_OK:
+ $text = $translate ? mt('monitoring', 'OK') : 'ok';
+ break;
+ case self::STATE_WARNING:
+ $text = $translate ? mt('monitoring', 'WARNING') : 'warning';
+ break;
+ case self::STATE_CRITICAL:
+ $text = $translate ? mt('monitoring', 'CRITICAL') : 'critical';
+ break;
+ case self::STATE_UNKNOWN:
+ $text = $translate ? mt('monitoring', 'UNKNOWN') : 'unknown';
+ break;
+ case self::STATE_PENDING:
+ $text = $translate ? mt('monitoring', 'PENDING') : 'pending';
+ break;
+ default:
+ throw new InvalidArgumentException(sprintf('Invalid service state \'%s\'', $state));
+ }
+ return $text;
+ }
+
+ public function getNotesUrls()
+ {
+ return $this->resolveAllStrings(
+ MonitoredObject::parseAttributeUrls($this->service_notes_url)
+ );
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Object/ServiceList.php b/modules/monitoring/library/Monitoring/Object/ServiceList.php
new file mode 100644
index 0000000..5bc0bdb
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Object/ServiceList.php
@@ -0,0 +1,184 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Object;
+
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterOr;
+use Icinga\Data\SimpleQuery;
+use Icinga\Util\StringHelper;
+
+/**
+ * A service list
+ */
+class ServiceList extends ObjectList
+{
+ protected $hostStateSummary;
+
+ protected $serviceStateSummary;
+
+ protected $dataViewName = 'servicestatus';
+
+ protected $columns = array('host_name', 'service_description');
+
+ protected function fetchObjects()
+ {
+ $services = array();
+ $query = $this->backend->select()->from($this->dataViewName, $this->columns)->applyFilter($this->filter)
+ ->getQuery()->getSelectQuery()->query();
+ foreach ($query as $row) {
+ /** @var object $row */
+ $service = new Service($this->backend, $row->host_name, $row->service_description);
+ $service->setProperties($row);
+ $services[] = $service;
+ }
+ return $services;
+ }
+
+ /**
+ * Create a state summary of all services that can be consumed by servicesummary.phtml
+ *
+ * @return SimpleQuery
+ */
+ public function getServiceStateSummary()
+ {
+ if (! $this->serviceStateSummary) {
+ $this->initStateSummaries();
+ }
+
+ $ds = new ArrayDatasource(array((object) $this->serviceStateSummary));
+ return $ds->select();
+ }
+
+ /**
+ * Create a state summary of all hosts that can be consumed by hostsummary.phtml
+ *
+ * @return SimpleQuery
+ */
+ public function getHostStateSummary()
+ {
+ if (! $this->hostStateSummary) {
+ $this->initStateSummaries();
+ }
+
+ $ds = new ArrayDatasource(array((object) $this->hostStateSummary));
+ return $ds->select();
+ }
+
+ /**
+ * Calculate the current state summary and populate hostStateSummary and serviceStateSummary
+ * properties
+ */
+ protected function initStateSummaries()
+ {
+ $serviceStates = array_fill_keys(self::getServiceStatesSummaryEmpty(), 0);
+ $hostStates = array_fill_keys(HostList::getHostStatesSummaryEmpty(), 0);
+
+ foreach ($this as $service) {
+ $unhandled = false;
+ if ((bool) $service->problem === true && (bool) $service->handled === false) {
+ $unhandled = true;
+ }
+
+ $stateName = 'services_' . $service::getStateText($service->state);
+ ++$serviceStates[$stateName];
+ ++$serviceStates[$stateName . ($unhandled ? '_unhandled' : '_handled')];
+
+ if (! isset($knownHostStates[$service->getHost()->getName()])) {
+ $unhandledHost = (bool) $service->host_problem === true && (bool) $service->host_handled === false;
+ ++$hostStates['hosts_' . $service->getHost()->getStateText($service->host_state)];
+ ++$hostStates['hosts_' . $service->getHost()->getStateText($service->host_state)
+ . ($unhandledHost ? '_unhandled' : '_handled')];
+ $knownHostStates[$service->getHost()->getName()] = true;
+ }
+ }
+
+ $serviceStates['services_total'] = count($this);
+ $this->hostStateSummary = $hostStates;
+ $this->serviceStateSummary = $serviceStates;
+ }
+
+ /**
+ * Return an empty array with all possible host state names
+ *
+ * @return array An array containing all possible host states as keys and 0 as values.
+ */
+ public static function getServiceStatesSummaryEmpty()
+ {
+ return StringHelper::cartesianProduct(
+ array(
+ array('services'),
+ array(
+ Service::getStateText(Service::STATE_OK),
+ Service::getStateText(Service::STATE_WARNING),
+ Service::getStateText(Service::STATE_CRITICAL),
+ Service::getStateText(Service::STATE_UNKNOWN),
+ Service::getStateText(Service::STATE_PENDING)
+ ),
+ array(null, 'handled', 'unhandled')
+ ),
+ '_'
+ );
+ }
+
+ /**
+ * Returns a Filter that matches all hosts in this HostList
+ *
+ * @param array $columns Override filter column names
+ *
+ * @return Filter
+ */
+ public function objectsFilter($columns = array('host' => 'host', 'service' => 'service'))
+ {
+ $filterExpression = array();
+ foreach ($this as $service) {
+ $filterExpression[] = Filter::matchAll(
+ Filter::where($columns['host'], $service->getHost()->getName()),
+ Filter::where($columns['service'], $service->getName())
+ );
+ }
+ return FilterOr::matchAny($filterExpression);
+ }
+
+ /**
+ * Get the comments
+ *
+ * @return \Icinga\Module\Monitoring\DataView\Hostcomment
+ */
+ public function getComments()
+ {
+ return $this->backend
+ ->select()
+ ->from('servicecomment', array('host_name', 'service_description'))
+ ->applyFilter(clone $this->filter);
+ }
+
+ /**
+ * Get the scheduled downtimes
+ *
+ * @return \Icinga\Module\Monitoring\DataView\Servicedowntime
+ */
+ public function getScheduledDowntimes()
+ {
+ return $this->backend
+ ->select()
+ ->from('servicedowntime', array('host_name', 'service_description'))
+ ->applyFilter(clone $this->filter);
+ }
+
+ /**
+ * @return ObjectList
+ */
+ public function getUnacknowledgedObjects()
+ {
+ $unhandledObjects = array();
+ foreach ($this as $object) {
+ if (! in_array((int) $object->state, array(0, 99)) &&
+ (bool) $object->service_acknowledged === false) {
+ $unhandledObjects[] = $object;
+ }
+ }
+ return $this->newFromArray($unhandledObjects);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Plugin/Perfdata.php b/modules/monitoring/library/Monitoring/Plugin/Perfdata.php
new file mode 100644
index 0000000..b98ffcc
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Plugin/Perfdata.php
@@ -0,0 +1,550 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Plugin;
+
+use Icinga\Util\Format;
+use InvalidArgumentException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Web\Widget\Chart\InlinePie;
+use Icinga\Module\Monitoring\Object\Service;
+use Zend_Controller_Front;
+
+class Perfdata
+{
+ const PERFDATA_OK = 'ok';
+ const PERFDATA_WARNING = 'warning';
+ const PERFDATA_CRITICAL = 'critical';
+
+ /**
+ * The performance data value being parsed
+ *
+ * @var string
+ */
+ protected $perfdataValue;
+
+ /**
+ * Unit of measurement (UOM)
+ *
+ * @var string
+ */
+ protected $unit;
+
+ /**
+ * The label
+ *
+ * @var string
+ */
+ protected $label;
+
+ /**
+ * The value
+ *
+ * @var float
+ */
+ protected $value;
+
+ /**
+ * The minimum value
+ *
+ * @var float
+ */
+ protected $minValue;
+
+ /**
+ * The maximum value
+ *
+ * @var float
+ */
+ protected $maxValue;
+
+ /**
+ * The WARNING threshold
+ *
+ * @var ThresholdRange
+ */
+ protected $warningThreshold;
+
+ /**
+ * The CRITICAL threshold
+ *
+ * @var ThresholdRange
+ */
+ protected $criticalThreshold;
+
+ /**
+ * Create a new Perfdata object based on the given performance data label and value
+ *
+ * @param string $label The perfdata label
+ * @param string $value The perfdata value
+ */
+ public function __construct($label, $value)
+ {
+ $this->perfdataValue = $value;
+ $this->label = $label;
+ $this->parse();
+
+ if ($this->unit === '%') {
+ if ($this->minValue === null) {
+ $this->minValue = 0.0;
+ }
+ if ($this->maxValue === null) {
+ $this->maxValue = 100.0;
+ }
+ }
+
+ $warn = $this->warningThreshold->getMax();
+ if ($warn !== null) {
+ $crit = $this->criticalThreshold->getMax();
+ if ($crit !== null && $warn > $crit) {
+ $this->warningThreshold->setInverted();
+ $this->criticalThreshold->setInverted();
+ }
+ }
+ }
+
+ /**
+ * Return a new Perfdata object based on the given performance data key=value pair
+ *
+ * @param string $perfdata The key=value pair to parse
+ *
+ * @return Perfdata
+ *
+ * @throws InvalidArgumentException In case the given performance data has no content or a invalid format
+ */
+ public static function fromString($perfdata)
+ {
+ if (empty($perfdata)) {
+ throw new InvalidArgumentException('Perfdata::fromString expects a string with content');
+ } elseif (strpos($perfdata, '=') === false) {
+ throw new InvalidArgumentException(
+ 'Perfdata::fromString expects a key=value formatted string. Got "' . $perfdata . '" instead'
+ );
+ }
+
+ list($label, $value) = explode('=', $perfdata, 2);
+ return new static(trim($label), trim($value));
+ }
+
+ /**
+ * Return whether this performance data's value is a number
+ *
+ * @return bool True in case it's a number, otherwise False
+ */
+ public function isNumber()
+ {
+ return $this->unit === null;
+ }
+
+ /**
+ * Return whether this performance data's value are seconds
+ *
+ * @return bool True in case it's seconds, otherwise False
+ */
+ public function isSeconds()
+ {
+ return in_array($this->unit, array('s', 'ms', 'us'));
+ }
+
+ /**
+ * Return whether this performance data's value is a temperature
+ *
+ * @return bool True in case it's temperature, otherwise False
+ */
+ public function isTemperature()
+ {
+ return in_array($this->unit, array('°c', '°f'));
+ }
+
+ /**
+ * Return whether this performance data's value is in percentage
+ *
+ * @return bool True in case it's in percentage, otherwise False
+ */
+ public function isPercentage()
+ {
+ return $this->unit === '%';
+ }
+
+ /**
+ * Return whether this performance data's value is in bytes
+ *
+ * @return bool True in case it's in bytes, otherwise False
+ */
+ public function isBytes()
+ {
+ return in_array($this->unit, array('b', 'kb', 'mb', 'gb', 'tb'));
+ }
+
+ /**
+ * Return whether this performance data's value is a counter
+ *
+ * @return bool True in case it's a counter, otherwise False
+ */
+ public function isCounter()
+ {
+ return $this->unit === 'c';
+ }
+
+ /**
+ * Returns whether it is possible to display a visual representation
+ *
+ * @return bool True when the perfdata is visualizable
+ */
+ public function isVisualizable()
+ {
+ return isset($this->minValue) && isset($this->maxValue) && isset($this->value);
+ }
+
+ /**
+ * Return this perfomance data's label
+ */
+ public function getLabel()
+ {
+ return $this->label;
+ }
+
+ /**
+ * Return the value or null if it is unknown (U)
+ *
+ * @return null|float
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Return the unit as a string
+ *
+ * @return string
+ */
+ public function getUnit()
+ {
+ return $this->unit;
+ }
+
+ /**
+ * Return the value as percentage (0-100)
+ *
+ * @return null|float
+ */
+ public function getPercentage()
+ {
+ if ($this->isPercentage()) {
+ return $this->value;
+ }
+
+ if ($this->maxValue !== null) {
+ $minValue = $this->minValue !== null ? $this->minValue : 0.0;
+ if ($this->maxValue == $minValue) {
+ return null;
+ }
+
+ if ($this->value > $minValue) {
+ return (($this->value - $minValue) / ($this->maxValue - $minValue)) * 100;
+ }
+ }
+ }
+
+ /**
+ * Return this performance data's warning treshold
+ *
+ * @return ThresholdRange
+ */
+ public function getWarningThreshold()
+ {
+ return $this->warningThreshold;
+ }
+
+ /**
+ * Return this performance data's critical treshold
+ *
+ * @return ThresholdRange
+ */
+ public function getCriticalThreshold()
+ {
+ return $this->criticalThreshold;
+ }
+
+ /**
+ * Return the minimum value or null if it is not available
+ *
+ * @return null|string
+ */
+ public function getMinimumValue()
+ {
+ return $this->minValue;
+ }
+
+ /**
+ * Return the maximum value or null if it is not available
+ *
+ * @return null|float
+ */
+ public function getMaximumValue()
+ {
+ return $this->maxValue;
+ }
+
+ /**
+ * Return this performance data as string
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->formatLabel();
+ }
+
+ /**
+ * Parse the current performance data value
+ *
+ * @todo Handle optional min/max if UOM == %
+ */
+ protected function parse()
+ {
+ $parts = explode(';', $this->perfdataValue);
+
+ $matches = array();
+ if (preg_match('@^(-?(?:\d+)?(?:\.\d+)?)([a-zA-Z%°]{1,3})$@u', $parts[0], $matches)) {
+ $this->unit = strtolower($matches[2]);
+ $this->value = self::convert($matches[1], $this->unit);
+ } else {
+ $this->value = self::convert($parts[0]);
+ }
+
+ switch (count($parts)) {
+ /* @noinspection PhpMissingBreakStatementInspection */
+ case 5:
+ if ($parts[4] !== '') {
+ $this->maxValue = self::convert($parts[4], $this->unit);
+ }
+ /* @noinspection PhpMissingBreakStatementInspection */
+ case 4:
+ if ($parts[3] !== '') {
+ $this->minValue = self::convert($parts[3], $this->unit);
+ }
+ /* @noinspection PhpMissingBreakStatementInspection */
+ case 3:
+ $this->criticalThreshold = self::convert(
+ ThresholdRange::fromString(trim($parts[2])),
+ $this->unit
+ );
+ // Fallthrough
+ case 2:
+ $this->warningThreshold = self::convert(
+ ThresholdRange::fromString(trim($parts[1])),
+ $this->unit
+ );
+ }
+
+ if ($this->warningThreshold === null) {
+ $this->warningThreshold = new ThresholdRange();
+ }
+ if ($this->criticalThreshold === null) {
+ $this->criticalThreshold = new ThresholdRange();
+ }
+ }
+
+ /**
+ * Return the given value converted to its smallest supported representation
+ *
+ * @param string $value The value to convert
+ * @param string $fromUnit The unit the value currently represents
+ *
+ * @return ThresholdRange|null|float Null in case the value is not a number
+ */
+ protected static function convert($value, $fromUnit = null)
+ {
+ if ($value instanceof ThresholdRange) {
+ $value = clone $value;
+
+ $min = $value->getMin();
+ if ($min !== null) {
+ $value->setMin(self::convert($min, $fromUnit));
+ }
+
+ $max = $value->getMax();
+ if ($max !== null) {
+ $value->setMax(self::convert($max, $fromUnit));
+ }
+
+ return $value;
+ }
+
+ if (is_numeric($value)) {
+ switch ($fromUnit) {
+ case 'us':
+ return $value / pow(10, 6);
+ case 'ms':
+ return $value / pow(10, 3);
+ case 'tb':
+ return floatval($value) * pow(2, 40);
+ case 'gb':
+ return floatval($value) * pow(2, 30);
+ case 'mb':
+ return floatval($value) * pow(2, 20);
+ case 'kb':
+ return floatval($value) * pow(2, 10);
+ default:
+ return (float) $value;
+ }
+ }
+ }
+
+ protected function calculatePieChartData()
+ {
+ $rawValue = $this->getValue();
+ $minValue = $this->getMinimumValue() !== null ? $this->getMinimumValue() : 0;
+ $usedValue = ($rawValue - $minValue);
+
+ $green = $orange = $red = 0;
+
+ if ($this->criticalThreshold->contains($rawValue)) {
+ if ($this->warningThreshold->contains($rawValue)) {
+ $green = $usedValue;
+ } else {
+ $orange = $usedValue;
+ }
+ } else {
+ $red = $usedValue;
+ }
+
+ return array($green, $orange, $red, ($this->getMaximumValue() - $minValue) - $usedValue);
+ }
+
+
+ public function asInlinePie()
+ {
+ if (! $this->isVisualizable()) {
+ throw new ProgrammingError('Cannot calculate piechart data for unvisualizable perfdata entry.');
+ }
+
+ $data = $this->calculatePieChartData();
+ $pieChart = new InlinePie($data, $this);
+ $pieChart->setColors(array('#44bb77', '#ffaa44', '#ff5566', '#ddccdd'));
+
+ return $pieChart;
+ }
+
+ /**
+ * Format the given value depending on the currently used unit
+ */
+ protected function format($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ if ($value instanceof ThresholdRange) {
+ if ($value->getMin()) {
+ return (string) $value;
+ }
+
+ $max = $value->getMax();
+ return $max === null ? '' : $this->format($max);
+ }
+
+ if ($this->isPercentage()) {
+ return (string)$value . '%';
+ }
+ if ($this->isBytes()) {
+ return Format::bytes($value);
+ }
+ if ($this->isSeconds()) {
+ return Format::seconds($value);
+ }
+ if ($this->isTemperature()) {
+ return (string)$value . strtoupper($this->unit);
+ }
+ return number_format($value, 2) . ($this->unit !== null ? ' ' . $this->unit : '');
+ }
+
+ /**
+ * Format the title string that represents this perfdata set
+ *
+ * @param bool $html
+ *
+ * @return string
+ */
+ public function formatLabel($html = false)
+ {
+ return sprintf(
+ $html ? '<b>%s %s</b> (%s%%)' : '%s %s (%s%%)',
+ htmlspecialchars($this->getLabel()),
+ $this->format($this->value),
+ number_format($this->getPercentage() ?? 0, 2)
+ );
+ }
+
+ public function toArray()
+ {
+ return array(
+ 'label' => $this->getLabel(),
+ 'value' => $this->format($this->getValue()),
+ 'min' => isset($this->minValue) && !$this->isPercentage()
+ ? $this->format($this->minValue)
+ : '',
+ 'max' => isset($this->maxValue) && !$this->isPercentage()
+ ? $this->format($this->maxValue)
+ : '',
+ 'warn' => $this->format($this->warningThreshold),
+ 'crit' => $this->format($this->criticalThreshold)
+ );
+ }
+
+ /**
+ * Return the state indicated by this perfdata
+ *
+ * @see Service
+ *
+ * @return int
+ */
+ public function getState()
+ {
+ if ($this->value === null) {
+ return Service::STATE_UNKNOWN;
+ }
+
+ if (! $this->criticalThreshold->contains($this->value)) {
+ return Service::STATE_CRITICAL;
+ }
+
+ if (! $this->warningThreshold->contains($this->value)) {
+ return Service::STATE_WARNING;
+ }
+
+ return Service::STATE_OK;
+ }
+
+ /**
+ * Return whether the state indicated by this perfdata is worse than
+ * the state indicated by the other perfdata
+ * CRITICAL > UNKNOWN > WARNING > OK
+ *
+ * @param Perfdata $rhs the other perfdata
+ *
+ * @return bool
+ */
+ public function worseThan(Perfdata $rhs)
+ {
+ if (($state = $this->getState()) === ($rhsState = $rhs->getState())) {
+ return $this->getPercentage() > $rhs->getPercentage();
+ }
+
+ if ($state === Service::STATE_CRITICAL) {
+ return true;
+ }
+
+ if ($state === Service::STATE_UNKNOWN) {
+ return $rhsState !== Service::STATE_CRITICAL;
+ }
+
+ if ($state === Service::STATE_WARNING) {
+ return $rhsState === Service::STATE_OK;
+ }
+
+ return false;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Plugin/PerfdataSet.php b/modules/monitoring/library/Monitoring/Plugin/PerfdataSet.php
new file mode 100644
index 0000000..ef1ca0c
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Plugin/PerfdataSet.php
@@ -0,0 +1,144 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Plugin;
+
+use ArrayIterator;
+use IteratorAggregate;
+use Traversable;
+
+class PerfdataSet implements IteratorAggregate
+{
+ /**
+ * The performance data being parsed
+ *
+ * @var string
+ */
+ protected $perfdataStr;
+
+ /**
+ * The current parsing position
+ *
+ * @var int
+ */
+ protected $parserPos = 0;
+
+ /**
+ * A list of Perfdata objects
+ *
+ * @var array
+ */
+ protected $perfdata = array();
+
+ /**
+ * Create a new set of performance data
+ *
+ * @param string $perfdataStr A space separated list of label/value pairs
+ */
+ protected function __construct($perfdataStr)
+ {
+ if ($perfdataStr && ($perfdataStr = trim($perfdataStr))) {
+ $this->perfdataStr = $perfdataStr;
+ $this->parse();
+ }
+ }
+
+ /**
+ * Return a iterator for this set of performance data
+ *
+ * @return ArrayIterator
+ */
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->asArray());
+ }
+
+ /**
+ * Return a new set of performance data
+ *
+ * @param string $perfdataStr A space separated list of label/value pairs
+ *
+ * @return PerfdataSet
+ */
+ public static function fromString($perfdataStr)
+ {
+ return new static($perfdataStr);
+ }
+
+ /**
+ * Return this set of performance data as array
+ *
+ * @return array
+ */
+ public function asArray()
+ {
+ return $this->perfdata;
+ }
+
+ /**
+ * Parse the current performance data
+ */
+ protected function parse()
+ {
+ while ($this->parserPos < strlen($this->perfdataStr)) {
+ $label = trim($this->readLabel());
+ $value = trim($this->readUntil(' '));
+
+ if ($label) {
+ $this->perfdata[] = new Perfdata($label, $value);
+ }
+ }
+ }
+
+ /**
+ * Return the next label found in the performance data
+ *
+ * @return string The label found
+ */
+ protected function readLabel()
+ {
+ $this->skipSpaces();
+ if (in_array($this->perfdataStr[$this->parserPos], array('"', "'"))) {
+ $quoteChar = $this->perfdataStr[$this->parserPos++];
+ $label = $this->readUntil('=');
+ $this->parserPos++;
+
+ if (($closingPos = strpos($label, $quoteChar)) > 0) {
+ $label = substr($label, 0, $closingPos);
+ }
+ } else {
+ $label = $this->readUntil('=');
+ $this->parserPos++;
+ }
+
+ $this->skipSpaces();
+ return $label;
+ }
+
+ /**
+ * Return all characters between the current parser position and the given character
+ *
+ * @param string $stopChar The character on which to stop
+ *
+ * @return string
+ */
+ protected function readUntil($stopChar)
+ {
+ $start = $this->parserPos;
+ while ($this->parserPos < strlen($this->perfdataStr) && $this->perfdataStr[$this->parserPos] !== $stopChar) {
+ $this->parserPos++;
+ }
+
+ return substr($this->perfdataStr, $start, $this->parserPos - $start);
+ }
+
+ /**
+ * Advance the parser position to the next non-whitespace character
+ */
+ protected function skipSpaces()
+ {
+ while ($this->parserPos < strlen($this->perfdataStr) && $this->perfdataStr[$this->parserPos] === ' ') {
+ $this->parserPos++;
+ }
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Plugin/ThresholdRange.php b/modules/monitoring/library/Monitoring/Plugin/ThresholdRange.php
new file mode 100644
index 0000000..bd27b8b
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Plugin/ThresholdRange.php
@@ -0,0 +1,179 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Plugin;
+
+/**
+ * The warning/critical threshold of a measured value
+ */
+class ThresholdRange
+{
+ /**
+ * The smallest value inside the range (null stands for -∞)
+ *
+ * @var float|null
+ */
+ protected $min;
+
+ /**
+ * The biggest value inside the range (null stands for ∞)
+ *
+ * @var float|null
+ */
+ protected $max;
+
+ /**
+ * Whether to invert the result of contains()
+ *
+ * @var bool
+ */
+ protected $inverted = false;
+
+ /**
+ * The unmodified range as passed to fromString()
+ *
+ * @var string
+ */
+ protected $raw;
+
+ /**
+ * Create a new instance based on a threshold range conforming to <https://nagios-plugins.org/doc/guidelines.html>
+ *
+ * @param string $rawRange
+ *
+ * @return ThresholdRange
+ */
+ public static function fromString($rawRange)
+ {
+ $range = new static();
+ $range->raw = $rawRange;
+
+ if ($rawRange == '') {
+ return $range;
+ }
+
+ $rawRange = ltrim($rawRange);
+ if (substr($rawRange, 0, 1) === '@') {
+ $range->setInverted();
+ $rawRange = substr($rawRange, 1);
+ }
+
+ if (strpos($rawRange, ':') === false) {
+ $min = 0.0;
+ $max = floatval(trim($rawRange));
+ } else {
+ list($min, $max) = explode(':', $rawRange, 2);
+ $min = trim($min);
+ $max = trim($max);
+
+ switch ($min) {
+ case '':
+ $min = 0.0;
+ break;
+ case '~':
+ $min = null;
+ break;
+ default:
+ $min = floatval($min);
+ }
+
+ $max = empty($max) ? null : floatval($max);
+ }
+
+ return $range->setMin($min)
+ ->setMax($max);
+ }
+
+ /**
+ * Set the smallest value inside the range (null stands for -∞)
+ *
+ * @param float|null $min
+ *
+ * @return $this
+ */
+ public function setMin($min)
+ {
+ $this->min = $min;
+ return $this;
+ }
+
+ /**
+ * Get the smallest value inside the range (null stands for -∞)
+ *
+ * @return float|null
+ */
+ public function getMin()
+ {
+ return $this->min;
+ }
+
+ /**
+ * Set the biggest value inside the range (null stands for ∞)
+ *
+ * @param float|null $max
+ *
+ * @return $this
+ */
+ public function setMax($max)
+ {
+ $this->max = $max;
+ return $this;
+ }
+
+ /**
+ * Get the biggest value inside the range (null stands for ∞)
+ *
+ * @return float|null
+ */
+ public function getMax()
+ {
+ return $this->max;
+ }
+
+ /**
+ * Set whether to invert the result of contains()
+ *
+ * @param bool $inverted
+ *
+ * @return $this
+ */
+ public function setInverted($inverted = true)
+ {
+ $this->inverted = $inverted;
+ return $this;
+ }
+
+ /**
+ * Get whether to invert the result of contains()
+ *
+ * @return bool
+ */
+ public function isInverted()
+ {
+ return $this->inverted;
+ }
+
+ /**
+ * Return whether $value is inside $this
+ *
+ * @param float $value
+ *
+ * @return bool
+ */
+ public function contains($value)
+ {
+ return (bool) ($this->inverted ^ (
+ ($this->min === null || $this->min <= $value) && ($this->max === null || $this->max >= $value)
+ ));
+ }
+
+ /**
+ * Return the textual representation of $this, suitable for fromString()
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return (string) $this->raw;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/ProvidedHook/ApplicationState.php b/modules/monitoring/library/Monitoring/ProvidedHook/ApplicationState.php
new file mode 100644
index 0000000..4e2e61c
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/ProvidedHook/ApplicationState.php
@@ -0,0 +1,32 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\ProvidedHook;
+
+use Icinga\Application\Hook\ApplicationStateHook;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+
+class ApplicationState extends ApplicationStateHook
+{
+ public function collectMessages()
+ {
+ $backend = MonitoringBackend::instance();
+
+ $programStatus = $backend
+ ->select()
+ ->from(
+ 'programstatus',
+ ['is_currently_running', 'status_update_time']
+ )
+ ->fetchRow();
+
+ if ($programStatus === false || ! (bool) $programStatus->is_currently_running) {
+ $message = sprintf(
+ mt('monitoring', "Monitoring backend '%s' is not running."),
+ $backend->getName()
+ );
+
+ $this->addError('monitoring/backend-down', $programStatus->status_update_time, $message);
+ }
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/ProvidedHook/Health.php b/modules/monitoring/library/Monitoring/ProvidedHook/Health.php
new file mode 100644
index 0000000..8f9c893
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/ProvidedHook/Health.php
@@ -0,0 +1,102 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\ProvidedHook;
+
+use Icinga\Application\Hook\HealthHook;
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use ipl\Web\Url;
+
+class Health extends HealthHook
+{
+ /** @var object */
+ protected $programStatus;
+
+ public function getName()
+ {
+ return 'Icinga';
+ }
+
+ public function getUrl()
+ {
+ return Url::fromPath('monitoring/health/info');
+ }
+
+ public function checkHealth()
+ {
+ $backendName = MonitoringBackend::instance()->getName();
+ $programStatus = $this->getProgramStatus();
+ if ($programStatus === false) {
+ $this->setState(self::STATE_UNKNOWN);
+ $this->setMessage(sprintf(t('%s is currently not up and running'), $backendName));
+ return;
+ }
+
+ if ($programStatus->is_currently_running) {
+ $this->setState(self::STATE_OK);
+ $this->setMessage(sprintf(
+ t(
+ '%1$s has been up and running with PID %2$d %3$s',
+ 'Last format parameter represents the time running'
+ ),
+ $backendName,
+ $programStatus->process_id,
+ DateFormatter::timeSince($programStatus->program_start_time)
+ ));
+
+ $warningMessages = [];
+
+ if (! $programStatus->active_host_checks_enabled) {
+ $this->setState(self::STATE_WARNING);
+ $warningMessages[] = t('Active host checks are disabled');
+ }
+
+ if (! $programStatus->active_service_checks_enabled) {
+ $this->setState(self::STATE_WARNING);
+ $warningMessages[] = t('Active service checks are disabled');
+ }
+
+ if (! $programStatus->notifications_enabled) {
+ $this->setState(self::STATE_WARNING);
+ $warningMessages[] = t('Notifications are disabled');
+ }
+
+ if ($this->getState() === self::STATE_WARNING) {
+ $this->setMessage(implode("; ", $warningMessages));
+ }
+ } else {
+ $this->setState(self::STATE_CRITICAL);
+ $this->setMessage(sprintf(t('Backend %s is not running'), $backendName));
+ }
+
+ $this->setMetrics((array) $programStatus);
+ }
+
+ protected function getProgramStatus()
+ {
+ if ($this->programStatus === null) {
+ $this->programStatus = MonitoringBackend::instance()->select()
+ ->from('programstatus', [
+ 'program_version',
+ 'status_update_time',
+ 'program_start_time',
+ 'program_end_time',
+ 'endpoint_name',
+ 'is_currently_running',
+ 'process_id',
+ 'last_command_check',
+ 'last_log_rotation',
+ 'notifications_enabled',
+ 'active_service_checks_enabled',
+ 'active_host_checks_enabled',
+ 'event_handlers_enabled',
+ 'flap_detection_enabled',
+ 'process_performance_data'
+ ])
+ ->fetchRow();
+ }
+
+ return $this->programStatus;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/ProvidedHook/X509/Sni.php b/modules/monitoring/library/Monitoring/ProvidedHook/X509/Sni.php
new file mode 100644
index 0000000..c649437
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/ProvidedHook/X509/Sni.php
@@ -0,0 +1,37 @@
+<?php
+/* Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\ProvidedHook\X509;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Module\X509\Hook\SniHook;
+
+class Sni extends SniHook
+{
+ public function getHosts(Filter $filter = null)
+ {
+ MonitoringBackend::clearInstances();
+
+ $hosts = MonitoringBackend::instance()
+ ->select()
+ ->from('hoststatus', [
+ 'host_name',
+ 'host_address',
+ 'host_address6'
+ ]);
+ if ($filter !== null) {
+ $hosts->applyFilter($filter);
+ }
+
+ foreach ($hosts as $host) {
+ if (! empty($host->host_address)) {
+ yield $host->host_address => $host->host_name;
+ }
+
+ if (! empty($host->host_address6)) {
+ yield $host->host_address6 => $host->host_name;
+ }
+ }
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/SecurityStep.php b/modules/monitoring/library/Monitoring/SecurityStep.php
new file mode 100644
index 0000000..94053b3
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/SecurityStep.php
@@ -0,0 +1,84 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring;
+
+use Exception;
+use Icinga\Module\Setup\Step;
+use Icinga\Application\Config;
+use Icinga\Exception\IcingaException;
+
+class SecurityStep extends Step
+{
+ protected $data;
+
+ protected $error;
+
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ }
+
+ public function apply()
+ {
+ $config = array();
+ $config['security'] = $this->data['securityConfig'];
+
+ try {
+ Config::fromArray($config)
+ ->setConfigFile(Config::resolvePath('modules/monitoring/config.ini'))
+ ->saveIni();
+ } catch (Exception $e) {
+ $this->error = $e;
+ return false;
+ }
+
+ $this->error = false;
+ return true;
+ }
+
+ public function getSummary()
+ {
+ $pageTitle = '<h2>' . mt('monitoring', 'Monitoring Security', 'setup.page.title') . '</h2>';
+ $pageDescription = '<p>' . mt(
+ 'monitoring',
+ 'Icinga Web 2 will protect your monitoring environment against'
+ . ' prying eyes using the configuration specified below:'
+ ) . '</p>';
+
+ $pageHtml = ''
+ . '<table>'
+ . '<tbody>'
+ . '<tr>'
+ . '<td><strong>' . mt('monitoring', 'Protected Custom Variables') . '</strong></td>'
+ . '<td>' . ($this->data['securityConfig']['protected_customvars'] ? (
+ $this->data['securityConfig']['protected_customvars']
+ ) : mt('monitoring', 'None', 'monitoring.protected_customvars')) . '</td>'
+ . '</tr>'
+ . '</tbody>'
+ . '</table>';
+
+ return $pageTitle . '<div class="topic">' . $pageDescription . $pageHtml . '</div>';
+ }
+
+ public function getReport()
+ {
+ if ($this->error === false) {
+ return array(sprintf(
+ mt('monitoring', 'Monitoring security configuration has been successfully created: %s'),
+ Config::resolvePath('modules/monitoring/config.ini')
+ ));
+ } elseif ($this->error !== null) {
+ return array(
+ sprintf(
+ mt(
+ 'monitoring',
+ 'Monitoring security configuration could not be written to: %s. An error occured:'
+ ),
+ Config::resolvePath('modules/monitoring/config.ini')
+ ),
+ sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error))
+ );
+ }
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Timeline/TimeEntry.php b/modules/monitoring/library/Monitoring/Timeline/TimeEntry.php
new file mode 100644
index 0000000..ee313b3
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Timeline/TimeEntry.php
@@ -0,0 +1,233 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Timeline;
+
+use DateTime;
+use Icinga\Web\Url;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * An event group that is part of a timeline
+ */
+class TimeEntry
+{
+ /**
+ * The name of this group
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * The amount of events that are part of this group
+ *
+ * @var int
+ */
+ protected $value;
+
+ /**
+ * The date and time of this group
+ *
+ * @var DateTime
+ */
+ protected $dateTime;
+
+ /**
+ * The url to this group's detail view
+ *
+ * @var Url
+ */
+ protected $detailUrl;
+
+ /**
+ * The weight of this group
+ *
+ * @var float
+ */
+ protected $weight = 1.0;
+
+ /**
+ * The label of this group
+ *
+ * @var string
+ */
+ protected $label;
+
+ /**
+ * The CSS class of the entry
+ *
+ * @var string
+ */
+ protected $class;
+
+ /**
+ * Return a new TimeEntry object with the given attributes being set
+ *
+ * @param array $attributes The attributes to set
+ * @return TimeEntry The resulting TimeEntry object
+ * @throws ProgrammingError If one of the given attributes cannot be set
+ */
+ public static function fromArray(array $attributes)
+ {
+ $entry = new TimeEntry();
+
+ foreach ($attributes as $name => $value) {
+ $methodName = 'set' . ucfirst($name);
+ if (method_exists($entry, $methodName)) {
+ $entry->{$methodName}($value);
+ } else {
+ throw new ProgrammingError(
+ 'Method "%s" does not exist on object of type "%s"',
+ $methodName,
+ __CLASS__
+ );
+ }
+ }
+
+ return $entry;
+ }
+
+ /**
+ * Set this group's name
+ *
+ * @param string $name The name to set
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ }
+
+ /**
+ * Return the name of this group
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set this group's amount of events
+ *
+ * @param int $value The value to set
+ */
+ public function setValue($value)
+ {
+ $this->value = intval($value);
+ }
+
+ /**
+ * Return the amount of events in this group
+ *
+ * @return int
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Set this group's date and time
+ *
+ * @param DateTime $dateTime The date and time to set
+ */
+ public function setDateTime(DateTime $dateTime)
+ {
+ $this->dateTime = $dateTime;
+ }
+
+ /**
+ * Return the date and time of this group
+ *
+ * @return DateTime
+ */
+ public function getDateTime()
+ {
+ return $this->dateTime;
+ }
+
+ /**
+ * Set the url to this group's detail view
+ *
+ * @param Url $detailUrl The url to set
+ */
+ public function setDetailUrl(Url $detailUrl)
+ {
+ $this->detailUrl = $detailUrl;
+ }
+
+ /**
+ * Return the url to this group's detail view
+ *
+ * @return Url
+ */
+ public function getDetailUrl()
+ {
+ return $this->detailUrl;
+ }
+
+ /**
+ * Set this group's weight
+ *
+ * @param float $weight The weight for this group
+ */
+ public function setWeight($weight)
+ {
+ $this->weight = floatval($weight);
+ }
+
+ /**
+ * Return the weight of this group
+ *
+ * @return float
+ */
+ public function getWeight()
+ {
+ return $this->weight;
+ }
+
+ /**
+ * Set this group's label
+ *
+ * @param string $label The label to set
+ */
+ public function setLabel($label)
+ {
+ $this->label = $label;
+ }
+
+ /**
+ * Return the label of this group
+ *
+ * @return string
+ */
+ public function getLabel()
+ {
+ return $this->label;
+ }
+
+ /**
+ * Get the CSS class
+ *
+ * @return string
+ */
+ public function getClass()
+ {
+ return $this->class;
+ }
+
+ /**
+ * Set the CSS class
+ *
+ * @param string $class
+ *
+ * @return $this
+ */
+ public function setClass($class)
+ {
+ $this->class = $class;
+ return $this;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Timeline/TimeLine.php b/modules/monitoring/library/Monitoring/Timeline/TimeLine.php
new file mode 100644
index 0000000..7a192a6
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Timeline/TimeLine.php
@@ -0,0 +1,491 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Timeline;
+
+use DateTime;
+use Exception;
+use ArrayIterator;
+use Icinga\Exception\IcingaException;
+use IteratorAggregate;
+use Icinga\Data\Filter\Filter;
+use Icinga\Web\Hook;
+use Icinga\Web\Session\SessionNamespace;
+use Icinga\Module\Monitoring\DataView\DataView;
+use Traversable;
+
+/**
+ * Represents a set of events in a specific range of time
+ */
+class TimeLine implements IteratorAggregate
+{
+ /**
+ * The resultset returned by the dataview
+ *
+ * @var array
+ */
+ private $resultset;
+
+ /**
+ * The groups this timeline uses for display purposes
+ *
+ * @var array
+ */
+ private $displayGroups;
+
+ /**
+ * The session to use
+ *
+ * @var SessionNamespace
+ */
+ protected $session;
+
+ /**
+ * The base that is used to calculate each circle's diameter
+ *
+ * @var float
+ */
+ protected $calculationBase;
+
+ /**
+ * The dataview to fetch entries from
+ *
+ * @var DataView
+ */
+ protected $dataview;
+
+ /**
+ * The names by which to group entries
+ *
+ * @var array
+ */
+ protected $identifiers;
+
+ /**
+ * The range of time for which to display entries
+ *
+ * @var TimeRange
+ */
+ protected $displayRange;
+
+ /**
+ * The range of time for which to calculate forecasts
+ *
+ * @var TimeRange
+ */
+ protected $forecastRange;
+
+ /**
+ * The maximum diameter each circle can have
+ *
+ * @var float
+ */
+ protected $circleDiameter = 100.0;
+
+ /**
+ * The minimum diameter each circle can have
+ *
+ * @var float
+ */
+ protected $minCircleDiameter = 1.0;
+
+ /**
+ * The unit of a circle's diameter
+ *
+ * @var string
+ */
+ protected $diameterUnit = 'px';
+
+ /**
+ * Return a iterator for this timeline
+ *
+ * @return ArrayIterator
+ */
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->toArray());
+ }
+
+ /**
+ * Create a new timeline
+ *
+ * The given dataview must provide the following columns:
+ * - name A string identifying an entry (Corresponds to the keys of "$identifiers")
+ * - time A unix timestamp that defines where to place an entry on the timeline
+ *
+ * @param DataView $dataview The dataview to fetch entries from
+ * @param array $identifiers The names by which to group entries
+ */
+ public function __construct(DataView $dataview, array $identifiers)
+ {
+ $this->dataview = $dataview;
+ $this->identifiers = $identifiers;
+ }
+
+ /**
+ * Set the session to use
+ *
+ * @param SessionNamespace $session The session to use
+ */
+ public function setSession(SessionNamespace $session)
+ {
+ $this->session = $session;
+ }
+
+ /**
+ * Set the range of time for which to display elements
+ *
+ * @param TimeRange $range The range of time for which to display elements
+ */
+ public function setDisplayRange(TimeRange $range)
+ {
+ $this->displayRange = $range;
+ }
+
+ /**
+ * Set the range of time for which to calculate forecasts
+ *
+ * @param TimeRange $range The range of time for which to calculate forecasts
+ */
+ public function setForecastRange(TimeRange $range)
+ {
+ $this->forecastRange = $range;
+ }
+
+ /**
+ * Set the maximum diameter each circle can have
+ *
+ * @param string $width The diameter to set, suffixed with its unit
+ *
+ * @throws Exception If the given diameter is invalid
+ */
+ public function setMaximumCircleWidth($width)
+ {
+ $matches = array();
+ if (preg_match('#([\d|\.]+)([a-z]+|%)#', $width, $matches)) {
+ $this->circleDiameter = floatval($matches[1]);
+ $this->diameterUnit = $matches[2];
+ } else {
+ throw new IcingaException(
+ 'Width "%s" is not a valid width',
+ $width
+ );
+ }
+ }
+
+ /**
+ * Set the minimum diameter each circle can have
+ *
+ * @param string $width The diameter to set, suffixed with its unit
+ *
+ * @throws Exception If the given diameter is invalid or its unit differs from the maximum
+ */
+ public function setMinimumCircleWidth($width)
+ {
+ $matches = array();
+ if (preg_match('#([\d|\.]+)([a-z]+|%)#', $width, $matches)) {
+ if ($matches[2] === $this->diameterUnit) {
+ $this->minCircleDiameter = floatval($matches[1]);
+ } else {
+ throw new IcingaException(
+ 'Unit needs to be in "%s"',
+ $this->diameterUnit
+ );
+ }
+ } else {
+ throw new IcingaException(
+ 'Width "%s" is not a valid width',
+ $width
+ );
+ }
+ }
+
+ /**
+ * Return all known group types (identifiers) with their respective labels and classess as array
+ *
+ * @return array
+ */
+ public function getGroupInfo()
+ {
+ $groupInfo = array();
+ foreach ($this->identifiers as $name => $attributes) {
+ if (isset($attributes['groupBy'])) {
+ $name = $attributes['groupBy'];
+ }
+
+ $groupInfo[$name]['class'] = $attributes['class'];
+ $groupInfo[$name]['label'] = $attributes['label'];
+ }
+
+ return $groupInfo;
+ }
+
+ /**
+ * Return the circle's diameter for the given event group
+ *
+ * @param TimeEntry $group The group for which to return a circle width
+ * @param int $precision Amount of decimal places to preserve
+ *
+ * @return string
+ */
+ public function calculateCircleWidth(TimeEntry $group, $precision = 0)
+ {
+ $base = $this->getCalculationBase(true);
+ $factor = log($group->getValue() * $group->getWeight(), $base) / 100;
+ $width = $this->circleDiameter * $factor;
+ return sprintf(
+ '%.' . $precision . 'F%s',
+ $width > $this->minCircleDiameter ? $width : $this->minCircleDiameter,
+ $this->diameterUnit
+ );
+ }
+
+ /**
+ * Return an extrapolated circle width for the given event group
+ *
+ * @param TimeEntry $group The event group for which to return an extrapolated circle width
+ * @param int $precision Amount of decimal places to preserve
+ *
+ * @return string
+ */
+ public function getExtrapolatedCircleWidth(TimeEntry $group, $precision = 0)
+ {
+ $eventCount = 0;
+ foreach ($this->displayGroups as $groups) {
+ if (array_key_exists($group->getName(), $groups)) {
+ $eventCount += $groups[$group->getName()]->getValue();
+ }
+ }
+
+ $extrapolatedCount = (int) $eventCount / count($this->displayGroups);
+ if ($extrapolatedCount < $group->getValue()) {
+ return $this->calculateCircleWidth($group, $precision);
+ }
+
+ return $this->calculateCircleWidth(
+ TimeEntry::fromArray(
+ array(
+ 'value' => $extrapolatedCount,
+ 'weight' => $group->getWeight()
+ )
+ ),
+ $precision
+ );
+ }
+
+ /**
+ * Return the base that should be used to calculate circle widths
+ *
+ * @param bool $create Whether to generate a new base if none is known yet
+ *
+ * @return float|null
+ */
+ public function getCalculationBase($create)
+ {
+ if ($this->calculationBase === null) {
+ $calculationBase = $this->session !== null ? $this->session->get('calculationBase') : null;
+
+ if ($create) {
+ $new = $this->generateCalculationBase();
+ if ($new > $calculationBase) {
+ $this->calculationBase = $new;
+
+ if ($this->session !== null) {
+ $this->session->calculationBase = $new;
+ }
+ } else {
+ $this->calculationBase = $calculationBase;
+ }
+ } else {
+ return $calculationBase;
+ }
+ }
+
+ return $this->calculationBase;
+ }
+
+ /**
+ * Generate a new base to calculate circle widths with
+ *
+ * @return float
+ */
+ protected function generateCalculationBase()
+ {
+ $allEntries = $this->groupEntries(
+ array_merge(
+ $this->fetchEntries(),
+ $this->fetchForecasts()
+ ),
+ new TimeRange(
+ $this->displayRange->getStart(),
+ $this->forecastRange->getEnd(),
+ $this->displayRange->getInterval()
+ )
+ );
+
+ $highestValue = 0;
+ foreach ($allEntries as $groups) {
+ foreach ($groups as $group) {
+ if ($group->getValue() * $group->getWeight() > $highestValue) {
+ $highestValue = $group->getValue() * $group->getWeight();
+ }
+ }
+ }
+
+ return pow($highestValue, 1 / 100); // 100 == 100%
+ }
+
+ /**
+ * Fetch all entries and forecasts by using the dataview associated with this timeline
+ *
+ * @return array The dataview's result
+ */
+ private function fetchResults()
+ {
+ $hookResults = array();
+ foreach (Hook::all('timeline') as $timelineProvider) {
+ $hookResults = array_merge(
+ $hookResults,
+ $timelineProvider->fetchEntries($this->displayRange),
+ $timelineProvider->fetchForecasts($this->forecastRange)
+ );
+
+ foreach ($timelineProvider->getIdentifiers() as $identifier => $attributes) {
+ if (!array_key_exists($identifier, $this->identifiers)) {
+ $this->identifiers[$identifier] = $attributes;
+ }
+ }
+ }
+
+ $query = $this->dataview;
+ $filter = Filter::matchAll(
+ Filter::where('type', array_keys($this->identifiers)),
+ Filter::expression('timestamp', '<=', $this->displayRange->getStart()->getTimestamp()),
+ Filter::expression('timestamp', '>', $this->displayRange->getEnd()->getTimestamp())
+ );
+ $query->applyFilter($filter);
+ return array_merge($query->getQuery()->fetchAll(), $hookResults);
+ }
+
+ /**
+ * Fetch all entries
+ *
+ * @return array The entries to display on the timeline
+ */
+ protected function fetchEntries()
+ {
+ if ($this->resultset === null) {
+ $this->resultset = $this->fetchResults();
+ }
+
+ $range = $this->displayRange;
+ return array_filter(
+ $this->resultset,
+ function ($e) use ($range) {
+ return $range->validateTime((int) $e->time);
+ }
+ );
+ }
+
+ /**
+ * Fetch all forecasts
+ *
+ * @return array The entries to calculate forecasts with
+ */
+ protected function fetchForecasts()
+ {
+ if ($this->resultset === null) {
+ $this->resultset = $this->fetchResults();
+ }
+
+ $range = $this->forecastRange;
+ return array_filter(
+ $this->resultset,
+ function ($e) use ($range) {
+ return $range->validateTime($e->time);
+ }
+ );
+ }
+
+ /**
+ * Return the given entries grouped together
+ *
+ * @param array $entries The entries to group
+ * @param TimeRange $timeRange The range of time to group by
+ *
+ * @return array displayGroups The grouped entries
+ */
+ protected function groupEntries(array $entries, TimeRange $timeRange)
+ {
+ $counts = array();
+ foreach ($entries as $entry) {
+ $entryTime = new DateTime();
+ $entryTime->setTimestamp($entry->time);
+ $timestamp = $timeRange->findTimeframe($entryTime, true);
+
+ if ($timestamp !== null) {
+ if (array_key_exists($entry->name, $counts)) {
+ if (array_key_exists($timestamp, $counts[$entry->name])) {
+ $counts[$entry->name][$timestamp] += 1;
+ } else {
+ $counts[$entry->name][$timestamp] = 1;
+ }
+ } else {
+ $counts[$entry->name][$timestamp] = 1;
+ }
+ }
+ }
+
+ $groups = array();
+ foreach ($counts as $name => $data) {
+ foreach ($data as $timestamp => $count) {
+ $dateTime = new DateTime();
+ $dateTime->setTimestamp($timestamp);
+
+ $groupName = $name;
+ if (isset($this->identifiers[$name]['groupBy'])) {
+ $groupName = $this->identifiers[$name]['groupBy'];
+ }
+
+ if (isset($groups[$timestamp][$groupName])) {
+ $groups[$timestamp][$groupName]->setValue(
+ $groups[$timestamp][$groupName]->getValue() + $count
+ );
+ } else {
+ $groups[$timestamp][$groupName] = TimeEntry::fromArray(
+ array(
+ 'name' => $groupName,
+ 'value' => $count,
+ 'dateTime' => $dateTime,
+ 'class' => $this->identifiers[$name]['class'],
+ 'detailUrl' => $this->identifiers[$name]['detailUrl'],
+ 'label' => $this->identifiers[$name]['label']
+ )
+ );
+ }
+ }
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Return the contents of this timeline as array
+ *
+ * @return array
+ */
+ protected function toArray()
+ {
+ $this->displayGroups = $this->groupEntries($this->fetchEntries(), $this->displayRange);
+
+ $array = array();
+ foreach ($this->displayRange as $timestamp => $timeframe) {
+ $array[] = array(
+ $timeframe,
+ array_key_exists($timestamp, $this->displayGroups) ? $this->displayGroups[$timestamp] : array()
+ );
+ }
+
+ return $array;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Timeline/TimeRange.php b/modules/monitoring/library/Monitoring/Timeline/TimeRange.php
new file mode 100644
index 0000000..aa63d3c
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Timeline/TimeRange.php
@@ -0,0 +1,258 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Timeline;
+
+use stdClass;
+use Iterator;
+use DateTime;
+use DateInterval;
+use Icinga\Util\Format;
+
+/**
+ * A range of time split into a specific interval
+ *
+ * @see Iterator
+ */
+class TimeRange implements Iterator
+{
+ /**
+ * The start of this time range
+ *
+ * @var DateTime
+ */
+ protected $start;
+
+ /**
+ * The end of this time range
+ *
+ * @var DateTime
+ */
+ protected $end;
+
+ /**
+ * The interval by which this time range is split
+ *
+ * @var DateInterval
+ */
+ protected $interval;
+
+ /**
+ * The current date in the iteration
+ *
+ * @var DateTime
+ */
+ protected $current;
+
+ /**
+ * Whether the date iteration is negative
+ *
+ * @var bool
+ */
+ protected $negative;
+
+ /**
+ * Initialize a new time range
+ *
+ * @param DateTime $start When the time range should start
+ * @param DateTime $end When the time range should end
+ * @param DateInterval $interval The interval of the time range
+ */
+ public function __construct(DateTime $start, DateTime $end, DateInterval $interval)
+ {
+ $this->interval = $interval;
+ $this->start = $start;
+ $this->end = $end;
+ $this->negative = $this->start > $this->end;
+ }
+
+ /**
+ * Return when this range of time starts
+ *
+ * @return DateTime
+ */
+ public function getStart()
+ {
+ return $this->start;
+ }
+
+ /**
+ * Return when this range of time ends
+ *
+ * @return DateTime
+ */
+ public function getEnd()
+ {
+ return $this->end;
+ }
+
+ /**
+ * Return the interval by which this time range is split
+ *
+ * @return DateInterval
+ */
+ public function getInterval()
+ {
+ return $this->interval;
+ }
+
+ /**
+ * Return the appropriate timeframe for the given date and time or null if none could be found
+ *
+ * @param DateTime $dateTime The date and time for which to search the timeframe
+ * @param bool $asTimestamp Whether the start of the timeframe should be returned as timestamp
+ * @return stdClass|int|null An object with a ´start´ and ´end´ property or a timestamp
+ */
+ public function findTimeframe(DateTime $dateTime, $asTimestamp = false)
+ {
+ foreach ($this as $timeframeIdentifier => $timeframe) {
+ if ($this->negative) {
+ if ($dateTime <= $timeframe->start && $dateTime >= $timeframe->end) {
+ return $asTimestamp ? $timeframeIdentifier : $timeframe;
+ }
+ } elseif ($dateTime >= $timeframe->start && $dateTime <= $timeframe->end) {
+ return $asTimestamp ? $timeframeIdentifier : $timeframe;
+ }
+ }
+ }
+
+ /**
+ * Return whether the given time is within this range of time
+ *
+ * @param string|int|DateTime $time The timestamp or date and time to check
+ */
+ public function validateTime($time)
+ {
+ if ($time instanceof DateTime) {
+ $dateTime = $time;
+ } elseif (is_string($time)) {
+ $dateTime = DateTime::createFromFormat('d/m/Y g:i A', $time);
+ } else {
+ $dateTime = new DateTime();
+ $dateTime->setTimestamp($time);
+ }
+
+ return ($this->negative && ($dateTime <= $this->start && $dateTime >= $this->end)) ||
+ (!$this->negative && ($dateTime >= $this->start && $dateTime <= $this->end));
+ }
+
+ /**
+ * Return the appropriate timeframe for the given timeframe start
+ *
+ * @param int|DateTime $time The timestamp or date and time for which to return the timeframe
+ * @return stdClass An object with a ´start´ and ´end´ property
+ */
+ public function getTimeframe($time)
+ {
+ if ($time instanceof DateTime) {
+ $startTime = clone $time;
+ } else {
+ $startTime = new DateTime();
+ $startTime->setTimestamp($time);
+ }
+
+ return $this->buildTimeframe($startTime, $this->applyInterval(clone $startTime, 1));
+ }
+
+ /**
+ * Apply the current interval to the given date and time
+ *
+ * @param DateTime $dateTime The date and time to apply the interval to
+ * @param int $adjustBy By how much seconds the resulting date and time should be adjusted
+ *
+ * @return DateTime
+ */
+ protected function applyInterval(DateTime $dateTime, $adjustBy)
+ {
+ if (!$this->interval->y && !$this->interval->m) {
+ if ($this->negative) {
+ return $dateTime->sub($this->interval)->add(new DateInterval('PT' . $adjustBy . 'S'));
+ } else {
+ return $dateTime->add($this->interval)->sub(new DateInterval('PT' . $adjustBy . 'S'));
+ }
+ } elseif ($this->interval->m) {
+ for ($i = 0; $i < $this->interval->m; $i++) {
+ if ($this->negative) {
+ $dateTime->sub(new DateInterval('PT' . Format::secondsByMonth($dateTime) . 'S'));
+ } else {
+ $dateTime->add(new DateInterval('PT' . Format::secondsByMonth($dateTime) . 'S'));
+ }
+ }
+ } elseif ($this->interval->y) {
+ for ($i = 0; $i < $this->interval->y; $i++) {
+ if ($this->negative) {
+ $dateTime->sub(new DateInterval('PT' . Format::secondsByYear($dateTime) . 'S'));
+ } else {
+ $dateTime->add(new DateInterval('PT' . Format::secondsByYear($dateTime) . 'S'));
+ }
+ }
+ }
+ $adjustment = new DateInterval('PT' . $adjustBy . 'S');
+ return $this->negative ? $dateTime->add($adjustment) : $dateTime->sub($adjustment);
+ }
+
+ /**
+ * Return an object representation of the given timeframe
+ *
+ * @param DateTime $start The start of the timeframe
+ * @param DateTime $end The end of the timeframe
+ * @return stdClass
+ */
+ protected function buildTimeframe(DateTime $start, DateTime $end)
+ {
+ $timeframe = new stdClass();
+ $timeframe->start = $start;
+ $timeframe->end = $end;
+ return $timeframe;
+ }
+
+ /**
+ * Reset the iterator to its initial state
+ */
+ public function rewind(): void
+ {
+ $this->current = clone $this->start;
+ }
+
+ /**
+ * Return whether the current iteration step is valid
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ if ($this->negative) {
+ return $this->current > $this->end;
+ } else {
+ return $this->current < $this->end;
+ }
+ }
+
+ /**
+ * Return the current value in the iteration
+ *
+ * @return stdClass
+ */
+ public function current(): object
+ {
+ return $this->getTimeframe($this->current);
+ }
+
+ /**
+ * Return a unique identifier for the current value in the iteration
+ *
+ * @return int
+ */
+ public function key(): int
+ {
+ return $this->current->getTimestamp();
+ }
+
+ /**
+ * Advance the iterator position by one
+ */
+ public function next(): void
+ {
+ $this->applyInterval($this->current, 0);
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/TransportStep.php b/modules/monitoring/library/Monitoring/TransportStep.php
new file mode 100644
index 0000000..d138eb4
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/TransportStep.php
@@ -0,0 +1,143 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring;
+
+use Exception;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Setup\Step;
+use Icinga\Application\Config;
+use Icinga\Exception\IcingaException;
+
+class TransportStep extends Step
+{
+ protected $data;
+
+ protected $error;
+
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ }
+
+ public function apply()
+ {
+ $transportConfig = $this->data['transportConfig'];
+ $transportName = $transportConfig['name'];
+ unset($transportConfig['name']);
+
+ try {
+ Config::fromArray(array($transportName => $transportConfig))
+ ->setConfigFile(Config::resolvePath('modules/monitoring/commandtransports.ini'))
+ ->saveIni();
+ } catch (Exception $e) {
+ $this->error = $e;
+ return false;
+ }
+
+ $this->error = false;
+ return true;
+ }
+
+ public function getSummary()
+ {
+ switch ($this->data['transportConfig']['transport']) {
+ case 'local':
+ $details = '<p>' . sprintf(
+ mt(
+ 'monitoring',
+ 'Icinga Web 2 will use the named pipe located at "%s"'
+ . ' to send commands to your monitoring instance.'
+ ),
+ $this->data['transportConfig']['path']
+ ) . '</p>';
+ break;
+ case 'remote':
+ $details = '<p>'
+ . sprintf(
+ mt(
+ 'monitoring',
+ 'Icinga Web 2 will use the named pipe located on a remote machine at "%s" to send commands'
+ . ' to your monitoring instance by using the connection details listed below:'
+ ),
+ $this->data['transportConfig']['path']
+ )
+ . '</p>'
+ . '<table>'
+ . '<tbody>'
+ . '<tr>'
+ . '<td><strong>' . mt('monitoring', 'Remote Host') . '</strong></td>'
+ . '<td>' . $this->data['transportConfig']['host'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('monitoring', 'Remote SSH Port') . '</strong></td>'
+ . '<td>' . $this->data['transportConfig']['port'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('monitoring', 'Remote SSH User') . '</strong></td>'
+ . '<td>' . $this->data['transportConfig']['user'] . '</td>'
+ . '</tr>'
+ . '</tbody>'
+ . '</table>';
+ break;
+ case 'api':
+ $details = '<p>'
+ . mt(
+ 'monitoring',
+ 'Icinga Web 2 will use the Icinga 2 API to send commands'
+ . ' to your monitoring instance by using the connection details listed below:'
+ )
+ . '</p>'
+ . '<table>'
+ . '<tbody>'
+ . '<tr>'
+ . '<td><strong>' . mt('monitoring', 'Host') . '</strong></td>'
+ . '<td>' . $this->data['transportConfig']['host'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('monitoring', 'Port') . '</strong></td>'
+ . '<td>' . $this->data['transportConfig']['port'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('monitoring', 'Username') . '</strong></td>'
+ . '<td>' . $this->data['transportConfig']['username'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('monitoring', 'Password') . '</strong></td>'
+ . '<td>' . str_repeat('*', strlen($this->data['transportConfig']['password'])) . '</td>'
+ . '</tr>'
+ . '</tbody>'
+ . '</table>';
+ break;
+ default:
+ throw new ProgrammingError(
+ 'Unknown command transport type: %s',
+ $this->data['transportConfig']['transport']
+ );
+ }
+
+ return '<h2>' . mt('monitoring', 'Command Transport', 'setup.page.title') . '</h2>'
+ . '<div class="topic">' . $details . '</div>';
+ }
+
+ public function getReport()
+ {
+ if ($this->error === false) {
+ return array(sprintf(
+ mt('monitoring', 'Command transport configuration has been successfully created: %s'),
+ Config::resolvePath('modules/monitoring/commandtransports.ini')
+ ));
+ } elseif ($this->error !== null) {
+ return array(
+ sprintf(
+ mt(
+ 'monitoring',
+ 'Command transport configuration could not be written to: %s. An error occured:'
+ ),
+ Config::resolvePath('modules/monitoring/commandtransports.ini')
+ ),
+ sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error))
+ );
+ }
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php b/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
new file mode 100644
index 0000000..b001ca8
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
@@ -0,0 +1,339 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Controller;
+
+use Exception;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\Forms\Command\Object\CheckNowCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\DeleteCommentCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\DeleteDowntimeCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ObjectsCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\RemoveAcknowledgementCommandForm;
+use Icinga\Module\Monitoring\Forms\Command\Object\ToggleObjectFeaturesCommandForm;
+use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook;
+use Icinga\Module\Monitoring\Hook\ObjectDetailsTabHook;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\Hook;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\MenuAction;
+
+/**
+ * Base class for the host and service controller
+ */
+abstract class MonitoredObjectController extends Controller
+{
+ /**
+ * The requested host or service
+ *
+ * @var \Icinga\Module\Monitoring\Object\Host|\Icinga\Module\Monitoring\Object\Host
+ */
+ protected $object;
+
+ /**
+ * URL to redirect to after a command was handled
+ *
+ * @var string
+ */
+ protected $commandRedirectUrl;
+
+ /**
+ * List of visible hooked tabs
+ *
+ * @var ObjectDetailsTabHook[]
+ */
+ protected $tabHooks = [];
+
+ /**
+ * (non-PHPDoc)
+ * @see \Icinga\Web\Controller\ActionController For the method documentation.
+ */
+ public function prepareInit()
+ {
+ parent::prepareInit();
+ if (Hook::has('ticket')) {
+ $this->view->tickets = Hook::first('ticket');
+ }
+ if (Hook::has('grapher')) {
+ $this->view->graphers = Hook::all('grapher');
+ }
+ }
+
+ /**
+ * Show a host or service
+ */
+ public function showAction()
+ {
+ $this->setAutorefreshInterval(10);
+ $this->setupQuickActionForms();
+ $auth = $this->Auth();
+ $this->object->populate();
+ $this->handleFormatRequest();
+ $toggleFeaturesForm = new ToggleObjectFeaturesCommandForm(array(
+ 'backend' => $this->backend,
+ 'objects' => $this->object
+ ));
+ $toggleFeaturesForm
+ ->load($this->object)
+ ->handleRequest();
+ $this->view->toggleFeaturesForm = $toggleFeaturesForm;
+ if (! empty($this->object->comments) && $auth->hasPermission('monitoring/command/comment/delete')) {
+ $delCommentForm = new DeleteCommentCommandForm();
+ $delCommentForm->handleRequest();
+ $this->view->delCommentForm = $delCommentForm;
+ }
+ if (! empty($this->object->downtimes) && $auth->hasPermission('monitoring/command/downtime/delete')) {
+ $delDowntimeForm = new DeleteDowntimeCommandForm();
+ $delDowntimeForm->handleRequest();
+ $this->view->delDowntimeForm = $delDowntimeForm;
+ }
+ $this->view->showInstance = $this->backend->select()->from('instance')->count() > 1;
+ $this->view->object = $this->object;
+
+ $this->view->extensionsHtml = array();
+ foreach (Hook::all('Monitoring\DetailviewExtension') as $hook) {
+ /** @var DetailviewExtensionHook $hook */
+
+ try {
+ $html = $hook->setView($this->view)->getHtmlForObject($this->object);
+ } catch (Exception $e) {
+ $html = $this->view->escape($e->getMessage());
+ }
+
+ if ($html) {
+ $module = $this->view->escape($hook->getModule()->getName());
+ $this->view->extensionsHtml[] =
+ '<div class="icinga-module module-' . $module . '" data-icinga-module="' . $module . '">'
+ . $html
+ . '</div>';
+ }
+ }
+ }
+
+ /**
+ * Show the history for a host or service
+ */
+ public function historyAction()
+ {
+ $this->getTabs()->activate('history');
+ $this->view->history = $this->object->fetchEventhistory()->eventhistory;
+ $this->applyRestriction('monitoring/filter/objects', $this->view->history);
+
+ $this->setupLimitControl(50);
+ $this->setupPaginationControl($this->view->history, 50);
+ $this->view->object = $this->object;
+ $this->render('object/detail-history', null, true);
+ }
+
+ /**
+ * Show the content of a custom tab
+ */
+ public function tabhookAction()
+ {
+ $hookName = $this->params->get('hook');
+ $this->getTabs()->activate($hookName);
+
+ $hook = $this->tabHooks[$hookName];
+
+ $this->view->header = $hook->getHeader($this->object, $this->getRequest());
+ $this->view->content = $hook->getContent($this->object, $this->getRequest());
+ $this->view->object = $this->object;
+ $this->render('object/detail-tabhook', null, true);
+ }
+
+ /**
+ * Handle a command form
+ *
+ * @param ObjectsCommandForm $form
+ *
+ * @return ObjectsCommandForm
+ */
+ protected function handleCommandForm(ObjectsCommandForm $form)
+ {
+ $form
+ ->setBackend($this->backend)
+ ->setObjects($this->object)
+ ->setRedirectUrl(Url::fromPath($this->commandRedirectUrl)->setParams($this->params))
+ ->handleRequest();
+ $this->view->form = $form;
+ $this->view->object = $this->object;
+ $this->view->tabs->remove('dashboard');
+ $this->view->tabs->remove('menu-entry');
+ $this->_helper->viewRenderer('partials/command/object-command-form', null, true);
+ $this->setupQuickActionForms();
+ return $form;
+ }
+
+ /**
+ * Export to JSON if requested
+ */
+ protected function handleFormatRequest($query = null)
+ {
+ if ($this->params->get('format') === 'json'
+ || $this->getRequest()->getHeader('Accept') === 'application/json'
+ ) {
+ $payload = (array) $this->object->properties;
+ $payload['vars'] = $this->object->customvars;
+
+ if ($this->hasPermission('*') || ! $this->hasPermission('no-monitoring/contacts')) {
+ $payload['contacts'] = $this->object->contacts->fetchPairs();
+ $payload['contact_groups'] = $this->object->contactgroups->fetchPairs();
+ } else {
+ $payload['contacts'] = [];
+ $payload['contact_groups'] = [];
+ }
+
+ $groupName = $this->object->getType() . 'groups';
+ $payload[$groupName] = $this->object->$groupName;
+ $this->getResponse()->json()
+ ->setSuccessData($payload)
+ ->setAutoSanitize()
+ ->sendResponse();
+ }
+ }
+
+ /**
+ * Acknowledge a problem
+ */
+ abstract public function acknowledgeProblemAction();
+
+ /**
+ * Add a comment
+ */
+ abstract public function addCommentAction();
+
+ /**
+ * Reschedule a check
+ */
+ abstract public function rescheduleCheckAction();
+
+ /**
+ * Schedule a downtime
+ */
+ abstract public function scheduleDowntimeAction();
+
+ /**
+ * Create tabs
+ */
+ protected function createTabs()
+ {
+ $tabs = $this->getTabs();
+ $object = $this->object;
+ if ($object->getType() === $object::TYPE_HOST) {
+ $isService = false;
+ $params = array(
+ 'host' => $object->getName()
+ );
+ if ($this->params->has('service')) {
+ $params['service'] = $this->params->get('service');
+ }
+ } else {
+ $isService = true;
+ /** @var Service $object */
+ $params = array(
+ 'host' => $object->getHost()->getName(),
+ 'service' => $object->getName()
+ );
+ }
+ $tabs->add(
+ 'host',
+ array(
+ 'title' => sprintf(
+ $this->translate('Show detailed information for host %s'),
+ $isService ? $object->getHost()->getName() : $object->getName()
+ ),
+ 'label' => $this->translate('Host'),
+ 'url' => 'monitoring/host/show',
+ 'urlParams' => $params
+ )
+ );
+ if ($isService || $this->params->has('service')) {
+ $tabs->add(
+ 'service',
+ array(
+ 'title' => sprintf(
+ $this->translate('Show detailed information for service %s on host %s'),
+ $isService ? $object->getName() : $this->params->get('service'),
+ $isService ? $object->getHost()->getName() : $object->getName()
+ ),
+ 'label' => $this->translate('Service'),
+ 'url' => 'monitoring/service/show',
+ 'urlParams' => $params
+ )
+ );
+ }
+ $tabs->add(
+ 'services',
+ array(
+ 'title' => sprintf(
+ $this->translate('List all services on host %s'),
+ $isService ? $object->getHost()->getName() : $object->getName()
+ ),
+ 'label' => $this->translate('Services'),
+ 'url' => 'monitoring/host/services',
+ 'urlParams' => $params
+ )
+ );
+ if ($this->backend->hasQuery('eventhistory')) {
+ $tabs->add(
+ 'history',
+ array(
+ 'title' => $isService
+ ? sprintf(
+ $this->translate('Show all event records of service %s on host %s'),
+ $object->getName(),
+ $object->getHost()->getName()
+ )
+ : sprintf($this->translate('Show all event records of host %s'), $object->getName())
+ ,
+ 'label' => $this->translate('History'),
+ 'url' => $isService ? 'monitoring/service/history' : 'monitoring/host/history',
+ 'urlParams' => $params
+ )
+ );
+ }
+
+ /** @var ObjectDetailsTabHook $hook */
+ foreach (Hook::all('Monitoring\\ObjectDetailsTab') as $hook) {
+ $hookName = $hook->getName();
+ if ($hook->shouldBeShown($object, $this->Auth())) {
+ $this->tabHooks[$hookName] = $hook;
+ $tabs->add($hookName, [
+ 'label' => $hook->getLabel(),
+ 'url' => $isService ? 'monitoring/service/tabhook' : 'monitoring/host/tabhook',
+ 'urlParams' => $params + [ 'hook' => $hookName ]
+ ]);
+ }
+ }
+
+ $tabs->extend(new DashboardAction())->extend(new MenuAction());
+ }
+
+ /**
+ * Create quick action forms and pass them to the view
+ */
+ protected function setupQuickActionForms()
+ {
+ $auth = $this->Auth();
+ if ($auth->hasPermission('monitoring/command/schedule-check')
+ || ($auth->hasPermission('monitoring/command/schedule-check/active-only')
+ && $this->object->active_checks_enabled
+ )
+ ) {
+ $this->view->checkNowForm = $checkNowForm = new CheckNowCommandForm();
+ $checkNowForm
+ ->setObjects($this->object)
+ ->handleRequest();
+ }
+ if (! in_array((int) $this->object->state, array(0, 99))
+ && $this->object->acknowledged
+ && $auth->hasPermission('monitoring/command/remove-acknowledgement')
+ ) {
+ $this->view->removeAckForm = $removeAckForm = new RemoveAcknowledgementCommandForm();
+ $removeAckForm
+ ->setObjects($this->object)
+ ->handleRequest();
+ }
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Helper/PluginOutputHookRenderer.php b/modules/monitoring/library/Monitoring/Web/Helper/PluginOutputHookRenderer.php
new file mode 100644
index 0000000..50b6c65
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Helper/PluginOutputHookRenderer.php
@@ -0,0 +1,105 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Helper;
+
+use Icinga\Application\Logger;
+use Icinga\Web\Hook;
+
+/**
+ * Renderer for plugin output based on hooks
+ */
+class PluginOutputHookRenderer
+{
+ /** @var array */
+ protected $commandMap = [];
+
+ /**
+ * Register PluginOutput hooks
+ *
+ * Map PluginOutput hooks to their responsible commands.
+ *
+ * @return $this
+ */
+ public function registerHooks()
+ {
+ if (! Hook::has('monitoring/PluginOutput')) {
+ return $this;
+ }
+
+ foreach (Hook::all('monitoring/PluginOutput') as $hook) {
+ /** @var \Icinga\Module\Monitoring\Hook\PluginOutputHook $hook */
+ try {
+ $commands = $hook->getCommands();
+ } catch (\Exception $e) {
+ Logger::error(
+ 'Failed to get applicable commands from hook "%s". An error occurred: %s',
+ get_class($hook),
+ $e
+ );
+
+ continue;
+ }
+
+ if (! is_array($commands)) {
+ $commands = [$commands];
+ }
+
+ foreach ($commands as $command) {
+ if (! isset($this->commandMap[$command])) {
+ $this->commandMap[$command] = [];
+ }
+
+ $this->commandMap[$command][] = $hook;
+ }
+ }
+
+ return $this;
+ }
+
+ protected function renderCommand($command, $output, $detail)
+ {
+ if (isset($this->commandMap[$command])) {
+ foreach ($this->commandMap[$command] as $hook) {
+ /** @var \Icinga\Module\Monitoring\Hook\PluginOutputHook $hook */
+
+ try {
+ $output = $hook->render($command, $output, $detail);
+ } catch (\Exception $e) {
+ Logger::error(
+ 'Failed to render plugin output from hook "%s". An error occurred: %s',
+ get_class($hook),
+ $e
+ );
+
+ continue;
+ }
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * Render the given plugin output based on the specified check command
+ *
+ * Traverse all hooks which are responsible for the specified check command and call their `render()` methods.
+ *
+ * @param string $command Check command
+ * @param string $output Plugin output
+ * @param bool $detail Whether the output is requested from the detail area
+ *
+ * @return string
+ */
+ public function render($command, $output, $detail)
+ {
+ if (empty($this->commandMap)) {
+ return $output;
+ }
+
+ $output = $this->renderCommand('*', $output, $detail);
+ $output = $this->renderCommand($command, $output, $detail);
+
+ return $output;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Hook/HostActionsHook.php b/modules/monitoring/library/Monitoring/Web/Hook/HostActionsHook.php
new file mode 100644
index 0000000..fdfe18f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Hook/HostActionsHook.php
@@ -0,0 +1,15 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Hook;
+
+use Icinga\Module\Monitoring\Hook\HostActionsHook as BaseHook;
+
+/**
+ * Compat only
+ *
+ * Please implement hooks in our Hook direcory
+ */
+abstract class HostActionsHook extends BaseHook
+{
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Hook/ServiceActionsHook.php b/modules/monitoring/library/Monitoring/Web/Hook/ServiceActionsHook.php
new file mode 100644
index 0000000..0ffbf45
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Hook/ServiceActionsHook.php
@@ -0,0 +1,15 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Hook;
+
+use Icinga\Module\Monitoring\Hook\ServiceActionsHook as BaseHook;
+
+/**
+ * Compat only
+ *
+ * Please implement hooks in our Hook direcory
+ */
+abstract class ServiceActionsHook extends BaseHook
+{
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Hook/TimelineProviderHook.php b/modules/monitoring/library/Monitoring/Web/Hook/TimelineProviderHook.php
new file mode 100644
index 0000000..f6f110f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Hook/TimelineProviderHook.php
@@ -0,0 +1,15 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Hook;
+
+use Icinga\Module\Monitoring\Hook\TimelineProviderHook as BaseHook;
+
+/**
+ * Compat only
+ *
+ * Please implement hooks in our Hook direcory
+ */
+abstract class TimelineProviderHook extends BaseHook
+{
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Navigation/Action.php b/modules/monitoring/library/Monitoring/Web/Navigation/Action.php
new file mode 100644
index 0000000..7e4ffe3
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Navigation/Action.php
@@ -0,0 +1,123 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Navigation;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Web\Navigation\NavigationItem;
+use Icinga\Module\Monitoring\Object\Macro;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use Icinga\Web\Url;
+
+/**
+ * Action for monitored objects
+ */
+class Action extends NavigationItem
+{
+ /**
+ * Whether this action's macros were already resolved
+ *
+ * @var bool
+ */
+ protected $resolved = false;
+
+ /**
+ * This action's object
+ *
+ * @var MonitoredObject
+ */
+ protected $object;
+
+ /**
+ * The filter to use when being asked whether to render this action
+ *
+ * @var string
+ */
+ protected $filter;
+
+ /**
+ * This action's raw url attribute
+ *
+ * @var string
+ */
+ protected $rawUrl;
+
+ /**
+ * Set this action's object
+ *
+ * @param MonitoredObject $object
+ *
+ * @return $this
+ */
+ public function setObject(MonitoredObject $object)
+ {
+ $this->object = $object;
+ return $this;
+ }
+
+ /**
+ * Return this action's object
+ *
+ * @return MonitoredObject
+ */
+ public function getObject()
+ {
+ return $this->object;
+ }
+
+ /**
+ * Set the filter to use when being asked whether to render this action
+ *
+ * @param string $filter
+ *
+ * @return $this
+ */
+ public function setFilter($filter)
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ /**
+ * Return the filter to use when being asked whether to render this action
+ *
+ * @return string
+ */
+ public function getFilter()
+ {
+ return $this->filter;
+ }
+
+ public function setUrl($url)
+ {
+ if (is_string($url)) {
+ $this->rawUrl = $url;
+ } else {
+ parent::setUrl($url);
+ }
+
+ return $this;
+ }
+
+ public function getUrl()
+ {
+ $url = parent::getUrl();
+ if (! $this->resolved && $url === null && $this->rawUrl !== null) {
+ $this->setUrl(Url::fromPath(Macro::resolveMacros($this->rawUrl, $this->getObject())));
+ $this->resolved = true;
+ return parent::getUrl();
+ } else {
+ return $url;
+ }
+ }
+
+ public function getRender()
+ {
+ if ($this->render === null) {
+ $filter = $this->getFilter();
+ $this->render = $filter ? Filter::fromQueryString($filter)->matches($this->getObject()) : true;
+ }
+
+ return $this->render;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Navigation/HostAction.php b/modules/monitoring/library/Monitoring/Web/Navigation/HostAction.php
new file mode 100644
index 0000000..2e950f1
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Navigation/HostAction.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Navigation;
+
+/**
+ * A host action
+ */
+class HostAction extends Action
+{
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Navigation/HostNote.php b/modules/monitoring/library/Monitoring/Web/Navigation/HostNote.php
new file mode 100644
index 0000000..2cf0cdf
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Navigation/HostNote.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Navigation;
+
+/**
+ * A host note
+ */
+class HostNote extends Action
+{
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Navigation/Renderer/MonitoringBadgeNavigationItemRenderer.php b/modules/monitoring/library/Monitoring/Web/Navigation/Renderer/MonitoringBadgeNavigationItemRenderer.php
new file mode 100644
index 0000000..054e387
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Navigation/Renderer/MonitoringBadgeNavigationItemRenderer.php
@@ -0,0 +1,171 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Navigation\Renderer;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filterable;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer;
+
+/**
+ * Render generic DataView columns as badges in menu items
+ *
+ * It is possible to configure the class of the rendered badge as option 'class', the
+ * columns to fetch using the option 'columns' and the DataView from which the columns
+ * will be fetched using the option 'dataview'.
+ */
+class MonitoringBadgeNavigationItemRenderer extends BadgeNavigationItemRenderer
+{
+ /**
+ * Cached count
+ *
+ * @var int
+ */
+ protected $count;
+
+ /**
+ * Caches the responses for all executed summaries
+ *
+ * @var array
+ */
+ protected static $summaries = array();
+
+ /**
+ * Accumulates all needed columns for a view to allow fetching the needed columns in
+ * one single query
+ *
+ * @var array
+ */
+ protected static $dataViews = array();
+
+ /**
+ * The dataview referred to by the navigation item
+ *
+ * @var string
+ */
+ protected $dataView;
+
+ /**
+ * The columns and titles displayed in the badge
+ *
+ * @var array
+ */
+ protected $columns;
+
+ /**
+ * Set the dataview referred to by the navigation item
+ *
+ * @param string $dataView
+ *
+ * @return $this
+ */
+ public function setDataView($dataView)
+ {
+ $this->dataView = $dataView;
+ return $this;
+ }
+
+ /**
+ * Return the dataview referred to by the navigation item
+ *
+ * @return string
+ */
+ public function getDataView()
+ {
+ return $this->dataView;
+ }
+
+ /**
+ * Set the columns and titles displayed in the badge
+ *
+ * @param array $columns
+ *
+ * @return $this
+ */
+ public function setColumns(array $columns)
+ {
+ $this->columns = $columns;
+ return $this;
+ }
+
+ /**
+ * Return the columns and titles displayed in the badge
+ *
+ * @return array
+ */
+ public function getColumns()
+ {
+ return $this->columns;
+ }
+
+ /**
+ * Apply a restriction on the given data view
+ *
+ * @param string $restriction The name of restriction
+ * @param Filterable $filterable The filterable to restrict
+ *
+ * @return Filterable The filterable
+ */
+ protected static function applyRestriction($restriction, Filterable $filterable)
+ {
+ $restrictions = Filter::matchAny();
+ foreach (Auth::getInstance()->getRestrictions($restriction) as $filter) {
+ if ($filter === '*') {
+ $filterable->addFilter(Filter::matchAll());
+ return $filterable;
+ }
+ $restrictions->addFilter(Filter::fromQueryString($filter));
+ }
+ $filterable->applyFilter($restrictions);
+ return $filterable;
+ }
+
+ /**
+ * Fetch the dataview from the database
+ *
+ * @return object
+ */
+ protected function fetchDataView()
+ {
+ $summary = MonitoringBackend::instance()->select()->from(
+ $this->getDataView(),
+ array_keys($this->getColumns())
+ );
+ static::applyRestriction('monitoring/filter/objects', $summary);
+ return $summary->fetchRow();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCount()
+ {
+ if ($this->count === null) {
+ try {
+ $summary = $this->fetchDataView();
+ } catch (Exception $e) {
+ Logger::debug($e);
+ $this->count = 1;
+ $this->state = static::STATE_UNKNOWN;
+ $this->title = $e->getMessage();
+ return $this->count;
+ }
+ $count = 0;
+ $titles = array();
+ foreach ($this->getColumns() as $column => $title) {
+ if (isset($summary->$column) && $summary->$column > 0) {
+ $titles[] = sprintf($title, $summary->$column);
+ $count += $summary->$column;
+ }
+ }
+ $this->count = $count;
+ $this->title = implode('. ', $titles);
+ }
+
+ return $this->count;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Navigation/ServiceAction.php b/modules/monitoring/library/Monitoring/Web/Navigation/ServiceAction.php
new file mode 100644
index 0000000..a88e94f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Navigation/ServiceAction.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Navigation;
+
+/**
+ * A service action
+ */
+class ServiceAction extends Action
+{
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Navigation/ServiceNote.php b/modules/monitoring/library/Monitoring/Web/Navigation/ServiceNote.php
new file mode 100644
index 0000000..4858bf5
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Navigation/ServiceNote.php
@@ -0,0 +1,11 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Navigation;
+
+/**
+ * A service note
+ */
+class ServiceNote extends Action
+{
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Rest/RestRequest.php b/modules/monitoring/library/Monitoring/Web/Rest/RestRequest.php
new file mode 100644
index 0000000..fcbe0ca
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Rest/RestRequest.php
@@ -0,0 +1,297 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Rest;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Util\Json;
+use Icinga\Module\Monitoring\Exception\CurlException;
+
+/**
+ * REST Request
+ */
+class RestRequest
+{
+ /**
+ * Request URI
+ *
+ * @var string
+ */
+ protected $uri;
+
+ /**
+ * Request method
+ *
+ * @var string
+ */
+ protected $method;
+
+ /**
+ * Request content type
+ *
+ * @var string
+ */
+ protected $contentType;
+
+ /**
+ * Whether to authenticate with basic auth
+ *
+ * @var bool
+ */
+ protected $hasBasicAuth;
+
+ /**
+ * Auth username
+ *
+ * @var string
+ */
+ protected $username;
+
+ /**
+ * Auth password
+ *
+ * @var string
+ */
+ protected $password;
+
+ /**
+ * Request payload
+ *
+ * @var mixed
+ */
+ protected $payload;
+
+ /**
+ * Whether strict SSL is enabled
+ *
+ * @var bool
+ */
+ protected $strictSsl = true;
+
+ /**
+ * Request timeout
+ *
+ * @var int
+ */
+ protected $timeout = 30;
+
+ /**
+ * Create a GET REST request
+ *
+ * @param string $uri
+ *
+ * @return static
+ */
+ public static function get($uri)
+ {
+ $request = new static;
+ $request->uri = $uri;
+ $request->method = 'GET';
+ return $request;
+ }
+
+ /**
+ * Create a POST REST request
+ *
+ * @param string $uri
+ *
+ * @return static
+ */
+ public static function post($uri)
+ {
+ $request = new static;
+ $request->uri = $uri;
+ $request->method = 'POST';
+ return $request;
+ }
+
+ /**
+ * Send content type JSON
+ *
+ * @return $this
+ */
+ public function sendJson()
+ {
+ $this->contentType = 'application/json';
+
+ return $this;
+ }
+
+ /**
+ * Set basic auth credentials
+ *
+ * @param string $username
+ * @param string $password
+ *
+ * @return $this
+ */
+ public function authenticateWith($username, $password)
+ {
+ $this->hasBasicAuth = true;
+ $this->username = $username;
+ $this->password = $password;
+
+ return $this;
+ }
+
+ /**
+ * Set request payload
+ *
+ * @param mixed $payload
+ *
+ * @return $this
+ */
+ public function setPayload($payload)
+ {
+ $this->payload = $payload;
+
+ return $this;
+ }
+
+ /**
+ * Disable strict SSL
+ *
+ * @return $this
+ */
+ public function noStrictSsl()
+ {
+ $this->strictSsl = false;
+
+ return $this;
+ }
+
+ /**
+ * Serialize payload according to content type
+ *
+ * @param mixed $payload
+ * @param string $contentType
+ *
+ * @return string
+ */
+ public function serializePayload($payload, $contentType)
+ {
+ switch ($contentType) {
+ case 'application/json':
+ $payload = Json::encode($payload);
+ break;
+ }
+
+ return $payload;
+ }
+
+ /**
+ * Send the request
+ *
+ * @return mixed
+ *
+ * @throws Exception
+ */
+ public function send()
+ {
+ $defaults = array(
+ 'host' => 'localhost',
+ 'path' => '/'
+ );
+
+ $url = array_merge($defaults, parse_url($this->uri));
+
+ if (isset($url['port'])) {
+ $url['host'] .= sprintf(':%u', $url['port']);
+ }
+
+ if (isset($url['query'])) {
+ $url['path'] .= sprintf('?%s', $url['query']);
+ }
+
+ $headers = array(
+ "{$this->method} {$url['path']} HTTP/1.1",
+ "Host: {$url['host']}",
+ "Content-Type: {$this->contentType}",
+ 'Accept: application/json',
+ // Bypass "Expect: 100-continue" timeouts
+ 'Expect:'
+ );
+
+ $options = array(
+ CURLOPT_URL => $this->uri,
+ CURLOPT_TIMEOUT => $this->timeout,
+ // Ignore proxy settings
+ CURLOPT_PROXY => '',
+ CURLOPT_CUSTOMREQUEST => $this->method
+ );
+
+ // Record cURL command line for debugging
+ $curlCmd = array('curl', '-s', '-X', $this->method, '-H', escapeshellarg('Accept: application/json'));
+
+ if ($this->strictSsl) {
+ $options[CURLOPT_SSL_VERIFYHOST] = 2;
+ $options[CURLOPT_SSL_VERIFYPEER] = true;
+ } else {
+ $options[CURLOPT_SSL_VERIFYHOST] = false;
+ $options[CURLOPT_SSL_VERIFYPEER] = false;
+ $curlCmd[] = '-k';
+ }
+
+ if ($this->hasBasicAuth) {
+ $options[CURLOPT_USERPWD] = sprintf('%s:%s', $this->username, $this->password);
+ $curlCmd[] = sprintf('-u %s:%s', escapeshellarg($this->username), escapeshellarg($this->password));
+ }
+
+ if (! empty($this->payload)) {
+ $payload = $this->serializePayload($this->payload, $this->contentType);
+ $options[CURLOPT_POSTFIELDS] = $payload;
+ $curlCmd[] = sprintf('-d %s', escapeshellarg($payload));
+ }
+
+ $options[CURLOPT_HTTPHEADER] = $headers;
+
+ $stream = null;
+ $logger = Logger::getInstance();
+ if ($logger !== null && $logger->getLevel() === Logger::DEBUG) {
+ $stream = fopen('php://temp', 'w');
+ $options[CURLOPT_VERBOSE] = true;
+ $options[CURLOPT_STDERR] = $stream;
+ }
+
+ Logger::debug(
+ 'Executing %s %s',
+ implode(' ', $curlCmd),
+ escapeshellarg($this->uri)
+ );
+
+ $result = $this->curlExec($options);
+
+ if (is_resource($stream)) {
+ rewind($stream);
+ Logger::debug(stream_get_contents($stream));
+ fclose($stream);
+ }
+
+ return Json::decode($result, true);
+ }
+
+ /**
+ * Set up a new cURL handle with the given options and call {@link curl_exec()}
+ *
+ * @param array $options
+ *
+ * @return string The response
+ *
+ * @throws CurlException
+ */
+ protected function curlExec(array $options)
+ {
+ $ch = curl_init();
+ $options[CURLOPT_RETURNTRANSFER] = true;
+ curl_setopt_array($ch, $options);
+ $result = curl_exec($ch);
+
+ if ($result === false) {
+ throw new CurlException('%s', curl_error($ch));
+ }
+
+ curl_close($ch);
+ return $result;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php b/modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php
new file mode 100644
index 0000000..b4273b5
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php
@@ -0,0 +1,272 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Widget;
+
+use Icinga\Module\Monitoring\Hook\CustomVarRendererHook;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\Web\Widget\Icon;
+use Closure;
+
+class CustomVarTable extends BaseHtmlElement
+{
+ /** @var iterable The variables */
+ protected $data;
+
+ /** @var ?MonitoredObject The object the variables are bound to */
+ protected $object;
+
+ /** @var Closure Callback to apply hooks */
+ protected $hookApplier;
+
+ /** @var array The groups as identified by hooks */
+ protected $groups = [];
+
+ /** @var string Header title */
+ protected $headerTitle;
+
+ /** @var int The nesting level */
+ protected $level = 0;
+
+ protected $tag = 'table';
+
+ /** @var HtmlElement The table body */
+ protected $body;
+
+ protected $defaultAttributes = [
+ 'class' => ['custom-var-table', 'name-value-table']
+ ];
+
+ /**
+ * Create a new CustomVarTable
+ *
+ * @param iterable $data
+ * @param ?MonitoredObject $object
+ */
+ public function __construct($data, MonitoredObject $object = null)
+ {
+ $this->data = $data;
+ $this->object = $object;
+ $this->body = new HtmlElement('tbody');
+ }
+
+ /**
+ * Set the header to show
+ *
+ * @param string $title
+ *
+ * @return $this
+ */
+ protected function setHeader($title)
+ {
+ $this->headerTitle = (string) $title;
+
+ return $this;
+ }
+
+ /**
+ * Add a new row to the body
+ *
+ * @param mixed $name
+ * @param mixed $value
+ *
+ * @return void
+ */
+ protected function addRow($name, $value)
+ {
+ $this->body->addHtml(new HtmlElement(
+ 'tr',
+ Attributes::create(['class' => "level-{$this->level}"]),
+ new HtmlElement('th', null, Html::wantHtml($name)),
+ new HtmlElement('td', null, Html::wantHtml($value))
+ ));
+ }
+
+ /**
+ * Render a variable
+ *
+ * @param mixed $name
+ * @param mixed $value
+ *
+ * @return void
+ */
+ protected function renderVar($name, $value)
+ {
+ if ($this->object !== null && $this->level === 0) {
+ list($name, $value, $group) = call_user_func($this->hookApplier, $name, $value);
+ if ($group !== null) {
+ $this->groups[$group][] = [$name, $value];
+ return;
+ }
+ }
+
+ $isArray = is_array($value);
+ if (! $isArray && $value instanceof \stdClass) {
+ $value = (array) $value;
+ $isArray = true;
+ }
+
+ switch (true) {
+ case $isArray && is_int(key($value)):
+ $this->renderArray($name, $value);
+ break;
+ case $isArray:
+ $this->renderObject($name, $value);
+ break;
+ default:
+ $this->renderScalar($name, $value);
+ }
+ }
+
+ /**
+ * Render an array
+ *
+ * @param mixed $name
+ * @param array $array
+ *
+ * @return void
+ */
+ protected function renderArray($name, array $array)
+ {
+ $numItems = count($array);
+ $name = (new HtmlDocument())->addHtml(
+ Html::wantHtml($name),
+ Text::create(' (Array)')
+ );
+
+ $this->addRow($name, sprintf(tp('%d item', '%d items', $numItems), $numItems));
+
+ ++$this->level;
+
+ ksort($array);
+ foreach ($array as $key => $value) {
+ $this->renderVar("[$key]", $value);
+ }
+
+ --$this->level;
+ }
+
+ /**
+ * Render an object (associative array)
+ *
+ * @param mixed $name
+ * @param array $object
+ *
+ * @return void
+ */
+ protected function renderObject($name, array $object)
+ {
+ $numItems = count($object);
+ $this->addRow($name, sprintf(tp('%d item', '%d items', $numItems), $numItems));
+
+ ++$this->level;
+
+ ksort($object);
+ foreach ($object as $key => $value) {
+ $this->renderVar($key, $value);
+ }
+
+ --$this->level;
+ }
+
+ /**
+ * Render a scalar
+ *
+ * @param mixed $name
+ * @param mixed $value
+ *
+ * @return void
+ */
+ protected function renderScalar($name, $value)
+ {
+ if ($value === '') {
+ $value = new HtmlElement('span', Attributes::create(['class' => 'empty']), Text::create(t('empty string')));
+ }
+
+ $this->addRow($name, $value);
+ }
+
+ /**
+ * Render a group
+ *
+ * @param string $name
+ * @param iterable $entries
+ *
+ * @return void
+ */
+ protected function renderGroup($name, $entries)
+ {
+ $table = new self($entries);
+
+ /** @var HtmlDocument $wrapper */
+ $wrapper = $this->getWrapper();
+ if ($wrapper === null) {
+ $wrapper = new HtmlDocument();
+ $wrapper->addHtml($this);
+ $this->prependWrapper($wrapper);
+ }
+
+ $wrapper->addHtml($table->setHeader($name));
+ }
+
+ protected function assemble()
+ {
+ if ($this->object !== null) {
+ $this->hookApplier = CustomVarRendererHook::prepareForObject($this->object);
+ }
+
+ if ($this->headerTitle !== null) {
+ $this->getAttributes()
+ ->add('class', 'collapsible')
+ ->add('data-visible-height', 100)
+ ->add('data-toggle-element', 'thead')
+ ->add(
+ 'id',
+ preg_replace('/\s+/', '-', strtolower($this->headerTitle)) . '-customvars'
+ );
+
+ $this->addHtml(new HtmlElement('thead', null, new HtmlElement(
+ 'tr',
+ null,
+ new HtmlElement(
+ 'th',
+ Attributes::create(['colspan' => 2]),
+ new HtmlElement(
+ 'span',
+ null,
+ new Icon('angle-right'),
+ new Icon('angle-down')
+ ),
+ Text::create($this->headerTitle)
+ )
+ )));
+ }
+
+ if (is_array($this->data)) {
+ ksort($this->data);
+ }
+
+ foreach ($this->data as $name => $value) {
+ $this->renderVar($name, $value);
+ }
+
+ $this->addHtml($this->body);
+
+ // Hooks can return objects as replacement for keys, hence a generator is needed for group entries
+ $genGenerator = function ($entries) {
+ foreach ($entries as list($key, $value)) {
+ yield $key => $value;
+ }
+ };
+
+ foreach ($this->groups as $group => $entries) {
+ $this->renderGroup($group, $genGenerator($entries));
+ }
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Widget/SelectBox.php b/modules/monitoring/library/Monitoring/Web/Widget/SelectBox.php
new file mode 100644
index 0000000..48b98ac
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Widget/SelectBox.php
@@ -0,0 +1,120 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Widget;
+
+use Icinga\Web\Form;
+use Icinga\Web\Request;
+use Icinga\Web\Widget\AbstractWidget;
+
+class SelectBox extends AbstractWidget
+{
+ /**
+ * The name of the form that will be created
+ *
+ * @var string
+ */
+ private $name;
+
+ /**
+ * An array containing all intervals with their associated labels
+ *
+ * @var array
+ */
+ private $values;
+
+ /**
+ * The label displayed next to the select box
+ *
+ * @var string
+ */
+ private $label;
+
+ /**
+ * The name of the url parameter to set
+ *
+ * @var string
+ */
+ private $parameter;
+
+ /**
+ * A request object used for initial form population
+ *
+ * @var Request
+ */
+ private $request;
+
+ /**
+ * Create a TimelineIntervalBox
+ *
+ * @param string $name The name of the form that will be created
+ * @param array $values An array containing all intervals with their associated labels
+ * @param string $label The label displayed next to the select box
+ * @param string $param The request parameter name to set
+ */
+ public function __construct($name, array $values, $label = 'Select', $param = 'selection')
+ {
+ $this->name = $name;
+ $this->values = $values;
+ $this->label = $label;
+ $this->parameter = $param;
+ }
+
+ /**
+ * Apply the parameters from the given request on this widget
+ *
+ * @param Request $request The request to use for populating the form
+ */
+ public function applyRequest(Request $request)
+ {
+ $this->request = $request;
+ }
+
+ /**
+ * Return the chosen interval value or null
+ *
+ * @param Request $request The request to fetch the value from
+ *
+ * @return string|null
+ */
+ public function getInterval(Request $request = null)
+ {
+ if ($request === null && $this->request) {
+ $request = $this->request;
+ }
+
+ if ($request) {
+ return $request->getParam('interval');
+ }
+ }
+
+ /**
+ * Renders this widget and returns the HTML as a string
+ *
+ * @return string
+ */
+ public function render()
+ {
+ $form = new Form();
+ $form->setAttrib('class', Form::DEFAULT_CLASSES . ' inline');
+ $form->setMethod('GET');
+ $form->setUidDisabled();
+ $form->setTokenDisabled();
+ $form->setName($this->name);
+ $form->addElement(
+ 'select',
+ $this->parameter,
+ array(
+ 'label' => $this->label,
+ 'multiOptions' => $this->values,
+ 'autosubmit' => true
+ )
+ );
+
+ if ($this->request) {
+ $form->populate($this->request->getParams());
+ }
+
+ return $form;
+ }
+}
diff --git a/modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php b/modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php
new file mode 100644
index 0000000..fdaac51
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php
@@ -0,0 +1,341 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Web\Widget;
+
+use Icinga\Web\Form;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Navigation\NavigationItem;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\AbstractWidget;
+use Icinga\Data\Filter\Filter;
+
+class StateBadges extends AbstractWidget
+{
+ /**
+ * CSS class for the widget
+ *
+ * @var string
+ */
+ const CSS_CLASS = 'state-badges';
+
+ /**
+ * State critical
+ *
+ * @var string
+ */
+ const STATE_CRITICAL = 'state-critical';
+
+ /**
+ * State critical handled
+ *
+ * @var string
+ */
+ const STATE_CRITICAL_HANDLED = 'state-critical handled';
+
+ /**
+ * State down
+ *
+ * @var string
+ */
+ const STATE_DOWN = 'state-down';
+
+ /**
+ * State down handled
+ *
+ * @var string
+ */
+ const STATE_DOWN_HANDLED = 'state-down handled';
+
+ /**
+ * State ok
+ *
+ * @var string
+ */
+ const STATE_OK = 'state-ok';
+
+ /**
+ * State pending
+ *
+ * @var string
+ */
+ const STATE_PENDING = 'state-pending';
+
+ /**
+ * State unknown
+ *
+ * @var string
+ */
+ const STATE_UNKNOWN = 'state-unknown';
+
+ /**
+ * State unknown handled
+ *
+ * @var string
+ */
+ const STATE_UNKNOWN_HANDLED = 'state-unknown handled';
+
+ /**
+ * State unreachable
+ *
+ * @var string
+ */
+ const STATE_UNREACHABLE = 'state-unreachable';
+
+ /**
+ * State unreachable handled
+ *
+ * @var string
+ */
+ const STATE_UNREACHABLE_HANDLED = 'state-unreachable handled';
+
+ /**
+ * State up
+ *
+ * @var string
+ */
+ const STATE_UP = 'state-up';
+
+ /**
+ * State warning
+ *
+ * @var string
+ */
+ const STATE_WARNING = 'state-warning';
+
+ /**
+ * State warning handled
+ *
+ * @var string
+ */
+ const STATE_WARNING_HANDLED = 'state-warning handled';
+
+ /**
+ * State badges
+ *
+ * @var object[]
+ */
+ protected $badges = array();
+
+ /**
+ * Internal counter for badge priorities
+ *
+ * @var int
+ */
+ protected $priority = 1;
+
+ /**
+ * The base filter applied to any badge link
+ *
+ * @var Filter
+ */
+ protected $baseFilter;
+
+ /**
+ * Base URL
+ *
+ * @var Url
+ */
+ protected $url;
+
+ /**
+ * Get the base URL
+ *
+ * @return Url
+ */
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * Set the base URL
+ *
+ * @param Url|string $url
+ *
+ * @return $this
+ */
+ public function setUrl($url)
+ {
+ if (! $url instanceof $url) {
+ $url = Url::fromPath($url);
+ }
+ $this->url = $url;
+ return $this;
+ }
+
+ /**
+ * Get the base filter
+ *
+ * @return Filter
+ */
+ public function getBaseFilter()
+ {
+ return $this->baseFilter;
+ }
+
+ /**
+ * Set the base filter
+ *
+ * @param Filter $baseFilter
+ *
+ * @return $this
+ */
+ public function setBaseFilter($baseFilter)
+ {
+ $this->baseFilter = $baseFilter;
+ return $this;
+ }
+
+ /**
+ * Add a state badge
+ *
+ * @param string $state
+ * @param int $count
+ * @param array $filter
+ * @param string $translateSingular
+ * @param string $translatePlural
+ * @param array $translateArgs
+ *
+ * @return $this
+ */
+ public function add(
+ $state,
+ $count,
+ array $filter,
+ $translateSingular,
+ $translatePlural,
+ array $translateArgs = array()
+ ) {
+ $this->badges[$state] = (object) array(
+ 'count' => (int) $count,
+ 'filter' => $filter,
+ 'translateArgs' => $translateArgs,
+ 'translatePlural' => $translatePlural,
+ 'translateSingular' => $translateSingular
+ );
+ return $this;
+ }
+
+ /**
+ * Create a badge
+ *
+ * @param string $state
+ * @param Navigation $badges
+ *
+ * @return $this
+ */
+ public function createBadge($state, Navigation $badges)
+ {
+ if ($this->has($state)) {
+ $badge = $this->get($state);
+ $url = clone $this->url->setParams($badge->filter);
+ if (isset($this->baseFilter)) {
+ $url->addFilter($this->baseFilter);
+ }
+ $badges->addItem(new NavigationItem($state, array(
+ 'attributes' => array('class' => 'badge ' . $state),
+ 'label' => $badge->count,
+ 'priority' => $this->priority++,
+ 'title' => vsprintf(
+ mtp('monitoring', $badge->translateSingular, $badge->translatePlural, $badge->count),
+ $badge->translateArgs
+ ),
+ 'url' => $url
+ )));
+ }
+ return $this;
+ }
+
+ /**
+ * Create a badge group
+ *
+ * @param array $states
+ * @param Navigation $badges
+ *
+ * @return $this
+ */
+ public function createBadgeGroup(array $states, Navigation $badges)
+ {
+ $group = array_intersect_key($this->badges, array_flip($states));
+ if (! empty($group)) {
+ $groupItem = new NavigationItem(
+ uniqid(),
+ array(
+ 'cssClass' => 'state-badge-group',
+ 'label' => '',
+ 'priority' => $this->priority++
+ )
+ );
+ $groupBadges = new Navigation();
+ $groupBadges->setLayout(Navigation::LAYOUT_TABS);
+ foreach (array_keys($group) as $state) {
+ $this->createBadge($state, $groupBadges);
+ }
+ $groupItem->setChildren($groupBadges);
+ $badges->addItem($groupItem);
+ }
+ return $this;
+ }
+
+ /**
+ * Get whether a badge for the given state has been added
+ *
+ * @param string $state
+ *
+ * @return bool
+ */
+ public function has($state)
+ {
+ return isset($this->badges[$state]) && $this->badges[$state]->count;
+ }
+
+ /**
+ * Get the badge for the given state
+ *
+ * @param string $state
+ *
+ * @return object
+ */
+ public function get($state)
+ {
+ return $this->badges[$state];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render()
+ {
+ $badges = new Navigation();
+ $badges->setLayout(Navigation::LAYOUT_TABS);
+ $this
+ ->createBadgeGroup(
+ array(static::STATE_CRITICAL, static::STATE_CRITICAL_HANDLED),
+ $badges
+ )
+ ->createBadgeGroup(
+ array(static::STATE_DOWN, static::STATE_DOWN_HANDLED),
+ $badges
+ )
+ ->createBadgeGroup(
+ array(static::STATE_WARNING, static::STATE_WARNING_HANDLED),
+ $badges
+ )
+ ->createBadgeGroup(
+ array(static::STATE_UNREACHABLE, static::STATE_UNREACHABLE_HANDLED),
+ $badges
+ )
+ ->createBadgeGroup(
+ array(static::STATE_UNKNOWN, static::STATE_UNKNOWN_HANDLED),
+ $badges
+ )
+ ->createBadge(static::STATE_OK, $badges)
+ ->createBadge(static::STATE_UP, $badges)
+ ->createBadge(static::STATE_PENDING, $badges);
+ return $badges
+ ->getRenderer()
+ ->setCssClass(static::CSS_CLASS)
+ ->render();
+ }
+}
diff --git a/modules/monitoring/module.info b/modules/monitoring/module.info
new file mode 100644
index 0000000..7e098c4
--- /dev/null
+++ b/modules/monitoring/module.info
@@ -0,0 +1,5 @@
+Module: monitoring
+Version: 2.12.1
+Description: Icinga monitoring module
+ IDO accessor and UI for your monitoring. This is the initial instalment for a
+ graphical presentation of Icinga environments. The predecessor of Icinga DB.
diff --git a/modules/monitoring/public/css/event-grid.less b/modules/monitoring/public/css/event-grid.less
new file mode 100644
index 0000000..45c4188
--- /dev/null
+++ b/modules/monitoring/public/css/event-grid.less
@@ -0,0 +1,9 @@
+.event-grid {
+ width: 33.5em;
+
+ .vertical {
+ display: inline-block;
+ vertical-align: top;
+ margin: 0.5em;
+ }
+}
diff --git a/modules/monitoring/public/css/module.less b/modules/monitoring/public/css/module.less
new file mode 100644
index 0000000..5755f1f
--- /dev/null
+++ b/modules/monitoring/public/css/module.less
@@ -0,0 +1,1922 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+.monitoring-statusbar {
+ position: relative;
+ background-color: @body-bg-color;
+ border-top: 1px solid @gray-lighter;
+ padding: .25em @gutter;
+ line-height: 1.3;
+
+ .services-summary,
+ .hosts-summary {
+ float: right;
+ margin-bottom: 0;
+ }
+
+ .selection-info {
+ float: left;
+ margin-top: 0.182em;
+ }
+}
+
+// Hostgroup- and servicegroup-grid styles
+
+.grid-toggle-link {
+ display: inline-block;
+ margin-left: 1em;
+ text-decoration: none;
+ vertical-align: middle;
+
+ > i {
+ font-size: 1.25em;
+
+ &.-active {
+ color: @icinga-blue;
+ }
+
+ &.-inactive {
+ color: @gray-light;
+ }
+ }
+}
+
+.group-grid {
+ display: grid;
+ grid-gap: 1em 3em;
+ grid-template-columns: repeat(auto-fit, 14em);
+
+ .group-grid-cell > a:last-child {
+ display: inline-block;
+ max-width: 10em;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ text-align: center;
+ vertical-align: middle;
+ }
+
+ .group-grid-cell > a:first-child,
+ .group-grid-cell > div.state-none {
+ .bg-stateful();
+ .rounded-corners();
+
+ display: inline-block;
+ margin-right: 1em;
+ padding: .5em;
+ height: 2.5em;
+ width: 2.5em;
+ text-align: center;
+ vertical-align: middle;
+ color: white;
+ }
+ .group-grid-cell > div.state-none {
+ background-color: @gray-light;
+ }
+}
+
+// Styles for the icon displayed if a check result is late
+.check-result-late {
+ &:before {
+ // Remove right margin because the check now form may be displayed right next to the icon and we already have a gap
+ // because of inline-blocks
+ margin-right: 0;
+ }
+}
+
+// Show more and load more links in overviews
+.action-links {
+ text-align: right;
+}
+
+.actions .nav {
+ li > a,
+ li > span {
+ display: inline-block;
+ }
+}
+
+// State summary badges
+.state-badges {
+ display: inline-block;
+ vertical-align: middle;
+
+ > ul > li {
+ padding-right: @vertical-padding;
+
+ &:last-child {
+ padding-right: 0;
+ }
+ }
+
+ .state-badge-group li {
+ margin-right: 1px;
+ }
+
+ .state-badge-group li:last-child {
+ margin-right: 0;
+ }
+
+ .state-badge-group .badge {
+ border-radius: 0;
+ }
+
+ .state-badge-group li:first-child > .badge {
+ border-top-left-radius: 0.4em;
+ border-bottom-left-radius: 0.4em;
+ }
+
+ .state-badge-group li:last-child > .badge {
+ border-top-right-radius: 0.4em;
+ border-bottom-right-radius: 0.4em;
+ }
+}
+
+// Performance data pie charts
+.inline-pie {
+ display: inline-block;
+ height: 14/12em;
+ margin-right: 0.1em;
+ position: relative;
+ top: 0.1em;
+ width: 14/12em;
+}
+
+// Host and service summaries in detail and list views
+.hosts-summary,
+.services-summary {
+ display: inline-block;
+ margin-bottom: 0.5em;
+
+ > .hosts-link,
+ > .services-link,
+ > .state-badges {
+ vertical-align: middle;
+ }
+}
+
+.service-on {
+ color: @text-color-light;
+
+ > a {
+ color: @text-color;
+ letter-spacing: normal;
+ font-weight: bold;
+ }
+}
+
+// State table in the host and service multi-selection and detail views
+.host-detail-state,
+.service-detail-state {
+ margin-bottom: 0.5em;
+}
+
+.grid {
+ .hosts-summary,
+ .services-summary {
+ float: left;
+ }
+}
+
+// Quick actions
+.quick-actions {
+ margin: 0 -.5em;
+
+ &:last-child {
+ margin-bottom: -.25em;
+ }
+
+ li {
+ color: @icinga-blue;
+ }
+
+ a,
+ button {
+ .rounded-corners();
+ padding: .25em .5em;
+
+ &:hover {
+ background-color: @gray-lighter;
+ text-decoration: none;
+ }
+ }
+}
+
+/* Generic box element */
+
+.boxview > div.box {
+ text-align: center;
+ vertical-align: top;
+ display: inline-block;
+ padding: 20px;
+}
+
+
+
+/* Box body of contents */
+
+.boxview div.box.contents {
+ padding-top: 20px;
+}
+
+.boxview div.box.contents table {
+ width: 100%;
+}
+
+.boxview div.box.contents td {
+ vertical-align: top;
+}
+
+/* Box entry */
+
+/* Any line of a box entry */
+.boxview div.box.entry a {
+ display: block;
+}
+
+.boxview div.box.badge {
+ padding: 5px;
+}
+
+
+/* First line of a box entry */
+.boxview div.box.entry a:first-child {
+}
+
+/* End of generic box element */
+
+/* Tactical overview element styles */
+
+.tactical > .boxview > div.box {
+ min-height: 45em;
+ padding: 0px;
+}
+
+.tactical div.box.header {
+ margin: 10px;
+ min-height: 8em;
+ color: @text-color-inverted;
+ font-size: @font-size-dashboard;
+}
+
+.tactical div.box.badge {
+ border-radius: 0.0em;
+}
+
+div.box.ok_hosts.state_up {
+ background-color: @color-ok;
+ border: 1px solid white;
+}
+
+div.box.problem_hosts.state_down {
+ background-color: @color-critical;
+ border: 1px solid white;
+}
+
+div.box.ok_hosts div.box.entry, div.box.problem_hosts div.box.entry {
+ min-width: 8em;
+ min-height: 4em;
+}
+
+.tactical div.box.contents {
+ background-color: white;
+ min-height: 13em;
+ font-size: @font-size-dashboard-small;
+ text-align: left;
+}
+
+div.box.monitoringfeatures {
+ border: 1px solid @gray-lighter;
+ border-left: 15px @gray;
+}
+
+div.box.monitoringfeatures div.box-separator {
+ color: white;
+ background-color: @color-ok;
+}
+
+div.box.monitoringfeatures div.feature-highlight {
+ background-color: @color-critical;
+}
+
+div.box.monitoringfeatures a.feature-highlight {
+}
+
+div.box.hostservicechecks {
+ border: 1px solid @gray-lighter;
+ border-left: 15px @gray;
+}
+
+div.box.hostservicechecks th {
+ padding-bottom: 20px;
+}
+
+/* Monitoring health - PROCESS - element styles */
+
+div.box.process {
+ width: 100%;
+ max-width: 50em;
+ border: 1px solid @gray-lighter;
+ border-left: 15px @gray;
+ margin-bottom: 1em;
+ margin-right: 1em;
+}
+
+.process div.box.header {
+ min-height: 5em;
+ border-bottom: 1px solid @gray-lighter;
+}
+
+.process > .boxview > div.box {
+ min-height: 30em;
+}
+
+.process h2 {
+ margin-top: 0;
+ margin-bottom: 1em;
+ padding-bottom: 1em;
+ border-bottom: 1px solid @gray-lighter;
+}
+
+.process th {
+ width: 50%;
+ text-align: right;
+}
+
+.process td {
+ width: 50%;
+ padding-left: 2em;
+ text-align: left;
+}
+
+div.backend-running {
+ background: @color-ok;
+ color: white;
+ text-align: center;
+ margin-top: 1em;
+ padding: 0.5em;
+
+ &.span {
+ color: white;
+ }
+}
+
+div.backend-not-running {
+ background: @color-critical;
+ color: white;
+ text-align: center;
+ padding: 0.1em;
+}
+
+
+/* Monitoring health - FEATURE - element styles */
+
+div.box.features {
+ width: 100%;
+ max-width: 50em;
+ border: 1px solid @gray-lighter;
+ border-left: 15px @gray;
+}
+
+.features div.box.header {
+ min-height: 5em;
+ border-bottom: 1px solid @gray-lighter;
+}
+
+.features > .boxview > div.box {
+ min-height: 30em;
+}
+
+.features h2 {
+ margin-top: 0;
+ margin-bottom: 1em;
+ padding-bottom: 1em;
+ border-bottom: 1px solid @gray-lighter;
+}
+
+
+/* Monitoring health - STATS - element styles */
+
+div.box.stats {
+ width: 100%;
+ max-width: 50em;
+ border: 1px solid @gray-lighter;
+ border-left: 15px @gray;
+ color: @text-color;
+}
+
+.stats > .boxview > div.box {
+ min-height: 30em;
+}
+
+.stats > .name-value-table {
+ table-layout: fixed;
+ text-align: left;
+}
+
+.stats > table > thead {
+ color: @gray;
+}
+
+.stats > h2 {
+ text-align: left;
+ border-bottom: 1px solid @gray-lighter;
+
+ > .hosts-summary,
+ > .services-summary {
+ width: 100%;
+ > .state-badges {
+ float: right;
+ }
+ }
+}
+
+.tinystatesummary .badge {
+ font-weight: normal;
+}
+
+/* Monitoring timeline styles */
+
+div.timeline-legend {
+ padding: 0.5em;
+ margin-top: 2em;
+ border: 1px solid @gray-lighter;
+ border-left-width: 15px;
+
+ h2 {
+ margin: 0;
+ margin-left: 0.5em;
+ line-height: 1.1em;
+ }
+
+ & > span {
+ display: inline-block;
+ padding: 0.5em;
+ margin: 0.5em;
+
+ span {
+ white-space: nowrap;
+ min-width: 25px;
+ font-family: tahoma, verdana, sans-serif;
+ font-weight: @font-weight-bold;
+ font-size: 11px;
+ text-align: center;
+ color: @text-color-inverted;
+ padding-left: 5px;
+ padding-right: 5px;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ }
+ }
+}
+
+div.timeline {
+ div.timeframe {
+ height: 7em;
+ margin-bottom: 1em;
+ clear: left;
+
+ span {
+ width: 8em;
+ margin-top: 2.3em;
+ margin-right: 1.5em;
+ display: block;
+ float: left;
+ text-align: center;
+
+ a {
+ font-weight: bold;
+ white-space: nowrap;
+ }
+ }
+
+ div.circle-box {
+ // width: inline-style;
+ height: 100%;
+ margin-right: 0.5em;
+ position: relative;
+ float: left;
+
+ div.outer-circle {
+ // width: inline-style;
+ // height: inline-style;
+ position: absolute;
+ top: 50%;
+ // margin-top: inline-style;
+
+ &.extrapolated {
+ border-width: 2px;
+ border-style: dotted;
+ //border-color: inline-style;
+ border-radius: 100%;
+ // background-color: inline-style;
+ }
+
+ a.inner-circle {
+ // width: inline-style;
+ // height: inline-style;
+ display: block;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ // margin-top: inline-style;
+ // margin-left: inline-style;
+ border-radius: 100%;
+ // background-color: inline-style;
+ }
+ }
+ }
+ }
+
+ hr {
+ border: 0;
+ height: 1px;
+ background-image: linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0));
+ background-image: -o-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0));
+ background-image: -ms-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0));
+ background-image: -moz-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0));
+ background-image: -webkit-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0));
+ }
+}
+
+@timeline-notification-color: #3a71ea;
+@timeline-hard-state-color: #ff7000;
+@timeline-comment-color: #79bdba;
+@timeline-ack-color: #a2721d;
+@timeline-downtime-start-color: #8e8e8e;
+@timeline-downtime-end-color: #d5d6ad;
+
+.timeline-notification {
+ background-color: @timeline-notification-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-notification-color, 20%);
+ }
+}
+
+.timeline-hard-state {
+ background-color: @timeline-hard-state-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-hard-state-color, 20%);
+ }
+}
+
+.timeline-comment {
+ background-color: @timeline-comment-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-comment-color, 20%);
+ }
+}
+
+.timeline-ack {
+ background-color: @timeline-ack-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-ack-color, 20%);
+ }
+}
+
+.timeline-downtime-start {
+ background-color: @timeline-downtime-start-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-downtime-start-color, 20%);
+ }
+}
+
+.timeline-downtime-end {
+ background-color: @timeline-downtime-end-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-downtime-end-color, 20%);
+ }
+}
+
+/* End of monitoring timeline styles */
+
+/* Object features */
+
+form.instance-features span.description, form.object-features span.description {
+ text-align: left;
+}
+
+.object-features {
+ .control-label-group {
+ text-align: left;
+ margin-right: 0;
+ width: @name-value-table-name-width;
+ color: @text-color-light;
+
+ label {
+ font-size: inherit;
+ }
+ }
+
+ .control-group {
+ margin-top: 0;
+ margin-bottom: 0;
+
+ &.indeterminate {
+ justify-content: flex-start;
+
+ .control-label-group {
+ flex: 0 1 auto;
+ }
+
+ select {
+ width: auto;
+ flex: 0 1 auto;
+
+ & + span.hint {
+ flex: 0 1 auto;
+ }
+ }
+ }
+ }
+
+ .toggle-switch {
+ margin-left: @table-column-padding;
+ }
+
+ select {
+ margin-right: .5em;
+ margin-left: @table-column-padding;
+
+ & + span.hint {
+ margin: .35em;
+ color: @gray-light;
+ font-style: italic;
+ }
+ }
+}
+
+.plugin-output {
+ border-left: 5px solid @gray-lighter;
+ padding: 0.66em 0.33em;
+ .output-table {
+ font-size: 0.75em;
+ }
+
+ .state-critical {
+ background-color: @color-critical;
+ color: @body-bg-color;
+ padding: 0.2em;
+ }
+
+ .state-ok {
+ background-color: @color-ok;
+ color: @body-bg-color;
+ padding: 0.2em;
+ }
+
+ .state-unknown {
+ background-color: @color-unknown;
+ color: @body-bg-color;
+ padding: 0.2em;
+ }
+
+ .state-warning {
+ background-color: @color-warning;
+ color: @body-bg-color;
+ padding: 0.2em;
+ }
+
+ .state-down {
+ background-color: @color-down;
+ color: @body-bg-color;
+ padding: 0.2em;
+ }
+
+ .state-up {
+ background-color: @color-up;
+ color: @body-bg-color;
+ padding: 0.2em;
+ }
+}
+
+.go-ahead,
+.markdown,
+.plugin-output {
+ a {
+ border-bottom: 1px dotted @gray-light;
+
+ &:hover {
+ border-bottom: 1px solid @text-color;
+ text-decoration: none;
+ }
+ }
+}
+
+.event-details {
+ .badge {
+ font-size: 0.6em;
+ margin-right: 0.5em;
+ }
+
+ .state-label {
+ vertical-align: middle;
+ }
+}
+
+/* Object customvars */
+.custom-var-table {
+ .level-1 th {
+ padding-left: .5em;
+ }
+
+ .level-2 th {
+ padding-left: 1em;
+ }
+
+ .level-3 th {
+ padding-left: 1.5em;
+ }
+
+ .level-4 th {
+ padding-left: 2em;
+ }
+
+ .level-5 th {
+ padding-left: 2.5em;
+ }
+
+ .level-6 th {
+ padding-left: 3em;
+ }
+
+ .empty {
+ color: @gray-semilight;
+ }
+
+ thead th {
+ padding-left: 0;
+ text-align: left;
+ font-weight: bold;
+ font-size: 1.167em;
+
+ > span {
+ :nth-child(1),
+ :nth-child(2) {
+ display: none;
+ }
+ }
+ }
+
+ &[data-can-collapse] thead th > span {
+ :nth-child(1) {
+ display: none;
+ }
+
+ :nth-child(2) {
+ display: inline-block;
+ }
+ }
+
+ &.collapsed thead th > span {
+ :nth-child(1) {
+ display: inline-block;
+ }
+
+ :nth-child(2) {
+ display: none;
+ }
+ }
+}
+
+//p.pluginoutput {
+// width: 100%;
+// white-space: pre-wrap;
+// font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'DejaVu Sans Mono', 'Courier New', Courier, monospace;
+//}
+//
+//table.action td .pluginoutput {
+// font-size: 0.875em;
+// line-height: 1.2em;
+// padding: 0;
+// margin: 0;
+//}
+//
+//div.pluginoutput {
+// overflow: auto;
+// color: #888;
+// margin-bottom: 1em;
+// padding: 0.2em;
+//}
+//
+//div.pluginoutput pre {
+// white-space: pre-wrap;
+// border-left: 4px solid #d8d8d8;
+// padding: 0.3em 0 0.3em 1em;
+// font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'DejaVu Sans Mono', 'Courier New', Courier, monospace;
+//}
+//
+//table.objectstate td.state {
+// padding-top: 0.5em;
+// padding-bottom: 0.5em;
+//}
+//
+//div.contacts div.contact {
+// background-color: #eee;
+// padding: 0.5em;
+// border: 1px solid #d9d9d9;
+// overflow: hidden;
+// margin: 0.125em;
+// float: left;
+//}
+//
+//div.contacts div.contact a{
+// color: @colorTextDefault;
+//}
+//
+//div.contacts div.contact > img {
+// width: 80px;
+// height: 80px;
+// margin-right: 8px;
+// float: left;
+//}
+//
+//div.contacts div.notification-periods {
+// margin-top: 0.5em;
+//}
+//
+//.tinystatesummary {
+// .badges {
+// display: inline-block;
+// margin-bottom: 4px;
+// margin-left: 1em;
+// height: auto;
+// }
+//
+// .state > a {
+// color: white;
+// font-size: 0.857em;
+// padding: 2px 5px;
+// }
+//}
+//
+///* State badges */
+//span.state {
+// font-weight: bold;
+// color: white;
+// font-weight: bold;
+// padding: 2px 3px;
+// margin-right: 5px;
+//}
+//
+//span.state.active {
+// border: 2px solid #555;
+// padding: 2px 4px;
+// margin-right: 4px;
+//}
+//
+//span.state span.state {
+// margin: 0 -6px 0 5px;
+//}
+//
+//span.state.ok {
+// background: @colorOk;
+//}
+//
+//span.state.up {
+// background: @colorOk;
+//}
+//
+//span.state.critical {
+// background: @colorCritical;
+//}
+//
+//span.state.down {
+// background: @colorCritical;
+//}
+//
+//span.state.handled.critical {
+// background: @colorCriticalHandled;
+//}
+//
+//span.state.handled.down {
+// background: @colorCriticalHandled;
+//}
+//
+//span.state.warning {
+// background: @colorWarning;
+//}
+//
+//span.state.handled.warning {
+// background: @colorWarningHandled;
+//}
+//
+//span.state.unknown {
+// background: @colorUnknown;
+//}
+//
+//span.state.handled.unknown {
+// background: @colorUnknownHandled;
+//}
+//
+//span.state.pending {
+// background: @colorPending;
+//}
+//
+//form.instance-features span.description, form.object-features span.description {
+// display: inline;
+//}
+//
+//.boxview div.box form.instance-features div.header {
+// border-bottom: 1px solid #d9d9d9;
+// margin-bottom: 0.5em;
+//
+// h2 {
+// border: 0;
+// padding-bottom: 0;
+// }
+//}
+//
+//table.avp form.object-features div.header h4 {
+// margin: 0;
+//}
+//
+//table.avp {
+// th {
+// font-weight: normal;
+// font-size: 0.875em;
+// padding-top: 0.25em;
+// }
+//
+// h2 {
+// font-size: 0.875em;
+// line-height: 1.2em;
+// padding-bottom: 0.1em;
+// }
+//
+// td {
+// color: #666;
+// padding-bottom: 0.3em;
+// line-height: 1.5em;
+// th, td {
+// padding: 0;
+// }
+// }
+//
+// .badge a[href] {
+// color: @colorGray;
+// }
+//
+// .go-ahead {
+// a, button.link-like {
+// color: #222;
+// }
+// }
+//
+// .object-features {
+// label {
+// font-weight: normal;
+// margin-right: 0;
+// width: 14em;
+// font-size: 0.875em;
+// }
+//
+// input {
+// margin: 0;
+// }
+// }
+//}
+//
+//table.avp .customvar ul {
+// list-style-type: none;
+// margin: 0;
+// padding: 0;
+// padding-left: 1.5em;
+//}
+//
+//div.selection-info {
+// padding-top: 0.4em;
+// float: right;
+// cursor: help;
+// font-size: 0.857em;
+//}
+//
+//.optionbox {
+// margin-left: 0em;
+// margin-right: 3em;
+//}
+//
+//.optionbox label {
+// max-width: 6.5em;
+// text-align: left;
+// vertgical-align: middle;
+// margin-right: 0em;
+//}
+//
+//.optionbox input {
+// vertical-align: middle;
+//}
+//
+//.object-command form h1, .objects-command form h1 {
+// border: none;
+//}
+//
+//hr.command-separator {
+// border: none;
+// border-bottom: 2px solid @colorPetrol;
+//}
+//
+//div.backend-not-running {
+// background: @colorCritical;
+// color: white;
+// text-align: center;
+// padding: 0.1em;
+//}
+//
+//td.state {
+// .time-ago,
+// .time-since,
+// .time-until {
+// text-transform: capitalize;
+// }
+//}
+//
+//.inline-comments {
+// padding: 0;
+// margin: 0;
+// font-size: 0.857em;
+//
+// .time-ago {
+// font-style: italic;
+// color: #919191;
+// }
+//
+// li {
+// list-style-type: none;
+// margin-bottom: 8px;
+// }
+//
+// h3 {
+// border: none;
+// border-bottom: 1px solid gray;
+// font-weight: normal;
+// font-size: inherit;
+// color: inherit;
+// margin: 0;
+// padding-bottom: 0.1em;
+// }
+//
+// h3 .author {
+// font-weight: bold;
+// }
+//
+// h3 form {
+// display: none;
+// }
+//
+// h3 form {
+// float: right;
+// }
+//
+// li:hover h3 {
+// background: #F9F9F9;
+// position: relative;
+//
+// form {
+// display: inline;
+// }
+// }
+//
+// p {
+// margin: 0;
+//
+// a {
+// color: #222;
+// }
+// }
+//}
+//
+///* Special tables and states */
+//
+//table.colors {
+// font-size: 0.8em;
+// width: 98%;
+// margin: 0 1%;
+//}
+//
+//table.colors td {
+// text-align: center;
+// vertical-align: middle;
+// width: 10%;
+// height: 1.6em;
+// font-weight: normal;
+// border: 0.079em solid white;
+//}
+//
+//table.action td.state, table.objectstate td.state {
+// font-size: 0.857em;
+// text-align: center;
+//}
+//
+//
+///* State row behaviour */
+//
+//tr.state img.icon {
+// margin-right: 2px;
+//}
+//
+///* Hostgroup badge quickfix */
+//tr.state span a {
+// color: white;
+// font-size: 0.857em;
+// padding: 2px 5px;
+//}
+//
+//tr.state:hover a {
+// color: inherit;
+//}
+//
+//tr.state a.active {
+//}
+//
+//tr.state.new td.state {
+// font-weight: bold;
+//}
+//
+//tr.state td.state {
+// width: 9em;
+// color: white;
+// border-bottom: none;
+//}
+//
+//tr.state.handled td.state, tr.state.ok td.state, tr.state.up td.state, tr.state.pending td.state {
+// border-left-style: solid;
+// border-left-width: 1.5em;
+// padding-left: 0em;
+// padding-right: 0.5em;
+// color: black;
+// background-color: transparent;
+//}
+//
+//tr.state.ok td.state, tr.state.up td.state {
+// border-left-color: @colorOk;
+//}
+//
+//tr.state.warning td.state {
+// background-color: @colorWarning;
+//}
+//
+//tr.state.warning.handled td.state {
+// border-left-color: @colorWarningHandled;
+//}
+//
+//tr.state.critical td.state, tr.state.down td.state {
+// background-color: @colorCritical;
+//}
+//
+//tr.state.critical.handled td.state, tr.state.down.handled td.state {
+// border-left-color: @colorCriticalHandled;
+//}
+//
+//tr.state.unreachable td.state {
+// background-color: @colorUnreachable;
+//}
+//
+//tr.state.unreachable.handled td.state {
+// border-left-color: @colorUnreachableHandled;
+//}
+//
+//tr.state.unknown td.state {
+// background-color: @colorUnknown;
+//}
+//
+//tr.state.unknown.handled td.state {
+// border-left-color: @colorUnknownHandled;
+//}
+//
+//tr.state.pending td.state {
+// border-left-color: @colorPending;
+//}
+//
+//tr.state.invalid td.state {
+// background-color: @colorInvalid;
+//}
+//
+//tr.state.unreachable td.state {
+// background-color: @colorUnreachable;
+//}
+//
+//tr.state.unreachable.handled td.state {
+// border-left-color: @colorUnreachableHandled;
+//}
+//
+//tr.state.handled td.state {
+// color: inherit;
+// background-color: transparent !important;
+//}
+//
+///* HOVER colors */
+//
+//tr.state[href]:hover td.state {
+// background-color: #eee;
+//}
+//
+//tr.state.ok[href]:hover, tr.state.up[href]:hover {
+// background-color: @colorOk;
+//}
+//
+//tr.state.handled[href]:hover, tr.state.handled[href]:hover td.state {
+// color: #121212 !important;
+//}
+//
+//tr.state.warning[href]:hover {
+// background-color: @colorWarning;
+// color: white;
+//}
+//
+//tr.state.warning.handled[href]:hover {
+// background-color: @colorWarningHandled;
+//}
+//
+//tr.state.critical[href]:hover, tr.state.down[href]:hover {
+// background-color: @colorCritical;
+// color: white;
+//}
+//
+//tr.state.critical.handled[href]:hover, tr.state.down.handled[href]:hover {
+// background-color: @colorCriticalHandled;
+// color: #333;
+//}
+//
+//tr.state.unknown[href]:hover {
+// background-color: @colorUnknown;
+// color: white;
+//}
+//
+//tr.state.unknown.handled[href]:hover {
+// background-color: @colorUnknownHandled;
+//}
+//
+//tr.state.pending[href]:hover {
+// background-color: @colorPending;
+//}
+//
+//tr.state.invalid[href]:hover {
+// background-color: @colorInvalid;
+// color: white;
+//}
+//
+//tr.state.unreachable[href]:hover {
+// background-color: @colorUnreachable;
+//}
+//
+//tr.state.unreachable.handled[href]:hover {
+// background-color: @colorUnreachableHandled;
+//}
+//
+//tr.state[href]:hover td.state {
+// background-color: inherit !important;
+//}
+//
+///* END of HOVER colors */
+//
+///* END of special tables and states */
+//
+//
+///* Generic colors */
+//
+//a.critical {
+// color: @colorCritical;
+//}
+//
+///* END of Generic colors */
+//
+//
+///* Generic box element */
+//
+//.boxview a {
+// text-decoration: none;
+//}
+//
+//.boxview > div.box {
+// text-align: center;
+// vertical-align: top;
+// display: inline-block;
+// padding: 0.4em;
+// margin: 0.4em;
+// border: 1px solid #d9d9d9;
+// background: #eee;
+//}
+//
+///* Box header */
+//.boxview div.box.header {
+// padding-bottom: 0.5em;
+// margin-bottom: 0.5em;
+// border-bottom: 1px solid #888;
+//}
+//
+//.boxview div.box.header h2 {
+// margin-top: 0.1em;
+// margin-bottom: 0;
+// font-size: 0.8em;
+// border-bottom: none;
+// color: @colorTextDefault;
+//}
+//
+//.boxview div.box.header h2:first-child {
+// margin-top: 0.2em;
+// font-size: inherit;
+// color: @colorTextDefault;
+//}
+//
+//.boxview div.box.header h2 > a {
+// color: inherit;
+//}
+//
+//.boxview div.box.header h2 > a:hover {
+// text-decoration: underline;
+//}
+//
+//.boxview div.box.header h3 {
+// line-height: 1.5em;
+// font-size: 0.9em;
+// color: #555;
+//}
+//
+///* Box body of contents */
+//.boxview div.box.contents {
+// padding: 0.2em;
+//}
+//
+//.boxview div.box.contents table {
+// width: 100%;
+//}
+//
+//.boxview div.box.contents td {
+// width: 13em;
+// vertical-align: top;
+//}
+//
+///* Box separator */
+//.boxview div.box-separator:first-child {
+// border-top-width: 0;
+//}
+//
+//.boxview div.box-separator {
+// font-size: 0.8em;
+// padding: 0.4em 0 0.4em;
+// border: 1px solid #d9d9d9;
+//
+// font-weight: bold;
+// letter-spacing: 0.1em;
+//}
+//
+///* Box entry */
+//.boxview div.box.entry {
+// min-height: 2.7em;
+// margin: 0.2em;
+// font-size: 0.9em;
+// white-space: nowrap;
+//
+// color: @colorTextDefault;
+//}
+//
+///* Any line of a box entry */
+//.boxview div.box.entry a {
+// display: block;
+//
+// color: inherit;
+//}
+//
+//.boxview div.box.entry a:hover {
+// color: @colorTextDefault;
+//}
+//
+///* First line of a box entry */
+//.boxview div.box.entry a:first-child {
+// font-size: 1em;
+//}
+//
+///* End of generic box element */
+//
+//
+///* Monitoring box element styles */
+//
+///* Host- and Servicegroup element styles */
+//
+//div.box.entry.state_up, div.box.entry.state_ok {
+// border: 1px solid @colorOk;
+// border-left: 1em solid @colorOk;
+//}
+//
+//div.box.entry.state_pending {
+// border: 1px solid @colorPending;
+// border-left: 1em solid @colorPending;
+//}
+//
+//div.box.entry.state_down, div.box.entry.state_critical {
+// border: 1px solid @colorCritical;
+// border-left: 1em solid @colorCritical;
+// background-color: @colorCritical;
+// color: white;
+//}
+//
+//div.box.entry.state_down a:hover, div.box.entry.state_critical a:hover {
+// color: #dcdcdc;
+//}
+//
+//div.box.entry.state_warning {
+// border: 1px solid @colorWarning;
+// border-left: 1em solid @colorWarning;
+// background-color: @colorWarning;
+// color: white;
+//}
+//
+//div.box.entry.state_warning a:hover {
+// color: #dcdcdc;
+//}
+//
+//div.box.entry.state_unreachable, div.box.entry.state_unknown {
+// border: 1px solid @colorUnknown;
+// border-left: 1em solid @colorUnknown;
+// background-color: @colorUnknown;
+// color: white;
+//}
+//
+//div.box.entry.state_unreachable a:hover, div.box.entry.state_unknown a:hover {
+// color: #dcdcdc;
+//}
+//
+//div.box.entry.handled {
+// background-color: transparent;
+// color: inherit;
+//}
+//
+//div.box.entry.handled a:hover {
+// color: @colorTextDefault;
+//}
+//
+/* Tactical overview element styles */
+//
+//.tactical > .boxview > div.box {
+// min-height: 20em;
+// min-width: 12.1em;
+//}
+//
+//.tactical div.box.contents {
+// min-height: 14.5em;
+//}
+//
+//div.box.contents.zero {
+// min-width: 11.1em;
+//
+// background-color: transparent;
+//}
+//
+//div.box.contents.zero span {
+// font-weight: bold;
+// line-height: 2em;
+//
+// color: #666;
+//}
+//
+//div.box.contents.zero h3 {
+// margin: 0;
+// font-size: 12em;
+// line-height: 1em;
+//
+// color: #666;
+//}
+//
+//div.box.ok_hosts.state_up {
+// border: 5px solid @colorOk;
+//}
+//
+//div.box.ok_hosts.state_pending {
+// background-color: @colorPending;
+//}
+//
+//div.box.problem_hosts.state_down {
+// border: 5px solid @colorCritical;
+//}
+//
+//div.box.problem_hosts.state_down.handled {
+// background-color: @colorCriticalHandled;
+//}
+//
+//div.box.problem_hosts.state_unreachable {
+// background-color: @colorUnreachable;
+//}
+//
+//div.box.problem_hosts.state_unreachable.handled {
+// background-color: @colorUnreachableHandled;
+//}
+//
+//div.box.ok_hosts div.box.entry, div.box.problem_hosts div.box.entry {
+// min-width: 11.1em;
+//}
+//
+//div.box.monitoringfeatures div.box.contents {
+// padding: 0 2 0em;
+//}
+//
+//div.box.monitoringfeatures {
+// border: 5px solid #d9d9d9;
+//}
+//
+//div.box.monitoringfeatures div.box-separator {
+// color: white;
+// background-color: @colorOk;
+//}
+//
+//div.box.monitoringfeatures div.feature-highlight {
+// background-color: @colorCritical;
+//}
+//
+//div.box.monitoringfeatures a.feature-highlight {
+// font-weight: bold;
+//}
+//
+//div.box.hostservicechecks {
+// border: 5px solid #d9d9d9;
+//}
+//
+///* Contactgroup element styles */
+//
+//div.box.contactgroup {
+// width: 18em;
+// padding: 0.8em;
+//}
+//
+//div.box.contactgroup div.box.contents {
+// padding: 0.6em;
+//}
+//
+//div.box.contactgroup div.box.entry {
+// overflow: hidden;
+// clear: left;
+//}
+//
+//div.box.contactgroup div.box.entry img {
+// width: 80px;
+// height: 80px;
+// float: left;
+//
+//}
+//
+//div.box.contactgroup div.box.entry a {
+// margin-top: 0.4em;
+//
+// font-weight: bold;
+//}
+//
+//div.box.contactgroup div.box.entry p {
+// margin: 0.4em 0 0;
+//}
+//
+//div.circular {
+// margin-top: 0.5em;
+// margin-left: 2em;
+// margin-right: 1em;
+// width: 80px;
+// height: 80px;
+// float: left;
+// background-size: 100% 100%;
+//}
+//
+///* End of monitoring box element styles */
+//
+//
+///* Monitoring pivot table styles */
+//
+//div.pivot-pagination {
+// margin: 1em;
+//
+// table {
+// table-layout: fixed;
+// border-spacing: 1px;
+// border-collapse: separate;
+// border: 1px solid LightGrey;
+// border-radius: 0.3em;
+//
+// td {
+// width: 16px;
+// height: 16px;
+// padding: 0;
+// background-color: #fbfbfb;
+//
+// &:hover, &.active {
+// background-color: #e5e5e5;
+// }
+//
+// a {
+// width: 16px;
+// height: 16px;
+// display: block;
+// }
+// }
+// }
+//}
+//
+//table.joystick-pagination {
+// margin-top: -1.5em;
+//
+// td {
+// width: 1.25em;
+// height: 1.3em;
+// }
+//}
+//
+///* End of monitoring pivot table styles */
+//
+///* Monitoring timeline styles */
+//
+//div.timeline-legend {
+// float: left;
+// padding: 0.5em;
+// border: 1px solid #d9d9d9;
+// background-color: #eee;
+//
+// h2 {
+// margin: 0;
+// margin-left: 0.5em;
+// line-height: 1.1em;
+// }
+//
+// & > span {
+// display: inline-block;
+// padding: 0.5em;
+// margin: 0.5em;
+//
+// span {
+// color: white;
+// font-size: 0.8em;
+// font-weight: bold;
+// white-space: nowrap;
+// }
+// }
+//}
+//
+//div.timeline {
+// div.timeframe {
+// height: 7em;
+// margin-bottom: 1em;
+// clear: left;
+//
+// span {
+// width: 8em;
+// margin-top: 2.3em;
+// margin-right: 1.5em;
+// display: block;
+// float: left;
+// text-align: center;
+//
+// a {
+// color: @colorTextDefault;
+// font-size: 0.8em;
+// font-weight: bold;
+// text-decoration: none;
+// white-space: nowrap;
+//
+// &:hover {
+// color: @colorTextDefault;
+// text-decoration: underline;
+//
+// }
+// }
+// }
+//
+// div.circle-box {
+// // width: inline-style;
+// height: 100%;
+// margin-right: 0.5em;
+// position: relative;
+// float: left;
+//
+// div.outer-circle {
+// // width: inline-style;
+// // height: inline-style;
+// position: absolute;
+// top: 50%;
+// // margin-top: inline-style;
+//
+// &.extrapolated {
+// border-width: 2px;
+// border-style: dotted;
+// //border-color: inline-style;
+// border-radius: 100%;
+// // background-color: inline-style;
+// }
+//
+// a.inner-circle {
+// // width: inline-style;
+// // height: inline-style;
+// display: block;
+// position: absolute;
+// top: 50%;
+// left: 50%;
+// // margin-top: inline-style;
+// // margin-left: inline-style;
+// border-radius: 100%;
+// // background-color: inline-style;
+// }
+// }
+// }
+// }
+//
+// hr {
+// border: 0;
+// height: 1px;
+// background-image: linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0));
+// background-image: -o-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0));
+// background-image: -ms-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0));
+// background-image: -moz-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0));
+// background-image: -webkit-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,0.3), rgba(0,0,0,0));
+// }
+//}
+//
+///* End of monitoring timeline styles */
+//
+///* Monitoring groupsummary styles */
+//
+//.dashboard table.groupview {
+// margin-top: 0;
+//}
+//
+//table.groupview {
+// width: 100%;
+// margin-top: 1em;
+// border-collapse: separate;
+// border-spacing: 0.1em;
+//
+// th {
+// font-size: 1.0em;
+// font-weight: normal;
+// text-align: center;
+// white-space: nowrap;
+// border-bottom: 2px solid @gray-light;
+// }
+//
+// td {
+// &.groupname {
+// width: 60%;
+//
+// a {
+// color: inherit;
+// text-decoration: none;
+//
+// &:hover {
+// text-decoration: underline;
+// }
+// }
+// }
+//
+// &.total {
+// width: 10%;
+// }
+//
+// &.state {
+// width: 20%;
+// white-space: nowrap;
+//
+// &.change {
+// width: 10%;
+// text-align: center;
+// border-left-width: 1.5em;
+// border-left-style: solid;
+// padding: 0.3em 0.5em 0.3em 0.5em;
+//
+// strong {
+// font-size: 0.8em;
+// }
+//
+// &.ok {
+// border-color: @colorOk;
+// }
+//
+// &.pending {
+// border-color: @colorPending;
+// }
+//
+// &.warning {
+// border-color: @colorWarningHandled;
+//
+// &.unhandled {
+// color: white;
+// border-left-width: 0;
+// background-color: @colorWarning;
+// }
+// }
+//
+// &.unknown {
+// border-color: @colorUnknownHandled;
+//
+// &.unhandled {
+// color: white;
+// border-left-width: 0;
+// background-color: @colorUnknown;
+// }
+// }
+//
+// &.critical {
+// border-color: @colorCriticalHandled;
+//
+// &.unhandled {
+// color: white;
+// border-left-width: 0;
+// background-color: @colorCritical;
+// }
+// }
+// }
+//
+// span.state {
+// &.handled {
+// margin-right: 2px;
+// }
+//
+// a {
+// font-size: 0.9em;
+// color: white;
+// text-decoration: none;
+//
+// &:hover {
+// text-decoration: underline;
+// }
+// }
+// }
+// }
+// }
+//}
+//
+///* End of monitoring groupsummary styles */
+//
+///* compact table */
+//table.statesummary {
+// text-align: left;
+// width: auto;
+// border-collapse: separate;
+//
+// tr.state td.state {
+// width: auto;
+// font-weight: bold;
+// }
+//
+// td {
+// font-size: 0.9em;
+// line-height: 1.2em;
+// padding-left: 0.2em;
+// margin: 0;
+// }
+//
+// td.state {
+// padding: 0.2em;
+// min-width: 75px;
+// font-size: 0.75em;
+// text-align: center;
+// }
+//
+// td.name {
+// font-weight: bold;
+// }
+//
+// td a {
+// color: inherit;
+// text-decoration: none;
+// }
+//}
+//
+//table.action .objectflags {
+// float: right;
+//}
+//
+//table.objectstate {
+// border-collapse: separate;
+// border-spacing: 1px;
+//}
+//
+//table.objectstate td {
+// padding-left: 1em;
+//}
+//
+//table.objectstate tr.state td.state {
+// width: 9em;
+// text-align: center;
+// padding-left: 0;
+// border-radius: 0;
+//}
+//
+//table.avp td.performance-data {
+// padding: 0.3em 0 0.3em 1em;
+//}
+//
+//table.perfdata {
+// min-width: 24em;
+// font-size: 0.9em;
+// width: 100%;
+//}
+//
+//table.perfdata th {
+// padding: 0;
+// text-align: left;
+// padding-right: 0.5em;
+//}
+//
+//table.perfdata td {
+// white-space: nowrap;
+// padding-right: 0.5em;
+//}
diff --git a/modules/monitoring/public/css/service-grid.less b/modules/monitoring/public/css/service-grid.less
new file mode 100644
index 0000000..fd22097
--- /dev/null
+++ b/modules/monitoring/public/css/service-grid.less
@@ -0,0 +1,75 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+.service-grid-table {
+ width: 0;
+ white-space: nowrap;
+
+ td {
+ color: @gray-light;
+ padding: 0.2em;
+ text-align: center;
+ width: 1em;
+ }
+
+ .rotate-45 {
+ height: 8em;
+
+ div {
+ .transform(translate(0.4em, 2.8em) rotate(315deg));
+ width: 1.5em;
+ }
+ }
+
+ .service-grid-table-more {
+ text-align: center;
+ a {
+ display: inline;
+ }
+ }
+}
+
+.joystick-pagination {
+ margin: 0 auto;
+ font-size: 130%;
+
+ a {
+ color: @text-color;
+ outline: none;
+
+ &:hover {
+ color: @text-color-light;
+ }
+ &:focus, &:active {
+ color: @icinga-blue;
+ }
+ }
+
+ i {
+ display: block;
+ height: 1.5em;
+ width: 1.5em;
+ }
+}
+
+.service-grid-link {
+ .bg-stateful();
+ .rounded-corners();
+
+ display: inline-block;
+ height: 1.5em;
+ vertical-align: middle;
+ width: 1.5em;
+}
+
+form.filter-toggle {
+ label:not(.toggle-switch) {
+ display: inline-block;
+ vertical-align: top;
+ margin-left: .5em;
+ color: @gray-light;
+ }
+
+ input[type="checkbox"]:checked ~ label {
+ color: inherit;
+ }
+}
diff --git a/modules/monitoring/public/css/tables.less b/modules/monitoring/public/css/tables.less
new file mode 100644
index 0000000..c5b5f27
--- /dev/null
+++ b/modules/monitoring/public/css/tables.less
@@ -0,0 +1,282 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+@border-left-width: 6px;
+
+// Icon images in list and detail views
+.host-icon-image,
+.service-icon-image {
+ max-width: 2em;
+ vertical-align: middle;
+}
+
+// Check source reachable information in the host and service detail views
+.check-source-meta {
+ font-size: @font-size-small;
+}
+
+// Object link and comment author in the comment overview
+.comment-author {
+ margin-bottom: 0.25em;
+
+ > a {
+ font-weight: bold;
+ }
+}
+
+// Comment icons, e.g. persistent in the comment overview
+.comment-icons {
+ float: right;
+}
+
+.caption {
+ height: 3em;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+
+ img {
+ max-height: 1em;
+ }
+}
+
+// Type information for backends in the monitoring config
+.config-label-meta {
+ font-size: @font-size-small;
+}
+
+// Column for counts, e.g. host group members
+.count-col {
+ width: 4em;
+}
+
+// Custom variables in the host and service detail view
+.custom-variables > ul {
+ list-style-type: none;
+ margin: 0;
+}
+
+// Host name and IP addresses in the host and service detail view
+.host-meta {
+ color: @text-color-light;
+ font-size: @font-size-small;
+}
+
+// Notification recipient in the notifications overview
+.notification-recipient {
+ color: @text-color-light;
+ float: right;
+ font-size: @font-size-small;
+}
+
+
+// Container for plugin output and performance data in overviews
+.overview-plugin-output-container {
+ .clearfix();
+}
+
+// Performance data pies in overviews
+.overview-performance-data {
+ float: right;
+ font-size: @font-size-small;
+}
+
+// Plugin output in detail views
+.plugin-output,
+// Plugin output in overviews
+.overview-plugin-output {
+ -webkit-hyphens: auto;
+ -moz-hyphens: auto;
+ -ms-hyphens: auto;
+ hyphens: auto;
+
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+}
+
+// Plugin output in overviews
+.overview-plugin-output {
+ color: @text-color-light;
+ font-family: @font-family-fixed;
+ font-size: @font-size-small;
+ margin: 0;
+ white-space: pre-wrap;
+ // Long text in table cells overflows the table's width if the table's layout is not fixed.
+ // Thus overflow-wrap will not have any effect. But w/ the following we set a width of any value
+ // plus a min-width of 100% to consume the full width nonetheless which seems to always
+ // instruct browsers to not overflow the table. Ridiculous.
+ min-width: 100%;
+ width: 1em;
+}
+
+// Table for performance data in detail views
+.performance-data-table {
+ display: block;
+ overflow-x: auto;
+ position: relative;
+
+ > thead > tr > th {
+ text-align: left;
+ }
+
+ > thead > tr > th:first-child,
+ > tbody > tr > td:first-child {
+ // Reset base padding
+ padding-left: 0;
+ }
+
+ > thead > tr > th,
+ > tbody > tr > td {
+ white-space: nowrap;
+ }
+}
+
+// Performance data table column for sparkline pie charts in detail views
+.sparkline-col {
+ width: 2em;
+}
+
+// Service description if in the service detail view
+.service-meta {
+ color: @text-color-light;
+ font-size: @font-size-small;
+}
+
+// State column for label and duration in overviews
+.state-col {
+ &.state-ok,
+ &.state-up {
+ border-left: @border-left-width solid @color-ok;
+ }
+
+ &.state-pending {
+ border-left: @border-left-width solid @color-pending;
+ }
+
+ &.state-critical,
+ &.state-down {
+ background-color: @color-critical;
+ color: @text-color-inverted;
+
+ &.handled {
+ background-color: inherit;
+ color: inherit;
+ border-left: @border-left-width solid @color-critical-handled;
+ }
+ }
+
+ &.state-warning {
+ background-color: @color-warning;
+ color: @text-color-inverted;
+
+ &.handled {
+ background-color: inherit;
+ color: inherit;
+ border-left: @border-left-width solid @color-warning-handled;
+ }
+ }
+
+ &.state-unknown {
+ background-color: @color-unknown;
+ color: @text-color-inverted;
+
+ &.handled {
+ background-color: inherit;
+ color: inherit;
+ border-left: @border-left-width solid @color-unknown-handled;
+ }
+ }
+
+ &.state-unreachable {
+ background-color: @color-unreachable;
+ color: @text-color-inverted;
+
+ &.handled {
+ background-color: inherit;
+ color: inherit;
+ border-left: @border-left-width solid @color-unreachable-handled;
+ }
+ }
+
+ // State class for history events
+ &.state-no-state {
+ border-left: @border-left-width solid @text-color-light;
+ }
+
+ * {
+ color: inherit;
+ }
+
+ text-align: center;
+ width: 8em;
+}
+
+// Wraps links, icons and meta in overviews
+.state-header {
+ .clearfix();
+
+ > a {
+ font-weight: bold;
+ }
+}
+
+// State icons, e.g. acknowledged in overviews
+.state-icons {
+ float: right;
+}
+
+// State labels in overviews
+.state-label {
+ font-family: @font-family-wide;
+ font-size: @font-size-small;
+ letter-spacing: 1px;
+}
+
+// State duration and state type information in overviews
+.state-meta {
+ font-size: @font-size-small;
+}
+
+.state-table {
+ border-collapse: separate;
+ border-spacing: 0 1px;
+ width: 100%;
+
+ tr[href] {
+ -webkit-transform: translate3d(0,0,0); /* Without this, hovering in Safari is broken in history table rows */
+ -moz-transform: none; /* Firefox collapses border spacing due to the above */
+ }
+
+ tr[href].active {
+ background-color: @tr-active-color;
+ }
+
+ tr[href]:hover {
+ background-color: @tr-hover-color;
+ cursor: pointer;
+ }
+
+ tr[href].state-outdated:not(:hover):not(.active) td:not(.state-col) {
+ opacity: 0.7;
+ }
+}
+
+// Event history
+.history-message-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ > .history-message-icon {
+ padding: 0.25em;
+ }
+
+ > .history-message-output {
+ flex: 1;
+
+ > a {
+ font-weight: bold;
+ }
+ }
+}
diff --git a/modules/monitoring/public/js/module.js b/modules/monitoring/public/js/module.js
new file mode 100644
index 0000000..d665e6b
--- /dev/null
+++ b/modules/monitoring/public/js/module.js
@@ -0,0 +1,84 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+(function(Icinga) {
+
+ var Monitoring = function(module) {
+ /**
+ * The Icinga.Module instance
+ */
+ this.module = module;
+
+ /**
+ * The observer used to handle the timeline's infinite loading
+ */
+ this.scrollCheckTimer = null;
+
+ /**
+ * Whether to skip the timeline's scroll-check
+ */
+ this.skipScrollCheck = false;
+
+ this.initialize();
+ };
+
+ Monitoring.prototype = {
+
+ initialize: function()
+ {
+ this.module.on('rendered', this.enableScrollCheck);
+ this.module.icinga.logger.debug('Monitoring module loaded');
+ },
+
+ /**
+ * Enable the timeline's scroll-check
+ */
+ enableScrollCheck: function()
+ {
+ /**
+ * Re-enable the scroll-check in case the timeline has just been extended
+ */
+ if (this.skipScrollCheck) {
+ this.skipScrollCheck = false;
+ }
+
+ /**
+ * Prepare the timer to handle the timeline's infinite loading
+ */
+ var $timeline = $('div.timeline');
+ if ($timeline.length && !$timeline.closest('.dashboard').length) {
+ if (this.scrollCheckTimer === null) {
+ this.scrollCheckTimer = this.module.icinga.timer.register(
+ this.checkTimelinePosition,
+ this,
+ 800
+ );
+ this.module.icinga.logger.debug('Enabled timeline scroll-check');
+ }
+ }
+ },
+
+ /**
+ * Check whether the user scrolled to the end of the timeline
+ */
+ checkTimelinePosition: function()
+ {
+ if (!$('div.timeline').length) {
+ this.module.icinga.timer.unregister(this.scrollCheckTimer);
+ this.scrollCheckTimer = null;
+ this.module.icinga.logger.debug('Disabled timeline scroll-check');
+ } else if (!this.skipScrollCheck && this.module.icinga.utils.isVisible('#end')) {
+ this.skipScrollCheck = true;
+ this.module.icinga.loader.loadUrl(
+ $('#end').remove().attr('href'),
+ $('div.timeline'),
+ undefined,
+ undefined,
+ 'append'
+ ).addToHistory = false;
+ }
+ }
+ };
+
+ Icinga.availableModules.monitoring = Monitoring;
+
+}(Icinga));
diff --git a/modules/monitoring/run.php b/modules/monitoring/run.php
new file mode 100644
index 0000000..6fe4921
--- /dev/null
+++ b/modules/monitoring/run.php
@@ -0,0 +1,8 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+/** @var $this \Icinga\Application\Modules\Module */
+
+$this->provideHook('ApplicationState');
+$this->provideHook('Health');
+$this->provideHook('X509/Sni');
diff --git a/modules/setup/application/clicommands/ConfigCommand.php b/modules/setup/application/clicommands/ConfigCommand.php
new file mode 100644
index 0000000..e50333e
--- /dev/null
+++ b/modules/setup/application/clicommands/ConfigCommand.php
@@ -0,0 +1,188 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Clicommands;
+
+use Icinga\Application\Logger;
+use Icinga\Cli\Command;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Setup\Webserver;
+
+class ConfigCommand extends Command
+{
+ /**
+ * Create Icinga Web 2's configuration directory
+ *
+ * USAGE:
+ *
+ * icingacli setup config directory [options]
+ *
+ * OPTIONS:
+ *
+ * --config=<directory> Path to Icinga Web 2's configuration files [/etc/icingaweb2]
+ *
+ * --mode=<mode> The access mode to use [2770]
+ *
+ * --group=<group> Owner group for the configuration directory [icingaweb2]
+ *
+ * EXAMPLES:
+ *
+ * icingacli setup config directory
+ *
+ * icingacli setup config directory --mode=2775 --config=/opt/icingaweb2/etc
+ */
+ public function directoryAction()
+ {
+ $configDir = trim($this->params->get('config', $this->app->getConfigDir()));
+ if (strlen($configDir) === 0) {
+ $this->fail($this->translate(
+ 'The argument --config expects a path to Icinga Web 2\'s configuration files'
+ ));
+ }
+
+ $group = trim($this->params->get('group', 'icingaweb2'));
+ if (strlen($group) === 0) {
+ $this->fail($this->translate(
+ 'The argument --group expects a owner group for the configuration directory'
+ ));
+ }
+
+ $mode = trim($this->params->get('mode', '2770'));
+ if (strlen($mode) === 0) {
+ $this->fail($this->translate(
+ 'The argument --mode expects an access mode for the configuration directory'
+ ));
+ }
+
+ if (! file_exists($configDir) && ! @mkdir($configDir, 0755, true)) {
+ $e = error_get_last();
+ $this->fail(sprintf(
+ $this->translate('Can\'t create configuration directory %s: %s'),
+ $configDir,
+ $e['message']
+ ));
+ }
+
+ if (! @chmod($configDir, octdec($mode))) {
+ $e = error_get_last();
+ $this->fail(sprintf(
+ $this->translate('Can\'t change the mode of the configuration directory to %s: %s'),
+ $mode,
+ $e['message']
+ ));
+ }
+
+ if (! @chgrp($configDir, $group)) {
+ $e = error_get_last();
+ $this->fail(sprintf(
+ $this->translate('Can\'t change the group of %s to %s: %s'),
+ $configDir,
+ $group,
+ $e['message']
+ ));
+ }
+
+ printf($this->translate('Successfully created configuration directory %s') . PHP_EOL, $configDir);
+ }
+
+ /**
+ * Create webserver configuration
+ *
+ * USAGE:
+ *
+ * icingacli setup config webserver <apache|nginx> [options]
+ *
+ * OPTIONS:
+ *
+ * --path=<urlpath> The URL path to Icinga Web 2 [/icingaweb2]
+ *
+ * --root|--document-root=<directory> The directory from which the webserver will serve files
+ * [/path/to/icingaweb2/public]
+ *
+ * --enable-fpm Enable FPM handler for Apache (Nginx is always enabled)
+ *
+ * --fpm-uri=<uri> Address or path where to pass requests to FPM [127.0.0.1:9000]
+ *
+ * --config=<directory> Path to Icinga Web 2's configuration files [/etc/icingaweb2]
+ *
+ * --file=<filename> Write configuration to file [stdout]
+ *
+ * EXAMPLES:
+ *
+ * icingacli setup config webserver apache
+ *
+ * icingacli setup config webserver apache \
+ * --path=/icingaweb2 \
+ * --document-root=/usr/share/icingaweb2/public \
+ * --config=/etc/icingaweb2
+ *
+ * icingacli setup config webserver apache \
+ * --file=/etc/apache2/conf.d/icingaweb2.conf
+ *
+ * icingacli setup config webserver nginx \
+ * --root=/usr/share/icingaweb2/public \
+ * --fpm-uri=unix:/var/run/php5-fpm.sock
+ */
+ public function webserverAction()
+ {
+ if (($type = $this->params->getStandalone()) === null) {
+ $this->fail($this->translate('Argument type is mandatory.'));
+ }
+
+ $webserver = null;
+ try {
+ $webserver = Webserver::createInstance($type);
+ } catch (ProgrammingError $e) {
+ $this->fail($this->translate('Unknown type') . ': ' . $type);
+ }
+ $urlPath = trim($this->params->get('path', $webserver->getUrlPath()));
+ if (strlen($urlPath) === 0) {
+ $this->fail($this->translate('The argument --path expects a URL path'));
+ }
+ $documentRoot = trim(
+ $this->params->get('root', $this->params->get('document-root', $webserver->getDocumentRoot()))
+ );
+ if (strlen($documentRoot) === 0) {
+ $this->fail($this->translate(
+ 'The argument --root/--document-root expects a directory from which the webserver will serve files'
+ ));
+ }
+ $configDir = trim($this->params->get('config', $webserver->getConfigDir()));
+ if (strlen($configDir) === 0) {
+ $this->fail($this->translate(
+ 'The argument --config expects a path to Icinga Web 2\'s configuration files'
+ ));
+ }
+
+ $enableFpm = $this->params->shift('enable-fpm', $webserver->getEnableFpm());
+
+ $fpmUri = trim($this->params->get('fpm-uri', $webserver->getFpmUri()));
+ if (empty($fpmUri)) {
+ $this->fail($this->translate(
+ 'The argument --fpm-uri expects an address or path where to pass requests to FPM'
+ ));
+ }
+ $webserver
+ ->setDocumentRoot($documentRoot)
+ ->setConfigDir($configDir)
+ ->setUrlPath($urlPath)
+ ->setEnableFpm($enableFpm)
+ ->setFpmUri($fpmUri);
+ $config = $webserver->generate() . "\n";
+ if (($file = $this->params->get('file')) !== null) {
+ if (file_exists($file) === true) {
+ $this->fail(sprintf($this->translate('File %s already exists. Please delete it first.'), $file));
+ }
+ Logger::info($this->translate('Write %s configuration to file: %s'), $type, $file);
+ $re = file_put_contents($file, $config);
+ if ($re === false) {
+ $this->fail($this->translate('Could not write to file') . ': ' . $file);
+ }
+ Logger::info($this->translate('Successfully written %d bytes to file'), $re);
+ return true;
+ }
+ echo $config;
+ return true;
+ }
+}
diff --git a/modules/setup/application/clicommands/TokenCommand.php b/modules/setup/application/clicommands/TokenCommand.php
new file mode 100644
index 0000000..f1c30d1
--- /dev/null
+++ b/modules/setup/application/clicommands/TokenCommand.php
@@ -0,0 +1,89 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Clicommands;
+
+use Icinga\Cli\Command;
+
+/**
+ * Maintain the setup wizard's authentication
+ *
+ * The token command allows you to display the current setup token or to create a new one.
+ *
+ * Usage: icingacli setup token <action>
+ */
+class TokenCommand extends Command
+{
+ /**
+ * Display the current setup token
+ *
+ * Shows you the current setup token used to authenticate when setting up Icinga Web 2 using the web-based wizard.
+ *
+ * USAGE:
+ *
+ * icingacli setup token show [options]
+ *
+ * OPTIONS:
+ *
+ * --config=<directory> Path to Icinga Web 2's configuration files [/etc/icingaweb2]
+ */
+ public function showAction()
+ {
+ $configDir = $this->params->get('config', $this->app->getConfigDir());
+ if (! is_string($configDir) || strlen(trim($configDir)) === 0) {
+ $this->fail($this->translate(
+ 'The argument --config expects a path to Icinga Web 2\'s configuration files'
+ ));
+ }
+
+ $token = file_get_contents($configDir . '/setup.token');
+ if (! $token) {
+ $this->fail(
+ $this->translate('Nothing to show. Please create a new setup token using the generateToken action.')
+ );
+ }
+
+ printf($this->translate("The current setup token is: %s\n"), $token);
+ }
+
+ /**
+ * Create a new setup token
+ *
+ * Re-generates the setup token used to authenticate when setting up Icinga Web 2 using the web-based wizard.
+ *
+ * USAGE:
+ *
+ * icingacli setup token create [options]
+ *
+ * OPTIONS:
+ *
+ * --config=<directory> Path to Icinga Web 2's configuration files [/etc/icingaweb2]
+ */
+ public function createAction()
+ {
+ $configDir = $this->params->get('config', $this->app->getConfigDir());
+ if (! is_string($configDir) || strlen(trim($configDir)) === 0) {
+ $this->fail($this->translate(
+ 'The argument --config expects a path to Icinga Web 2\'s configuration files'
+ ));
+ }
+
+ $file = $configDir . '/setup.token';
+
+ if (function_exists('openssl_random_pseudo_bytes')) {
+ $token = bin2hex(openssl_random_pseudo_bytes(8));
+ } else {
+ $token = substr(md5(mt_rand()), 16);
+ }
+
+ if (false === file_put_contents($file, $token)) {
+ $this->fail(sprintf($this->translate('Cannot write setup token "%s" to disk.'), $file));
+ }
+
+ if (! chmod($file, 0660)) {
+ $this->fail(sprintf($this->translate('Cannot change access mode of "%s" to %o.'), $file, 0660));
+ }
+
+ printf($this->translate("The newly generated setup token is: %s\n"), $token);
+ }
+}
diff --git a/modules/setup/application/controllers/IndexController.php b/modules/setup/application/controllers/IndexController.php
new file mode 100644
index 0000000..b75643c
--- /dev/null
+++ b/modules/setup/application/controllers/IndexController.php
@@ -0,0 +1,91 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Controllers;
+
+use Icinga\Module\Setup\WebWizard;
+use Icinga\Web\Controller;
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+
+class IndexController extends Controller
+{
+ /**
+ * Whether the controller requires the user to be authenticated
+ *
+ * FALSE as the wizard uses token authentication
+ *
+ * @var bool
+ */
+ protected $requiresAuthentication = false;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $innerLayout = 'inline';
+
+ /**
+ * Show the web wizard and run the configuration once finished
+ */
+ public function indexAction()
+ {
+ $wizard = new WebWizard();
+
+ if ($wizard->isFinished()) {
+ $setup = $wizard->getSetup();
+ $success = $setup->run();
+ if ($success) {
+ $wizard->clearSession();
+ } else {
+ $wizard->setIsFinished(false);
+ }
+
+ $this->view->success = $success;
+ $this->view->report = $setup->getReport();
+ } else {
+ $wizard->handleRequest();
+
+ $restartForm = new Form();
+ $restartForm->setUidDisabled();
+ $restartForm->setName('setup_restart_form');
+ $restartForm->setAction(Url::fromPath('setup/index/restart'));
+ $restartForm->setAttrib('class', 'restart-form');
+ $restartForm->addElement(
+ 'button',
+ 'btn_submit',
+ array(
+ 'type' => 'submit',
+ 'value' => 'btn_submit',
+ 'escape' => false,
+ 'label' => $this->view->icon('reply-all'),
+ 'title' => $this->translate('Restart the setup'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+
+ $this->view->restartForm = $restartForm;
+ }
+
+ $this->view->wizard = $wizard;
+ $this->view->title = $this->translate('Setup') . ' :: ' . $this->view->defaultTitle;
+ }
+
+ /**
+ * Reset session and restart the wizard
+ */
+ public function restartAction()
+ {
+ $this->assertHttpMethod('POST');
+
+ $form = new Form(array(
+ 'onSuccess' => function () {
+ $wizard = new WebWizard();
+ $wizard->clearSession(false);
+ }
+ ));
+ $form->setUidDisabled();
+ $form->setRedirectUrl('setup');
+ $form->setSubmitLabel('btn_submit');
+ $form->handleRequest();
+ }
+}
diff --git a/modules/setup/application/forms/AdminAccountPage.php b/modules/setup/application/forms/AdminAccountPage.php
new file mode 100644
index 0000000..b33749e
--- /dev/null
+++ b/modules/setup/application/forms/AdminAccountPage.php
@@ -0,0 +1,431 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Authentication\User\ExternalBackend;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Authentication\User\DbUserBackend;
+use Icinga\Authentication\User\LdapUserBackend;
+use Icinga\Authentication\UserGroup\UserGroupBackend;
+use Icinga\Authentication\UserGroup\LdapUserGroupBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Data\Selectable;
+use Icinga\Exception\NotImplementedError;
+use Icinga\Protocol\Ldap\LdapQuery;
+use Icinga\Web\Form;
+
+/**
+ * Wizard page to define the initial administrative account
+ */
+class AdminAccountPage extends Form
+{
+ /**
+ * The resource configuration to use
+ *
+ * @var array
+ */
+ protected $resourceConfig;
+
+ /**
+ * The user backend configuration to use
+ *
+ * @var array
+ */
+ protected $backendConfig;
+
+ /**
+ * The user group backend configuration to use
+ *
+ * @var array
+ */
+ protected $groupConfig;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_admin_account');
+ $this->setTitle($this->translate('Administration', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'Now it\'s time to configure your first administrative account or group for Icinga Web 2.'
+ ));
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setResourceConfig(array $config)
+ {
+ $this->resourceConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Set the user backend configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setBackendConfig(array $config)
+ {
+ $this->backendConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Set the user group backend configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setGroupConfig(array $config = null)
+ {
+ $this->groupConfig = $config;
+ return $this;
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $choices = array();
+ $groups = [];
+ if ($this->backendConfig['backend'] !== 'db') {
+ $choices['by_name'] = $this->translate('By Name', 'setup.admin');
+ $choice = isset($formData['user_type']) ? $formData['user_type'] : 'by_name';
+
+ if (in_array($this->backendConfig['backend'], array('ldap', 'msldap'))) {
+ $groups = $this->fetchGroups();
+ if (! empty($groups)) {
+ $choices['user_group'] = $this->translate('User Group', 'setup.admin');
+ }
+ }
+ } else {
+ $choices['new_user'] = $this->translate('New User', 'setup.admin');
+ $choice = isset($formData['user_type']) ? $formData['user_type'] : 'new_user';
+ }
+
+ $users = [];
+ if (in_array($this->backendConfig['backend'], array('db', 'ldap', 'msldap'))) {
+ $users = $this->fetchUsers();
+ if (! empty($users)) {
+ $choices['existing_user'] = $this->translate('Existing User', 'setup.admin');
+ }
+ }
+
+ if (count($choices) > 1) {
+ $this->addElement(
+ 'select',
+ 'user_type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Type Of Definition'),
+ 'description' => $this->translate('Choose how to define the desired account.'),
+ 'multiOptions' => $choices,
+ 'value' => $choice
+ )
+ );
+ } else {
+ $this->addElement(
+ 'hidden',
+ 'user_type',
+ array(
+ 'required' => true,
+ 'value' => key($choices)
+ )
+ );
+ }
+
+ if ($choice === 'by_name') {
+ $this->addElement(
+ 'text',
+ 'by_name',
+ array(
+ 'required' => true,
+ 'value' => $this->getUsername(),
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate(
+ 'Define the initial administrative account by providing a username that reflects'
+ . ' a user created later or one that is authenticated using external mechanisms.'
+ )
+ )
+ );
+ }
+
+ if ($choice === 'user_group') {
+ $this->addElement(
+ 'select',
+ 'user_group',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Group Name'),
+ 'description' => $this->translate(
+ 'Choose a user group reported by the LDAP backend'
+ . ' to permit its members administrative access.',
+ 'setup.admin'
+ ),
+ 'multiOptions' => array_combine($groups, $groups)
+ )
+ );
+ }
+
+ if ($choice === 'existing_user') {
+ $this->addElement(
+ 'select',
+ 'existing_user',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Username'),
+ 'description' => sprintf(
+ $this->translate(
+ 'Choose a user reported by the %s backend as the initial administrative account.',
+ 'setup.admin'
+ ),
+ $this->backendConfig['backend'] === 'db'
+ ? $this->translate('database', 'setup.admin.authbackend')
+ : 'LDAP'
+ ),
+ 'multiOptions' => array_combine($users, $users)
+ )
+ );
+ }
+
+ if ($choice === 'new_user') {
+ $this->addElement(
+ 'text',
+ 'new_user',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate(
+ 'Enter the username to be used when creating an initial administrative account.'
+ )
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'new_user_password',
+ array(
+ 'required' => true,
+ 'renderPassword' => true,
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate(
+ 'Enter the password to assign to the newly created account.'
+ ),
+ 'autocomplete' => 'new-password'
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'new_user_2ndpass',
+ array(
+ 'required' => true,
+ 'renderPassword' => true,
+ 'label' => $this->translate('Repeat password'),
+ 'description' => $this->translate(
+ 'Please repeat the password given above to avoid typing errors.'
+ ),
+ 'validators' => array(
+ array('identical', false, array('new_user_password'))
+ )
+ )
+ );
+ }
+ }
+
+ /**
+ * Validate the given request data and ensure that any new user does not already exist
+ *
+ * @param array $data The request data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (false === parent::isValid($data)) {
+ return false;
+ }
+
+ if ($data['user_type'] === 'new_user' && $this->hasUser($data['new_user'])) {
+ $this->getElement('new_user')->addError($this->translate('Username already exists.'));
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return the name of the externally authenticated user
+ *
+ * @return string
+ */
+ protected function getUsername()
+ {
+ list($name, $_) = ExternalBackend::getRemoteUserInformation();
+ if ($name === null) {
+ return '';
+ }
+
+ if (isset($this->backendConfig['strip_username_regexp']) && $this->backendConfig['strip_username_regexp']) {
+ // No need to silence or log anything here because the pattern has
+ // already been successfully compiled during backend configuration
+ $name = preg_replace($this->backendConfig['strip_username_regexp'], '', $name);
+ }
+
+ return $name;
+ }
+
+ /**
+ * Return the names of all users the user backend currently provides
+ *
+ * @return array
+ */
+ protected function fetchUsers()
+ {
+ try {
+ $query = $this
+ ->createUserBackend()
+ ->select(array('user_name'))
+ ->order('user_name', 'asc', true);
+ if (in_array($this->backendConfig['backend'], array('ldap', 'msldap'))) {
+ /** @var LdapQuery $ldapQuery */
+ $ldapQuery = $query->getQuery();
+ $ldapQuery->setUsePagedResults();
+ }
+
+ return $query->fetchColumn();
+ } catch (Exception $_) {
+ // No need to handle anything special here. Error means no users found.
+ return array();
+ }
+ }
+
+ /**
+ * Return whether the user backend provides a user with the given name
+ *
+ * @param string $username
+ *
+ * @return bool
+ */
+ protected function hasUser($username)
+ {
+ try {
+ return $this
+ ->createUserBackend()
+ ->select()
+ ->where('user_name', $username)
+ ->count() > 1;
+ } catch (Exception $_) {
+ return false;
+ }
+ }
+
+ /**
+ * Create and return the user backend
+ *
+ * @return DbUserBackend|LdapUserBackend
+ */
+ protected function createUserBackend()
+ {
+ $resourceConfig = new Config();
+ $resourceConfig->setSection($this->resourceConfig['name'], $this->resourceConfig);
+ ResourceFactory::setConfig($resourceConfig);
+
+ $config = new ConfigObject($this->backendConfig);
+ $config->resource = $this->resourceConfig['name'];
+ return UserBackend::create(null, $config);
+ }
+
+ /**
+ * Return the names of all user groups the user group backend currently provides
+ *
+ * @return array
+ */
+ protected function fetchGroups()
+ {
+ try {
+ $query = $this
+ ->createUserGroupBackend()
+ ->select(array('group_name'));
+ if (in_array($this->backendConfig['backend'], array('ldap', 'msldap'))) {
+ /** @var LdapQuery $ldapQuery */
+ $ldapQuery = $query->getQuery();
+ $ldapQuery->setUsePagedResults();
+ }
+
+ return $query->fetchColumn();
+ } catch (Exception $_) {
+ // No need to handle anything special here. Error means no groups found.
+ return array();
+ }
+ }
+
+ /**
+ * Return whether the user group backend provides a user group with the given name
+ *
+ * @param string $groupname
+ *
+ * @return bool
+ */
+ protected function hasGroup($groupname)
+ {
+ try {
+ return $this
+ ->createUserGroupBackend()
+ ->select()
+ ->where('group_name', $groupname)
+ ->count() > 1;
+ } catch (Exception $_) {
+ return false;
+ }
+ }
+
+ /**
+ * Create and return the user group backend
+ *
+ * @return LdapUserGroupBackend
+ */
+ protected function createUserGroupBackend()
+ {
+ $resourceConfig = new Config();
+ $resourceConfig->setSection($this->resourceConfig['name'], $this->resourceConfig);
+ ResourceFactory::setConfig($resourceConfig);
+
+ $backendConfig = new Config();
+ $backendConfig->setSection($this->backendConfig['name'], array_merge(
+ $this->backendConfig,
+ array('resource' => $this->resourceConfig['name'])
+ ));
+ UserBackend::setConfig($backendConfig);
+
+ if (empty($this->groupConfig)) {
+ $groupConfig = new ConfigObject(array(
+ 'backend' => $this->backendConfig['backend'], // _Should_ be "db" or "msldap"
+ 'resource' => $this->resourceConfig['name'],
+ 'user_backend' => $this->backendConfig['name'] // Gets ignored if 'backend' is "db"
+ ));
+ } else {
+ $groupConfig = new ConfigObject($this->groupConfig);
+ }
+
+ $backend = UserGroupBackend::create(null, $groupConfig);
+ if (! $backend instanceof Selectable) {
+ throw new NotImplementedError('Unsupported, until #9772 has been resolved');
+ }
+
+ return $backend;
+ }
+}
diff --git a/modules/setup/application/forms/AuthBackendPage.php b/modules/setup/application/forms/AuthBackendPage.php
new file mode 100644
index 0000000..88c77e6
--- /dev/null
+++ b/modules/setup/application/forms/AuthBackendPage.php
@@ -0,0 +1,274 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Application\Config;
+use Icinga\Data\ResourceFactory;
+use Icinga\Forms\Config\UserBackendConfigForm;
+use Icinga\Forms\Config\UserBackend\DbBackendForm;
+use Icinga\Forms\Config\UserBackend\LdapBackendForm;
+use Icinga\Forms\Config\UserBackend\ExternalBackendForm;
+use Icinga\Web\Form;
+
+/**
+ * Wizard page to define authentication backend specific details
+ */
+class AuthBackendPage extends Form
+{
+ /**
+ * The resource configuration to use
+ *
+ * @var array
+ */
+ protected $config;
+
+ /**
+ * Default values for the subform's elements suggested by a previous step
+ *
+ * @var string[]
+ */
+ protected $suggestions = array();
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_authentication_backend');
+ $this->setTitle($this->translate('Authentication Backend', 'setup.page.title'));
+ $this->setValidatePartial(true);
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setResourceConfig(array $config)
+ {
+ $resourceConfig = new Config();
+ $resourceConfig->setSection($config['name'], $config);
+ ResourceFactory::setConfig($resourceConfig);
+
+ $this->config = $config;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ $this->addSkipValidationCheckbox();
+ }
+
+ $backendForm = null;
+ if (! isset($this->config) || $this->config['type'] === 'external') {
+ $backendForm = new ExternalBackendForm();
+ $backendForm->create($formData);
+ $this->addDescription($this->translate(
+ 'You\'ve chosen to authenticate using a web server\'s mechanism so it may be necessary'
+ . ' to adjust usernames before any permissions, restrictions, etc. are being applied.'
+ ));
+ } elseif ($this->config['type'] === 'db') {
+ $this->setRequiredCue(null);
+ $backendForm = new DbBackendForm();
+ $backendForm->setRequiredCue(null);
+ $backendForm->create($formData)->removeElement('resource');
+ $this->addDescription($this->translate(
+ 'As you\'ve chosen to use a database for authentication all you need '
+ . 'to do now is defining a name for your first authentication backend.'
+ ));
+ } elseif ($this->config['type'] === 'ldap') {
+ $type = null;
+ if (! isset($formData['type'])) {
+ if (isset($formData['backend'])) {
+ $formData['type'] = $type = $formData['backend'];
+ } elseif (isset($this->suggestions['backend'])) {
+ $formData['type'] = $type = $this->suggestions['backend'];
+ }
+ }
+
+ $backendForm = new LdapBackendForm();
+ $backendForm->setSuggestions($this->suggestions);
+ $backendForm->setResources(array($this->config['name']));
+ $backendForm->create($formData);
+ $backendForm->getElement('resource')->setIgnore(true);
+ $this->addDescription($this->translate(
+ 'Before you are able to authenticate using the LDAP connection defined earlier you need to'
+ . ' provide some more information so that Icinga Web 2 is able to locate account details.'
+ ));
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'ignore' => true,
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate(
+ 'The type of the resource being used for this authenticaton provider'
+ ),
+ 'multiOptions' => array(
+ 'ldap' => 'LDAP',
+ 'msldap' => 'ActiveDirectory'
+ ),
+ 'value' => $type
+ )
+ );
+ }
+
+ $backendForm->getElement('name')->setValue('icingaweb2');
+ $this->addSubForm($backendForm, 'backend_form');
+ }
+
+ /**
+ * Retrieve all form element values
+ *
+ * @param bool $suppressArrayNotation Ignored
+ *
+ * @return array
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues();
+ $values = array_merge($values, $values['backend_form']);
+ unset($values['backend_form']);
+ return $values;
+ }
+
+ /**
+ * Validate the given form data and check whether it's possible to authenticate using the configured backend
+ *
+ * @param array $data The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (! parent::isValid($data)) {
+ return false;
+ }
+
+ if (isset($this->config)) {
+ if ($this->config['type'] === 'ldap' && (
+ ! isset($data['skip_validation']) || $data['skip_validation'] == 0)
+ ) {
+ $self = clone $this;
+ $self->getSubForm('backend_form')->getElement('resource')->setIgnore(false);
+ $inspection = UserBackendConfigForm::inspectUserBackend($self);
+ if ($inspection && $inspection->hasError()) {
+ $this->error($inspection->getError());
+ $this->addSkipValidationCheckbox();
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Run the configured backend's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (isset($formData['backend_validation']) && parent::isValid($formData)) {
+ $self = clone $this;
+ if (($resourceElement = $self->getSubForm('backend_form')->getElement('resource')) !== null) {
+ $resourceElement->setIgnore(false);
+ }
+
+ $inspection = UserBackendConfigForm::inspectUserBackend($self);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_string($e) ? $e : join("\n", array_map($join, $e));
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ } elseif (isset($formData['discovery_btn']) || isset($formData['btn_discover_domain'])) {
+ return parent::isValidPartial($formData);
+ } elseif (! isset($formData['backend_validation'])) {
+ // This is usually done by isValid(Partial), but as we're not calling any of these...
+ $this->populate($formData);
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a checkbox to this form by which the user can skip the authentication validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'order' => 0,
+ 'ignore' => true,
+ 'required' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate('Check this to not to validate authentication using this backend')
+ )
+ );
+ }
+
+ /**
+ * Get default values for the subform's elements suggested by a previous step
+ *
+ * @return string[]
+ */
+ public function getSuggestions()
+ {
+ return $this->suggestions;
+ }
+
+ /**
+ * Set default values for the subform's elements suggested by a previous step
+ *
+ * @param string[] $suggestions
+ *
+ * @return $this
+ */
+ public function setSuggestions(array $suggestions)
+ {
+ $this->suggestions = $suggestions;
+
+ return $this;
+ }
+}
diff --git a/modules/setup/application/forms/AuthenticationPage.php b/modules/setup/application/forms/AuthenticationPage.php
new file mode 100644
index 0000000..52e3c66
--- /dev/null
+++ b/modules/setup/application/forms/AuthenticationPage.php
@@ -0,0 +1,69 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Authentication\User\ExternalBackend;
+use Icinga\Web\Form;
+use Icinga\Application\Platform;
+
+/**
+ * Wizard page to choose an authentication backend
+ */
+class AuthenticationPage extends Form
+{
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setRequiredCue(null);
+ $this->setName('setup_authentication_type');
+ $this->setTitle($this->translate('Authentication', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'Please choose how you want to authenticate when accessing Icinga Web 2.'
+ . ' Configuring backend specific details follows in a later step.'
+ ));
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ if (isset($formData['type']) && $formData['type'] === 'external') {
+ list($username, $_) = ExternalBackend::getRemoteUserInformation();
+ if ($username === null) {
+ $this->info(
+ $this->translate(
+ 'You\'re currently not authenticated using any of the web server\'s authentication '
+ . 'mechanisms. Make sure you\'ll configure such, otherwise you\'ll not be able to '
+ . 'log into Icinga Web 2.'
+ ),
+ false
+ );
+ }
+ }
+
+ $backendTypes = array();
+ if (Platform::hasMysqlSupport() || Platform::hasPostgresqlSupport()) {
+ $backendTypes['db'] = $this->translate('Database');
+ }
+ if (Platform::extensionLoaded('ldap')) {
+ $backendTypes['ldap'] = 'LDAP';
+ }
+ $backendTypes['external'] = $this->translate('External');
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Authentication Type'),
+ 'description' => $this->translate('The type of authentication to use when accessing Icinga Web 2'),
+ 'multiOptions' => $backendTypes
+ )
+ );
+ }
+}
diff --git a/modules/setup/application/forms/DatabaseCreationPage.php b/modules/setup/application/forms/DatabaseCreationPage.php
new file mode 100644
index 0000000..f7092a1
--- /dev/null
+++ b/modules/setup/application/forms/DatabaseCreationPage.php
@@ -0,0 +1,209 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use PDOException;
+use Icinga\Web\Form;
+use Icinga\Module\Setup\Utils\DbTool;
+
+/**
+ * Wizard page to define a database user that is able to create databases and tables
+ */
+class DatabaseCreationPage extends Form
+{
+ /**
+ * The resource configuration to use
+ *
+ * @var array
+ */
+ protected $config;
+
+ /**
+ * The required privileges to setup the database
+ *
+ * @var array
+ */
+ protected $databaseSetupPrivileges;
+
+ /**
+ * The required privileges to operate the database
+ *
+ * @var array
+ */
+ protected $databaseUsagePrivileges;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setTitle($this->translate('Database Setup', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'It seems that either the database you defined earlier does not yet exist and cannot be created'
+ . ' using the provided access credentials, the database does not have the required schema to be'
+ . ' operated by Icinga Web 2 or the provided access credentials do not have the sufficient '
+ . 'permissions to access the database. Please provide appropriate access credentials to solve this.'
+ ));
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setResourceConfig(array $config)
+ {
+ $this->config = $config;
+ return $this;
+ }
+
+ /**
+ * Set the required privileges to setup the database
+ *
+ * @param array $privileges The privileges
+ *
+ * @return $this
+ */
+ public function setDatabaseSetupPrivileges(array $privileges)
+ {
+ $this->databaseSetupPrivileges = $privileges;
+ return $this;
+ }
+
+ /**
+ * Set the required privileges to operate the database
+ *
+ * @param array $privileges The privileges
+ *
+ * @return $this
+ */
+ public function setDatabaseUsagePrivileges(array $privileges)
+ {
+ $this->databaseUsagePrivileges = $privileges;
+ return $this;
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $skipValidation = isset($formData['skip_validation']) && $formData['skip_validation'];
+ $this->addElement(
+ 'text',
+ 'username',
+ array(
+ 'required' => false === $skipValidation,
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate(
+ 'A user which is able to create databases and/or touch the database schema'
+ )
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'renderPassword' => true,
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate('The password for the database user defined above'),
+ 'autocomplete' => 'new-password'
+ )
+ );
+
+ if ($skipValidation) {
+ $this->addSkipValidationCheckbox();
+ } else {
+ $this->addElement(
+ 'hidden',
+ 'skip_validation',
+ array(
+ 'required' => true,
+ 'value' => 0
+ )
+ );
+ }
+ }
+
+ /**
+ * Validate the given form data and check whether the defined user has sufficient access rights
+ *
+ * @param array $data The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (false === parent::isValid($data)) {
+ return false;
+ }
+
+ if (isset($data['skip_validation']) && $data['skip_validation']) {
+ return true;
+ }
+
+ $config = $this->config;
+ $config['username'] = $this->getValue('username');
+ $config['password'] = $this->getValue('password');
+ $db = new DbTool($config);
+
+ try {
+ $db->connectToDb(); // Are we able to login on the database?
+ } catch (PDOException $_) {
+ try {
+ $db->connectToHost(); // Are we able to login on the server?
+ } catch (PDOException $e) {
+ // We are NOT able to login on the server..
+ $this->error($e->getMessage());
+ $this->addSkipValidationCheckbox();
+ return false;
+ }
+ }
+
+ // In case we are connected the credentials filled into this
+ // form need to be granted to create databases, users...
+ if (false === $db->checkPrivileges($this->databaseSetupPrivileges)) {
+ $this->error(
+ $this->translate('The provided credentials cannot be used to create the database and/or the user.')
+ );
+ $this->addSkipValidationCheckbox();
+ return false;
+ }
+
+ // ...and to grant all required usage privileges to others
+ if (false === $db->isGrantable($this->databaseUsagePrivileges)) {
+ $this->error(sprintf(
+ $this->translate(
+ 'The provided credentials cannot be used to grant all required privileges to the login "%s".'
+ ),
+ $this->config['username']
+ ));
+ $this->addSkipValidationCheckbox();
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a checkbox to the form by which the user can skip the login and privilege validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'order' => 0,
+ 'required' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate(
+ 'Check this to not to validate the ability to login and required privileges'
+ )
+ )
+ );
+ }
+}
diff --git a/modules/setup/application/forms/DbResourcePage.php b/modules/setup/application/forms/DbResourcePage.php
new file mode 100644
index 0000000..a417710
--- /dev/null
+++ b/modules/setup/application/forms/DbResourcePage.php
@@ -0,0 +1,183 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Exception;
+use Icinga\Web\Form;
+use Icinga\Forms\Config\Resource\DbResourceForm;
+use Icinga\Module\Setup\Utils\DbTool;
+
+/**
+ * Wizard page to define connection details for a database resource
+ */
+class DbResourcePage extends Form
+{
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setTitle($this->translate('Database Resource', 'setup.page.title'));
+ $this->setValidatePartial(true);
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'hidden',
+ 'type',
+ array(
+ 'required' => true,
+ 'value' => 'db'
+ )
+ );
+
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ $this->addSkipValidationCheckbox();
+ } else {
+ $this->addElement(
+ 'hidden',
+ 'skip_validation',
+ array(
+ 'required' => true,
+ 'value' => 0
+ )
+ );
+ }
+
+ $resourceForm = new DbResourceForm();
+ $this->addElements($resourceForm->createElements($formData)->getElements());
+ $this->getElement('name')->setValue('icingaweb_db');
+ }
+
+ /**
+ * Validate the given form data and check whether it's possible to connect to the database server
+ *
+ * @param array $data The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (false === parent::isValid($data)) {
+ return false;
+ }
+
+ if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) {
+ if (! $this->validateConfiguration()) {
+ $this->addSkipValidationCheckbox();
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Check whether it's possible to connect to the database server
+ *
+ * This will only run the check if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (isset($formData['backend_validation']) && parent::isValid($formData)) {
+ if (! $this->validateConfiguration()) {
+ return false;
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ } elseif (! isset($formData['backend_validation'])) {
+ // This is usually done by isValid(Partial), but as we're not calling any of these...
+ $this->populate($formData);
+ }
+
+ return true;
+ }
+
+ /**
+ * Return whether the configuration is valid
+ *
+ * @return bool
+ */
+ protected function validateConfiguration()
+ {
+ try {
+ $db = new DbTool($this->getValues());
+ $db->checkConnectivity();
+ } catch (Exception $e) {
+ $this->error(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $e->getMessage()
+ ));
+ return false;
+ }
+
+ $state = true;
+ $connectionError = null;
+
+ try {
+ $db->connectToDb();
+ } catch (Exception $e) {
+ $connectionError = $e;
+ }
+
+ if ($connectionError === null && array_search('icinga_instances', $db->listTables(), true) !== false) {
+ $this->warning($this->translate(
+ 'The database you\'ve configured to use for Icinga Web 2 seems to be the one of Icinga. Please be aware'
+ . ' that this database configuration is supposed to be used for Icinga Web 2\'s configuration and that'
+ . ' it is highly recommended to not mix different schemas in the same database. If this is intentional,'
+ . ' you can skip the validation and ignore this warning. If not, please provide a different database.'
+ ));
+ $state = false;
+ }
+
+ if ($this->getValue('db') === 'pgsql') {
+ if ($connectionError !== null) {
+// $this->warning(sprintf(
+// $this->translate('Unable to check the server\'s version. This is usually not a critical error'
+// . ' as there is probably only access to the database permitted which does not exist yet. If you are'
+// . ' absolutely sure you are running PostgreSQL in a version equal to or newer than 9.1,'
+// . ' you can skip the validation and safely proceed to the next step. The error was: %s'),
+// $connectionError->getMessage()
+// ));
+// $state = false;
+ } else {
+ $version = $db->getServerVersion();
+ if (version_compare($version, '9.1', '<')) {
+ $this->error(sprintf(
+ $this->translate('The server\'s version %s is too old. The minimum required version is %s.'),
+ $version,
+ '9.1'
+ ));
+ $state = false;
+ }
+ }
+ }
+
+ return $state;
+ }
+
+ /**
+ * Add a checkbox to the form by which the user can skip the configuration validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate('Check this to not to validate the configuration')
+ )
+ );
+ }
+}
diff --git a/modules/setup/application/forms/GeneralConfigPage.php b/modules/setup/application/forms/GeneralConfigPage.php
new file mode 100644
index 0000000..5b9f011
--- /dev/null
+++ b/modules/setup/application/forms/GeneralConfigPage.php
@@ -0,0 +1,41 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Forms\Config\General\ApplicationConfigForm;
+use Icinga\Forms\Config\General\LoggingConfigForm;
+use Icinga\Web\Form;
+
+/**
+ * Wizard page to define the application and logging configuration
+ */
+class GeneralConfigPage extends Form
+{
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_general_config');
+ $this->setTitle($this->translate('Application Configuration', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'Now please adjust all application and logging related configuration options to fit your needs.'
+ ));
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $appConfigForm = new ApplicationConfigForm();
+ $appConfigForm->createElements($formData);
+ $appConfigForm->removeElement('global_module_path');
+ $appConfigForm->removeElement('global_config_resource');
+ $this->addElements($appConfigForm->getElements());
+
+ $loggingConfigForm = new LoggingConfigForm();
+ $this->addElements($loggingConfigForm->createElements($formData)->getElements());
+ }
+}
diff --git a/modules/setup/application/forms/LdapDiscoveryConfirmPage.php b/modules/setup/application/forms/LdapDiscoveryConfirmPage.php
new file mode 100644
index 0000000..33bc907
--- /dev/null
+++ b/modules/setup/application/forms/LdapDiscoveryConfirmPage.php
@@ -0,0 +1,133 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Data\ConfigObject;
+use Icinga\Web\Form;
+
+/**
+ * Wizard page to define the connection details for a LDAP resource
+ */
+class LdapDiscoveryConfirmPage extends Form
+{
+ const TYPE_AD = 'MS ActiveDirectory';
+ const TYPE_MISC = 'LDAP';
+
+ private $infoTemplate = <<< 'EOT'
+<table><tbody>
+ <tr><td><strong>Type:</strong></td><td>{type}</td></tr>
+ <tr><td><strong>Port:</strong></td><td>{port}</td></tr>
+ <tr><td><strong>Root DN:</strong></td><td>{root_dn}</td></tr>
+ <tr><td><strong>User Object Class:</strong></td><td>{user_class}</td></tr>
+ <tr><td><strong>User Name Attribute:</strong></td><td>{user_attribute}</td></tr>
+</tbody></table>
+EOT;
+
+ /**
+ * The previous configuration
+ *
+ * @var array
+ */
+ private $config;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_ldap_discovery_confirm');
+ $this->setTitle($this->translate('LDAP Discovery Results', 'setup.page.title'));
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setResourceConfig(array $config)
+ {
+ $this->config = $config;
+ return $this;
+ }
+
+ /**
+ * Return the resource configuration as Config object
+ *
+ * @return ConfigObject
+ */
+ public function getResourceConfig()
+ {
+ return new ConfigObject($this->config);
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $resource = $this->config['resource'];
+ $backend = $this->config['backend'];
+ $html = $this->infoTemplate;
+ $html = str_replace('{type}', $this->config['type'], $html);
+ $html = str_replace('{hostname}', $resource['hostname'], $html);
+ $html = str_replace('{port}', $resource['port'], $html);
+ $html = str_replace('{root_dn}', $resource['root_dn'], $html);
+ $html = str_replace('{user_attribute}', $backend['user_name_attribute'], $html);
+ $html = str_replace('{user_class}', $backend['user_class'], $html);
+
+ $this->addDescription(sprintf(
+ $this->translate('The following directory service has been found on domain "%s".'),
+ $this->config['domain']
+ ));
+
+ $this->addElement(
+ 'note',
+ 'suggestion',
+ array(
+ 'value' => $html,
+ 'decorators' => array(
+ 'ViewHelper',
+ array(
+ 'HtmlTag', array('tag' => 'div')
+ )
+ )
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'confirm',
+ array(
+ 'value' => '1',
+ 'label' => $this->translate('Use this configuration?')
+ )
+ );
+ }
+
+ /**
+ * Validate the given form data and check whether a BIND-request is successful
+ *
+ * @param array $data The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (false === parent::isValid($data)) {
+ return false;
+ }
+ return true;
+ }
+
+ public function getValues($suppressArrayNotation = false)
+ {
+ if ($this->getValue('confirm') === '1') {
+ // use configuration
+ return $this->config;
+ }
+ return null;
+ }
+}
diff --git a/modules/setup/application/forms/LdapDiscoveryPage.php b/modules/setup/application/forms/LdapDiscoveryPage.php
new file mode 100644
index 0000000..7b5de17
--- /dev/null
+++ b/modules/setup/application/forms/LdapDiscoveryPage.php
@@ -0,0 +1,115 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Exception;
+use Zend_Validate_NotEmpty;
+use Icinga\Exception\IcingaException;
+use Icinga\Web\Form;
+use Icinga\Web\Form\ErrorLabeller;
+use Icinga\Forms\LdapDiscoveryForm;
+use Icinga\Protocol\Ldap\Discovery;
+use Icinga\Module\Setup\Forms\LdapDiscoveryConfirmPage;
+
+/**
+ * Wizard page to define the connection details for a LDAP resource
+ */
+class LdapDiscoveryPage extends Form
+{
+ /**
+ * @var Discovery
+ */
+ private $discovery;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_ldap_discovery');
+ $this->setTitle($this->translate('LDAP Discovery', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'You can use this page to discover LDAP or ActiveDirectory servers ' .
+ ' for authentication. If you don\'t want to execute a discovery, just skip this step.'
+ ));
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $discoveryForm = new LdapDiscoveryForm();
+ $this->addElements($discoveryForm->createElements($formData)->getElements());
+
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'label' => $this->translate('Skip'),
+ 'description' => $this->translate('Do not discover LDAP servers and enter all settings manually.')
+ )
+ );
+ }
+
+ /**
+ * Validate the given form data and check whether a BIND-request is successful
+ *
+ * @param array $data The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (false === parent::isValid($data)) {
+ return false;
+ }
+ if (isset($data['skip_validation']) && $data['skip_validation']) {
+ return true;
+ }
+
+ if (isset($data['domain']) && $data['domain']) {
+ try {
+ $this->discovery = Discovery::discoverDomain($data['domain']);
+ if ($this->discovery->isSuccess()) {
+ return true;
+ } else {
+ $this->error($this->discovery->getError()->getMessage());
+ }
+ } catch (Exception $e) {
+ $this->error(sprintf(
+ $this->translate('Could not find any LDAP servers on the domain "%s". An error occurred: %s'),
+ $data['domain'],
+ IcingaException::describe($e)
+ ));
+ }
+ } else {
+ $labeller = new ErrorLabeller(array('element' => $this->getElement('domain')));
+ $this->getElement('domain')->addError($labeller->translate(Zend_Validate_NotEmpty::IS_EMPTY));
+ }
+
+ return false;
+ }
+
+ /**
+ * Suggest settings based on the underlying discovery
+ *
+ * @param bool $suppressArrayNotation
+ *
+ * @return array
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ if (! isset($this->discovery) || ! $this->discovery->isSuccess()) {
+ return [];
+ }
+ $disc = $this->discovery;
+ return array(
+ 'domain' => $this->getValue('domain'),
+ 'type' => $disc->isAd() ? LdapDiscoveryConfirmPage::TYPE_AD : LdapDiscoveryConfirmPage::TYPE_MISC,
+ 'resource' => $disc->suggestResourceSettings(),
+ 'backend' => $disc->suggestBackendSettings()
+ );
+ }
+}
diff --git a/modules/setup/application/forms/LdapResourcePage.php b/modules/setup/application/forms/LdapResourcePage.php
new file mode 100644
index 0000000..7786407
--- /dev/null
+++ b/modules/setup/application/forms/LdapResourcePage.php
@@ -0,0 +1,152 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Web\Form;
+use Icinga\Forms\Config\ResourceConfigForm;
+use Icinga\Forms\Config\Resource\LdapResourceForm;
+
+/**
+ * Wizard page to define the connection details for a LDAP resource
+ */
+class LdapResourcePage extends Form
+{
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_ldap_resource');
+ $this->setTitle($this->translate('LDAP Resource', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'Now please configure your AD/LDAP resource. This will later '
+ . 'be used to authenticate users logging in to Icinga Web 2.'
+ ));
+ $this->setValidatePartial(true);
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'hidden',
+ 'type',
+ array(
+ 'required' => true,
+ 'value' => 'ldap'
+ )
+ );
+
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ $this->addSkipValidationCheckbox();
+ } else {
+ $this->addElement(
+ 'hidden',
+ 'skip_validation',
+ array(
+ 'required' => true,
+ 'value' => 0
+ )
+ );
+ }
+
+ $resourceForm = new LdapResourceForm();
+ $this->addElements($resourceForm->createElements($formData)->getElements());
+ $this->getElement('name')->setValue('icingaweb_ldap');
+ }
+
+ /**
+ * Validate the given form data and check whether a BIND-request is successful
+ *
+ * @param array $data The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (! parent::isValid($data)) {
+ return false;
+ }
+
+ if (! isset($data['skip_validation']) || $data['skip_validation'] == 0) {
+ $inspection = ResourceConfigForm::inspectResource($this);
+ if ($inspection !== null && $inspection->hasError()) {
+ $this->error($inspection->getError());
+ $this->addSkipValidationCheckbox();
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Run the configured backend's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (isset($formData['backend_validation']) && parent::isValid($formData)) {
+ $inspection = ResourceConfigForm::inspectResource($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_string($e) ? $e : join("\n", array_map($join, $e));
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ } elseif (! isset($formData['backend_validation'])) {
+ // This is usually done by isValid(Partial), but as we're not calling any of these...
+ $this->populate($formData);
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a checkbox to the form by which the user can skip the connection validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate(
+ 'Check this to not to validate connectivity with the given directory service'
+ )
+ )
+ );
+ }
+}
diff --git a/modules/setup/application/forms/ModulePage.php b/modules/setup/application/forms/ModulePage.php
new file mode 100644
index 0000000..d62b5a9
--- /dev/null
+++ b/modules/setup/application/forms/ModulePage.php
@@ -0,0 +1,108 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use Icinga\Web\Form;
+
+class ModulePage extends Form
+{
+ protected $modules;
+
+ protected $modulePaths;
+
+ protected $foundIcingaDB = false;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_modules');
+ $this->setViewScript('form/setup-modules.phtml');
+
+ $this->modulePaths = array();
+ if (($appModulePath = realpath(Icinga::app()->getApplicationDir() . '/../modules')) !== false) {
+ $this->modulePaths[] = $appModulePath;
+ }
+ }
+
+ public function createElements(array $formData)
+ {
+ foreach ($this->getModules() as $module) {
+ $checked = false;
+ if ($module->getName() === 'monitoring') {
+ $checked = ! $this->foundIcingaDB;
+ } elseif ($this->foundIcingaDB && $module->getName() === 'icingadb') {
+ $checked = true;
+ }
+
+ $this->addElement(
+ 'checkbox',
+ $module->getName(),
+ array(
+ 'description' => $module->getDescription(),
+ 'label' => ucfirst($module->getName()),
+ 'value' => (int) $checked,
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ }
+ }
+
+ /**
+ * @return Module[]
+ */
+ protected function getModules()
+ {
+ if ($this->modules !== null) {
+ return $this->modules;
+ } else {
+ $this->modules = array();
+ }
+
+ $moduleManager = Icinga::app()->getModuleManager();
+ $moduleManager->detectInstalledModules($this->modulePaths);
+ foreach ($moduleManager->listInstalledModules() as $moduleName) {
+ if ($moduleName !== 'setup') {
+ $this->modules[$moduleName] = $moduleManager->loadModule($moduleName)->getModule($moduleName);
+ }
+
+ if ($moduleName === 'icingadb') {
+ $this->foundIcingaDB = true;
+ }
+ }
+
+ return $this->modules;
+ }
+
+ public function getCheckedModules()
+ {
+ $modules = $this->getModules();
+
+ $checked = array();
+ foreach ($this->getElements() as $name => $element) {
+ if (array_key_exists($name, $modules) && $element->isChecked()) {
+ $checked[$name] = $modules[$name];
+ }
+ }
+
+ return $checked;
+ }
+
+ public function getModuleWizards()
+ {
+ $checked = $this->getCheckedModules();
+
+ $wizards = array();
+ foreach ($checked as $name => $module) {
+ if ($module->providesSetupWizard()) {
+ $wizards[$name] = $module->getSetupWizard();
+ }
+ }
+
+ return $wizards;
+ }
+}
diff --git a/modules/setup/application/forms/RequirementsPage.php b/modules/setup/application/forms/RequirementsPage.php
new file mode 100644
index 0000000..d1fb70e
--- /dev/null
+++ b/modules/setup/application/forms/RequirementsPage.php
@@ -0,0 +1,68 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Web\Form;
+use Icinga\Module\Setup\SetupWizard;
+
+/**
+ * Wizard page to list setup requirements
+ */
+class RequirementsPage extends Form
+{
+ /**
+ * The wizard
+ *
+ * @var SetupWizard
+ */
+ protected $wizard;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_requirements');
+ $this->setViewScript('form/setup-requirements.phtml');
+ }
+
+ /**
+ * Set the wizard
+ *
+ * @param SetupWizard $wizard
+ *
+ * @return $this
+ */
+ public function setWizard(SetupWizard $wizard)
+ {
+ $this->wizard = $wizard;
+ return $this;
+ }
+
+ /**
+ * Return the wizard
+ *
+ * @return SetupWizard
+ */
+ public function getWizard()
+ {
+ return $this->wizard;
+ }
+
+ /**
+ * Validate the given form data and check whether the wizard's requirements are fulfilled
+ *
+ * @param array $data The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($data)
+ {
+ if (false === parent::isValid($data)) {
+ return false;
+ }
+
+ return $this->wizard->getRequirements()->fulfilled();
+ }
+}
diff --git a/modules/setup/application/forms/SummaryPage.php b/modules/setup/application/forms/SummaryPage.php
new file mode 100644
index 0000000..ab62d55
--- /dev/null
+++ b/modules/setup/application/forms/SummaryPage.php
@@ -0,0 +1,84 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use LogicException;
+use Icinga\Web\Form;
+
+/**
+ * Wizard page that displays a summary of what is going to be "done"
+ */
+class SummaryPage extends Form
+{
+ /**
+ * The title of what is being set up
+ *
+ * @var string
+ */
+ protected $title;
+
+ /**
+ * The summary to show
+ *
+ * @var array
+ */
+ protected $summary;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ if ($this->getName() === $this->filterName(get_class($this))) {
+ throw new LogicException(
+ 'When utilizing ' . get_class($this) . ' it is required to set a unique name by using the form options'
+ );
+ }
+
+ $this->setViewScript('form/setup-summary.phtml');
+ }
+
+ /**
+ * Set the title of what is being set up
+ *
+ * @param string $title
+ */
+ public function setSubjectTitle($title)
+ {
+ $this->title = $title;
+ }
+
+ /**
+ * Return the title of what is being set up
+ *
+ * @return string
+ */
+ public function getSubjectTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * Set the summary to show
+ *
+ * @param array $summary
+ *
+ * @return $this
+ */
+ public function setSummary(array $summary)
+ {
+ $this->summary = $summary;
+ return $this;
+ }
+
+ /**
+ * Return the summary to show
+ *
+ * @return array
+ */
+ public function getSummary()
+ {
+ return $this->summary;
+ }
+}
diff --git a/modules/setup/application/forms/UserGroupBackendPage.php b/modules/setup/application/forms/UserGroupBackendPage.php
new file mode 100644
index 0000000..751270f
--- /dev/null
+++ b/modules/setup/application/forms/UserGroupBackendPage.php
@@ -0,0 +1,147 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Application\Config;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Data\ResourceFactory;
+use Icinga\Forms\Config\UserGroup\LdapUserGroupBackendForm;
+use Icinga\Web\Form;
+
+/**
+ * Wizard page to define user group backend specific details
+ */
+class UserGroupBackendPage extends Form
+{
+ /**
+ * The resource configuration to use
+ *
+ * @var array
+ */
+ protected $resourceConfig;
+
+ /**
+ * The user backend configuration to use
+ *
+ * @var array
+ */
+ protected $backendConfig;
+
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setName('setup_usergroup_backend');
+ $this->setTitle($this->translate('User Group Backend', 'setup.page.title'));
+ $this->addDescription($this->translate(
+ 'To allow Icinga Web 2 to associate users and groups, you\'ll need to provide some further information'
+ . ' about the LDAP Connection that is already going to be used to locate account details.'
+ ));
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setResourceConfig(array $config)
+ {
+ $this->resourceConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Set the user backend configuration to use
+ *
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setBackendConfig(array $config)
+ {
+ $this->backendConfig = $config;
+ return $this;
+ }
+
+ /**
+ * Return the resource configuration as Config object
+ *
+ * @return Config
+ */
+ protected function createResourceConfiguration()
+ {
+ $config = new Config();
+ $config->setSection($this->resourceConfig['name'], $this->resourceConfig);
+ return $config;
+ }
+
+ /**
+ * Return the user backend configuration as Config object
+ *
+ * @return Config
+ */
+ protected function createBackendConfiguration()
+ {
+ $config = new Config();
+ $backendConfig = $this->backendConfig;
+ $backendConfig['resource'] = $this->resourceConfig['name'];
+ $config->setSection($this->backendConfig['name'], $backendConfig);
+ return $config;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ // LdapUserGroupBackendForm requires these factories to provide valid configurations
+ ResourceFactory::setConfig($this->createResourceConfiguration());
+ UserBackend::setConfig($this->createBackendConfiguration());
+
+ $backendForm = new LdapUserGroupBackendForm();
+ $formData['type'] = 'ldap';
+ $backendForm->create($formData);
+ $backendForm->getElement('name')->setValue('icingaweb2');
+ $this->addSubForm($backendForm, 'backend_form');
+
+ $backendForm->addElement(
+ 'hidden',
+ 'resource',
+ array(
+ 'required' => true,
+ 'value' => $this->resourceConfig['name'],
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ $backendForm->addElement(
+ 'hidden',
+ 'user_backend',
+ array(
+ 'required' => true,
+ 'value' => $this->backendConfig['name'],
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ }
+
+ /**
+ * Retrieve all form element values
+ *
+ * @param bool $suppressArrayNotation Ignored
+ *
+ * @return array
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues();
+ $values = array_merge($values, $values['backend_form']);
+ unset($values['backend_form']);
+ return $values;
+ }
+}
diff --git a/modules/setup/application/forms/WelcomePage.php b/modules/setup/application/forms/WelcomePage.php
new file mode 100644
index 0000000..124a31f
--- /dev/null
+++ b/modules/setup/application/forms/WelcomePage.php
@@ -0,0 +1,45 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Forms;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Form;
+use Icinga\Module\Setup\Web\Form\Validator\TokenValidator;
+
+/**
+ * Wizard page to authenticate and welcome the user
+ */
+class WelcomePage extends Form
+{
+ /**
+ * Initialize this page
+ */
+ public function init()
+ {
+ $this->setRequiredCue(null);
+ $this->setName('setup_welcome');
+ $this->setViewScript('form/setup-welcome.phtml');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'token',
+ array(
+ 'class' => 'autofocus',
+ 'required' => true,
+ 'label' => $this->translate('Setup Token'),
+ 'description' => $this->translate(
+ 'For security reasons we need to ensure that you are permitted to run this wizard.'
+ . ' Please provide a token by following the instructions below.'
+ ),
+ 'validators' => array(new TokenValidator(Icinga::app()->getConfigDir() . '/setup.token'))
+ )
+ );
+ }
+}
diff --git a/modules/setup/application/views/scripts/form/setup-modules.phtml b/modules/setup/application/views/scripts/form/setup-modules.phtml
new file mode 100644
index 0000000..e57c7dc
--- /dev/null
+++ b/modules/setup/application/views/scripts/form/setup-modules.phtml
@@ -0,0 +1,33 @@
+<?php
+
+use Icinga\Web\Wizard;
+
+?>
+<form
+ id="<?= $this->escape($form->getName()); ?>"
+ name="<?= $this->escape($form->getName()); ?>"
+ enctype="<?= $this->escape($form->getEncType()); ?>"
+ method="<?= $this->escape($form->getMethod()); ?>"
+ action="<?= $this->escape($form->getAction()); ?>"
+ class="icinga-controls"
+ data-progress-element="<?= Wizard::PROGRESS_ELEMENT; ?>"
+>
+<h2><?= $this->translate('Modules', 'setup.page.title'); ?></h2>
+<p><?= $this->translate('The following modules were found in your Icinga Web 2 installation. To enable and configure a module, just tick it and click "Next".'); ?></p>
+<?php foreach ($form->getElements() as $element): ?>
+ <?php if (! in_array($element->getName(), array(Wizard::BTN_PREV, Wizard::BTN_NEXT, Wizard::PROGRESS_ELEMENT, $form->getTokenElementName(), $form->getUidElementName()))): ?>
+ <div class="module">
+ <div class="header">
+ <h3><label for="<?= $element->getId(); ?>"><strong><?= $element->getLabel(); ?></strong></label></h3>
+ <div class="element">
+ <?= $element; ?>
+ </div>
+ </div>
+ <label class="description" for="<?= $element->getId(); ?>"><?= $element->getDescription(); ?></label>
+ </div>
+ <?php endif ?>
+<?php endforeach ?>
+ <?= $form->getElement($form->getTokenElementName()); ?>
+ <?= $form->getElement($form->getUidElementName()); ?>
+ <?= $form->getDisplayGroup('buttons'); ?>
+</form>
diff --git a/modules/setup/application/views/scripts/form/setup-requirements.phtml b/modules/setup/application/views/scripts/form/setup-requirements.phtml
new file mode 100644
index 0000000..544f284
--- /dev/null
+++ b/modules/setup/application/views/scripts/form/setup-requirements.phtml
@@ -0,0 +1,48 @@
+<?php
+
+use Icinga\Web\Wizard;
+
+if (! $form->getWizard()->getRequirements()->fulfilled()) {
+ $form->getElement(Wizard::BTN_NEXT)->setAttrib('disabled', 1);
+}
+
+?>
+<h1>Icinga Web 2</h1>
+<?= $form->getWizard()->getRequirements(true); ?>
+<?php foreach ($form->getWizard()->getPage('setup_modules')->getModuleWizards() as $moduleName => $wizard): ?>
+<h1><?= ucwords($moduleName) . ' ' . $this->translate('Module'); ?></h1>
+<?= $wizard->getRequirements(); ?>
+<?php endforeach ?>
+<form
+ id="<?= $this->escape($form->getName()); ?>"
+ name="<?= $this->escape($form->getName()); ?>"
+ enctype="<?= $this->escape($form->getEncType()); ?>"
+ method="<?= $this->escape($form->getMethod()); ?>"
+ action="<?= $this->escape($form->getAction()); ?>"
+ data-progress-element="<?= Wizard::PROGRESS_ELEMENT; ?>"
+>
+ <?= $form->getElement($form->getTokenElementName()); ?>
+ <?= $form->getElement($form->getUidElementName()); ?>
+ <div class="buttons">
+ <?php
+ $double = clone $form->getElement(Wizard::BTN_NEXT);
+ echo $double->setAttrib('class', 'double');
+ ?>
+ <?= $form->getElement(Wizard::BTN_PREV); ?>
+ <?= $form->getElement(Wizard::BTN_NEXT); ?>
+ <?= $form->getElement(Wizard::PROGRESS_ELEMENT); ?>
+ <div class="requirements-refresh">
+ <?php $title = $this->translate('You may also need to restart the web-server for the changes to take effect!'); ?>
+ <?= $this->qlink(
+ $this->translate('Refresh'),
+ null,
+ null,
+ array(
+ 'class' => 'button-link',
+ 'title' => $title,
+ 'aria-label' => sprintf($this->translate('Refresh the page; %s'), $title)
+ )
+ ); ?>
+ </div>
+ </div>
+</form> \ No newline at end of file
diff --git a/modules/setup/application/views/scripts/form/setup-summary.phtml b/modules/setup/application/views/scripts/form/setup-summary.phtml
new file mode 100644
index 0000000..3ad0265
--- /dev/null
+++ b/modules/setup/application/views/scripts/form/setup-summary.phtml
@@ -0,0 +1,40 @@
+<?php
+
+use Icinga\Web\Wizard;
+
+$form->getElement(Wizard::BTN_NEXT)->setAttrib(
+ 'class',
+ $form->getElement(Wizard::BTN_NEXT)->getAttrib('class') . ' finish'
+);
+
+?>
+<p><?= sprintf(
+ $this->translate(
+ 'You\'ve configured %1$s successfully. You can review the changes supposed to be made before setting it up.'
+ . ' Make sure that everything is correct (Feel free to navigate back to make any corrections!) so'
+ . ' that you can start using %1$s right after it has successfully been set up.'
+ ),
+ $form->getSubjectTitle()
+); ?></p>
+<div class="summary">
+<?php foreach ($form->getSummary() as $pageHtml): ?>
+ <?php if ($pageHtml): ?>
+ <div class="page">
+ <?= $pageHtml; ?>
+ </div>
+ <?php endif ?>
+<?php endforeach ?>
+</div>
+<form
+ id="<?= $this->escape($form->getName()); ?>"
+ name="<?= $this->escape($form->getName()); ?>"
+ enctype="<?= $this->escape($form->getEncType()); ?>"
+ method="<?= $this->escape($form->getMethod()); ?>"
+ action="<?= $this->escape($form->getAction()); ?>"
+ data-progress-element="<?= Wizard::PROGRESS_ELEMENT; ?>"
+ class="summary"
+>
+ <?= $form->getElement($form->getTokenElementName()); ?>
+ <?= $form->getElement($form->getUidElementName()); ?>
+ <?= $form->getDisplayGroup('buttons'); ?>
+</form> \ No newline at end of file
diff --git a/modules/setup/application/views/scripts/form/setup-welcome.phtml b/modules/setup/application/views/scripts/form/setup-welcome.phtml
new file mode 100644
index 0000000..1be68f3
--- /dev/null
+++ b/modules/setup/application/views/scripts/form/setup-welcome.phtml
@@ -0,0 +1,120 @@
+<?php
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Config;
+use Icinga\Application\Platform;
+use Icinga\Web\Wizard;
+
+$phpUser = Platform::getPhpUser();
+$configDir = Icinga::app()->getConfigDir();
+$setupTokenPath = rtrim($configDir, '/') . '/setup.token';
+$cliPath = realpath(Icinga::app()->getApplicationDir() . '/../bin/icingacli');
+
+$groupadd = null;
+$docker = getenv('ICINGAWEB_OFFICIAL_DOCKER_IMAGE');
+
+if (! (false === ($distro = Platform::getLinuxDistro(1)) || $distro === 'linux')) {
+ foreach (array(
+ 'groupadd -r icingaweb2' => array(
+ 'redhat', 'rhel', 'centos', 'fedora',
+ 'suse', 'sles', 'sled', 'opensuse'
+ ),
+ 'addgroup --system icingaweb2' => array('debian', 'ubuntu')
+ ) as $groupadd_ => $distros) {
+ if (in_array($distro, $distros)) {
+ $groupadd = $groupadd_;
+ break;
+ }
+ }
+
+ switch ($distro) {
+ case 'redhat':
+ case 'rhel':
+ case 'centos':
+ case 'fedora':
+ $usermod = 'usermod -a -G icingaweb2 %s';
+ $webSrvUser = 'apache';
+ break;
+ case 'suse':
+ case 'sles':
+ case 'sled':
+ case 'opensuse':
+ $usermod = 'usermod -A icingaweb2 %s';
+ $webSrvUser = 'wwwrun';
+ break;
+ case 'debian':
+ case 'ubuntu':
+ $usermod = 'usermod -a -G icingaweb2 %s';
+ $webSrvUser = 'www-data';
+ break;
+ default:
+ $usermod = $webSrvUser = null;
+ }
+}
+?>
+<div class="welcome-page">
+ <h2><?= $this->translate('Welcome to the configuration of Icinga Web 2!') ?></h2>
+ <?php if (false === file_exists($setupTokenPath) && file_exists(Config::resolvePath('config.ini'))): ?>
+ <p class="restart-warning"><?= $this->translate(
+ 'You\'ve already completed the configuration of Icinga Web 2. Note that most of your configuration'
+ . ' files will be overwritten in case you\'ll re-configure Icinga Web 2 using this wizard!'
+ ); ?></p>
+ <?php else: ?>
+ <p><?= $this->translate(
+ 'This wizard will guide you through the configuration of Icinga Web 2. Once completed and successfully'
+ . ' finished you are able to log in and to explore all the new and stunning features!'
+ ); ?></p>
+ <?php endif ?>
+ <form id="<?= $form->getName(); ?>" name="<?= $form->getName(); ?>" enctype="<?= $form->getEncType(); ?>" method="<?= $form->getMethod(); ?>" action="<?= $form->getAction(); ?>" class="icinga-controls">
+ <?= $form->getElement('token'); ?>
+ <?= $form->getElement($form->getTokenElementName()); ?>
+ <?= $form->getElement($form->getUidElementName()); ?>
+ <div class="buttons">
+ <?= $form->getElement(Wizard::BTN_NEXT); ?>
+ </div>
+ </form>
+ <div class="note">
+ <h3><?= $this->translate('Generating a New Setup Token'); ?></h3>
+ <div>
+ <p><?=
+ $this->translate(
+ 'To run this wizard a user needs to authenticate using a token which is usually'
+ . ' provided to him by an administrator who\'d followed the instructions below.'
+ ); ?></p>
+ <?php if (! $docker): ?>
+ <p><?= $this->translate('In any case, make sure that all of the following applies to your environment:'); ?></p>
+ <ul>
+ <li><?= $this->translate('A system group called "icingaweb2" exists'); ?></li>
+ <?php if ($phpUser): ?>
+ <li><?= sprintf($this->translate('The user "%s" is a member of the system group "icingaweb2"'), $phpUser); ?></li>
+ <?php else: ?>
+ <li><?= $this->translate('Your webserver\'s user is a member of the system group "icingaweb2"'); ?></li>
+ <?php endif ?>
+ </ul>
+ <?php if (! ($groupadd === null || $usermod === null)) { ?>
+ <div class="code">
+ <span><?= $this->escape($groupadd . ';') ?></span>
+ <span><?= $this->escape(sprintf($usermod, $phpUser ?: $webSrvUser) . ';') ?></span>
+ </div>
+ <?php } ?>
+ <p><?= $this->translate('If you\'ve got the IcingaCLI installed you can do the following:'); ?></p>
+ <?php endif; ?>
+ <div class="code">
+ <?php if (! $docker): ?>
+ <span><?= $cliPath ? $cliPath : 'icingacli'; ?> setup config directory --group icingaweb2<?= $configDir !== '/etc/icingaweb2' ? ' --config ' . $configDir : ''; ?>;</span>
+ <?php endif; ?>
+ <span><?= $cliPath ? $cliPath : 'icingacli'; ?> setup token create;</span>
+ </div>
+ <?php if (! $docker): ?>
+ <p><?= $this->translate('In case the IcingaCLI is missing you can create the token manually:'); ?></p>
+ <div class="code">
+ <span>su <?= $phpUser ?: $this->translate('<your-webserver-user>'); ?> -s /bin/sh -c "mkdir -m 2770 <?= dirname($setupTokenPath); ?>; chgrp icingaweb2 <?= dirname($setupTokenPath); ?>; head -c 12 /dev/urandom | base64 | tee <?= $setupTokenPath; ?>; chmod 0660 <?= $setupTokenPath; ?>;";</span>
+ </div>
+ <?php endif; ?>
+ <p><?= sprintf(
+ $this->translate('Please see the %s for an extensive description on how to access and use this wizard.'),
+ '<a href="http://docs.icinga.com/">' . $this->translate('Icinga Web 2 documentation') . '</a>' // TODO: Add link to iw2 docs which points to the installation topic
+ ); ?></p>
+ </div>
+ </div>
+</div>
diff --git a/modules/setup/application/views/scripts/index/index.phtml b/modules/setup/application/views/scripts/index/index.phtml
new file mode 100644
index 0000000..32952e7
--- /dev/null
+++ b/modules/setup/application/views/scripts/index/index.phtml
@@ -0,0 +1,224 @@
+<?php
+
+use Icinga\Util\Csp;
+use Icinga\Web\Notification;
+use ipl\Web\Style;
+
+$pages = $wizard->getPages();
+$finished = isset($success);
+$configPages = array_slice($pages, 3, count($pages) - 4, true);
+$currentPos = array_search($wizard->getCurrentPage(), $pages, true);
+list($configPagesLeft, $configPagesRight) = array_chunk($configPages, (int)(count($configPages) / 2), true);
+$setupStyle = (new Style())
+ ->setSelector('.setup-header > .progress-bar')
+ ->setNonce(Csp::getStyleNonce());
+
+$visitedPages = array_keys($wizard->getPageData());
+$maxProgress = max(array_merge([0], array_keys(array_filter(
+ $pages,
+ function ($page) use ($visitedPages) { return in_array($page->getName(), $visitedPages); }
+))));
+
+$setupStyle->add(
+ '.width-percent-10',
+ ['width' => '10%']
+)->add(
+ '.width-percent-60',
+ ['width' => '60%']
+);
+?>
+<div id="setup-content-wrapper" data-base-target="layout">
+ <div class="setup-header">
+ <?= $this->img('img/icinga-logo-big.png'); ?>
+ <div class="progress-bar">
+ <div class="step width-percent-10">
+ <h1><?= $this->translate('Welcome', 'setup.progress'); ?></h1>
+ <?php $stateClass = $finished || $currentPos > 0 ? 'complete' : (
+ $maxProgress > 0 ? 'visited' : 'active'
+ ); ?>
+ <table><tbody><tr>
+ <td class="left"></td>
+ <td class="middle"><div class="bubble <?= $stateClass; ?>"></div></td>
+ <td class="right"><div class="line right <?= $stateClass; ?>"></div></td>
+ </tr></tbody></table>
+ </div>
+ <div class="step width-percent-10">
+ <h1><?= $this->translate('Modules', 'setup.progress'); ?></h1>
+ <?php $stateClass = $finished || $currentPos > 1 ? ' complete' : (
+ $maxProgress > 1 ? ' visited' : (
+ $currentPos === 1 ? ' active' : ''
+ )
+ ); ?>
+ <table><tbody><tr>
+ <td class="left"><div class="line left<?= $stateClass; ?>"></div></td>
+ <td class="middle"><div class="bubble <?= $stateClass; ?>"></div></td>
+ <td class="right"><div class="line right <?= $stateClass; ?>"></div></td>
+ </tr></tbody></table>
+ <?php if (($maxProgress < $currentPos && $currentPos === 1) || ($maxProgress >= $currentPos && $maxProgress === 1)): ?>
+ <?= $this->restartForm ?>
+ <?php endif ?>
+ </div>
+ <div class="step width-percent-10">
+ <h1><?= $this->translate('Requirements', 'setup.progress'); ?></h1>
+ <?php $stateClass = $finished || $currentPos > 2 ? ' complete' : (
+ $maxProgress > 2 ? ' visited' : (
+ $currentPos === 2 ? ' active' : ''
+ )
+ ); ?>
+ <table><tbody><tr>
+ <td class="left"><div class="line left<?= $stateClass; ?>"></div></td>
+ <td class="middle"><div class="bubble<?= $stateClass; ?>"></div></td>
+ <td class="right"><div class="line right<?= $stateClass; ?>"></div></td>
+ </tr></tbody></table>
+ <?php if (($maxProgress < $currentPos && $currentPos === 2) || ($maxProgress >= $currentPos && $maxProgress === 2)): ?>
+ <?= $this->restartForm ?>
+ <?php endif ?>
+ </div>
+ <div class="step width-percent-60">
+ <h1><?= $this->translate('Configuration', 'setup.progress'); ?></h1>
+ <table><tbody><tr>
+ <td class="left">
+ <?php
+ $firstPage = current($configPagesLeft);
+ $lastPage = end($configPagesLeft);
+ $lineWidth = sprintf('%.2F', round(100 / count($configPagesLeft), 2, PHP_ROUND_HALF_DOWN));
+ ?>
+ <?php foreach ($configPagesLeft as $pos => $page): ?>
+ <?php $stateClass = $finished || $pos < $currentPos ? ' complete' : (
+ $pos < $maxProgress ? ' visited' : ($currentPos > 2 ? ' active' : '')
+ ); ?>
+ <?php if ($page === $firstPage): ?>
+ <?php
+ $setupStyle->add(
+ '.step .left-line-' . $pos,
+ [
+ 'float' => 'left',
+ 'width' => sprintf(
+ '%.2F%%',
+ 100 - (count($configPagesLeft) - 1) * $lineWidth
+ ),
+ 'margin-right' => 0
+ ]
+ );
+ ?>
+ <div class="line left<?= $stateClass; ?> left-line-<?= $pos; ?>"></div>
+ <?php elseif ($page === $lastPage): ?>
+ <?php
+ $setupStyle->add(
+ '.step .left-line-' . $pos,
+ [
+ 'float' => 'left',
+ 'width' => $lineWidth . '%',
+ 'margin-right' => '-0.1em'
+ ]
+ );
+ ?>
+ <div class="line<?= $stateClass; ?> left-line-<?= $pos; ?>"></div>
+ <?php else: ?>
+ <?php
+ $setupStyle->add(
+ '.step .left-line-' . $pos,
+ [
+ 'float' => 'left',
+ 'width' => $lineWidth . '%'
+ ]
+ );
+ ?>
+ <div class="line<?= $stateClass; ?> left-line-<?= $pos; ?>"></div>
+ <?php endif ?>
+ <?php endforeach ?>
+ </td>
+ <td class="middle">
+ <div class="bubble<?= array_key_exists($currentPos, $configPagesLeft) ? (
+ key($configPagesRight) <= $maxProgress ? ' visited' : ' active') : (
+ $finished || $currentPos > 2 ? ' complete' : (
+ key($configPagesRight) < $maxProgress ? ' visited' : ''
+ )
+ ); ?>"></div>
+ </td>
+ <td class="right">
+ <?php
+ $firstPage = current($configPagesRight);
+ $lastPage = end($configPagesRight);
+ $lineWidth = sprintf('%.2F', round(100 / count($configPagesRight), 2, PHP_ROUND_HALF_DOWN));
+ ?>
+ <?php foreach ($configPagesRight as $pos => $page): ?>
+ <?php $stateClass = $finished || $pos < $currentPos ? ' complete' : (
+ $pos < $maxProgress ? ' visited' : ($currentPos > 2 ? ' active' : '')
+ ); ?>
+ <?php if ($page === $firstPage): ?>
+ <?php
+ $setupStyle->add(
+ '.step .right-line-' . $pos,
+ [
+ 'float' => 'left',
+ 'width' => $lineWidth . '%',
+ 'margin-right' => '-0.1em'
+ ]
+ );
+ ?>
+ <div class="line<?= $stateClass; ?> right-line-<?= $pos; ?>"></div>
+ <?php elseif ($page === $lastPage): ?>
+ <?php
+ $setupStyle->add(
+ '.step .right-line-' . $pos,
+ [
+ 'float' => 'left',
+ 'width' => sprintf(
+ '%.2F%%',
+ 100 - (count($configPagesRight) - 1) * $lineWidth
+ ),
+ 'margin-right' => 0
+ ]
+ );
+ ?>
+ <div class="line right<?= $stateClass; ?> right-line-<?= $pos; ?>"></div>
+ <?php else: ?>
+ <?php
+ $setupStyle->add(
+ '.step .right-line-' . $pos,
+ [
+ 'float' => 'left',
+ 'width' => $lineWidth . '%'
+ ]
+ );
+ ?>
+ <div class="line<?= $stateClass; ?> right-line-<?= $pos; ?>"></div>
+ <?php endif ?>
+ <?php endforeach ?>
+ </td>
+ </tr></tbody></table>
+ <?php if ($maxProgress > 2 || $currentPos > 2): ?>
+ <?= $this->restartForm ?>
+ <?php endif ?>
+ </div>
+ <div class="step width-percent-10">
+ <h1><?= $this->translate('Finish', 'setup.progress'); ?></h1>
+ <?php $stateClass = $finished ? ' complete' : ($pages[$currentPos] === end($pages) ? ' active' : ''); ?>
+ <table><tbody><tr>
+ <td class="left"><div class="line left<?= $stateClass; ?>"></div></td>
+ <td class="middle"><div class="bubble<?= $stateClass; ?>"></div></td>
+ <td class="right"></td>
+ </tr></tbody></table>
+ </div>
+ </div>
+ </div>
+ <div class="setup-content">
+<?php if ($finished): ?>
+ <?= $this->render('index/parts/finish.phtml'); ?>
+<?php else: ?>
+ <?= $this->render('index/parts/wizard.phtml'); ?>
+<?php endif ?>
+ </div>
+</div>
+<div id="footer">
+ <ul role="alert" id="notifications"><?php
+ $notifications = Notification::getInstance();
+ if ($notifications->hasMessages()) {
+ foreach ($notifications->popMessages() as $m) {
+ echo '<li class="' . $m->type . '">' . $this->escape($m->message) . '</li>';
+ }
+ }
+ ?></ul>
+</div>
+<?= $setupStyle; ?>
diff --git a/modules/setup/application/views/scripts/index/parts/finish.phtml b/modules/setup/application/views/scripts/index/parts/finish.phtml
new file mode 100644
index 0000000..dcb34dc
--- /dev/null
+++ b/modules/setup/application/views/scripts/index/parts/finish.phtml
@@ -0,0 +1,34 @@
+<div id="setup-finish">
+ <?php if ($success): ?>
+ <h2 class="success"><?= $this->translate('Congratulations! Icinga Web 2 has been successfully set up.'); ?></h2>
+ <?php else: ?>
+ <h2 class="failure"><?= $this->translate('Sorry! Failed to set up Icinga Web 2 successfully.'); ?></h2>
+ <?php endif ?>
+ <div class="buttons pull-right">
+ <?php if ($success): ?>
+ <?= $this->qlink(
+ $this->translate('Login to Icinga Web 2'),
+ 'authentication/login',
+ null,
+ array(
+ 'class' => 'button-link login',
+ 'data-no-icinga-ajax' => true,
+ 'title' => $this->translate('Show the login page of Icinga Web 2')
+ )
+ ); ?>
+ <?php else: ?>
+ <?= $this->qlink(
+ $this->translate('Back'),
+ null,
+ null,
+ array(
+ 'class' => 'button-link',
+ 'title' => $this->translate('Show previous wizard-page')
+ )
+ ); ?>
+ <?php endif ?>
+ </div>
+ <pre class="log-output"><?= join("\n\n", array_map(function($a) {
+ return join("\n", $a);
+ }, $report ?? [])); ?></pre>
+</div>
diff --git a/modules/setup/application/views/scripts/index/parts/wizard.phtml b/modules/setup/application/views/scripts/index/parts/wizard.phtml
new file mode 100644
index 0000000..94891f9
--- /dev/null
+++ b/modules/setup/application/views/scripts/index/parts/wizard.phtml
@@ -0,0 +1 @@
+<?= $wizard->getForm()->render(); ?> \ No newline at end of file
diff --git a/modules/setup/library/Setup/Exception/SetupException.php b/modules/setup/library/Setup/Exception/SetupException.php
new file mode 100644
index 0000000..c3ae591
--- /dev/null
+++ b/modules/setup/library/Setup/Exception/SetupException.php
@@ -0,0 +1,22 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Exception;
+
+use Icinga\Exception\IcingaException;
+
+/**
+ * Class SetupException
+ *
+ * Used to indicate that a setup should be aborted.
+ */
+class SetupException extends IcingaException
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct()
+ {
+ parent::__construct('Setup abortion');
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement.php b/modules/setup/library/Setup/Requirement.php
new file mode 100644
index 0000000..1df02ef
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement.php
@@ -0,0 +1,343 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+use LogicException;
+
+abstract class Requirement
+{
+ /**
+ * The state of this requirement
+ *
+ * @var bool
+ */
+ protected $state;
+
+ /**
+ * A descriptive text representing the current state of this requirement
+ *
+ * @var string
+ */
+ protected $stateText;
+
+ /**
+ * The descriptions of this requirement
+ *
+ * @var array
+ */
+ protected $descriptions;
+
+ /**
+ * The title of this requirement
+ *
+ * @var string
+ */
+ protected $title;
+
+ /**
+ * The condition of this requirement
+ *
+ * @var mixed
+ */
+ protected $condition;
+
+ /**
+ * Whether this requirement is optional
+ *
+ * @var bool
+ */
+ protected $optional;
+
+ /**
+ * The alias to display the condition with in a human readable way
+ *
+ * @var string
+ */
+ protected $alias;
+
+ /**
+ * The text to display if the given requirement is fulfilled
+ *
+ * @var string
+ */
+ protected $textAvailable;
+
+ /**
+ * The text to display if the given requirement is not fulfilled
+ *
+ * @var string
+ */
+ protected $textMissing;
+
+ /**
+ * Create a new requirement
+ *
+ * @param array $options
+ *
+ * @throws LogicException In case there exists no setter for an option's key
+ */
+ public function __construct(array $options = array())
+ {
+ $this->optional = false;
+ $this->descriptions = array();
+
+ foreach ($options as $key => $value) {
+ $setMethod = 'set' . ucfirst($key);
+ $addMethod = 'add' . ucfirst($key);
+ if (method_exists($this, $setMethod)) {
+ $this->$setMethod($value);
+ } elseif (method_exists($this, $addMethod)) {
+ $this->$addMethod($value);
+ } else {
+ throw new LogicException('No setter found for option key: ' . $key);
+ }
+ }
+ }
+
+ /**
+ * Set the state of this requirement
+ *
+ * @param bool $state
+ *
+ * @return Requirement
+ */
+ public function setState($state)
+ {
+ $this->state = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return the state of this requirement
+ *
+ * Evaluates the requirement in case there is no state set yet.
+ *
+ * @return int
+ */
+ public function getState()
+ {
+ if ($this->state === null) {
+ $this->state = $this->evaluate();
+ }
+
+ return $this->state;
+ }
+
+ /**
+ * Set a descriptive text for this requirement's current state
+ *
+ * @param string $text
+ *
+ * @return Requirement
+ */
+ public function setStateText($text)
+ {
+ $this->stateText = $text;
+ return $this;
+ }
+
+ /**
+ * Return a descriptive text for this requirement's current state
+ *
+ * @return string
+ */
+ public function getStateText()
+ {
+ $state = $this->getState();
+ if ($this->stateText === null) {
+ return $state ? $this->getTextAvailable() : $this->getTextMissing();
+ }
+ return $this->stateText;
+ }
+
+ /**
+ * Add a description for this requirement
+ *
+ * @param string $description
+ *
+ * @return Requirement
+ */
+ public function addDescription($description)
+ {
+ $this->descriptions[] = $description;
+ return $this;
+ }
+
+ /**
+ * Return the descriptions of this wizard
+ *
+ * @return array
+ */
+ public function getDescriptions()
+ {
+ return $this->descriptions;
+ }
+
+ /**
+ * Set the title for this requirement
+ *
+ * @param string $title
+ *
+ * @return Requirement
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ /**
+ * Return the title of this requirement
+ *
+ * In case there is no title set the alias is returned instead.
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ if ($this->title === null) {
+ return $this->getAlias();
+ }
+
+ return $this->title;
+ }
+
+ /**
+ * Set the condition for this requirement
+ *
+ * @param mixed $condition
+ *
+ * @return Requirement
+ */
+ public function setCondition($condition)
+ {
+ $this->condition = $condition;
+ return $this;
+ }
+
+ /**
+ * Return the condition of this requirement
+ *
+ * @return mixed
+ */
+ public function getCondition()
+ {
+ return $this->condition;
+ }
+
+ /**
+ * Set whether this requirement is optional
+ *
+ * @param bool $state
+ *
+ * @return Requirement
+ */
+ public function setOptional($state = true)
+ {
+ $this->optional = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether this requirement is optional
+ *
+ * @return bool
+ */
+ public function isOptional()
+ {
+ return $this->optional;
+ }
+
+ /**
+ * Set the alias to display the condition with in a human readable way
+ *
+ * @param string $alias
+ *
+ * @return Requirement
+ */
+ public function setAlias($alias)
+ {
+ $this->alias = $alias;
+ return $this;
+ }
+
+ /**
+ * Return the alias to display the condition with in a human readable way
+ *
+ * @return string
+ */
+ public function getAlias()
+ {
+ return $this->alias;
+ }
+
+ /**
+ * Set the text to display if the given requirement is fulfilled
+ *
+ * @param string $textAvailable
+ *
+ * @return Requirement
+ */
+ public function setTextAvailable($textAvailable)
+ {
+ $this->textAvailable = $textAvailable;
+ return $this;
+ }
+
+ /**
+ * Get the text to display if the given requirement is fulfilled
+ *
+ * @return string
+ */
+ public function getTextAvailable()
+ {
+ return $this->textAvailable;
+ }
+
+ /**
+ * Set the text to display if the given requirement is not fulfilled
+ *
+ * @param string $textMissing
+ *
+ * @return Requirement
+ */
+ public function setTextMissing($textMissing)
+ {
+ $this->textMissing = $textMissing;
+ return $this;
+ }
+
+ /**
+ * Get the text to display if the given requirement is not fulfilled
+ *
+ * @return string
+ */
+ public function getTextMissing()
+ {
+ return $this->textMissing;
+ }
+
+ /**
+ * Evaluate this requirement and return whether it is fulfilled
+ *
+ * @return bool
+ */
+ abstract protected function evaluate();
+
+ /**
+ * Return whether the given requirement equals this one
+ *
+ * @param Requirement $requirement
+ *
+ * @return bool
+ */
+ public function equals(Requirement $requirement)
+ {
+ if ($requirement instanceof static) {
+ return $this->getCondition() === $requirement->getCondition();
+ }
+
+ return false;
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/ClassRequirement.php b/modules/setup/library/Setup/Requirement/ClassRequirement.php
new file mode 100644
index 0000000..d884c31
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/ClassRequirement.php
@@ -0,0 +1,48 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Application\Platform;
+use Icinga\Module\Setup\Requirement;
+
+class ClassRequirement extends Requirement
+{
+ protected function evaluate()
+ {
+ return Platform::classExists($this->getCondition());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStateText()
+ {
+ $stateText = parent::getStateText();
+ if ($stateText === null) {
+ $alias = $this->getAlias();
+ if ($this->getState()) {
+ $stateText = $alias === null
+ ? sprintf(
+ mt('setup', 'The %s class is available.', 'setup.requirement.class'),
+ $this->getCondition()
+ )
+ : sprintf(
+ mt('setup', 'The %s is available.', 'setup.requirement.class'),
+ $alias
+ );
+ } else {
+ $stateText = $alias === null
+ ? sprintf(
+ mt('setup', 'The %s class is missing.', 'setup.requirement.class'),
+ $this->getCondition()
+ )
+ : sprintf(
+ mt('setup', 'The %s is missing.', 'setup.requirement.class'),
+ $alias
+ );
+ }
+ }
+ return $stateText;
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php b/modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php
new file mode 100644
index 0000000..7e9044c
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php
@@ -0,0 +1,42 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Module\Setup\Requirement;
+
+class ConfigDirectoryRequirement extends Requirement
+{
+ public function getTitle()
+ {
+ $title = parent::getTitle();
+ if ($title === null) {
+ return mt('setup', 'Read- and writable configuration directory');
+ }
+
+ return $title;
+ }
+
+ protected function evaluate()
+ {
+ $path = $this->getCondition();
+ if (file_exists($path)) {
+ $readable = is_readable($path);
+ if ($readable && is_writable($path)) {
+ $this->setStateText(sprintf(mt('setup', 'The directory %s is read- and writable.'), $path));
+ return true;
+ } else {
+ $this->setStateText(sprintf(
+ $readable
+ ? mt('setup', 'The directory %s is not writable.')
+ : mt('setup', 'The directory %s is not readable.'),
+ $path
+ ));
+ return false;
+ }
+ } else {
+ $this->setStateText(sprintf(mt('setup', 'The directory %s does not exist.'), $path));
+ return false;
+ }
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/OSRequirement.php b/modules/setup/library/Setup/Requirement/OSRequirement.php
new file mode 100644
index 0000000..760c97a
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/OSRequirement.php
@@ -0,0 +1,27 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Application\Platform;
+use Icinga\Module\Setup\Requirement;
+
+class OSRequirement extends Requirement
+{
+ public function getTitle()
+ {
+ $title = parent::getTitle();
+ if ($title === null) {
+ return sprintf(mt('setup', '%s Platform'), ucfirst($this->getCondition()));
+ }
+
+ return $title;
+ }
+
+ protected function evaluate()
+ {
+ $phpOS = Platform::getOperatingSystemName();
+ $this->setStateText(sprintf(mt('setup', 'You are running PHP on a %s system.'), ucfirst($phpOS)));
+ return strtolower($phpOS) === strtolower($this->getCondition());
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/PhpConfigRequirement.php b/modules/setup/library/Setup/Requirement/PhpConfigRequirement.php
new file mode 100644
index 0000000..6c77af5
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/PhpConfigRequirement.php
@@ -0,0 +1,22 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Application\Platform;
+use Icinga\Module\Setup\Requirement;
+
+class PhpConfigRequirement extends Requirement
+{
+ protected function evaluate()
+ {
+ list($configDirective, $value) = $this->getCondition();
+ $configValue = Platform::getPhpConfig($configDirective);
+ $this->setStateText(
+ $configValue
+ ? sprintf(mt('setup', 'The PHP config `%s\' is set to "%s".'), $configDirective, $configValue)
+ : sprintf(mt('setup', 'The PHP config `%s\' is not defined.'), $configDirective)
+ );
+ return is_bool($value) ? $configValue == $value : $configValue === $value;
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/PhpModuleRequirement.php b/modules/setup/library/Setup/Requirement/PhpModuleRequirement.php
new file mode 100644
index 0000000..f8ab129
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/PhpModuleRequirement.php
@@ -0,0 +1,42 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Application\Platform;
+use Icinga\Module\Setup\Requirement;
+
+class PhpModuleRequirement extends Requirement
+{
+ public function getTitle()
+ {
+ $title = parent::getTitle();
+ if ($title === $this->getAlias()) {
+ if ($title === null) {
+ $title = $this->getCondition();
+ }
+
+ return sprintf(mt('setup', 'PHP Module: %s'), $title);
+ }
+
+ return $title;
+ }
+
+ protected function evaluate()
+ {
+ $moduleName = $this->getCondition();
+ if (Platform::extensionLoaded($moduleName)) {
+ $this->setStateText(sprintf(
+ mt('setup', 'The PHP module %s is available.'),
+ $this->getAlias() ?: $moduleName
+ ));
+ return true;
+ } else {
+ $this->setStateText(sprintf(
+ mt('setup', 'The PHP module %s is missing.'),
+ $this->getAlias() ?: $moduleName
+ ));
+ return false;
+ }
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/PhpVersionRequirement.php b/modules/setup/library/Setup/Requirement/PhpVersionRequirement.php
new file mode 100644
index 0000000..b811ca8
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/PhpVersionRequirement.php
@@ -0,0 +1,28 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Application\Platform;
+use Icinga\Module\Setup\Requirement;
+
+class PhpVersionRequirement extends Requirement
+{
+ public function getTitle()
+ {
+ $title = parent::getTitle();
+ if ($title === null) {
+ return mt('setup', 'PHP Version');
+ }
+
+ return $title;
+ }
+
+ protected function evaluate()
+ {
+ $phpVersion = Platform::getPhpVersion();
+ $this->setStateText(sprintf(mt('setup', 'You are running PHP version %s.'), $phpVersion));
+ list($operator, $requiredVersion) = $this->getCondition();
+ return version_compare($phpVersion, $requiredVersion, $operator);
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/SetRequirement.php b/modules/setup/library/Setup/Requirement/SetRequirement.php
new file mode 100644
index 0000000..77cbaf0
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/SetRequirement.php
@@ -0,0 +1,34 @@
+<?php
+/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Module\Setup\Requirement;
+
+/**
+ * Add requirement field
+ *
+ * @package Icinga\Module\Setup\Requirement
+ */
+class SetRequirement extends Requirement
+{
+ protected function evaluate()
+ {
+ $condition = $this->getCondition();
+
+ if ($condition->getState()) {
+ $this->setStateText(sprintf(
+ mt('setup', '%s is available.'),
+ $this->getAlias() ?: $this->getTitle()
+ ));
+ return true;
+ }
+
+ $this->setStateText(sprintf(
+ mt('setup', '%s is missing.'),
+ $this->getAlias() ?: $this->getTitle()
+ ));
+
+ return false;
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/WebLibraryRequirement.php b/modules/setup/library/Setup/Requirement/WebLibraryRequirement.php
new file mode 100644
index 0000000..bab587a
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/WebLibraryRequirement.php
@@ -0,0 +1,24 @@
+<?php
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Setup\Requirement;
+
+class WebLibraryRequirement extends Requirement
+{
+ protected function evaluate()
+ {
+ list($name, $op, $version) = $this->getCondition();
+
+ $libs = Icinga::app()->getLibraries();
+ if (! $libs->has($name)) {
+ $this->setStateText(sprintf(mt('setup', '%s is not installed'), $this->getAlias()));
+ return false;
+ }
+
+ $this->setStateText(sprintf(mt('setup', '%s version: %s'), $this->getAlias(), $libs->get($name)->getVersion()));
+ return $libs->has($name, $op . $version);
+ }
+}
diff --git a/modules/setup/library/Setup/Requirement/WebModuleRequirement.php b/modules/setup/library/Setup/Requirement/WebModuleRequirement.php
new file mode 100644
index 0000000..ad600e1
--- /dev/null
+++ b/modules/setup/library/Setup/Requirement/WebModuleRequirement.php
@@ -0,0 +1,31 @@
+<?php
+/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Setup\Requirement;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Setup\Requirement;
+
+class WebModuleRequirement extends Requirement
+{
+ protected function evaluate()
+ {
+ list($name, $op, $version) = $this->getCondition();
+
+ $mm = Icinga::app()->getModuleManager();
+ if (! $mm->hasInstalled($name)) {
+ $this->setStateText(sprintf(mt('setup', '%s is not installed'), $this->getAlias()));
+ return false;
+ }
+
+ $module = $mm->getModule($name, false);
+
+ $moduleVersion = $module->getVersion();
+ if ($moduleVersion[0] === 'v') {
+ $moduleVersion = substr($moduleVersion, 1);
+ }
+
+ $this->setStateText(sprintf(mt('setup', '%s version: %s'), $this->getAlias(), $moduleVersion));
+ return version_compare($moduleVersion, $version, $op);
+ }
+}
diff --git a/modules/setup/library/Setup/RequirementSet.php b/modules/setup/library/Setup/RequirementSet.php
new file mode 100644
index 0000000..0baf4c0
--- /dev/null
+++ b/modules/setup/library/Setup/RequirementSet.php
@@ -0,0 +1,335 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+use LogicException;
+use RecursiveIterator;
+use Traversable;
+
+/**
+ * Container to store and handle requirements
+ */
+class RequirementSet implements RecursiveIterator
+{
+ /**
+ * Mode AND (all requirements must be met)
+ */
+ const MODE_AND = 0;
+
+ /**
+ * Mode OR (at least one requirement must be met)
+ */
+ const MODE_OR = 1;
+
+ /**
+ * Whether all requirements meet their condition
+ *
+ * @var bool
+ */
+ protected $state;
+
+ /**
+ * Whether this set is optional
+ *
+ * @var bool
+ */
+ protected $optional;
+
+ /**
+ * The mode by which the requirements are evaluated
+ *
+ * @var string
+ */
+ protected $mode;
+
+ /**
+ * The registered requirements
+ *
+ * @var array
+ */
+ protected $requirements;
+
+ /**
+ * The raw state of this set's requirements
+ *
+ * @var bool
+ */
+ private $forcedState;
+
+ /**
+ * Initialize a new set of requirements
+ *
+ * @param bool $optional Whether this set is optional
+ * @param int $mode The mode by which to evaluate this set
+ */
+ public function __construct($optional = false, $mode = null)
+ {
+ $this->optional = $optional;
+ $this->requirements = array();
+ $this->setMode($mode ?: static::MODE_AND);
+ }
+
+ /**
+ * Set the state of this set
+ *
+ * @param bool $state
+ *
+ * @return RequirementSet
+ */
+ public function setState($state)
+ {
+ $this->state = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return the state of this set
+ *
+ * Alias for RequirementSet::fulfilled(true).
+ *
+ * @return bool
+ */
+ public function getState()
+ {
+ return $this->fulfilled(true);
+ }
+
+ /**
+ * Set whether this set of requirements should be optional
+ *
+ * @param bool $state
+ *
+ * @return RequirementSet
+ */
+ public function setOptional($state = true)
+ {
+ $this->optional = (bool) $state;
+ return $this;
+ }
+
+ /**
+ * Return whether this set of requirements is optional
+ *
+ * @return bool
+ */
+ public function isOptional()
+ {
+ return $this->optional;
+ }
+
+ /**
+ * Set the mode by which to evaluate the requirements
+ *
+ * @param int $mode
+ *
+ * @return RequirementSet
+ *
+ * @throws LogicException In case the given mode is invalid
+ */
+ public function setMode($mode)
+ {
+ if ($mode !== static::MODE_AND && $mode !== static::MODE_OR) {
+ throw new LogicException(sprintf('Invalid mode %u given.', $mode));
+ }
+
+ $this->mode = $mode;
+ return $this;
+ }
+
+ /**
+ * Return the mode by which the requirements are evaluated
+ *
+ * @return int
+ */
+ public function getMode()
+ {
+ return $this->mode;
+ }
+
+ /**
+ * Register a requirement
+ *
+ * @param Requirement $requirement The requirement to add
+ *
+ * @return RequirementSet
+ */
+ public function add(Requirement $requirement)
+ {
+ $merged = false;
+ foreach ($this->requirements as $knownRequirement) {
+ if ($knownRequirement instanceof Requirement && $requirement->equals($knownRequirement)) {
+ $knownRequirement->setOptional($requirement->isOptional());
+ foreach ($requirement->getDescriptions() as $description) {
+ $knownRequirement->addDescription($description);
+ }
+
+ $merged = true;
+ break;
+ }
+ }
+
+ if (! $merged) {
+ $this->requirements[] = $requirement;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return all registered requirements
+ *
+ * @return array
+ */
+ public function getAll()
+ {
+ return $this->requirements;
+ }
+
+ /**
+ * Register the given set of requirements
+ *
+ * @param RequirementSet $set The set to register
+ *
+ * @return RequirementSet
+ */
+ public function merge(RequirementSet $set)
+ {
+ if ($this->getMode() === $set->getMode() && $this->isOptional() === $set->isOptional()) {
+ foreach ($set->getAll() as $requirement) {
+ if ($requirement instanceof static) {
+ $this->merge($requirement);
+ } else {
+ $this->add($requirement);
+ }
+ }
+ } else {
+ $this->requirements[] = $set;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return whether all requirements can successfully be evaluated based on the current mode
+ *
+ * In case this is a optional set of requirements (and $force is false), true is returned immediately.
+ *
+ * @param bool $force Whether to ignore the optionality of a set or single requirement
+ *
+ * @return bool
+ */
+ public function fulfilled($force = false)
+ {
+ $state = $this->isOptional();
+ if (! $force && $state) {
+ return true;
+ }
+
+ if (! $force && $this->state !== null) {
+ return $this->state;
+ } elseif ($force && $this->forcedState !== null) {
+ return $this->forcedState;
+ }
+
+ $self = $this->requirements;
+ foreach ($self as $requirement) {
+ if ($requirement->getState()) {
+ $state = true;
+ if ($this->getMode() === static::MODE_OR) {
+ break;
+ }
+ } elseif ($force || !$requirement->isOptional()) {
+ $state = false;
+ if ($this->getMode() === static::MODE_AND) {
+ break;
+ }
+ }
+ }
+
+ if ($force) {
+ return $this->forcedState = $state;
+ }
+
+ return $this->state = $state;
+ }
+
+ /**
+ * Return whether the current element represents a nested set of requirements
+ *
+ * @return bool
+ */
+ public function hasChildren(): bool
+ {
+ $current = $this->current();
+ return $current instanceof static;
+ }
+
+ /**
+ * Return a iterator for the current nested set of requirements
+ *
+ * @return ?RecursiveIterator
+ */
+ public function getChildren(): ?RecursiveIterator
+ {
+ return $this->current();
+ }
+
+ /**
+ * Rewind the iterator to its first element
+ */
+ public function rewind(): void
+ {
+ reset($this->requirements);
+ }
+
+ /**
+ * Return whether the current iterator position is valid
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ return key($this->requirements) !== null;
+ }
+
+ /**
+ * Return the current element in the iteration
+ *
+ * @return Requirement|RequirementSet
+ */
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ return current($this->requirements);
+ }
+
+ /**
+ * Return the position of the current element in the iteration
+ *
+ * @return int
+ */
+ public function key(): int
+ {
+ return key($this->requirements);
+ }
+
+ /**
+ * Advance the iterator to the next element
+ */
+ public function next(): void
+ {
+ next($this->requirements);
+ }
+
+ /**
+ * Return this set of requirements rendered as HTML
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ $renderer = new RequirementsRenderer($this);
+ return (string) $renderer;
+ }
+}
diff --git a/modules/setup/library/Setup/RequirementsRenderer.php b/modules/setup/library/Setup/RequirementsRenderer.php
new file mode 100644
index 0000000..94f0f2b
--- /dev/null
+++ b/modules/setup/library/Setup/RequirementsRenderer.php
@@ -0,0 +1,67 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+use RecursiveIteratorIterator;
+
+class RequirementsRenderer extends RecursiveIteratorIterator
+{
+ protected $tags;
+
+ public function beginIteration(): void
+ {
+ $this->tags[] = '<ul class="requirements">';
+ }
+
+ public function endIteration(): void
+ {
+ $this->tags[] = '</ul>';
+ }
+
+ public function beginChildren(): void
+ {
+ $this->tags[] = '<li>';
+ /** @var RequirementSet $currentSet */
+ $currentSet = $this->getSubIterator();
+ $state = $currentSet->getState() ? 'fulfilled' : ($currentSet->isOptional() ? 'not-available' : 'missing');
+ $this->tags[] = '<ul class="set-state ' . $state . '">';
+ }
+
+ public function endChildren(): void
+ {
+ $this->tags[] = '</ul>';
+ $this->tags[] = '</li>';
+ }
+
+ public function render()
+ {
+ foreach ($this as $requirement) {
+ $this->tags[] = '<li class="clearfix">';
+ $this->tags[] = '<div class="title"><h2>' . $requirement->getTitle() . '</h2></div>';
+ $this->tags[] = '<div class="description">';
+ $descriptions = $requirement->getDescriptions();
+ if (count($descriptions) > 1) {
+ $this->tags[] = '<ul>';
+ foreach ($descriptions as $d) {
+ $this->tags[] = '<li>' . $d . '</li>';
+ }
+ $this->tags[] = '</ul>';
+ } elseif (! empty($descriptions)) {
+ $this->tags[] = $descriptions[0];
+ }
+ $this->tags[] = '</div>';
+ $this->tags[] = '<div class="state ' . ($requirement->getState() ? 'fulfilled' : (
+ $requirement->isOptional() ? 'not-available' : 'missing'
+ )) . '">' . $requirement->getStateText() . '</div>';
+ $this->tags[] = '</li>';
+ }
+
+ return implode("\n", $this->tags);
+ }
+
+ public function __toString()
+ {
+ return $this->render();
+ }
+}
diff --git a/modules/setup/library/Setup/Setup.php b/modules/setup/library/Setup/Setup.php
new file mode 100644
index 0000000..7b0baed
--- /dev/null
+++ b/modules/setup/library/Setup/Setup.php
@@ -0,0 +1,99 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+use ArrayIterator;
+use IteratorAggregate;
+use Icinga\Module\Setup\Exception\SetupException;
+use Traversable;
+
+/**
+ * Container for multiple configuration steps
+ */
+class Setup implements IteratorAggregate
+{
+ protected $steps;
+
+ protected $state;
+
+ public function __construct()
+ {
+ $this->steps = array();
+ }
+
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->getSteps());
+ }
+
+ public function addStep(Step $step)
+ {
+ $this->steps[] = $step;
+ }
+
+ public function addSteps(array $steps)
+ {
+ foreach ($steps as $step) {
+ $this->addStep($step);
+ }
+ }
+
+ public function getSteps()
+ {
+ return $this->steps;
+ }
+
+ /**
+ * Run the configuration and return whether it succeeded
+ *
+ * @return bool
+ */
+ public function run()
+ {
+ $this->state = true;
+
+ try {
+ foreach ($this->steps as $step) {
+ $this->state &= $step->apply();
+ }
+ } catch (SetupException $_) {
+ $this->state = false;
+ }
+
+ return $this->state;
+ }
+
+ /**
+ * Return a summary of all actions designated to run
+ *
+ * @return array An array of HTML strings
+ */
+ public function getSummary()
+ {
+ $summaries = array();
+ foreach ($this->steps as $step) {
+ $summaries[] = $step->getSummary();
+ }
+
+ return $summaries;
+ }
+
+ /**
+ * Return a report of all actions that were run
+ *
+ * @return array An array of arrays of strings
+ */
+ public function getReport()
+ {
+ $reports = array();
+ foreach ($this->steps as $step) {
+ $report = $step->getReport();
+ if (! empty($report)) {
+ $reports[] = $report;
+ }
+ }
+
+ return $reports;
+ }
+}
diff --git a/modules/setup/library/Setup/SetupWizard.php b/modules/setup/library/Setup/SetupWizard.php
new file mode 100644
index 0000000..c7ad0c3
--- /dev/null
+++ b/modules/setup/library/Setup/SetupWizard.php
@@ -0,0 +1,24 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+/**
+ * Interface for wizards providing a setup and requirements
+ */
+interface SetupWizard
+{
+ /**
+ * Return the setup for this wizard
+ *
+ * @return Setup
+ */
+ public function getSetup();
+
+ /**
+ * Return the requirements of this wizard
+ *
+ * @return RequirementSet
+ */
+ public function getRequirements();
+}
diff --git a/modules/setup/library/Setup/Step.php b/modules/setup/library/Setup/Step.php
new file mode 100644
index 0000000..4b9afcc
--- /dev/null
+++ b/modules/setup/library/Setup/Step.php
@@ -0,0 +1,31 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+/**
+ * Class to implement functionality for a single setup step
+ */
+abstract class Step
+{
+ /**
+ * Apply this step's configuration changes
+ *
+ * @return bool
+ */
+ abstract public function apply();
+
+ /**
+ * Return a HTML representation of this step's configuration changes supposed to be made
+ *
+ * @return string
+ */
+ abstract public function getSummary();
+
+ /**
+ * Return a textual summary of all configuration changes made
+ *
+ * @return ?array
+ */
+ abstract public function getReport();
+}
diff --git a/modules/setup/library/Setup/Steps/AuthenticationStep.php b/modules/setup/library/Setup/Steps/AuthenticationStep.php
new file mode 100644
index 0000000..3c6c64a
--- /dev/null
+++ b/modules/setup/library/Setup/Steps/AuthenticationStep.php
@@ -0,0 +1,238 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Steps;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\IcingaException;
+use Icinga\Authentication\User\DbUserBackend;
+use Icinga\Module\Setup\Step;
+
+class AuthenticationStep extends Step
+{
+ protected $data;
+
+ protected $dbError;
+
+ protected $authIniError;
+
+ protected $permIniError;
+
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ }
+
+ public function apply()
+ {
+ $success = $this->createAuthenticationIni();
+ if (isset($this->data['adminAccountData']['resourceConfig'])) {
+ $success &= $this->createAccount();
+ }
+
+ $success &= $this->createRolesIni();
+ return $success;
+ }
+
+ protected function createAuthenticationIni()
+ {
+ $config = array();
+ $backendConfig = $this->data['backendConfig'];
+ $backendName = $backendConfig['name'];
+ unset($backendConfig['name']);
+ $config[$backendName] = $backendConfig;
+ if (isset($this->data['resourceName'])) {
+ $config[$backendName]['resource'] = $this->data['resourceName'];
+ }
+
+ try {
+ Config::fromArray($config)
+ ->setConfigFile(Config::resolvePath('authentication.ini'))
+ ->saveIni();
+ } catch (Exception $e) {
+ $this->authIniError = $e;
+ return false;
+ }
+
+ $this->authIniError = false;
+ return true;
+ }
+
+ protected function createRolesIni()
+ {
+ if (isset($this->data['adminAccountData']['username'])) {
+ $config = array(
+ 'users' => $this->data['adminAccountData']['username'],
+ 'permissions' => '*'
+ );
+
+ if ($this->data['backendConfig']['backend'] === 'db') {
+ $config['groups'] = mt('setup', 'Administrators', 'setup.role.name');
+ }
+ } else { // isset($this->data['adminAccountData']['groupname'])
+ $config = array(
+ 'groups' => $this->data['adminAccountData']['groupname'],
+ 'permissions' => '*'
+ );
+ }
+
+ try {
+ Config::fromArray(array(mt('setup', 'Administrators', 'setup.role.name') => $config))
+ ->setConfigFile(Config::resolvePath('roles.ini'))
+ ->saveIni();
+ } catch (Exception $e) {
+ $this->permIniError = $e;
+ return false;
+ }
+
+ $this->permIniError = false;
+ return true;
+ }
+
+ protected function createAccount()
+ {
+ try {
+ $backend = new DbUserBackend(
+ ResourceFactory::createResource(new ConfigObject($this->data['adminAccountData']['resourceConfig']))
+ );
+
+ if ($backend->select()->where('user_name', $this->data['adminAccountData']['username'])->count() === 0) {
+ $backend->insert('user', array(
+ 'user_name' => $this->data['adminAccountData']['username'],
+ 'password' => $this->data['adminAccountData']['password'],
+ 'is_active' => true
+ ));
+ $this->dbError = false;
+ }
+ } catch (Exception $e) {
+ $this->dbError = $e;
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getSummary()
+ {
+ $pageTitle = '<h2>' . mt('setup', 'Authentication', 'setup.page.title') . '</h2>';
+ $backendTitle = '<h3>' . mt('setup', 'Authentication Backend', 'setup.page.title') . '</h3>';
+ $adminTitle = '<h3>' . mt('setup', 'Administration', 'setup.page.title') . '</h3>';
+
+ $authType = $this->data['backendConfig']['backend'];
+ $backendDesc = '<p>' . sprintf(
+ mt('setup', 'Users will authenticate using %s.', 'setup.summary.auth'),
+ $authType === 'db' ? mt('setup', 'a database', 'setup.summary.auth.type') : (
+ $authType === 'ldap' || $authType === 'msldap' ? 'LDAP' : (
+ mt('setup', 'webserver authentication', 'setup.summary.auth.type')
+ )
+ )
+ ) . '</p>';
+
+ $backendHtml = ''
+ . '<table>'
+ . '<tbody>'
+ . '<tr>'
+ . '<td><strong>' . t('Backend Name') . '</strong></td>'
+ . '<td>' . $this->data['backendConfig']['name'] . '</td>'
+ . '</tr>'
+ . ($authType === 'ldap' || $authType === 'msldap' ? (
+ '<tr>'
+ . '<td><strong>' . mt('setup', 'User Object Class') . '</strong></td>'
+ . '<td>' . ($authType === 'msldap' ? 'user' : $this->data['backendConfig']['user_class']) . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('setup', 'Custom Filter') . '</strong></td>'
+ . '<td>' . (trim($this->data['backendConfig']['filter']) ?: t('None', 'auth.ldap.filter')) . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('setup', 'User Name Attribute') . '</strong></td>'
+ . '<td>' . ($authType === 'msldap'
+ ? 'sAMAccountName'
+ : $this->data['backendConfig']['user_name_attribute']) . '</td>'
+ . '</tr>'
+ ) : ($authType === 'external' ? (
+ '<tr>'
+ . '<td><strong>' . t('Filter Pattern') . '</strong></td>'
+ . '<td>' . $this->data['backendConfig']['strip_username_regexp'] . '</td>'
+ . '</tr>'
+ ) : ''))
+ . '</tbody>'
+ . '</table>';
+
+ if (isset($this->data['adminAccountData']['username'])) {
+ $adminHtml = '<p>' . (isset($this->data['adminAccountData']['resourceConfig']) ? sprintf(
+ mt('setup', 'Administrative rights will initially be granted to a new account called "%s".'),
+ $this->data['adminAccountData']['username']
+ ) : sprintf(
+ mt('setup', 'Administrative rights will initially be granted to an existing account called "%s".'),
+ $this->data['adminAccountData']['username']
+ )) . '</p>';
+ } else { // isset($this->data['adminAccountData']['groupname'])
+ $adminHtml = '<p>' . sprintf(
+ mt('setup', 'Administrative rights will initially be granted to members of the user group "%s".'),
+ $this->data['adminAccountData']['groupname']
+ ) . '</p>';
+ }
+
+ return $pageTitle . '<div class="topic">' . $backendDesc . $backendTitle . $backendHtml . '</div>'
+ . '<div class="topic">' . $adminTitle . $adminHtml . '</div>';
+ }
+
+ public function getReport()
+ {
+ $report = array();
+
+ if ($this->authIniError === false) {
+ $report[] = sprintf(
+ mt('setup', 'Authentication configuration has been successfully written to: %s'),
+ Config::resolvePath('authentication.ini')
+ );
+ } elseif ($this->authIniError !== null) {
+ $report[] = sprintf(
+ mt('setup', 'Authentication configuration could not be written to: %s. An error occured:'),
+ Config::resolvePath('authentication.ini')
+ );
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->authIniError));
+ }
+
+ if ($this->dbError === false) {
+ $report[] = sprintf(
+ mt('setup', 'Account "%s" has been successfully created.'),
+ $this->data['adminAccountData']['username']
+ );
+ } elseif ($this->dbError !== null) {
+ $report[] = sprintf(
+ mt('setup', 'Unable to create account "%s". An error occured:'),
+ $this->data['adminAccountData']['username']
+ );
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->dbError));
+ }
+
+ if ($this->permIniError === false) {
+ $report[] = isset($this->data['adminAccountData']['username']) ? sprintf(
+ mt('setup', 'Account "%s" has been successfully defined as initial administrator.'),
+ $this->data['adminAccountData']['username']
+ ) : sprintf(
+ mt('setup', 'The members of the user group "%s" were successfully defined as initial administrators.'),
+ $this->data['adminAccountData']['groupname']
+ );
+ } elseif ($this->permIniError !== null) {
+ $report[] = isset($this->data['adminAccountData']['username']) ? sprintf(
+ mt('setup', 'Unable to define account "%s" as initial administrator. An error occured:'),
+ $this->data['adminAccountData']['username']
+ ) : sprintf(
+ mt(
+ 'setup',
+ 'Unable to define the members of the user group "%s" as initial administrators. An error occured:'
+ ),
+ $this->data['adminAccountData']['groupname']
+ );
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->permIniError));
+ }
+
+ return $report;
+ }
+}
diff --git a/modules/setup/library/Setup/Steps/DatabaseStep.php b/modules/setup/library/Setup/Steps/DatabaseStep.php
new file mode 100644
index 0000000..32b2d15
--- /dev/null
+++ b/modules/setup/library/Setup/Steps/DatabaseStep.php
@@ -0,0 +1,266 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Steps;
+
+use Exception;
+use PDOException;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Setup\Step;
+use Icinga\Module\Setup\Utils\DbTool;
+use Icinga\Module\Setup\Exception\SetupException;
+
+class DatabaseStep extends Step
+{
+ protected $data;
+
+ protected $error;
+
+ protected $messages;
+
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ $this->messages = array();
+ }
+
+ public function apply()
+ {
+ $resourceConfig = $this->data['resourceConfig'];
+ if (isset($this->data['adminName'])) {
+ $resourceConfig['username'] = $this->data['adminName'];
+ if (isset($this->data['adminPassword'])) {
+ $resourceConfig['password'] = $this->data['adminPassword'];
+ }
+ }
+
+ $db = new DbTool($resourceConfig);
+
+ try {
+ if ($resourceConfig['db'] === 'mysql') {
+ $this->setupMysqlDatabase($db);
+ } elseif ($resourceConfig['db'] === 'pgsql') {
+ $this->setupPgsqlDatabase($db);
+ }
+ } catch (Exception $e) {
+ $this->error = $e;
+ throw new SetupException();
+ }
+
+ $this->error = false;
+ return true;
+ }
+
+ protected function setupMysqlDatabase(DbTool $db)
+ {
+ try {
+ $db->connectToDb();
+ $this->log(
+ mt('setup', 'Successfully connected to existing database "%s"...'),
+ $this->data['resourceConfig']['dbname']
+ );
+ } catch (PDOException $_) {
+ $db->connectToHost();
+ $this->log(mt('setup', 'Creating new database "%s"...'), $this->data['resourceConfig']['dbname']);
+ $db->exec('CREATE DATABASE ' . $db->quoteIdentifier($this->data['resourceConfig']['dbname']));
+ $db->reconnect($this->data['resourceConfig']['dbname']);
+ }
+
+ if (array_search(reset($this->data['tables']), $db->listTables(), true) !== false) {
+ $this->log(mt('setup', 'Database schema already exists...'));
+ } else {
+ $this->log(mt('setup', 'Creating database schema...'));
+ $db->import($this->data['schemaPath'] . '/mysql.schema.sql');
+ }
+
+ if ($db->hasLogin($this->data['resourceConfig']['username'])) {
+ $this->log(mt('setup', 'Login "%s" already exists...'), $this->data['resourceConfig']['username']);
+ } else {
+ $this->log(mt('setup', 'Creating login "%s"...'), $this->data['resourceConfig']['username']);
+ $db->addLogin($this->data['resourceConfig']['username'], $this->data['resourceConfig']['password']);
+ }
+
+ $username = $this->data['resourceConfig']['username'];
+ if ($db->checkPrivileges($this->data['privileges'], $this->data['tables'], $username)) {
+ $this->log(
+ mt('setup', 'Required privileges were already granted to login "%s".'),
+ $this->data['resourceConfig']['username']
+ );
+ } else {
+ $this->log(
+ mt('setup', 'Granting required privileges to login "%s"...'),
+ $this->data['resourceConfig']['username']
+ );
+ $db->grantPrivileges(
+ $this->data['privileges'],
+ $this->data['tables'],
+ $this->data['resourceConfig']['username']
+ );
+ }
+ }
+
+ protected function setupPgsqlDatabase(DbTool $db)
+ {
+ try {
+ $db->connectToDb();
+ $this->log(
+ mt('setup', 'Successfully connected to existing database "%s"...'),
+ $this->data['resourceConfig']['dbname']
+ );
+ } catch (PDOException $_) {
+ $db->connectToHost();
+ $this->log(mt('setup', 'Creating new database "%s"...'), $this->data['resourceConfig']['dbname']);
+ $db->exec(sprintf(
+ "CREATE DATABASE %s WITH ENCODING 'UTF-8'",
+ $db->quoteIdentifier($this->data['resourceConfig']['dbname'])
+ ));
+ $db->reconnect($this->data['resourceConfig']['dbname']);
+ }
+
+ if (array_search(reset($this->data['tables']), $db->listTables(), true) !== false) {
+ $this->log(mt('setup', 'Database schema already exists...'));
+ } else {
+ $this->log(mt('setup', 'Creating database schema...'));
+ $db->import($this->data['schemaPath'] . '/pgsql.schema.sql');
+ }
+
+ if ($db->hasLogin($this->data['resourceConfig']['username'])) {
+ $this->log(mt('setup', 'Login "%s" already exists...'), $this->data['resourceConfig']['username']);
+ } else {
+ $this->log(mt('setup', 'Creating login "%s"...'), $this->data['resourceConfig']['username']);
+ $db->addLogin($this->data['resourceConfig']['username'], $this->data['resourceConfig']['password']);
+ }
+
+ $username = $this->data['resourceConfig']['username'];
+ if ($db->checkPrivileges($this->data['privileges'], $this->data['tables'], $username)) {
+ $this->log(
+ mt('setup', 'Required privileges were already granted to login "%s".'),
+ $this->data['resourceConfig']['username']
+ );
+ } else {
+ $this->log(
+ mt('setup', 'Granting required privileges to login "%s"...'),
+ $this->data['resourceConfig']['username']
+ );
+ $db->grantPrivileges(
+ $this->data['privileges'],
+ $this->data['tables'],
+ $this->data['resourceConfig']['username']
+ );
+ }
+ }
+
+ public function getSummary()
+ {
+ $resourceConfig = $this->data['resourceConfig'];
+ if (isset($this->data['adminName'])) {
+ $resourceConfig['username'] = $this->data['adminName'];
+ if (isset($this->data['adminPassword'])) {
+ $resourceConfig['password'] = $this->data['adminPassword'];
+ }
+ }
+
+ $db = new DbTool($resourceConfig);
+
+ try {
+ $db->connectToDb();
+ if (array_search(reset($this->data['tables']), $db->listTables(), true) === false) {
+ if ($resourceConfig['username'] !== $this->data['resourceConfig']['username']) {
+ $message = sprintf(
+ mt(
+ 'setup',
+ 'The database user "%s" will be used to setup the missing schema required by Icinga'
+ . ' Web 2 in database "%s" and to grant access to it to a new login called "%s".'
+ ),
+ $resourceConfig['username'],
+ $resourceConfig['dbname'],
+ $this->data['resourceConfig']['username']
+ );
+ } else {
+ $message = sprintf(
+ mt(
+ 'setup',
+ 'The database user "%s" will be used to setup the missing'
+ . ' schema required by Icinga Web 2 in database "%s".'
+ ),
+ $resourceConfig['username'],
+ $resourceConfig['dbname']
+ );
+ }
+ } else {
+ $message = sprintf(
+ mt('setup', 'The database "%s" already seems to be fully set up. No action required.'),
+ $resourceConfig['dbname']
+ );
+ }
+ } catch (PDOException $_) {
+ try {
+ $db->connectToHost();
+ if ($resourceConfig['username'] !== $this->data['resourceConfig']['username']) {
+ if ($db->hasLogin($this->data['resourceConfig']['username'])) {
+ $message = sprintf(
+ mt(
+ 'setup',
+ 'The database user "%s" will be used to create the missing database'
+ . ' "%s" with the schema required by Icinga Web 2 and to grant'
+ . ' access to it to an existing login called "%s".'
+ ),
+ $resourceConfig['username'],
+ $resourceConfig['dbname'],
+ $this->data['resourceConfig']['username']
+ );
+ } else {
+ $message = sprintf(
+ mt(
+ 'setup',
+ 'The database user "%s" will be used to create the missing database'
+ . ' "%s" with the schema required by Icinga Web 2 and to grant'
+ . ' access to it to a new login called "%s".'
+ ),
+ $resourceConfig['username'],
+ $resourceConfig['dbname'],
+ $this->data['resourceConfig']['username']
+ );
+ }
+ } else {
+ $message = sprintf(
+ mt(
+ 'setup',
+ 'The database user "%s" will be used to create the missing'
+ . ' database "%s" with the schema required by Icinga Web 2.'
+ ),
+ $resourceConfig['username'],
+ $resourceConfig['dbname']
+ );
+ }
+ } catch (Exception $_) {
+ $message = mt(
+ 'setup',
+ 'No connection to database host possible. You\'ll need to setup the'
+ . ' database with the schema required by Icinga Web 2 manually.'
+ );
+ }
+ }
+
+ return '<h2>' . mt('setup', 'Database Setup', 'setup.page.title') . '</h2><p>' . $message . '</p>';
+ }
+
+ public function getReport()
+ {
+ if ($this->error === false) {
+ $report = $this->messages;
+ $report[] = mt('setup', 'The database has been fully set up!');
+ return $report;
+ } elseif ($this->error !== null) {
+ $report = $this->messages;
+ $report[] = mt('setup', 'Failed to fully setup the database. An error occured:');
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error));
+ return $report;
+ }
+ }
+
+ protected function log()
+ {
+ $this->messages[] = call_user_func_array('sprintf', func_get_args());
+ }
+}
diff --git a/modules/setup/library/Setup/Steps/GeneralConfigStep.php b/modules/setup/library/Setup/Steps/GeneralConfigStep.php
new file mode 100644
index 0000000..5deb18d
--- /dev/null
+++ b/modules/setup/library/Setup/Steps/GeneralConfigStep.php
@@ -0,0 +1,133 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Steps;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Application\Config;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Setup\Step;
+
+class GeneralConfigStep extends Step
+{
+ protected $data;
+
+ protected $error;
+
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ }
+
+ public function apply()
+ {
+ $config = array();
+ foreach ($this->data['generalConfig'] as $sectionAndPropertyName => $value) {
+ list($section, $property) = explode('_', $sectionAndPropertyName, 2);
+ $config[$section][$property] = $value;
+ }
+
+ $config['global']['config_resource'] = $this->data['resourceName'];
+
+ try {
+ Config::fromArray($config)
+ ->setConfigFile(Config::resolvePath('config.ini'))
+ ->saveIni();
+ } catch (Exception $e) {
+ $this->error = $e;
+ return false;
+ }
+
+ $this->error = false;
+ return true;
+ }
+
+ public function getSummary()
+ {
+ $pageTitle = '<h2>' . mt('setup', 'Application Configuration', 'setup.page.title') . '</h2>';
+ $generalTitle = '<h3>' . t('General', 'app.config') . '</h3>';
+ $loggingTitle = '<h3>' . t('Logging', 'app.config') . '</h3>';
+
+ $generalHtml = ''
+ . '<ul>'
+ . '<li>' . ($this->data['generalConfig']['global_show_stacktraces']
+ ? t('An exception\'s stacktrace is shown to every user by default.')
+ : t('An exception\'s stacktrace is hidden from every user by default.')
+ ) . '</li>'
+ . '<li>' . t('Preferences will be stored using a database.') . '</li>'
+ . '</ul>';
+
+ $type = $this->data['generalConfig']['logging_log'];
+ if ($type === 'none') {
+ $loggingHtml = '<p>' . mt('setup', 'Logging will be disabled.') . '</p>';
+ } else {
+ $level = $this->data['generalConfig']['logging_level'];
+
+ $typeDescription = null;
+ $typeSpecificHtml = null;
+ switch ($type) {
+ case 'php':
+ $typeDescription = t('Webserver Log', 'app.config.logging.type');
+ $typeSpecificHtml = '';
+ break;
+
+ case 'syslog':
+ $typeDescription = 'Syslog';
+ $typeSpecificHtml = '<td><strong>' . t('Application Prefix') . '</strong></td>'
+ . '<td>' . $this->data['generalConfig']['logging_application'] . '</td>';
+ break;
+
+ case 'file':
+ $typeDescription = t('File', 'app.config.logging.type');
+ $typeSpecificHtml = '<td><strong>' . t('Filepath') . '</strong></td>'
+ . '<td>' . $this->data['generalConfig']['logging_file'] . '</td>';
+ break;
+ }
+
+ $loggingHtml = ''
+ . '<table>'
+ . '<tbody>'
+ . '<tr>'
+ . '<td><strong>' . t('Type', 'app.config.logging') . '</strong></td>'
+ . '<td>' . $typeDescription . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Level', 'app.config.logging') . '</strong></td>'
+ . '<td>' . ($level === Logger::$levels[Logger::ERROR] ? t('Error', 'app.config.logging.level') : (
+ $level === Logger::$levels[Logger::WARNING] ? t('Warning', 'app.config.logging.level') : (
+ $level === Logger::$levels[Logger::INFO] ? t('Information', 'app.config.logging.level') : (
+ t('Debug', 'app.config.logging.level')
+ )
+ )
+ )) . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . $typeSpecificHtml
+ . '</tr>'
+ . '</tbody>'
+ . '</table>';
+ }
+
+ return $pageTitle . '<div class="topic">' . $generalTitle . $generalHtml . '</div>'
+ . '<div class="topic">' . $loggingTitle . $loggingHtml . '</div>';
+ }
+
+ public function getReport()
+ {
+ if ($this->error === false) {
+ return array(sprintf(
+ mt('setup', 'General configuration has been successfully written to: %s'),
+ Config::resolvePath('config.ini')
+ ));
+ } elseif ($this->error !== null) {
+ return array(
+ sprintf(
+ mt('setup', 'General configuration could not be written to: %s. An error occured:'),
+ Config::resolvePath('config.ini')
+ ),
+ sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error))
+ );
+ }
+ }
+}
diff --git a/modules/setup/library/Setup/Steps/ResourceStep.php b/modules/setup/library/Setup/Steps/ResourceStep.php
new file mode 100644
index 0000000..d69d325
--- /dev/null
+++ b/modules/setup/library/Setup/Steps/ResourceStep.php
@@ -0,0 +1,201 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Steps;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Setup\Step;
+
+class ResourceStep extends Step
+{
+ protected $data;
+
+ protected $error;
+
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ }
+
+ public function apply()
+ {
+ $resourceConfig = array();
+ if (isset($this->data['dbResourceConfig'])) {
+ $dbConfig = $this->data['dbResourceConfig'];
+ $resourceName = $dbConfig['name'];
+ unset($dbConfig['name']);
+ $resourceConfig[$resourceName] = $dbConfig;
+ }
+
+ if (isset($this->data['ldapResourceConfig'])) {
+ $ldapConfig = $this->data['ldapResourceConfig'];
+ $resourceName = $ldapConfig['name'];
+ unset($ldapConfig['name']);
+ $resourceConfig[$resourceName] = $ldapConfig;
+ }
+
+ try {
+ Config::fromArray($resourceConfig)
+ ->setConfigFile(Config::resolvePath('resources.ini'))
+ ->saveIni();
+ } catch (Exception $e) {
+ $this->error = $e;
+ return false;
+ }
+
+ $this->error = false;
+ return true;
+ }
+
+ public function getSummary()
+ {
+ if (isset($this->data['dbResourceConfig']) && isset($this->data['ldapResourceConfig'])) {
+ $pageTitle = '<h2>' . mt('setup', 'Resources', 'setup.page.title') . '</h2>';
+ } else {
+ $pageTitle = '<h2>' . mt('setup', 'Resource', 'setup.page.title') . '</h2>';
+ }
+
+ $dbHtml = null;
+ if (isset($this->data['dbResourceConfig'])) {
+ $dbTitle = '<h3>' . mt('setup', 'Database', 'setup.page.title') . '</h3>';
+ $dbHtml = ''
+ . '<table>'
+ . '<tbody>'
+ . '<tr>'
+ . '<td><strong>' . t('Resource Name') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['name'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Database Type') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['db'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Host') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['host'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Port') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['port'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Database Name') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['dbname'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Username') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['username'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Password') . '</strong></td>'
+ . '<td>' . str_repeat('*', strlen($this->data['dbResourceConfig']['password'])) . '</td>'
+ . '</tr>';
+
+ if (defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')
+ && isset($this->data['resourceConfig']['ssl_do_not_verify_server_cert'])
+ && $this->data['resourceConfig']['ssl_do_not_verify_server_cert']
+ ) {
+ $dbHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('SSL Do Not Verify Server Certificate') . '</strong></td>'
+ . '<td>' . $this->data['resourceConfig']['ssl_do_not_verify_server_cert'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['dbResourceConfig']['ssl_key']) && $this->data['dbResourceConfig']['ssl_key']) {
+ $dbHtml .= ''
+ .'<tr>'
+ . '<td><strong>' . t('SSL Key') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['ssl_key'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['dbResourceConfig']['ssl_cert']) && $this->data['dbResourceConfig']['ssl_cert']) {
+ $dbHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('SSL Cert') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['ssl_cert'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['dbResourceConfig']['ssl_ca']) && $this->data['dbResourceConfig']['ssl_ca']) {
+ $dbHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('CA') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['ssl_ca'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['dbResourceConfig']['ssl_capath']) && $this->data['dbResourceConfig']['ssl_capath']) {
+ $dbHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('CA Path') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['ssl_capath'] . '</td>'
+ . '</tr>';
+ }
+ if (isset($this->data['dbResourceConfig']['ssl_cipher']) && $this->data['dbResourceConfig']['ssl_cipher']) {
+ $dbHtml .= ''
+ . '<tr>'
+ . '<td><strong>' . t('Cipher') . '</strong></td>'
+ . '<td>' . $this->data['dbResourceConfig']['ssl_cipher'] . '</td>'
+ . '</tr>';
+ }
+
+ $dbHtml .= ''
+ . '</tbody>'
+ . '</table>';
+ }
+
+ $ldapHtml = null;
+ if (isset($this->data['ldapResourceConfig'])) {
+ $ldapTitle = '<h3>LDAP</h3>';
+ $ldapHtml = ''
+ . '<table>'
+ . '<tbody>'
+ . '<tr>'
+ . '<td><strong>' . t('Resource Name') . '</strong></td>'
+ . '<td>' . $this->data['ldapResourceConfig']['name'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Host') . '</strong></td>'
+ . '<td>' . $this->data['ldapResourceConfig']['hostname'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Port') . '</strong></td>'
+ . '<td>' . $this->data['ldapResourceConfig']['port'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Root DN') . '</strong></td>'
+ . '<td>' . $this->data['ldapResourceConfig']['root_dn'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Bind DN') . '</strong></td>'
+ . '<td>' . $this->data['ldapResourceConfig']['bind_dn'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . t('Bind Password') . '</strong></td>'
+ . '<td>' . str_repeat('*', strlen($this->data['ldapResourceConfig']['bind_pw'])) . '</td>'
+ . '</tr>'
+ . '</tbody>'
+ . '</table>';
+ }
+
+ return $pageTitle . (isset($dbTitle) ? '<div class="topic">' . $dbTitle . $dbHtml . '</div>' : '')
+ . (isset($ldapTitle) ? '<div class="topic">' . $ldapTitle . $ldapHtml . '</div>' : '');
+ }
+
+ public function getReport()
+ {
+ if ($this->error === false) {
+ return array(sprintf(
+ mt('setup', 'Resource configuration has been successfully written to: %s'),
+ Config::resolvePath('resources.ini')
+ ));
+ } elseif ($this->error !== null) {
+ return array(
+ sprintf(
+ mt('setup', 'Resource configuration could not be written to: %s. An error occured:'),
+ Config::resolvePath('resources.ini')
+ ),
+ sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error))
+ );
+ }
+ }
+}
diff --git a/modules/setup/library/Setup/Steps/UserGroupStep.php b/modules/setup/library/Setup/Steps/UserGroupStep.php
new file mode 100644
index 0000000..4aab676
--- /dev/null
+++ b/modules/setup/library/Setup/Steps/UserGroupStep.php
@@ -0,0 +1,213 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Steps;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Authentication\UserGroup\DbUserGroupBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Setup\Step;
+
+class UserGroupStep extends Step
+{
+ protected $data;
+
+ protected $groupError;
+
+ protected $memberError;
+
+ protected $groupIniError;
+
+ public function __construct(array $data)
+ {
+ $this->data = $data;
+ }
+
+ public function apply()
+ {
+ $success = $this->createGroupsIni();
+ if (isset($this->data['resourceConfig'])) {
+ $success &= $this->createUserGroup();
+ if ($success) {
+ $success &= $this->createMembership();
+ }
+ }
+
+ return $success;
+ }
+
+ protected function createGroupsIni()
+ {
+ $config = array();
+ if (isset($this->data['groupConfig'])) {
+ $backendConfig = $this->data['groupConfig'];
+ $backendName = $backendConfig['name'];
+ unset($backendConfig['name']);
+ $config[$backendName] = $backendConfig;
+ } else {
+ $backendConfig = array(
+ 'backend' => $this->data['backendConfig']['backend'], // "db" or "msldap"
+ 'resource' => $this->data['resourceName']
+ );
+
+ if ($backendConfig['backend'] === 'msldap') {
+ $backendConfig['user_backend'] = $this->data['backendConfig']['name'];
+ }
+
+ $config[$this->data['backendConfig']['name']] = $backendConfig;
+ }
+
+ try {
+ Config::fromArray($config)
+ ->setConfigFile(Config::resolvePath('groups.ini'))
+ ->saveIni();
+ } catch (Exception $e) {
+ $this->groupIniError = $e;
+ return false;
+ }
+
+ $this->groupIniError = false;
+ return true;
+ }
+
+ protected function createUserGroup()
+ {
+ try {
+ $backend = new DbUserGroupBackend(
+ ResourceFactory::createResource(new ConfigObject($this->data['resourceConfig']))
+ );
+
+ $groupName = mt('setup', 'Administrators', 'setup.role.name');
+ if ($backend->select()->where('group_name', $groupName)->count() === 0) {
+ $backend->insert('group', array(
+ 'group_name' => $groupName
+ ));
+ $this->groupError = false;
+ }
+ } catch (Exception $e) {
+ $this->groupError = $e;
+ return false;
+ }
+
+ return true;
+ }
+
+ protected function createMembership()
+ {
+ try {
+ $backend = new DbUserGroupBackend(
+ ResourceFactory::createResource(new ConfigObject($this->data['resourceConfig']))
+ );
+
+ $groupName = mt('setup', 'Administrators', 'setup.role.name');
+ $userName = $this->data['username'];
+ if ($backend
+ ->select()
+ ->from('group_membership')
+ ->where('group_name', $groupName)
+ ->where('user_name', $userName)
+ ->count() === 0
+ ) {
+ $backend->insert('group_membership', array(
+ 'group_name' => $groupName,
+ 'user_name' => $userName
+ ));
+ $this->memberError = false;
+ }
+ } catch (Exception $e) {
+ $this->memberError = $e;
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getSummary()
+ {
+ if (! isset($this->data['groupConfig'])) {
+ return; // It's not necessary to show the user something he didn't configure..
+ }
+
+ $pageTitle = '<h2>' . mt('setup', 'User Groups', 'setup.page.title') . '</h2>';
+ $backendTitle = '<h3>' . mt('setup', 'User Group Backend', 'setup.page.title') . '</h3>';
+
+ $backendHtml = ''
+ . '<table>'
+ . '<tbody>'
+ . '<tr>'
+ . '<td><strong>' . t('Backend Name') . '</strong></td>'
+ . '<td>' . $this->data['groupConfig']['name'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('setup', 'Group Object Class') . '</strong></td>'
+ . '<td>' . $this->data['groupConfig']['group_class'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('setup', 'Custom Filter') . '</strong></td>'
+ . '<td>' . (trim($this->data['groupConfig']['group_filter']) ?: t('None', 'auth.ldap.filter')) . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('setup', 'Group Name Attribute') . '</strong></td>'
+ . '<td>' . $this->data['groupConfig']['group_name_attribute'] . '</td>'
+ . '</tr>'
+ . '<tr>'
+ . '<td><strong>' . mt('setup', 'Group Member Attribute') . '</strong></td>'
+ . '<td>' . $this->data['groupConfig']['group_member_attribute'] . '</td>'
+ . '</tr>'
+ . '</tbody>'
+ . '</table>';
+
+ return $pageTitle . '<div class="topic">' . $backendTitle . $backendHtml . '</div>';
+ }
+
+ public function getReport()
+ {
+ $report = array();
+
+ if ($this->groupIniError === false) {
+ $report[] = sprintf(
+ mt('setup', 'User Group Backend configuration has been successfully written to: %s'),
+ Config::resolvePath('groups.ini')
+ );
+ } elseif ($this->groupIniError !== null) {
+ $report[] = sprintf(
+ mt('setup', 'User Group Backend configuration could not be written to: %s. An error occured:'),
+ Config::resolvePath('groups.ini')
+ );
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->groupIniError));
+ }
+
+ if ($this->groupError === false) {
+ $report[] = sprintf(
+ mt('setup', 'User Group "%s" has been successfully created.'),
+ mt('setup', 'Administrators', 'setup.role.name')
+ );
+ } elseif ($this->groupError !== null) {
+ $report[] = sprintf(
+ mt('setup', 'Unable to create user group "%s". An error occured:'),
+ mt('setup', 'Administrators', 'setup.role.name')
+ );
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->groupError));
+ }
+
+ if ($this->memberError === false) {
+ $report[] = sprintf(
+ mt('setup', 'Account "%s" has been successfully added as member to user group "%s".'),
+ $this->data['username'],
+ mt('setup', 'Administrators', 'setup.role.name')
+ );
+ } elseif ($this->memberError !== null) {
+ $report[] = sprintf(
+ mt('setup', 'Unable to add account "%s" as member to user group "%s". An error occured:'),
+ $this->data['username'],
+ mt('setup', 'Administrators', 'setup.role.name')
+ );
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->memberError));
+ }
+
+ return $report;
+ }
+}
diff --git a/modules/setup/library/Setup/Utils/DbTool.php b/modules/setup/library/Setup/Utils/DbTool.php
new file mode 100644
index 0000000..7578462
--- /dev/null
+++ b/modules/setup/library/Setup/Utils/DbTool.php
@@ -0,0 +1,950 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Utils;
+
+use PDO;
+use PDOException;
+use LogicException;
+use Zend_Db_Adapter_Pdo_Abstract;
+use Zend_Db_Adapter_Pdo_Mysql;
+use Zend_Db_Adapter_Pdo_Pgsql;
+use Icinga\Util\File;
+use Icinga\Exception\ConfigurationError;
+
+/**
+ * Utility class to ease working with databases when setting up Icinga Web 2 or one of its modules
+ */
+class DbTool
+{
+ /**
+ * The PDO database connection
+ *
+ * @var PDO
+ */
+ protected $pdoConn;
+
+ /**
+ * The Zend database adapter
+ *
+ * @var Zend_Db_Adapter_Pdo_Abstract
+ */
+ protected $zendConn;
+
+ /**
+ * The resource configuration
+ *
+ * @var array
+ */
+ protected $config;
+
+ /**
+ * Whether we are connected to the database from the resource configuration
+ *
+ * @var bool
+ */
+ protected $dbFromConfig = false;
+
+ /**
+ * GRANT privilege level identifiers
+ */
+ const GLOBAL_LEVEL = 1;
+ const PROCEDURE_LEVEL = 2;
+ const DATABASE_LEVEL = 4;
+ const TABLE_LEVEL = 8;
+ const COLUMN_LEVEL = 16;
+ const FUNCTION_LEVEL = 32;
+
+ /**
+ * All MySQL GRANT privileges with their respective level identifiers
+ *
+ * @var array
+ */
+ protected $mysqlGrantContexts = array(
+ 'ALL' => 31,
+ 'ALL PRIVILEGES' => 31,
+ 'ALTER' => 13,
+ 'ALTER ROUTINE' => 7,
+ 'CREATE' => 13,
+ 'CREATE ROUTINE' => 5,
+ 'CREATE TEMPORARY TABLES' => 5,
+ 'CREATE USER' => 1,
+ 'CREATE VIEW' => 13,
+ 'DELETE' => 13,
+ 'DROP' => 13,
+ 'EXECUTE' => 5, // MySQL reference states this also supports database level, 5.1.73 not though
+ 'FILE' => 1,
+ 'GRANT OPTION' => 15,
+ 'INDEX' => 13,
+ 'INSERT' => 29,
+ 'LOCK TABLES' => 5,
+ 'PROCESS' => 1,
+ 'REFERENCES' => 12,
+ 'RELOAD' => 1,
+ 'REPLICATION CLIENT' => 1,
+ 'REPLICATION SLAVE' => 1,
+ 'SELECT' => 29,
+ 'SHOW DATABASES' => 1,
+ 'SHOW VIEW' => 13,
+ 'SHUTDOWN' => 1,
+ 'SUPER' => 1,
+ 'UPDATE' => 29
+ );
+
+ /**
+ * All PostgreSQL GRANT privileges with their respective level identifiers
+ *
+ * @var array
+ */
+ protected $pgsqlGrantContexts = array(
+ 'ALL' => 63,
+ 'ALL PRIVILEGES' => 63,
+ 'CREATE' => 13,
+ 'CONNECT' => 4,
+ 'TEMPORARY' => 4,
+ 'TEMP' => 4,
+ 'EXECUTE' => 32,
+ 'USAGE' => 33,
+ 'CREATEROLE' => 1
+ );
+
+ /**
+ * Create a new DbTool
+ *
+ * @param array $config The resource configuration to use
+ */
+ public function __construct(array $config)
+ {
+ if (! isset($config['port'])) {
+ // TODO: This is not quite correct, but works as it previously did. Previously empty values were not
+ // transformed no NULL (now they are) so if the port is now null, it's been the empty string.
+ $config['port'] = '';
+ }
+
+ $this->config = $config;
+ }
+
+ /**
+ * Connect to the server
+ *
+ * @return $this
+ */
+ public function connectToHost()
+ {
+ $this->assertHostAccess();
+
+ if ($this->config['db'] == 'pgsql') {
+ // PostgreSQL requires us to specify a database on each connection and will use
+ // the current user name as default database in cases none is provided. If
+ // that database doesn't exist (which might be the case here) it will error.
+ // Therefore, we specify the maintenance database 'postgres' as database, which
+ // is most probably present and public. (http://stackoverflow.com/q/4483139)
+ $this->connect('postgres');
+ } else {
+ $this->connect();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Connect to the database
+ *
+ * @return $this
+ */
+ public function connectToDb()
+ {
+ $this->assertHostAccess();
+ $this->assertDatabaseAccess();
+ $this->connect($this->config['dbname']);
+ return $this;
+ }
+
+ /**
+ * Assert that all configuration values exist that are required to connect to a server
+ *
+ * @throws ConfigurationError
+ */
+ protected function assertHostAccess()
+ {
+ if (! isset($this->config['db'])) {
+ throw new ConfigurationError('Can\'t connect to database server of unknown type');
+ } elseif (! isset($this->config['host'])) {
+ throw new ConfigurationError('Can\'t connect to database server without a hostname or address');
+ } elseif (! isset($this->config['port'])) {
+ throw new ConfigurationError('Can\'t connect to database server without a port');
+ } elseif (! isset($this->config['username'])) {
+ throw new ConfigurationError('Can\'t connect to database server without a username');
+ } elseif (! isset($this->config['password'])) {
+ throw new ConfigurationError('Can\'t connect to database server without a password');
+ }
+ }
+
+ /**
+ * Assert that all configuration values exist that are required to connect to a database
+ *
+ * @throws ConfigurationError
+ */
+ protected function assertDatabaseAccess()
+ {
+ if (! isset($this->config['dbname'])) {
+ throw new ConfigurationError('Can\'t connect to database without a valid database name');
+ }
+ }
+
+ /**
+ * Assert that a connection with a database has been established
+ *
+ * @throws LogicException
+ */
+ protected function assertConnectedToDb()
+ {
+ if ($this->zendConn === null) {
+ throw new LogicException('Not connected to database');
+ }
+ }
+
+ /**
+ * Return whether a connection with the server has been established
+ *
+ * @return bool
+ */
+ public function isConnected()
+ {
+ return $this->pdoConn !== null;
+ }
+
+ /**
+ * Establish a connection with the database or just the server by omitting the database name
+ *
+ * @param string $dbname The name of the database to connect to
+ */
+ public function connect($dbname = null)
+ {
+ $this->pdoConnect($dbname);
+ if ($dbname !== null) {
+ $this->zendConnect($dbname);
+ $this->dbFromConfig = $dbname === $this->config['dbname'];
+ }
+ }
+
+ /**
+ * Reestablish a connection with the database or just the server by omitting the database name
+ *
+ * @param string $dbname The name of the database to connect to
+ */
+ public function reconnect($dbname = null)
+ {
+ $this->pdoConn = null;
+ $this->zendConn = null;
+ $this->connect($dbname);
+ }
+
+ /**
+ * Initialize Zend database adapter
+ *
+ * @param string $dbname The name of the database to connect with
+ *
+ * @throws ConfigurationError In case the resource type is not a supported PDO driver name
+ */
+ private function zendConnect($dbname)
+ {
+ if ($this->zendConn !== null) {
+ return;
+ }
+
+ $config = array(
+ 'dbname' => $dbname,
+ 'host' => $this->config['host'],
+ 'port' => $this->config['port'],
+ 'username' => $this->config['username'],
+ 'password' => $this->config['password']
+ );
+
+ if ($this->config['db'] === 'mysql') {
+ if (isset($this->config['use_ssl']) && $this->config['use_ssl']) {
+ $this->config['driver_options'] = array();
+ # The presence of these keys as empty strings or null cause non-ssl connections to fail
+ if ($this->config['ssl_key']) {
+ $config['driver_options'][PDO::MYSQL_ATTR_SSL_KEY] = $this->config['ssl_key'];
+ }
+ if ($this->config['ssl_cert']) {
+ $config['driver_options'][PDO::MYSQL_ATTR_SSL_CERT] = $this->config['ssl_cert'];
+ }
+ if ($this->config['ssl_ca']) {
+ $config['driver_options'][PDO::MYSQL_ATTR_SSL_CA] = $this->config['ssl_ca'];
+ }
+ if ($this->config['ssl_capath']) {
+ $config['driver_options'][PDO::MYSQL_ATTR_SSL_CAPATH] = $this->config['ssl_capath'];
+ }
+ if ($this->config['ssl_cipher']) {
+ $config['driver_options'][PDO::MYSQL_ATTR_SSL_CIPHER] = $this->config['ssl_cipher'];
+ }
+ if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')
+ && $this->config['ssl_do_not_verify_server_cert']
+ ) {
+ $config['driver_options'][PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false;
+ }
+ }
+ $this->zendConn = new Zend_Db_Adapter_Pdo_Mysql($config);
+ } elseif ($this->config['db'] === 'pgsql') {
+ $this->zendConn = new Zend_Db_Adapter_Pdo_Pgsql($config);
+ } else {
+ throw new ConfigurationError(
+ 'Failed to connect to database. Unsupported PDO driver "%s"',
+ $this->config['db']
+ );
+ }
+
+ $this->zendConn->getConnection(); // Force connection attempt
+ }
+
+ /**
+ * Initialize PDO connection
+ *
+ * @param string $dbname The name of the database to connect with
+ */
+ private function pdoConnect($dbname)
+ {
+ if ($this->pdoConn !== null) {
+ return;
+ }
+
+ $driverOptions = array(
+ PDO::ATTR_TIMEOUT => 1,
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
+ );
+
+ if ($this->config['db'] === 'mysql'
+ && isset($this->config['use_ssl'])
+ && $this->config['use_ssl']
+ ) {
+ # The presence of these keys as empty strings or null cause non-ssl connections to fail
+ if ($this->config['ssl_key']) {
+ $driverOptions[PDO::MYSQL_ATTR_SSL_KEY] = $this->config['ssl_key'];
+ }
+ if ($this->config['ssl_cert']) {
+ $driverOptions[PDO::MYSQL_ATTR_SSL_CERT] = $this->config['ssl_cert'];
+ }
+ if ($this->config['ssl_ca']) {
+ $driverOptions[PDO::MYSQL_ATTR_SSL_CA] = $this->config['ssl_ca'];
+ }
+ if ($this->config['ssl_capath']) {
+ $driverOptions[PDO::MYSQL_ATTR_SSL_CAPATH] = $this->config['ssl_capath'];
+ }
+ if ($this->config['ssl_cipher']) {
+ $driverOptions[PDO::MYSQL_ATTR_SSL_CIPHER] = $this->config['ssl_cipher'];
+ }
+ if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')
+ && $this->config['ssl_do_not_verify_server_cert']
+ ) {
+ $driverOptions[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false;
+ }
+ }
+
+ $this->pdoConn = new PDO(
+ $this->buildDsn($this->config['db'], $dbname),
+ $this->config['username'],
+ $this->config['password'],
+ $driverOptions
+ );
+ }
+
+ /**
+ * Return a datasource name for the given database type and name
+ *
+ * @param string $dbtype
+ * @param string $dbname
+ *
+ * @return string
+ *
+ * @throws ConfigurationError In case the passed database type is not supported
+ */
+ protected function buildDsn($dbtype, $dbname = null)
+ {
+ if ($dbtype === 'mysql') {
+ return 'mysql:host=' . $this->config['host'] . ';port=' . $this->config['port']
+ . ($dbname !== null ? ';dbname=' . $dbname : '');
+ } elseif ($dbtype === 'pgsql') {
+ return 'pgsql:host=' . $this->config['host'] . ';port=' . $this->config['port']
+ . ($dbname !== null ? ';dbname=' . $dbname : '');
+ } else {
+ throw new ConfigurationError(
+ 'Failed to build data source name. Unsupported PDO driver "%s"',
+ $dbtype
+ );
+ }
+ }
+
+ /**
+ * Try to connect to the server and throw an exception if this fails
+ *
+ * @throws PDOException In case an error occurs that does not indicate that authentication failed
+ */
+ public function checkConnectivity()
+ {
+ try {
+ $this->connectToHost();
+ } catch (PDOException $e) {
+ if ($this->config['db'] === 'mysql') {
+ $code = $e->getCode();
+ /*
+ * 1040 .. Too many connections
+ * 1045 .. Access denied for user '%s'@'%s' (using password: %s)
+ * 1698 .. Access denied for user '%s'@'%s'
+ */
+ if ($code !== 1040 && $code !== 1045 && $code !== 1698) {
+ throw $e;
+ }
+ } elseif ($this->config['db'] === 'pgsql') {
+ if (strpos($e->getMessage(), $this->config['username']) === false) {
+ throw $e;
+ }
+ }
+ }
+ }
+
+ /**
+ * Return the given identifier escaped with backticks
+ *
+ * @param string $identifier The identifier to escape
+ *
+ * @return string
+ *
+ * @throws LogicException In case there is no behaviour implemented for the current PDO driver
+ */
+ public function quoteIdentifier($identifier)
+ {
+ if ($this->config['db'] === 'mysql') {
+ return '`' . str_replace('`', '``', $identifier) . '`';
+ } elseif ($this->config['db'] === 'pgsql') {
+ return '"' . str_replace('"', '""', $identifier) . '"';
+ } else {
+ throw new LogicException('Unable to quote identifier.');
+ }
+ }
+
+ /**
+ * Return the given table name with all wildcards being escaped
+ *
+ * @param string $tableName
+ *
+ * @return string
+ *
+ * @throws LogicException In case there is no behaviour implemented for the current PDO driver
+ */
+ public function escapeTableWildcards($tableName)
+ {
+ if ($this->config['db'] === 'mysql') {
+ return str_replace(array('_', '%'), array('\_', '\%'), $tableName);
+ }
+
+ throw new LogicException('Unable to escape table wildcards.');
+ }
+
+ /**
+ * Return the given value escaped as string
+ *
+ * @param mixed $value The value to escape
+ *
+ * @return string
+ *
+ * @throws LogicException In case there is no behaviour implemented for the current PDO driver
+ */
+ public function quote($value)
+ {
+ $quoted = $this->pdoConn->quote($value);
+ if ($quoted === false) {
+ throw new LogicException(sprintf('Unable to quote value: %s', $value));
+ }
+
+ return $quoted;
+ }
+
+ /**
+ * Execute a SQL statement and return the affected row count
+ *
+ * Use $params to use a prepared statement.
+ *
+ * @param string $statement The statement to execute
+ * @param array $params The params to bind
+ *
+ * @return int
+ */
+ public function exec($statement, $params = array())
+ {
+ if (empty($params)) {
+ return $this->pdoConn->exec($statement);
+ }
+
+ $stmt = $this->pdoConn->prepare($statement);
+ $stmt->execute($params);
+ return $stmt->rowCount();
+ }
+
+ /**
+ * Execute a SQL statement and return the result
+ *
+ * Use $params to use a prepared statement.
+ *
+ * @param string $statement The statement to execute
+ * @param array $params The params to bind
+ *
+ * @return mixed
+ */
+ public function query($statement, $params = array())
+ {
+ if ($this->zendConn !== null) {
+ return $this->zendConn->query($statement, $params);
+ }
+
+ if (empty($params)) {
+ return $this->pdoConn->query($statement);
+ }
+
+ $stmt = $this->pdoConn->prepare($statement);
+ $stmt->execute($params);
+ return $stmt;
+ }
+
+ /**
+ * Return the version of the server currently connected to
+ *
+ * @return string|null
+ */
+ public function getServerVersion()
+ {
+ if ($this->config['db'] === 'mysql') {
+ return $this->query('show variables like "version"')->fetchColumn(1) ?: null;
+ } elseif ($this->config['db'] === 'pgsql') {
+ return $this->query('show server_version')->fetchColumn() ?: null;
+ } else {
+ throw new LogicException(
+ sprintf('Unable to fetch the server\'s version. Unsupported PDO driver "%s"', $this->config['db'])
+ );
+ }
+ }
+
+ /**
+ * Import the given SQL file
+ *
+ * @param string $filepath The file to import
+ */
+ public function import($filepath)
+ {
+ $file = new File($filepath);
+ $content = join(PHP_EOL, iterator_to_array($file)); // There is no fread() before PHP 5.5 :(
+
+ foreach (preg_split('@;(?! \\\\)@', $content) as $statement) {
+ if (($statement = trim($statement)) !== '') {
+ $this->exec($statement);
+ }
+ }
+ }
+
+ /**
+ * Return whether the given privileges were granted
+ *
+ * @param array $privileges An array of strings with the required privilege names
+ * @param array $context An array describing the context for which the given privileges need to apply.
+ * Only one or more table names are currently supported
+ * @param string $username The login name for which to check the privileges,
+ * if NULL the current login is used
+ *
+ * @return ?bool
+ */
+ public function checkPrivileges(array $privileges, array $context = null, $username = null)
+ {
+ if ($this->config['db'] === 'mysql') {
+ return $this->checkMysqlPrivileges($privileges, false, $context, $username);
+ } elseif ($this->config['db'] === 'pgsql') {
+ return $this->checkPgsqlPrivileges($privileges, false, $context, $username);
+ }
+ }
+
+ /**
+ * Return whether the given privileges are grantable to other users
+ *
+ * @param array $privileges The privileges that should be grantable
+ *
+ * @return ?bool
+ */
+ public function isGrantable($privileges)
+ {
+ if ($this->config['db'] === 'mysql') {
+ return $this->checkMysqlPrivileges($privileges, true);
+ } elseif ($this->config['db'] === 'pgsql') {
+ return $this->checkPgsqlPrivileges($privileges, true);
+ }
+ }
+
+ /**
+ * Grant all given privileges to the given user
+ *
+ * @param array $privileges The privilege names to grant
+ * @param array $context An array describing the context for which the given privileges need to apply.
+ * Only one or more table names are currently supported
+ * @param string $username The username to grant the privileges to
+ */
+ public function grantPrivileges(array $privileges, array $context, $username)
+ {
+ if ($this->config['db'] === 'mysql') {
+ list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn());
+ $quotedDbName = $this->quoteIdentifier($this->config['dbname']);
+
+ $grant = 'GRANT %s';
+ $on = ' ON %s.%s';
+ $to = sprintf(
+ ' TO %s@%s',
+ $this->quoteIdentifier($username),
+ $this->quoteIdentifier($host)
+ );
+
+ $dbPrivileges = array();
+ $tablePrivileges = array();
+ foreach (array_intersect($privileges, array_keys($this->mysqlGrantContexts)) as $privilege) {
+ if (! empty($context) && $this->mysqlGrantContexts[$privilege] & static::TABLE_LEVEL) {
+ $tablePrivileges[] = $privilege;
+ } elseif ($this->mysqlGrantContexts[$privilege] & static::DATABASE_LEVEL) {
+ $dbPrivileges[] = $privilege;
+ }
+ }
+
+ if (! empty($tablePrivileges)) {
+ $tableGrant = sprintf($grant, join(',', $tablePrivileges));
+ foreach ($context as $table) {
+ $this->exec($tableGrant . sprintf($on, $quotedDbName, $this->quoteIdentifier($table)) . $to);
+ }
+ }
+
+ if (! empty($dbPrivileges)) {
+ $this->exec(
+ sprintf($grant, join(',', $dbPrivileges))
+ . sprintf($on, $this->escapeTableWildcards($quotedDbName), '*')
+ . $to
+ );
+ }
+ } elseif ($this->config['db'] === 'pgsql') {
+ $dbPrivileges = array();
+ $schemaPrivileges = [];
+ foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) {
+ if ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) {
+ $dbPrivileges[] = $privilege;
+ }
+
+ if ($this->pgsqlGrantContexts[$privilege] & static::GLOBAL_LEVEL) {
+ $schemaPrivileges[] = $privilege;
+ }
+ }
+
+ if (! empty($schemaPrivileges)) {
+ // Allow the user to create,alter and use all attribute types in schema public
+ // such as creating and dropping custom data types (boolenum)
+ $this->exec(sprintf('GRANT %s ON SCHEMA public TO %s', implode(',', $schemaPrivileges), $username));
+ }
+
+ if (! empty($dbPrivileges)) {
+ $this->exec(sprintf(
+ 'GRANT %s ON DATABASE %s TO %s',
+ join(',', $dbPrivileges),
+ $this->config['dbname'],
+ $username
+ ));
+ }
+
+ foreach ($context as $table) {
+ // PostgreSQL documentation says "You must own the table to use ALTER TABLE.", hence it isn't
+ // sufficient to just issue grants, as the user is still not allowed to alter that table.
+ $this->exec(sprintf('ALTER TABLE %s OWNER TO %s', $table, $username));
+ }
+ }
+ }
+
+ /**
+ * Return a list of all existing database tables
+ *
+ * @return array
+ */
+ public function listTables()
+ {
+ $this->assertConnectedToDb();
+ return $this->zendConn->listTables();
+ }
+
+ /**
+ * Return whether the given database login exists
+ *
+ * @param string $username The username to search
+ *
+ * @return ?bool
+ */
+ public function hasLogin($username)
+ {
+ if ($this->config['db'] === 'mysql') {
+ $queryString = <<<EOD
+SELECT 1
+ FROM information_schema.user_privileges
+ WHERE grantee = REPLACE(CONCAT("'", REPLACE(CURRENT_USER(), '@', "'@'"), "'"), :current, :wanted)
+EOD;
+
+ $query = $this->query(
+ $queryString,
+ array(
+ ':current' => $this->config['username'],
+ ':wanted' => $username
+ )
+ );
+ return count($query->fetchAll()) > 0;
+ } elseif ($this->config['db'] === 'pgsql') {
+ $query = $this->query(
+ 'SELECT 1 FROM pg_catalog.pg_user WHERE usename = :ident LIMIT 1',
+ array(':ident' => $username)
+ );
+ return count($query->fetchAll()) === 1;
+ }
+ }
+
+ /**
+ * Add a new database login
+ *
+ * @param string $username The username of the new login
+ * @param string $password The password of the new login
+ */
+ public function addLogin($username, $password)
+ {
+ if ($this->config['db'] === 'mysql') {
+ list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn());
+ $this->exec(
+ 'CREATE USER :user@:host IDENTIFIED BY :passw',
+ array(':user' => $username, ':host' => $host, ':passw' => $password)
+ );
+ } elseif ($this->config['db'] === 'pgsql') {
+ $this->exec(sprintf(
+ 'CREATE USER %s WITH PASSWORD %s',
+ $this->quoteIdentifier($username),
+ $this->quote($password)
+ ));
+ }
+ }
+
+ /**
+ * Check whether the current user has the given privileges
+ *
+ * @param array $privileges The privilege names
+ * @param bool $requireGrants Only return true when all privileges can be granted to others
+ * @param array $context An array describing the context for which the given privileges need to apply.
+ * Only one or more table names are currently supported
+ * @param string $username The login name to which the passed privileges need to be granted
+ *
+ * @return bool
+ */
+ protected function checkMysqlPrivileges(
+ array $privileges,
+ $requireGrants = false,
+ array $context = null,
+ $username = null
+ ) {
+ $mysqlPrivileges = array_intersect($privileges, array_keys($this->mysqlGrantContexts));
+ list($_, $host) = explode('@', $this->query('select current_user()')->fetchColumn());
+ $grantee = "'" . ($username === null ? $this->config['username'] : $username) . "'@'" . $host . "'";
+
+ if (isset($this->config['dbname'])) {
+ $dbPrivileges = array();
+ $tablePrivileges = array();
+ foreach ($mysqlPrivileges as $privilege) {
+ if (! empty($context) && $this->mysqlGrantContexts[$privilege] & static::TABLE_LEVEL) {
+ $tablePrivileges[] = $privilege;
+ }
+ if ($this->mysqlGrantContexts[$privilege] & static::DATABASE_LEVEL) {
+ $dbPrivileges[] = $privilege;
+ }
+ }
+
+ $dbPrivilegesGranted = true;
+ $tablePrivilegesGranted = true;
+
+ if (! empty($dbPrivileges)) {
+ $queryString = 'SELECT COUNT(*) as matches'
+ . ' FROM information_schema.schema_privileges'
+ . ' WHERE grantee = :grantee'
+ . ' AND table_schema = :dbname'
+ . ' AND privilege_type IN (%s)'
+ . ($requireGrants ? " AND is_grantable = 'YES'" : '');
+
+ $dbAndTableQuery = $this->query(
+ sprintf($queryString, join(',', array_map(array($this, 'quote'), $dbPrivileges))),
+ array(':grantee' => $grantee, ':dbname' => $this->escapeTableWildcards($this->config['dbname']))
+ );
+ $grantedDbAndTablePrivileges = (int) $dbAndTableQuery->fetchObject()->matches;
+ if ($grantedDbAndTablePrivileges === count($dbPrivileges)) {
+ $tableExclusivePrivileges = array_diff($tablePrivileges, $dbPrivileges);
+ if (! empty($tableExclusivePrivileges)) {
+ $tablePrivileges = $tableExclusivePrivileges;
+ $tablePrivilegesGranted = false;
+ }
+ } else {
+ $tablePrivilegesGranted = false;
+ $dbExclusivePrivileges = array_diff($dbPrivileges, $tablePrivileges);
+ if (! empty($dbExclusivePrivileges)) {
+ $dbExclusiveQuery = $this->query(
+ sprintf($queryString, join(',', array_map(array($this, 'quote'), $dbExclusivePrivileges))),
+ array(
+ ':grantee' => $grantee,
+ ':dbname' => $this->escapeTableWildcards($this->config['dbname'])
+ )
+ );
+ $dbPrivilegesGranted = (int) $dbExclusiveQuery->fetchObject()->matches === count(
+ $dbExclusivePrivileges
+ );
+ }
+ }
+ }
+
+ if (! $tablePrivilegesGranted && !empty($tablePrivileges)) {
+ $query = $this->query(
+ 'SELECT COUNT(*) as matches'
+ . ' FROM information_schema.table_privileges'
+ . ' WHERE grantee = :grantee'
+ . ' AND table_schema = :dbname'
+ . ' AND table_name IN (' . join(',', array_map(array($this, 'quote'), $context)) . ')'
+ . ' AND privilege_type IN (' . join(',', array_map(array($this, 'quote'), $tablePrivileges)) . ')'
+ . ($requireGrants ? " AND is_grantable = 'YES'" : ''),
+ array(':grantee' => $grantee, ':dbname' => $this->config['dbname'])
+ );
+ $expectedAmountOfMatches = count($context) * count($tablePrivileges);
+ $tablePrivilegesGranted = (int) $query->fetchObject()->matches === $expectedAmountOfMatches;
+ }
+
+ if ($dbPrivilegesGranted && $tablePrivilegesGranted) {
+ return true;
+ }
+ }
+
+ $query = $this->query(
+ 'SELECT COUNT(*) as matches FROM information_schema.user_privileges WHERE grantee = :grantee'
+ . ' AND privilege_type IN (' . join(',', array_map(array($this, 'quote'), $mysqlPrivileges)) . ')'
+ . ($requireGrants ? " AND is_grantable = 'YES'" : ''),
+ array(':grantee' => $grantee)
+ );
+ return (int) $query->fetchObject()->matches === count($mysqlPrivileges);
+ }
+
+ /**
+ * Check whether the current user has the given privileges
+ *
+ * Note that database and table specific privileges (i.e. not SUPER, CREATE and CREATEROLE) are ignored
+ * in case no connection to the database defined in the resource configuration has been established
+ *
+ * @param array $privileges The privilege names
+ * @param bool $requireGrants Only return true when all privileges can be granted to others
+ * @param array $context An array describing the context for which the given privileges need to apply.
+ * Only one or more table names are currently supported
+ * @param string $username The login name to which the passed privileges need to be granted
+ *
+ * @return bool
+ */
+ public function checkPgsqlPrivileges(
+ array $privileges,
+ $requireGrants = false,
+ array $context = null,
+ $username = null
+ ) {
+ $privilegesGranted = true;
+ $owner = $username ?: $this->config['username'];
+ $isSuperUser = $this->query('select rolsuper from pg_roles where rolname = :user', [':user' => $owner])
+ ->fetchColumn();
+
+ if ($this->dbFromConfig) {
+ $schemaPrivileges = [];
+ $dbPrivileges = array();
+ if (! $isSuperUser) {
+ foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) {
+ if ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) {
+ $dbPrivileges[] = $privilege;
+ }
+ if ($this->pgsqlGrantContexts[$privilege] & static::GLOBAL_LEVEL) {
+ $schemaPrivileges[] = $privilege;
+ }
+ }
+
+ if (! empty($schemaPrivileges)) {
+ foreach ($schemaPrivileges as $schemaPrivilege) {
+ $query = $this->query(
+ 'SELECT has_schema_privilege(:user, :schema, :privilege) AS db_privilege_granted',
+ [
+ ':user' => $owner,
+ ':schema' => 'public',
+ ':privilege' => $schemaPrivilege . ($requireGrants ? ' WITH GRANT OPTION' : '')
+ ]
+ );
+
+ if (! $query->fetchObject()->db_privilege_granted) {
+ // The user doesn't fully have the provided privileges.
+ $privilegesGranted = false;
+ break;
+ }
+ }
+ }
+
+ if ($privilegesGranted && ! empty($dbPrivileges)) {
+ foreach ($dbPrivileges as $dbPrivilege) {
+ $query = $this->query(
+ 'SELECT has_database_privilege(:user, :dbname, :privilege) AS db_privilege_granted',
+ array(
+ ':user' => $owner,
+ ':dbname' => $this->config['dbname'],
+ ':privilege' => $dbPrivilege . ($requireGrants ? ' WITH GRANT OPTION' : '')
+ )
+ );
+ if (! $query->fetchObject()->db_privilege_granted) {
+ // The user doesn't fully have the provided privileges.
+ $privilegesGranted = false;
+ break;
+ }
+ }
+ }
+
+ if ($privilegesGranted && ! empty($context)) {
+ foreach (array_intersect($context, $this->listTables()) as $table) {
+ $query = $this->query(
+ 'SELECT tableowner FROM pg_catalog.pg_tables WHERE tablename = :tablename',
+ [':tablename' => $table]
+ );
+
+ if ($query->fetchColumn() !== $owner) {
+ $privilegesGranted = false;
+ break;
+ }
+ }
+ }
+ }
+ } else {
+ // In case we cannot check whether the user got the required db-/table-privileges due to not being
+ // connected to the database defined in the resource configuration it is safe to just ignore them
+ // as the chances are very high that the database is created later causing the current user being
+ // the owner with ALL privileges. (Which in turn can be granted to others.)
+
+ if (in_array('CREATE', $privileges, true)) {
+ $query = $this->query(
+ 'select rolcreatedb from pg_roles where rolname = :user',
+ array(':user' => $username !== null ? $username : $this->config['username'])
+ );
+ $privilegesGranted = $query->fetchColumn() !== false;
+ }
+ }
+
+ if ($privilegesGranted && in_array('CREATEROLE', $privileges, true)) {
+ $query = $this->query(
+ 'select rolcreaterole from pg_roles where rolname = :user',
+ array(':user' => $username !== null ? $username : $this->config['username'])
+ );
+ $privilegesGranted = $query->fetchColumn() !== false;
+ }
+
+ if ($privilegesGranted && in_array('SUPER', $privileges, true)) {
+ $privilegesGranted = $isSuperUser === true;
+ }
+
+ return $privilegesGranted;
+ }
+}
diff --git a/modules/setup/library/Setup/Utils/EnableModuleStep.php b/modules/setup/library/Setup/Utils/EnableModuleStep.php
new file mode 100644
index 0000000..92af5b7
--- /dev/null
+++ b/modules/setup/library/Setup/Utils/EnableModuleStep.php
@@ -0,0 +1,77 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Utils;
+
+use Exception;
+use Icinga\Application\Icinga;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Setup\Step;
+
+class EnableModuleStep extends Step
+{
+ protected $modulePaths;
+
+ protected $moduleNames;
+
+ protected $errors;
+
+ protected $warnings;
+
+ public function __construct(array $moduleNames)
+ {
+ $this->moduleNames = $moduleNames;
+
+ $this->modulePaths = array();
+ if (($appModulePath = realpath(Icinga::app()->getApplicationDir() . '/../modules')) !== false) {
+ $this->modulePaths[] = $appModulePath;
+ }
+ }
+
+ public function apply()
+ {
+ $moduleManager = Icinga::app()->getModuleManager();
+ $moduleManager->detectInstalledModules($this->modulePaths);
+
+ $success = true;
+ foreach ($this->moduleNames as $moduleName) {
+ try {
+ $moduleManager->enableModule($moduleName);
+ } catch (ConfigurationError $e) {
+ $this->warnings[$moduleName] = $e;
+ } catch (Exception $e) {
+ $this->errors[$moduleName] = $e;
+ $success = false;
+ }
+ }
+
+ return $success;
+ }
+
+ public function getSummary()
+ {
+ // Enabling a module is like a implicit action, which does not need to be shown to the user...
+ }
+
+ public function getReport()
+ {
+ $okMessage = mt('setup', 'Module "%s" has been successfully enabled.');
+ $failMessage = mt('setup', 'Module "%s" could not be enabled. An error occured:');
+
+ $report = array();
+ foreach ($this->moduleNames as $moduleName) {
+ if (isset($this->errors[$moduleName])) {
+ $report[] = sprintf($failMessage, $moduleName);
+ $report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->errors[$moduleName]));
+ } elseif (isset($this->warnings[$moduleName])) {
+ $report[] = sprintf($failMessage, $moduleName);
+ $report[] = sprintf(mt('setup', 'WARNING: %s'), $this->warnings[$moduleName]->getMessage());
+ } else {
+ $report[] = sprintf($okMessage, $moduleName);
+ }
+ }
+
+ return $report;
+ }
+}
diff --git a/modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php b/modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php
new file mode 100644
index 0000000..a3f218b
--- /dev/null
+++ b/modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php
@@ -0,0 +1,73 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Web\Form\Validator;
+
+use Exception;
+use Zend_Validate_Abstract;
+use Icinga\Util\File;
+
+/**
+ * Validator that checks if a token matches with the contents of a corresponding token-file
+ */
+class TokenValidator extends Zend_Validate_Abstract
+{
+ /**
+ * The path to the token file
+ *
+ * @var string
+ */
+ protected $tokenPath;
+
+ /**
+ * Create a new TokenValidator
+ *
+ * @param string $tokenPath The path to the token-file
+ */
+ public function __construct($tokenPath)
+ {
+ $this->tokenPath = $tokenPath;
+ $this->_messageTemplates = array(
+ 'TOKEN_FILE_ERROR' => sprintf(
+ mt('setup', 'Cannot validate token: %s (%s)'),
+ $tokenPath,
+ '%value%'
+ ),
+ 'TOKEN_FILE_EMPTY' => sprintf(
+ mt('setup', 'Cannot validate token, file "%s" is empty. Please define a token.'),
+ $tokenPath
+ ),
+ 'TOKEN_INVALID' => mt('setup', 'Invalid token supplied.')
+ );
+ }
+
+ /**
+ * Validate the given token with the one in the token-file
+ *
+ * @param string $value The token to validate
+ * @param null $context The form context (ignored)
+ *
+ * @return bool
+ */
+ public function isValid($value, $context = null)
+ {
+ try {
+ $file = new File($this->tokenPath);
+ $expectedToken = trim($file->fgets());
+ } catch (Exception $e) {
+ $msg = $e->getMessage();
+ $this->_error('TOKEN_FILE_ERROR', substr($msg, strpos($msg, ']: ') + 3));
+ return false;
+ }
+
+ if (empty($expectedToken)) {
+ $this->_error('TOKEN_FILE_EMPTY');
+ return false;
+ } elseif ($value !== $expectedToken) {
+ $this->_error('TOKEN_INVALID');
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/modules/setup/library/Setup/WebWizard.php b/modules/setup/library/Setup/WebWizard.php
new file mode 100644
index 0000000..f3b5557
--- /dev/null
+++ b/modules/setup/library/Setup/WebWizard.php
@@ -0,0 +1,768 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+use Icinga\Application\Platform;
+use Icinga\Module\Setup\Requirement\SetRequirement;
+use Icinga\Module\Setup\Requirement\WebLibraryRequirement;
+use InvalidArgumentException;
+use PDOException;
+use Icinga\Web\Form;
+use Icinga\Web\Wizard;
+use Icinga\Web\Request;
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Module\Setup\Forms\ModulePage;
+use Icinga\Module\Setup\Forms\WelcomePage;
+use Icinga\Module\Setup\Forms\SummaryPage;
+use Icinga\Module\Setup\Forms\DbResourcePage;
+use Icinga\Module\Setup\Forms\AuthBackendPage;
+use Icinga\Module\Setup\Forms\AdminAccountPage;
+use Icinga\Module\Setup\Forms\LdapDiscoveryPage;
+//use Icinga\Module\Setup\Forms\LdapDiscoveryConfirmPage;
+use Icinga\Module\Setup\Forms\LdapResourcePage;
+use Icinga\Module\Setup\Forms\RequirementsPage;
+use Icinga\Module\Setup\Forms\GeneralConfigPage;
+use Icinga\Module\Setup\Forms\AuthenticationPage;
+use Icinga\Module\Setup\Forms\DatabaseCreationPage;
+use Icinga\Module\Setup\Forms\UserGroupBackendPage;
+use Icinga\Module\Setup\Steps\DatabaseStep;
+use Icinga\Module\Setup\Steps\GeneralConfigStep;
+use Icinga\Module\Setup\Steps\ResourceStep;
+use Icinga\Module\Setup\Steps\AuthenticationStep;
+use Icinga\Module\Setup\Steps\UserGroupStep;
+use Icinga\Module\Setup\Utils\EnableModuleStep;
+use Icinga\Module\Setup\Utils\DbTool;
+use Icinga\Module\Setup\Requirement\OSRequirement;
+use Icinga\Module\Setup\Requirement\PhpModuleRequirement;
+use Icinga\Module\Setup\Requirement\PhpVersionRequirement;
+use Icinga\Module\Setup\Requirement\ConfigDirectoryRequirement;
+use Icinga\Module\Monitoring\Forms\Config\Transport\ApiTransportForm;
+
+/**
+ * Icinga Web 2 Setup Wizard
+ */
+class WebWizard extends Wizard implements SetupWizard
+{
+ /**
+ * The privileges required by Icinga Web 2 to create the database and a login
+ *
+ * @var array
+ */
+ protected $databaseCreationPrivileges = array(
+ 'CREATE',
+ 'CREATE USER', // MySQL
+ 'CREATEROLE' // PostgreSQL
+ );
+
+ /**
+ * The privileges required by Icinga Web 2 to setup the database
+ *
+ * @var array
+ */
+ protected $databaseSetupPrivileges = array(
+ 'CREATE',
+ 'ALTER', // MySQL only
+ 'REFERENCES'
+ );
+
+ /**
+ * The privileges required by Icinga Web 2 to operate the database
+ *
+ * @var array
+ */
+ protected $databaseUsagePrivileges = array(
+ 'SELECT',
+ 'INSERT',
+ 'UPDATE',
+ 'DELETE',
+ 'EXECUTE',
+ 'CREATE',
+ 'CREATE VIEW',
+ 'ALTER',
+ 'DROP',
+ 'INDEX',
+ 'USAGE', // PostgreSQL
+ 'TEMPORARY', // PostgreSql
+ 'CREATE TEMPORARY TABLES' // MySQL
+ );
+
+ /**
+ * The database tables operated by Icinga Web 2
+ *
+ * @var array
+ */
+ protected $databaseTables = array(
+ 'icingaweb_group',
+ 'icingaweb_group_membership',
+ 'icingaweb_user',
+ 'icingaweb_user_preference',
+ 'icingaweb_rememberme',
+ 'icingaweb_schema'
+ );
+
+ /**
+ * Register all pages and module wizards for this wizard
+ */
+ protected function init()
+ {
+ $this->addPage(new WelcomePage());
+ $this->addPage(new ModulePage());
+ $this->addPage(new RequirementsPage());
+ $this->addPage(new AuthenticationPage());
+ $this->addPage(new DbResourcePage(array('name' => 'setup_auth_db_resource')));
+ $this->addPage(new DatabaseCreationPage(array('name' => 'setup_auth_db_creation')));
+ $this->addPage(new LdapDiscoveryPage());
+ //$this->addPage(new LdapDiscoveryConfirmPage());
+ $this->addPage(new LdapResourcePage());
+ $this->addPage(new AuthBackendPage());
+ $this->addPage(new UserGroupBackendPage());
+ $this->addPage(new AdminAccountPage());
+ $this->addPage(new GeneralConfigPage());
+ $this->addPage(new DbResourcePage(array('name' => 'setup_config_db_resource')));
+ $this->addPage(new DatabaseCreationPage(array('name' => 'setup_config_db_creation')));
+ $this->addPage(new SummaryPage(array('name' => 'setup_summary')));
+
+ if (($modulePageData = $this->getPageData('setup_modules')) !== null) {
+ /** @var ModulePage $modulePage */
+ $modulePage = $this->getPage('setup_modules')->populate($modulePageData);
+ foreach ($modulePage->getModuleWizards() as $moduleWizard) {
+ $this->addPage($moduleWizard);
+ }
+ }
+ }
+
+ /**
+ * Setup the given page that is either going to be displayed or validated
+ *
+ * @param Form $page The page to setup
+ * @param Request $request The current request
+ */
+ public function setupPage(Form $page, Request $request)
+ {
+ if ($page->getName() === 'setup_requirements') {
+ /** @var RequirementsPage $page */
+ $page->setWizard($this);
+ } elseif ($page->getName() === 'setup_authentication_backend') {
+ /** @var AuthBackendPage $page */
+
+ $authData = $this->getPageData('setup_authentication_type');
+ if ($authData['type'] === 'db') {
+ $page->setResourceConfig($this->getPageData('setup_auth_db_resource'));
+ } elseif ($authData['type'] === 'ldap') {
+ $page->setResourceConfig($this->getPageData('setup_ldap_resource'));
+
+ $suggestions = $this->getPageData('setup_ldap_discovery');
+ if (isset($suggestions['backend'])) {
+ $page->setSuggestions($suggestions['backend']);
+ }
+
+ if ($this->getDirection() === static::FORWARD) {
+ $backendConfig = $this->getPageData('setup_authentication_backend');
+ if ($backendConfig !== null && $request->getPost('name') !== $backendConfig['name']) {
+ $pageData = & $this->getPageData();
+ unset($pageData['setup_usergroup_backend']);
+ }
+ }
+ }
+
+ if ($this->getDirection() === static::FORWARD) {
+ $backendConfig = $this->getPageData('setup_authentication_backend');
+ if ($backendConfig !== null && $request->getPost('backend') !== $backendConfig['backend']) {
+ $pageData = & $this->getPageData();
+ unset($pageData['setup_usergroup_backend']);
+ }
+ }
+ /*} elseif ($page->getName() === 'setup_ldap_discovery_confirm') {
+ $page->setResourceConfig($this->getPageData('setup_ldap_discovery'));*/
+ } elseif ($page->getName() === 'setup_auth_db_resource') {
+ $page->addDescription(mt(
+ 'setup',
+ 'Now please configure the database resource where to store users and user groups.'
+ ));
+ $page->addDescription(mt(
+ 'setup',
+ 'Note that the database itself does not need to exist at this time as'
+ . ' it is going to be created once the wizard is about to be finished.'
+ ));
+ } elseif ($page->getName() === 'setup_usergroup_backend') {
+ /** @var UserGroupBackendPage $page */
+ $page->setResourceConfig($this->getPageData('setup_ldap_resource'));
+ $page->setBackendConfig($this->getPageData('setup_authentication_backend'));
+ } elseif ($page->getName() === 'setup_admin_account') {
+ /** @var AdminAccountPage $page */
+ $page->setBackendConfig($this->getPageData('setup_authentication_backend'));
+ $page->setGroupConfig($this->getPageData('setup_usergroup_backend'));
+ $authData = $this->getPageData('setup_authentication_type');
+ if ($authData['type'] === 'db') {
+ $page->setResourceConfig($this->getPageData('setup_auth_db_resource'));
+ } elseif ($authData['type'] === 'ldap') {
+ $page->setResourceConfig($this->getPageData('setup_ldap_resource'));
+ }
+ } elseif ($page->getName() === 'setup_auth_db_creation' || $page->getName() === 'setup_config_db_creation') {
+ /** @var DatabaseCreationPage $page */
+ $page->setDatabaseSetupPrivileges(
+ array_unique(array_merge($this->databaseCreationPrivileges, $this->databaseSetupPrivileges))
+ );
+ $page->setDatabaseUsagePrivileges($this->databaseUsagePrivileges);
+ $page->setResourceConfig(
+ $this->getPageData('setup_auth_db_resource') ?: $this->getPageData('setup_config_db_resource')
+ );
+ } elseif ($page->getName() === 'setup_summary') {
+ /** @var SummaryPage $page */
+ $page->setSubjectTitle('Icinga Web 2');
+ $page->setSummary($this->getSetup()->getSummary());
+ } elseif ($page->getName() === 'setup_config_db_resource') {
+ $page->addDescription(mt(
+ 'setup',
+ 'Now please configure the database resource where to store user preferences.'
+ ));
+ $page->addDescription(mt(
+ 'setup',
+ 'Note that the database itself does not need to exist at this time as'
+ . ' it is going to be created once the wizard is about to be finished.'
+ ));
+
+ $ldapData = $this->getPageData('setup_ldap_resource');
+ if ($ldapData !== null && $request->getPost('name') === $ldapData['name']) {
+ $page->error(
+ mt('setup', 'The given resource name must be unique and is already in use by the LDAP resource')
+ );
+ }
+ } elseif ($page->getName() === 'setup_ldap_resource') {
+ $suggestion = $this->getPageData('setup_ldap_discovery');
+ if (isset($suggestion['resource'])) {
+ $page->populate($suggestion['resource']);
+ }
+
+ if ($this->getDirection() === static::FORWARD) {
+ $resourceConfig = $this->getPageData('setup_ldap_resource');
+ if ($resourceConfig !== null && $request->getPost('name') !== $resourceConfig['name']) {
+ $pageData = & $this->getPageData();
+ unset($pageData['setup_usergroup_backend']);
+ }
+ }
+ } elseif ($page->getName() === 'setup_authentication_type') {
+ $authData = $this->getPageData($page->getName());
+ $pageData = & $this->getPageData();
+
+ if ($authData !== null && $request->getPost('type') !== $authData['type']) {
+ // Drop any existing page data in case the authentication type has changed,
+ // otherwise it will conflict with other forms that depend on this one
+ unset($pageData['setup_admin_account']);
+ unset($pageData['setup_authentication_backend']);
+
+ if ($authData['type'] === 'db') {
+ unset($pageData['setup_auth_db_resource']);
+ unset($pageData['setup_auth_db_creation']);
+ } elseif ($request->getPost('type') === 'db') {
+ unset($pageData['setup_config_db_resource']);
+ unset($pageData['setup_config_db_creation']);
+ }
+ } elseif (isset($authData['type']) && $authData['type'] == 'external') {
+ // If you choose the authentication type external and validate the database and then come
+ // back to change the authentication type but do not change it, you will get an database configuration
+ // related error message on the next page. To avoid this error, the 'setup_config_db_resource'
+ // page must be unset.
+
+ unset($pageData['setup_config_db_resource']);
+ }
+ }
+ }
+
+ /**
+ * Return the new page to set as current page
+ *
+ * {@inheritdoc} Runs additional checks related to some registered pages.
+ *
+ * @param string $requestedPage The name of the requested page
+ * @param Form $originPage The origin page
+ *
+ * @return Form The new page
+ *
+ * @throws InvalidArgumentException In case the requested page does not exist or is not permitted yet
+ */
+ protected function getNewPage($requestedPage, Form $originPage)
+ {
+ $skip = false;
+ $newPage = parent::getNewPage($requestedPage, $originPage);
+ if ($newPage->getName() === 'setup_auth_db_resource') {
+ $authData = $this->getPageData('setup_authentication_type');
+ $skip = $authData['type'] !== 'db';
+ } elseif ($newPage->getName() === 'setup_ldap_discovery') {
+ $authData = $this->getPageData('setup_authentication_type');
+ $skip = $authData['type'] !== 'ldap';
+ /*} elseif ($newPage->getName() === 'setup_ldap_discovery_confirm') {
+ $skip = false === $this->hasPageData('setup_ldap_discovery');*/
+ } elseif ($newPage->getName() === 'setup_ldap_resource') {
+ $authData = $this->getPageData('setup_authentication_type');
+ $skip = $authData['type'] !== 'ldap';
+ } elseif ($newPage->getName() === 'setup_usergroup_backend') {
+ $backendConfig = $this->getPageData('setup_authentication_backend');
+ $skip = $backendConfig['backend'] !== 'ldap';
+ } elseif ($newPage->getName() === 'setup_config_db_resource') {
+ $authData = $this->getPageData('setup_authentication_type');
+ $skip = $authData['type'] === 'db';
+ } elseif (in_array($newPage->getName(), array('setup_auth_db_creation', 'setup_config_db_creation'))) {
+ if (($newPage->getName() === 'setup_auth_db_creation' || $this->hasPageData('setup_config_db_resource'))
+ && (($config = $this->getPageData('setup_auth_db_resource')) !== null
+ || ($config = $this->getPageData('setup_config_db_resource')) !== null)
+ && !$config['skip_validation'] && $this->getDirection() == static::FORWARD
+ ) {
+ // Execute this code only if the direction is forward.
+ // Otherwise, an error will be output when you go back.
+ $db = new DbTool($config);
+
+ try {
+ $db->connectToDb(); // Are we able to login on the database?
+
+ if (array_search(reset($this->databaseTables), $db->listTables(), true) === false) {
+ // In case the database schema does not yet exist the
+ // user needs the privileges to setup the database
+ $skip = $db->checkPrivileges($this->databaseSetupPrivileges, $this->databaseTables);
+ } else {
+ // In case the database schema exists the user needs the required privileges
+ // to operate the database, if those are missing we ask for another user
+ $skip = $db->checkPrivileges($this->databaseUsagePrivileges, $this->databaseTables);
+ }
+ } catch (PDOException $_) {
+ try {
+ $db->connectToHost(); // Are we able to login on the server?
+ // It is not possible to reliably determine whether a database exists or not if a user can't
+ // log in to the database, so we just require the user to be able to create the database
+ $skip = $db->checkPrivileges(
+ array_unique(
+ array_merge($this->databaseCreationPrivileges, $this->databaseSetupPrivileges)
+ ),
+ $this->databaseTables
+ );
+ } catch (PDOException $_) {
+ // We are NOT able to login on the server..
+ }
+ }
+ } else {
+ $skip = true;
+ }
+ }
+
+ return $skip ? $this->skipPage($newPage) : $newPage;
+ }
+
+ /**
+ * Add buttons to the given page based on its position in the page-chain
+ *
+ * @param Form $page The page to add the buttons to
+ */
+ protected function addButtons(Form $page)
+ {
+ parent::addButtons($page);
+
+ $pages = $this->getPages();
+ $index = array_search($page, $pages, true);
+ if ($index === 0) {
+ $page->getElement(static::BTN_NEXT)->setLabel(
+ mt('setup', 'Start', 'setup.welcome.btn.next')
+ );
+ } elseif ($index === count($pages) - 1) {
+ $page->getElement(static::BTN_NEXT)->setLabel(
+ mt('setup', 'Setup Icinga Web 2', 'setup.summary.btn.finish')
+ );
+ }
+
+ $authData = $this->getPageData('setup_authentication_type');
+ $veto = $page->getName() === 'setup_authentication_backend' && $authData['type'] === 'db';
+ if (! $veto && in_array($page->getName(), array(
+ 'setup_authentication_backend',
+ 'setup_auth_db_resource',
+ 'setup_config_db_resource',
+ 'setup_ldap_resource',
+ 'setup_monitoring_ido',
+ 'setup_icingadb_resource',
+ 'setup_icingadb_redis',
+ 'setup_icingadb_api_transport'
+ ))) {
+ $page->addElement(
+ 'submit',
+ 'backend_validation',
+ array(
+ 'ignore' => true,
+ 'label' => t('Validate Configuration'),
+ 'data-progress-label' => t('Validation In Progress'),
+ 'decorators' => array('ViewHelper'),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ $page->getDisplayGroup('buttons')->addElement($page->getElement('backend_validation'));
+ }
+
+ if ($page->getName() === 'setup_command_transport') {
+ if ($page->getSubForm('transport_form')->getSubForm('transport_form') instanceof ApiTransportForm) {
+ $page->addElement(
+ 'submit',
+ 'transport_validation',
+ array(
+ 'ignore' => true,
+ 'label' => t('Validate Configuration'),
+ 'data-progress-label' => t('Validation In Progress'),
+ 'decorators' => array('ViewHelper'),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ $page->getDisplayGroup('buttons')->addElement($page->getElement('transport_validation'));
+ }
+ }
+ }
+
+ /**
+ * Clear the session being used by this wizard
+ *
+ * @param bool $removeToken If true, the setup token will be removed
+ */
+ public function clearSession($removeToken = true)
+ {
+ parent::clearSession();
+
+ if ($removeToken) {
+ $tokenPath = Config::resolvePath('setup.token');
+ if (file_exists($tokenPath)) {
+ @unlink($tokenPath);
+ }
+ }
+ }
+
+ /**
+ * Return the setup for this wizard
+ *
+ * @return Setup
+ */
+ public function getSetup()
+ {
+ $pageData = $this->getPageData();
+ $setup = new Setup();
+
+ if (isset($pageData['setup_auth_db_resource'])
+ && !$pageData['setup_auth_db_resource']['skip_validation']
+ && (! isset($pageData['setup_auth_db_creation'])
+ || !$pageData['setup_auth_db_creation']['skip_validation']
+ )
+ ) {
+ $setup->addStep(
+ new DatabaseStep(array(
+ 'tables' => $this->databaseTables,
+ 'privileges' => $this->databaseUsagePrivileges,
+ 'resourceConfig' => $pageData['setup_auth_db_resource'],
+ 'adminName' => isset($pageData['setup_auth_db_creation']['username'])
+ ? $pageData['setup_auth_db_creation']['username']
+ : null,
+ 'adminPassword' => isset($pageData['setup_auth_db_creation']['password'])
+ ? $pageData['setup_auth_db_creation']['password']
+ : null,
+ 'schemaPath' => Config::module('setup')
+ ->get('schema', 'path', Icinga::app()->getBaseDir('schema'))
+ ))
+ );
+ } elseif (isset($pageData['setup_config_db_resource'])
+ && !$pageData['setup_config_db_resource']['skip_validation']
+ && (! isset($pageData['setup_config_db_creation'])
+ || !$pageData['setup_config_db_creation']['skip_validation']
+ )
+ ) {
+ $setup->addStep(
+ new DatabaseStep(array(
+ 'tables' => $this->databaseTables,
+ 'privileges' => $this->databaseUsagePrivileges,
+ 'resourceConfig' => $pageData['setup_config_db_resource'],
+ 'adminName' => isset($pageData['setup_config_db_creation']['username'])
+ ? $pageData['setup_config_db_creation']['username']
+ : null,
+ 'adminPassword' => isset($pageData['setup_config_db_creation']['password'])
+ ? $pageData['setup_config_db_creation']['password']
+ : null,
+ 'schemaPath' => Config::module('setup')
+ ->get('schema', 'path', Icinga::app()->getBaseDir('schema'))
+ ))
+ );
+ }
+
+ $setup->addStep(
+ new GeneralConfigStep(array(
+ 'generalConfig' => $pageData['setup_general_config'],
+ 'resourceName' => isset($pageData['setup_auth_db_resource']['name'])
+ ? $pageData['setup_auth_db_resource']['name']
+ : (isset($pageData['setup_config_db_resource']['name'])
+ ? $pageData['setup_config_db_resource']['name']
+ : null
+ )
+ ))
+ );
+
+ $adminAccountType = $pageData['setup_admin_account']['user_type'];
+ if ($adminAccountType === 'user_group') {
+ $adminAccountData = array('groupname' => $pageData['setup_admin_account'][$adminAccountType]);
+ } else {
+ $adminAccountData = array('username' => $pageData['setup_admin_account'][$adminAccountType]);
+ if ($adminAccountType === 'new_user' && !$pageData['setup_auth_db_resource']['skip_validation']
+ && (! isset($pageData['setup_auth_db_creation'])
+ || !$pageData['setup_auth_db_creation']['skip_validation']
+ )
+ ) {
+ $adminAccountData['resourceConfig'] = $pageData['setup_auth_db_resource'];
+ $adminAccountData['password'] = $pageData['setup_admin_account']['new_user_password'];
+ }
+ }
+ $authType = $pageData['setup_authentication_type']['type'];
+ $setup->addStep(
+ new AuthenticationStep(array(
+ 'adminAccountData' => $adminAccountData,
+ 'backendConfig' => $pageData['setup_authentication_backend'],
+ 'resourceName' => $authType === 'db' ? $pageData['setup_auth_db_resource']['name'] : (
+ $authType === 'ldap' ? $pageData['setup_ldap_resource']['name'] : null
+ )
+ ))
+ );
+
+ if ($authType !== 'external') {
+ $setup->addStep(
+ new UserGroupStep(array(
+ 'backendConfig' => $pageData['setup_authentication_backend'],
+ 'groupConfig' => isset($pageData['setup_usergroup_backend'])
+ ? $pageData['setup_usergroup_backend']
+ : null,
+ 'resourceName' => $authType === 'db'
+ ? $pageData['setup_auth_db_resource']['name']
+ : $pageData['setup_ldap_resource']['name'],
+ 'resourceConfig' => $authType === 'db'
+ ? $pageData['setup_auth_db_resource']
+ : null,
+ 'username' => $authType === 'db'
+ ? $pageData['setup_admin_account'][$adminAccountType]
+ : null
+ ))
+ );
+ }
+
+ if (isset($pageData['setup_auth_db_resource'])
+ || isset($pageData['setup_config_db_resource'])
+ || isset($pageData['setup_ldap_resource'])
+ ) {
+ $setup->addStep(
+ new ResourceStep(array(
+ 'dbResourceConfig' => isset($pageData['setup_auth_db_resource'])
+ ? array_diff_key($pageData['setup_auth_db_resource'], array('skip_validation' => null))
+ : (isset($pageData['setup_config_db_resource'])
+ ? array_diff_key($pageData['setup_config_db_resource'], array('skip_validation' => null))
+ : null
+ ),
+ 'ldapResourceConfig' => isset($pageData['setup_ldap_resource'])
+ ? array_diff_key($pageData['setup_ldap_resource'], array('skip_validation' => null))
+ : null
+ ))
+ );
+ }
+
+ foreach ($this->getWizards() as $wizard) {
+ if ($wizard->isComplete()) {
+ $setup->addSteps($wizard->getSetup()->getSteps());
+ }
+ }
+
+ /** @var ModulePage $setupPage */
+ $setupPage = $this->getPage('setup_modules');
+ $setup->addStep(new EnableModuleStep(array_keys($setupPage->getCheckedModules())));
+
+ return $setup;
+ }
+
+ /**
+ * Return the requirements of this wizard
+ *
+ * @return RequirementSet
+ */
+ public function getRequirements($skipModules = false)
+ {
+ $set = new RequirementSet();
+
+ $set->add(new PhpVersionRequirement(array(
+ 'condition' => array('>=', '7.2'),
+ 'description' => sprintf(mt(
+ 'setup',
+ 'Running Icinga Web 2 requires PHP version %s.'
+ ), '7.2')
+ )));
+
+ $set->add(new OSRequirement(array(
+ 'optional' => true,
+ 'condition' => 'linux',
+ 'description' => mt(
+ 'setup',
+ 'Icinga Web 2 is developed for and tested on Linux. While we cannot'
+ . ' guarantee they will, other platforms may also perform as well.'
+ )
+ )));
+
+ $set->add(new WebLibraryRequirement(array(
+ 'condition' => ['icinga-php-library', '>=', '0.13.0'],
+ 'alias' => 'Icinga PHP library',
+ 'description' => mt(
+ 'setup',
+ 'The Icinga PHP library (IPL) is required for Icinga Web 2 and modules'
+ )
+ )));
+
+ $set->add(new WebLibraryRequirement(array(
+ 'condition' => ['icinga-php-thirdparty', '>=', '0.12.0'],
+ 'alias' => 'Icinga PHP Thirdparty',
+ 'description' => mt(
+ 'setup',
+ 'The Icinga PHP Thirdparty library is required for Icinga Web 2 and modules'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'condition' => 'OpenSSL',
+ 'description' => mt(
+ 'setup',
+ 'The PHP module for OpenSSL is required to generate cryptographically safe password salts.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'condition' => 'XML',
+ 'description' => mt(
+ 'setup',
+ 'The XML module for PHP is required for Markdown and custom HTML annotations.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'condition' => 'JSON',
+ 'description' => mt(
+ 'setup',
+ 'The JSON module for PHP is required for various export functionalities as well as APIs.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'condition' => 'gettext',
+ 'description' => mt(
+ 'setup',
+ 'For message localization, the gettext module for PHP is required.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'condition' => 'INTL',
+ 'description' => mt(
+ 'setup',
+ 'For language, timezone and date/time format negotiation, the INTL module for PHP is required.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'condition' => 'DOM',
+ 'description' => mt(
+ 'setup',
+ 'For charts and exports of views and reports to PDF, the DOM module for PHP is required.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'optional' => true,
+ 'condition' => 'LDAP',
+ 'description' => mt(
+ 'setup',
+ 'If you\'d like to authenticate users using LDAP the corresponding PHP module is required.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'optional' => true,
+ 'condition' => 'mbstring',
+ 'description' => mt(
+ 'setup',
+ 'In case you want views being exported to PDF, you\'ll need the mbstring extension for PHP.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'optional' => true,
+ 'condition' => 'GD',
+ 'description' => mt(
+ 'setup',
+ 'In case you want views being exported to PDF, you\'ll need the GD extension for PHP.'
+ )
+ )));
+
+ $set->add(new PhpModuleRequirement(array(
+ 'optional' => true,
+ 'condition' => 'Imagick',
+ 'description' => mt(
+ 'setup',
+ 'In case you want graphs being exported to PDF as well, you\'ll need the ImageMagick extension for PHP.'
+ )
+ )));
+
+ $dbSet = new RequirementSet(false, RequirementSet::MODE_OR);
+ $dbSet->add(new PhpModuleRequirement(array(
+ 'optional' => true,
+ 'condition' => 'pdo_mysql',
+ 'alias' => 'PDO-MySQL',
+ 'description' => mt(
+ 'setup',
+ 'To store users or preferences in a MySQL database the PDO-MySQL module for PHP is required.'
+ )
+ )));
+ $dbSet->add(new PhpModuleRequirement(array(
+ 'optional' => true,
+ 'condition' => 'pdo_pgsql',
+ 'alias' => 'PDO-PostgreSQL',
+ 'description' => mt(
+ 'setup',
+ 'To store users or preferences in a PostgreSQL database the PDO-PostgreSQL module for PHP is required.'
+ )
+ )));
+ $set->merge($dbSet);
+
+ $dbRequire = (new SetRequirement(array(
+ 'optional' => false,
+ 'condition' => $dbSet,
+ 'title' =>'Database',
+ 'alias' => 'PDO-MySQL OR PDO-PostgreSQL',
+ 'description' => mt(
+ 'setup',
+ 'A database is mandatory, therefore at least one module '
+ . 'PDO-MySQL or PDO-PostgreSQL for PHP is required.'
+ )
+ )));
+
+ $set->add($dbRequire);
+
+ $set->add(new ConfigDirectoryRequirement(array(
+ 'condition' => Icinga::app()->getStorageDir(),
+ 'title' => mt('setup', 'Read- and writable storage directory'),
+ 'description' => mt(
+ 'setup',
+ 'The Icinga Web 2 storage directory defaults to "/var/lib/icingaweb2", if' .
+ ' not explicitly set in the environment variable "ICINGAWEB_STORAGEDIR".'
+ )
+ )));
+
+ $set->add(new ConfigDirectoryRequirement(array(
+ 'condition' => Icinga::app()->getConfigDir(),
+ 'description' => mt(
+ 'setup',
+ 'The Icinga Web 2 configuration directory defaults to "/etc/icingaweb2", if' .
+ ' not explicitly set in the environment variable "ICINGAWEB_CONFIGDIR".'
+ )
+ )));
+
+ if (! $skipModules) {
+ foreach ($this->getWizards() as $wizard) {
+ $set->merge($wizard->getRequirements());
+ }
+ }
+
+ return $set;
+ }
+}
diff --git a/modules/setup/library/Setup/Webserver.php b/modules/setup/library/Setup/Webserver.php
new file mode 100644
index 0000000..77ff237
--- /dev/null
+++ b/modules/setup/library/Setup/Webserver.php
@@ -0,0 +1,233 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+use Icinga\Application\Icinga;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Base class for generating webserver configuration
+ */
+abstract class Webserver
+{
+ /**
+ * Document root
+ *
+ * @var string
+ */
+ protected $documentRoot;
+
+ /**
+ * URL path of Icinga Web 2
+ *
+ * @var string
+ */
+ protected $urlPath = '/icingaweb2';
+
+ /**
+ * Path to Icinga Web 2's configuration files
+ *
+ * @var string
+ */
+ protected $configDir;
+
+ /**
+ * Address or path where to pass requests to FPM
+ *
+ * @var string
+ */
+ protected $fpmUri;
+
+ /**
+ * Enable to pass requests to FPM
+ *
+ * @var bool
+ */
+ protected $enableFpm = false;
+
+ /**
+ * Create instance by type name
+ *
+ * @param string $type
+ *
+ * @return Webserver
+ *
+ * @throws ProgrammingError
+ */
+ public static function createInstance($type)
+ {
+ $class = __NAMESPACE__ . '\\Webserver\\' . ucfirst($type);
+ if (class_exists($class)) {
+ return new $class();
+ }
+ throw new ProgrammingError('Class "%s" does not exist', $class);
+ }
+
+ /**
+ * Generate configuration
+ *
+ * @return string
+ */
+ public function generate()
+ {
+ $template = $this->getTemplate();
+
+ $searchTokens = array(
+ '{urlPath}',
+ '{documentRoot}',
+ '{aliasDocumentRoot}',
+ '{configDir}',
+ '{fpmUri}'
+ );
+ $replaceTokens = array(
+ $this->getUrlPath(),
+ $this->getDocumentRoot(),
+ preg_match('~/$~', $this->getUrlPath()) ? $this->getDocumentRoot() . '/' : $this->getDocumentRoot(),
+ $this->getConfigDir(),
+ $this->getFpmUri()
+ );
+ $template = str_replace($searchTokens, $replaceTokens, $template);
+ return $template;
+ }
+
+ /**
+ * Specific template
+ *
+ * @return string
+ */
+ abstract protected function getTemplate();
+
+ /**
+ * Set the URL path of Icinga Web 2
+ *
+ * @param string $urlPath
+ *
+ * @return $this
+ */
+ public function setUrlPath($urlPath)
+ {
+ $this->urlPath = '/' . ltrim(trim((string) $urlPath), '/');
+ return $this;
+ }
+
+ /**
+ * Get the URL path of Icinga Web 2
+ *
+ * @return string
+ */
+ public function getUrlPath()
+ {
+ return $this->urlPath;
+ }
+
+ /**
+ * Set the document root
+ *
+ * @param string $documentRoot
+ *
+ * @return $this
+ */
+ public function setDocumentRoot($documentRoot)
+ {
+ $this->documentRoot = trim((string) $documentRoot);
+ return $this;
+ }
+
+ /**
+ * Detect the document root
+ *
+ * @return string
+ */
+ public function detectDocumentRoot()
+ {
+ return Icinga::app()->getBaseDir('public');
+ }
+
+ /**
+ * Get the document root
+ *
+ * @return string
+ */
+ public function getDocumentRoot()
+ {
+ if ($this->documentRoot === null) {
+ $this->documentRoot = $this->detectDocumentRoot();
+ }
+ return $this->documentRoot;
+ }
+
+ /**
+ * Set the configuration directory
+ *
+ * @param string $configDir
+ *
+ * @return $this
+ */
+ public function setConfigDir($configDir)
+ {
+ $this->configDir = (string) $configDir;
+ return $this;
+ }
+
+ /**
+ * Get the configuration directory
+ *
+ * @return string
+ */
+ public function getConfigDir()
+ {
+ if ($this->configDir === null) {
+ return Icinga::app()->getConfigDir();
+ }
+ return $this->configDir;
+ }
+
+ /**
+ * Get whether FPM is enabled
+ *
+ * @return bool
+ */
+ public function getEnableFpm()
+ {
+ return $this->enableFpm;
+ }
+
+ /**
+ * Set FPM enabled
+ *
+ * @param bool $flag
+ *
+ * @return $this
+ */
+ public function setEnableFpm($flag)
+ {
+ $this->enableFpm = (bool) $flag;
+
+ return $this;
+ }
+
+ /**
+ * Get the address or path where to pass requests to FPM
+ *
+ * @return string
+ */
+ public function getFpmUri()
+ {
+ return $this->fpmUri;
+ }
+
+ /**
+ * Set the address or path where to pass requests to FPM
+ *
+ * @param string $uri
+ *
+ * @return $this
+ */
+ public function setFpmUri($uri)
+ {
+ $this->fpmUri = (string) $uri;
+
+ return $this;
+ }
+}
diff --git a/modules/setup/library/Setup/Webserver/Apache.php b/modules/setup/library/Setup/Webserver/Apache.php
new file mode 100644
index 0000000..fdb367f
--- /dev/null
+++ b/modules/setup/library/Setup/Webserver/Apache.php
@@ -0,0 +1,142 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Webserver;
+
+use Icinga\Module\Setup\Webserver;
+
+/**
+ * Generate Apache 2.x configuration
+ */
+class Apache extends Webserver
+{
+ protected $fpmUri = '127.0.0.1:9000';
+
+ protected function getTemplate()
+ {
+ if (! $this->enableFpm) {
+ return <<<'EOD'
+Alias {urlPath} "{aliasDocumentRoot}"
+
+# Remove comments if you want to use PHP FPM and your Apache version is older than 2.4
+#<IfVersion < 2.4>
+# # Forward PHP requests to FPM
+# SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+# <LocationMatch "^{urlPath}/(.*\.php)$">
+# ProxyPassMatch "fcgi://{fpmUri}/{documentRoot}/$1"
+# </LocationMatch>
+#</IfVersion>
+
+<Directory "{documentRoot}">
+ Options SymLinksIfOwnerMatch
+ AllowOverride None
+
+ DirectoryIndex index.php
+
+ <IfModule mod_authz_core.c>
+ # Apache 2.4
+ <RequireAll>
+ Require all granted
+ </RequireAll>
+ </IfModule>
+
+ <IfModule !mod_authz_core.c>
+ # Apache 2.2
+ Order allow,deny
+ Allow from all
+ </IfModule>
+
+ SetEnv ICINGAWEB_CONFIGDIR "{configDir}"
+
+ EnableSendfile Off
+
+ <IfModule mod_rewrite.c>
+ RewriteEngine on
+ RewriteBase {urlPath}/
+ RewriteCond %{REQUEST_FILENAME} -s [OR]
+ RewriteCond %{REQUEST_FILENAME} -l [OR]
+ RewriteCond %{REQUEST_FILENAME} -d
+ RewriteRule ^.*$ - [NC,L]
+ RewriteRule ^.*$ index.php [NC,L]
+ </IfModule>
+
+ <IfModule !mod_rewrite.c>
+ DirectoryIndex error_norewrite.html
+ ErrorDocument 404 {urlPath}/error_norewrite.html
+ </IfModule>
+
+# Remove comments if you want to use PHP FPM and your Apache version
+# is greater than or equal to 2.4
+# <IfVersion >= 2.4>
+# # Forward PHP requests to FPM
+# SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+# <FilesMatch "\.php$">
+# SetHandler "proxy:fcgi://{fpmUri}"
+# ErrorDocument 503 {urlPath}/error_unavailable.html
+# </FilesMatch>
+# </IfVersion>
+</Directory>
+EOD;
+ } else {
+ return <<<'EOD'
+Alias {urlPath} "{aliasDocumentRoot}"
+
+<IfVersion < 2.4>
+ # Forward PHP requests to FPM
+ SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+ <LocationMatch "^{urlPath}/(.*\.php)$">
+ ProxyPassMatch "fcgi://{fpmUri}/{documentRoot}/$1"
+ </LocationMatch>
+</IfVersion>
+
+<Directory "{documentRoot}">
+ Options SymLinksIfOwnerMatch
+ AllowOverride None
+
+ DirectoryIndex index.php
+
+ <IfModule mod_authz_core.c>
+ # Apache 2.4
+ <RequireAll>
+ Require all granted
+ </RequireAll>
+ </IfModule>
+
+ <IfModule !mod_authz_core.c>
+ # Apache 2.2
+ Order allow,deny
+ Allow from all
+ </IfModule>
+
+ SetEnv ICINGAWEB_CONFIGDIR "{configDir}"
+
+ EnableSendfile Off
+
+ <IfModule mod_rewrite.c>
+ RewriteEngine on
+ RewriteBase {urlPath}/
+ RewriteCond %{REQUEST_FILENAME} -s [OR]
+ RewriteCond %{REQUEST_FILENAME} -l [OR]
+ RewriteCond %{REQUEST_FILENAME} -d
+ RewriteRule ^.*$ - [NC,L]
+ RewriteRule ^.*$ index.php [NC,L]
+ </IfModule>
+
+ <IfModule !mod_rewrite.c>
+ DirectoryIndex error_norewrite.html
+ ErrorDocument 404 {urlPath}/error_norewrite.html
+ </IfModule>
+
+ <IfVersion >= 2.4>
+ # Forward PHP requests to FPM
+ SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+ <FilesMatch "\.php$">
+ SetHandler "proxy:fcgi://{fpmUri}"
+ ErrorDocument 503 {urlPath}/error_unavailable.html
+ </FilesMatch>
+ </IfVersion>
+</Directory>
+EOD;
+ }
+ }
+}
diff --git a/modules/setup/library/Setup/Webserver/Nginx.php b/modules/setup/library/Setup/Webserver/Nginx.php
new file mode 100644
index 0000000..c7ae716
--- /dev/null
+++ b/modules/setup/library/Setup/Webserver/Nginx.php
@@ -0,0 +1,36 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup\Webserver;
+
+use Icinga\Module\Setup\Webserver;
+
+/**
+ * Generate nginx configuration
+ */
+class Nginx extends Webserver
+{
+ protected $fpmUri = '127.0.0.1:9000';
+
+ protected $enableFpm = true;
+
+ protected function getTemplate()
+ {
+ return <<<'EOD'
+location ~ ^{urlPath}/index\.php(.*)$ {
+ fastcgi_pass {fpmUri};
+ fastcgi_index index.php;
+ include fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME {documentRoot}/index.php;
+ fastcgi_param ICINGAWEB_CONFIGDIR {configDir};
+ fastcgi_param REMOTE_USER $remote_user;
+}
+
+location ~ ^{urlPath}(.+)? {
+ alias {documentRoot};
+ index index.php;
+ try_files $1 $uri $uri/ {urlPath}/index.php$is_args$args;
+}
+EOD;
+ }
+}
diff --git a/modules/setup/module.info b/modules/setup/module.info
new file mode 100644
index 0000000..6127e6d
--- /dev/null
+++ b/modules/setup/module.info
@@ -0,0 +1,6 @@
+Module: setup
+Version: 2.12.1
+Description: Setup module
+ Web based wizard for setting up Icinga Web 2 and its modules.
+ This includes the data backends (e.g. relational database, LDAP),
+ the authentication method, where to store the user preferences and much more.
diff --git a/modules/translation/application/clicommands/CompileCommand.php b/modules/translation/application/clicommands/CompileCommand.php
new file mode 100644
index 0000000..8408009
--- /dev/null
+++ b/modules/translation/application/clicommands/CompileCommand.php
@@ -0,0 +1,40 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Translation\Clicommands;
+
+use Icinga\Module\Translation\Cli\TranslationCommand;
+
+/**
+ * Translation compiler
+ *
+ * This command will compile gettext catalogs of modules.
+ *
+ * Once a catalog is compiled its content is used by Icinga Web 2 to display
+ * messages in the configured language.
+ */
+class CompileCommand extends TranslationCommand
+{
+ /**
+ * Compile a module gettext catalog
+ *
+ * This will compile the catalog of the given module and locale.
+ *
+ * USAGE:
+ *
+ * icingacli translation compile <module> <locale>
+ *
+ * EXAMPLES:
+ *
+ * icingacli translation compile demo de_DE
+ * icingacli translation compile demo fr_FR
+ */
+ public function moduleAction()
+ {
+ $module = $this->validateModuleName($this->params->shift());
+ $locale = $this->validateLocaleCode($this->params->shift());
+
+ $helper = $this->getTranslationHelper($locale);
+ $helper->compileModuleTranslation($module);
+ }
+}
diff --git a/modules/translation/application/clicommands/RefreshCommand.php b/modules/translation/application/clicommands/RefreshCommand.php
new file mode 100644
index 0000000..b4b2dc0
--- /dev/null
+++ b/modules/translation/application/clicommands/RefreshCommand.php
@@ -0,0 +1,40 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Translation\Clicommands;
+
+use Icinga\Module\Translation\Cli\TranslationCommand;
+
+/**
+ * Translation updater
+ *
+ * This command will create a new or update any existing gettext catalog of a module.
+ *
+ * Once a catalog has been created/updated one can open it with a editor for
+ * PO-files and start with the actual translation.
+ */
+class RefreshCommand extends TranslationCommand
+{
+ /**
+ * Generate or update a module gettext catalog
+ *
+ * This will create/update the PO-file of the given module and locale.
+ *
+ * USAGE:
+ *
+ * icingacli translation refresh module <module> <locale>
+ *
+ * EXAMPLES:
+ *
+ * icingacli translation refresh module demo de_DE
+ * icingacli translation refresh module demo fr_FR
+ */
+ public function moduleAction()
+ {
+ $module = $this->validateModuleName($this->params->shift());
+ $locale = $this->validateLocaleCode($this->params->shift());
+
+ $helper = $this->getTranslationHelper($locale);
+ $helper->updateModuleTranslations($module);
+ }
+}
diff --git a/modules/translation/application/clicommands/TestCommand.php b/modules/translation/application/clicommands/TestCommand.php
new file mode 100644
index 0000000..347c2c9
--- /dev/null
+++ b/modules/translation/application/clicommands/TestCommand.php
@@ -0,0 +1,140 @@
+<?php
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Translation\Clicommands;
+
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Translation\Cli\ArrayToTextTableHelper;
+use Icinga\Module\Translation\Cli\TranslationCommand;
+use ipl\I18n\GettextTranslator;
+use ipl\I18n\StaticTranslator;
+
+/**
+ * Timestamp test helper
+ *
+ *
+ */
+class TestCommand extends TranslationCommand
+{
+ protected $locales = array();
+
+ /**
+ * Get translation examples for DateFormatter
+ *
+ * To help you check if the values got translated correctly
+ *
+ * USAGE:
+ *
+ * icingacli translation test dateformatter <locale>
+ *
+ * EXAMPLES:
+ *
+ * icingacli translation test dateformatter de_DE
+ * icingacli translation test dateformatter fr_FR
+ */
+ public function dateformatterAction()
+ {
+ $time = time();
+
+ /** @uses DateFormatter::timeAgo */
+ $this->printTable($this->getMultiTranslated(
+ 'Time Ago',
+ array('Icinga\Date\DateFormatter', 'timeAgo'),
+ array(
+ "15 sec" => $time - 15,
+ "62 sec" => $time - 62,
+ "10 min" => $time - 600,
+ "1h" => $time - 1 * 3600,
+ "3h" => $time - 3 * 3600,
+ "25h" => $time - 25 * 3600,
+ "31d" => $time - 31 * 24 * 3600,
+ )
+ ));
+
+ $this->printTable($this->getMultiTranslated(
+ 'Time Since',
+ array('Icinga\Date\DateFormatter', 'timeSince'),
+ array(
+ "15 sec" => $time - 15,
+ "62 sec" => $time - 62,
+ "10 min" => $time - 600,
+ "1h" => $time - 1 * 3600,
+ "3h" => $time - 3 * 3600,
+ "25h" => $time - 25 * 3600,
+ "31d" => $time - 31 * 24 * 3600,
+ )
+ ));
+
+ $this->printTable($this->getMultiTranslated(
+ 'Time Until',
+ array('Icinga\Date\DateFormatter', 'timeUntil'),
+ array(
+ "15 sec" => $time + 15,
+ "62 sec" => $time + 62,
+ "10 min" => $time + 600,
+ "1h" => $time + 1 * 3600,
+ "3h" => $time + 3 * 3600,
+ "25h" => $time + 25 * 3600,
+ "31d" => $time + 31 * 24 * 3600,
+ )
+ ));
+ }
+
+ public function defaultAction()
+ {
+ $this->dateformatterAction();
+ }
+
+ public function init()
+ {
+ foreach ($this->params->getAllStandalone() as $l) {
+ $this->locales[] = $l;
+ }
+
+ if (empty($this->locales)) {
+ /** @var GettextTranslator $translator */
+ $translator = StaticTranslator::$instance;
+ $this->locales = $translator->listLocales();
+ }
+ }
+
+ protected function callTranslated($callback, $arguments, $locale = 'en_US')
+ {
+ /** @var GettextTranslator $translator */
+ $translator = StaticTranslator::$instance;
+ $translator->setLocale($locale);
+ return call_user_func_array($callback, $arguments);
+ }
+
+ protected function getMultiTranslated($name, $callback, $arguments, $locales = null)
+ {
+ if ($locales === null) {
+ $locales = $this->locales;
+ }
+ array_unshift($locales, 'C');
+
+ $rows = array();
+
+ foreach ($arguments as $k => $args) {
+ $row = array($name => $k);
+
+ if (! is_array($args)) {
+ $args = array($args);
+ }
+ foreach ($locales as $locale) {
+ $row[$locale] = $this->callTranslated($callback, $args, $locale);
+ }
+ $rows[] = $row;
+ }
+
+ return $rows;
+ }
+
+ protected function printTable($rows)
+ {
+ $tt = new ArrayToTextTableHelper($rows);
+ $tt->showHeaders(true);
+ $tt->render();
+ echo "\n\n";
+ }
+}
diff --git a/modules/translation/doc/01-About.md b/modules/translation/doc/01-About.md
new file mode 100644
index 0000000..2eaacfa
--- /dev/null
+++ b/modules/translation/doc/01-About.md
@@ -0,0 +1,6 @@
+# About the Translation Module <a id="translation-module-about"></a>
+
+Please read the following chapters for more insights on this module:
+
+* [Installation](02-Installation.md#translation-module-installation)
+* [Translations](03-Translation.md#module-translation-introduction)
diff --git a/modules/translation/doc/02-Installation.md b/modules/translation/doc/02-Installation.md
new file mode 100644
index 0000000..04f85c8
--- /dev/null
+++ b/modules/translation/doc/02-Installation.md
@@ -0,0 +1,15 @@
+# Translation Module Installation <a id="translation-module-installation"></a>
+
+This module is provided with the Icinga Web 2 package and does
+not need any extra installation step.
+
+## Enable the Module <a id="translation-module-enable"></a>
+
+Navigate to `Configuration` -> `Modules` -> `translation` and enable
+the module.
+
+You can also enable the module during the setup wizard, or on the CLI:
+
+```
+icingacli module enable translation
+```
diff --git a/modules/translation/doc/03-Translation.md b/modules/translation/doc/03-Translation.md
new file mode 100644
index 0000000..14e2e88
--- /dev/null
+++ b/modules/translation/doc/03-Translation.md
@@ -0,0 +1,204 @@
+# Introduction <a id="module-translation-introduction"></a>
+
+Icinga Web 2 provides localization out of the box - for itself and the core modules.
+This module is for third party module developers to aid them to localize their work.
+
+The chapters [Translation for Developers](03-Translation.md#module-translation-developers),
+[Translation for Translators](03-Translation.md#module-translation-translators) and
+[Testing Translations](03-Translation.md#module-translation-tests) will introduce and
+explain you, how to take part on localizing modules to different languages.
+
+## Translation for Developers <a id="module-translation-developers"></a>
+
+To make use of the built-in translations in your module's code or views, you should use the method
+`$this->translate('String to be translated')`, let's have a look at an example:
+
+```php
+<?php
+
+class ExampleController extends Controller
+{
+ public function indexAction()
+ {
+ $this->view->title = $this->translate('Hello World');
+ }
+}
+```
+
+So if there a translation available for the `Hello World` string you will get an translated output, depends on the
+language which is set in your configuration as the default language, if it is `de_DE` the output would be
+`Hallo Welt`.
+
+The same works also for views:
+
+```
+<h1><?= $this->title ?></h1>
+<p>
+ <?= $this->translate('Hello World') ?>
+ <?= $this->translate('String to be translated') ?>
+</p>
+```
+
+If you need to provide placeholders in your messages, you should wrap the `$this->translate()` with `sprintf()` for e.g.
+ sprintf($this->translate('Hello User: (%s)'), $user->getName())
+
+### Translating plural forms <a id="module-translation-plural-forms"></a>
+
+To provide a plural translation, just use the `translatePlural()` function.
+
+```php
+<?php
+
+class ExampleController extends Controller
+{
+ public function indexAction()
+ {
+ $this->view->message = $this->translatePlural('Service', 'Services', 3);
+ }
+}
+```
+
+### Context based translation <a id="module-translation-context-based"></a>
+
+If you want to provide context based translations, you can easily do it with an extra parameter in both methods
+`translate()` and `translatePlural()`.
+
+```php
+<?php
+
+class ExampleController extends Controller
+{
+ public function indexAction()
+ {
+ $this->view->title = $this->translate('My Title', 'mycontext');
+ $this->view->message = $this->translatePlural('Service', 'Services', 3, 'mycontext');
+ }
+}
+```
+
+## Translation for Translators <a id="module-translation-translators"></a>
+
+> **Note**:
+>
+> If you want to translate Icinga Web 2 or any module made by Icinga, please head over to
+> [translate.icinga.com](https://translate.icinga.com) instead. We won't accept any contributions
+> in this regard other than those made there.
+
+Icinga Web 2 internally uses the UNIX standard gettext tool to perform internationalization, this means translation
+files in the .po file format are supplied for text strings used in the code.
+
+There are a lot of tools and techniques to work with .po localization files, you can choose what ever you prefer. We
+won't let you alone on your first steps and therefore we'll introduce you a nice tool, called Poedit.
+
+### Poedit <a id="module-translation-translators-poedit"></a>
+
+First of all, you have to download and install [Poedit](http://poedit.net).
+When you are done, you have to configure Poedit.
+
+#### Configuration <a id="module-translation-translators-poedit-configuration"></a>
+
+`Personalize`: Please provide your Name and E-Mail under Identity.
+
+![Personalize](img/poedit_001.png)
+
+`Editor`: Under the `Behavior` the Automatically compile .mo files on save, should be disabled.
+
+![Editor](img/poedit_002.png)
+
+`Translations Memory`: Under the `Database` please add your languages, for which are you writing translations.
+
+![Translations Memory](img/poedit_003.png)
+
+When you are done, just save your new settings.
+
+#### Editing .po files <a id="module-translation-translators-poedit-edit-po-files"></a>
+
+> **Note**
+>
+> ll_CC stands for ll=language and CC=country code for e.g `de_DE`, `fr_FR`, `ru_RU`, `it_IT` etc.
+
+To work with .po files, open or create the one for your language located under
+`application/locale/ll_CC/LC_MESSAGES/yourmodule.po`. As shown below, you will
+get then a full list of all available translation strings for the module. Each
+module names its translation files `%module_name%.po`.
+
+![Full list of strings](img/poedit_004.png)
+
+Now you can make changes and when there is no translation available, Poedit would mark it with a blue color, as shown
+below.
+
+![Untranslated strings](img/poedit_005.png)
+
+And when you want to test your changes, please read more about under the chapter
+[Testing Translations](Testing Translations).
+
+## Testing Translations <a id="module-translation-tests"></a>
+
+If you want to try out your translation changes in Icinga Web 2, you can make use of the CLI translations commands.
+
+> **Note**:
+>
+> Please make sure that the gettext package is installed
+
+To get an easier development with translations, you can activate the `translation module` which provides CLI commands,
+after that you would be able to refresh and compile your .po files.
+
+Let's assume, we want to provide German translations for our just new created module `yourmodule`.
+
+If we haven't yet any translations strings in our .po file or even the .po file, we can use the CLI command, to do the
+job for us:
+
+```
+icingacli translation refresh module yourmodule de_DE
+```
+
+This will go through all .php and .phtml files inside the module and a look after `$this->translate()` if there is
+something to translate - if there is something and is not available in the `yourmodule.po` it will update this file
+for us with new strings.
+
+Now you can open the `application/locale/de_DE/LC_MESSAGES/yourmodule.po` and you will see something similar:
+
+```
+# Icinga Web 2 - Head for multiple monitoring backends.
+# Copyright (C) 2014 Icinga Development Team
+# This file is distributed under the same license as Development Module.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Development Module (0.0.1)\n"
+"Report-Msgid-Bugs-To: dev@icinga.com\n"
+"POT-Creation-Date: 2014-09-09 10:12+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: ll_CC\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: /modules/yourmodule/configuration.php:6
+msgid "yourmodule"
+msgstr ""
+```
+
+Great, now you can adjust the file and provide the German `msgstr` for `yourmodule`.
+
+```
+#: /modules/yourmodule/configuration.php:6
+msgid "Dummy"
+msgstr "Attrappe"
+```
+
+The last step is to compile the `yourmodule.po` to the `yourmodule.mo`:
+
+```
+icingacli translation compile module yourmodule de_DE
+```
+
+> **Note**
+>
+> After compiling it you need to restart the web server to get new translations available in your module.
+
+At this moment, everywhere in the module where the `Dummy` should be translated, it would return the translated
+string `Attrappe`.
diff --git a/modules/translation/doc/img/poedit_001.png b/modules/translation/doc/img/poedit_001.png
new file mode 100644
index 0000000..2d07b8e
--- /dev/null
+++ b/modules/translation/doc/img/poedit_001.png
Binary files differ
diff --git a/modules/translation/doc/img/poedit_002.png b/modules/translation/doc/img/poedit_002.png
new file mode 100644
index 0000000..d31e5ba
--- /dev/null
+++ b/modules/translation/doc/img/poedit_002.png
Binary files differ
diff --git a/modules/translation/doc/img/poedit_003.png b/modules/translation/doc/img/poedit_003.png
new file mode 100644
index 0000000..5f285f9
--- /dev/null
+++ b/modules/translation/doc/img/poedit_003.png
Binary files differ
diff --git a/modules/translation/doc/img/poedit_004.png b/modules/translation/doc/img/poedit_004.png
new file mode 100644
index 0000000..2c85dd9
--- /dev/null
+++ b/modules/translation/doc/img/poedit_004.png
Binary files differ
diff --git a/modules/translation/doc/img/poedit_005.png b/modules/translation/doc/img/poedit_005.png
new file mode 100644
index 0000000..3ae59ba
--- /dev/null
+++ b/modules/translation/doc/img/poedit_005.png
Binary files differ
diff --git a/modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php b/modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
new file mode 100644
index 0000000..46d4e81
--- /dev/null
+++ b/modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
@@ -0,0 +1,229 @@
+<?php
+
+namespace Icinga\Module\Translation\Cli;
+
+/**
+ * Array to Text Table Generation Class
+ *
+ * @author Tony Landis <tony@tonylandis.com>
+ * @link http://www.tonylandis.com/
+ * @copyright Copyright (C) 2006-2009 Tony Landis
+ * @license http://www.opensource.org/licenses/bsd-license.php
+ */
+class ArrayToTextTableHelper
+{
+ /**
+ * @var array The array for processing
+ */
+ protected $rows;
+
+ /**
+ * @var int The column width settings
+ */
+ protected $cs = array();
+
+ /**
+ * @var int The Row lines settings
+ */
+ protected $rs = array();
+
+ /**
+ * @var int The Column index of keys
+ */
+ protected $keys = array();
+
+ /**
+ * @var int Max Column Height (returns)
+ */
+ protected $mH = 2;
+
+ /**
+ * @var int Max Row Width (chars)
+ */
+ protected $mW = 30;
+
+ protected $head = false;
+ protected $pcen = "+";
+ protected $prow = "-";
+ protected $pcol = "|";
+
+
+ /**
+ * Prepare array into textual format
+ *
+ * @param $rows
+ */
+ public function __construct($rows)
+ {
+ $this->rows =& $rows;
+ $this->cs = array();
+ $this->rs = array();
+
+ if (! $xc = count($this->rows)) {
+ return false;
+ }
+
+ $this->keys = array_keys($this->rows[0]);
+ $columns = count($this->keys);
+
+ for ($x = 0; $x < $xc; $x++) {
+ for ($y = 0; $y < $columns; $y++) {
+ $this->setMax($x, $y, $this->rows[$x][$this->keys[$y]]);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Show the headers using the key values of the array for the titles
+ *
+ * @param bool $bool
+ */
+ public function showHeaders($bool)
+ {
+ if ($bool) {
+ $this->setHeading();
+ }
+ }
+
+ /**
+ * Set the maximum width (number of characters) per column before truncating
+ *
+ * @param int $maxWidth
+ */
+ public function setMaxWidth($maxWidth)
+ {
+ $this->mW = (int) $maxWidth;
+ }
+
+ /**
+ * Set the maximum height (number of lines) per row before truncating
+ *
+ * @param int $maxHeight
+ */
+ public function setMaxHeight($maxHeight)
+ {
+ $this->mH = (int) $maxHeight;
+ }
+
+ /**
+ * Prints the data to a text table
+ *
+ * @param bool $return Set to 'true' to return text rather than printing
+ *
+ * @return mixed
+ */
+ public function render($return = false)
+ {
+ if ($return) {
+ ob_start(null, 0, true);
+ }
+
+ $this->printLine();
+ $this->printHeading();
+
+ $rc = count($this->rows);
+ for ($i = 0; $i < $rc; $i++) {
+ $this->printRow($i);
+ }
+
+ $this->printLine(false);
+
+ if ($return) {
+ $contents = ob_get_contents();
+ ob_end_clean();
+ return $contents;
+ }
+ return null;
+ }
+
+ protected function setHeading()
+ {
+ $data = array();
+ foreach ($this->keys as $colKey => $value) {
+ $this->setMax(false, $colKey, $value);
+ $data[$colKey] = strtoupper($value);
+ }
+ if (! is_array($data)) {
+ return false;
+ }
+ $this->head = $data;
+
+ return $this;
+ }
+
+ protected function printLine($nl = true)
+ {
+ print $this->pcen;
+ foreach ($this->cs as $key => $val) {
+ print $this->prow .
+ str_pad('', $val, $this->prow, STR_PAD_RIGHT) .
+ $this->prow .
+ $this->pcen;
+ }
+ if ($nl) {
+ print "\n";
+ }
+ }
+
+ protected function printHeading()
+ {
+ if (! is_array($this->head)) {
+ return false;
+ }
+
+ print $this->pcol;
+ foreach ($this->cs as $key => $val) {
+ print ' ' .
+ str_pad($this->head[$key], $val, ' ', STR_PAD_BOTH) .
+ ' ' .
+ $this->pcol;
+ }
+
+ print "\n";
+ $this->printLine();
+
+ return $this;
+ }
+
+ protected function printRow($rowKey)
+ {
+ // loop through each line
+ for ($line = 1; $line <= $this->rs[$rowKey]; $line++) {
+ print $this->pcol;
+ for ($colKey = 0; $colKey < count($this->keys); $colKey++) {
+ print " ";
+ print str_pad(
+ substr($this->rows[$rowKey][$this->keys[$colKey]], ($this->mW * ($line - 1)), $this->mW),
+ $this->cs[$colKey],
+ ' ',
+ STR_PAD_RIGHT
+ );
+ print " " . $this->pcol;
+ }
+ print "\n";
+ }
+ }
+
+ protected function setMax($rowKey, $colKey, &$colVal)
+ {
+ $w = mb_strlen($colVal);
+ $h = 1;
+ if ($w > $this->mW) {
+ $h = ceil($w % $this->mW);
+ if ($h > $this->mH) {
+ $h = $this->mH;
+ }
+ $w = $this->mW;
+ }
+
+ if (! isset($this->cs[$colKey]) || $this->cs[$colKey] < $w) {
+ $this->cs[$colKey] = $w;
+ }
+
+ if ($rowKey !== false && (! isset($this->rs[$rowKey]) || $this->rs[$rowKey] < $h)) {
+ $this->rs[$rowKey] = $h;
+ }
+ }
+}
diff --git a/modules/translation/library/Translation/Cli/TranslationCommand.php b/modules/translation/library/Translation/Cli/TranslationCommand.php
new file mode 100644
index 0000000..af3582c
--- /dev/null
+++ b/modules/translation/library/Translation/Cli/TranslationCommand.php
@@ -0,0 +1,73 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Translation\Cli;
+
+use Exception;
+use Icinga\Cli\Command;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Translation\Util\GettextTranslationHelper;
+
+/**
+ * Base class for translation commands
+ */
+class TranslationCommand extends Command
+{
+ /**
+ * Get the gettext translation helper
+ *
+ * @param string $locale
+ *
+ * @return GettextTranslationHelper
+ */
+ public function getTranslationHelper($locale)
+ {
+ $helper = new GettextTranslationHelper($this->app, $locale);
+ $helper->setConfig($this->Config());
+ return $helper;
+ }
+
+ /**
+ * Check whether the given locale code is valid
+ *
+ * @param string $code The locale code to validate
+ *
+ * @return string The validated locale code
+ *
+ * @throws Exception In case the locale code is invalid
+ */
+ public function validateLocaleCode($code)
+ {
+ if (! preg_match('@[a-z]{2}_[A-Z]{2}@', $code)) {
+ throw new IcingaException(
+ 'Locale code \'%s\' is not valid. Expected format is: ll_CC',
+ $code
+ );
+ }
+
+ return $code;
+ }
+
+ /**
+ * Check whether the given module is available and enabled
+ *
+ * @param string $name The module name to validate
+ *
+ * @return string The validated module name
+ *
+ * @throws Exception In case the given module is not available or not enabled
+ */
+ public function validateModuleName($name)
+ {
+ $enabledModules = $this->app->getModuleManager()->listEnabledModules();
+
+ if (! in_array($name, $enabledModules)) {
+ throw new IcingaException(
+ 'Module with name \'%s\' not found or is not enabled',
+ $name
+ );
+ }
+
+ return $name;
+ }
+}
diff --git a/modules/translation/library/Translation/Util/GettextTranslationHelper.php b/modules/translation/library/Translation/Util/GettextTranslationHelper.php
new file mode 100644
index 0000000..043b2b7
--- /dev/null
+++ b/modules/translation/library/Translation/Util/GettextTranslationHelper.php
@@ -0,0 +1,441 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Translation\Util;
+
+use Exception;
+use Icinga\Application\ApplicationBootstrap;
+use Icinga\Application\Config;
+use Icinga\Application\Modules\Manager;
+use Icinga\Exception\IcingaException;
+use Icinga\Util\File;
+
+/**
+ * This class provides some useful utility functions to handle gettext translations
+ */
+class GettextTranslationHelper
+{
+ /**
+ * All project files are supposed to have the same/this encoding
+ */
+ const FILE_ENCODING = 'UTF-8';
+
+ /**
+ * Config
+ *
+ * @var Config
+ */
+ protected $config;
+
+ /**
+ * The source files to parse
+ *
+ * @var array
+ */
+ private $sourceExtensions = array(
+ 'php',
+ 'phtml'
+ );
+
+ /**
+ * The module manager of the application's bootstrap
+ *
+ * @var Manager
+ */
+ private $moduleMgr;
+
+ /**
+ * The current version of Icingaweb 2 or of the module the catalog is being created for
+ *
+ * @var string
+ */
+ private $version;
+
+ /**
+ * The name of the module if any
+ *
+ * @var string
+ */
+ private $moduleName;
+
+ /**
+ * The locale used by this helper
+ *
+ * @var string
+ */
+ private $locale;
+
+ /**
+ * The path to the module, if any
+ *
+ * @var string
+ */
+ private $moduleDir;
+
+ /**
+ * The path to the file catalog
+ *
+ * @var string
+ */
+ private $catalogPath;
+
+ /**
+ * The path to the *.pot file
+ *
+ * @var string
+ */
+ private $templatePath;
+
+ /**
+ * The path to the *.po file
+ *
+ * @var string
+ */
+ private $tablePath;
+
+ /**
+ * Create a new TranslationHelper object
+ *
+ * @param ApplicationBootstrap $bootstrap The application's bootstrap object
+ * @param string $locale The locale to be used by this helper
+ */
+ public function __construct(ApplicationBootstrap $bootstrap, $locale)
+ {
+ $this->moduleMgr = $bootstrap->getModuleManager();
+ $this->locale = $locale;
+ }
+
+ /**
+ * Cleanup temporary files
+ */
+ public function __destruct()
+ {
+ if ($this->catalogPath !== null && file_exists($this->catalogPath)) {
+ unlink($this->catalogPath);
+ }
+
+ if ($this->templatePath !== null && file_exists($this->templatePath)) {
+ unlink($this->templatePath);
+ }
+ }
+
+ /**
+ * Get the config
+ *
+ * @return Config
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Set the config
+ *
+ * @param Config $config
+ *
+ * @return $this
+ */
+ public function setConfig(Config $config)
+ {
+ $this->config = $config;
+ return $this;
+ }
+
+ /**
+ * Update the translation table for a particular module
+ *
+ * @param string $module The name of the module for which to update the translation table
+ */
+ public function updateModuleTranslations($module)
+ {
+ $this->catalogPath = tempnam(sys_get_temp_dir(), 'IcingaTranslation_');
+ $this->templatePath = tempnam(sys_get_temp_dir(), 'IcingaPot_');
+ $this->version = $this->moduleMgr->getModule($module)->getVersion();
+ $this->moduleName = $this->moduleMgr->getModule($module)->getName();
+
+ $this->moduleDir = $this->moduleMgr->getModuleDir($module);
+ $this->tablePath = implode(
+ DIRECTORY_SEPARATOR,
+ array(
+ $this->moduleDir,
+ 'application',
+ 'locale',
+ $this->locale,
+ 'LC_MESSAGES',
+ $module . '.po'
+ )
+ );
+
+ $this->createFileCatalog();
+ $this->createTemplateFile();
+ $this->updateTranslationTable();
+ }
+
+ /**
+ * Compile the translation table for a particular module
+ *
+ * @param string $module The name of the module for which to compile the translation table
+ */
+ public function compileModuleTranslation($module)
+ {
+ $this->moduleDir = $this->moduleMgr->getModuleDir($module);
+ $this->tablePath = implode(
+ DIRECTORY_SEPARATOR,
+ array(
+ $this->moduleDir,
+ 'application',
+ 'locale',
+ $this->locale,
+ 'LC_MESSAGES',
+ $module . '.po'
+ )
+ );
+
+ $this->compileTranslationTable();
+ }
+
+ /**
+ * Update any existing or create a new translation table using the gettext tools
+ *
+ * @throws Exception In case the translation table does not yet exist and cannot be created
+ */
+ private function updateTranslationTable()
+ {
+ if (is_file($this->tablePath)) {
+ shell_exec(sprintf(
+ '%s --update --backup=none %s %s 2>&1',
+ $this->getConfig()->get('translation', 'msgmerge', '/usr/bin/env msgmerge'),
+ $this->tablePath,
+ $this->templatePath
+ ));
+ } else {
+ if ((!is_dir(dirname($this->tablePath)) && !@mkdir(dirname($this->tablePath), 0755, true)) ||
+ !rename($this->templatePath, $this->tablePath)) {
+ throw new IcingaException(
+ 'Unable to create %s',
+ $this->tablePath
+ );
+ }
+ }
+ $this->updateHeader($this->tablePath);
+ $this->fixSourceLocations($this->tablePath);
+ }
+
+ /**
+ * Create the template file using the gettext tools
+ */
+ private function createTemplateFile()
+ {
+ shell_exec(
+ implode(
+ ' ',
+ array(
+ $this->getConfig()->get('translation', 'xgettext', '/usr/bin/env xgettext'),
+ '--language=PHP',
+ '--keyword=translate',
+ '--keyword=translate:1,2c',
+ '--keyword=translateInDomain:2',
+ '--keyword=translateInDomain:2,3c',
+ '--keyword=translatePlural:1,2',
+ '--keyword=translatePlural:1,2,4c',
+ '--keyword=translatePluralInDomain:2,3',
+ '--keyword=translatePluralInDomain:2,3,5c',
+ '--keyword=mt:2',
+ '--keyword=mt:2,3c',
+ '--keyword=mtp:2,3',
+ '--keyword=mtp:2,3,5c',
+ '--keyword=t',
+ '--keyword=t:1,2c',
+ '--keyword=tp:1,2',
+ '--keyword=tp:1,2,4c',
+ '--keyword=N_',
+ '--sort-output',
+ '--force-po',
+ '--omit-header',
+ '--from-code=' . self::FILE_ENCODING,
+ '--files-from="' . $this->catalogPath . '"',
+ '--output="' . $this->templatePath . '"'
+ )
+ )
+ );
+ }
+
+ /**
+ * Create or update a gettext conformant header in the given file
+ *
+ * @param string $path The path to the file
+ */
+ private function updateHeader($path)
+ {
+ $headerInfo = array(
+ 'title' => $this->moduleMgr->getModule($this->moduleName)->getTitle(),
+ 'copyright_holder' => 'TEAM NAME',
+ 'copyright_year' => date('Y'),
+ 'author_name' => 'FIRST AUTHOR',
+ 'author_mail' => 'EMAIL@ADDRESS',
+ 'author_year' => 'YEAR',
+ 'project_name' => ucfirst($this->moduleName) . ' Module',
+ 'project_version' => $this->version,
+ 'project_bug_mail' => 'ISSUE TRACKER',
+ 'pot_creation_date' => date('Y-m-d H:iO'),
+ 'po_revision_date' => 'YEAR-MO-DA HO:MI+ZONE',
+ 'translator_name' => 'FULL NAME',
+ 'translator_mail' => 'EMAIL@ADDRESS',
+ 'language' => $this->locale,
+ 'language_team_name' => 'LANGUAGE',
+ 'language_team_url' => 'LL@li.org',
+ 'charset' => self::FILE_ENCODING
+ );
+
+ $content = file_get_contents($path);
+ if (strpos($content, '# ') === 0) {
+ $authorInfo = array();
+ if (preg_match('@# (.+) <(.+)>, (\d+|YEAR)\.@', $content, $authorInfo)) {
+ $headerInfo['author_name'] = $authorInfo[1];
+ $headerInfo['author_mail'] = $authorInfo[2];
+ $headerInfo['author_year'] = $authorInfo[3];
+ }
+ $revisionInfo = array();
+ if (preg_match('@Revision-Date: (\d{4}-\d{2}-\d{2} \d{2}:\d{2}\+\d{4})@', $content, $revisionInfo)) {
+ $headerInfo['po_revision_date'] = $revisionInfo[1];
+ }
+ $translatorInfo = array();
+ if (preg_match('@Last-Translator: (.+) <(.+)>@', $content, $translatorInfo)) {
+ $headerInfo['translator_name'] = $translatorInfo[1];
+ $headerInfo['translator_mail'] = $translatorInfo[2];
+ }
+ $languageTeamInfo = array();
+ if (preg_match('@Language-Team: (.+) <(.+)>@', $content, $languageTeamInfo)) {
+ $headerInfo['language_team_name'] = $languageTeamInfo[1];
+ $headerInfo['language_team_url'] = $languageTeamInfo[2];
+ }
+ $languageInfo = array();
+ if (preg_match('@Language: ([a-z]{2}_[A-Z]{2})@', $content, $languageInfo)) {
+ $headerInfo['language'] = $languageInfo[1];
+ }
+ }
+
+ file_put_contents(
+ $path,
+ implode(
+ PHP_EOL,
+ array(
+ '# ' . $headerInfo['title'] . '.',
+ '# Copyright (C) ' . $headerInfo['copyright_year'] . ' ' . $headerInfo['copyright_holder'],
+ '# This file is distributed under the same license as ' . $headerInfo['project_name'] . '.',
+ '# ' . $headerInfo['author_name'] . ' <' . $headerInfo['author_mail']
+ . '>, ' . $headerInfo['author_year'] . '.',
+ '# ',
+ '#, fuzzy',
+ 'msgid ""',
+ 'msgstr ""',
+ '"Project-Id-Version: ' . $headerInfo['project_name'] . ' ('
+ . $headerInfo['project_version'] . ')\n"',
+ '"Report-Msgid-Bugs-To: ' . $headerInfo['project_bug_mail'] . '\n"',
+ '"POT-Creation-Date: ' . $headerInfo['pot_creation_date'] . '\n"',
+ '"PO-Revision-Date: ' . $headerInfo['po_revision_date'] . '\n"',
+ '"Last-Translator: ' . $headerInfo['translator_name'] . ' <'
+ . $headerInfo['translator_mail'] . '>\n"',
+ '"Language: ' . $headerInfo['language'] . '\n"',
+ '"Language-Team: ' . $headerInfo['language_team_name'] . ' <'
+ . $headerInfo['language_team_url'] . '>\n"',
+ '"MIME-Version: 1.0\n"',
+ '"Content-Type: text/plain; charset=' . $headerInfo['charset'] . '\n"',
+ '"Content-Transfer-Encoding: 8bit\n"',
+ '"Plural-Forms: nplurals=2; plural=(n != 1);\n"',
+ '"X-Poedit-Basepath: .\n"',
+ '"X-Poedit-SearchPath-0: .\n"',
+ ''
+ )
+ ) . PHP_EOL . substr($content, strpos($content, '#: '))
+ );
+ }
+
+ /**
+ * Adjust all absolute source file paths so that they're all relative to the catalog's location
+ *
+ * @param string $path
+ */
+ protected function fixSourceLocations($path)
+ {
+ shell_exec(sprintf(
+ "sed -i 's;%s;../../../..;g' %s",
+ $this->moduleDir,
+ $path
+ ));
+ }
+
+ /**
+ * Create the file catalog
+ *
+ * @throws Exception In case the catalog-file cannot be created
+ */
+ private function createFileCatalog()
+ {
+ $catalog = new File($this->catalogPath, 'w');
+
+ try {
+ $this->getSourceFileNames($this->moduleDir, $catalog);
+ } catch (Exception $error) {
+ throw $error;
+ }
+
+ $catalog->fflush();
+ }
+
+ /**
+ * Recursively scan the given directory for translatable source files
+ *
+ * @param string $directory The directory where to search for sources
+ * @param File $file The file where to write the results
+ *
+ * @throws Exception In case the given directory is not readable
+ */
+ private function getSourceFileNames($directory, File $file)
+ {
+ $directoryHandle = opendir($directory);
+ if (!$directoryHandle) {
+ throw new IcingaException(
+ 'Unable to read files from %s',
+ $directory
+ );
+ }
+
+ $subdirs = array();
+ while (($filename = readdir($directoryHandle)) !== false) {
+ if ($filename[0] === '.' || $filename === 'vendor') {
+ continue;
+ }
+ $filepath = $directory . DIRECTORY_SEPARATOR . $filename;
+ if (preg_match('@^[^\.].+\.(' . implode('|', $this->sourceExtensions) . ')$@', $filename)) {
+ $file->fwrite($filepath . PHP_EOL);
+ } elseif (! is_link($filepath) && is_dir($filepath)) {
+ $subdirs[] = $filepath;
+ }
+ }
+ closedir($directoryHandle);
+
+ foreach ($subdirs as $subdir) {
+ $this->getSourceFileNames($subdir, $file);
+ }
+ }
+
+ /**
+ * Compile the translation table
+ */
+ private function compileTranslationTable()
+ {
+ $targetPath = substr($this->tablePath, 0, strrpos($this->tablePath, '.')) . '.mo';
+ shell_exec(
+ implode(
+ ' ',
+ array(
+ $this->getConfig()->get('translation', 'msgfmt', '/usr/bin/env msgfmt'),
+ '-o ' . $targetPath,
+ $this->tablePath
+ )
+ )
+ );
+ }
+}
diff --git a/modules/translation/module.info b/modules/translation/module.info
new file mode 100644
index 0000000..e412916
--- /dev/null
+++ b/modules/translation/module.info
@@ -0,0 +1,7 @@
+Module: translation
+Version: 2.12.1
+Description: Translation module
+ This module allows developers and translators to translate modules for multiple
+ languages. You do not need this module to run an internationalized web frontend.
+ This is only for people who want to contribute translations or translate just
+ their own modules.
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 0000000..22a196d
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,26246 @@
+parameters:
+ ignoreErrors:
+ -
+ message: "#^Cannot cast mixed to int\\.$#"
+ count: 1
+ path: application/clicommands/AutocompleteCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\AutocompleteCommand\\:\\:completeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/AutocompleteCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\AutocompleteCommand\\:\\:suggest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/AutocompleteCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\AutocompleteCommand\\:\\:suggest\\(\\) has parameter \\$suggestions with no type specified\\.$#"
+ count: 1
+ path: application/clicommands/AutocompleteCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Clicommands\\\\AutocompleteCommand\\:\\:\\$defaultActionName has no type specified\\.$#"
+ count: 1
+ path: application/clicommands/AutocompleteCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\HelpCommand\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/HelpCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Clicommands\\\\HelpCommand\\:\\:\\$defaultActionName has no type specified\\.$#"
+ count: 1
+ path: application/clicommands/HelpCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\ModuleCommand\\:\\:disableAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ModuleCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\ModuleCommand\\:\\:enableAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ModuleCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\ModuleCommand\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ModuleCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\ModuleCommand\\:\\:installAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ModuleCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\ModuleCommand\\:\\:listAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ModuleCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\ModuleCommand\\:\\:permissionsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ModuleCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\ModuleCommand\\:\\:purgeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ModuleCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\ModuleCommand\\:\\:removeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ModuleCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\ModuleCommand\\:\\:restrictionsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ModuleCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\ModuleCommand\\:\\:searchAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ModuleCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:disableModule\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/clicommands/ModuleCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:enableModule\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/clicommands/ModuleCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:hasEnabled\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/clicommands/ModuleCommand.php
+
+ -
+ message: "#^Part \\$type \\(mixed\\) of encapsed string cannot be cast to string\\.$#"
+ count: 1
+ path: application/clicommands/ModuleCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\VersionCommand\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/VersionCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Clicommands\\\\VersionCommand\\:\\:\\$defaultActionName has no type specified\\.$#"
+ count: 1
+ path: application/clicommands/VersionCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\WebCommand\\:\\:forkAndExit\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/WebCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\WebCommand\\:\\:serveAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/WebCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Clicommands\\\\WebCommand\\:\\:stopAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/WebCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of function pcntl_exec expects string, string\\|false given\\.$#"
+ count: 1
+ path: application/clicommands/WebCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of function realpath expects string, mixed given\\.$#"
+ count: 1
+ path: application/clicommands/WebCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\AboutController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/AboutController.php
+
+ -
+ message: "#^Cannot call method can\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: application/controllers/AccountController.php
+
+ -
+ message: "#^Cannot call method getAdditional\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 2
+ path: application/controllers/AccountController.php
+
+ -
+ message: "#^Cannot call method getPreferences\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: application/controllers/AccountController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\AccountController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/AccountController.php
+
+ -
+ message: "#^Parameter \\#1 \\$backend of method Icinga\\\\Forms\\\\Account\\\\ChangePasswordForm\\:\\:setBackend\\(\\) expects Icinga\\\\Authentication\\\\User\\\\DbUserBackend, Icinga\\\\Authentication\\\\User\\\\UserBackendInterface given\\.$#"
+ count: 1
+ path: application/controllers/AccountController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of static method Icinga\\\\Authentication\\\\User\\\\UserBackend\\:\\:create\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/AccountController.php
+
+ -
+ message: "#^Parameter \\#2 \\$user of static method Icinga\\\\User\\\\Preferences\\\\PreferencesStore\\:\\:create\\(\\) expects Icinga\\\\User, Icinga\\\\User\\|null given\\.$#"
+ count: 1
+ path: application/controllers/AccountController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\AnnouncementsController\\:\\:acknowledgeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/AnnouncementsController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\AnnouncementsController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/AnnouncementsController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\AnnouncementsController\\:\\:newAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/AnnouncementsController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\AnnouncementsController\\:\\:removeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/AnnouncementsController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\AnnouncementsController\\:\\:updateAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/AnnouncementsController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\RepositoryForm\\:\\:edit\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/AnnouncementsController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\RepositoryForm\\:\\:remove\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/AnnouncementsController.php
+
+ -
+ message: "#^Access to an undefined property Zend_Controller_Action_HelperBroker\\:\\:\\$layout\\.$#"
+ count: 1
+ path: application/controllers/ApplicationStateController.php
+
+ -
+ message: "#^Access to an undefined property Zend_Controller_Action_HelperBroker\\:\\:\\$viewRenderer\\.$#"
+ count: 1
+ path: application/controllers/ApplicationStateController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ApplicationStateController\\:\\:acknowledgeMessageAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ApplicationStateController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ApplicationStateController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ApplicationStateController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ApplicationStateController\\:\\:summaryAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ApplicationStateController.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of function setcookie expects string, int\\<1, max\\> given\\.$#"
+ count: 1
+ path: application/controllers/ApplicationStateController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\View\\:\\:layout\\(\\)\\.$#"
+ count: 1
+ path: application/controllers/AuthenticationController.php
+
+ -
+ message: "#^Cannot call method isExternalUser\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: application/controllers/AuthenticationController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\AuthenticationController\\:\\:loginAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/AuthenticationController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\AuthenticationController\\:\\:logoutAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/AuthenticationController.php
+
+ -
+ message: "#^Parameter \\#1 \\$url of static method Icinga\\\\Web\\\\Url\\:\\:fromPath\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/AuthenticationController.php
+
+ -
+ message: "#^Parameter \\#1 \\$user of static method Icinga\\\\Application\\\\Hook\\\\AuthenticationHook\\:\\:triggerLogin\\(\\) expects Icinga\\\\User, Icinga\\\\User\\|null given\\.$#"
+ count: 1
+ path: application/controllers/AuthenticationController.php
+
+ -
+ message: "#^Parameter \\#1 \\$user of static method Icinga\\\\Application\\\\Hook\\\\AuthenticationHook\\:\\:triggerLogout\\(\\) expects Icinga\\\\User, Icinga\\\\User\\|null given\\.$#"
+ count: 1
+ path: application/controllers/AuthenticationController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\Form\\:\\:setIniConfig\\(\\)\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\Widget\\\\AbstractWidget\\:\\:add\\(\\)\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Cannot access property \\$enabled on mixed\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Cannot access property \\$loaded on mixed\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:createApplicationTabs\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:createresourceAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:createuserbackendAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:devtoolsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:editresourceAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:edituserbackendAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:generalAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:moduleAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:moduledisableAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:moduleenableAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:modulesAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:removeresourceAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:removeuserbackendAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:resourceAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ConfigController\\:\\:userbackendAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:disableModule\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:enableModule\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:getModule\\(\\) expects string, mixed given\\.$#"
+ count: 3
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:getModuleDir\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:hasEnabled\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:hasInstalled\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:hasLoaded\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\Config\\\\UserBackendConfigForm\\:\\:delete\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\Config\\\\UserBackendConfigForm\\:\\:edit\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\Config\\\\UserBackendConfigForm\\:\\:load\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#2 \\$name of class Icinga\\\\Application\\\\Modules\\\\Module constructor expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 9
+ path: application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\DashboardController\\:\\:createTabs\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\DashboardController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\DashboardController\\:\\:newDashletAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\DashboardController\\:\\:removeDashletAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\DashboardController\\:\\:removePaneAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\DashboardController\\:\\:renamePaneAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\DashboardController\\:\\:settingsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\DashboardController\\:\\:updateDashletAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of class Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane constructor expects string, mixed given\\.$#"
+ count: 2
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:activate\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:getPane\\(\\) expects string, mixed given\\.$#"
+ count: 7
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Parameter \\#1 \\$pane of method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:hasPane\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function rawurldecode expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Parameter \\#1 \\$title of class Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet constructor expects string, mixed given\\.$#"
+ count: 2
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Parameter \\#1 \\$title of method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet\\:\\:setTitle\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Parameter \\#1 \\$title of method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:getDashlet\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Parameter \\#1 \\$title of method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:removeDashlet\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Parameter \\#1 \\$title of method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:setTitle\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Parameter \\#1 \\$url of method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet\\:\\:setUrl\\(\\) expects Icinga\\\\Web\\\\Url\\|string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Parameter \\#1 \\$user of method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:setUser\\(\\) expects Icinga\\\\User, Icinga\\\\User\\|null given\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Parameter \\#2 \\$url of class Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet constructor expects Icinga\\\\Web\\\\Url\\|string, mixed given\\.$#"
+ count: 2
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: application/controllers/DashboardController.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Request_Abstract\\:\\:get\\(\\)\\.$#"
+ count: 1
+ path: application/controllers/ErrorController.php
+
+ -
+ message: "#^Cannot access property \\$exception on mixed\\.$#"
+ count: 1
+ path: application/controllers/ErrorController.php
+
+ -
+ message: "#^Cannot access property \\$request on mixed\\.$#"
+ count: 1
+ path: application/controllers/ErrorController.php
+
+ -
+ message: "#^Cannot access property \\$type on mixed\\.$#"
+ count: 1
+ path: application/controllers/ErrorController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ErrorController\\:\\:errorAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ErrorController.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_shift expects array, array\\<int, string\\>\\|false given\\.$#"
+ count: 1
+ path: application/controllers/ErrorController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackendInterface\\:\\:delete\\(\\)\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackendInterface\\:\\:select\\(\\)\\.$#"
+ count: 3
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Authentication\\\\User\\\\DomainAwareInterface\\:\\:getName\\(\\)\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\GroupController\\:\\:addAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\GroupController\\:\\:addmemberAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\GroupController\\:\\:createListTabs\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\GroupController\\:\\:createShowTabs\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\GroupController\\:\\:editAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\GroupController\\:\\:listAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\GroupController\\:\\:removeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\GroupController\\:\\:removememberAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\GroupController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Parameter \\#1 \\$backend of method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\AddMemberForm\\:\\:setBackend\\(\\) expects Icinga\\\\Data\\\\Extensible, Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackendInterface given\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Parameter \\#1 \\$groupName of method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\AddMemberForm\\:\\:setGroupName\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\RepositoryForm\\:\\:edit\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\RepositoryForm\\:\\:remove\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Web\\\\Controller\\\\AuthBackendController\\:\\:getUserGroupBackend\\(\\) expects string\\|null, mixed given\\.$#"
+ count: 7
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Parameter \\#1 \\$repository of method Icinga\\\\Forms\\\\RepositoryForm\\:\\:setRepository\\(\\) expects Icinga\\\\Repository\\\\Repository, Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackendInterface given\\.$#"
+ count: 3
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Parameter \\#2 \\$filter of static method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:where\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Parameter \\#2 \\$groupName of method Icinga\\\\Controllers\\\\GroupController\\:\\:createShowTabs\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 5
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Parameter \\#3 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: application/controllers/GroupController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\HealthController\\:\\:handleFormatRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/HealthController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\HealthController\\:\\:handleFormatRequest\\(\\) has parameter \\$query with no type specified\\.$#"
+ count: 1
+ path: application/controllers/HealthController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\HealthController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/HealthController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\IframeController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/IframeController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\IndexController\\:\\:welcomeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/IndexController.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Action_HelperBroker\\:\\:layout\\(\\)\\.$#"
+ count: 2
+ path: application/controllers/LayoutController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\LayoutController\\:\\:announcementsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/LayoutController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\LayoutController\\:\\:menuAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/LayoutController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ListController\\:\\:addTitleTab\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ListController\\:\\:applicationlogAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ManageUserDevicesController\\:\\:deleteAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ManageUserDevicesController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ManageUserDevicesController\\:\\:devicesAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ManageUserDevicesController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\ManageUserDevicesController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ManageUserDevicesController.php
+
+ -
+ message: "#^Parameter \\#1 \\$iv of method Icinga\\\\Web\\\\RememberMe\\:\\:remove\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/ManageUserDevicesController.php
+
+ -
+ message: "#^Parameter \\#1 \\$username of method Icinga\\\\Web\\\\RememberMeUserDevicesList\\:\\:setUsername\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/ManageUserDevicesController.php
+
+ -
+ message: "#^Cannot call method getUser\\(\\) on Icinga\\\\Authentication\\\\Auth\\|null\\.$#"
+ count: 1
+ path: application/controllers/MyDevicesController.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: application/controllers/MyDevicesController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\MyDevicesController\\:\\:deleteAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/MyDevicesController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\MyDevicesController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/MyDevicesController.php
+
+ -
+ message: "#^Parameter \\#1 \\$iv of method Icinga\\\\Web\\\\RememberMe\\:\\:remove\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/MyDevicesController.php
+
+ -
+ message: "#^Cannot call method can\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Cannot call method getLabel\\(\\) on Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\|null\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 5
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\NavigationController\\:\\:addAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\NavigationController\\:\\:dashboardAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\NavigationController\\:\\:editAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\NavigationController\\:\\:fetchSharedNavigationItemConfigs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\NavigationController\\:\\:fetchUserNavigationItemConfigs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\NavigationController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\NavigationController\\:\\:listItemTypes\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\NavigationController\\:\\:removeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\NavigationController\\:\\:sharedAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\NavigationController\\:\\:unshareAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:delete\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:edit\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:load\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:findItem\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function rawurldecode expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function ucwords expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Parameter \\#1 \\$type of static method Icinga\\\\Application\\\\Config\\:\\:navigation\\(\\) expects string, mixed given\\.$#"
+ count: 6
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Parameter \\#1 \\$user of method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:setUser\\(\\) expects Icinga\\\\User, Icinga\\\\User\\|null given\\.$#"
+ count: 4
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Parameter \\#2 \\$username of static method Icinga\\\\Application\\\\Config\\:\\:navigation\\(\\) expects string\\|null, mixed given\\.$#"
+ count: 2
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 5
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Property Icinga\\\\Controllers\\\\NavigationController\\:\\:\\$itemTypeConfig type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/controllers/NavigationController.php
+
+ -
+ message: "#^Cannot access offset 'label' on mixed\\.$#"
+ count: 1
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\RoleController\\:\\:addAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\RoleController\\:\\:auditAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\RoleController\\:\\:createListTabs\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\RoleController\\:\\:editAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\RoleController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\RoleController\\:\\:listAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\RoleController\\:\\:removeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\RoleController\\:\\:suggestRoleMemberAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$count of method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:limit\\(\\) expects int\\|null, mixed given\\.$#"
+ count: 2
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\RepositoryForm\\:\\:edit\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\RepositoryForm\\:\\:remove\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$username of class Icinga\\\\User constructor expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Parameter \\#2 \\$haystack of function in_array expects array, mixed given\\.$#"
+ count: 1
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 2
+ path: application/controllers/RoleController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\SearchController\\:\\:hintAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/SearchController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\SearchController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/SearchController.php
+
+ -
+ message: "#^Parameter \\#1 \\$searchString of method Icinga\\\\Web\\\\Widget\\\\SearchDashboard\\:\\:search\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/SearchController.php
+
+ -
+ message: "#^Parameter \\#1 \\$user of method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:setUser\\(\\) expects Icinga\\\\User, Icinga\\\\User\\|null given\\.$#"
+ count: 1
+ path: application/controllers/SearchController.php
+
+ -
+ message: "#^Access to an undefined property Zend_Controller_Action_HelperBroker\\:\\:\\$viewRenderer\\.$#"
+ count: 1
+ path: application/controllers/StaticController.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Action_HelperBroker\\:\\:layout\\(\\)\\.$#"
+ count: 1
+ path: application/controllers/StaticController.php
+
+ -
+ message: "#^Cannot access offset 'ino' on array\\{0\\: int, 1\\: int, 2\\: int, 3\\: int, 4\\: int, 5\\: int, 6\\: int, 7\\: int, \\.\\.\\.\\}\\|false\\.$#"
+ count: 1
+ path: application/controllers/StaticController.php
+
+ -
+ message: "#^Cannot access offset 'mtime' on array\\{0\\: int, 1\\: int, 2\\: int, 3\\: int, 4\\: int, 5\\: int, 6\\: int, 7\\: int, \\.\\.\\.\\}\\|false\\.$#"
+ count: 2
+ path: application/controllers/StaticController.php
+
+ -
+ message: "#^Cannot access offset 'size' on array\\{0\\: int, 1\\: int, 2\\: int, 3\\: int, 4\\: int, 5\\: int, 6\\: int, 7\\: int, \\.\\.\\.\\}\\|false\\.$#"
+ count: 1
+ path: application/controllers/StaticController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\StaticController\\:\\:imgAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/StaticController.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function readfile expects string, string\\|false given\\.$#"
+ count: 1
+ path: application/controllers/StaticController.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function stat expects string, string\\|false given\\.$#"
+ count: 1
+ path: application/controllers/StaticController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:getModule\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/StaticController.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_match expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/StaticController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Authentication\\\\User\\\\UserBackendInterface\\:\\:select\\(\\)\\.$#"
+ count: 3
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\UserController\\:\\:addAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\UserController\\:\\:createListTabs\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\UserController\\:\\:createShowTabs\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\UserController\\:\\:createmembershipAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\UserController\\:\\:editAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\UserController\\:\\:listAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\UserController\\:\\:removeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\UserController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\RepositoryForm\\:\\:edit\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\RepositoryForm\\:\\:remove\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Web\\\\Controller\\\\AuthBackendController\\:\\:getUserBackend\\(\\) expects string\\|null, mixed given\\.$#"
+ count: 6
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Parameter \\#1 \\$repository of method Icinga\\\\Forms\\\\RepositoryForm\\:\\:setRepository\\(\\) expects Icinga\\\\Repository\\\\Repository, Icinga\\\\Authentication\\\\User\\\\UserBackendInterface given\\.$#"
+ count: 3
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Parameter \\#1 \\$userName of method Icinga\\\\Forms\\\\Config\\\\User\\\\CreateMembershipForm\\:\\:setUsername\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Parameter \\#1 \\$username of class Icinga\\\\User constructor expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Parameter \\#2 \\$userName of method Icinga\\\\Controllers\\\\UserController\\:\\:createShowTabs\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 4
+ path: application/controllers/UserController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\UsergroupbackendController\\:\\:createAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/UsergroupbackendController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\UsergroupbackendController\\:\\:editAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/UsergroupbackendController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\UsergroupbackendController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/UsergroupbackendController.php
+
+ -
+ message: "#^Method Icinga\\\\Controllers\\\\UsergroupbackendController\\:\\:removeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/UsergroupbackendController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupBackendForm\\:\\:delete\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/UsergroupbackendController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupBackendForm\\:\\:edit\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/UsergroupbackendController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupBackendForm\\:\\:load\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/UsergroupbackendController.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 3
+ path: application/controllers/UsergroupbackendController.php
+
+ -
+ message: "#^Cannot call method addError\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/Account/ChangePasswordForm.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: application/forms/Account/ChangePasswordForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: application/forms/Account/ChangePasswordForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Account\\\\ChangePasswordForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Account/ChangePasswordForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Account\\\\ChangePasswordForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Account/ChangePasswordForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Account\\\\ChangePasswordForm\\:\\:isValid\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Account/ChangePasswordForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$user of method Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:authenticate\\(\\) expects Icinga\\\\User, Icinga\\\\User\\|null given\\.$#"
+ count: 1
+ path: application/forms/Account/ChangePasswordForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$password of method Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:authenticate\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Account/ChangePasswordForm.php
+
+ -
+ message: "#^Cannot call method icon\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: application/forms/AcknowledgeApplicationStateMessageForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\AcknowledgeApplicationStateMessageForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/AcknowledgeApplicationStateMessageForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\AcknowledgeApplicationStateMessageForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/AcknowledgeApplicationStateMessageForm.php
+
+ -
+ message: "#^Cannot call method icon\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: application/forms/ActionForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\ActionForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/ActionForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\ActionForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/ActionForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\ActionForm\\:\\:isValid\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/ActionForm.php
+
+ -
+ message: "#^Cannot access property \\$hash on mixed\\.$#"
+ count: 1
+ path: application/forms/Announcement/AcknowledgeAnnouncementForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/Announcement/AcknowledgeAnnouncementForm.php
+
+ -
+ message: "#^Cannot call method icon\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: application/forms/Announcement/AcknowledgeAnnouncementForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Announcement\\\\AcknowledgeAnnouncementForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Announcement/AcknowledgeAnnouncementForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Announcement\\\\AcknowledgeAnnouncementForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Announcement/AcknowledgeAnnouncementForm.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$end\\.$#"
+ count: 1
+ path: application/forms/Announcement/AnnouncementForm.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$start\\.$#"
+ count: 1
+ path: application/forms/Announcement/AnnouncementForm.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: application/forms/Announcement/AnnouncementForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Announcement\\\\AnnouncementForm\\:\\:createDeleteElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Announcement/AnnouncementForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Announcement\\\\AnnouncementForm\\:\\:createDeleteElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Announcement/AnnouncementForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Announcement\\\\AnnouncementForm\\:\\:createInsertElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Announcement/AnnouncementForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Announcement\\\\AnnouncementForm\\:\\:createInsertElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Announcement/AnnouncementForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Announcement\\\\AnnouncementForm\\:\\:createUpdateElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Announcement/AnnouncementForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Announcement\\\\AnnouncementForm\\:\\:createUpdateElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Announcement/AnnouncementForm.php
+
+ -
+ message: "#^Cannot call method addError\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/Authentication/LoginForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 3
+ path: application/forms/Authentication/LoginForm.php
+
+ -
+ message: "#^Cannot call method isChecked\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/Authentication/LoginForm.php
+
+ -
+ message: "#^Cannot call method setAttrib\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/Authentication/LoginForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Authentication\\\\LoginForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Authentication/LoginForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Authentication\\\\LoginForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Authentication/LoginForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Authentication\\\\LoginForm\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Authentication/LoginForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$cue of method Icinga\\\\Web\\\\Form\\:\\:setRequiredCue\\(\\) expects string, null given\\.$#"
+ count: 1
+ path: application/forms/Authentication/LoginForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$domain of method Icinga\\\\User\\:\\:setDomain\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Authentication/LoginForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$haystack of function strpos expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Authentication/LoginForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$username of class Icinga\\\\User constructor expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Authentication/LoginForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$password of method Icinga\\\\Authentication\\\\AuthChain\\:\\:authenticate\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Authentication/LoginForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$password of static method Icinga\\\\Web\\\\RememberMe\\:\\:fromCredentials\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Authentication/LoginForm.php
+
+ -
+ message: "#^Property Icinga\\\\Forms\\\\Authentication\\\\LoginForm\\:\\:\\$defaultElementDecorators type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Authentication/LoginForm.php
+
+ -
+ message: "#^Cannot call method getPreferences\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 2
+ path: application/forms/AutoRefreshForm.php
+
+ -
+ message: "#^Cannot call method setPreferences\\(\\) on mixed\\.$#"
+ count: 1
+ path: application/forms/AutoRefreshForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\AutoRefreshForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/AutoRefreshForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\AutoRefreshForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/AutoRefreshForm.php
+
+ -
+ message: "#^Parameter \\#3 \\$default of method Icinga\\\\User\\\\Preferences\\:\\:getValue\\(\\) expects null, true given\\.$#"
+ count: 2
+ path: application/forms/AutoRefreshForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\General\\\\ApplicationConfigForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/General/ApplicationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\General\\\\DefaultAuthenticationDomainConfigForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\General\\\\LoggingConfigForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/General/LoggingConfigForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getThemes\\(\\)\\.$#"
+ count: 1
+ path: application/forms/Config/General/ThemingConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\General\\\\ThemingConfigForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/General/ThemingConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\General\\\\ThemingConfigForm\\:\\:getValues\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/General/ThemingConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\GeneralConfigForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/GeneralConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\GeneralConfigForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/GeneralConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\Resource\\\\DbResourceForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/DbResourceForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\Resource\\\\DbResourceForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/DbResourceForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\Resource\\\\FileResourceForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/FileResourceForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\Resource\\\\FileResourceForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/FileResourceForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\Resource\\\\LdapResourceForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/LdapResourceForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\Resource\\\\LdapResourceForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/LdapResourceForm.php
+
+ -
+ message: "#^Cannot call method escape\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/SshResourceForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: application/forms/Config/Resource/SshResourceForm.php
+
+ -
+ message: "#^Cannot call method setValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/SshResourceForm.php
+
+ -
+ message: "#^Cannot call method url\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/SshResourceForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\Resource\\\\SshResourceForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/SshResourceForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\Resource\\\\SshResourceForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/SshResourceForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function file_exists expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/SshResourceForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function unlink expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/SshResourceForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$str of method Icinga\\\\Util\\\\File\\:\\:fwrite\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/SshResourceForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function sha1 expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/SshResourceForm.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/Resource/SshResourceForm.php
+
+ -
+ message: "#^Call to an undefined method Zend_Form_Element\\:\\:isChecked\\(\\)\\.$#"
+ count: 1
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Cannot call method isChecked\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Cannot call method setDecorators\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\ResourceConfigForm\\:\\:add\\(\\) has parameter \\$values with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\ResourceConfigForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\ResourceConfigForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\ResourceConfigForm\\:\\:edit\\(\\) has parameter \\$values with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\ResourceConfigForm\\:\\:getValues\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\ResourceConfigForm\\:\\:isValidPartial\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\ResourceConfigForm\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\ResourceConfigForm\\:\\:writeConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Config\\:\\:getSection\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Config\\:\\:hasSection\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\Config\\\\ResourceConfigForm\\:\\:edit\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strlen expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$type of method Icinga\\\\Forms\\\\Config\\\\ResourceConfigForm\\:\\:getResourceForm\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/ResourceConfigForm.php
+
+ -
+ message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: application/forms/Config/User/CreateMembershipForm.php
+
+ -
+ message: "#^Cannot access property \\$backend_name on mixed\\.$#"
+ count: 2
+ path: application/forms/Config/User/CreateMembershipForm.php
+
+ -
+ message: "#^Cannot access property \\$group_name on mixed\\.$#"
+ count: 2
+ path: application/forms/Config/User/CreateMembershipForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\User\\\\CreateMembershipForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/User/CreateMembershipForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\User\\\\CreateMembershipForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/User/CreateMembershipForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\User\\\\CreateMembershipForm\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/User/CreateMembershipForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\User\\\\CreateMembershipForm\\:\\:setBackends\\(\\) has parameter \\$backends with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/User/CreateMembershipForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$string of function explode expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/User/CreateMembershipForm.php
+
+ -
+ message: "#^Property Icinga\\\\Forms\\\\Config\\\\User\\\\CreateMembershipForm\\:\\:\\$backends type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/User/CreateMembershipForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\User\\\\UserForm\\:\\:createDeleteElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/User/UserForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\User\\\\UserForm\\:\\:createDeleteElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/User/UserForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\User\\\\UserForm\\:\\:createInsertElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/User/UserForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\User\\\\UserForm\\:\\:createInsertElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/User/UserForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\User\\\\UserForm\\:\\:createUpdateElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/User/UserForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\User\\\\UserForm\\:\\:createUpdateElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/User/UserForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\User\\\\UserForm\\:\\:getValues\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/User/UserForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\User\\\\UserForm\\:\\:isValid\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/User/UserForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method Icinga\\\\Web\\\\Url\\:\\:setParam\\(\\) expects array\\|bool\\|string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/User/UserForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\DbBackendForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackend/DbBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\DbBackendForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackend/DbBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\DbBackendForm\\:\\:setResources\\(\\) has parameter \\$resources with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackend/DbBackendForm.php
+
+ -
+ message: "#^Property Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\DbBackendForm\\:\\:\\$resources type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackend/DbBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\ExternalBackendForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackend/ExternalBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\ExternalBackendForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackend/ExternalBackendForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Db\\\\DbConnection\\|Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:bind\\(\\)\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackend/LdapBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\LdapBackendForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackend/LdapBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\LdapBackendForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackend/LdapBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\LdapBackendForm\\:\\:discoverDomain\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackend/LdapBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\LdapBackendForm\\:\\:getSuggestion\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackend/LdapBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\LdapBackendForm\\:\\:isValidPartial\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackend/LdapBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\LdapBackendForm\\:\\:setResources\\(\\) has parameter \\$resources with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackend/LdapBackendForm.php
+
+ -
+ message: "#^Property Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\LdapBackendForm\\:\\:\\$resources type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackend/LdapBackendForm.php
+
+ -
+ message: "#^Call to an undefined method Zend_Form_Element\\:\\:isChecked\\(\\)\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Cannot call method isChecked\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Cannot call method setDecorators\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackendConfigForm\\:\\:add\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackendConfigForm\\:\\:addSkipValidationCheckbox\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackendConfigForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackendConfigForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackendConfigForm\\:\\:edit\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackendConfigForm\\:\\:getBackendForm\\(\\) should return Icinga\\\\Web\\\\Form but returns object\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackendConfigForm\\:\\:isValid\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackendConfigForm\\:\\:isValidPartial\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackendConfigForm\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of static method Icinga\\\\Authentication\\\\User\\\\UserBackend\\:\\:create\\(\\) expects string, null given\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$offset of function array_splice expects int, int\\|string\\|false given\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Property Icinga\\\\Forms\\\\Config\\\\UserBackendConfigForm\\:\\:\\$customBackends type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Property Icinga\\\\Forms\\\\Config\\\\UserBackendConfigForm\\:\\:\\$resources type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendConfigForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Forms\\\\ConfigForm\\:\\:move\\(\\)\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendReorderForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackendReorderForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendReorderForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackendReorderForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendReorderForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserBackendReorderForm\\:\\:getBackendOrder\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendReorderForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$string of function explode expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/UserBackendReorderForm.php
+
+ -
+ message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/AddMemberForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Extensible\\:\\:select\\(\\)\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/AddMemberForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Fetchable\\:\\:applyFilter\\(\\)\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/AddMemberForm.php
+
+ -
+ message: "#^Dead catch \\- Icinga\\\\Exception\\\\NotFoundError is never thrown in the try block\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/AddMemberForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\AddMemberForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/AddMemberForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\AddMemberForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/AddMemberForm.php
+
+ -
+ message: "#^Parameter \\#1 \\.\\.\\.\\$arrays of function array_merge expects array, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/AddMemberForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$string of function explode expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/AddMemberForm.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 2
+ path: application/forms/Config/UserGroup/AddMemberForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\DbUserGroupBackendForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/DbUserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\DbUserGroupBackendForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/DbUserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\DbUserGroupBackendForm\\:\\:getDatabaseResourceNames\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/DbUserGroupBackendForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/DbUserGroupBackendForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Db\\\\DbConnection\\|Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:getHostname\\(\\)\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Db\\\\DbConnection\\|Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:getPort\\(\\)\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\LdapUserGroupBackendForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\LdapUserGroupBackendForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\LdapUserGroupBackendForm\\:\\:createGroupConfigElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\LdapUserGroupBackendForm\\:\\:createHiddenUserConfigElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\LdapUserGroupBackendForm\\:\\:createUserConfigElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\LdapUserGroupBackendForm\\:\\:getLdapResourceNames\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\LdapUserGroupBackendForm\\:\\:getLdapUserBackendNames\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$ds of class Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend constructor expects Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection, Icinga\\\\Data\\\\Db\\\\DbConnection\\|Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection given\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$resource of method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\LdapUserGroupBackendForm\\:\\:getLdapUserBackendNames\\(\\) expects Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection, Icinga\\\\Data\\\\Db\\\\DbConnection\\|Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection given\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$resourceName of static method Icinga\\\\Data\\\\ResourceFactory\\:\\:create\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 2
+ path: application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
+
+ -
+ message: "#^Cannot call method setDecorators\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupBackendForm\\:\\:add\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupBackendForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupBackendForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupBackendForm\\:\\:edit\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupBackendForm\\:\\:getBackendForm\\(\\) should return Icinga\\\\Web\\\\Form but returns object\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupBackendForm\\:\\:isValidPartial\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupBackendForm\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupBackendForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of static method Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackend\\:\\:create\\(\\) expects string, null given\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupBackendForm.php
+
+ -
+ message: "#^Property Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupBackendForm\\:\\:\\$customBackends type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupBackendForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupForm\\:\\:createDeleteElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupForm\\:\\:createDeleteElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupForm\\:\\:createInsertElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupForm\\:\\:createInsertElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Config\\\\UserGroup\\\\UserGroupForm\\:\\:isValid\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method Icinga\\\\Web\\\\Url\\:\\:setParam\\(\\) expects array\\|bool\\|string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Config/UserGroup/UserGroupForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\ConfigForm\\:\\:getValues\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/ConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\ConfigForm\\:\\:isEmptyConfig\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/ConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\ConfigForm\\:\\:isEmptyConfig\\(\\) has parameter \\$config with no value type specified in iterable type array\\|Icinga\\\\Application\\\\Config\\.$#"
+ count: 1
+ path: application/forms/ConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\ConfigForm\\:\\:isValid\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/ConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\ConfigForm\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/ConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\ConfigForm\\:\\:transformEmptyValuesToNull\\(\\) has parameter \\$values with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/ConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\ConfigForm\\:\\:transformEmptyValuesToNull\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/ConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\ConfigForm\\:\\:writeConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/ConfigForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/Control/LimiterControlForm.php
+
+ -
+ message: "#^Cannot cast mixed to int\\.$#"
+ count: 1
+ path: application/forms/Control/LimiterControlForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Control\\\\LimiterControlForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Control/LimiterControlForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Control\\\\LimiterControlForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Control/LimiterControlForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method Icinga\\\\Web\\\\Url\\:\\:setParam\\(\\) expects array\\|bool\\|string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Control/LimiterControlForm.php
+
+ -
+ message: "#^Property Icinga\\\\Forms\\\\Control\\\\LimiterControlForm\\:\\:\\$defaultElementDecorators type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Control/LimiterControlForm.php
+
+ -
+ message: "#^Static property Icinga\\\\Forms\\\\Control\\\\LimiterControlForm\\:\\:\\$limits \\(array\\<int\\>\\) does not accept default value of type array\\<int, string\\>\\.$#"
+ count: 1
+ path: application/forms/Control/LimiterControlForm.php
+
+ -
+ message: "#^Cannot call method getRelativeUrl\\(\\) on Icinga\\\\Web\\\\Url\\|null\\.$#"
+ count: 1
+ path: application/forms/Dashboard/DashletForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Dashboard\\\\DashletForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Dashboard/DashletForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Dashboard\\\\DashletForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Dashboard/DashletForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Dashboard\\\\DashletForm\\:\\:load\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Dashboard/DashletForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Dashboard\\\\DashletForm\\:\\:setDashboard\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Dashboard/DashletForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\LdapDiscoveryForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/LdapDiscoveryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\LdapDiscoveryForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/LdapDiscoveryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\DashletForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Navigation/DashletForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\DashletForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Navigation/DashletForm.php
+
+ -
+ message: "#^Cannot call method removeMultiOption\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: application/forms/Navigation/MenuItemForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\MenuItemForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Navigation/MenuItemForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\MenuItemForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Navigation/MenuItemForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\Form\\:\\:requiresParentSelection\\(\\)\\.$#"
+ count: 2
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Cannot call method addError\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Cannot call method getSection\\(\\) on Icinga\\\\Application\\\\Config\\|null\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:add\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:edit\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:getFlattenedChildren\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:getItemTypes\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:isValid\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:listAvailableParents\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:setDefaultUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:setDefaultUrl\\(\\) has parameter \\$url with no type specified\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:setItemTypes\\(\\) has parameter \\$itemTypes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:writeConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:hasBeenShared\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$type of static method Icinga\\\\Application\\\\Config\\:\\:navigation\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of function implode expects array\\<string\\>, mixed given\\.$#"
+ count: 2
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of function implode expects array\\|null, mixed given\\.$#"
+ count: 2
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$owner of method Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:listAvailableParents\\(\\) expects string\\|null, mixed given\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$username of static method Icinga\\\\Application\\\\Config\\:\\:navigation\\(\\) expects string\\|null, mixed given\\.$#"
+ count: 2
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Property Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:\\$defaultUrl has no type specified\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Property Icinga\\\\Forms\\\\Navigation\\\\NavigationConfigForm\\:\\:\\$itemTypes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationItemForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationItemForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationItemForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationItemForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Navigation\\\\NavigationItemForm\\:\\:getValues\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Navigation/NavigationItemForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getThemes\\(\\)\\.$#"
+ count: 1
+ path: application/forms/PreferenceForm.php
+
+ -
+ message: "#^Cannot call method getPreferences\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 2
+ path: application/forms/PreferenceForm.php
+
+ -
+ message: "#^Cannot call method href\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 3
+ path: application/forms/PreferenceForm.php
+
+ -
+ message: "#^Cannot call method isChecked\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: application/forms/PreferenceForm.php
+
+ -
+ message: "#^Cannot call method setPreferences\\(\\) on mixed\\.$#"
+ count: 1
+ path: application/forms/PreferenceForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\PreferenceForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/PreferenceForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\PreferenceForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/PreferenceForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\PreferenceForm\\:\\:getDefaultShowStacktraces\\(\\) should return bool but returns mixed\\.$#"
+ count: 1
+ path: application/forms/PreferenceForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\PreferenceForm\\:\\:getLocale\\(\\) has parameter \\$availableLocales with no type specified\\.$#"
+ count: 1
+ path: application/forms/PreferenceForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\PreferenceForm\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/PreferenceForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#"
+ count: 1
+ path: application/forms/PreferenceForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$locale of function setlocale expects array\\|string\\|null, int given\\.$#"
+ count: 2
+ path: application/forms/PreferenceForm.php
+
+ -
+ message: "#^Parameter \\#3 \\$default of method Icinga\\\\User\\\\Preferences\\:\\:getValue\\(\\) expects null, mixed given\\.$#"
+ count: 1
+ path: application/forms/PreferenceForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Repository\\\\Repository\\:\\:delete\\(\\)\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Repository\\\\Repository\\:\\:insert\\(\\)\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Repository\\\\Repository\\:\\:update\\(\\)\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:add\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:createDeleteElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:createDeleteElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:createInsertElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:createInsertElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:createUpdateElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:createUpdateElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:deleteEntry\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:edit\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:getData\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:insertEntry\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:onDeleteRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:onInsertRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:onUpdateRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\RepositoryForm\\:\\:updateEntry\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Property Icinga\\\\Forms\\\\RepositoryForm\\:\\:\\$data \\(array\\) does not accept array\\|null\\.$#"
+ count: 2
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Property Icinga\\\\Forms\\\\RepositoryForm\\:\\:\\$data type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/RepositoryForm.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$description\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$groups\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$name\\.$#"
+ count: 2
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$parent\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$permissions\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$unrestricted\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$users\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Access to an undefined property object\\|object\\:\\:\\$permissions\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Access to an undefined property object\\|object\\:\\:\\$refusals\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Repository\\\\Repository\\:\\:update\\(\\)\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Cannot access property \\$name on mixed\\.$#"
+ count: 6
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Cannot call method getDecorator\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Cannot call method icon\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 2
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Cannot call method setOption\\(\\) on Zend_Form_Decorator_Abstract\\|false\\.$#"
+ count: 2
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Security\\\\RoleForm\\:\\:collectProvidedPrivileges\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Security\\\\RoleForm\\:\\:collectRoles\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Security\\\\RoleForm\\:\\:createDeleteElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Security\\\\RoleForm\\:\\:createDeleteElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Security\\\\RoleForm\\:\\:createInsertElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Security\\\\RoleForm\\:\\:createInsertElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Security\\\\RoleForm\\:\\:getValues\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Security\\\\RoleForm\\:\\:isValid\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Security\\\\RoleForm\\:\\:sortPermissions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Method Icinga\\\\Forms\\\\Security\\\\RoleForm\\:\\:sortPermissions\\(\\) has parameter \\$permissions with no type specified\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$string of function explode expects string, TKey of int\\|string given\\.$#"
+ count: 2
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Property Icinga\\\\Forms\\\\Security\\\\RoleForm\\:\\:\\$providedPermissions type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Property Icinga\\\\Forms\\\\Security\\\\RoleForm\\:\\:\\$providedRestrictions type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/Security/RoleForm.php
+
+ -
+ message: "#^Cannot access property \\$tickets on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: application/views/helpers/CreateTicketLinks.php
+
+ -
+ message: "#^Cannot call method createLinks\\(\\) on array\\|Icinga\\\\Application\\\\Hook\\\\TicketHook\\.$#"
+ count: 1
+ path: application/views/helpers/CreateTicketLinks.php
+
+ -
+ message: "#^PHPDoc tag @var for variable \\$tickets has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/views/helpers/CreateTicketLinks.php
+
+ -
+ message: "#^Method Zend_View_Helper_FormDate\\:\\:formDate\\(\\) has parameter \\$attribs with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/views/helpers/FormDate.php
+
+ -
+ message: "#^Parameter \\#1 \\$attribs of method Zend_View_Helper_HtmlElement\\:\\:_htmlAttribs\\(\\) expects array, array\\|null given\\.$#"
+ count: 1
+ path: application/views/helpers/FormDate.php
+
+ -
+ message: "#^Parameter \\#1 \\$var of method Icinga\\\\Web\\\\View\\:\\:escape\\(\\) expects string\\|null, int\\|null given\\.$#"
+ count: 1
+ path: application/views/helpers/FormDate.php
+
+ -
+ message: "#^Cannot call method escape\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 3
+ path: application/views/helpers/FormDateTime.php
+
+ -
+ message: "#^Method Zend_View_Helper_FormDateTime\\:\\:formDateTime\\(\\) has parameter \\$attribs with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/views/helpers/FormDateTime.php
+
+ -
+ message: "#^Offset 'local' does not exist on array\\|null\\.$#"
+ count: 2
+ path: application/views/helpers/FormDateTime.php
+
+ -
+ message: "#^Cannot call method escape\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 4
+ path: application/views/helpers/FormNumber.php
+
+ -
+ message: "#^Method Zend_View_Helper_FormNumber\\:\\:formNumber\\(\\) has parameter \\$attribs with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/views/helpers/FormNumber.php
+
+ -
+ message: "#^Method Zend_View_Helper_FormNumber\\:\\:formatNumber\\(\\) has parameter \\$number with no type specified\\.$#"
+ count: 1
+ path: application/views/helpers/FormNumber.php
+
+ -
+ message: "#^Method Zend_View_Helper_FormNumber\\:\\:formatNumber\\(\\) should return string but returns array\\|float\\|int\\|string\\|false\\|null\\.$#"
+ count: 1
+ path: application/views/helpers/FormNumber.php
+
+ -
+ message: "#^Parameter \\#1 \\$attribs of method Zend_View_Helper_HtmlElement\\:\\:_htmlAttribs\\(\\) expects array, array\\<mixed, mixed\\>\\|null given\\.$#"
+ count: 1
+ path: application/views/helpers/FormNumber.php
+
+ -
+ message: "#^Method Zend_View_Helper_FormTime\\:\\:formTime\\(\\) has parameter \\$attribs with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/views/helpers/FormTime.php
+
+ -
+ message: "#^Parameter \\#1 \\$attribs of method Zend_View_Helper_HtmlElement\\:\\:_htmlAttribs\\(\\) expects array, array\\|null given\\.$#"
+ count: 1
+ path: application/views/helpers/FormTime.php
+
+ -
+ message: "#^Parameter \\#1 \\$var of method Icinga\\\\Web\\\\View\\:\\:escape\\(\\) expects string\\|null, int\\|null given\\.$#"
+ count: 1
+ path: application/views/helpers/FormTime.php
+
+ -
+ message: "#^Cannot call method protectId\\(\\) on Zend_Controller_Request_Abstract\\|null\\.$#"
+ count: 1
+ path: application/views/helpers/ProtectId.php
+
+ -
+ message: "#^Method Zend_View_Helper_ProtectId\\:\\:protectId\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/views/helpers/ProtectId.php
+
+ -
+ message: "#^Method Zend_View_Helper_ProtectId\\:\\:protectId\\(\\) has parameter \\$id with no type specified\\.$#"
+ count: 1
+ path: application/views/helpers/ProtectId.php
+
+ -
+ message: "#^Method Zend_View_Helper_Util\\:\\:showHourMin\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/views/helpers/Util.php
+
+ -
+ message: "#^Method Zend_View_Helper_Util\\:\\:showHourMin\\(\\) has parameter \\$sec with no type specified\\.$#"
+ count: 1
+ path: application/views/helpers/Util.php
+
+ -
+ message: "#^Method Zend_View_Helper_Util\\:\\:showSeconds\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/views/helpers/Util.php
+
+ -
+ message: "#^Method Zend_View_Helper_Util\\:\\:showSeconds\\(\\) has parameter \\$sec with no type specified\\.$#"
+ count: 1
+ path: application/views/helpers/Util.php
+
+ -
+ message: "#^Method Zend_View_Helper_Util\\:\\:showTime\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/views/helpers/Util.php
+
+ -
+ message: "#^Method Zend_View_Helper_Util\\:\\:showTime\\(\\) has parameter \\$timestamp with no type specified\\.$#"
+ count: 1
+ path: application/views/helpers/Util.php
+
+ -
+ message: "#^Method Zend_View_Helper_Util\\:\\:showTimeSince\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/views/helpers/Util.php
+
+ -
+ message: "#^Method Zend_View_Helper_Util\\:\\:showTimeSince\\(\\) has parameter \\$timestamp with no type specified\\.$#"
+ count: 1
+ path: application/views/helpers/Util.php
+
+ -
+ message: "#^Method Zend_View_Helper_Util\\:\\:util\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/views/helpers/Util.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getAvailableModulePaths\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/ApplicationBootstrap.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getLocaleDir\\(\\) should return string but returns string\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Application/ApplicationBootstrap.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:hasLocales\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/ApplicationBootstrap.php
+
+ -
+ message: "#^Parameter \\#2 \\$callback of function array_filter expects callable\\(non\\-empty\\-string\\|false\\)\\: mixed, 'is_dir' given\\.$#"
+ count: 1
+ path: library/Icinga/Application/ApplicationBootstrap.php
+
+ -
+ message: "#^Parameter \\#2 \\$string of function explode expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Application/ApplicationBootstrap.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:\\$libDir \\(string\\) does not accept string\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Application/ApplicationBootstrap.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:\\$libraryPaths \\(array\\<string\\>\\) does not accept array\\<int, string\\|false\\>\\.$#"
+ count: 1
+ path: library/Icinga/Application/ApplicationBootstrap.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:\\$localeDir \\(string\\) does not accept false\\.$#"
+ count: 1
+ path: library/Icinga/Application/ApplicationBootstrap.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$columns\\.$#"
+ count: 2
+ path: library/Icinga/Application/Benchmark.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$rows\\.$#"
+ count: 2
+ path: library/Icinga/Application/Benchmark.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Benchmark\\:\\:dump\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Benchmark.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Benchmark\\:\\:measure\\(\\) has parameter \\$message with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Benchmark.php
+
+ -
+ message: "#^PHPDoc tag @param has invalid value \\(string A comment identifying the current measurement\\)\\: Unexpected token \"A\", expected variable at offset 143$#"
+ count: 1
+ path: library/Icinga/Application/Benchmark.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Benchmark\\:\\:\\$instance has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Benchmark.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Benchmark\\:\\:\\$measures has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Benchmark.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Benchmark\\:\\:\\$start has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Benchmark.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\ClassLoader\\:\\:buildClassFilename\\(\\) has parameter \\$class with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/ClassLoader.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\ClassLoader\\:\\:buildClassFilename\\(\\) has parameter \\$namespace with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/ClassLoader.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\ClassLoader\\:\\:classBelongsToModule\\(\\) has parameter \\$class with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/ClassLoader.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\ClassLoader\\:\\:extractModuleName\\(\\) has parameter \\$class with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/ClassLoader.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\ClassLoader\\:\\:extractModuleNamespace\\(\\) has parameter \\$class with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/ClassLoader.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\ClassLoader\\:\\:namespaceHasApplictionDirectory\\(\\) has parameter \\$namespace with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/ClassLoader.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\ClassLoader\\:\\:register\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/ClassLoader.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\ClassLoader\\:\\:unregister\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/ClassLoader.php
+
+ -
+ message: "#^Parameter \\#1 \\$callback of function spl_autoload_register expects \\(callable\\(string\\)\\: void\\)\\|null, array\\{\\$this\\(Icinga\\\\Application\\\\ClassLoader\\), 'loadClass'\\} given\\.$#"
+ count: 1
+ path: library/Icinga/Application/ClassLoader.php
+
+ -
+ message: "#^Parameter \\#2 \\$offset of function substr expects int, int\\<0, max\\>\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Application/ClassLoader.php
+
+ -
+ message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\<0, max\\>\\|false given\\.$#"
+ count: 2
+ path: library/Icinga/Application/ClassLoader.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\ClassLoader\\:\\:\\$applicationDirectories type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/ClassLoader.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\ClassLoader\\:\\:\\$applicationPrefixes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/ClassLoader.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\ClassLoader\\:\\:\\$namespaces type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/ClassLoader.php
+
+ -
+ message: "#^Cannot cast mixed to int\\.$#"
+ count: 2
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Cli\\:\\:cliLoader\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Cli\\:\\:dispatch\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Cli\\:\\:dispatchEndless\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Cli\\:\\:dispatchModule\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Cli\\:\\:dispatchModule\\(\\) has parameter \\$basedir with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Cli\\:\\:dispatchModule\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Cli\\:\\:dispatchOnce\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Cli\\:\\:getParams\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Cli\\:\\:parseBasicParams\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Cli\\:\\:setupFakeAuthentication\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Cli\\:\\:\\$cliLoader has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Cli\\:\\:\\$debug has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Cli\\:\\:\\$params has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Cli\\:\\:\\$showBenchmark has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Cli\\:\\:\\$verbose has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Cli\\:\\:\\$watchTimeout has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^While loop condition is always true\\.$#"
+ count: 1
+ path: library/Icinga/Application/Cli.php
+
+ -
+ message: "#^Class Icinga\\\\Application\\\\Config implements generic interface Iterator but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Application/Config.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Config\\:\\:current\\(\\) should return Icinga\\\\Data\\\\ConfigObject but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Application/Config.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Config\\:\\:fromArray\\(\\) has parameter \\$array with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Config.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Config\\:\\:fromIni\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Config.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Config\\:\\:getSection\\(\\) should return Icinga\\\\Data\\\\ConfigObject but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Application/Config.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Config\\:\\:keys\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Config.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Config\\:\\:saveIni\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Config.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Config\\:\\:setSection\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Config.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Config\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Config.php
+
+ -
+ message: "#^Parameter \\#2 \\$filename of class Icinga\\\\File\\\\Ini\\\\IniWriter constructor expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Config.php
+
+ -
+ message: "#^Parameter \\#3 \\$filemode of class Icinga\\\\File\\\\Ini\\\\IniWriter constructor expects int, int\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Config.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Config\\:\\:\\$app type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Config.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Config\\:\\:\\$modules type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Config.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Config\\:\\:\\$navigation type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Config.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\:\\:all\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\:\\:assertValidHook\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\:\\:clean\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\:\\:normalizeHookName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\:\\:normalizeHookName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\:\\:register\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\:\\:splitHookName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\:\\:splitHookName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook.php
+
+ -
+ message: "#^Parameter \\#1 \\$object of function get_class expects object, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Hook\\:\\:\\$hooks type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Hook\\:\\:\\$instances type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\ApplicationStateHook\\:\\:collectMessages\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/ApplicationStateHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\ApplicationStateHook\\:\\:getAllMessages\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/ApplicationStateHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\ApplicationStateHook\\:\\:getMessages\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/ApplicationStateHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\ApplicationStateHook\\:\\:hasMessages\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/ApplicationStateHook.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Hook\\\\ApplicationStateHook\\:\\:\\$messages has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/ApplicationStateHook.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/AuditHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\AuditHook\\:\\:extractMessageValue\\(\\) has parameter \\$messageData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/AuditHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\AuditHook\\:\\:extractMessageValue\\(\\) has parameter \\$path with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/AuditHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\AuditHook\\:\\:formatMessage\\(\\) has parameter \\$messageData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/AuditHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\AuditHook\\:\\:formatMessage\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/AuditHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\AuditHook\\:\\:logActivity\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/AuditHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\AuditHook\\:\\:logActivity\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/AuditHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\AuditHook\\:\\:logMessage\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/AuditHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\AuditHook\\:\\:logMessage\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/AuditHook.php
+
+ -
+ message: "#^Parameter \\#2 \\$callback of function preg_replace_callback expects callable\\(array\\<int\\|string, string\\>\\)\\: string, Closure\\(mixed\\)\\: mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/AuditHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\AuthenticationHook\\:\\:onLogin\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/AuthenticationHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\AuthenticationHook\\:\\:onLogout\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/AuthenticationHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\AuthenticationHook\\:\\:triggerLogin\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/AuthenticationHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\AuthenticationHook\\:\\:triggerLogout\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/AuthenticationHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\ConfigFormEventsHook\\:\\:getLastErrors\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/ConfigFormEventsHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\ConfigFormEventsHook\\:\\:isValid\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/ConfigFormEventsHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\ConfigFormEventsHook\\:\\:onSuccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/ConfigFormEventsHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\ConfigFormEventsHook\\:\\:runAppliesTo\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/ConfigFormEventsHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\ConfigFormEventsHook\\:\\:runEventMethod\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/ConfigFormEventsHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\ConfigFormEventsHook\\:\\:runEventMethod\\(\\) has parameter \\$eventMethod with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/ConfigFormEventsHook.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Hook\\\\ConfigFormEventsHook\\:\\:\\$lastErrors type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/ConfigFormEventsHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\HealthHook\\:\\:getMetrics\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/HealthHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\HealthHook\\:\\:setMetrics\\(\\) has parameter \\$metrics with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/HealthHook.php
+
+ -
+ message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\<0, max\\>\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/HealthHook.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Hook\\\\HealthHook\\:\\:\\$metrics type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/HealthHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\PdfexportHook\\:\\:streamPdfFromHtml\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/PdfexportHook.php
+
+ -
+ message: "#^Class Icinga\\\\Application\\\\Hook\\\\Ticket\\\\TicketPattern implements generic interface ArrayAccess but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Application/Hook/Ticket/TicketPattern.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\Ticket\\\\TicketPattern\\:\\:getMatch\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/Ticket/TicketPattern.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\Ticket\\\\TicketPattern\\:\\:setMatch\\(\\) has parameter \\$match with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/Ticket/TicketPattern.php
+
+ -
+ message: "#^Parameter \\#1 \\$key of function array_key_exists expects int\\|string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/Ticket/TicketPattern.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Hook\\\\Ticket\\\\TicketPattern\\:\\:\\$match type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/Ticket/TicketPattern.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\TicketHook\\:\\:createLink\\(\\) has parameter \\$match with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/TicketHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\TicketHook\\:\\:createLinks\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/TicketHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\TicketHook\\:\\:fail\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/TicketHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Hook\\\\TicketHook\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/TicketHook.php
+
+ -
+ message: "#^PHPDoc tag @param references unknown parameter\\: \\$arg$#"
+ count: 1
+ path: library/Icinga/Application/Hook/TicketHook.php
+
+ -
+ message: "#^Parameter \\#2 \\$values of function vsprintf expects array\\<bool\\|float\\|int\\|string\\|null\\>, array\\<int, mixed\\> given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/TicketHook.php
+
+ -
+ message: "#^Access to an undefined property Zend_Controller_Action_Helper_Abstract\\:\\:\\$view\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/WebBaseHook.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Action_Helper_Abstract\\:\\:initView\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Application/Hook/WebBaseHook.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Icinga\\:\\:setApp\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Icinga.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\LegacyWeb\\:\\:\\$legacyBasedir has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/LegacyWeb.php
+
+ -
+ message: "#^Class Icinga\\\\Application\\\\Libraries implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Application/Libraries.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Libraries\\:\\:getIterator\\(\\) return type with generic class ArrayIterator does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Application/Libraries.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Libraries\\\\Library\\:\\:assets\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Libraries/Library.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Libraries\\\\Library\\:\\:metaData\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Libraries/Library.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Libraries\\\\Library\\:\\:metaData\\(\\) should return array but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Application/Libraries/Library.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function ltrim expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Libraries/Library.php
+
+ -
+ message: "#^Parameter \\#2 \\$pieces of function join expects array, array\\<int, string\\>\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Libraries/Library.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Libraries\\\\Library\\:\\:\\$assets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Libraries/Library.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Libraries\\\\Library\\:\\:\\$metaData \\(array\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Application/Libraries/Library.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Libraries\\\\Library\\:\\:\\$metaData type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Libraries/Library.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\:\\:createWriter\\(\\) should return Icinga\\\\Application\\\\Logger\\\\LogWriter but returns object\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\:\\:debug\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\:\\:error\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\:\\:formatMessage\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\:\\:info\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\:\\:log\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\:\\:warning\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger.php
+
+ -
+ message: "#^PHPDoc tag @param references unknown parameter\\: \\$arg$#"
+ count: 5
+ path: library/Icinga/Application/Logger.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Application/Logger.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtoupper expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Logger\\:\\:\\$configErrors type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Logger\\:\\:\\$levels type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\\\LogWriter\\:\\:log\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/LogWriter.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\\\LogWriter\\:\\:log\\(\\) has parameter \\$message with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/LogWriter.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\\\LogWriter\\:\\:log\\(\\) has parameter \\$severity with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/LogWriter.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\\\Writer\\\\FileWriter\\:\\:log\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/Writer/FileWriter.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\\\Writer\\\\FileWriter\\:\\:write\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/Writer/FileWriter.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of function dirname expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Application/Logger/Writer/FileWriter.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function substr expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/Writer/FileWriter.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Logger\\\\Writer\\\\FileWriter\\:\\:\\$file \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/Writer/FileWriter.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\\\Writer\\\\PhpWriter\\:\\:log\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/Writer/PhpWriter.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\\\Writer\\\\PhpWriter\\:\\:log\\(\\) has parameter \\$message with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/Writer/PhpWriter.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\\\Writer\\\\PhpWriter\\:\\:log\\(\\) has parameter \\$severity with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/Writer/PhpWriter.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Logger\\\\Writer\\\\PhpWriter\\:\\:\\$ident \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/Writer/PhpWriter.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\\\Writer\\\\StderrWriter\\:\\:log\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/Writer/StderrWriter.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Logger\\\\Writer\\\\SyslogWriter\\:\\:log\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/Writer/SyslogWriter.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Logger\\\\Writer\\\\SyslogWriter\\:\\:\\$facilities type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/Writer/SyslogWriter.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Logger\\\\Writer\\\\SyslogWriter\\:\\:\\$ident \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/Writer/SyslogWriter.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Logger\\\\Writer\\\\SyslogWriter\\:\\:\\$severityMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Logger/Writer/SyslogWriter.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\DashboardContainer\\:\\:getDashlets\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/DashboardContainer.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\DashboardContainer\\:\\:setDashlets\\(\\) has parameter \\$dashlets with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/DashboardContainer.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\DashboardContainer\\:\\:\\$dashlets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/DashboardContainer.php
+
+ -
+ message: "#^If condition is always true\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:__construct\\(\\) has parameter \\$availableDirs with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:detectEnabledModules\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:detectInstalledModules\\(\\) has parameter \\$availableDirs with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:getModuleDirs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:getModuleInfo\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:listEnabledModules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:listInstalledModules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:listLoadedModules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Offset 'message' does not exist on array\\{type\\: int, message\\: string, file\\: string, line\\: int\\}\\|null\\.$#"
+ count: 4
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Parameter \\#1 \\$app of class Icinga\\\\Application\\\\Modules\\\\Module constructor expects Icinga\\\\Application\\\\ApplicationBootstrap, Icinga\\\\Application\\\\Icinga given\\.$#"
+ count: 3
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Parameter \\#3 \\$basedir of class Icinga\\\\Application\\\\Modules\\\\Module constructor expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:\\$app \\(Icinga\\\\Application\\\\Icinga\\) does not accept Icinga\\\\Application\\\\ApplicationBootstrap\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:\\$enabledDirs type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:\\$installedBaseDirs type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:\\$loadedModules type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:\\$modulePaths type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Manager.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\MenuItemContainer\\:\\:add\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/MenuItemContainer.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\MenuItemContainer\\:\\:getChildren\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/MenuItemContainer.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$depends\\.$#"
+ count: 3
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$description\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$libraries\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$modules\\.$#"
+ count: 2
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$title\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$version\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Router_Interface\\:\\:addRoute\\(\\)\\.$#"
+ count: 3
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Cannot access an offset on array\\<int, string\\>\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Cannot use array destructuring on array\\<int, string\\>\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:createMenu\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:dashboard\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:getCssFiles\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:getDependencies\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:getJsFiles\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:getNavigationItems\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:getProvidedPermissions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:getProvidedRestrictions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:getRequiredLibraries\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:getRequiredModules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:getSearchUrls\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:getSetupWizard\\(\\) should return Icinga\\\\Module\\\\Setup\\\\SetupWizard but returns object\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:getUserBackends\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:getUserGroupBackends\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:hasLocales\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:listLocales\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:menuSection\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:provideConfigTab\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:providePermission\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:provideRestriction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:slashesToNamespace\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\Module\\:\\:slashesToNamespace\\(\\) has parameter \\$class with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Parameter \\#1 \\$dir_handle of function closedir expects resource\\|null, resource\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Parameter \\#1 \\$dir_handle of function readdir expects resource\\|null, resource\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function rtrim expects string, array\\|string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, array\\<int, string\\>\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Module\\:\\:\\$app \\(Icinga\\\\Application\\\\Web\\) does not accept Icinga\\\\Application\\\\ApplicationBootstrap\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Module\\:\\:\\$configTabs type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Module\\:\\:\\$cssFiles type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Module\\:\\:\\$jsFiles type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Module\\:\\:\\$navigationItems type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Module\\:\\:\\$paneItems type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Module\\:\\:\\$permissionList type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Module\\:\\:\\$restrictionList type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Module\\:\\:\\$routes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Module\\:\\:\\$searchUrls type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Module\\:\\:\\$userBackends type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\Module\\:\\:\\$userGroupBackends type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Variable \\$router in PHPDoc tag @var does not match any variable in the foreach loop\\: \\$name, \\$route$#"
+ count: 1
+ path: library/Icinga/Application/Modules/Module.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\NavigationItemContainer\\:\\:__call\\(\\) has parameter \\$arguments with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/NavigationItemContainer.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\NavigationItemContainer\\:\\:__construct\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/NavigationItemContainer.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\NavigationItemContainer\\:\\:getProperties\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/NavigationItemContainer.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Modules\\\\NavigationItemContainer\\:\\:setProperties\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/NavigationItemContainer.php
+
+ -
+ message: "#^Parameter \\#1 \\$callback of function call_user_func expects callable\\(\\)\\: mixed, array\\{\\$this\\(Icinga\\\\Application\\\\Modules\\\\NavigationItemContainer\\), string\\} given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/NavigationItemContainer.php
+
+ -
+ message: "#^Parameter \\#2 \\$pieces of function join expects array, array\\<int, string\\>\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/NavigationItemContainer.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Modules\\\\NavigationItemContainer\\:\\:\\$properties type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Modules/NavigationItemContainer.php
+
+ -
+ message: "#^Cannot access offset 'name' on array\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Application/Platform.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Platform\\:\\:discoverHostname\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Platform.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_shift expects array, array\\<int, string\\>\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Platform.php
+
+ -
+ message: "#^Parameter \\#1 \\$hostname of function gethostbyname expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Platform.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strlen expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Platform.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#"
+ count: 2
+ path: library/Icinga/Application/Platform.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_split expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Platform.php
+
+ -
+ message: "#^Static property Icinga\\\\Application\\\\Platform\\:\\:\\$fqdn \\(string\\) does not accept string\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Application/Platform.php
+
+ -
+ message: "#^Static property Icinga\\\\Application\\\\Platform\\:\\:\\$hostname \\(string\\) does not accept string\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Application/Platform.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Test\\:\\:getFrontController\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Test.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_flip expects array\\<int\\|string\\>, array\\<int, string\\>\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Test.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Test\\:\\:\\$request \\(Icinga\\\\Web\\\\Request\\) in isset\\(\\) is not nullable\\.$#"
+ count: 2
+ path: library/Icinga/Application/Test.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Version\\:\\:get\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Version.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getFrontController\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Router_Interface\\:\\:addRoute\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Interface\\:\\:addHelperPath\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Interface\\:\\:headTitle\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Interface\\:\\:setEncoding\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Cannot call method can\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Cannot call method getPreferences\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 3
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Web\\:\\:detectLocale\\(\\) should return string but returns array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Web\\:\\:detectTimezone\\(\\) should return string\\|null but returns array\\|string\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Web\\:\\:dispatch\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Web\\:\\:hasAccessToSharedNavigationItem\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Method Icinga\\\\Application\\\\Web\\:\\:hasAccessToSharedNavigationItem\\(\\) has parameter \\$config with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:addItem\\(\\) expects Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\|string, int\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function substr expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Parameter \\#1 \\$user of method Icinga\\\\Web\\\\Request\\:\\:setUser\\(\\) expects Icinga\\\\User, Icinga\\\\User\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Web\\:\\:\\$accessibleMenuItems type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Web\\:\\:\\$session is never read, only written\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Web\\:\\:\\$user \\(Icinga\\\\User\\) does not accept Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Property Icinga\\\\Application\\\\Web\\:\\:\\$viewRenderer \\(Icinga\\\\Web\\\\View\\) does not accept Zend_Controller_Action_Helper_ViewRenderer\\.$#"
+ count: 1
+ path: library/Icinga/Application/Web.php
+
+ -
+ message: "#^Cannot use array destructuring on array\\<int, string\\>\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Application/webrouter.php
+
+ -
+ message: "#^Argument of an invalid type array\\<string\\>\\|null supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\ConfigObject\\:\\:getSection\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\ConfigObject\\:\\:hasSection\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Cannot call method addChild\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Cannot call method getName\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\AdmissionLoader\\:\\:applyRoles\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\AdmissionLoader\\:\\:match\\(\\) has parameter \\$userGroups with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\AdmissionLoader\\:\\:migrateLegacyPermissions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\AdmissionLoader\\:\\:migrateLegacyPermissions\\(\\) has parameter \\$permissions with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Authentication\\\\AdmissionLoader\\:\\:loadRole\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Parameter \\#1 \\$parent of method Icinga\\\\Authentication\\\\Role\\:\\:setParent\\(\\) expects Icinga\\\\Authentication\\\\Role, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Parameter \\#1 \\$restrictions of method Icinga\\\\Authentication\\\\Role\\:\\:setRestrictions\\(\\) expects array\\<string\\>, array\\<string\\>\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Parameter \\#1 \\$restrictions of method Icinga\\\\User\\:\\:setRestrictions\\(\\) expects array\\<string\\>, array\\<array\\<int, string\\>\\> given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Parameter \\#1 \\$state of method Icinga\\\\Authentication\\\\Role\\:\\:setIsUnrestricted\\(\\) expects bool, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of static method Icinga\\\\Util\\\\StringHelper\\:\\:trimSplit\\(\\) expects string, mixed given\\.$#"
+ count: 4
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Parameter \\#2 \\$section of method Icinga\\\\Authentication\\\\AdmissionLoader\\:\\:loadRole\\(\\) expects Icinga\\\\Data\\\\ConfigObject, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method Icinga\\\\User\\:\\:setAdditional\\(\\) expects string, array\\<int\\<0, max\\>, string\\> given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Parameter \\#3 \\$section of method Icinga\\\\Authentication\\\\AdmissionLoader\\:\\:match\\(\\) expects Icinga\\\\Data\\\\ConfigObject, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\AdmissionLoader\\:\\:\\$roleConfig \\(Icinga\\\\Data\\\\ConfigObject\\) does not accept Icinga\\\\Application\\\\Config\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AdmissionLoader.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getRequest\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getResponse\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Cannot call method can\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Cannot call method getExternalUserInformation\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Cannot call method getGroups\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Cannot call method getRestrictions\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Cannot call method isExternalUser\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\Auth\\:\\:authenticateFromSession\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\Auth\\:\\:challengeHttp\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\Auth\\:\\:getGroups\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\Auth\\:\\:getRestrictions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\Auth\\:\\:persistCurrentUser\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\Auth\\:\\:removeAuthorization\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\Auth\\:\\:setAuthenticated\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\Auth\\:\\:setAuthenticated\\(\\) has parameter \\$persist with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Parameter \\#1 \\$domain of method Icinga\\\\User\\:\\:setDomain\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Parameter \\#2 \\$locale of function setlocale expects array\\|string\\|null, int given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of function setcookie expects string, int\\<1, max\\> given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\Auth\\:\\:\\$user \\(Icinga\\\\User\\|null\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Auth.php
+
+ -
+ message: "#^Class Icinga\\\\Authentication\\\\AuthChain implements generic interface Iterator but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Authentication/AuthChain.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method Icinga\\\\User\\:\\:setAdditional\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AuthChain.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\AuthChain\\:\\:\\$currentBackend \\(Icinga\\\\Authentication\\\\User\\\\UserBackendInterface\\) does not accept null\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/AuthChain.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\Role\\:\\:getRestrictions\\(\\) should return array\\<string\\>\\|null but returns string\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Role.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\Role\\:\\:setRefusals\\(\\) has parameter \\$refusals with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/Role.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\RolesConfig\\:\\:initializeQueryColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/RolesConfig.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\RolesConfig\\:\\:\\$configs type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/RolesConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:initializeFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:insert\\(\\) has parameter \\$bind with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:insert\\(\\) has parameter \\$types with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:update\\(\\) has parameter \\$bind with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:update\\(\\) has parameter \\$types with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:insert\\(\\) expects string, array\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:update\\(\\) expects string, array\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:\\$blacklistedQueryColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:\\$conversionRules type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:\\$queryColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:\\$searchColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:\\$sortRules type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:\\$statementColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Return type \\(void\\) of method Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\:\\:insert\\(\\) should be compatible with return type \\(int\\) of method Icinga\\\\Repository\\\\DbRepository\\:\\:insert\\(\\)$#"
+ count: 1
+ path: library/Icinga/Authentication/User/DbUserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\ExternalBackend\\:\\:getRemoteUserInformation\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/ExternalBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\User\\\\ExternalBackend\\:\\:\\$stripUsernameRegexp \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/ExternalBackend.php
+
+ -
+ message: "#^Strict comparison using \\=\\=\\= between array\\|string\\|null and false will always evaluate to false\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/ExternalBackend.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:setBase\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/LdapUserBackend.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:setNativeFilter\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/LdapUserBackend.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:setUnfoldAttribute\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/LdapUserBackend.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:setUsePagedResults\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/LdapUserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\:\\:initializeConversionRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/LdapUserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\:\\:initializeFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/LdapUserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\:\\:initializeQueryColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/LdapUserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\:\\:initializeVirtualTables\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/LdapUserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\:\\:retrieveShadowExpire\\(\\) should return bool but returns null\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/LdapUserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\:\\:retrieveUserAccountControl\\(\\) should return bool but returns null\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/LdapUserBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\:\\:\\$blacklistedQueryColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/LdapUserBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\:\\:\\$searchColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/LdapUserBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\:\\:\\$sortRules type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/LdapUserBackend.php
+
+ -
+ message: "#^Cannot call method setName\\(\\) on Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\|Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\UserBackend\\:\\:assertBackendsExist\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\UserBackend\\:\\:create\\(\\) should return Icinga\\\\Authentication\\\\User\\\\UserBackendInterface but returns Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\|Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\UserBackend\\:\\:getCustomBackendConfigForms\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\UserBackend\\:\\:registerCustomUserBackends\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\User\\\\UserBackend\\:\\:setConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$baseDn of method Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\:\\:setBaseDn\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$domain of method Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\:\\:setDomain\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$ds of class Icinga\\\\Authentication\\\\User\\\\DbUserBackend constructor expects Icinga\\\\Data\\\\Db\\\\DbConnection, Icinga\\\\Data\\\\Selectable given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$ds of class Icinga\\\\Authentication\\\\User\\\\LdapUserBackend constructor expects Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection, Icinga\\\\Data\\\\Selectable given\\.$#"
+ count: 2
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$filter of method Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\:\\:setFilter\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Authentication\\\\User\\\\ExternalBackend\\:\\:setName\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Authentication\\\\User\\\\UserBackendInterface\\:\\:setName\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$userClass of method Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\:\\:setUserClass\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$userNameAttribute of method Icinga\\\\Authentication\\\\User\\\\LdapUserBackend\\:\\:setUserNameAttribute\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\User\\\\UserBackend\\:\\:\\$customBackends type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\User\\\\UserBackend\\:\\:\\$defaultBackends type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Static call to instance method stdClass\\:\\:getConfigurationFormClass\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/User/UserBackend.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:join\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:joinLeft\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Cannot access property \\$group_name on mixed\\.$#"
+ count: 3
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Cannot access property \\$parent_name on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:getMemberships\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:initializeFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:insert\\(\\) has parameter \\$bind with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:insert\\(\\) has parameter \\$types with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:joinGroup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:joinGroupMembership\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:persistGroupId\\(\\) has parameter \\$groupName with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:persistGroupId\\(\\) should return int but returns array\\.$#"
+ count: 2
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:persistGroupId\\(\\) should return int but returns array\\|string\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:persistGroupId\\(\\) should return int but returns string\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:update\\(\\) has parameter \\$bind with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:update\\(\\) has parameter \\$types with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#2 \\$filter of static method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:where\\(\\) expects string, array given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:\\$blacklistedQueryColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:\\$conversionRules type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:\\$queryColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:\\$searchColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:\\$statementColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\:\\:\\$tableAliases type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:setBase\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:setNativeFilter\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:setUnfoldAttribute\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Selectable\\:\\:getHostname\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Selectable\\:\\:getPort\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:getMemberships\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:initializeConversionRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:initializeFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:initializeQueryColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:initializeVirtualTables\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:retrieveUserName\\(\\) has parameter \\$dn with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^PHPDoc tag @param references unknown parameter\\: \\$username$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$baseDn of method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:setGroupBaseDn\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$baseDn of method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:setUserBaseDn\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$domain of method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:setDomain\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$filter of method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:setGroupFilter\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$filter of method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:setUserFilter\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$groupClass of method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:setGroupClass\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$groupMemberAttribute of method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:setGroupMemberAttribute\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$groupNameAttribute of method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:setGroupNameAttribute\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of static method Icinga\\\\Authentication\\\\User\\\\UserBackend\\:\\:create\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$userClass of method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:setUserClass\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$userNameAttribute of method Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:setUserNameAttribute\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:\\$blacklistedQueryColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:\\$searchColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\:\\:\\$sortRules type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
+
+ -
+ message: "#^Cannot call method setName\\(\\) on Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\|Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/UserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackend\\:\\:create\\(\\) should return Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackendInterface but returns Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend\\|Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/UserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackend\\:\\:getCustomBackendConfigForms\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/UserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackend\\:\\:registerCustomUserGroupBackends\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/UserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$ds of class Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend constructor expects Icinga\\\\Data\\\\Db\\\\DbConnection, Icinga\\\\Data\\\\Selectable given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/UserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$ds of class Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend constructor expects Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection, Icinga\\\\Data\\\\Selectable given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/UserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackendInterface\\:\\:setName\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/UserGroupBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/UserGroupBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackend\\:\\:\\$customBackends type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/UserGroupBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackend\\:\\:\\$defaultBackends type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/UserGroupBackend.php
+
+ -
+ message: "#^Static call to instance method stdClass\\:\\:getConfigurationFormClass\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/UserGroupBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackendInterface\\:\\:getMemberships\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Axis\\:\\:addDataset\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Axis\\:\\:addDataset\\(\\) has parameter \\$dataset with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Axis\\:\\:getRequiredPadding\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Axis\\:\\:labelsOversized\\(\\) has parameter \\$maxLength with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Axis\\:\\:renderHorizontalAxis\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Axis\\:\\:renderVerticalAxis\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Axis\\:\\:ticksPerX\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Axis\\:\\:ticksPerX\\(\\) has parameter \\$min with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Axis\\:\\:ticksPerX\\(\\) has parameter \\$ticks with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Axis\\:\\:ticksPerX\\(\\) has parameter \\$units with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Axis\\:\\:transform\\(\\) has parameter \\$dataSet with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Axis\\:\\:transform\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strlen expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of class Icinga\\\\Chart\\\\Primitive\\\\Text constructor expects int, float given\\.$#"
+ count: 2
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Parameter \\#1 \\$x1 of class Icinga\\\\Chart\\\\Primitive\\\\Line constructor expects int, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of class Icinga\\\\Chart\\\\Primitive\\\\Text constructor expects int, float given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Parameter \\#3 \\$text of class Icinga\\\\Chart\\\\Primitive\\\\Text constructor expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Parameter \\#3 \\$x2 of class Icinga\\\\Chart\\\\Primitive\\\\Line constructor expects int, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Axis\\:\\:\\$labelRotationStyle has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Axis.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Chart\\:\\:alignTopLeft\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Chart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Chart\\:\\:build\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Chart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Chart\\:\\:disableLegend\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Chart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Chart\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Chart.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Chart\\:\\:\\$align has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Chart.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Chart\\:\\:\\$legend \\(Icinga\\\\Chart\\\\Legend\\) does not accept null\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Chart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Donut\\:\\:addSlice\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Donut.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Donut\\:\\:assemble\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Donut.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Donut\\:\\:encode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Donut.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Donut\\:\\:encode\\(\\) has parameter \\$content with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Donut.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Donut\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Donut.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Donut\\:\\:renderAttributes\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Donut.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Donut\\:\\:renderAttributes\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Donut.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Donut\\:\\:renderContent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Donut.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Donut\\:\\:renderContent\\(\\) has parameter \\$element with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Donut.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Donut\\:\\:shortenLabel\\(\\) should return string but returns int\\|string\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Donut.php
+
+ -
+ message: "#^Parameter \\#1 \\$num of function round expects float\\|int, int\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Donut.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strlen expects string, int\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Donut.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Donut\\:\\:\\$slices type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Donut.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Format\\:\\:formatSVGNumber\\(\\) has parameter \\$number with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Format.php
+
+ -
+ message: "#^Argument of an invalid type array\\|null supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/BarGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\BarGraph\\:\\:__construct\\(\\) has parameter \\$dataSet with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/BarGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\BarGraph\\:\\:__construct\\(\\) has parameter \\$graphs with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/BarGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\BarGraph\\:\\:__construct\\(\\) has parameter \\$tooltips with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/BarGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\BarGraph\\:\\:drawSingleBar\\(\\) has parameter \\$point with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/BarGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\BarGraph\\:\\:drawSingleBar\\(\\) has parameter \\$strokeWidth with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/BarGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\BarGraph\\:\\:setStyleFromConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/BarGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\BarGraph\\:\\:setStyleFromConfig\\(\\) has parameter \\$cfg with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/BarGraph.php
+
+ -
+ message: "#^PHPDoc tag @var has invalid value \\(\\)\\: Unexpected token \"\\\\n \", expected type at offset 40$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/BarGraph.php
+
+ -
+ message: "#^PHPDoc tag @var has invalid value \\(\\)\\: Unexpected token \"\\\\n \", expected type at offset 42$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/BarGraph.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Graph\\\\BarGraph\\:\\:\\$dataSet type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/BarGraph.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Graph\\\\BarGraph\\:\\:\\$graphs has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/BarGraph.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Graph\\\\BarGraph\\:\\:\\$tooltips has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/BarGraph.php
+
+ -
+ message: "#^Argument of an invalid type array\\|null supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/LineGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\LineGraph\\:\\:__construct\\(\\) has parameter \\$dataset with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/LineGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\LineGraph\\:\\:__construct\\(\\) has parameter \\$graphs with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/LineGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\LineGraph\\:\\:__construct\\(\\) has parameter \\$order with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/LineGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\LineGraph\\:\\:__construct\\(\\) has parameter \\$tooltips with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/LineGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\LineGraph\\:\\:setShowDataPoints\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/LineGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\LineGraph\\:\\:setStyleFromConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/LineGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\LineGraph\\:\\:setStyleFromConfig\\(\\) has parameter \\$cfg with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/LineGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\LineGraph\\:\\:sortByX\\(\\) has parameter \\$v1 with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/LineGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\LineGraph\\:\\:sortByX\\(\\) has parameter \\$v2 with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/LineGraph.php
+
+ -
+ message: "#^PHPDoc tag @var has invalid value \\(\\)\\: Unexpected token \"\\\\n \", expected type at offset 42$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/LineGraph.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Graph\\\\LineGraph\\:\\:\\$dataset type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/LineGraph.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Graph\\\\LineGraph\\:\\:\\$graphs type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/LineGraph.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Graph\\\\LineGraph\\:\\:\\$tooltips has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/LineGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\StackedGraph\\:\\:addGraph\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/StackedGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\StackedGraph\\:\\:addGraph\\(\\) has parameter \\$subGraph with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/StackedGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\StackedGraph\\:\\:addToStack\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/StackedGraph.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\StackedGraph\\:\\:addToStack\\(\\) has parameter \\$graph with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/StackedGraph.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Graph\\\\StackedGraph\\:\\:\\$points type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/StackedGraph.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Graph\\\\StackedGraph\\:\\:\\$stack type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/StackedGraph.php
+
+ -
+ message: "#^Invalid array key type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/Tooltip.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\Tooltip\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/Tooltip.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\Tooltip\\:\\:addDataPoint\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/Tooltip.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\Tooltip\\:\\:addDataPoint\\(\\) has parameter \\$point with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/Tooltip.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\Tooltip\\:\\:render\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/Tooltip.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\Tooltip\\:\\:render\\(\\) has parameter \\$order with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/Tooltip.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\Tooltip\\:\\:renderNoHtml\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/Tooltip.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Graph\\\\Tooltip\\:\\:renderNoHtml\\(\\) has parameter \\$order with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/Tooltip.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strip_tags expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/Tooltip.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Graph\\\\Tooltip\\:\\:\\$data type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/Tooltip.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Graph\\\\Tooltip\\:\\:\\$points type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Graph/Tooltip.php
+
+ -
+ message: "#^Cannot access offset mixed on Icinga\\\\Chart\\\\Graph\\\\Tooltip\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Cannot call method addDataPoint\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Cannot call method setStyleFromConfig\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\GridChart\\:\\:build\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\GridChart\\:\\:configureAxisFromDatasets\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\GridChart\\:\\:createContentClipBox\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\GridChart\\:\\:draw\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\GridChart\\:\\:draw\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\GridChart\\:\\:drawBars\\(\\) has parameter \\$axis with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\GridChart\\:\\:drawLines\\(\\) has parameter \\$axis with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\GridChart\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\GridChart\\:\\:initTooltips\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\GridChart\\:\\:initTooltips\\(\\) has parameter \\$data with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\GridChart\\:\\:renderGraphContent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\GridChart\\:\\:setupGraph\\(\\) has parameter \\$graphConfig with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Parameter \\#1 \\$child of method Icinga\\\\Chart\\\\Primitive\\\\Canvas\\:\\:addElement\\(\\) expects Icinga\\\\Chart\\\\Primitive\\\\Drawable, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of class Icinga\\\\Chart\\\\Primitive\\\\Rect constructor expects int, float given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of function array_key_exists expects array, Icinga\\\\Chart\\\\Graph\\\\Tooltip given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Parameter \\#4 \\$height of class Icinga\\\\Chart\\\\Primitive\\\\Rect constructor expects int, float given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Parameter \\#4 \\$tooltips of class Icinga\\\\Chart\\\\Graph\\\\BarGraph constructor expects array\\|null, Icinga\\\\Chart\\\\Graph\\\\Tooltip given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Parameter \\#4 \\$tooltips of class Icinga\\\\Chart\\\\Graph\\\\LineGraph constructor expects array\\|null, Icinga\\\\Chart\\\\Graph\\\\Tooltip given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\GridChart\\:\\:\\$axis type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\GridChart\\:\\:\\$graphs type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\GridChart\\:\\:\\$stacks type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\GridChart\\:\\:\\$tooltips \\(Icinga\\\\Chart\\\\Graph\\\\Tooltip\\) does not accept default value of type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/GridChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Inline\\\\Inline\\:\\:initFromRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Inline/Inline.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Inline\\\\Inline\\:\\:sanitizeStringArray\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Inline/Inline.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Inline\\\\Inline\\:\\:sanitizeStringArray\\(\\) has parameter \\$arr with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Inline/Inline.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Inline\\\\Inline\\:\\:\\$colors type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Inline/Inline.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Inline\\\\Inline\\:\\:\\$data type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Inline/Inline.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Inline\\\\Inline\\:\\:\\$labels type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Inline/Inline.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Inline\\\\PieChart\\:\\:getChart\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Inline/PieChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Inline\\\\PieChart\\:\\:toPng\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Inline/PieChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Inline\\\\PieChart\\:\\:toPng\\(\\) has parameter \\$output with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Inline/PieChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Inline\\\\PieChart\\:\\:toSvg\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Inline/PieChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Inline\\\\PieChart\\:\\:toSvg\\(\\) has parameter \\$output with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Inline/PieChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Legend\\:\\:addDataset\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Legend.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Legend\\:\\:addDataset\\(\\) has parameter \\$dataset with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Legend.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of class Icinga\\\\Chart\\\\Primitive\\\\Rect constructor expects int, float\\|int given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Legend.php
+
+ -
+ message: "#^Parameter \\#2 \\$y of class Icinga\\\\Chart\\\\Primitive\\\\Text constructor expects int, float\\|int given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Legend.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Legend\\:\\:\\$dataset type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Legend.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Palette\\:\\:\\$colorSets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Palette.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\PieChart\\:\\:build\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\PieChart\\:\\:createContentClipBox\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\PieChart\\:\\:drawPie\\(\\) has parameter \\$dataSet with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\PieChart\\:\\:getColorForPieSlice\\(\\) has parameter \\$pie with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\PieChart\\:\\:normalizeDataSet\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\PieChart\\:\\:normalizeDataSet\\(\\) has parameter \\$pie with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\PieChart\\:\\:renderPieRow\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\PieChart\\:\\:renderPies\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\PieChart\\:\\:renderStackedPie\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Parameter \\#1 \\$dataset of method Icinga\\\\Chart\\\\Legend\\:\\:addDataset\\(\\) expects array, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Parameter \\#1 \\$offset of method Icinga\\\\Chart\\\\Primitive\\\\PieSlice\\:\\:setCaptionOffset\\(\\) expects int, float\\|int\\<min, 39\\> given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Parameter \\#1 \\$radius of class Icinga\\\\Chart\\\\Primitive\\\\PieSlice constructor expects int, float\\|int\\<1, 50\\> given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Parameter \\#1 \\$radius of class Icinga\\\\Chart\\\\Primitive\\\\PieSlice constructor expects int, float\\|int\\<min, 40\\> given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of class Icinga\\\\Chart\\\\Primitive\\\\Rect constructor expects int, float given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of class Icinga\\\\Chart\\\\Render\\\\LayoutBox constructor expects int, float given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of method Icinga\\\\Chart\\\\Primitive\\\\PieSlice\\:\\:setX\\(\\) expects int, float\\|int\\<1, max\\> given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Parameter \\#4 \\$height of class Icinga\\\\Chart\\\\Primitive\\\\Rect constructor expects int, float given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\PieChart\\:\\:\\$pies type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/PieChart.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Primitive\\\\Animatable\\:\\:appendAnimation\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Animatable.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Primitive\\\\Animatable\\:\\:setAnimation\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Animatable.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method DOMElement\\:\\:setAttribute\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Chart/Primitive/Animation.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Primitive\\\\Canvas\\:\\:addElement\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Canvas.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Primitive\\\\Canvas\\:\\:setAriaRole\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Canvas.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Primitive\\\\Canvas\\:\\:setAriaRole\\(\\) has parameter \\$role with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Canvas.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Primitive\\\\Canvas\\:\\:toClipPath\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Canvas.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method DOMElement\\:\\:setAttribute\\(\\) expects string, int given\\.$#"
+ count: 2
+ path: library/Icinga/Chart/Primitive/Canvas.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Primitive\\\\Canvas\\:\\:\\$ariaRole \\(string\\) in isset\\(\\) is not nullable\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Canvas.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Primitive\\\\Canvas\\:\\:\\$children type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Canvas.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method DOMElement\\:\\:setAttribute\\(\\) expects string, int given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Circle.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Primitive\\\\Path\\:\\:__construct\\(\\) has parameter \\$points with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Path.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Primitive\\\\Path\\:\\:append\\(\\) has parameter \\$points with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Path.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Primitive\\\\Path\\:\\:prepend\\(\\) has parameter \\$points with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Path.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Primitive\\\\Path\\:\\:\\$points type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Path.php
+
+ -
+ message: "#^Parameter \\#1 \\$x of class Icinga\\\\Chart\\\\Primitive\\\\Text constructor expects int, float given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/PieSlice.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Primitive\\\\Rect\\:\\:keepRatio\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Rect.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Primitive\\\\Styleable\\:\\:applyAttributes\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Styleable.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Primitive\\\\Styleable\\:\\:setAttribute\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Styleable.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Primitive\\\\Styleable\\:\\:setAttribute\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Styleable.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Primitive\\\\Styleable\\:\\:setAttribute\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Styleable.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Primitive\\\\Styleable\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Styleable.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Primitive\\\\Text\\:\\:\\$fontStretch has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Primitive/Text.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Render\\\\LayoutBox\\:\\:getPadding\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Render/LayoutBox.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Render\\\\LayoutBox\\:\\:setPadding\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Render/LayoutBox.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Render\\\\LayoutBox\\:\\:setUniformPadding\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Render/LayoutBox.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Render\\\\LayoutBox\\:\\:\\$padding type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Render/LayoutBox.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Render\\\\RenderContext\\:\\:ignoreRatio\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Render/RenderContext.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Render\\\\RenderContext\\:\\:keepRatio\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Render/RenderContext.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Render\\\\RenderContext\\:\\:paddingToScaleFactor\\(\\) has parameter \\$padding with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Render/RenderContext.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Render\\\\RenderContext\\:\\:paddingToScaleFactor\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Render/RenderContext.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Render\\\\RenderContext\\:\\:toAbsolute\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Render/RenderContext.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Render\\\\RenderContext\\:\\:toRelative\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Render/RenderContext.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Render\\\\RenderContext\\:\\:xToAbsolute\\(\\) should return int but returns float\\|int\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Render/RenderContext.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Render\\\\RenderContext\\:\\:yToAbsolute\\(\\) should return int but returns float\\|int\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Render/RenderContext.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Render\\\\RenderContext\\:\\:\\$viewBoxSize type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Render/RenderContext.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Render\\\\Rotator\\:\\:rotate\\(\\) has parameter \\$degrees with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Render/Rotator.php
+
+ -
+ message: "#^Call to an undefined method DOMNode\\:\\:setAttribute\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Cannot call method createElement\\(\\) on DOMDocument\\|null\\.$#"
+ count: 2
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Cannot call method createTextNode\\(\\) on DOMDocument\\|null\\.$#"
+ count: 2
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:addAriaDescription\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:addAriaDescription\\(\\) has parameter \\$descriptionText with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:addAriaDescription\\(\\) has parameter \\$titleText with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:createRootDocument\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:preserveAspectRatio\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:render\\(\\) should return string but returns string\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:setAriaDescription\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:setAriaDescription\\(\\) has parameter \\$text with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:setAriaRole\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:setAriaRole\\(\\) has parameter \\$text with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:setAriaTitle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:setAriaTitle\\(\\) has parameter \\$text with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:setXAspectRatioAlignment\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:setXAspectRatioAlignment\\(\\) has parameter \\$alignment with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:setYAspectRatioAlignment\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:setYAspectRatioAlignment\\(\\) has parameter \\$alignment with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:stripNonAlphanumeric\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\SVGRenderer\\:\\:stripNonAlphanumeric\\(\\) has parameter \\$str with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\SVGRenderer\\:\\:\\$ariaDescription \\(string\\) in isset\\(\\) is not nullable\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\SVGRenderer\\:\\:\\$ariaTitle \\(string\\) in isset\\(\\) is not nullable\\.$#"
+ count: 1
+ path: library/Icinga/Chart/SVGRenderer.php
+
+ -
+ message: "#^Interface Icinga\\\\Chart\\\\Unit\\\\AxisUnit extends generic interface Iterator but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/AxisUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\AxisUnit\\:\\:addValues\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/AxisUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\AxisUnit\\:\\:addValues\\(\\) has parameter \\$dataset with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/AxisUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\AxisUnit\\:\\:setMax\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/AxisUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\AxisUnit\\:\\:setMin\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/AxisUnit.php
+
+ -
+ message: "#^Binary operation \"/\" between int\\|string\\|null and int\\<0, max\\> results in an error\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/CalendarUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\CalendarUnit\\:\\:addValues\\(\\) has parameter \\$dataset with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/CalendarUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\CalendarUnit\\:\\:calculateLabels\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/CalendarUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\CalendarUnit\\:\\:createLabels\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/CalendarUnit.php
+
+ -
+ message: "#^Parameter \\#1 \\$timestamp of method DateTime\\:\\:setTimestamp\\(\\) expects int, float\\|int given\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/CalendarUnit.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Unit\\\\CalendarUnit\\:\\:\\$labels type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/CalendarUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\LinearUnit\\:\\:addValues\\(\\) has parameter \\$dataset with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LinearUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\LinearUnit\\:\\:setMax\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LinearUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\LinearUnit\\:\\:setMin\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LinearUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\LogarithmicUnit\\:\\:__construct\\(\\) has parameter \\$base with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\LogarithmicUnit\\:\\:addValues\\(\\) has parameter \\$dataset with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\LogarithmicUnit\\:\\:getMax\\(\\) should return int but returns float\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\LogarithmicUnit\\:\\:getMin\\(\\) should return int but returns float\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\LogarithmicUnit\\:\\:log\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\LogarithmicUnit\\:\\:logCeil\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\LogarithmicUnit\\:\\:logFloor\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\LogarithmicUnit\\:\\:pow\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\LogarithmicUnit\\:\\:setMax\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\LogarithmicUnit\\:\\:setMin\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^PHPDoc tag @param references unknown parameter\\: \\$nrOfTicks$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^PHPDoc tag @var has invalid value \\(\\)\\: Unexpected token \"\\\\n \", expected type at offset 15$#"
+ count: 3
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Unit\\\\LogarithmicUnit\\:\\:\\$currentTick has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Unit\\\\LogarithmicUnit\\:\\:\\$maxExp has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Unit\\\\LogarithmicUnit\\:\\:\\$minExp has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/LogarithmicUnit.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\StaticAxis\\:\\:addValues\\(\\) has parameter \\$dataset with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/StaticAxis.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\StaticAxis\\:\\:setMax\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/StaticAxis.php
+
+ -
+ message: "#^Method Icinga\\\\Chart\\\\Unit\\\\StaticAxis\\:\\:setMin\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/StaticAxis.php
+
+ -
+ message: "#^Property Icinga\\\\Chart\\\\Unit\\\\StaticAxis\\:\\:\\$items has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Chart/Unit/StaticAxis.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:bgColor\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:bgColor\\(\\) has parameter \\$color with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:clear\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:colorize\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:colorize\\(\\) has parameter \\$bgColor with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:colorize\\(\\) has parameter \\$fgColor with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:colorize\\(\\) has parameter \\$text with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:fgColor\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:fgColor\\(\\) has parameter \\$color with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:startColor\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:startColor\\(\\) has parameter \\$bgColor with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:startColor\\(\\) has parameter \\$fgColor with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:stripAnsiCodes\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:stripAnsiCodes\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:strlen\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:strlen\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:underline\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\AnsiScreen\\:\\:underline\\(\\) has parameter \\$text with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\AnsiScreen\\:\\:\\$bgColors has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\AnsiScreen\\:\\:\\$fgColors has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/AnsiScreen.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getParams\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:Config\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:Config\\(\\) has parameter \\$file with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:__construct\\(\\) has parameter \\$actionName with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:__construct\\(\\) has parameter \\$commandName with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:__construct\\(\\) has parameter \\$initialize with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:__construct\\(\\) has parameter \\$moduleName with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:docs\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:fail\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:fail\\(\\) has parameter \\$msg with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:getDefaultActionName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:getMainConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:getMainConfig\\(\\) has parameter \\$file with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:getModuleConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:getModuleConfig\\(\\) has parameter \\$file with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:hasActionName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:hasActionName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:hasDefaultActionName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:hasRemainingParams\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:isModule\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:listActions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:setParams\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:showTrace\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:showUsage\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Command\\:\\:showUsage\\(\\) has parameter \\$action with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Command\\:\\:\\$actionName has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Command\\:\\:\\$app has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Command\\:\\:\\$commandName has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Command\\:\\:\\$config has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Command\\:\\:\\$configs has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Command\\:\\:\\$defaultActionName has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Command\\:\\:\\$docs has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Command\\:\\:\\$moduleName has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Command\\:\\:\\$screen has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Unreachable statement \\- code above always terminates\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Command.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:cliLoader\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:commandUsage\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:commandUsage\\(\\) has parameter \\$action with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:commandUsage\\(\\) has parameter \\$command with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:getClassDocumentation\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:getClassDocumentation\\(\\) has parameter \\$class with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:getClassTitle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:getClassTitle\\(\\) has parameter \\$class with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:getMethodDocumentation\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:getMethodDocumentation\\(\\) has parameter \\$class with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:getMethodDocumentation\\(\\) has parameter \\$method with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:getMethodTitle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:getMethodTitle\\(\\) has parameter \\$class with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:getMethodTitle\\(\\) has parameter \\$method with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:globalUsage\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:moduleUsage\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:moduleUsage\\(\\) has parameter \\$action with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:moduleUsage\\(\\) has parameter \\$command with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:moduleUsage\\(\\) has parameter \\$module with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:usage\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:usage\\(\\) has parameter \\$action with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:usage\\(\\) has parameter \\$command with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\:\\:usage\\(\\) has parameter \\$module with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Documentation\\:\\:\\$app has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Documentation\\:\\:\\$icinga has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Documentation\\:\\:\\$loader has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation.php
+
+ -
+ message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation/CommentParser.php
+
+ -
+ message: "#^Cannot access an offset on array\\|float\\|int\\|string\\|false\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation/CommentParser.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\\\CommentParser\\:\\:__construct\\(\\) has parameter \\$raw with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation/CommentParser.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\\\CommentParser\\:\\:dump\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation/CommentParser.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\\\CommentParser\\:\\:getTitle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation/CommentParser.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Documentation\\\\CommentParser\\:\\:parse\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation/CommentParser.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function rtrim expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation/CommentParser.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_split expects string, array\\<int, string\\>\\|string\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation/CommentParser.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Documentation\\\\CommentParser\\:\\:\\$paragraphs has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation/CommentParser.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Documentation\\\\CommentParser\\:\\:\\$plain has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation/CommentParser.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Documentation\\\\CommentParser\\:\\:\\$raw has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation/CommentParser.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Documentation\\\\CommentParser\\:\\:\\$title has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Documentation/CommentParser.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:assertCommandExists\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:assertCommandExists\\(\\) has parameter \\$command with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:assertModuleCommandExists\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:assertModuleCommandExists\\(\\) has parameter \\$command with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:assertModuleCommandExists\\(\\) has parameter \\$module with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:assertModuleExists\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:assertModuleExists\\(\\) has parameter \\$module with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:dispatch\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:fail\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:formatTrace\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:formatTrace\\(\\) has parameter \\$trace with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:getActionName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:getCommandInstance\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:getCommandInstance\\(\\) has parameter \\$command with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:getCommandName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:getLastSuggestions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:getModuleCommandInstance\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:getModuleCommandInstance\\(\\) has parameter \\$command with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:getModuleCommandInstance\\(\\) has parameter \\$module with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:getModuleName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:handleParams\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:hasCommand\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:hasCommand\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:hasModule\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:hasModule\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:hasModuleCommand\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:hasModuleCommand\\(\\) has parameter \\$module with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:hasModuleCommand\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:listCommands\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:listModuleCommands\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:listModuleCommands\\(\\) has parameter \\$module with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:listModules\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:parseParams\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:resolveCommandName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:resolveCommandName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:resolveModuleCommandName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:resolveModuleCommandName\\(\\) has parameter \\$module with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:resolveModuleCommandName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:resolveModuleName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:resolveModuleName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:resolveName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:resolveName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:resolveObjectActionName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:resolveObjectActionName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:resolveObjectActionName\\(\\) has parameter \\$obj with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:retrieveCommandsFromDir\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:retrieveCommandsFromDir\\(\\) has parameter \\$dirname with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:searchMatch\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:searchMatch\\(\\) has parameter \\$haystack with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:searchMatch\\(\\) has parameter \\$needle with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:setModuleName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:setModuleName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Loader\\:\\:showLastSuggestions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_values expects array, array\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strlen expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, array\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Parameter \\#2 \\$return of function var_export expects bool, int given\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$actionName has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$app has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$commandClassMap has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$commandFileMap has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$commandInstances has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$commandName has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$commands has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$coreAppDir has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$docs has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$lastSuggestions has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$moduleClassMap has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$moduleCommands has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$moduleFileMap has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$moduleInstances has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$moduleName has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$modules has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Loader\\:\\:\\$screen has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Loader.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Params\\:\\:__construct\\(\\) has parameter \\$argv with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Params.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Params\\:\\:__get\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Params.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Params\\:\\:__get\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Params.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Params\\:\\:__isset\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Params.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Params\\:\\:getAllStandalone\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Params.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Params\\:\\:getParams\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Params.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Params\\:\\:parse\\(\\) has parameter \\$argv with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Params.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Params\\:\\:remove\\(\\) has parameter \\$keys with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Params.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Params\\:\\:without\\(\\) has parameter \\$keys with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Params.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strlen expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Cli/Params.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Params\\:\\:\\$params type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Params.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Params\\:\\:\\$standalone type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Params.php
+
+ -
+ message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Cannot use array destructuring on array\\<int, string\\>\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:center\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:center\\(\\) has parameter \\$txt with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:clear\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:colorize\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:colorize\\(\\) has parameter \\$bgColor with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:colorize\\(\\) has parameter \\$fgColor with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:colorize\\(\\) has parameter \\$text with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:getColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:getRows\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:hasUtf8\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:instance\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:instance\\(\\) has parameter \\$output with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:newlines\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:newlines\\(\\) has parameter \\$count with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:strlen\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:strlen\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:underline\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Method Icinga\\\\Cli\\\\Screen\\:\\:underline\\(\\) has parameter \\$text with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Parameter \\#2 \\$locale of function setlocale expects array\\|string\\|null, int given\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_split expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Parameter \\#2 \\$times of function str_repeat expects int, float given\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Screen\\:\\:\\$instances has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Property Icinga\\\\Cli\\\\Screen\\:\\:\\$isUtf8 has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Cli/Screen.php
+
+ -
+ message: "#^Constant Icinga\\\\Crypt\\\\AesCrypt\\:\\:METHODS type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Crypt/AesCrypt.php
+
+ -
+ message: "#^Method Icinga\\\\Crypt\\\\AesCrypt\\:\\:__construct\\(\\) has parameter \\$keyLength with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Crypt/AesCrypt.php
+
+ -
+ message: "#^Method Icinga\\\\Crypt\\\\AesCrypt\\:\\:setIV\\(\\) has parameter \\$iv with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Crypt/AesCrypt.php
+
+ -
+ message: "#^Method Icinga\\\\Crypt\\\\AesCrypt\\:\\:setKey\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Crypt/AesCrypt.php
+
+ -
+ message: "#^Method Icinga\\\\Crypt\\\\AesCrypt\\:\\:setMethod\\(\\) has parameter \\$method with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Crypt/AesCrypt.php
+
+ -
+ message: "#^Method Icinga\\\\Crypt\\\\AesCrypt\\:\\:setTag\\(\\) has parameter \\$tag with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Crypt/AesCrypt.php
+
+ -
+ message: "#^Parameter \\#1 \\$length of function random_bytes expects int\\<1, max\\>, int given\\.$#"
+ count: 1
+ path: library/Icinga/Crypt/AesCrypt.php
+
+ -
+ message: "#^Parameter \\#1 \\$length of function random_bytes expects int\\<1, max\\>, int\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Crypt/AesCrypt.php
+
+ -
+ message: "#^Class Icinga\\\\Data\\\\ConfigObject implements generic interface ArrayAccess but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Data/ConfigObject.php
+
+ -
+ message: "#^Class Icinga\\\\Data\\\\ConfigObject implements generic interface Iterator but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Data/ConfigObject.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\ConfigObject\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/ConfigObject.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\ConfigObject\\:\\:key\\(\\) should return string but returns int\\|string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Data/ConfigObject.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\ConfigObject\\:\\:keys\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/ConfigObject.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\ConfigObject\\:\\:merge\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/ConfigObject.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\ConfigObject\\:\\:merge\\(\\) has parameter \\$data with no value type specified in iterable type array\\|Icinga\\\\Data\\\\ConfigObject\\.$#"
+ count: 1
+ path: library/Icinga/Data/ConfigObject.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\ConfigObject\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/ConfigObject.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\DataArray\\\\ArrayDatasource\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/DataArray/ArrayDatasource.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\DataArray\\\\ArrayDatasource\\:\\:createResult\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/DataArray/ArrayDatasource.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\DataArray\\\\ArrayDatasource\\:\\:fetchAll\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/DataArray/ArrayDatasource.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\DataArray\\\\ArrayDatasource\\:\\:fetchColumn\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/DataArray/ArrayDatasource.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\DataArray\\\\ArrayDatasource\\:\\:fetchPairs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/DataArray/ArrayDatasource.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\DataArray\\\\ArrayDatasource\\:\\:getResult\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/DataArray/ArrayDatasource.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\DataArray\\\\ArrayDatasource\\:\\:query\\(\\) return type with generic class ArrayIterator does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Data/DataArray/ArrayDatasource.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\DataArray\\\\ArrayDatasource\\:\\:setResult\\(\\) has parameter \\$result with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/DataArray/ArrayDatasource.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\DataArray\\\\ArrayDatasource\\:\\:\\$data type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/DataArray/ArrayDatasource.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\DataArray\\\\ArrayDatasource\\:\\:\\$result type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/DataArray/ArrayDatasource.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getColumn\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getExpression\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getSign\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:connect\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:fetchAll\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:fetchAll\\(\\) should return array but returns array\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:fetchColumn\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:fetchOne\\(\\) should return string but returns string\\|false\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:fetchPairs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:fromResourceName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:fromResourceName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:insert\\(\\) has parameter \\$bind with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:insert\\(\\) has parameter \\$types with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:query\\(\\) should return Iterator but returns Zend_Db_Statement\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:renderFilter\\(\\) has parameter \\$level with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:update\\(\\) has parameter \\$bind with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:update\\(\\) has parameter \\$types with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function trim expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Parameter \\#3 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 3
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Parameter \\#4 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:\\$config \\(Icinga\\\\Data\\\\ConfigObject\\) does not accept Icinga\\\\Data\\\\ConfigObject\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:\\$driverOptions has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Db\\\\DbConnection\\:\\:\\$genericAdapterOptions has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbConnection.php
+
+ -
+ message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
+ count: 2
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:filters\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getColumn\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getExpression\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:setExpression\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Cannot access offset 'joinType' on mixed\\.$#"
+ count: 3
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Cannot access offset 'tableName' on mixed\\.$#"
+ count: 2
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Cannot access offset string on mixed\\.$#"
+ count: 2
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Cannot call method getDbAdapter\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Cannot call method getDbType\\(\\) on mixed\\.$#"
+ count: 2
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Cannot call method renderFilter\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:applyFilterSql\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:applyFilterSql\\(\\) has parameter \\$select with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:dbSelect\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:escapeForSql\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:escapeForSql\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:escapeWildcards\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:escapeWildcards\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:expressionsToTimestamp\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:from\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:getGroup\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:getIsSubQuery\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:getJoinedTableAlias\\(\\) should return string\\|null but empty return statement found\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:getJoinedTableAlias\\(\\) should return string\\|null but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:group\\(\\) has parameter \\$group with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:join\\(\\) has parameter \\$cols with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:join\\(\\) has parameter \\$name with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:joinCross\\(\\) has parameter \\$cols with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:joinCross\\(\\) has parameter \\$name with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:joinFull\\(\\) has parameter \\$cols with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:joinFull\\(\\) has parameter \\$name with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:joinInner\\(\\) has parameter \\$cols with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:joinInner\\(\\) has parameter \\$name with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:joinLeft\\(\\) has parameter \\$cols with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:joinLeft\\(\\) has parameter \\$name with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:joinNatural\\(\\) has parameter \\$cols with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:joinNatural\\(\\) has parameter \\$name with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:joinRight\\(\\) has parameter \\$cols with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:joinRight\\(\\) has parameter \\$name with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:setUseSubqueryCount\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:setUseSubqueryCount\\(\\) has parameter \\$useSubqueryCount with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:union\\(\\) has parameter \\$select with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:valueToTimestamp\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:valueToTimestamp\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Zend_Db_Select\\:\\:from\\(\\) expects array\\|string\\|Zend_Db_Expr, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Db\\\\DbQuery\\:\\:\\$group type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Db/DbQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Extensible\\:\\:insert\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Extensible.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Extensible\\:\\:insert\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Extensible.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Fetchable\\:\\:fetchAll\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Fetchable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Fetchable\\:\\:fetchColumn\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Fetchable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Fetchable\\:\\:fetchPairs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Fetchable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:andFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:applyChanges\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:applyChanges\\(\\) has parameter \\$changes with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:chain\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:chain\\(\\) has parameter \\$filters with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:chain\\(\\) has parameter \\$operator with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:expression\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:expression\\(\\) has parameter \\$col with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:expression\\(\\) has parameter \\$expression with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:expression\\(\\) has parameter \\$op with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:fromQueryString\\(\\) has parameter \\$query with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getById\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getById\\(\\) has parameter \\$id with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getId\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getParent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getParentId\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getUrlParams\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:hasId\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:hasId\\(\\) has parameter \\$id with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:isChain\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:isEmpty\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:isExpression\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:isRootNode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:listFilteredColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:orFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:setId\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:setId\\(\\) has parameter \\$id with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:toQueryString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^PHPDoc tag @param references unknown parameter\\: \\$filter$#"
+ count: 3
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:\\$id has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/Filter.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterAnd\\:\\:andFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterAnd.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterAnd\\:\\:orFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterAnd.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterAnd\\:\\:\\$operatorName has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterAnd.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterAnd\\:\\:\\$operatorSymbol has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterAnd.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:filters\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getColumn\\(\\)\\.$#"
+ count: 3
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:__construct\\(\\) has parameter \\$filters with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:count\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:filters\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:getById\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:getById\\(\\) has parameter \\$id with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:getOperatorName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:getOperatorSymbol\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:hasId\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:hasId\\(\\) has parameter \\$id with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:isChain\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:isEmpty\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:isExpression\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:listFilteredColumns\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:listFilteredColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:refreshChildIds\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:removeId\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:removeId\\(\\) has parameter \\$id with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:replaceById\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:replaceById\\(\\) has parameter \\$filter with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:replaceById\\(\\) has parameter \\$id with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:setAllowedFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:setAllowedFilterColumns\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:setFilters\\(\\) has parameter \\$filters with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:setId\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:setId\\(\\) has parameter \\$id with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:setOperatorName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:setOperatorName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:toQueryString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:validateFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:\\$allowedColumns has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:\\$filters has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:\\$operatorName has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterChain\\:\\:\\$operatorSymbol has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterChain.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterEqualOrLessThan\\:\\:toQueryString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterEqualOrLessThan.php
+
+ -
+ message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Cannot cast mixed to string\\.$#"
+ count: 2
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Dead catch \\- Exception is never thrown in the try block\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:__construct\\(\\) has parameter \\$column with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:__construct\\(\\) has parameter \\$expression with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:__construct\\(\\) has parameter \\$sign with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:andFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:getColumn\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:getExpression\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:getSign\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:isBooleanTrue\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:isChain\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:isEmpty\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:isExpression\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:listFilteredColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:orFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:setColumn\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:setColumn\\(\\) has parameter \\$column with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:setExpression\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:setExpression\\(\\) has parameter \\$expression with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:setSign\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:setSign\\(\\) has parameter \\$sign with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:toQueryString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, bool\\|float\\|int\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_match expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:\\$column has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:\\$expression has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterExpression\\:\\:\\$sign has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterExpression.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterLessThan\\:\\:toQueryString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterLessThan.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterMatchCaseInsensitive\\:\\:__construct\\(\\) has parameter \\$column with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterMatchCaseInsensitive.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterMatchCaseInsensitive\\:\\:__construct\\(\\) has parameter \\$expression with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterMatchCaseInsensitive.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterMatchCaseInsensitive\\:\\:__construct\\(\\) has parameter \\$sign with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterMatchCaseInsensitive.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterMatchNotCaseInsensitive\\:\\:__construct\\(\\) has parameter \\$column with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterMatchNotCaseInsensitive.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterMatchNotCaseInsensitive\\:\\:__construct\\(\\) has parameter \\$expression with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterMatchNotCaseInsensitive.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterMatchNotCaseInsensitive\\:\\:__construct\\(\\) has parameter \\$sign with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterMatchNotCaseInsensitive.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterNot\\:\\:andFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterNot.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterNot\\:\\:orFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterNot.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterNot\\:\\:toQueryString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterNot.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterNot\\:\\:\\$operatorName has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterNot.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterNot\\:\\:\\$operatorSymbol has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterNot.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterOr\\:\\:andFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterOr.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterOr\\:\\:orFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterOr.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterOr\\:\\:setOperatorName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterOr.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterOr\\:\\:setOperatorName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterOr.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterOr\\:\\:\\$operatorName has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterOr.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterOr\\:\\:\\$operatorSymbol has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterOr.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:debug\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:debug\\(\\) has parameter \\$level with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:debug\\(\\) has parameter \\$msg with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:debug\\(\\) has parameter \\$op with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:nextChar\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:parse\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:parse\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:parseError\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:parseError\\(\\) has parameter \\$char with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:parseError\\(\\) has parameter \\$extraMsg with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:parseQueryString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:parseQueryString\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:readChar\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:readExpressionOperator\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:readFilters\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:readFilters\\(\\) has parameter \\$nestingLevel with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:readFilters\\(\\) has parameter \\$op with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:readNextExpression\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:readNextKey\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:readNextValue\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:readUnless\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:readUnless\\(\\) has parameter \\$char with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:readUnlessSpecialChar\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:\\$debug has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:\\$length has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:\\$pos has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:\\$reportDebug has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Filter\\\\FilterQueryString\\:\\:\\$string has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filter/FilterQueryString.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\FilterColumns\\:\\:getFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/FilterColumns.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\FilterColumns\\:\\:getSearchColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/FilterColumns.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filterable\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filterable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filterable\\:\\:applyFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filterable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filterable\\:\\:getFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filterable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filterable\\:\\:setFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filterable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filterable\\:\\:where\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filterable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filterable\\:\\:where\\(\\) has parameter \\$condition with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filterable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Filterable\\:\\:where\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Filterable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Inspection\\:\\:__construct\\(\\) has parameter \\$description with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Inspection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Inspection\\:\\:error\\(\\) has parameter \\$entry with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Inspection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Inspection\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Inspection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Inspection\\:\\:write\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Inspection.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Inspection\\:\\:write\\(\\) has parameter \\$entry with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Inspection.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Inspection\\:\\:\\$error \\(Icinga\\\\Data\\\\Inspection\\|string\\) in isset\\(\\) is not nullable\\.$#"
+ count: 3
+ path: library/Icinga/Data/Inspection.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Inspection\\:\\:\\$log type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Inspection.php
+
+ -
+ message: "#^Unreachable statement \\- code above always terminates\\.$#"
+ count: 2
+ path: library/Icinga/Data/Inspection.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getRequest\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Data/PivotTable.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\SimpleQuery\\:\\:clearGroupingRules\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Data/PivotTable.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\SimpleQuery\\:\\:group\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Data/PivotTable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\PivotTable\\:\\:getOrder\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/PivotTable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\PivotTable\\:\\:paginateXAxis\\(\\) return type has no value type specified in iterable type Zend_Paginator\\.$#"
+ count: 1
+ path: library/Icinga/Data/PivotTable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\PivotTable\\:\\:paginateYAxis\\(\\) return type has no value type specified in iterable type Zend_Paginator\\.$#"
+ count: 1
+ path: library/Icinga/Data/PivotTable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\PivotTable\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/PivotTable.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\PivotTable\\:\\:\\$order type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/PivotTable.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\PivotTable\\:\\:\\$xAxisFilter \\(Icinga\\\\Data\\\\Filter\\\\Filter\\) does not accept Icinga\\\\Data\\\\Filter\\\\Filter\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Data/PivotTable.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\PivotTable\\:\\:\\$yAxisFilter \\(Icinga\\\\Data\\\\Filter\\\\Filter\\) does not accept Icinga\\\\Data\\\\Filter\\\\Filter\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Data/PivotTable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Queryable\\:\\:from\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Queryable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Reducible\\:\\:delete\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Reducible.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\ResourceFactory\\:\\:assertResourcesExist\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/ResourceFactory.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\ResourceFactory\\:\\:create\\(\\) should return Icinga\\\\Data\\\\Db\\\\DbConnection\\|Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection but returns Icinga\\\\Data\\\\Selectable\\.$#"
+ count: 1
+ path: library/Icinga/Data/ResourceFactory.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\ResourceFactory\\:\\:getResourceConfig\\(\\) has parameter \\$resourceName with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/ResourceFactory.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\ResourceFactory\\:\\:setConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/ResourceFactory.php
+
+ -
+ message: "#^Parameter \\#1 \\$file of static method Icinga\\\\Application\\\\Config\\:\\:fromIni\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Data/ResourceFactory.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Data/ResourceFactory.php
+
+ -
+ message: "#^Cannot call method count\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Cannot call method fetchAll\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Cannot call method fetchColumn\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Cannot call method fetchOne\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Cannot call method fetchPairs\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Cannot call method fetchRow\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Cannot call method query\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Class Icinga\\\\Data\\\\SimpleQuery implements generic interface Iterator but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:__construct\\(\\) has parameter \\$columns with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:applyFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:columns\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:fetchAll\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:fetchColumn\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:fetchPairs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:from\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:getColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:getFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:getOrder\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:peekAhead\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:setFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:setOrderColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:setOrderColumns\\(\\) has parameter \\$orderColumns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SimpleQuery\\:\\:splitOrder\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\SimpleQuery\\:\\:\\$columns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\SimpleQuery\\:\\:\\$desiredColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\SimpleQuery\\:\\:\\$filter has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\SimpleQuery\\:\\:\\$flippedColumns \\(array\\) does not accept null\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\SimpleQuery\\:\\:\\$flippedColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\SimpleQuery\\:\\:\\$iterator \\(Iterator\\) does not accept Traversable\\<mixed, mixed\\>\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\SimpleQuery\\:\\:\\$iteratorPosition \\(int\\) does not accept null\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\SimpleQuery\\:\\:\\$limitCount \\(int\\) does not accept int\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\SimpleQuery\\:\\:\\$order type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/SimpleQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\SortRules\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/SortRules.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Sortable\\:\\:getOrder\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Sortable.php
+
+ -
+ message: "#^Class Icinga\\\\Data\\\\Tree\\\\SimpleTree implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Data/Tree/SimpleTree.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Data/Tree/SimpleTree.php
+
+ -
+ message: "#^Parameter \\#3 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Data/Tree/SimpleTree.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Tree\\\\SimpleTree\\:\\:\\$nodes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Tree/SimpleTree.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Tree\\\\TreeNode\\:\\:getChildren\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Tree/TreeNode.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Tree\\\\TreeNode\\:\\:\\$children type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Tree/TreeNode.php
+
+ -
+ message: "#^Class Icinga\\\\Data\\\\Tree\\\\TreeNodeIterator implements generic interface RecursiveIterator but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Data/Tree/TreeNodeIterator.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Tree\\\\TreeNodeIterator\\:\\:current\\(\\) should return Icinga\\\\Data\\\\Tree\\\\TreeNode but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Data/Tree/TreeNodeIterator.php
+
+ -
+ message: "#^Property Icinga\\\\Data\\\\Tree\\\\TreeNodeIterator\\:\\:\\$children with generic class ArrayIterator does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Data/Tree/TreeNodeIterator.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Updatable\\:\\:update\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Data/Updatable.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\Updatable\\:\\:update\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Data/Updatable.php
+
+ -
+ message: "#^Method Icinga\\\\Date\\\\DateFormatter\\:\\:diff\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Date/DateFormatter.php
+
+ -
+ message: "#^Method Icinga\\\\Exception\\\\Http\\\\BaseHttpException\\:\\:getHeaders\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Exception/Http/BaseHttpException.php
+
+ -
+ message: "#^Method Icinga\\\\Exception\\\\Http\\\\BaseHttpException\\:\\:setHeaders\\(\\) has parameter \\$headers with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Exception/Http/BaseHttpException.php
+
+ -
+ message: "#^Property Icinga\\\\Exception\\\\Http\\\\BaseHttpException\\:\\:\\$headers type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Exception/Http/BaseHttpException.php
+
+ -
+ message: "#^PHPDoc tag @param references unknown parameter\\: \\$arg$#"
+ count: 1
+ path: library/Icinga/Exception/Http/HttpException.php
+
+ -
+ message: "#^Parameter \\#1 \\$callback of function call_user_func_array expects callable\\(\\)\\: mixed, 'parent\\:\\:__construct' given\\.$#"
+ count: 1
+ path: library/Icinga/Exception/Http/HttpException.php
+
+ -
+ message: "#^Method Icinga\\\\Exception\\\\Http\\\\HttpExceptionInterface\\:\\:getHeaders\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Exception/Http/HttpExceptionInterface.php
+
+ -
+ message: "#^Method Icinga\\\\Exception\\\\IcingaException\\:\\:create\\(\\) has parameter \\$args with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Exception/IcingaException.php
+
+ -
+ message: "#^Offset 'line' does not exist on array\\{function\\: string, line\\?\\: int, file\\: string, class\\?\\: class\\-string, type\\?\\: '\\-\\>'\\|'\\:\\:', args\\?\\: array, object\\?\\: object\\}\\.$#"
+ count: 1
+ path: library/Icinga/Exception/IcingaException.php
+
+ -
+ message: "#^PHPDoc tag @param references unknown parameter\\: \\$arg$#"
+ count: 1
+ path: library/Icinga/Exception/IcingaException.php
+
+ -
+ message: "#^Parameter \\#1 \\$object of function get_class expects object, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Exception/IcingaException.php
+
+ -
+ message: "#^Parameter \\#2 \\$values of function vsprintf expects array\\<bool\\|float\\|int\\|string\\|null\\>, array\\<int, mixed\\> given\\.$#"
+ count: 1
+ path: library/Icinga/Exception/IcingaException.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Csv\\:\\:dump\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Csv.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Csv\\:\\:fromQuery\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Csv.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Csv\\:\\:fromQuery\\(\\) has parameter \\$query with no value type specified in iterable type Traversable\\.$#"
+ count: 1
+ path: library/Icinga/File/Csv.php
+
+ -
+ message: "#^Property Icinga\\\\File\\\\Csv\\:\\:\\$query has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Csv.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Comment\\:\\:setContent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Comment.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Comment\\:\\:setContent\\(\\) has parameter \\$content with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Comment.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Directive\\:\\:sanitizeKey\\(\\) has parameter \\$str with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Directive.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Directive\\:\\:sanitizeValue\\(\\) has parameter \\$str with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Directive.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Directive\\:\\:setCommentPost\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Directive.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Directive\\:\\:setCommentsPre\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Directive.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Directive\\:\\:setValue\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Directive.php
+
+ -
+ message: "#^Parameter \\#3 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Directive.php
+
+ -
+ message: "#^Property Icinga\\\\File\\\\Ini\\\\Dom\\\\Directive\\:\\:\\$commentPost \\(Icinga\\\\File\\\\Ini\\\\Dom\\\\Comment\\) in isset\\(\\) is not nullable\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Directive.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Document\\:\\:addSection\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Document.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Document\\:\\:getCommentsDangling\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Document.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Document\\:\\:removeSection\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Document.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Document\\:\\:setCommentsDangling\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Document.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Document\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Document.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Section\\:\\:addDirective\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Section.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Section\\:\\:getDirective\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Section.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Section\\:\\:removeDirective\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Section.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Section\\:\\:sanitize\\(\\) has parameter \\$str with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Section.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Section\\:\\:setCommentPost\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Section.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Section\\:\\:setCommentsPre\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Section.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\Dom\\\\Section\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Section.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of function implode expects array\\<string\\>, array\\<Icinga\\\\File\\\\Ini\\\\Dom\\\\Comment\\|string\\> given\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Section.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Section.php
+
+ -
+ message: "#^Property Icinga\\\\File\\\\Ini\\\\Dom\\\\Section\\:\\:\\$commentPost \\(Icinga\\\\File\\\\Ini\\\\Dom\\\\Comment\\) in isset\\(\\) is not nullable\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/Dom/Section.php
+
+ -
+ message: "#^Argument of an invalid type array\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/IniParser.php
+
+ -
+ message: "#^Cannot call method addDirective\\(\\) on Icinga\\\\File\\\\Ini\\\\Dom\\\\Section\\|null\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/IniParser.php
+
+ -
+ message: "#^Cannot call method setCommentPost\\(\\) on Icinga\\\\File\\\\Ini\\\\Dom\\\\Section\\|null\\.$#"
+ count: 2
+ path: library/Icinga/File/Ini/IniParser.php
+
+ -
+ message: "#^Cannot call method setValue\\(\\) on Icinga\\\\File\\\\Ini\\\\Dom\\\\Directive\\|null\\.$#"
+ count: 3
+ path: library/Icinga/File/Ini/IniParser.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\IniParser\\:\\:parseIni\\(\\) has parameter \\$str with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/IniParser.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\IniParser\\:\\:throwParseError\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/IniParser.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\IniParser\\:\\:throwParseError\\(\\) has parameter \\$line with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/IniParser.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\IniParser\\:\\:throwParseError\\(\\) has parameter \\$message with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/IniParser.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\IniParser\\:\\:unescapeOptionValue\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/IniParser.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\IniWriter\\:\\:__construct\\(\\) has parameter \\$options with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/IniWriter.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\IniWriter\\:\\:diffPropertyDeletions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/IniWriter.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\IniWriter\\:\\:diffPropertyUpdates\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/IniWriter.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Ini\\\\IniWriter\\:\\:write\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/IniWriter.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function trim expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/IniWriter.php
+
+ -
+ message: "#^Property Icinga\\\\File\\\\Ini\\\\IniWriter\\:\\:\\$options type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/File/Ini/IniWriter.php
+
+ -
+ message: "#^Cannot call method streamPdfFromHtml\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/File/Pdf.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Pdf\\:\\:assertNoHeadersSent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Pdf.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Pdf\\:\\:renderControllerAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Pdf.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Pdf\\:\\:renderControllerAction\\(\\) has parameter \\$controller with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Pdf.php
+
+ -
+ message: "#^Cannot access offset 0 on array\\<mixed, mixed\\>\\|false\\.$#"
+ count: 1
+ path: library/Icinga/File/Storage/LocalFileStorage.php
+
+ -
+ message: "#^Cannot access offset int\\<0, max\\> on array\\<int\\|string, mixed\\>\\|false\\.$#"
+ count: 2
+ path: library/Icinga/File/Storage/LocalFileStorage.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Storage\\\\LocalFileStorage\\:\\:create\\(\\) return type has no value type specified in iterable type \\$this\\(Icinga\\\\File\\\\Storage\\\\LocalFileStorage\\)\\.$#"
+ count: 1
+ path: library/Icinga/File/Storage/LocalFileStorage.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Storage\\\\LocalFileStorage\\:\\:delete\\(\\) return type has no value type specified in iterable type \\$this\\(Icinga\\\\File\\\\Storage\\\\LocalFileStorage\\)\\.$#"
+ count: 1
+ path: library/Icinga/File/Storage/LocalFileStorage.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Storage\\\\LocalFileStorage\\:\\:ensureDir\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/File/Storage/LocalFileStorage.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Storage\\\\LocalFileStorage\\:\\:getIterator\\(\\) return type has no value type specified in iterable type Traversable\\.$#"
+ count: 1
+ path: library/Icinga/File/Storage/LocalFileStorage.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Storage\\\\LocalFileStorage\\:\\:update\\(\\) return type has no value type specified in iterable type \\$this\\(Icinga\\\\File\\\\Storage\\\\LocalFileStorage\\)\\.$#"
+ count: 1
+ path: library/Icinga/File/Storage/LocalFileStorage.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_splice expects array, array\\<int\\|string, mixed\\>\\|false given\\.$#"
+ count: 2
+ path: library/Icinga/File/Storage/LocalFileStorage.php
+
+ -
+ message: "#^Parameter \\#1 \\$stream of function fclose expects resource, resource\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/File/Storage/LocalFileStorage.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, array\\<int\\|string, mixed\\>\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/File/Storage/LocalFileStorage.php
+
+ -
+ message: "#^Interface Icinga\\\\File\\\\Storage\\\\StorageInterface extends generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/File/Storage/StorageInterface.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Storage\\\\StorageInterface\\:\\:create\\(\\) return type has no value type specified in iterable type \\$this\\(Icinga\\\\File\\\\Storage\\\\StorageInterface\\)\\.$#"
+ count: 1
+ path: library/Icinga/File/Storage/StorageInterface.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Storage\\\\StorageInterface\\:\\:delete\\(\\) return type has no value type specified in iterable type \\$this\\(Icinga\\\\File\\\\Storage\\\\StorageInterface\\)\\.$#"
+ count: 1
+ path: library/Icinga/File/Storage/StorageInterface.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Storage\\\\StorageInterface\\:\\:getIterator\\(\\) return type has no value type specified in iterable type Traversable\\.$#"
+ count: 1
+ path: library/Icinga/File/Storage/StorageInterface.php
+
+ -
+ message: "#^Method Icinga\\\\File\\\\Storage\\\\StorageInterface\\:\\:update\\(\\) return type has no value type specified in iterable type \\$this\\(Icinga\\\\File\\\\Storage\\\\StorageInterface\\)\\.$#"
+ count: 1
+ path: library/Icinga/File/Storage/StorageInterface.php
+
+ -
+ message: "#^Parameter \\#1 \\$directory of function rmdir expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/File/Storage/TemporaryLocalFileStorage.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function unlink expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/File/Storage/TemporaryLocalFileStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Legacy\\\\DashboardConfig\\:\\:saveIni\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Legacy/DashboardConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Call\\:\\:compile\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Call.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Call\\:\\:compile\\(\\) has parameter \\$env with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Call.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Call\\:\\:fromCall\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Call.php
+
+ -
+ message: "#^Argument of an invalid type Less_Tree_Color supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Less/ColorProp.php
+
+ -
+ message: "#^Access to an undefined property Less_Tree\\:\\:\\$value\\.$#"
+ count: 1
+ path: library/Icinga/Less/ColorPropOrVariable.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\ColorPropOrVariable\\:\\:compile\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/ColorPropOrVariable.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\ColorPropOrVariable\\:\\:compile\\(\\) has parameter \\$env with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/ColorPropOrVariable.php
+
+ -
+ message: "#^Property Icinga\\\\Less\\\\ColorPropOrVariable\\:\\:\\$type has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/ColorPropOrVariable.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\DeferredColorProp\\:\\:__construct\\(\\) has parameter \\$currentFileInfo with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/DeferredColorProp.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\DeferredColorProp\\:\\:__construct\\(\\) has parameter \\$index with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/DeferredColorProp.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\DeferredColorProp\\:\\:__construct\\(\\) has parameter \\$variable with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/DeferredColorProp.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\DeferredColorProp\\:\\:fromVariable\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/DeferredColorProp.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\DeferredColorProp\\:\\:getName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/DeferredColorProp.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\DeferredColorProp\\:\\:getRef\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/DeferredColorProp.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\DeferredColorProp\\:\\:hasReference\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/DeferredColorProp.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\DeferredColorProp\\:\\:isResolved\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/DeferredColorProp.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\DeferredColorProp\\:\\:setReference\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/DeferredColorProp.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\DeferredColorProp\\:\\:setReference\\(\\) has parameter \\$ref with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/DeferredColorProp.php
+
+ -
+ message: "#^Property Icinga\\\\Less\\\\DeferredColorProp\\:\\:\\$resolved has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/DeferredColorProp.php
+
+ -
+ message: "#^Class Icinga\\\\Less\\\\LightMode implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Less/LightMode.php
+
+ -
+ message: "#^Property Icinga\\\\Less\\\\LightMode\\:\\:\\$envs type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Less/LightMode.php
+
+ -
+ message: "#^Property Icinga\\\\Less\\\\LightMode\\:\\:\\$modes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Less/LightMode.php
+
+ -
+ message: "#^Property Icinga\\\\Less\\\\LightMode\\:\\:\\$selectors type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Less/LightMode.php
+
+ -
+ message: "#^PHPDoc tag @var has invalid value \\(\\$frame Less_Tree_Ruleset\\)\\: Unexpected token \"\\$frame\", expected type at offset 9$#"
+ count: 1
+ path: library/Icinga/Less/LightModeDefinition.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\LightModeVisitor\\:\\:run\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/LightModeVisitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\LightModeVisitor\\:\\:run\\(\\) has parameter \\$node with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/LightModeVisitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\LightModeVisitor\\:\\:visitRulesetCall\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/LightModeVisitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\LightModeVisitor\\:\\:visitRulesetCall\\(\\) has parameter \\$c with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/LightModeVisitor.php
+
+ -
+ message: "#^Property Icinga\\\\Less\\\\LightModeVisitor\\:\\:\\$isPreVisitor has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/LightModeVisitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:run\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:run\\(\\) has parameter \\$node with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitCall\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitCall\\(\\) has parameter \\$c with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitDetachedRuleset\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitDetachedRuleset\\(\\) has parameter \\$drs with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitMixinCall\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitMixinCall\\(\\) has parameter \\$c with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitMixinDefinition\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitMixinDefinition\\(\\) has parameter \\$m with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitRule\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitRule\\(\\) has parameter \\$r with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitRuleOut\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitRuleOut\\(\\) has parameter \\$r with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitRuleset\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitRuleset\\(\\) has parameter \\$rs with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitRulesetOut\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitRulesetOut\\(\\) has parameter \\$rs with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitSelector\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitSelector\\(\\) has parameter \\$s with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitVariable\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Less\\\\Visitor\\:\\:visitVariable\\(\\) has parameter \\$v with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Parameter \\#1 \\$mode of method Icinga\\\\Less\\\\LightMode\\:\\:getSelector\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Parameter \\#1 \\$mode of method Icinga\\\\Less\\\\LightMode\\:\\:hasSelector\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Parameter \\#1 \\$prefix of function uniqid expects string, int\\<0, max\\> given\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Parameter \\#2 \\$selector of method Icinga\\\\Less\\\\LightMode\\:\\:setSelector\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Part \\$mode \\(mixed\\) of encapsed string cannot be cast to string\\.$#"
+ count: 2
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Property Icinga\\\\Less\\\\Visitor\\:\\:\\$isPreEvalVisitor has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Property Icinga\\\\Less\\\\Visitor\\:\\:\\$variableOrigin \\(Less_Tree_Rule\\) does not accept null\\.$#"
+ count: 1
+ path: library/Icinga/Less/Visitor.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Dns\\:\\:getSrvRecords\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Dns.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Dns\\:\\:ipv4\\(\\) has parameter \\$hostname with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Dns.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Dns\\:\\:ipv6\\(\\) has parameter \\$hostname with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Dns.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Dns\\:\\:ptr\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Dns.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Dns\\:\\:records\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Dns.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Dns\\:\\:records\\(\\) should return array\\|null but returns array\\<int, array\\>\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Dns.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\File\\\\FileIterator\\:\\:__construct\\(\\) has parameter \\$fields with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileIterator.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\File\\\\FileIterator\\:\\:__construct\\(\\) has parameter \\$filename with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileIterator.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\File\\\\FileIterator\\:\\:current\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileIterator.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_match expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileIterator.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\File\\\\FileIterator\\:\\:\\$currentData type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileIterator.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\File\\\\FileQuery\\:\\:applyFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\File\\\\FileQuery\\:\\:getFilters\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileQuery.php
+
+ -
+ message: "#^PHPDoc tag @param references unknown parameter\\: \\$dir$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\File\\\\FileQuery\\:\\:\\$filters type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\File\\\\FileQuery\\:\\:\\$sortDir \\(int\\) does not accept string\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\File\\\\FileReader\\:\\:fetchAll\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileReader.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\File\\\\FileReader\\:\\:fetchColumn\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileReader.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\File\\\\FileReader\\:\\:fetchPairs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileReader.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\File\\\\FileReader\\:\\:fetchRow\\(\\) should return object but returns null\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileReader.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\File\\\\FileReader\\:\\:iterate\\(\\) should return Icinga\\\\Protocol\\\\File\\\\FileIterator but returns Icinga\\\\Protocol\\\\File\\\\LogFileIterator\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileReader.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\File\\\\FileReader\\:\\:query\\(\\) return type with generic class ArrayIterator does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Protocol/File/FileReader.php
+
+ -
+ message: "#^Class Icinga\\\\Protocol\\\\File\\\\LogFileIterator implements generic interface Iterator but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Protocol/File/LogFileIterator.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\File\\\\LogFileIterator\\:\\:current\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/LogFileIterator.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\File\\\\LogFileIterator\\:\\:nextMessage\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/LogFileIterator.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of function implode expects array\\<string\\>, array\\<int, array\\|bool\\|string\\> given\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/LogFileIterator.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_match expects string, array\\|string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/LogFileIterator.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\File\\\\LogFileIterator\\:\\:\\$current type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/LogFileIterator.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\File\\\\LogFileIterator\\:\\:\\$next \\(string\\) does not accept array\\|string\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/LogFileIterator.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\File\\\\LogFileIterator\\:\\:\\$next \\(string\\) does not accept null\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/LogFileIterator.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\File\\\\LogFileIterator\\:\\:\\$valid \\(bool\\) does not accept null\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/File/LogFileIterator.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Discovery\\:\\:discover\\(\\) has parameter \\$host with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Discovery.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Discovery\\:\\:discover\\(\\) has parameter \\$port with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Discovery.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Discovery\\:\\:discoverDomain\\(\\) should return Icinga\\\\Protocol\\\\Ldap\\\\Discovery but returns false\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Discovery.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Discovery\\:\\:suggestBackendSettings\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Discovery.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Discovery\\:\\:suggestResourceSettings\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Discovery.php
+
+ -
+ message: "#^Argument of an invalid type stdClass supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapCapabilities.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapCapabilities\\:\\:__construct\\(\\) has parameter \\$attributes with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapCapabilities.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapCapabilities\\:\\:discoverAdConfigOptions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapCapabilities.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapCapabilities\\:\\:getVendor\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapCapabilities.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapCapabilities\\:\\:getVersion\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapCapabilities.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapCapabilities\\:\\:hasOid\\(\\) has parameter \\$oid with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapCapabilities.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapCapabilities\\:\\:namingContexts\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapCapabilities.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapCapabilities\\:\\:setAttributes\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapCapabilities.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapCapabilities\\:\\:setAttributes\\(\\) has parameter \\$attributes with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapCapabilities.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Ldap\\\\LdapCapabilities\\:\\:\\$attributes \\(stdClass\\) in isset\\(\\) is not nullable\\.$#"
+ count: 2
+ path: library/Icinga/Protocol/Ldap/LdapCapabilities.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Ldap\\\\LdapCapabilities\\:\\:\\$oids type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapCapabilities.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:filters\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getOperatorSymbol\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Cannot access offset 1 on array\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Cannot cast mixed to int\\.$#"
+ count: 2
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Function ldap_control_paged_result not found\\.$#"
+ count: 2
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Function ldap_control_paged_result_response not found\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Left side of && is always false\\.$#"
+ count: 3
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Left side of \\|\\| is always false\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:addEntry\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:bind\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:cleanupAttributes\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:cleanupAttributes\\(\\) has parameter \\$requestedFields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:cleanupAttributes\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:encodeSortRules\\(\\) has parameter \\$sortRules with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:encodeSortRules\\(\\) should return string but returns string\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:fetchAll\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:fetchAll\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:fetchByDn\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:fetchByDn\\(\\) should return bool\\|stdClass but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:fetchColumn\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:fetchColumn\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:fetchDn\\(\\) should return string but returns int\\|string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:fetchOne\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:fetchOne\\(\\) should return string but returns false\\.$#"
+ count: 2
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:fetchOne\\(\\) should return string but returns mixed\\.$#"
+ count: 2
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:fetchPairs\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:fetchPairs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:fetchRow\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:ldapSearch\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:ldapSearch\\(\\) has parameter \\$controls with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:modifyEntry\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:moveEntry\\(\\) should return resource but returns true\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:normalizeHostname\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:normalizeHostname\\(\\) has parameter \\$hostname with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:query\\(\\) return type with generic class ArrayIterator does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:runPagedQuery\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:runPagedQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:runQuery\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:runQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Negated boolean expression is always true\\.$#"
+ count: 7
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^PHPDoc tag @var has invalid value \\(\\$filter FilterChain\\)\\: Unexpected token \"\\$filter\", expected type at offset 9$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Parameter \\#1 \\$filter of method Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:renderFilterExpression\\(\\) expects Icinga\\\\Data\\\\Filter\\\\FilterExpression, Icinga\\\\Data\\\\Filter\\\\Filter given\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Parameter \\#1 \\$num of function dechex expects int, float\\|int\\<0, 126\\> given\\.$#"
+ count: 2
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Parameter \\#1 \\$num of function dechex expects int, float\\|int\\<1, 126\\> given\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Parameter \\#1 \\$object of function get_object_vars expects object, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Parameter \\#2 \\$code of class LogicException constructor expects int, string given\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Parameter \\#2 \\$offset of function array_splice expects int, int\\|null given\\.$#"
+ count: 2
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Parameter \\#4 \\$attributes of callable 'ldap_list'\\|'ldap_read'\\|'ldap_search' expects array, array\\|null given\\.$#"
+ count: 2
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:\\$bindDn \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:\\$bindPw \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:\\$encryption \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:\\$hostname \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection\\:\\:\\$rootDn \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Result of && is always false\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Result of \\|\\| is always true\\.$#"
+ count: 10
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:filters\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Cannot call method fetchDn\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Cannot call method getDn\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Cannot call method renderFilter\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapQuery\\:\\:from\\(\\) has parameter \\$fields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapQuery\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapQuery\\:\\:makeCaseInsensitive\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapQuery\\:\\:setFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$connection of static method Icinga\\\\Protocol\\\\Ldap\\\\Root\\:\\:forConnection\\(\\) expects Icinga\\\\Protocol\\\\Ldap\\\\LdapConnection, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$dn of static method Icinga\\\\Protocol\\\\Ldap\\\\LdapUtils\\:\\:explodeDN\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$code of class LogicException constructor expects int, string given\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Parameter \\#3 \\$previous of class LogicException constructor expects Throwable\\|null, string given\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Ldap\\\\LdapQuery\\:\\:\\$scopes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Variable \\$filter in PHPDoc tag @var does not match any variable in the foreach loop\\: \\$subFilter$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapQuery.php
+
+ -
+ message: "#^Argument of an invalid type array\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapUtils.php
+
+ -
+ message: "#^Cannot access offset \\(int\\|string\\) on non\\-empty\\-array\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapUtils.php
+
+ -
+ message: "#^Cannot use array destructuring on array\\<int, string\\>\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapUtils\\:\\:explodeDN\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapUtils\\:\\:implodeDN\\(\\) has parameter \\$parts with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapUtils\\:\\:quoteChars\\(\\) has parameter \\$chars with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapUtils\\:\\:quoteChars\\(\\) has parameter \\$str with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapUtils\\:\\:quoteChars\\(\\) should return string but returns array\\<int, string\\>\\|string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\LdapUtils\\:\\:quoteForSearch\\(\\) has parameter \\$str with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapUtils.php
+
+ -
+ message: "#^PHPDoc tag @param has invalid value \\(string String to be escaped\\)\\: Unexpected token \"String\", expected variable at offset 142$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapUtils.php
+
+ -
+ message: "#^Parameter \\#1 \\$codepoint of function chr expects int, float\\|int given\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/LdapUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Node\\:\\:createWithRDN\\(\\) has parameter \\$parent with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Node\\:\\:createWithRDN\\(\\) has parameter \\$props with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Node\\:\\:createWithRDN\\(\\) has parameter \\$rdn with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Node.php
+
+ -
+ message: "#^PHPDoc tag @var has invalid value \\(\\)\\: Unexpected token \"\\\\n \", expected type at offset 15$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Root\\:\\:__get\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Root.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Root\\:\\:__isset\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Root.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Root\\:\\:assertSubDN\\(\\) has parameter \\$dn with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Root.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Root\\:\\:children\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Root.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Root\\:\\:countChildren\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Root.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Root\\:\\:createChildByDN\\(\\) has parameter \\$dn with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Root.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Root\\:\\:createChildByDN\\(\\) has parameter \\$props with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Root.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Root\\:\\:getChildByRDN\\(\\) has parameter \\$rdn with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Root.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Root\\:\\:hasChildRDN\\(\\) has parameter \\$rdn with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Root.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Ldap\\\\Root\\:\\:stripMyDN\\(\\) has parameter \\$dn with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Root.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strlen expects string, mixed given\\.$#"
+ count: 3
+ path: library/Icinga/Protocol/Ldap/Root.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Protocol/Ldap/Root.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Ldap\\\\Root\\:\\:\\$children type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Root.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Ldap\\\\Root\\:\\:\\$props type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Ldap/Root.php
+
+ -
+ message: "#^Cannot access offset 1 on array\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:__construct\\(\\) has parameter \\$host with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:__construct\\(\\) has parameter \\$port with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:connect\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:connection\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:disconnect\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:getLastReturnCode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:send\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:sendCommand\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:sendCommand\\(\\) has parameter \\$args with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:sendCommand\\(\\) has parameter \\$command with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:useSsl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:useSsl\\(\\) has parameter \\$use_ssl with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:\\$connection has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:\\$host has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:\\$lastReturnCode has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:\\$port has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Nrpe\\\\Connection\\:\\:\\$use_ssl has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Connection.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Packet\\:\\:__construct\\(\\) has parameter \\$body with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Packet.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Packet\\:\\:__construct\\(\\) has parameter \\$type with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Packet.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Packet\\:\\:createQuery\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Packet.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Packet\\:\\:createQuery\\(\\) has parameter \\$body with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Packet.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Packet\\:\\:getBinary\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Packet.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Packet\\:\\:getFillString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Packet.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Packet\\:\\:getFillString\\(\\) has parameter \\$length with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Packet.php
+
+ -
+ message: "#^Method Icinga\\\\Protocol\\\\Nrpe\\\\Packet\\:\\:regenerateRandomBytes\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Packet.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Nrpe\\\\Packet\\:\\:\\$body has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Packet.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Nrpe\\\\Packet\\:\\:\\$randomBytes has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Packet.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Nrpe\\\\Packet\\:\\:\\$type has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Packet.php
+
+ -
+ message: "#^Property Icinga\\\\Protocol\\\\Nrpe\\\\Packet\\:\\:\\$version has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Protocol/Nrpe/Packet.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:hasJoinedTable\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Function type not found\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:applyTableAlias\\(\\) has parameter \\$table with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:applyTableAlias\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:clearTableAlias\\(\\) has parameter \\$table with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:getConverter\\(\\) should return string but empty return statement found\\.$#"
+ count: 2
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:getConverter\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:getJoinProbabilities\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:getQueryColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:getStatementAliasColumnMap\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:getStatementAliasTableMap\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:getStatementColumnAliasMap\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:getStatementColumnTableMap\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:getStatementColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:getTableAliases\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:initializeAliasMaps\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:initializeJoinProbabilities\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:initializeStatementColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:initializeStatementMaps\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:initializeTableAliases\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:insert\\(\\) has parameter \\$bind with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:insert\\(\\) has parameter \\$types with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:joinColumn\\(\\) should return string\\|null but empty return statement found\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:prependTablePrefix\\(\\) has parameter \\$table with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:prependTablePrefix\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:removeCollateInstruction\\(\\) has parameter \\$queryColumns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:removeCollateInstruction\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:removeTablePrefix\\(\\) has parameter \\$table with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:removeTablePrefix\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:requireTable\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:update\\(\\) has parameter \\$bind with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\DbRepository\\:\\:update\\(\\) has parameter \\$types with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Repository\\\\DbRepository\\:\\:reassembleQueryColumnAlias\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Repository\\\\DbRepository\\:\\:reassembleStatementColumnAlias\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Possibly invalid array key type array\\|string\\.$#"
+ count: 2
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\DbRepository\\:\\:\\$caseInsensitiveColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\DbRepository\\:\\:\\$joinProbabilities type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\DbRepository\\:\\:\\$statementAliasColumnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\DbRepository\\:\\:\\$statementAliasTableMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\DbRepository\\:\\:\\$statementColumnAliasMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\DbRepository\\:\\:\\$statementColumnTableMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\DbRepository\\:\\:\\$statementColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\DbRepository\\:\\:\\$tableAliases type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/DbRepository.php
+
+ -
+ message: "#^Cannot call method get\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Cannot clone non\\-object variable \\$config of type mixed\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:createConfig\\(\\) has parameter \\$meta with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:delete\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:extractSectionName\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:extractSectionName\\(\\) has parameter \\$config with no value type specified in iterable type array\\|Icinga\\\\Data\\\\ConfigObject\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:getConfigs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:getDataSource\\(\\) should return Icinga\\\\Application\\\\Config but returns Icinga\\\\Data\\\\Selectable\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:getTrigger\\(\\) should return string\\|null but empty return statement found\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:getTriggers\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:initializeConfigs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:initializeTriggers\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:insert\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:insert\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:onDelete\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:update\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\IniRepository\\:\\:update\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Config\\:\\:removeSection\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Config\\:\\:setSection\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Parameter \\#2 \\$old of method Icinga\\\\Repository\\\\IniRepository\\:\\:onUpdate\\(\\) expects Icinga\\\\Data\\\\ConfigObject, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Parameter \\#3 \\$new of method Icinga\\\\Repository\\\\IniRepository\\:\\:onUpdate\\(\\) expects Icinga\\\\Data\\\\ConfigObject, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\IniRepository\\:\\:\\$configs type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\IniRepository\\:\\:\\$triggers type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Variable \\$config in PHPDoc tag @var does not match assigned variable \\$newSection\\.$#"
+ count: 1
+ path: library/Icinga/Repository/IniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\LdapRepository\\:\\:getNormedAttribute\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Repository/LdapRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\LdapRepository\\:\\:\\$normedAttributes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/LdapRepository.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:filters\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getColumn\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getExpression\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:setColumn\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:setExpression\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:getAliasColumnMap\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:getAliasTableMap\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:getBaseTable\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:getBlacklistedQueryColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:getColumnAliasMap\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:getColumnTableMap\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:getConversionRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:getFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:getQueryColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:getSearchColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:getVirtualTables\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:initializeAliasMaps\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:initializeBlacklistedQueryColumns\\(\\) invoked with 1 parameter, 0 required\\.$#"
+ count: 2
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:initializeBlacklistedQueryColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:initializeConversionRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:initializeFilterColumns\\(\\) invoked with 1 parameter, 0 required\\.$#"
+ count: 2
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:initializeFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:initializeQueryColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:initializeSearchColumns\\(\\) invoked with 1 parameter, 0 required\\.$#"
+ count: 2
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:initializeSearchColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:initializeSortRules\\(\\) invoked with 1 parameter, 0 required\\.$#"
+ count: 2
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:initializeSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:initializeVirtualTables\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:persistCommaSeparatedString\\(\\) has parameter \\$value with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:persistDateTime\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:requireAllQueryColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:requireStatementColumns\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:requireStatementColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:retrieveCommaSeparatedString\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:retrieveDateTime\\(\\) should return int but returns int\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\Repository\\:\\:select\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^PHPDoc tag @param references unknown parameter\\: \\$table$#"
+ count: 4
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Parameter \\#2 \\$timestamp of function date expects int\\|null, float\\|int\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Parameter \\#4 \\$filter of method Icinga\\\\Repository\\\\Repository\\:\\:requireFilterColumn\\(\\) expects Icinga\\\\Data\\\\Filter\\\\FilterExpression\\|null, Icinga\\\\Data\\\\Filter\\\\Filter given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\Repository\\:\\:\\$aliasColumnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\Repository\\:\\:\\$aliasTableMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\Repository\\:\\:\\$baseTable \\(string\\) does not accept int\\|string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\Repository\\:\\:\\$blacklistedQueryColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\Repository\\:\\:\\$columnAliasMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\Repository\\:\\:\\$columnTableMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\Repository\\:\\:\\$conversionRules type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\Repository\\:\\:\\$ds \\(Icinga\\\\Data\\\\Selectable\\) does not accept Icinga\\\\Data\\\\Selectable\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\Repository\\:\\:\\$filterColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\Repository\\:\\:\\$queryColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\Repository\\:\\:\\$searchColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\Repository\\:\\:\\$sortRules type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\Repository\\:\\:\\$virtualTables type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/Repository.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:columns\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:getColumns\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:getIteratorPosition\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:hasMore\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:hasResult\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\QueryInterface\\:\\:peekAhead\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Queryable\\:\\:columns\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Selectable\\:\\:query\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Cannot cast Icinga\\\\Data\\\\QueryInterface to string\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Class Icinga\\\\Repository\\\\RepositoryQuery implements generic interface Iterator but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:columns\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:fetchAll\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:fetchColumn\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:fetchPairs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:fetchRow\\(\\) should return object\\|false but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:from\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:getFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:getLimit\\(\\) should return int but returns int\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:getOffset\\(\\) should return int but returns int\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:getOrder\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:getOrder\\(\\) should return array but returns array\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:getSearchColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:prepareQueryColumns\\(\\) has parameter \\$desiredColumns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:prepareQueryColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:splitOrder\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$customAlias of method Icinga\\\\Repository\\\\RepositoryQuery\\:\\:getNativeAlias\\(\\) expects string, int\\|string\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Repository\\\\Repository\\:\\:getDataSource\\(\\) expects string\\|null, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Repository\\\\Repository\\:\\:getFilterColumns\\(\\) expects string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Repository\\\\Repository\\:\\:getSearchColumns\\(\\) expects string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Repository\\\\Repository\\:\\:getSortRules\\(\\) expects string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Repository\\\\Repository\\:\\:providesValueConversion\\(\\) expects string, mixed given\\.$#"
+ count: 12
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Repository\\\\Repository\\:\\:reassembleQueryColumnAlias\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Repository\\\\Repository\\:\\:requireAllQueryColumns\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Repository\\\\Repository\\:\\:requireFilter\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Repository\\\\Repository\\:\\:requireFilterColumn\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Repository\\\\Repository\\:\\:requireQueryColumn\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Repository\\\\Repository\\:\\:requireTable\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$table of method Icinga\\\\Repository\\\\Repository\\:\\:retrieveColumn\\(\\) expects string, mixed given\\.$#"
+ count: 8
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$callback of function uasort expects callable\\(mixed, mixed\\)\\: int, array\\{Icinga\\\\Data\\\\QueryInterface, 'compare'\\} given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$filter of static method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:where\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\RepositoryQuery\\:\\:\\$customAliases type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\RepositoryQuery\\:\\:\\$iterator \\(Iterator\\) does not accept Traversable\\<mixed, mixed\\>\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Repository\\\\RepositoryQuery\\:\\:\\$query \\(Icinga\\\\Data\\\\QueryInterface\\) does not accept Icinga\\\\Data\\\\Queryable\\.$#"
+ count: 1
+ path: library/Icinga/Repository/RepositoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\User\\:\\:getExternalUserInformation\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User.php
+
+ -
+ message: "#^Method Icinga\\\\User\\:\\:getGroups\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User.php
+
+ -
+ message: "#^Method Icinga\\\\User\\:\\:getPermissions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User.php
+
+ -
+ message: "#^Method Icinga\\\\User\\:\\:getRestrictions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User.php
+
+ -
+ message: "#^Method Icinga\\\\User\\:\\:setGroups\\(\\) has parameter \\$groups with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User.php
+
+ -
+ message: "#^Method Icinga\\\\User\\:\\:setPermissions\\(\\) has parameter \\$permissions with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:addItem\\(\\) expects Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\|string, int\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/User.php
+
+ -
+ message: "#^Parameter \\#1 \\$timezone of class DateTimeZone constructor expects string, array\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/User.php
+
+ -
+ message: "#^Property Icinga\\\\User\\:\\:\\$additionalInformation type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User.php
+
+ -
+ message: "#^Property Icinga\\\\User\\:\\:\\$domain \\(string\\) does not accept null\\.$#"
+ count: 1
+ path: library/Icinga/User.php
+
+ -
+ message: "#^Property Icinga\\\\User\\:\\:\\$externalUserInformation type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User.php
+
+ -
+ message: "#^Property Icinga\\\\User\\:\\:\\$groups type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User.php
+
+ -
+ message: "#^Property Icinga\\\\User\\:\\:\\$permissions type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User.php
+
+ -
+ message: "#^Property Icinga\\\\User\\:\\:\\$restrictions type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User.php
+
+ -
+ message: "#^Method Icinga\\\\User\\\\Preferences\\:\\:__construct\\(\\) has parameter \\$preferences with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User/Preferences.php
+
+ -
+ message: "#^Method Icinga\\\\User\\\\Preferences\\:\\:get\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User/Preferences.php
+
+ -
+ message: "#^Method Icinga\\\\User\\\\Preferences\\:\\:getValue\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User/Preferences.php
+
+ -
+ message: "#^Method Icinga\\\\User\\\\Preferences\\:\\:remove\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/User/Preferences.php
+
+ -
+ message: "#^Method Icinga\\\\User\\\\Preferences\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User/Preferences.php
+
+ -
+ message: "#^Property Icinga\\\\User\\\\Preferences\\:\\:\\$preferences type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User/Preferences.php
+
+ -
+ message: "#^Cannot call method getDbAdapter\\(\\) on mixed\\.$#"
+ count: 4
+ path: library/Icinga/User/Preferences/PreferencesStore.php
+
+ -
+ message: "#^Method Icinga\\\\User\\\\Preferences\\\\PreferencesStore\\:\\:delete\\(\\) has parameter \\$preferenceKeys with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User/Preferences/PreferencesStore.php
+
+ -
+ message: "#^Method Icinga\\\\User\\\\Preferences\\\\PreferencesStore\\:\\:insert\\(\\) has parameter \\$preferences with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User/Preferences/PreferencesStore.php
+
+ -
+ message: "#^Method Icinga\\\\User\\\\Preferences\\\\PreferencesStore\\:\\:load\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User/Preferences/PreferencesStore.php
+
+ -
+ message: "#^Method Icinga\\\\User\\\\Preferences\\\\PreferencesStore\\:\\:update\\(\\) has parameter \\$preferences with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User/Preferences/PreferencesStore.php
+
+ -
+ message: "#^Property Icinga\\\\User\\\\Preferences\\\\PreferencesStore\\:\\:\\$preferences type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/User/Preferences/PreferencesStore.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Color\\:\\:arrayToRgb\\(\\) has parameter \\$rgb with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/Color.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Color\\:\\:changeBrightness\\(\\) has parameter \\$change with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Color.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Color\\:\\:changeBrightness\\(\\) has parameter \\$color with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Color.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Color\\:\\:changeRgbBrightness\\(\\) has parameter \\$change with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Color.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Color\\:\\:changeRgbBrightness\\(\\) has parameter \\$rgb with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/Color.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Color\\:\\:changeRgbBrightness\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/Color.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Color\\:\\:changeRgbSaturation\\(\\) has parameter \\$change with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Color.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Color\\:\\:changeRgbSaturation\\(\\) has parameter \\$rgb with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/Color.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Color\\:\\:changeRgbSaturation\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/Color.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Color\\:\\:changeSaturation\\(\\) has parameter \\$change with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Color.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Color\\:\\:changeSaturation\\(\\) has parameter \\$color with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Color.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Color\\:\\:rgbAsArray\\(\\) has parameter \\$color with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Color.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Color\\:\\:rgbAsArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/Color.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Color\\:\\:rgbAsArray\\(\\) should return array but empty return statement found\\.$#"
+ count: 1
+ path: library/Icinga/Util/Color.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\ConfigAwareFactory\\:\\:setConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/ConfigAwareFactory.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Dimension\\:\\:fromString\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Dimension.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Dimension\\:\\:setValue\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Dimension.php
+
+ -
+ message: "#^Cannot access offset 0 on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Util/DirectoryIterator.php
+
+ -
+ message: "#^Class Icinga\\\\Util\\\\DirectoryIterator implements generic interface RecursiveIterator but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Util/DirectoryIterator.php
+
+ -
+ message: "#^Do\\-while loop condition is always false\\.$#"
+ count: 1
+ path: library/Icinga/Util/DirectoryIterator.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of class ArrayIterator constructor expects array\\<int, string\\>, array\\<int, string\\>\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Util/DirectoryIterator.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function natcasesort expects array, array\\<int, string\\>\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Util/DirectoryIterator.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of static method Icinga\\\\Util\\\\DirectoryIterator\\:\\:isReadable\\(\\) expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Util/DirectoryIterator.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of static method Icinga\\\\Util\\\\StringHelper\\:\\:endsWith\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Util/DirectoryIterator.php
+
+ -
+ message: "#^Property Icinga\\\\Util\\\\DirectoryIterator\\:\\:\\$files with generic class ArrayIterator does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Util/DirectoryIterator.php
+
+ -
+ message: "#^Property Icinga\\\\Util\\\\DirectoryIterator\\:\\:\\$key \\(string\\|false\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Util/DirectoryIterator.php
+
+ -
+ message: "#^Property Icinga\\\\Util\\\\DirectoryIterator\\:\\:\\$queue type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/DirectoryIterator.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Environment\\:\\:raiseExecutionTime\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Environment.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Environment\\:\\:raiseMemoryLimit\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Environment.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\File\\:\\:__construct\\(\\) has parameter \\$context with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/File.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\File\\:\\:__construct\\(\\) has parameter \\$filename with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/File.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\File\\:\\:__construct\\(\\) has parameter \\$openMode with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/File.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\File\\:\\:__construct\\(\\) has parameter \\$useIncludePath with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/File.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\File\\:\\:assertOpenForWriting\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/File.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\File\\:\\:createDirectories\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/File.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\File\\:\\:setupErrorHandler\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/File.php
+
+ -
+ message: "#^Offset 'message' does not exist on array\\{type\\: int, message\\: string, file\\: string, line\\: int\\}\\|null\\.$#"
+ count: 3
+ path: library/Icinga/Util/File.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Format\\:\\:bits\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Format\\:\\:bits\\(\\) has parameter \\$standard with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Format\\:\\:bits\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Format\\:\\:bytes\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Format\\:\\:bytes\\(\\) has parameter \\$standard with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Format\\:\\:bytes\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Format\\:\\:formatForUnits\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Format\\:\\:formatForUnits\\(\\) has parameter \\$base with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Format\\:\\:formatForUnits\\(\\) has parameter \\$units with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Format\\:\\:formatForUnits\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Format\\:\\:getInstance\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Format\\:\\:seconds\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Format\\:\\:seconds\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Format\\:\\:unpackShorthandBytes\\(\\) should return int but returns float\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Property Icinga\\\\Util\\\\Format\\:\\:\\$bitBase has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Property Icinga\\\\Util\\\\Format\\:\\:\\$bitPrefix has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Property Icinga\\\\Util\\\\Format\\:\\:\\$byteBase has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Property Icinga\\\\Util\\\\Format\\:\\:\\$bytePrefix has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Property Icinga\\\\Util\\\\Format\\:\\:\\$instance has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Property Icinga\\\\Util\\\\Format\\:\\:\\$secondBase has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Property Icinga\\\\Util\\\\Format\\:\\:\\$secondPrefix has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/Format.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\GlobFilter\\:\\:__construct\\(\\) has parameter \\$filters with no value type specified in iterable type iterable\\.$#"
+ count: 1
+ path: library/Icinga/Util/GlobFilter.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\GlobFilter\\:\\:removeMatching\\(\\) has parameter \\$dataStructure with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/GlobFilter.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\GlobFilter\\:\\:removeMatching\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/GlobFilter.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\GlobFilter\\:\\:removeMatchingRecursive\\(\\) has parameter \\$dataStructure with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/GlobFilter.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\GlobFilter\\:\\:removeMatchingRecursive\\(\\) has parameter \\$filter with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/GlobFilter.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\GlobFilter\\:\\:removeMatchingRecursive\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/GlobFilter.php
+
+ -
+ message: "#^Property Icinga\\\\Util\\\\GlobFilter\\:\\:\\$filters type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/GlobFilter.php
+
+ -
+ message: "#^Argument of an invalid type object supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Util/Json.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\Json\\:\\:encodeAndSanitize\\(\\) should return string but returns string\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Util/Json.php
+
+ -
+ message: "#^Parameter \\#3 \\$depth of function json_decode expects int\\<1, max\\>, int given\\.$#"
+ count: 1
+ path: library/Icinga/Util/Json.php
+
+ -
+ message: "#^Parameter \\#3 \\$depth of function json_encode expects int\\<1, max\\>, int given\\.$#"
+ count: 1
+ path: library/Icinga/Util/Json.php
+
+ -
+ message: "#^Else branch is unreachable because ternary operator condition is always true\\.$#"
+ count: 1
+ path: library/Icinga/Util/StringHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\StringHelper\\:\\:cartesianProduct\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/StringHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\StringHelper\\:\\:cartesianProduct\\(\\) has parameter \\$sets with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/StringHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\StringHelper\\:\\:findSimilar\\(\\) has parameter \\$possibilities with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/StringHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\StringHelper\\:\\:findSimilar\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/StringHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\StringHelper\\:\\:trimSplit\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Util/StringHelper.php
+
+ -
+ message: "#^Parameter \\#1 \\$separator of function explode expects non\\-empty\\-string, string given\\.$#"
+ count: 2
+ path: library/Icinga/Util/StringHelper.php
+
+ -
+ message: "#^Parameter \\#2 \\$offset of function substr expects int, float given\\.$#"
+ count: 1
+ path: library/Icinga/Util/StringHelper.php
+
+ -
+ message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, float given\\.$#"
+ count: 1
+ path: library/Icinga/Util/StringHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Util\\\\TimezoneDetect\\:\\:reset\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Util/TimezoneDetect.php
+
+ -
+ message: "#^Static property Icinga\\\\Util\\\\TimezoneDetect\\:\\:\\$offset \\(int\\) does not accept string\\.$#"
+ count: 1
+ path: library/Icinga/Util/TimezoneDetect.php
+
+ -
+ message: "#^Static property Icinga\\\\Util\\\\TimezoneDetect\\:\\:\\$success \\(bool\\) does not accept null\\.$#"
+ count: 1
+ path: library/Icinga/Util/TimezoneDetect.php
+
+ -
+ message: "#^Static property Icinga\\\\Util\\\\TimezoneDetect\\:\\:\\$timezone is unused\\.$#"
+ count: 1
+ path: library/Icinga/Util/TimezoneDetect.php
+
+ -
+ message: "#^Static property Icinga\\\\Util\\\\TimezoneDetect\\:\\:\\$timezoneName \\(string\\) does not accept null\\.$#"
+ count: 1
+ path: library/Icinga/Util/TimezoneDetect.php
+
+ -
+ message: "#^Static property Icinga\\\\Util\\\\TimezoneDetect\\:\\:\\$timezoneName \\(string\\) does not accept string\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Util/TimezoneDetect.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Announcement\\:\\:__construct\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement.php
+
+ -
+ message: "#^Cannot access offset 'acknowledged' on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementCookie.php
+
+ -
+ message: "#^Cannot access offset 'etag' on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementCookie.php
+
+ -
+ message: "#^Cannot access offset 'next' on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementCookie.php
+
+ -
+ message: "#^Parameter \\#1 \\$acknowledged of method Icinga\\\\Web\\\\Announcement\\\\AnnouncementCookie\\:\\:setAcknowledged\\(\\) expects array\\<string\\>, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementCookie.php
+
+ -
+ message: "#^Parameter \\#1 \\$etag of method Icinga\\\\Web\\\\Announcement\\\\AnnouncementCookie\\:\\:setEtag\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementCookie.php
+
+ -
+ message: "#^Parameter \\#1 \\$nextActive of method Icinga\\\\Web\\\\Announcement\\\\AnnouncementCookie\\:\\:setNextActive\\(\\) expects int\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementCookie.php
+
+ -
+ message: "#^Cannot access property \\$end on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementIniRepository.php
+
+ -
+ message: "#^Cannot access property \\$start on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementIniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Announcement\\\\AnnouncementIniRepository\\:\\:findActive\\(\\) should return Icinga\\\\Data\\\\SimpleQuery but returns Icinga\\\\Repository\\\\RepositoryQuery\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementIniRepository.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Announcement\\\\AnnouncementIniRepository\\:\\:getEtag\\(\\) should return string but returns null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementIniRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Announcement\\\\AnnouncementIniRepository\\:\\:\\$configs type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementIniRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Announcement\\\\AnnouncementIniRepository\\:\\:\\$conversionRules type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementIniRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Announcement\\\\AnnouncementIniRepository\\:\\:\\$queryColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementIniRepository.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Announcement\\\\AnnouncementIniRepository\\:\\:\\$triggers type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementIniRepository.php
+
+ -
+ message: "#^Strict comparison using \\=\\=\\= between DateTime and null will always evaluate to false\\.$#"
+ count: 1
+ path: library/Icinga/Web/Announcement/AnnouncementIniRepository.php
+
+ -
+ message: "#^Cannot access offset 'acknowledged…' on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/ApplicationStateCookie.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/ApplicationStateCookie.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\ApplicationStateCookie\\:\\:getAcknowledgedMessages\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/ApplicationStateCookie.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\ApplicationStateCookie\\:\\:setAcknowledgedMessages\\(\\) has parameter \\$acknowledged with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/ApplicationStateCookie.php
+
+ -
+ message: "#^Parameter \\#1 \\$acknowledged of method Icinga\\\\Web\\\\ApplicationStateCookie\\:\\:setAcknowledgedMessages\\(\\) expects array, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/ApplicationStateCookie.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\ApplicationStateCookie\\:\\:\\$acknowledgedMessages type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/ApplicationStateCookie.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\:\\:getPageSize\\(\\) should return int but returns int\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\:\\:handleSortControlSubmit\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\:\\:httpBadRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\:\\:httpNotFound\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\:\\:renderForm\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\:\\:setupFilterControl\\(\\) has parameter \\$filterColumns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\:\\:setupFilterControl\\(\\) has parameter \\$preserveParams with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\:\\:setupFilterControl\\(\\) has parameter \\$searchColumns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\:\\:setupSortControl\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\:\\:setupSortControl\\(\\) has parameter \\$defaults with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller.php
+
+ -
+ message: "#^PHPDoc tag @param references unknown parameter\\: \\$arg$#"
+ count: 2
+ path: library/Icinga/Web/Controller.php
+
+ -
+ message: "#^Parameter \\#1 \\$count of method Icinga\\\\Data\\\\Limitable\\:\\:limit\\(\\) expects int\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method Icinga\\\\Web\\\\Url\\:\\:setParam\\(\\) expects array\\|bool\\|string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Web/Controller.php
+
+ -
+ message: "#^Access to an undefined property Zend_Controller_Action_HelperBroker\\:\\:\\$viewRenderer\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Action_HelperBroker\\:\\:layout\\(\\)\\.$#"
+ count: 8
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Action_Helper_Abstract\\:\\:getLayout\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Action_Helper_Abstract\\:\\:getResponseSegment\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Action_Helper_Abstract\\:\\:setLayout\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:Config\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:Config\\(\\) has parameter \\$file with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:Window\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:__construct\\(\\) has parameter \\$invokeArgs with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:assertHttpMethod\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:assertPermission\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:disableAutoRefresh\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:getRestrictions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:getTabs\\(\\) should return Icinga\\\\Web\\\\Widget\\\\Tabs but returns null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:ignoreXhrBody\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:isXhr\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:newSendAsPdf\\(\\) should always throw an exception or terminate script execution but doesn't do that\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:postDispatchXhr\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:prepareInit\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:redirectToLogin\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:reloadCss\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:rerenderLayout\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:sendAsPdf\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:shutdownSession\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(mixed\\)\\: mixed\\)\\|null, 'strtoupper' given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Parameter \\#1 \\$callback of function call_user_func_array expects callable\\(\\)\\: mixed, array\\{\\$this\\(Icinga\\\\Web\\\\Controller\\\\ActionController\\), non\\-falsy\\-string\\} given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function base64_encode expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function rawurlencode expects string, null given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Parameter \\#1 \\$title of method Icinga\\\\Module\\\\Pdfexport\\\\PrintableHtmlDocument\\:\\:setTitle\\(\\) expects string, null given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Parameter \\#2 \\$args of function call_user_func_array expects array\\<int\\|string, mixed\\>, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Parameter \\#2 \\$args of method Zend_Controller_Action\\:\\:__call\\(\\) expects array, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Parameter \\#3 \\$default of method Icinga\\\\User\\\\Preferences\\:\\:getValue\\(\\) expects null, false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Parameter \\#3 \\$default of method Icinga\\\\User\\\\Preferences\\:\\:getValue\\(\\) expects null, float given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Parameter \\#3 \\$default of method Icinga\\\\User\\\\Preferences\\:\\:getValue\\(\\) expects null, true given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:\\$autorefreshInterval \\(int\\) does not accept float\\|int\\<1, max\\>\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:\\$autorefreshInterval \\(int\\) does not accept null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:\\$reloadCss has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:\\$rerenderLayout has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Controller\\\\ActionController\\:\\:\\$window has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\AuthBackendController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/AuthBackendController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\AuthBackendController\\:\\:loadUserBackends\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/AuthBackendController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\AuthBackendController\\:\\:loadUserGroupBackends\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/AuthBackendController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\BasePreferenceController\\:\\:createProvidedTabs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/BasePreferenceController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ControllerTabCollector\\:\\:collectModuleTabs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ControllerTabCollector.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ControllerTabCollector\\:\\:createModuleConfigurationTabs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ControllerTabCollector.php
+
+ -
+ message: "#^Parameter \\#1 \\$content of method Zend_Controller_Response_Abstract\\:\\:appendBody\\(\\) expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/Dispatcher.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ModuleActionController\\:\\:Config\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ModuleActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ModuleActionController\\:\\:Config\\(\\) has parameter \\$file with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ModuleActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ModuleActionController\\:\\:moduleInit\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ModuleActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ModuleActionController\\:\\:postDispatchXhr\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ModuleActionController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\ModuleActionController\\:\\:prepareInit\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ModuleActionController.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Controller\\\\ModuleActionController\\:\\:\\$config has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ModuleActionController.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Controller\\\\ModuleActionController\\:\\:\\$configs has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ModuleActionController.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Controller\\\\ModuleActionController\\:\\:\\$module has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/ModuleActionController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getResponse\\(\\)\\.$#"
+ count: 6
+ path: library/Icinga/Web/Controller/StaticController.php
+
+ -
+ message: "#^Cannot access offset 'ino' on array\\{0\\: int, 1\\: int, 2\\: int, 3\\: int, 4\\: int, 5\\: int, 6\\: int, 7\\: int, \\.\\.\\.\\}\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/StaticController.php
+
+ -
+ message: "#^Cannot access offset 'mtime' on array\\{0\\: int, 1\\: int, 2\\: int, 3\\: int, 4\\: int, 5\\: int, 6\\: int, 7\\: int, \\.\\.\\.\\}\\|false\\.$#"
+ count: 2
+ path: library/Icinga/Web/Controller/StaticController.php
+
+ -
+ message: "#^Cannot access offset 'size' on array\\{0\\: int, 1\\: int, 2\\: int, 3\\: int, 4\\: int, 5\\: int, 6\\: int, 7\\: int, \\.\\.\\.\\}\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/StaticController.php
+
+ -
+ message: "#^Cannot call method getName\\(\\) on mixed\\.$#"
+ count: 3
+ path: library/Icinga/Web/Controller/StaticController.php
+
+ -
+ message: "#^Cannot call method getStaticAssetPath\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/StaticController.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Controller\\\\StaticController\\:\\:handle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/StaticController.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function str_pad expects string, int given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Controller/StaticController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getRequest\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Web/Cookie.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Cookie\\:\\:getDomain\\(\\) should return string but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Cookie.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Cookie\\:\\:getPath\\(\\) should return string but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Cookie.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of method Icinga\\\\Web\\\\Cookie\\:\\:setValue\\(\\) expects string, null given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Cookie.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Cookie\\:\\:\\$domain \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Cookie.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Cookie\\:\\:\\$path \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Cookie.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Cookie\\:\\:\\$value \\(string\\) does not accept string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Cookie.php
+
+ -
+ message: "#^Class Icinga\\\\Web\\\\CookieSet implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Web/CookieSet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\CookieSet\\:\\:getIterator\\(\\) return type with generic class ArrayIterator does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Web/CookieSet.php
+
+ -
+ message: "#^Cannot call method hasChildNodes\\(\\) on DOMNode\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Dom/DomNodeIterator.php
+
+ -
+ message: "#^Class Icinga\\\\Web\\\\Dom\\\\DomNodeIterator implements generic interface RecursiveIterator but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Web/Dom/DomNodeIterator.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Dom\\\\DomNodeIterator\\:\\:current\\(\\) should return DOMNode\\|null but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Dom/DomNodeIterator.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Dom\\\\DomNodeIterator\\:\\:key\\(\\) should return int but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Dom/DomNodeIterator.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Dom\\\\DomNodeIterator\\:\\:\\$children with generic class IteratorIterator does not specify its types\\: TKey, TValue, TIterator$#"
+ count: 1
+ path: library/Icinga/Web/Dom/DomNodeIterator.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\FileCache\\:\\:etagForFiles\\(\\) has parameter \\$files with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/FileCache.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\FileCache\\:\\:etagMatchesFiles\\(\\) has parameter \\$files with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/FileCache.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\FileCache\\:\\:store\\(\\) should return bool but returns int\\<0, max\\>\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Web/FileCache.php
+
+ -
+ message: "#^Parameter \\#2 \\$permissions of function mkdir expects int, float\\|int given\\.$#"
+ count: 2
+ path: library/Icinga/Web/FileCache.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\FileCache\\:\\:\\$instances type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/FileCache.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getFrontController\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getRequest\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Call to an undefined method Zend_Form_Decorator_Abstract\\:\\:setAccessible\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Call to an undefined method Zend_Form_Decorator_Abstract\\:\\:setRequiredSuffix\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Cannot access offset 'attribs' on mixed\\.$#"
+ count: 2
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Cannot access offset 'class' on mixed\\.$#"
+ count: 2
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Cannot access offset 'data\\-progress\\-label' on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Cannot access offset 'decorators' on mixed\\.$#"
+ count: 2
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Cannot access offset 'type' on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Cannot call method escape\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 3
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Cannot call method format\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Cannot call method isChecked\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Cannot call method setEscape\\(\\) on Zend_Form_Decorator_Abstract\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:addDescription\\(\\) has parameter \\$description with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:addHint\\(\\) has parameter \\$hint with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:addNotification\\(\\) has parameter \\$notification with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:assertPermission\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:create\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:error\\(\\) has parameter \\$message with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:getDescriptions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:getHints\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:getName\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:getNotifications\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:getRequestData\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:info\\(\\) has parameter \\$message with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:isValid\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:isValidPartial\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:populate\\(\\) has parameter \\$defaults with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:preserveDefaults\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:preserveDefaults\\(\\) has parameter \\$defaults with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:setDefaults\\(\\) has parameter \\$values with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:setDescriptions\\(\\) has parameter \\$descriptions with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:setHints\\(\\) has parameter \\$hints with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:setNotifications\\(\\) has parameter \\$notifications with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:warning\\(\\) has parameter \\$message with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\:\\:wasSent\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Parameter \\#1 \\$message of method Zend_Form\\:\\:addErrorMessage\\(\\) expects string, array\\|string given\\.$#"
+ count: 3
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of function array_key_exists expects array, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Parameter \\#2 \\$name of method Zend_Form\\:\\:addSubForm\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Parameter \\#3 \\$options of method Zend_Form\\:\\:createElement\\(\\) expects array\\|Zend_Config\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\:\\:\\$defaultElementDecorators type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\:\\:\\$descriptions type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\:\\:\\$hints type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\:\\:\\$notifications type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getViewRenderer\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/Autosubmit.php
+
+ -
+ message: "#^Cannot call method getId\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/Autosubmit.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\Decorator\\\\Autosubmit\\:\\:getAccessible\\(\\) should return bool but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/Autosubmit.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\\\Decorator\\\\Autosubmit\\:\\:\\$accessible \\(bool\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/Autosubmit.php
+
+ -
+ message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/ElementDoubler.php
+
+ -
+ message: "#^Call to an undefined method Zend_Form\\|Zend_Form_Element\\:\\:getElement\\(\\)\\.$#"
+ count: 3
+ path: library/Icinga/Web/Form/Decorator/ElementDoubler.php
+
+ -
+ message: "#^Parameter \\#1 \\$element of method Icinga\\\\Web\\\\Form\\\\Decorator\\\\ElementDoubler\\:\\:applyAttributes\\(\\) expects Zend_Form_Element, Zend_Form_Element\\|null given\\.$#"
+ count: 2
+ path: library/Icinga/Web/Form/Decorator/ElementDoubler.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Zend_Form_Element\\:\\:setAttrib\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/ElementDoubler.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getViewRenderer\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/FormDescriptions.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Interface\\:\\:escape\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Web/Form/Decorator/FormDescriptions.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Interface\\:\\:propertiesToString\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/FormDescriptions.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\Decorator\\\\FormDescriptions\\:\\:recurseForm\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/FormDescriptions.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\Decorator\\\\FormDescriptions\\:\\:recurseForm\\(\\) should return array but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/FormDescriptions.php
+
+ -
+ message: "#^Call to an undefined method Zend_Form_Decorator_Abstract\\:\\:setRequiredSuffix\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/FormHints.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Interface\\:\\:escape\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Web/Form/Decorator/FormHints.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Interface\\:\\:propertiesToString\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/FormHints.php
+
+ -
+ message: "#^Cannot call method translate\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/FormHints.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\Decorator\\\\FormHints\\:\\:__construct\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/FormHints.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\Decorator\\\\FormHints\\:\\:recurseForm\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/FormHints.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\Decorator\\\\FormHints\\:\\:recurseForm\\(\\) should return array but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/FormHints.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\\\Decorator\\\\FormHints\\:\\:\\$blacklist type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/FormHints.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getViewRenderer\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/FormNotifications.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Interface\\:\\:escape\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Web/Form/Decorator/FormNotifications.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Interface\\:\\:propertiesToString\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/FormNotifications.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\Decorator\\\\FormNotifications\\:\\:recurseForm\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/FormNotifications.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getViewRenderer\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/Help.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getViewRenderer\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Decorator/Spinner.php
+
+ -
+ message: "#^Access to an undefined property Icinga\\\\Web\\\\Form\\\\Element\\\\Button\\:\\:\\$content\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Element/Button.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getRequest\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Element/Button.php
+
+ -
+ message: "#^Cannot access offset 'ignore' on array\\|string\\|Zend_Config\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Element/Button.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\Element\\\\Button\\:\\:__construct\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Element/Button.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\Element\\\\Button\\:\\:__construct\\(\\) has parameter \\$spec with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Element/Button.php
+
+ -
+ message: "#^Parameter \\#1 \\$timestamp of method DateTime\\:\\:setTimestamp\\(\\) expects int, string given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Element/DateTimePicker.php
+
+ -
+ message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Element/DateTimePicker.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Element/Number.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\Element\\\\Textarea\\:\\:__construct\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Element/Textarea.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\Element\\\\Textarea\\:\\:__construct\\(\\) has parameter \\$spec with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Element/Textarea.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\ErrorLabeller\\:\\:__construct\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/ErrorLabeller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\ErrorLabeller\\:\\:_loadTranslationData\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/ErrorLabeller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\ErrorLabeller\\:\\:_loadTranslationData\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/ErrorLabeller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\ErrorLabeller\\:\\:createMessages\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/ErrorLabeller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\ErrorLabeller\\:\\:createMessages\\(\\) has parameter \\$element with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/ErrorLabeller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\ErrorLabeller\\:\\:translate\\(\\) has parameter \\$messageId with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/ErrorLabeller.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\ErrorLabeller\\:\\:translate\\(\\) should return string but returns array\\|string\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/ErrorLabeller.php
+
+ -
+ message: "#^Parameter \\#1 \\$key of function array_key_exists expects int\\|string, array\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/ErrorLabeller.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\\\ErrorLabeller\\:\\:\\$messages has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/ErrorLabeller.php
+
+ -
+ message: "#^PHPDoc type bool\\|null of property Icinga\\\\Web\\\\Form\\\\FormElement\\:\\:\\$_disableLoadDefaultDecorators is not covariant with PHPDoc type bool of overridden property Zend_Form_Element\\:\\:\\$_disableLoadDefaultDecorators\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/FormElement.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\\\Validator\\\\DateFormatValidator\\:\\:\\$_messageTemplates type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Validator/DateFormatValidator.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\\\Validator\\\\DateFormatValidator\\:\\:\\$validChars type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Validator/DateFormatValidator.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\\\Validator\\\\DateFormatValidator\\:\\:\\$validSeparators type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Validator/DateFormatValidator.php
+
+ -
+ message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Validator/DateTimeValidator.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\\\Validator\\\\DateTimeValidator\\:\\:\\$_messageTemplates type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Validator/DateTimeValidator.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\\\Validator\\\\DateTimeValidator\\:\\:\\$local has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Validator/DateTimeValidator.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of static method Icinga\\\\Util\\\\StringHelper\\:\\:findSimilar\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Validator/InArray.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Web/Form/Validator/InArray.php
+
+ -
+ message: "#^Parameter \\#1 \\$url of static method Icinga\\\\Web\\\\Url\\:\\:fromPath\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Validator/InternalUrlValidator.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\\\Validator\\\\ReadablePathValidator\\:\\:\\$_messageTemplates type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Validator/ReadablePathValidator.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\\\Validator\\\\TimeFormatValidator\\:\\:\\$_messageTemplates type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Validator/TimeFormatValidator.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\\\Validator\\\\TimeFormatValidator\\:\\:\\$validChars type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Validator/TimeFormatValidator.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\\\Validator\\\\TimeFormatValidator\\:\\:\\$validSeparators type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Validator/TimeFormatValidator.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Form\\\\Validator\\\\WritablePathValidator\\:\\:setRequireExistence\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Validator/WritablePathValidator.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Form\\\\Validator\\\\WritablePathValidator\\:\\:\\$_messageTemplates type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Form/Validator/WritablePathValidator.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\CookieHelper\\:\\:cleanupCheck\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/CookieHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\CookieHelper\\:\\:provideCheck\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/CookieHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\HtmlPurifier\\:\\:__construct\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/HtmlPurifier.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\HtmlPurifier\\:\\:configure\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/HtmlPurifier.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\HtmlPurifier\\:\\:process\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/HtmlPurifier.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\HtmlPurifier\\:\\:purify\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/HtmlPurifier.php
+
+ -
+ message: "#^Parameter \\#2 \\$config of method HTMLPurifier\\:\\:purify\\(\\) expects HTMLPurifier_Config\\|null, array\\|Closure\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/HtmlPurifier.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\Markdown\\:\\:line\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/Markdown.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\Markdown\\:\\:line\\(\\) has parameter \\$config with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/Markdown.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\Markdown\\:\\:line\\(\\) has parameter \\$content with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/Markdown.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\Markdown\\:\\:text\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/Markdown.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\Markdown\\:\\:text\\(\\) has parameter \\$config with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/Markdown.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\Markdown\\:\\:text\\(\\) has parameter \\$content with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/Markdown.php
+
+ -
+ message: "#^Cannot call method getAnonymousModule\\(\\) on HTMLPurifier_HTMLDefinition\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/Markdown/LinkTransformer.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\Markdown\\\\LinkTransformer\\:\\:attachTo\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/Markdown/LinkTransformer.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\Markdown\\\\LinkTransformer\\:\\:transform\\(\\) has parameter \\$attr with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/Markdown/LinkTransformer.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Helper\\\\Markdown\\\\LinkTransformer\\:\\:transform\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Helper/Markdown/LinkTransformer.php
+
+ -
+ message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getRequest\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Cannot call method getJsAssetPath\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Cannot call method getJsAssets\\(\\) on mixed\\.$#"
+ count: 2
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Cannot call method getName\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\JavaScript\\:\\:send\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\JavaScript\\:\\:sendMinified\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Parameter \\#1 \\$js of static method Icinga\\\\Web\\\\JavaScript\\:\\:optimizeDefine\\(\\) expects string, string\\|false given\\.$#"
+ count: 2
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function ltrim expects string, bool\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strlen expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function trim expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_match expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\JavaScript\\:\\:\\$baseFiles has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\JavaScript\\:\\:\\$jsFiles has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\JavaScript\\:\\:\\$vendorFiles has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/JavaScript.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_match_all expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/LessCompiler.php
+
+ -
+ message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/LessCompiler.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\LessCompiler\\:\\:\\$lessFiles \\(array\\<string\\>\\) does not accept array\\<bool\\|string\\>\\.$#"
+ count: 1
+ path: library/Icinga/Web/LessCompiler.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\LessCompiler\\:\\:\\$moduleLessFiles type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/LessCompiler.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\LessCompiler\\:\\:\\$theme \\(string\\) does not accept string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/LessCompiler.php
+
+ -
+ message: "#^Cannot call method addChild\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Menu.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Menu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Menu\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Menu.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getRequest\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Cannot access property \\$state on mixed\\.$#"
+ count: 3
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:assemble\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:assembleCogMenuItem\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:assembleCogMenuItem\\(\\) has parameter \\$cogMenuItem with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:assembleLevel2Nav\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:assembleUserMenuItem\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:createCogMenuItem\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:createLevel2Menu\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:createLevel2MenuItem\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:createLevel2MenuItem\\(\\) has parameter \\$item with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:createLevel2MenuItem\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:createUserMenuItem\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:getHealthCount\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:isSelectedItem\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:isSelectedItem\\(\\) has parameter \\$item with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:\\$children has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:\\$selected has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\ConfigMenu\\:\\:\\$state has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/ConfigMenu.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\DashboardPane\\:\\:getDashlets\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/DashboardPane.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\DashboardPane\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/DashboardPane.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\DashboardPane\\:\\:setDashlets\\(\\) has parameter \\$dashlets with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/DashboardPane.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\DashboardPane\\:\\:setDisabled\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/DashboardPane.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\DashboardPane\\:\\:\\$dashlets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/DashboardPane.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\DashboardPane\\:\\:\\$disabled has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/DashboardPane.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\DropdownItem\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/DropdownItem.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getSharedNavigation\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Cannot call method can\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Cannot call method getName\\(\\) on mixed\\.$#"
+ count: 2
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Cannot call method getNavigation\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Cannot call method setName\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Class Icinga\\\\Web\\\\Navigation\\\\Navigation implements generic interface ArrayAccess but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Class Icinga\\\\Web\\\\Navigation\\\\Navigation implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:addItem\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:createItem\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:createItem\\(\\) has parameter \\$properties with no value type specified in iterable type array\\|Icinga\\\\Data\\\\ConfigObject\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:createItem\\(\\) should return Icinga\\\\Web\\\\Navigation\\\\NavigationItem but returns object\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:findItem\\(\\) should return Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\|null but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:fromArray\\(\\) has parameter \\$array with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:fromConfig\\(\\) has parameter \\$config with no value type specified in iterable type Traversable\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:fromConfig\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:fromConfig\\(\\) has parameter \\$config with no value type specified in iterable type array\\|Traversable\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:getActiveItem\\(\\) should return Icinga\\\\Web\\\\Navigation\\\\NavigationItem but returns Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:getItemTypeConfiguration\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:getItems\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Parameter \\#1 \\$item of method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:conflictsWith\\(\\) expects Icinga\\\\Web\\\\Navigation\\\\NavigationItem, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Parameter \\#1 \\$item of method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:merge\\(\\) expects Icinga\\\\Web\\\\Navigation\\\\NavigationItem, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:addItem\\(\\) expects Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\|string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Parameter \\#2 \\$replacement of function preg_replace expects array\\|string, int given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:\\$items \\(array\\<Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\>\\) does not accept array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\Navigation\\:\\:\\$types type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Navigation.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getRequest\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Cannot call method getLocalUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Cannot call method setParent\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Class Icinga\\\\Web\\\\Navigation\\\\NavigationItem implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:__construct\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:createRenderer\\(\\) has parameter \\$name with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:getAttributes\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:getEscapedName\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:getUrlParameters\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:setAttributes\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:setChildren\\(\\) has parameter \\$children with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:setChildren\\(\\) has parameter \\$children with no value type specified in iterable type array\\|Icinga\\\\Web\\\\Navigation\\\\Navigation\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:setProperties\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:setRenderer\\(\\) has parameter \\$renderer with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:setUrlParameters\\(\\) has parameter \\$urlParameters with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:\\$attributes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:\\$urlParameters type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/NavigationItem.php
+
+ -
+ message: "#^Cannot access property \\$message on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/HealthNavigationRenderer.php
+
+ -
+ message: "#^Cannot access property \\$state on mixed\\.$#"
+ count: 3
+ path: library/Icinga/Web/Navigation/Renderer/HealthNavigationRenderer.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getViewRenderer\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Renderer\\\\NavigationItemRenderer\\:\\:__construct\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Renderer\\\\NavigationItemRenderer\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Renderer\\\\NavigationItemRenderer\\:\\:setOptions\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\Renderer\\\\NavigationItemRenderer\\:\\:\\$internalLinkTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getViewRenderer\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php
+
+ -
+ message: "#^Class Icinga\\\\Web\\\\Navigation\\\\Renderer\\\\NavigationRenderer implements generic interface RecursiveIterator but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Navigation\\\\Renderer\\\\NavigationRenderer\\:\\:current\\(\\) should return Icinga\\\\Web\\\\Navigation\\\\NavigationItem but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\Renderer\\\\NavigationRenderer\\:\\:\\$content type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\Renderer\\\\NavigationRenderer\\:\\:\\$iterator \\(ArrayIterator\\) does not accept Traversable\\<mixed, mixed\\>\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\Renderer\\\\NavigationRenderer\\:\\:\\$iterator with generic class ArrayIterator does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php
+
+ -
+ message: "#^Class Icinga\\\\Web\\\\Navigation\\\\Renderer\\\\RecursiveNavigationRenderer extends generic class RecursiveIteratorIterator but does not specify its types\\: T$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/RecursiveNavigationRenderer.php
+
+ -
+ message: "#^Parameter \\#1 \\$icon of method Icinga\\\\Web\\\\Navigation\\\\NavigationItem\\:\\:setIcon\\(\\) expects string, null given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/RecursiveNavigationRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\Renderer\\\\RecursiveNavigationRenderer\\:\\:\\$content type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/RecursiveNavigationRenderer.php
+
+ -
+ message: "#^Cannot call method getRenderer\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\Renderer\\\\SummaryNavigationItemRenderer\\:\\:\\$severityStateMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Navigation\\\\Renderer\\\\SummaryNavigationItemRenderer\\:\\:\\$stateSeverityMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\Session\\:\\:get\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Notification.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\Session\\:\\:set\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Notification.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\Session\\:\\:write\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Notification.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Notification\\:\\:addMessage\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Notification.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Notification\\:\\:error\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Notification.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Notification\\:\\:info\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Notification.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Notification\\:\\:popMessages\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Notification.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Notification\\:\\:success\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Notification.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Notification\\:\\:warning\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Notification.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Notification\\:\\:\\$messages type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Notification.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Notification\\:\\:\\$session \\(Icinga\\\\Web\\\\Session\\) does not accept Icinga\\\\Web\\\\Session\\\\Session\\.$#"
+ count: 1
+ path: library/Icinga/Web/Notification.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Limitable\\:\\:fetchAll\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Paginator/Adapter/QueryAdapter.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Paginator\\\\Adapter\\\\QueryAdapter\\:\\:getItems\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Paginator/Adapter/QueryAdapter.php
+
+ -
+ message: "#^Method Icinga_Web_Paginator_ScrollingStyle_SlidingWithBorder\\:\\:getPages\\(\\) has parameter \\$paginator with no value type specified in iterable type Zend_Paginator\\.$#"
+ count: 1
+ path: library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php
+
+ -
+ message: "#^Method Icinga_Web_Paginator_ScrollingStyle_SlidingWithBorder\\:\\:getPages\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php
+
+ -
+ message: "#^Cannot access property \\$passphrase on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMe.php
+
+ -
+ message: "#^Cannot access property \\$username on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMe.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\RememberMe\\:\\:getAllByUsername\\(\\) has parameter \\$username with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMe.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\RememberMe\\:\\:getAllByUsername\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMe.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\RememberMe\\:\\:getAllUser\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMe.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\RememberMe\\:\\:removeExpired\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMe.php
+
+ -
+ message: "#^Parameter \\#1 \\$domain of method Icinga\\\\User\\:\\:setDomain\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMe.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\RememberMeUserDevicesList\\:\\:assemble\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMeUserDevicesList.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\RememberMeUserDevicesList\\:\\:getDevicesList\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMeUserDevicesList.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\RememberMeUserDevicesList\\:\\:setDevicesList\\(\\) has parameter \\$devicesList with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMeUserDevicesList.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\RememberMeUserDevicesList\\:\\:\\$devicesList type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMeUserDevicesList.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\RememberMeUserList\\:\\:assemble\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMeUserList.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\RememberMeUserList\\:\\:getUsers\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMeUserList.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\RememberMeUserList\\:\\:setUsers\\(\\) has parameter \\$users with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMeUserList.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\RememberMeUserList\\:\\:\\$users type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/RememberMeUserList.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getResponse\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Request.php
+
+ -
+ message: "#^Parameter \\#1 \\$headerValue of method Icinga\\\\Web\\\\Request\\:\\:extractMediaType\\(\\) expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Request.php
+
+ -
+ message: "#^Parameter \\#1 \\$json of static method Icinga\\\\Util\\\\Json\\:\\:decode\\(\\) expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Request.php
+
+ -
+ message: "#^Parameter \\#1 \\$params of static method Icinga\\\\Web\\\\Url\\:\\:fromRequest\\(\\) expects array\\|Icinga\\\\Web\\\\UrlParams, \\$this\\(Icinga\\\\Web\\\\Request\\) given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Request.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getRequest\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getViewRenderer\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Response\\:\\:getHeader\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Response\\:\\:prepare\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Response\\:\\:sendCookies\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response.php
+
+ -
+ message: "#^Parameter \\#1 \\$url of static method Icinga\\\\Web\\\\Url\\:\\:fromPath\\(\\) expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method Icinga\\\\Web\\\\UrlParams\\:\\:set\\(\\) expects string, true given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method Zend_Controller_Response_Abstract\\:\\:setHeader\\(\\) expects string, int given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Action_Helper_Abstract\\:\\:setNoRender\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response/JsonResponse.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Response\\\\JsonResponse\\:\\:getFailData\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response/JsonResponse.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Response\\\\JsonResponse\\:\\:getSuccessData\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response/JsonResponse.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Response\\\\JsonResponse\\:\\:setFailData\\(\\) has parameter \\$failData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response/JsonResponse.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Response\\\\JsonResponse\\:\\:setSuccessData\\(\\) has parameter \\$successData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response/JsonResponse.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Response\\\\JsonResponse\\:\\:\\$failData type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response/JsonResponse.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Response\\\\JsonResponse\\:\\:\\$successData type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Response/JsonResponse.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\Php72Session\\:\\:open\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/Php72Session.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\PhpSession\\:\\:__construct\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/PhpSession.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\PhpSession\\:\\:clearCookies\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/PhpSession.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\PhpSession\\:\\:create\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/PhpSession.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\PhpSession\\:\\:create\\(\\) should return static\\(Icinga\\\\Web\\\\Session\\\\PhpSession\\) but returns Icinga\\\\Web\\\\Session\\\\PhpSession\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/PhpSession.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\PhpSession\\:\\:getId\\(\\) should return string but returns string\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/PhpSession.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\PhpSession\\:\\:open\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/PhpSession.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\PhpSession\\:\\:purge\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/PhpSession.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\PhpSession\\:\\:read\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/PhpSession.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\PhpSession\\:\\:refreshId\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/PhpSession.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\PhpSession\\:\\:write\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/PhpSession.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of function setcookie expects string, string\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/PhpSession.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\Session\\:\\:clear\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/Session.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\Session\\:\\:purge\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/Session.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\Session\\:\\:read\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/Session.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\Session\\:\\:refreshId\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/Session.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\Session\\:\\:removeNamespace\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/Session.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\Session\\:\\:write\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/Session.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Session\\\\Session\\:\\:\\$namespaces type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/Session.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Session\\\\Session\\:\\:\\$removedNamespaces type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/Session.php
+
+ -
+ message: "#^Class Icinga\\\\Web\\\\Session\\\\SessionNamespace implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:clear\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:delete\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:getAll\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:getByRef\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:getByRef\\(\\) has parameter \\$default with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:getByRef\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:getIterator\\(\\) return type with generic class ArrayIterator does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:setAll\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:setAll\\(\\) has parameter \\$values with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:setByRef\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:setByRef\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:setByRef\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:\\$removed type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Session\\\\SessionNamespace\\:\\:\\$values type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Session/SessionNamespace.php
+
+ -
+ message: "#^Binary operation \"\\.\" between non\\-falsy\\-string and 'none'\\|array\\|null results in an error\\.$#"
+ count: 1
+ path: library/Icinga/Web/StyleSheet.php
+
+ -
+ message: "#^Cannot call method getCssAssets\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/StyleSheet.php
+
+ -
+ message: "#^Cannot call method getPreferences\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/StyleSheet.php
+
+ -
+ message: "#^Cannot call method getThemeFile\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/StyleSheet.php
+
+ -
+ message: "#^Constant Icinga\\\\Web\\\\StyleSheet\\:\\:THEME_WHITELIST type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/StyleSheet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\StyleSheet\\:\\:collect\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/StyleSheet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\StyleSheet\\:\\:forPdf\\(\\) should return \\$this\\(Icinga\\\\Web\\\\StyleSheet\\) but returns Icinga\\\\Web\\\\StyleSheet\\.$#"
+ count: 1
+ path: library/Icinga/Web/StyleSheet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\StyleSheet\\:\\:getThemeFile\\(\\) has parameter \\$theme with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/StyleSheet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\StyleSheet\\:\\:send\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/StyleSheet.php
+
+ -
+ message: "#^Parameter \\#1 \\$content of method Zend_Controller_Response_Abstract\\:\\:setBody\\(\\) expects string, bool\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/Web/StyleSheet.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/StyleSheet.php
+
+ -
+ message: "#^Parameter \\#3 \\$default of method Icinga\\\\User\\\\Preferences\\:\\:getValue\\(\\) expects null, string given\\.$#"
+ count: 1
+ path: library/Icinga/Web/StyleSheet.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\StyleSheet\\:\\:\\$app \\(Icinga\\\\Application\\\\EmbeddedWeb\\) does not accept Icinga\\\\Application\\\\ApplicationBootstrap\\.$#"
+ count: 1
+ path: library/Icinga/Web/StyleSheet.php
+
+ -
+ message: "#^Argument of an invalid type array\\|Icinga\\\\Web\\\\UrlParams supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Binary operation \"\\.\" between string and 0\\|0\\.0\\|array\\{\\}\\|string\\|false\\|null results in an error\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getRequest\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:addParams\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:buildPathQueryAndFragment\\(\\) has parameter \\$querySeparator with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:fromPath\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:fromRequest\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:getAbsoluteUrl\\(\\) has parameter \\$separator with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:getQueryString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:getQueryString\\(\\) has parameter \\$separator with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:getRelativeUrl\\(\\) has parameter \\$separator with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:getUrlWithout\\(\\) has parameter \\$keyOrArrayOfKeys with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:onlyWith\\(\\) has parameter \\$keyOrArrayOfKeys with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:overwriteParams\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:remove\\(\\) has parameter \\$keyOrArrayOfKeys with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:setParam\\(\\) has parameter \\$value with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:setParams\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:setQueryString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:setQueryString\\(\\) has parameter \\$queryString with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:with\\(\\) has parameter \\$param with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:without\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Url\\:\\:without\\(\\) has parameter \\$keyOrArrayOfKeys with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Parameter \\#1 \\$port of method Icinga\\\\Web\\\\Url\\:\\:setPort\\(\\) expects string, int\\<0, 65535\\> given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Parameter \\#2 \\$default of method Icinga\\\\Web\\\\UrlParams\\:\\:get\\(\\) expects bool\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Parameter \\#2 \\$default of method Icinga\\\\Web\\\\UrlParams\\:\\:shift\\(\\) expects string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method Icinga\\\\Web\\\\UrlParams\\:\\:set\\(\\) expects string, array\\|bool\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Url.php
+
+ -
+ message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Cannot use array destructuring on array\\<int, string\\>\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:addEncoded\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:addEncoded\\(\\) has parameter \\$param with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:addEncoded\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:addParamToIndex\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:addParamToIndex\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:addParamToIndex\\(\\) has parameter \\$param with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:addValues\\(\\) has parameter \\$param with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:addValues\\(\\) has parameter \\$values with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:cleanupValue\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:cleanupValue\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:clearValues\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:fromQueryString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:fromQueryString\\(\\) has parameter \\$queryString with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:getValues\\(\\) has parameter \\$default with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:indexLastOne\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:isEmpty\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:mergeValues\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:mergeValues\\(\\) has parameter \\$param with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:mergeValues\\(\\) has parameter \\$values with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:parseQueryString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:parseQueryString\\(\\) has parameter \\$queryString with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:parseQueryStringPart\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:parseQueryStringPart\\(\\) has parameter \\$part with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:reIndexAll\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:remove\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:remove\\(\\) has parameter \\$param with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:setSeparator\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:setSeparator\\(\\) has parameter \\$separator with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:setValues\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:setValues\\(\\) has parameter \\$param with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:setValues\\(\\) has parameter \\$values with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:toString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:toString\\(\\) has parameter \\$separator with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:urlEncode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:urlEncode\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:without\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UrlParams\\:\\:without\\(\\) has parameter \\$param with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^PHPDoc tag @param references unknown parameter\\: \\$value$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Parameter \\#1 \\$param of method Icinga\\\\Web\\\\UrlParams\\:\\:add\\(\\) expects string, array\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strlen expects string, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\UrlParams\\:\\:\\$index has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\UrlParams\\:\\:\\$params has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\UrlParams\\:\\:\\$separator has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UrlParams.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UserAgent\\:\\:__construct\\(\\) has parameter \\$agent with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/UserAgent.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\UserAgent\\:\\:getAgent\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/UserAgent.php
+
+ -
+ message: "#^Parameter \\#1 \\$haystack of function strstr expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Web/UserAgent.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Web/UserAgent.php
+
+ -
+ message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Web/View.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\:\\:__call\\(\\) has parameter \\$args with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/View.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\:\\:__call\\(\\) should return string but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/View.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\:\\:__construct\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/View.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\:\\:callHelperFunction\\(\\) has parameter \\$args with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/View.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\:\\:loadGlobalHelpers\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\View\\:\\:\\$helperFunctions has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\AppHealth\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type Traversable\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/AppHealth.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\AppHealth\\:\\:assemble\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/AppHealth.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\AppHealth\\:\\:getStateClass\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/AppHealth.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\AppHealth\\:\\:getStateClass\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/AppHealth.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\AppHealth\\:\\:getStateText\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/AppHealth.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\AppHealth\\:\\:getStateText\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/AppHealth.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\View\\\\AppHealth\\:\\:\\$data type has no value type specified in iterable type Traversable\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/AppHealth.php
+
+ -
+ message: "#^Cannot call method protectId\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/Helper/IcingaCheckbox.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\Helper\\\\IcingaCheckbox\\:\\:icingaCheckbox\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/Helper/IcingaCheckbox.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\Helper\\\\IcingaCheckbox\\:\\:icingaCheckbox\\(\\) has parameter \\$attribs with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/Helper/IcingaCheckbox.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\Helper\\\\IcingaCheckbox\\:\\:icingaCheckbox\\(\\) has parameter \\$checkedOptions with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/Helper/IcingaCheckbox.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\Helper\\\\IcingaCheckbox\\:\\:icingaCheckbox\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/Helper/IcingaCheckbox.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\Helper\\\\IcingaCheckbox\\:\\:icingaCheckbox\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/Helper/IcingaCheckbox.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\PrivilegeAudit\\:\\:__construct\\(\\) has parameter \\$roles with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/PrivilegeAudit.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\PrivilegeAudit\\:\\:assemble\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/PrivilegeAudit.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\PrivilegeAudit\\:\\:auditPermission\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/PrivilegeAudit.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\PrivilegeAudit\\:\\:auditPermission\\(\\) has parameter \\$permission with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/PrivilegeAudit.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\PrivilegeAudit\\:\\:auditRestriction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/PrivilegeAudit.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\PrivilegeAudit\\:\\:auditRestriction\\(\\) has parameter \\$restriction with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/PrivilegeAudit.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\PrivilegeAudit\\:\\:collectRestrictions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/PrivilegeAudit.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\PrivilegeAudit\\:\\:collectRestrictions\\(\\) has parameter \\$restrictionName with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/PrivilegeAudit.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\PrivilegeAudit\\:\\:createRestrictionLinks\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/PrivilegeAudit.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\PrivilegeAudit\\:\\:createRestrictionLinks\\(\\) has parameter \\$restrictionName with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/PrivilegeAudit.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\View\\\\PrivilegeAudit\\:\\:createRestrictionLinks\\(\\) has parameter \\$restrictions with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/View/PrivilegeAudit.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of function array_map expects array, array\\<int, string\\>\\|false given\\.$#"
+ count: 2
+ path: library/Icinga/Web/View/PrivilegeAudit.php
+
+ -
+ message: "#^Variable \\$this might not be defined\\.$#"
+ count: 8
+ path: library/Icinga/Web/View/helpers/format.php
+
+ -
+ message: "#^Variable \\$this might not be defined\\.$#"
+ count: 2
+ path: library/Icinga/Web/View/helpers/generic.php
+
+ -
+ message: "#^Undefined variable\\: \\$this$#"
+ count: 2
+ path: library/Icinga/Web/View/helpers/string.php
+
+ -
+ message: "#^Variable \\$this might not be defined\\.$#"
+ count: 4
+ path: library/Icinga/Web/View/helpers/string.php
+
+ -
+ message: "#^Variable \\$this might not be defined\\.$#"
+ count: 8
+ path: library/Icinga/Web/View/helpers/url.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\:\\:create\\(\\) has parameter \\$module_name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\:\\:create\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\:\\:create\\(\\) should return Icinga\\\\Web\\\\Widget\\\\AbstractWidget but returns object\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getViewRenderer\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/AbstractWidget.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\AbstractWidget\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/AbstractWidget.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\AbstractWidget\\:\\:\\$properties has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/AbstractWidget.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getResponse\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Announcements.php
+
+ -
+ message: "#^Cannot access property \\$hash on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Announcements.php
+
+ -
+ message: "#^Cannot access property \\$message on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Announcements.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Announcements\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Announcements.php
+
+ -
+ message: "#^Cannot call method getPreferences\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/ApplicationStateMessages.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\ApplicationStateMessages\\:\\:getMessages\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/ApplicationStateMessages.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\ApplicationStateMessages\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/ApplicationStateMessages.php
+
+ -
+ message: "#^Parameter \\#3 \\$default of method Icinga\\\\User\\\\Preferences\\:\\:getValue\\(\\) expects null, string given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/ApplicationStateMessages.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:__construct\\(\\) has parameter \\$color with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:__construct\\(\\) has parameter \\$end with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:__construct\\(\\) has parameter \\$start with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:calculateColor\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:createGrid\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:monthName\\(\\) has parameter \\$year with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:renderDay\\(\\) has parameter \\$day with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:renderHorizontal\\(\\) has parameter \\$grid with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:renderVertical\\(\\) has parameter \\$grid with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:renderWeekdayHorizontal\\(\\) has parameter \\$weeks with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:setColor\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:setColor\\(\\) has parameter \\$color with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:setData\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:setData\\(\\) has parameter \\$events with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:setOpacity\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:setOpacity\\(\\) has parameter \\$opacity with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:toDateStr\\(\\) has parameter \\$day with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:toDateStr\\(\\) has parameter \\$mon with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:toDateStr\\(\\) has parameter \\$year with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:tsToDateStr\\(\\) has parameter \\$timestamp with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:tsToDateStr\\(\\) never returns bool so it can be removed from the return type\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:weekdayName\\(\\) has parameter \\$weekday with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Parameter \\#2 \\$timestamp of function date expects int\\|null, int\\|false given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:\\$color has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:\\$data has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:\\$end has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:\\$maxValue has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:\\$opacity has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:\\$orientation has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:\\$start has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:\\$weekFlow has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\HistoryColorGrid\\:\\:\\$weekStartMonday has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/HistoryColorGrid.php
+
+ -
+ message: "#^Argument of an invalid type stdClass supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Abstract\\:\\:layout\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:__construct\\(\\) has parameter \\$colors with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:createFromStateSummary\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:createFromStateSummary\\(\\) has parameter \\$colors with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:createFromStateSummary\\(\\) has parameter \\$title with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:setColors\\(\\) has parameter \\$colors with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:setData\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:setSparklineClass\\(\\) has parameter \\$class with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^PHPDoc tag @var has invalid value \\(\\)\\: Unexpected token \"\\\\n \", expected type at offset 15$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:\\$class has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:\\$colors \\(array\\) does not accept array\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:\\$colors type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:\\$colorsHostStates has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:\\$colorsHostStatesHandledUnhandled has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:\\$colorsServiceStates has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:\\$colorsServiceStatesHandleUnhandled has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:\\$data type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:\\$size \\(int\\) does not accept int\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Chart\\\\InlinePie\\:\\:\\$title \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Chart/InlinePie.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\Config\\:\\:setUser\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Cannot call method getChildren\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Cannot call method getLabel\\(\\) on mixed\\.$#"
+ count: 3
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Cannot call method getUrl\\(\\) on mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:activate\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:getActivePane\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:getPaneKeyTitleArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:getPanes\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:loadUserDashboards\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:mergePanes\\(\\) has parameter \\$panes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:removePane\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:removePane\\(\\) has parameter \\$title with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:setUser\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^PHPDoc tag @var has invalid value \\(\\$current Pane\\)\\: Unexpected token \"\\$current\", expected type at offset 9$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^PHPDoc tag @var has invalid value \\(\\$pane Pane\\)\\: Unexpected token \"\\$pane\", expected type at offset 9$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:activate\\(\\) expects string, int\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:activate\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Parameter \\#1 \\$pane of method Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:hasPane\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Dashboard\\:\\:\\$panes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Abstract\\:\\:translate\\(\\)\\.$#"
+ count: 5
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Cannot call method getRelativeUrl\\(\\) on Icinga\\\\Web\\\\Url\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Cannot call method getUrlWithout\\(\\) on Icinga\\\\Web\\\\Url\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Cannot call method setParam\\(\\) on Icinga\\\\Web\\\\Url\\|null\\.$#"
+ count: 2
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Cannot clone non\\-object variable \\$url of type Icinga\\\\Web\\\\Url\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet\\:\\:fromIni\\(\\) has parameter \\$title with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet\\:\\:getName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet\\:\\:setDisabled\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet\\:\\:setName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet\\:\\:setName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet\\:\\:setPane\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet\\:\\:setTitle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^PHPDoc tag @var has invalid value \\(\\)\\: Unexpected token \"\\\\n \", expected type at offset 70$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Parameter \\#1 \\$url of static method Icinga\\\\Web\\\\Url\\:\\:fromPath\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet\\:\\:\\$name has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet\\:\\:\\$title has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet\\:\\:\\$url \\(Icinga\\\\Web\\\\Url\\|null\\) does not accept Icinga\\\\Web\\\\Url\\|string\\.$#"
+ count: 2
+ path: library/Icinga/Web/Widget/Dashboard/Dashlet.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:add\\(\\) has parameter \\$title with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Pane.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:add\\(\\) has parameter \\$url with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Pane.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:addDashlets\\(\\) has parameter \\$dashlets with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Pane.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:fromIni\\(\\) has parameter \\$title with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Pane.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:getDashlets\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Pane.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:removeDashlets\\(\\) has parameter \\$dashlets with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Pane.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Pane.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:setDisabled\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Pane.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:setName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Pane.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Pane.php
+
+ -
+ message: "#^Parameter \\#1 \\$title of method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:setTitle\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Pane.php
+
+ -
+ message: "#^Parameter \\#2 \\$url of class Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Dashlet constructor expects Icinga\\\\Web\\\\Url\\|string, string\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Pane.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\Pane\\:\\:\\$dashlets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/Pane.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Dashboard\\\\UserWidget\\:\\:setUserWidget\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Dashboard/UserWidget.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getFrontController\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:filters\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getColumn\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getExpression\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getOperatorName\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getSign\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Abstract\\:\\:icon\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Abstract\\:\\:propertiesToString\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Abstract\\:\\:qlink\\(\\)\\.$#"
+ count: 2
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Cannot use array destructuring on array\\<int, string\\>\\|false\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Data\\\\FilterColumns\\:\\:getSearchColumns\\(\\) invoked with 1 parameter, 0 required\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:__construct\\(\\) has parameter \\$props with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:addFilterToId\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:addFilterToId\\(\\) has parameter \\$id with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:addLink\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:applyChanges\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:applyChanges\\(\\) has parameter \\$changes with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:arrayForSelect\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:arrayForSelect\\(\\) has parameter \\$array with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:arrayForSelect\\(\\) has parameter \\$flip with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:cancelLink\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:elementId\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:elementId\\(\\) has parameter \\$prefix with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:getFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:handleRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:handleRequest\\(\\) has parameter \\$request with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:ignoreParams\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:mergeRootExpression\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:mergeRootExpression\\(\\) has parameter \\$column with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:mergeRootExpression\\(\\) has parameter \\$expression with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:mergeRootExpression\\(\\) has parameter \\$filter with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:mergeRootExpression\\(\\) has parameter \\$sign with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:preserveParams\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:preservedUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:redirectNow\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:redirectNow\\(\\) has parameter \\$url with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:removeIndex\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:removeIndex\\(\\) has parameter \\$idx with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:removeLink\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:renderFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:renderFilter\\(\\) has parameter \\$filter with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:renderFilter\\(\\) has parameter \\$level with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:renderFilterChain\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:renderFilterChain\\(\\) has parameter \\$level with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:renderFilterExpression\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:renderNewFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:renderSearch\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:resetSearchColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:select\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:select\\(\\) has parameter \\$attributes with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:select\\(\\) has parameter \\$list with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:select\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:select\\(\\) has parameter \\$selected with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:selectColumn\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:selectOperator\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:selectSign\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:setColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:setColumns\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:setFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:setSearchColumns\\(\\) has parameter \\$searchColumns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:setUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:setUrl\\(\\) has parameter \\$url with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:shorten\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:shorten\\(\\) has parameter \\$length with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:shorten\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:stripLink\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:text\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:url\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Negated boolean expression is always true\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^PHPDoc tag @param references unknown parameter\\: \\$filter$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Parameter \\#3 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 2
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Parameter \\#4 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:\\$addTo has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:\\$cachedColumnSelect has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:\\$ignoreParams has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:\\$preserveParams has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:\\$preservedParams has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:\\$preservedUrl has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:\\$searchColumns has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:\\$selectedIdx is never read, only written\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\FilterEditor\\:\\:\\$url has no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/FilterEditor.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Limiter\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Limiter.php
+
+ -
+ message: "#^Parameter \\#1 \\$defaultLimit of method Icinga\\\\Forms\\\\Control\\\\LimiterControlForm\\:\\:setDefaultLimit\\(\\) expects int, int\\|null given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Limiter.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Abstract\\:\\:partial\\(\\)\\.$#"
+ count: 3
+ path: library/Icinga/Web/Widget/Paginator.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Paginator\\:\\:getPages\\(\\) has parameter \\$currentPage with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Paginator.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Paginator\\:\\:getPages\\(\\) has parameter \\$pageCount with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Paginator.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Paginator\\:\\:getPages\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Paginator.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Paginator\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Paginator.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Paginator\\:\\:setViewScript\\(\\) has parameter \\$script with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Paginator.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Paginator\\:\\:\\$viewScript type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Paginator.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$priority\\.$#"
+ count: 4
+ path: library/Icinga/Web/Widget/SearchDashboard.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getRequest\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SingleValueSearchControl.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\SingleValueSearchControl\\:\\:assemble\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SingleValueSearchControl.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\SingleValueSearchControl\\:\\:createSuggestions\\(\\) has parameter \\$groups with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SingleValueSearchControl.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of class ipl\\\\Html\\\\FormElement\\\\InputElement constructor expects string, null given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SingleValueSearchControl.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\SingleValueSearchControl\\:\\:\\$metaDataNames type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SingleValueSearchControl.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getRequest\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SortBox.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Abstract\\:\\:icon\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SortBox.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Abstract\\:\\:translate\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SortBox.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\SortBox\\:\\:__construct\\(\\) has parameter \\$sortDefaults with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SortBox.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\SortBox\\:\\:__construct\\(\\) has parameter \\$sortFields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SortBox.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\SortBox\\:\\:create\\(\\) has parameter \\$sortDefaults with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SortBox.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\SortBox\\:\\:create\\(\\) has parameter \\$sortFields with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SortBox.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\SortBox\\:\\:getSortDefaults\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SortBox.php
+
+ -
+ message: "#^Parameter \\#1 \\$column of method Icinga\\\\Web\\\\Widget\\\\SortBox\\:\\:getSortDefaults\\(\\) expects string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SortBox.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SortBox.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\SortBox\\:\\:\\$sortDefaults \\(array\\) does not accept array\\|null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SortBox.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\SortBox\\:\\:\\$sortDefaults type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SortBox.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\SortBox\\:\\:\\$sortFields type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/SortBox.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Abstract\\:\\:icon\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Abstract\\:\\:img\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Call to an undefined method Zend_View_Abstract\\:\\:propertiesToString\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Cannot call method getAbsoluteUrl\\(\\) on Icinga\\\\Web\\\\Url\\|string\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Cannot call method overwriteParams\\(\\) on Icinga\\\\Web\\\\Url\\|string\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:__construct\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:getUrl\\(\\) should return Icinga\\\\Web\\\\Url but returns Icinga\\\\Web\\\\Url\\|string\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:setBaseTarget\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:setBaseTarget\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:setIcon\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:setIconCls\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:setLabel\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:setName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:setTagParams\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:setTagParams\\(\\) has parameter \\$tagParams with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:setTargetBlank\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:setTargetBlank\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:setTitle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:setUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:setUrlParams\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:setUrlParams\\(\\) has parameter \\$urlParams with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^PHPDoc tag @param references unknown parameter\\: \\$url$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Parameter \\#4 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:\\$iconCls is never read, only written\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:\\$name \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:\\$name type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:\\$tagParams type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:\\$title \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:\\$urlParams \\(string\\) does not accept array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Tab\\:\\:\\$urlParams type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tab.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabextension\\\\DashboardAction\\:\\:apply\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabextension/DashboardAction.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabextension\\\\DashboardSettings\\:\\:apply\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabextension/DashboardSettings.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabextension\\\\MenuAction\\:\\:apply\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabextension/MenuAction.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabextension\\\\OutputFormat\\:\\:__construct\\(\\) has parameter \\$disabled with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabextension/OutputFormat.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabextension\\\\OutputFormat\\:\\:apply\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabextension/OutputFormat.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabextension\\\\OutputFormat\\:\\:getSupportedTypes\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabextension/OutputFormat.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Tabextension\\\\OutputFormat\\:\\:\\$tabs type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabextension/OutputFormat.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabextension\\\\Tabextension\\:\\:apply\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabextension/Tabextension.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:add\\(\\) has parameter \\$tab with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:addAsDropdown\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:addAsDropdown\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:addAsDropdown\\(\\) has parameter \\$tab with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:get\\(\\) should return Icinga\\\\Web\\\\Widget\\\\Tab but returns null\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:getTabs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:hideCloseButton\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:renderCloseTab\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:renderRefreshTab\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:set\\(\\) has parameter \\$tab with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Parameter \\#2 \\$offset of function array_splice expects int, int\\|string given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:\\$dropdownTabs type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:\\$tab_class is never read, only written\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:\\$tabs type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Widget/Tabs.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getRequest\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Window.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getResponse\\(\\)\\.$#"
+ count: 1
+ path: library/Icinga/Web/Window.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Window\\:\\:__construct\\(\\) has parameter \\$id with no type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Window.php
+
+ -
+ message: "#^Static property Icinga\\\\Web\\\\Window\\:\\:\\$window \\(Icinga\\\\Web\\\\Window\\) in isset\\(\\) is not nullable\\.$#"
+ count: 1
+ path: library/Icinga/Web/Window.php
+
+ -
+ message: "#^Binary operation \"\\+\" between int\\<min, \\-1\\>\\|int\\<1, max\\>\\|string\\|false and 1 results in an error\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Binary operation \"\\+\" between int\\|string\\|false and 1 results in an error\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Binary operation \"\\-\" between \\-1\\|int\\<1, max\\>\\|string\\|false and 1 results in an error\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Binary operation \"\\-\" between int\\<1, max\\>\\|string and 1 results in an error\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Binary operation \"\\-\" between int\\<min, \\-1\\>\\|int\\<1, max\\>\\|string\\|false and 1 results in an error\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Binary operation \"\\-\" between int\\|string\\|false and 1 results in an error\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Wizard\\:\\:addButtons\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Wizard\\:\\:addPages\\(\\) has parameter \\$pages with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Wizard\\:\\:assertHasPages\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Wizard\\:\\:clearSession\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Wizard\\:\\:getPageData\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Wizard\\:\\:getPages\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Wizard\\:\\:getRequestData\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Wizard\\:\\:getRequestedPage\\(\\) has parameter \\$requestData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Wizard\\:\\:getWizards\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Wizard\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Wizard\\:\\:isFinished\\(\\) should return bool but returns mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Method Icinga\\\\Web\\\\Wizard\\:\\:setupPage\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Web\\\\Wizard\\:\\:getPage\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Wizard\\:\\:\\$currentPage \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Property Icinga\\\\Web\\\\Wizard\\:\\:\\$pages type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Strict comparison using \\=\\=\\= between Icinga\\\\Web\\\\Form and null will always evaluate to false\\.$#"
+ count: 1
+ path: library/Icinga/Web/Wizard.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\IcingawebController\\:\\:chapterAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/IcingawebController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\IcingawebController\\:\\:pdfAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/IcingawebController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\IcingawebController\\:\\:tocAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/IcingawebController.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderChapter\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/IcingawebController.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderPdf\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/IcingawebController.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderToc\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/IcingawebController.php
+
+ -
+ message: "#^Parameter \\#2 \\$chapter of method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderChapter\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/IcingawebController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\IndexController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/IndexController.php
+
+ -
+ message: "#^Access to an undefined property Zend_Controller_Action_HelperBroker\\:\\:\\$viewRenderer\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Action_HelperBroker\\:\\:layout\\(\\)\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\ModuleController\\:\\:assertModuleInstalled\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\ModuleController\\:\\:chapterAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\ModuleController\\:\\:imageAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\ModuleController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\ModuleController\\:\\:pdfAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\ModuleController\\:\\:tocAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of class SplFileInfo constructor expects string, string\\|false given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function readfile expects string, string\\|false given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of method finfo\\:\\:file\\(\\) expects string, string\\|false given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$module of method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\ModuleController\\:\\:getPath\\(\\) expects string, mixed given\\.$#"
+ count: 4
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$moduleName of method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\ModuleController\\:\\:assertModuleInstalled\\(\\) expects string, mixed given\\.$#"
+ count: 3
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:getModule\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:getModuleDir\\(\\) expects string, mixed given\\.$#"
+ count: 4
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderChapter\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderPdf\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderToc\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strlen expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#2 \\$chapter of method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderChapter\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#2 \\$name of method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderPdf\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method Zend_Controller_Response_Abstract\\:\\:setHeader\\(\\) expects string, \\(int\\|false\\) given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method Zend_Controller_Response_Abstract\\:\\:setHeader\\(\\) expects string, string\\|false given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/ModuleController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\SearchController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/SearchController.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of class Icinga\\\\Module\\\\Doc\\\\DocParser constructor expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/SearchController.php
+
+ -
+ message: "#^Parameter \\#1 \\$search of class Icinga\\\\Module\\\\Doc\\\\Search\\\\DocSearch constructor expects string, mixed given\\.$#"
+ count: 2
+ path: modules/doc/application/controllers/SearchController.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strlen expects string, mixed given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/SearchController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\Widget\\\\AbstractWidget\\:\\:add\\(\\)\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/StyleController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\StyleController\\:\\:fontAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/StyleController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\StyleController\\:\\:guideAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/StyleController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Controllers\\\\StyleController\\:\\:tabs\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/StyleController.php
+
+ -
+ message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
+ count: 1
+ path: modules/doc/application/controllers/StyleController.php
+
+ -
+ message: "#^Access to an undefined property Zend_Controller_Action_HelperBroker\\:\\:\\$viewRenderer\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocController.php
+
+ -
+ message: "#^Cannot call method getTitle\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:moduleInit\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderChapter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderChapter\\(\\) has parameter \\$urlParams with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderPdf\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderPdf\\(\\) has parameter \\$urlParams with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderToc\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\DocController\\:\\:renderToc\\(\\) has parameter \\$urlParams with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocController.php
+
+ -
+ message: "#^Parameter \\#1 \\$highlightSearch of method Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocSectionRenderer\\:\\:setHighlightSearch\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocController.php
+
+ -
+ message: "#^Parameter \\#1 \\$imageUrl of method Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocRenderer\\:\\:setImageUrl\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocController.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method Icinga\\\\Web\\\\UrlParams\\:\\:set\\(\\) expects string, mixed given\\.$#"
+ count: 3
+ path: modules/doc/library/Doc/DocController.php
+
+ -
+ message: "#^Cannot call method appendContent\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocParser.php
+
+ -
+ message: "#^Cannot call method getLevel\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocParser.php
+
+ -
+ message: "#^Cannot call method getTitle\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocParser.php
+
+ -
+ message: "#^Left side of && is always true\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\DocParser\\:\\:extractHeader\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocParser.php
+
+ -
+ message: "#^PHPDoc tag @var has invalid value \\(\\$section DocSection\\)\\: Unexpected token \"\\$section\", expected type at offset 9$#"
+ count: 1
+ path: modules/doc/library/Doc/DocParser.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of class SplFileObject constructor expects string, mixed given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocParser.php
+
+ -
+ message: "#^Parameter \\#1 \\$line of method Icinga\\\\Module\\\\Doc\\\\DocParser\\:\\:extractHeader\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocParser.php
+
+ -
+ message: "#^Parameter \\#1 \\$section of method Icinga\\\\Module\\\\Doc\\\\DocSection\\:\\:setChapter\\(\\) expects Icinga\\\\Module\\\\Doc\\\\DocSection, mixed given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocParser.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function substr expects string, mixed given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocParser.php
+
+ -
+ message: "#^Parameter \\#2 \\$filename of method Icinga\\\\Module\\\\Doc\\\\DocParser\\:\\:uuid\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: modules/doc/library/Doc/DocParser.php
+
+ -
+ message: "#^Parameter \\#2 \\$nextLine of method Icinga\\\\Module\\\\Doc\\\\DocParser\\:\\:extractHeader\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocParser.php
+
+ -
+ message: "#^Parameter \\#2 \\$parent of method Icinga\\\\Data\\\\Tree\\\\SimpleTree\\:\\:addChild\\(\\) expects Icinga\\\\Data\\\\Tree\\\\TreeNode\\|null, mixed given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\DocSection\\:\\:appendContent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocSection.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\DocSection\\:\\:getContent\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocSection.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Doc\\\\DocSection\\:\\:\\$content type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/DocSection.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getViewRenderer\\(\\)\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocRenderer.php
+
+ -
+ message: "#^Class Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocRenderer extends generic class RecursiveIteratorIterator but does not specify its types\\: T$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocRenderer\\:\\:getUrlParams\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocRenderer\\:\\:setUrlParams\\(\\) has parameter \\$urlParams with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocRenderer\\:\\:\\$urlParams type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocRenderer.php
+
+ -
+ message: "#^Call to an undefined method Iterator\\<mixed, mixed\\>\\:\\:getMatches\\(\\)\\.$#"
+ count: 3
+ path: modules/doc/library/Doc/Renderer/DocSearchRenderer.php
+
+ -
+ message: "#^Call to an undefined method Iterator\\<mixed, mixed\\>\\:\\:getSearch\\(\\)\\.$#"
+ count: 3
+ path: modules/doc/library/Doc/Renderer/DocSearchRenderer.php
+
+ -
+ message: "#^Call to an undefined method object\\:\\:url\\(\\)\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSearchRenderer.php
+
+ -
+ message: "#^Cannot call method getChapter\\(\\) on mixed\\.$#"
+ count: 4
+ path: modules/doc/library/Doc/Renderer/DocSearchRenderer.php
+
+ -
+ message: "#^Cannot call method getId\\(\\) on mixed\\.$#"
+ count: 2
+ path: modules/doc/library/Doc/Renderer/DocSearchRenderer.php
+
+ -
+ message: "#^Cannot call method getNoFollow\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSearchRenderer.php
+
+ -
+ message: "#^Cannot call method getTitle\\(\\) on mixed\\.$#"
+ count: 2
+ path: modules/doc/library/Doc/Renderer/DocSearchRenderer.php
+
+ -
+ message: "#^Cannot call method hasChildren\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSearchRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocSearchRenderer\\:\\:\\$content type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSearchRenderer.php
+
+ -
+ message: "#^Call to an undefined method DOMNode\\:\\:setAttribute\\(\\)\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Tree\\\\TreeNode\\:\\:getChapter\\(\\)\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Tree\\\\TreeNode\\:\\:getNoFollow\\(\\)\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Call to an undefined method object\\:\\:url\\(\\)\\.$#"
+ count: 3
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Cannot access property \\$nodeType on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Cannot access property \\$nodeValue on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Cannot access property \\$parentNode on mixed\\.$#"
+ count: 3
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Cannot call method getContent\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Cannot call method getId\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Cannot call method getLevel\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Cannot call method getTitle\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Cannot call method item\\(\\) on DOMNodeList\\<DOMNode\\>\\|false\\.$#"
+ count: 2
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocSectionRenderer\\:\\:highlightPhp\\(\\) has parameter \\$match with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocSectionRenderer\\:\\:markupNotes\\(\\) has parameter \\$match with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocSectionRenderer\\:\\:markupNotes\\(\\) should return string but returns string\\|false\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocSectionRenderer\\:\\:replaceChapterLink\\(\\) has parameter \\$match with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocSectionRenderer\\:\\:replaceImg\\(\\) has parameter \\$match with no type specified\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocSectionRenderer\\:\\:replaceSectionLink\\(\\) has parameter \\$match with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Parameter \\#1 \\$anchor of method Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocRenderer\\:\\:encodeAnchor\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Parameter \\#1 \\$child of method DOMNode\\:\\:removeChild\\(\\) expects DOMNode, DOMDocumentType\\|null given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Parameter \\#1 \\$html of method Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocSectionRenderer\\:\\:highlightSearch\\(\\) expects string, array\\<int, string\\>\\|string\\|null given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Parameter \\#1 \\$param of method Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocRenderer\\:\\:encodeUrlParam\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function substr_replace expects array\\|string, string\\|false given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function trim expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Parameter \\#3 \\$subject of function preg_replace_callback expects array\\|string, array\\<int, string\\>\\|string\\|null given\\.$#"
+ count: 3
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocSectionRenderer\\:\\:\\$content type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Variable \\$section in PHPDoc tag @var does not match assigned variable \\$path\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocSectionRenderer.php
+
+ -
+ message: "#^Call to an undefined method Iterator\\<mixed, mixed\\>\\:\\:isEmpty\\(\\)\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocTocRenderer.php
+
+ -
+ message: "#^Call to an undefined method object\\:\\:url\\(\\)\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocTocRenderer.php
+
+ -
+ message: "#^Cannot call method getChapter\\(\\) on mixed\\.$#"
+ count: 4
+ path: modules/doc/library/Doc/Renderer/DocTocRenderer.php
+
+ -
+ message: "#^Cannot call method getId\\(\\) on mixed\\.$#"
+ count: 2
+ path: modules/doc/library/Doc/Renderer/DocTocRenderer.php
+
+ -
+ message: "#^Cannot call method getNoFollow\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocTocRenderer.php
+
+ -
+ message: "#^Cannot call method getTitle\\(\\) on mixed\\.$#"
+ count: 2
+ path: modules/doc/library/Doc/Renderer/DocTocRenderer.php
+
+ -
+ message: "#^Cannot call method hasChildren\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocTocRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Doc\\\\Renderer\\\\DocTocRenderer\\:\\:\\$content type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Renderer/DocTocRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Search\\\\DocSearch\\:\\:getCriteria\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Search/DocSearch.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Doc\\\\Search\\\\DocSearch\\:\\:\\$search type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Search/DocSearch.php
+
+ -
+ message: "#^Call to an undefined method Iterator\\<mixed, mixed\\>\\:\\:getChildren\\(\\)\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Search/DocSearchIterator.php
+
+ -
+ message: "#^Call to an undefined method Iterator\\<mixed, mixed\\>\\:\\:getMatches\\(\\)\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Search/DocSearchIterator.php
+
+ -
+ message: "#^Cannot call method getContent\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Search/DocSearchIterator.php
+
+ -
+ message: "#^Cannot call method getTitle\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Search/DocSearchIterator.php
+
+ -
+ message: "#^Cannot call method hasChildren\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Search/DocSearchIterator.php
+
+ -
+ message: "#^PHPDoc tag @var has invalid value \\(\\$section DocSection\\)\\: Unexpected token \"\\$section\", expected type at offset 9$#"
+ count: 1
+ path: modules/doc/library/Doc/Search/DocSearchIterator.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Application\\\\ApplicationBootstrap\\:\\:getViewRenderer\\(\\)\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Search/DocSearchMatch.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Doc\\\\Search\\\\DocSearchMatch\\:\\:getMatches\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Search/DocSearchMatch.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Doc\\\\Search\\\\DocSearchMatch\\:\\:\\$matches type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/doc/library/Doc/Search/DocSearchMatch.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Clicommands\\\\ConfigCommand\\:\\:usersAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/migrate/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$domain of method Icinga\\\\User\\:\\:setDomain\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/migrate/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function file_get_contents expects string, mixed given\\.$#"
+ count: 1
+ path: modules/migrate/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function trim expects string, string\\|false given\\.$#"
+ count: 1
+ path: modules/migrate/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$username of class Icinga\\\\User constructor expects string, mixed given\\.$#"
+ count: 1
+ path: modules/migrate/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Parameter \\#2 \\$delimiter of static method Icinga\\\\Util\\\\StringHelper\\:\\:trimSplit\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/migrate/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Cannot call method getMessage\\(\\) on Throwable\\|null\\.$#"
+ count: 1
+ path: modules/migrate/application/clicommands/PreferencesCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Clicommands\\\\PreferencesCommand\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/migrate/application/clicommands/PreferencesCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Clicommands\\\\PreferencesCommand\\:\\:loadIniFile\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/migrate/application/clicommands/PreferencesCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function is_dir expects string, mixed given\\.$#"
+ count: 1
+ path: modules/migrate/application/clicommands/PreferencesCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of function basename expects string, mixed given\\.$#"
+ count: 1
+ path: modules/migrate/application/clicommands/PreferencesCommand.php
+
+ -
+ message: "#^Cannot access property \\$author on mixed\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:fromDomains\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:fromDomains\\(\\) has parameter \\$fromDomain with no type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:fromDomains\\(\\) has parameter \\$toDomain with no type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:fromMap\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:fromMap\\(\\) has parameter \\$map with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:migrate\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:migrateAnnounces\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:migrateDashboards\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:migrateNavigation\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:migratePreferences\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:migrateRoles\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:migrateUser\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:migrateUsers\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:mustMigrate\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Parameter \\#1 \\$file of static method Icinga\\\\Application\\\\Config\\:\\:fromIni\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Parameter \\#1 \\$from of function rename expects string, int\\|string given\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of function dirname expects string, mixed given\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 2
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Parameter \\#1 \\$username of class Icinga\\\\User constructor expects string, mixed given\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of static method Icinga\\\\Util\\\\StringHelper\\:\\:trimSplit\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Parameter \\#2 \\$filter of static method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:where\\(\\) expects string, array\\<int, int\\|string\\> given\\.$#"
+ count: 3
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Parameter \\#2 \\$filter of static method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:where\\(\\) expects string, int\\|string given\\.$#"
+ count: 3
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:\\$fromDomain has no type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:\\$map has no type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Migrate\\\\Config\\\\UserDomainMigration\\:\\:\\$toDomain has no type specified\\.$#"
+ count: 1
+ path: modules/migrate/library/Migrate/Config/UserDomainMigration.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$service_handled\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$service_output\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$service_perfdata\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$service_state\\.$#"
+ count: 2
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$services_cnt\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$services_problem\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$services_problem_unhandled\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Cannot call method getLabel\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Cannot call method getPercentage\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:getPercentageSign\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:getPercentageSign\\(\\) has parameter \\$percent with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:getQuery\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:getQuery\\(\\) has parameter \\$columns with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:getQuery\\(\\) has parameter \\$table with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:hostsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:renderStatusQuery\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:renderStatusQuery\\(\\) has parameter \\$query with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:servicesAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:showFormatted\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:showFormatted\\(\\) has parameter \\$columns with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:showFormatted\\(\\) has parameter \\$format with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:showFormatted\\(\\) has parameter \\$query with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of static method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\MonitoringBackend\\:\\:instance\\(\\) expects string\\|null, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function ucfirst expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_split expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:\\$backend has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:\\$defaultActionName has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\ListCommand\\:\\:\\$dumpSql has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/ListCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\NrpeCommand\\:\\:checkAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/NrpeCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Clicommands\\\\NrpeCommand\\:\\:\\$defaultActionName has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/clicommands/NrpeCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ActionsController\\:\\:removeHostDowntimeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ActionsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ActionsController\\:\\:removeServiceDowntimeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ActionsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ActionsController\\:\\:scheduleHostDowntimeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ActionsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ActionsController\\:\\:scheduleServiceDowntimeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ActionsController.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$id\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/CommentController.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$name\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/CommentController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\CommentController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/CommentController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\CommentsController\\:\\:deleteAllAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/CommentsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\CommentsController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/CommentsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ConfigController\\:\\:createbackendAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ConfigController\\:\\:createtransportAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ConfigController\\:\\:editbackendAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ConfigController\\:\\:edittransportAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ConfigController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ConfigController\\:\\:removebackendAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ConfigController\\:\\:removetransportAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ConfigController\\:\\:securityAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\BackendConfigForm\\:\\:delete\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\BackendConfigForm\\:\\:edit\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\BackendConfigForm\\:\\:load\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportConfigForm\\:\\:delete\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportConfigForm\\:\\:edit\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportConfigForm\\:\\:load\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 10
+ path: modules/monitoring/application/controllers/ConfigController.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$host_name\\.$#"
+ count: 2
+ path: modules/monitoring/application/controllers/DowntimeController.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$host_state\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/DowntimeController.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$id\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/DowntimeController.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$name\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/DowntimeController.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$service_description\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/DowntimeController.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$service_state\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/DowntimeController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\DowntimeController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/DowntimeController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\DowntimesController\\:\\:deleteAllAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/DowntimesController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\DowntimesController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/DowntimesController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filterable\\:\\:fetchRow\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\View\\:\\:createTicketLinks\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\View\\:\\:markdown\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\View\\:\\:nl2br\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Call to an undefined method object\\:\\:pluginOutput\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Cannot call method fetchRow\\(\\) on Icinga\\\\Data\\\\Queryable\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Cannot use array destructuring on array\\<string\\>\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\EventController\\:\\:getDetails\\(\\) should return array\\<array\\<string\\>\\>\\|null but returns array\\<int, array\\<int, int\\|string\\>\\>\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\EventController\\:\\:getDetails\\(\\) should return array\\<array\\<string\\>\\>\\|null but returns mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\EventController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Parameter \\#1 \\$eventType of method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\EventController\\:\\:getIconAndLabel\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Parameter \\#1 \\$type of method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\EventController\\:\\:getDetails\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Parameter \\#1 \\$type of method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\EventController\\:\\:query\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Parameter \\#2 \\$id of method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\EventController\\:\\:query\\(\\) expects int, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$arrays of function array_merge expects array, array\\<array\\<string\\>\\>\\|null given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/EventController.php
+
+ -
+ message: "#^Cannot access property \\$hosts_down_unhandled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HealthController.php
+
+ -
+ message: "#^Cannot access property \\$hosts_unreachable_unhandled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HealthController.php
+
+ -
+ message: "#^Cannot access property \\$notifications_enabled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HealthController.php
+
+ -
+ message: "#^Cannot access property \\$services_critical_unhandled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HealthController.php
+
+ -
+ message: "#^Cannot access property \\$services_unknown_unhandled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HealthController.php
+
+ -
+ message: "#^Cannot access property \\$services_warning_unhandled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HealthController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HealthController\\:\\:disableNotificationsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HealthController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HealthController\\:\\:infoAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HealthController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HealthController\\:\\:statsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HealthController.php
+
+ -
+ message: "#^Parameter \\#1 \\$instanceStatus of method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Instance\\\\ToggleInstanceFeaturesCommandForm\\:\\:load\\(\\) expects object, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HealthController.php
+
+ -
+ message: "#^Parameter \\#1 \\$status of method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Instance\\\\ToggleInstanceFeaturesCommandForm\\:\\:setStatus\\(\\) expects object, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HealthController.php
+
+ -
+ message: "#^Parameter \\#2 \\$name of method Zend_Controller_Action\\:\\:render\\(\\) expects string\\|null, true given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HealthController.php
+
+ -
+ message: "#^Parameter \\#3 \\$noController of method Zend_Controller_Action\\:\\:render\\(\\) expects bool, null given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HealthController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostController\\:\\:acknowledgeProblemAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostController\\:\\:addCommentAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostController\\:\\:processCheckResultAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostController\\:\\:rescheduleCheckAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostController\\:\\:scheduleDowntimeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostController\\:\\:sendCustomNotificationAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostController\\:\\:servicesAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostController.php
+
+ -
+ message: "#^Parameter \\#2 \\$host of class Icinga\\\\Module\\\\Monitoring\\\\Object\\\\Host constructor expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostController.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Action_HelperBroker\\:\\:viewRenderer\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostsController\\:\\:acknowledgeProblemAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostsController\\:\\:addCommentAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostsController\\:\\:handleCommandForm\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostsController\\:\\:processCheckResultAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostsController\\:\\:rescheduleCheckAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostsController\\:\\:scheduleDowntimeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostsController\\:\\:sendCustomNotificationAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\HostsController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/HostsController.php
+
+ -
+ message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Argument of an invalid type array\\|stdClass supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:addColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:addTitleTab\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:addTitleTab\\(\\) has parameter \\$action with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:addTitleTab\\(\\) has parameter \\$tip with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:addTitleTab\\(\\) has parameter \\$title with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:commentsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:contactgroupsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:contactsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:downtimesAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:eventgridAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:eventhistoryAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:hostgroupGridAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:hostgroupsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:hostsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:notificationsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:servicegridAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:servicegroupGridAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:servicegroupsAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:servicesAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ListController\\:\\:setBackend\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Parameter \\#1 \\$haystack of function strpos expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 2
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Parameter \\#2 \\$default of method Icinga\\\\Web\\\\UrlParams\\:\\:shift\\(\\) expects string\\|null, false given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Parameter \\#2 \\$default of method Icinga\\\\Web\\\\UrlParams\\:\\:shift\\(\\) expects string\\|null, int given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Parameter \\#2 \\$filter of static method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:where\\(\\) expects string, int given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_split expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ListController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServiceController\\:\\:acknowledgeProblemAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServiceController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServiceController\\:\\:addCommentAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServiceController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServiceController\\:\\:processCheckResultAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServiceController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServiceController\\:\\:rescheduleCheckAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServiceController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServiceController\\:\\:scheduleDowntimeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServiceController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServiceController\\:\\:sendCustomNotificationAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServiceController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServiceController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServiceController.php
+
+ -
+ message: "#^Parameter \\#2 \\$host of class Icinga\\\\Module\\\\Monitoring\\\\Object\\\\Service constructor expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServiceController.php
+
+ -
+ message: "#^Parameter \\#3 \\$service of class Icinga\\\\Module\\\\Monitoring\\\\Object\\\\Service constructor expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServiceController.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Controller\\\\MonitoredObjectController\\:\\:\\$object \\(Icinga\\\\Module\\\\Monitoring\\\\Object\\\\Host\\) does not accept Icinga\\\\Module\\\\Monitoring\\\\Object\\\\Service\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServiceController.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Action_HelperBroker\\:\\:viewRenderer\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServicesController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServicesController\\:\\:acknowledgeProblemAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServicesController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServicesController\\:\\:addCommentAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServicesController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServicesController\\:\\:handleCommandForm\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServicesController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServicesController\\:\\:processCheckResultAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServicesController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServicesController\\:\\:rescheduleCheckAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServicesController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServicesController\\:\\:scheduleDowntimeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServicesController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServicesController\\:\\:sendCustomNotificationAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServicesController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ServicesController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ServicesController.php
+
+ -
+ message: "#^Cannot access property \\$contact_id on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ShowController.php
+
+ -
+ message: "#^Cannot access property \\$contact_name on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ShowController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\ShowController\\:\\:contactAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/ShowController.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\View\\:\\:filteredUrl\\(\\)\\.$#"
+ count: 2
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$hosts_down_handled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$hosts_down_unhandled on mixed\\.$#"
+ count: 3
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$hosts_pending on mixed\\.$#"
+ count: 2
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$hosts_pending_not_checked on mixed\\.$#"
+ count: 2
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$hosts_unreachable_handled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$hosts_unreachable_unhandled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$hosts_up on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$services_critical_handled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$services_critical_unhandled on mixed\\.$#"
+ count: 5
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$services_ok on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$services_pending on mixed\\.$#"
+ count: 2
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$services_pending_not_checked on mixed\\.$#"
+ count: 2
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$services_unknown_handled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$services_unknown_unhandled on mixed\\.$#"
+ count: 5
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$services_warning_handled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot access property \\$services_warning_unhandled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot call method getFilter\\(\\) on null\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\TacticalController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TacticalController.php
+
+ -
+ message: "#^Cannot call method getInterval\\(\\) on null\\.$#"
+ count: 4
+ path: modules/monitoring/application/controllers/TimelineController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\TimelineController\\:\\:buildTimeRanges\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TimelineController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\TimelineController\\:\\:extrapolateDateTime\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TimelineController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\TimelineController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TimelineController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controllers\\\\TimelineController\\:\\:setupIntervalBox\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TimelineController.php
+
+ -
+ message: "#^Parameter \\#1 \\$dataview of class Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine constructor expects Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView, Icinga\\\\Data\\\\Filterable given\\.$#"
+ count: 1
+ path: modules/monitoring/application/controllers/TimelineController.php
+
+ -
+ message: "#^Parameter \\#1 \\$datetime of function strtotime expects string, mixed given\\.$#"
+ count: 2
+ path: modules/monitoring/application/controllers/TimelineController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Config\\:\\:getSection\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/CommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Config\\:\\:hasSection\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/CommandForm.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/CommandForm.php
+
+ -
+ message: "#^Cannot call method getTimestamp\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Instance/DisableNotificationsExpireCommandForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Instance/DisableNotificationsExpireCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Instance\\\\DisableNotificationsExpireCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Instance/DisableNotificationsExpireCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Instance\\\\DisableNotificationsExpireCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Instance/DisableNotificationsExpireCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$cue of method Icinga\\\\Web\\\\Form\\:\\:setRequiredCue\\(\\) expects string, null given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Instance/DisableNotificationsExpireCommandForm.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$disable_notif_expire_time\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$notifications_enabled\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$program_version\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php
+
+ -
+ message: "#^Cannot call method href\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php
+
+ -
+ message: "#^Cannot call method setChecked\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php
+
+ -
+ message: "#^Cannot call method timeUntil\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Instance\\\\ToggleInstanceFeaturesCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Instance\\\\ToggleInstanceFeaturesCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Instance/ToggleInstanceFeaturesCommandForm.php
+
+ -
+ message: "#^Argument of an invalid type array\\|ArrayAccess\\|Traversable supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php
+
+ -
+ message: "#^Cannot call method getTimestamp\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php
+
+ -
+ message: "#^Cannot call method isChecked\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 4
+ path: modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\AcknowledgeProblemCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\AcknowledgeProblemCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$comment of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\WithCommentCommand\\:\\:setComment\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$duration of class DateInterval constructor expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, array\\|ArrayAccess\\|Traversable given\\.$#"
+ count: 2
+ path: modules/monitoring/application/forms/Command/Object/AcknowledgeProblemCommandForm.php
+
+ -
+ message: "#^Argument of an invalid type array\\|ArrayAccess\\|Traversable supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php
+
+ -
+ message: "#^Call to an undefined method Zend_Form_Element\\:\\:isChecked\\(\\)\\.$#"
+ count: 2
+ path: modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php
+
+ -
+ message: "#^Cannot call method getTimestamp\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\AddCommentCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\AddCommentCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$comment of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\WithCommentCommand\\:\\:setComment\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$duration of class DateInterval constructor expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, array\\|ArrayAccess\\|Traversable given\\.$#"
+ count: 2
+ path: modules/monitoring/application/forms/Command/Object/AddCommentCommandForm.php
+
+ -
+ message: "#^Argument of an invalid type array\\|ArrayAccess\\|Traversable supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/CheckNowCommandForm.php
+
+ -
+ message: "#^Cannot call method icon\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/CheckNowCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, array\\|ArrayAccess\\|Traversable given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/CheckNowCommandForm.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 4
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php
+
+ -
+ message: "#^Cannot call method icon\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\DeleteCommentCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\DeleteCommentCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$commentId of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\DeleteCommentCommand\\:\\:setCommentId\\(\\) expects int, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$commentName of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\DeleteCommentCommand\\:\\:setCommentName\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$isService of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\DeleteCommentCommand\\:\\:setIsService\\(\\) expects bool, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$url of method Icinga\\\\Web\\\\Form\\:\\:setRedirectUrl\\(\\) expects Icinga\\\\Web\\\\Url\\|string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentCommandForm.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\DeleteCommentsCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\DeleteCommentsCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\DeleteCommentsCommandForm\\:\\:setComments\\(\\) has parameter \\$comments with no value type specified in iterable type iterable\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$url of method Icinga\\\\Web\\\\Form\\:\\:setRedirectUrl\\(\\) expects Icinga\\\\Web\\\\Url\\|string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\DeleteCommentsCommandForm\\:\\:\\$comments \\(array\\) does not accept iterable\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\DeleteCommentsCommandForm\\:\\:\\$comments type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteCommentsCommandForm.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 4
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php
+
+ -
+ message: "#^Cannot call method icon\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\DeleteDowntimeCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\DeleteDowntimeCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$downtimeId of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\DeleteDowntimeCommand\\:\\:setDowntimeId\\(\\) expects int, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$downtimeName of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\DeleteDowntimeCommand\\:\\:setDowntimeName\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$isService of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\DeleteDowntimeCommand\\:\\:setIsService\\(\\) expects bool, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$url of method Icinga\\\\Web\\\\Form\\:\\:setRedirectUrl\\(\\) expects Icinga\\\\Web\\\\Url\\|string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimeCommandForm.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\DeleteDowntimesCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\DeleteDowntimesCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\DeleteDowntimesCommandForm\\:\\:setDowntimes\\(\\) has parameter \\$downtimes with no value type specified in iterable type iterable\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$url of method Icinga\\\\Web\\\\Form\\:\\:setRedirectUrl\\(\\) expects Icinga\\\\Web\\\\Url\\|string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\DeleteDowntimesCommandForm\\:\\:\\$downtimes \\(array\\) does not accept iterable\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\DeleteDowntimesCommandForm\\:\\:\\$downtimes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/DeleteDowntimesCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ObjectsCommandForm\\:\\:getObjects\\(\\) return type has no value type specified in iterable type Traversable\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ObjectsCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ObjectsCommandForm\\:\\:getObjects\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ObjectsCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ObjectsCommandForm\\:\\:getObjects\\(\\) return type with generic interface ArrayAccess does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ObjectsCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ObjectsCommandForm\\:\\:setObjects\\(\\) has parameter \\$objects with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ObjectsCommandForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ObjectsCommandForm\\:\\:\\$objects type has no value type specified in iterable type Traversable\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ObjectsCommandForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ObjectsCommandForm\\:\\:\\$objects type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ObjectsCommandForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ObjectsCommandForm\\:\\:\\$objects with generic interface ArrayAccess does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ObjectsCommandForm.php
+
+ -
+ message: "#^Argument of an invalid type array\\|ArrayAccess\\|Traversable supplied for foreach, only iterables are supported\\.$#"
+ count: 2
+ path: modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php
+
+ -
+ message: "#^Cannot access constant TYPE_HOST on Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php
+
+ -
+ message: "#^Cannot call method getType\\(\\) on Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ProcessCheckResultCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ProcessCheckResultCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ProcessCheckResultCommandForm\\:\\:getHostMultiOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$output of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\ProcessCheckResultCommand\\:\\:setOutput\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$performanceData of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\ProcessCheckResultCommand\\:\\:setPerformanceData\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$status of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\ProcessCheckResultCommand\\:\\:setStatus\\(\\) expects int, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, array\\|ArrayAccess\\|Traversable given\\.$#"
+ count: 2
+ path: modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php
+
+ -
+ message: "#^Argument of an invalid type array\\|ArrayAccess\\|Traversable supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/RemoveAcknowledgementCommandForm.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/RemoveAcknowledgementCommandForm.php
+
+ -
+ message: "#^Cannot call method icon\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/RemoveAcknowledgementCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, array\\|ArrayAccess\\|Traversable given\\.$#"
+ count: 3
+ path: modules/monitoring/application/forms/Command/Object/RemoveAcknowledgementCommandForm.php
+
+ -
+ message: "#^Argument of an invalid type array\\|ArrayAccess\\|Traversable supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostCheckCommandForm.php
+
+ -
+ message: "#^Cannot call method isChecked\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostCheckCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ScheduleHostCheckCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostCheckCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ScheduleHostCheckCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostCheckCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, array\\|ArrayAccess\\|Traversable given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostCheckCommandForm.php
+
+ -
+ message: "#^Argument of an invalid type array\\|ArrayAccess\\|Traversable supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php
+
+ -
+ message: "#^Cannot call method getTimestamp\\(\\) on mixed\\.$#"
+ count: 2
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php
+
+ -
+ message: "#^Cannot call method isChecked\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php
+
+ -
+ message: "#^Cannot cast mixed to int\\.$#"
+ count: 2
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ScheduleHostDowntimeCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ScheduleHostDowntimeCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$duration of class DateInterval constructor expects string, mixed given\\.$#"
+ count: 3
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, array\\|ArrayAccess\\|Traversable given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleHostDowntimeCommandForm.php
+
+ -
+ message: "#^Argument of an invalid type array\\|ArrayAccess\\|Traversable supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php
+
+ -
+ message: "#^Cannot call method getTimestamp\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php
+
+ -
+ message: "#^Cannot call method isChecked\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ScheduleServiceCheckCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ScheduleServiceCheckCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ScheduleServiceCheckCommandForm\\:\\:scheduleCheck\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, array\\|ArrayAccess\\|Traversable given\\.$#"
+ count: 2
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceCheckCommandForm.php
+
+ -
+ message: "#^Argument of an invalid type array\\|ArrayAccess\\|Traversable supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php
+
+ -
+ message: "#^Cannot call method getTimestamp\\(\\) on mixed\\.$#"
+ count: 4
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 6
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php
+
+ -
+ message: "#^Cannot cast mixed to float\\.$#"
+ count: 2
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ScheduleServiceDowntimeCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ScheduleServiceDowntimeCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ScheduleServiceDowntimeCommandForm\\:\\:scheduleDowntime\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$comment of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\WithCommentCommand\\:\\:setComment\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$duration of class DateInterval constructor expects string, mixed given\\.$#"
+ count: 3
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$duration of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\ScheduleServiceDowntimeCommand\\:\\:setDuration\\(\\) expects int, float given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, array\\|ArrayAccess\\|Traversable given\\.$#"
+ count: 2
+ path: modules/monitoring/application/forms/Command/Object/ScheduleServiceDowntimeCommandForm.php
+
+ -
+ message: "#^Argument of an invalid type array\\|ArrayAccess\\|Traversable supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php
+
+ -
+ message: "#^Call to an undefined method Zend_Form_Element\\:\\:isChecked\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php
+
+ -
+ message: "#^Cannot call method getValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php
+
+ -
+ message: "#^Cannot call method isChecked\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\SendCustomNotificationCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\SendCustomNotificationCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$comment of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\WithCommentCommand\\:\\:setComment\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, array\\|ArrayAccess\\|Traversable given\\.$#"
+ count: 2
+ path: modules/monitoring/application/forms/Command/Object/SendCustomNotificationCommandForm.php
+
+ -
+ message: "#^Argument of an invalid type array\\|ArrayAccess\\|Traversable supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ToggleObjectFeaturesCommandForm.php
+
+ -
+ message: "#^Cannot call method getAttrib\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ToggleObjectFeaturesCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ToggleObjectFeaturesCommandForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ToggleObjectFeaturesCommandForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ToggleObjectFeaturesCommandForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ToggleObjectFeaturesCommandForm.php
+
+ -
+ message: "#^Offset 'label' does not exist on string\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ToggleObjectFeaturesCommandForm.php
+
+ -
+ message: "#^Offset 'permission' does not exist on string\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ToggleObjectFeaturesCommandForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Command\\\\Object\\\\ToggleObjectFeaturesCommandForm\\:\\:\\$features \\(array\\<string\\>\\) does not accept array\\<string, array\\<string, string\\>\\>\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Command/Object/ToggleObjectFeaturesCommandForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Fetchable\\:\\:count\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/BackendConfigForm.php
+
+ -
+ message: "#^Call to an undefined method Zend_Form_Element\\:\\:isChecked\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/BackendConfigForm.php
+
+ -
+ message: "#^Cannot call method isChecked\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/BackendConfigForm.php
+
+ -
+ message: "#^Cannot call method url\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/BackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\BackendConfigForm\\:\\:add\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/BackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\BackendConfigForm\\:\\:addSkipValidationCheckbox\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/BackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\BackendConfigForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/BackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\BackendConfigForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/BackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\BackendConfigForm\\:\\:edit\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/BackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\BackendConfigForm\\:\\:isValid\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/BackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\BackendConfigForm\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/BackendConfigForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\BackendConfigForm\\:\\:\\$resources type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/BackendConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\SecurityConfigForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/SecurityConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\SecurityConfigForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/SecurityConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\SecurityConfigForm\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/SecurityConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\Transport\\\\ApiTransportForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/Transport/ApiTransportForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\Transport\\\\ApiTransportForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/Transport/ApiTransportForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\Transport\\\\LocalTransportForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/Transport/LocalTransportForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\Transport\\\\LocalTransportForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/Transport/LocalTransportForm.php
+
+ -
+ message: "#^Cannot call method url\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/Transport/RemoteTransportForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\Transport\\\\RemoteTransportForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/Transport/RemoteTransportForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\Transport\\\\RemoteTransportForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/Transport/RemoteTransportForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\Transport\\\\RemoteTransportForm\\:\\:\\$resources type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/Transport/RemoteTransportForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\ApiCommandTransport\\|Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\LocalCommandFile\\|Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:probe\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportConfigForm.php
+
+ -
+ message: "#^Call to an undefined method Zend_Form_Element\\:\\:isChecked\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportConfigForm\\:\\:add\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportConfigForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportConfigForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportConfigForm\\:\\:edit\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportConfigForm\\:\\:getInstanceNames\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportConfigForm\\:\\:isValid\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportConfigForm\\:\\:isValidPartial\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportConfigForm\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportConfigForm\\:\\:setInstanceNames\\(\\) has parameter \\$names with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportConfigForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportConfigForm\\:\\:\\$instanceNames type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportReorderForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportReorderForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Config\\\\TransportReorderForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportReorderForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Config\\:\\:setSection\\(\\) expects string, int\\|string given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportReorderForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$offset of function array_splice expects int, int\\|string\\|false given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportReorderForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$offset of function array_splice expects int, string given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportReorderForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$string of function explode expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Config/TransportReorderForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\EventOverviewForm\\:\\:commentFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/EventOverviewForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\EventOverviewForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/EventOverviewForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\EventOverviewForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/EventOverviewForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\EventOverviewForm\\:\\:downtimeFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/EventOverviewForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\EventOverviewForm\\:\\:flappingFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/EventOverviewForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\EventOverviewForm\\:\\:getFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/EventOverviewForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\EventOverviewForm\\:\\:notificationFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/EventOverviewForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\EventOverviewForm\\:\\:stateChangeFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/EventOverviewForm.php
+
+ -
+ message: "#^Cannot call method addError\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Navigation/ActionForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Navigation\\\\ActionForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Navigation/ActionForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Navigation\\\\ActionForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Navigation/ActionForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Navigation\\\\ActionForm\\:\\:isValid\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Navigation/ActionForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\BackendPage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/BackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\BackendPage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/BackendPage.php
+
+ -
+ message: "#^Cannot call method setValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/IdoResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\IdoResourcePage\\:\\:addSkipValidationCheckbox\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/IdoResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\IdoResourcePage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/IdoResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\IdoResourcePage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/IdoResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\IdoResourcePage\\:\\:isValid\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/IdoResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\IdoResourcePage\\:\\:isValidPartial\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/IdoResourcePage.php
+
+ -
+ message: "#^Parameter \\#1 \\$version1 of function version_compare expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/IdoResourcePage.php
+
+ -
+ message: "#^Cannot call method setValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/SecurityPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\SecurityPage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/SecurityPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\SecurityPage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/SecurityPage.php
+
+ -
+ message: "#^Cannot call method getValues\\(\\) on \\$this\\(Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\TransportPage\\)\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/TransportPage.php
+
+ -
+ message: "#^Cannot call method setValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/TransportPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\TransportPage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/TransportPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\TransportPage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/TransportPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\TransportPage\\:\\:getValues\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/TransportPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\TransportPage\\:\\:isValidPartial\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/TransportPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\WelcomePage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/WelcomePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\Setup\\\\WelcomePage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/Setup/WelcomePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\StatehistoryForm\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/StatehistoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\StatehistoryForm\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/StatehistoryForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Forms\\\\StatehistoryForm\\:\\:getFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/forms/StatehistoryForm.php
+
+ -
+ message: "#^Method Zend_View_Helper_CheckPerformance\\:\\:create\\(\\) has parameter \\$results with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/CheckPerformance.php
+
+ -
+ message: "#^Argument of an invalid type object supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/ContactFlags.php
+
+ -
+ message: "#^Method Zend_View_Helper_Customvar\\:\\:customvar\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/Customvar.php
+
+ -
+ message: "#^Method Zend_View_Helper_Customvar\\:\\:customvar\\(\\) has parameter \\$struct with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/Customvar.php
+
+ -
+ message: "#^Method Zend_View_Helper_Customvar\\:\\:renderArray\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/Customvar.php
+
+ -
+ message: "#^Method Zend_View_Helper_Customvar\\:\\:renderArray\\(\\) has parameter \\$array with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/Customvar.php
+
+ -
+ message: "#^Method Zend_View_Helper_Customvar\\:\\:renderObject\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/Customvar.php
+
+ -
+ message: "#^Method Zend_View_Helper_Customvar\\:\\:renderObject\\(\\) has parameter \\$object with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/Customvar.php
+
+ -
+ message: "#^Method Zend_View_Helper_HostFlags\\:\\:hostFlags\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/HostFlags.php
+
+ -
+ message: "#^Method Zend_View_Helper_HostFlags\\:\\:hostFlags\\(\\) has parameter \\$host with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/HostFlags.php
+
+ -
+ message: "#^Cannot call method qlink\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 2
+ path: modules/monitoring/application/views/helpers/Link.php
+
+ -
+ message: "#^Cannot call method translate\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 2
+ path: modules/monitoring/application/views/helpers/Link.php
+
+ -
+ message: "#^Method Zend_View_Helper_MonitoringFlags\\:\\:monitoringFlags\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/MonitoringFlags.php
+
+ -
+ message: "#^Result of && is always false\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/Perfdata.php
+
+ -
+ message: "#^Strict comparison using \\=\\=\\= between float and 100 will always evaluate to false\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/Perfdata.php
+
+ -
+ message: "#^Cannot call method baseUrl\\(\\) on Zend_View_Interface\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Cannot call method insertBefore\\(\\) on DOMNode\\|null\\.$#"
+ count: 3
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Cannot call method removeChild\\(\\) on DOMNode\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Method Zend_View_Helper_PluginOutput\\:\\:pluginOutput\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Parameter \\#1 \\$html of method Zend_View_Helper_PluginOutput\\:\\:processHtml\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Parameter \\#1 \\$html of static method Icinga\\\\Web\\\\Helper\\\\HtmlPurifier\\:\\:process\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strlen expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|null given\\.$#"
+ count: 2
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function trim expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function urlencode expects string, array\\|string given\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Property Zend_View_Helper_PluginOutput\\:\\:\\$htmlPatterns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Property Zend_View_Helper_PluginOutput\\:\\:\\$htmlReplacements type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Property Zend_View_Helper_PluginOutput\\:\\:\\$txtPatterns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Property Zend_View_Helper_PluginOutput\\:\\:\\$txtReplacements type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/PluginOutput.php
+
+ -
+ message: "#^Method Zend_View_Helper_ServiceFlags\\:\\:serviceFlags\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/ServiceFlags.php
+
+ -
+ message: "#^Method Zend_View_Helper_ServiceFlags\\:\\:serviceFlags\\(\\) has parameter \\$service with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/application/views/helpers/ServiceFlags.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\AllcontactsQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/AllcontactsQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\AllcontactsQuery\\:\\:\\$baseQuery has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/AllcontactsQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\AllcontactsQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/AllcontactsQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\AllcontactsQuery\\:\\:\\$contactgroups has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/AllcontactsQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\AllcontactsQuery\\:\\:\\$contacts has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/AllcontactsQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommandQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommandQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommandQuery\\:\\:joinContacts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommandQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommandQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommandQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommentQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommentQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommentQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommentQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_keys expects array, \\(float\\|int\\) given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommentQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommentQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommentdeletionhistoryQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommentdeletionhistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommentdeletionhistoryQuery\\:\\:joinHistory\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommentdeletionhistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommentdeletionhistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_keys expects array, \\(float\\|int\\) given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommentdeletionhistoryQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommentdeletionhistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommentdeletionhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommenteventQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenteventQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommenteventQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenteventQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommenthistoryQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommenthistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommenthistoryQuery\\:\\:joinHistory\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommenthistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommenthistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_keys expects array, \\(float\\|int\\) given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommenthistoryQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CommenthistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactQuery\\:\\:transformToUnion\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactQuery\\:\\:\\$contactQuery \\(Zend_Db_Select\\) does not accept static\\(Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactQuery\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactgroupQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactgroupQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactgroupQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactgroupQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactgroupQuery\\:\\:joinMembers\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactgroupQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactgroupQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactgroupQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactgroupQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactgroupQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactgroupQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ContactgroupQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ContactgroupQuery.php
+
+ -
+ message: "#^Cannot call method getDbType\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CustomvarQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CustomvarQuery\\:\\:getGroup\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CustomvarQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CustomvarQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CustomvarQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CustomvarQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CustomvarQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$haystack of function in_array expects array, array\\|string given\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CustomvarQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\CustomvarQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/CustomvarQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_keys expects array, \\(float\\|int\\) given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeendhistoryQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeendhistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeendhistoryQuery\\:\\:joinHistory\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeendhistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeendhistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_keys expects array, \\(float\\|int\\) given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeendhistoryQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeendhistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeendhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeeventQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeeventQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimeeventQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimeeventQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimestarthistoryQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimestarthistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimestarthistoryQuery\\:\\:joinHistory\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimestarthistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimestarthistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_keys expects array, \\(float\\|int\\) given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimestarthistoryQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\DowntimestarthistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/DowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EmptyhostgroupQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyhostgroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EmptyhostgroupQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyhostgroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EmptyhostgroupQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyhostgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EmptyservicegroupQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyservicegroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EmptyservicegroupQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyservicegroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EmptyservicegroupQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyservicegroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EmptyservicegroupQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EmptyservicegroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EventgridQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EventgridQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EventgridQuery\\:\\:\\$additionalColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EventgridhostsQuery\\:\\:joinHistory\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridhostsQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EventgridservicesQuery\\:\\:joinHistory\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridservicesQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EventhistoryQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EventhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EventhistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EventhistoryQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EventhistoryQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EventhistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EventhistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EventhistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\EventhistoryQuery\\:\\:\\$subQueries has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/EventhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\FlappingendhistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingendhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\FlappingendhistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingendhistoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_keys expects array, \\(float\\|int\\) given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingendhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\FlappingeventQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingeventQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\FlappingeventQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingeventQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\FlappingstarthistoryQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\FlappingstarthistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\FlappingstarthistoryQuery\\:\\:joinHistory\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\FlappingstarthistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\FlappingstarthistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_keys expects array, \\(float\\|int\\) given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\FlappingstarthistoryQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\FlappingstarthistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/FlappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\GroupsummaryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/GroupsummaryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\GroupsummaryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/GroupsummaryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommentQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommentQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommentQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommentQuery\\:\\:joinHoststatus\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommentQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommentQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommentQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommentQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommentQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommentQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommentQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommentQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommentdeletionhistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentdeletionhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommentdeletionhistoryQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommentdeletionhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommenthistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommenthistoryQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommenthistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommenthistoryQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommenthistoryQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommenthistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommenthistoryQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommenthistoryQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommenthistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommenthistoryQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommenthistoryQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcommenthistoryQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcontactQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcontactQuery\\:\\:joinContactgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcontactQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcontactQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcontactQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcontactQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcontactQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcontactQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcontactQuery\\:\\:joinTimeperiods\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcontactQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcontactQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcontactQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostcontactQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostcontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimeQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimeQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimeQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimeQuery\\:\\:joinHoststatus\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimeQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimeQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimeQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimeQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimeQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimeQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimeQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimeQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimeendhistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeendhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimeendhistoryQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeendhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimestarthistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimestarthistoryQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimestarthistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimestarthistoryQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimestarthistoryQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimestarthistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimestarthistoryQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimestarthistoryQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimestarthistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimestarthistoryQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimestarthistoryQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostdowntimestarthistoryQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostflappingendhistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingendhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostflappingstarthistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostflappingstarthistoryQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostflappingstarthistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostflappingstarthistoryQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostflappingstarthistoryQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostflappingstarthistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostflappingstarthistoryQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostflappingstarthistoryQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostflappingstarthistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostflappingstarthistoryQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostflappingstarthistoryQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostflappingstarthistoryQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:joinContactgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:joinContacts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:joinHoststatus\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:joinMembers\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:joinServicestatus\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupsummaryQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupsummaryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupsummaryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupsummaryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostgroupsummaryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostgroupsummaryQuery.php
+
+ -
+ message: "#^Cannot call method getDbType\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostnotificationQuery\\:\\:getGroup\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostnotificationQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostnotificationQuery\\:\\:joinContactnotifications\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostnotificationQuery\\:\\:joinHistory\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostnotificationQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostnotificationQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostnotificationQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostnotificationQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostnotificationQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostnotificationQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostnotificationQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostnotificationQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostnotificationQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatehistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatehistoryQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatehistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatehistoryQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatehistoryQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatehistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatehistoryQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatehistoryQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatehistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatehistoryQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatehistoryQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatehistoryQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatehistoryQuery\\:\\:\\$types type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:joinChecktimeperiods\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:joinContactgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:joinContacts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:joinHoststatus\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatussummaryQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatussummaryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatussummaryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatussummaryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatussummaryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatussummaryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatussummaryQuery\\:\\:\\$subSelect \\(Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\) does not accept static\\(Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatussummaryQuery\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/HoststatussummaryQuery.php
+
+ -
+ message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:filters\\(\\)\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:getOperatorSymbol\\(\\)\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:replaceById\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:setFilters\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Cannot access offset 'joinCondition' on mixed\\.$#"
+ count: 3
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Cannot access offset 'joinType' on mixed\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Cannot access offset 'tableName' on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Cannot call method getConfig\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Cannot call method getDbType\\(\\) on mixed\\.$#"
+ count: 4
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Cannot call method getTablePrefix\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Dead catch \\- Icinga\\\\Exception\\\\NotImplementedError is never thrown in the try block\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:aliasToColumnName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:aliasToColumnName\\(\\) has parameter \\$alias with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:clearGroupingRules\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:columns\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:conflictsWithVirtualTable\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:conflictsWithVirtualTable\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:createSubQuery\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:createSubQuery\\(\\) should return static\\(Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\) but returns object\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:customvarNameToTypeName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:customvarNameToTypeName\\(\\) has parameter \\$customvar with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:distinct\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:getColumnMap\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:getDefaultColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:getGroup\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:getIdoVersion\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:getMappedField\\(\\) should return string but returns null\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:hasCustomvar\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:hasCustomvar\\(\\) has parameter \\$customvar with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:hasJoinedVirtualTable\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:hasJoinedVirtualTable\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:initializeForOracle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:initializeForPostgres\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:joinCustomvar\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:joinCustomvar\\(\\) has parameter \\$customvar with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:prepareAliasIndexes\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:registerGroupColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:registerGroupColumns\\(\\) has parameter \\$groupedColumns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:registerGroupColumns\\(\\) has parameter \\$groupedTables with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:requireCustomvar\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:requireCustomvar\\(\\) has parameter \\$customvar with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:resolveColumns\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:resolveColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$alias of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:aliasToTableName\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$alias of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:isCustomVar\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$alias of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:registerGroupColumns\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$customvar of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:getCustomvarColumnName\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$field of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:getMappedField\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of function array_key_exists expects array, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$cond of method Zend_Db_Select\\:\\:joinInner\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$cond of method Zend_Db_Select\\:\\:joinLeft\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\SimpleQuery\\:\\:order\\(\\)$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$direction of method Icinga\\\\Data\\\\SimpleQuery\\:\\:order\\(\\) expects string\\|null, int\\|null given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#3 \\$cols of method Zend_Db_Select\\:\\:joinInner\\(\\) expects array\\|string, null given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#3 \\$cols of method Zend_Db_Select\\:\\:joinLeft\\(\\) expects array\\|string, null given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, array\\<int, string\\>\\|string\\|null given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, array\\<int, string\\>\\|string\\|null given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Parameter \\#4 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$aggregateColumnIdx type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$caseInsensitiveColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$customVars type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$hookedVirtualTables type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$idxAliasColumn type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$idxAliasTable type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$idxCustomAliases type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$joinedVirtualTables type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$orderColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Static property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$idoVersion \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Static property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:\\$idoVersion \\(string\\) does not accept string\\|false\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\InstanceQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/InstanceQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\InstanceQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/InstanceQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\NotificationQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\NotificationQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\NotificationQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\NotificationQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\NotificationQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\NotificationQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\NotificationeventQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationeventQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\NotificationeventQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationeventQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\NotificationhistoryQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\NotificationhistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\NotificationhistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\NotificationhistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationhistoryQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\NotificationhistoryQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationhistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\NotificationhistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/NotificationhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ProgramstatusQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ProgramstatusQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ProgramstatusQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ProgramstatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\RuntimesummaryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimesummaryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\RuntimesummaryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimesummaryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\RuntimevariablesQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/RuntimevariablesQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentQuery\\:\\:joinHoststatus\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentQuery\\:\\:joinServicestatus\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentdeletionhistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentdeletionhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommentdeletionhistoryQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommentdeletionhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommenthistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommenthistoryQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommenthistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommenthistoryQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommenthistoryQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommenthistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommenthistoryQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommenthistoryQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommenthistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommenthistoryQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommenthistoryQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecommenthistoryQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecommenthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecontactQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecontactQuery\\:\\:joinContactgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecontactQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecontactQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecontactQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecontactQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecontactQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecontactQuery\\:\\:joinTimeperiods\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecontactQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecontactQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecontactQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicecontactQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicecontactQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeQuery\\:\\:joinHoststatus\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeQuery\\:\\:joinServicestatus\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeendhistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeendhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimeendhistoryQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeendhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimestarthistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimestarthistoryQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimestarthistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimestarthistoryQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimestarthistoryQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimestarthistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimestarthistoryQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimestarthistoryQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimestarthistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimestarthistoryQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimestarthistoryQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicedowntimestarthistoryQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceflappingendhistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingendhistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceflappingstarthistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceflappingstarthistoryQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceflappingstarthistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceflappingstarthistoryQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceflappingstarthistoryQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceflappingstarthistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceflappingstarthistoryQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceflappingstarthistoryQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceflappingstarthistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceflappingstarthistoryQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceflappingstarthistoryQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceflappingstarthistoryQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServiceflappingstarthistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:joinContactgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:joinContacts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:joinHostcontactgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:joinHostcontacts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:joinMembers\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:joinServicestatus\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupsummaryQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupsummaryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupsummaryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupsummaryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicegroupsummaryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicegroupsummaryQuery.php
+
+ -
+ message: "#^Cannot call method getDbType\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicenotificationQuery\\:\\:getGroup\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicenotificationQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicenotificationQuery\\:\\:joinContactnotifications\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicenotificationQuery\\:\\:joinHistory\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicenotificationQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicenotificationQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicenotificationQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicenotificationQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicenotificationQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicenotificationQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicenotificationQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicenotificationQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicenotificationQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatehistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatehistoryQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatehistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatehistoryQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatehistoryQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatehistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatehistoryQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatehistoryQuery\\:\\:requireFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatehistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatehistoryQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatehistoryQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatehistoryQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatehistoryQuery\\:\\:\\$types type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:joinChecktimeperiods\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:joinContactgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:joinContacts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:joinHostcontactgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:joinHostcontacts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:joinHostgroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:joinHoststatus\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:joinInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:joinServicegroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:joinServicestatus\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:joinSubQuery\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:registerGroupColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:registerGroupColumns\\(\\) has parameter \\$groupedColumns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:registerGroupColumns\\(\\) has parameter \\$groupedTables with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:\\$groupBase type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:\\$groupOrigin type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\:\\:\\$subQueryTargets type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatussummaryQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatussummaryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatussummaryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatussummaryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatussummaryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatussummaryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatussummaryQuery\\:\\:\\$subSelect \\(Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\) does not accept static\\(Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatussummaryQuery\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicestatussummaryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\StatechangeeventQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/StatechangeeventQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\StatechangeeventQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/StatechangeeventQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\StatehistoryQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\StatehistoryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\StatehistoryQuery\\:\\:joinHistory\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\StatehistoryQuery\\:\\:joinHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\StatehistoryQuery\\:\\:joinServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_keys expects array, \\(float\\|int\\) given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\StatehistoryQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\StatehistoryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/StatehistoryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\StatussummaryQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/StatussummaryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\StatussummaryQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/StatussummaryQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$dir \\(int\\) of method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\StatussummaryQuery\\:\\:order\\(\\) should be compatible with parameter \\$direction \\(string\\) of method Icinga\\\\Data\\\\Sortable\\:\\:order\\(\\)$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/StatussummaryQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\StatussummaryQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/StatussummaryQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\UnhandledhostproblemsQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledhostproblemsQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\UnhandledhostproblemsQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledhostproblemsQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\UnhandledhostproblemsQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledhostproblemsQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\UnhandledhostproblemsQuery\\:\\:\\$subSelect \\(Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HoststatusQuery\\) does not accept static\\(Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\UnhandledhostproblemsQuery\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledhostproblemsQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\UnhandledserviceproblemsQuery\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledserviceproblemsQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\UnhandledserviceproblemsQuery\\:\\:joinBaseTables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledserviceproblemsQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\UnhandledserviceproblemsQuery\\:\\:\\$columnMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledserviceproblemsQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\UnhandledserviceproblemsQuery\\:\\:\\$subSelect \\(Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServicestatusQuery\\) does not accept static\\(Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\UnhandledserviceproblemsQuery\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/Ido/Query/UnhandledserviceproblemsQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Selectable\\:\\:getDbType\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Selectable\\:\\:setTablePrefix\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\MonitoringBackend\\:\\:clearInstances\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\MonitoringBackend\\:\\:from\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\MonitoringBackend\\:\\:from\\(\\) should return Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView but returns object\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\MonitoringBackend\\:\\:getProgramVersion\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\MonitoringBackend\\:\\:loadConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\MonitoringBackend\\:\\:loadConfig\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\MonitoringBackend\\:\\:query\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\MonitoringBackend\\:\\:query\\(\\) should return Icinga\\\\Data\\\\QueryInterface but returns object\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_pop expects array, array\\<int, string\\>\\|false given\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
+
+ -
+ message: "#^Parameter \\#1 \\$key of function array_key_exists expects int\\|string, string\\|null given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\MonitoringBackend\\:\\:\\$instances type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\BackendStep\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/BackendStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\BackendStep\\:\\:apply\\(\\) should return bool but returns int\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/BackendStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\BackendStep\\:\\:createBackendsIni\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/BackendStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\BackendStep\\:\\:createResourcesIni\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/BackendStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\BackendStep\\:\\:getReport\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/BackendStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\BackendStep\\:\\:\\$backendIniError has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/BackendStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\BackendStep\\:\\:\\$data has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/BackendStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\BackendStep\\:\\:\\$resourcesIniError has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/BackendStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:hostStateBackground\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:hostStateBackground\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:hostStateBackground\\(\\) has parameter \\$text with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:objectStateFlags\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:objectStateFlags\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:objectStateFlags\\(\\) has parameter \\$type with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:serviceStateBackground\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:serviceStateBackground\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:serviceStateBackground\\(\\) has parameter \\$text with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:setHostState\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:setHostState\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:setServiceState\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:setServiceState\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:shortHostState\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:shortHostState\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:shortServiceState\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:shortServiceState\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:\\$hostColors has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:\\$hostState has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:\\$hostStates has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:\\$screen has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:\\$serviceColors has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:\\$serviceState has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Cli\\\\CliUtils\\:\\:\\$serviceStates has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Cli/CliUtils.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\IcingaApiCommand\\:\\:create\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/IcingaApiCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\IcingaApiCommand\\:\\:getData\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/IcingaApiCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\IcingaApiCommand\\:\\:setData\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/IcingaApiCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Command\\\\IcingaApiCommand\\:\\:\\$data type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/IcingaApiCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Command\\\\IcingaApiCommand\\:\\:\\$next \\(static\\(Icinga\\\\Module\\\\Monitoring\\\\Command\\\\IcingaApiCommand\\)\\) does not accept Icinga\\\\Module\\\\Monitoring\\\\Command\\\\IcingaApiCommand\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/IcingaApiCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Instance\\\\DisableNotificationsExpireCommand\\:\\:setExpireTime\\(\\) has parameter \\$expireTime with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Instance/DisableNotificationsExpireCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\AddCommentCommand\\:\\:\\$persistent has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Object/AddCommentCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Object\\\\ProcessCheckResultCommand\\:\\:\\$statusCodes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Object/ProcessCheckResultCommand.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:getName\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaApiCommandRenderer\\:\\:applyFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaApiCommandRenderer\\:\\:applyFilter\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaApiCommandRenderer\\:\\:renderAcknowledgeProblem\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaApiCommandRenderer\\:\\:renderAddComment\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaApiCommandRenderer\\:\\:renderDeleteComment\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaApiCommandRenderer\\:\\:renderDeleteDowntime\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaApiCommandRenderer\\:\\:renderProcessCheckResult\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaApiCommandRenderer\\:\\:renderRemoveAcknowledgement\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaApiCommandRenderer\\:\\:renderScheduleCheck\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaApiCommandRenderer\\:\\:renderScheduleDowntime\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaApiCommandRenderer\\:\\:renderSendCustomNotification\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaApiCommandRenderer\\:\\:renderToggleInstanceFeature\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaApiCommandRenderer\\:\\:renderToggleObjectFeature\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaCommandFileCommandRenderer\\:\\:renderAcknowledgeProblem\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaCommandFileCommandRenderer\\:\\:renderAddComment\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaCommandFileCommandRenderer\\:\\:renderDeleteComment\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaCommandFileCommandRenderer\\:\\:renderDeleteDowntime\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaCommandFileCommandRenderer\\:\\:renderDisableNotificationsExpire\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaCommandFileCommandRenderer\\:\\:renderProcessCheckResult\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaCommandFileCommandRenderer\\:\\:renderRemoveAcknowledgement\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaCommandFileCommandRenderer\\:\\:renderScheduleCheck\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaCommandFileCommandRenderer\\:\\:renderScheduleDowntime\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaCommandFileCommandRenderer\\:\\:renderSendCustomNotification\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaCommandFileCommandRenderer\\:\\:renderToggleInstanceFeature\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Renderer\\\\IcingaCommandFileCommandRenderer\\:\\:renderToggleObjectFeature\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Renderer/IcingaCommandFileCommandRenderer.php
+
+ -
+ message: "#^Cannot access offset 'code' on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php
+
+ -
+ message: "#^Cannot access offset 'error' on mixed\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php
+
+ -
+ message: "#^Cannot access offset 'results' on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php
+
+ -
+ message: "#^Cannot access offset 'status' on mixed\\.$#"
+ count: 3
+ path: modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\ApiCommandTransport\\:\\:probe\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\ApiCommandTransport\\:\\:send\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\ApiCommandTransport\\:\\:sendCommand\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_pop expects array, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php
+
+ -
+ message: "#^Parameter \\#1 \\$endpoint of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\ApiCommandTransport\\:\\:getUriFor\\(\\) expects string, null given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/ApiCommandTransport.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\CommandTransport\\:\\:send\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\CommandTransportInterface\\:\\:send\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/CommandTransportInterface.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\LocalCommandFile\\:\\:send\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\LocalCommandFile\\:\\:\\$path \\(string\\) in isset\\(\\) is not nullable\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/LocalCommandFile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:forkSsh\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:getSshPipes\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:isSshAlive\\(\\) should return bool but returns mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:readStderr\\(\\) should return string but returns string\\|false\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:send\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:sendCommandString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:setResource\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:throwSshFailure\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Parameter \\#1 \\$privateKey of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:setPrivateKey\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Parameter \\#1 \\$user of method Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:setUser\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:\\$host \\(string\\) in isset\\(\\) is not nullable\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:\\$path \\(string\\) in isset\\(\\) is not nullable\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:\\$privateKey \\(string\\) in isset\\(\\) is not nullable\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:\\$sshPipes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:\\$sshProcess \\(resource\\) does not accept resource\\|false\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Command\\\\Transport\\\\RemoteCommandFile\\:\\:\\$user \\(string\\) in isset\\(\\) is not nullable\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Command/Transport/RemoteCommandFile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controller\\:\\:handleFormatRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controller\\:\\:handleFormatRequest\\(\\) has parameter \\$query with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Controller\\:\\:moduleInit\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Controller.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of static method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\MonitoringBackend\\:\\:instance\\(\\) expects string\\|null, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Controller.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Data\\\\ColumnFilterIterator\\:\\:__construct\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Data/ColumnFilterIterator.php
+
+ -
+ message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Data/CustomvarProtectionIterator.php
+
+ -
+ message: "#^Class Icinga\\\\Module\\\\Monitoring\\\\Data\\\\CustomvarProtectionIterator extends generic class IteratorIterator but does not specify its types\\: TKey, TValue, TIterator$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Data/CustomvarProtectionIterator.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Data\\\\CustomvarProtectionIterator\\:\\:current\\(\\) should return object but returns mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Data/CustomvarProtectionIterator.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_match expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Data/CustomvarProtectionIterator.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Command\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Command.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Comment\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Comment.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Comment\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Comment.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Comment\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Comment.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Commentevent\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Commentevent.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Commentevent\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Commentevent.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Contact\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Contact.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Contact\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Contact.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Contact\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Contact.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Contactgroup\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Contactgroup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Contactgroup\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Contactgroup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Contactgroup\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Contactgroup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Customvar\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Customvar.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Customvar\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Customvar.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Customvar\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Customvar.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\ConnectionInterface\\:\\:query\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery\\:\\:clearFilter\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Class Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:__construct\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:applyFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:clearFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:dump\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:fetchAll\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:fetchColumn\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:fetchPairs\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:fromParams\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:fromParams\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:getDynamicFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:getFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:getHookedColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:getIterator\\(\\) should return Icinga\\\\Module\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\IdoQuery but returns Icinga\\\\Data\\\\SimpleQuery\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:getMappedField\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:getMappedField\\(\\) has parameter \\$field with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:getOrder\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:peekAhead\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:setFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:validateFilterColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:where\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:where\\(\\) has parameter \\$condition with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:where\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:\\$connection has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:\\$filterColumns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:\\$isSorted has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/DataView.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Downtime\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Downtime.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Downtime\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Downtime.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Downtime\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Downtime.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Downtimeevent\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Downtimeevent.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Downtimeevent\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Downtimeevent.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Eventgrid\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Eventgrid.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Eventgrid\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Eventgrid.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Eventgrid\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Eventgrid.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\EventHistory\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Eventhistory.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\EventHistory\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Eventhistory.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\EventHistory\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Eventhistory.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Flappingevent\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Flappingevent.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Flappingevent\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Flappingevent.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostcomment\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hostcomment.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostcomment\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hostcomment.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostcontact\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hostcontact.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostdowntime\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hostdowntime.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostdowntime\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hostdowntime.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostgroup\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hostgroup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostgroup\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hostgroup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostgroup\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hostgroup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostgroupsummary\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hostgroupsummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostgroupsummary\\:\\:getFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hostgroupsummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostgroupsummary\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hostgroupsummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostgroupsummary\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hostgroupsummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hoststatus\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hoststatus.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hoststatus\\:\\:getSearchColumns\\(\\) has parameter \\$search with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hoststatus.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hoststatus\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hoststatus.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hoststatus\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hoststatus.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hoststatussummary\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hoststatussummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hoststatussummary\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Hoststatussummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Instance\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Instance.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Instance\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Instance.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Notification\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Notification.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Notification\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Notification.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Notification\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Notification.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Notificationevent\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Notificationevent.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Notificationevent\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Notificationevent.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Programstatus\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Programstatus.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Runtimesummary\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Runtimesummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Runtimesummary\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Runtimesummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Runtimevariables\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Runtimevariables.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Runtimevariables\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Runtimevariables.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicecomment\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicecomment.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicecomment\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicecomment.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicedowntime\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicedowntime.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicedowntime\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicedowntime.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicegroup\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicegroup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicegroup\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicegroup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicegroup\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicegroup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicegroupsummary\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicegroupsummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicegroupsummary\\:\\:getFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicegroupsummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicegroupsummary\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicegroupsummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicegroupsummary\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicegroupsummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicestatus\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicestatus.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicestatus\\:\\:getSortRules\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicestatus.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicestatus\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicestatus.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicestatussummary\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicestatussummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicestatussummary\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Servicestatussummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Statechangeevent\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Statechangeevent.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Statechangeevent\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Statechangeevent.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\StatusSummary\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Statussummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\StatusSummary\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Statussummary.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Unhandledhostproblems\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Unhandledhostproblems.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Unhandledhostproblems\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Unhandledhostproblems.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Unhandledserviceproblems\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Unhandledserviceproblems.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Unhandledserviceproblems\\:\\:getStaticFilterColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/DataView/Unhandledserviceproblems.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\DataviewExtensionHook\\:\\:getAdditionalQueryColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/DataviewExtensionHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\DataviewExtensionHook\\:\\:getAdditionalQueryColumns\\(\\) has parameter \\$queryName with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/DataviewExtensionHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\DataviewExtensionHook\\:\\:provideAdditionalQueryColumns\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/DataviewExtensionHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\DataviewExtensionHook\\:\\:provideAdditionalQueryColumns\\(\\) has parameter \\$queryName with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/DataviewExtensionHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\DetailviewExtensionHook\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/DetailviewExtensionHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\EventDetailsExtensionHook\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/EventDetailsExtensionHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\HostActionsHook\\:\\:getActionsForHost\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/HostActionsHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\HostActionsHook\\:\\:getActionsForObject\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/HostActionsHook.php
+
+ -
+ message: "#^Parameter \\#1 \\$host of method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\HostActionsHook\\:\\:getActionsForHost\\(\\) expects Icinga\\\\Module\\\\Monitoring\\\\Object\\\\Host, Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/HostActionsHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\IdoQueryExtensionHook\\:\\:extendColumnMap\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/IdoQueryExtensionHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\IdoQueryExtensionHook\\:\\:joinVirtualTable\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/IdoQueryExtensionHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\IdoQueryExtensionHook\\:\\:joinVirtualTable\\(\\) has parameter \\$virtualTable with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/IdoQueryExtensionHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\ObjectActionsHook\\:\\:createNavigation\\(\\) has parameter \\$actions with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/ObjectActionsHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\ObjectActionsHook\\:\\:getActionsForObject\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/ObjectActionsHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\ObjectDetailsTabHook\\:\\:getHeader\\(\\) should return string but returns true\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/ObjectDetailsTabHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\PluginOutputHook\\:\\:getCommands\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/PluginOutputHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\ServiceActionsHook\\:\\:getActionsForObject\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/ServiceActionsHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\ServiceActionsHook\\:\\:getActionsForService\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/ServiceActionsHook.php
+
+ -
+ message: "#^Parameter \\#1 \\$service of method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\ServiceActionsHook\\:\\:getActionsForService\\(\\) expects Icinga\\\\Module\\\\Monitoring\\\\Object\\\\Service, Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/ServiceActionsHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\TimelineProviderHook\\:\\:fetchEntries\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/TimelineProviderHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\TimelineProviderHook\\:\\:fetchForecasts\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/TimelineProviderHook.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Hook\\\\TimelineProviderHook\\:\\:getIdentifiers\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Hook/TimelineProviderHook.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\Form\\:\\:setRequirements\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/MonitoringWizard.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\Form\\:\\:setSubjectTitle\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/MonitoringWizard.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Web\\\\Form\\:\\:setSummary\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/MonitoringWizard.php
+
+ -
+ message: "#^Cannot call method addElement\\(\\) on Zend_Form_DisplayGroup\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/MonitoringWizard.php
+
+ -
+ message: "#^Cannot call method setLabel\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/MonitoringWizard.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\MonitoringWizard\\:\\:addButtons\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/MonitoringWizard.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\MonitoringWizard\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/MonitoringWizard.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\MonitoringWizard\\:\\:setupPage\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/MonitoringWizard.php
+
+ -
+ message: "#^Argument of an invalid type array\\|object supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/Acknowledgement.php
+
+ -
+ message: "#^Instanceof between mixed and Traversable will always evaluate to false\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/Acknowledgement.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\Acknowledgement\\:\\:__construct\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/Acknowledgement.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\Acknowledgement\\:\\:setProperties\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/Acknowledgement.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\Host\\:\\:getDataView\\(\\) should return Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hoststatus but returns Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/Host.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\Host\\:\\:getNotesUrls\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/Host.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$host_name\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/HostList.php
+
+ -
+ message: "#^Cannot access property \\$handled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/HostList.php
+
+ -
+ message: "#^Cannot access property \\$host_acknowledged on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/HostList.php
+
+ -
+ message: "#^Cannot access property \\$problem on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/HostList.php
+
+ -
+ message: "#^Cannot access property \\$state on mixed\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Object/HostList.php
+
+ -
+ message: "#^Cannot call method getName\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/HostList.php
+
+ -
+ message: "#^Cannot call static method getStateText\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/HostList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\HostList\\:\\:fetchObjects\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/HostList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\HostList\\:\\:getHostStatesSummaryEmpty\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/HostList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\HostList\\:\\:objectsFilter\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/HostList.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\HostList\\:\\:\\$columns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/HostList.php
+
+ -
+ message: "#^Return type \\(Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostcomment\\) of method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\HostList\\:\\:getComments\\(\\) should be compatible with return type \\(Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Comment\\) of method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:getComments\\(\\)$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/HostList.php
+
+ -
+ message: "#^Return type \\(Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostdowntime\\) of method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\HostList\\:\\:getScheduledDowntimes\\(\\) should be compatible with return type \\(Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Downtime\\) of method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:getScheduledDowntimes\\(\\)$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/HostList.php
+
+ -
+ message: "#^Dead catch \\- Exception is never thrown in the try block\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/Macro.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\Macro\\:\\:\\$icingaMacros type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/Macro.php
+
+ -
+ message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Cannot access property \\$is_json on mixed\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Cannot access property \\$varname on mixed\\.$#"
+ count: 9
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Cannot access property \\$varvalue on mixed\\.$#"
+ count: 5
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Left side of && is always true\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:__get\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:__get\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:__isset\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:applyFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:assertOneOf\\(\\) has parameter \\$oneOf with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:fetchAcknowledgement\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:getActionUrls\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:getFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:getNotesUrls\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:hideBlacklistedProperties\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:obfuscateCustomVars\\(\\) has parameter \\$_ with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:obfuscateCustomVars\\(\\) has parameter \\$customvars with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:obfuscateCustomVars\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:parseAttributeUrls\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:protectCustomVars\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:protectCustomVars\\(\\) has parameter \\$customvars with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:resolveAllStrings\\(\\) has parameter \\$strs with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:resolveAllStrings\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:setFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:where\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:where\\(\\) has parameter \\$condition with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:where\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Parameter \\#2 \\$string of function explode expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$comments type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$contactgroups \\(array\\) does not accept Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$contactgroups type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$contacts \\(array\\) does not accept Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$contacts type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$customvars \\(array\\) does not accept array\\|stdClass\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$customvars type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$customvarsWithOriginalNames \\(array\\) does not accept array\\|stdClass\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$customvarsWithOriginalNames type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$downtimes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$eventhistory \\(Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\EventHistory\\) does not accept Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$hostVariables type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$hostgroups type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$serviceVariables type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\MonitoredObject\\:\\:\\$servicegroups type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/MonitoredObject.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:addFilter\\(\\)\\.$#"
+ count: 3
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Cannot access property \\$acknowledged on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Cannot access property \\$active_checks_enabled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Cannot access property \\$event_handler_enabled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Cannot access property \\$flap_detection_enabled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Cannot access property \\$handled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Cannot access property \\$in_downtime on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Cannot access property \\$notifications_enabled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Cannot access property \\$obsessing on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Cannot access property \\$passive_checks_enabled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Cannot access property \\$problem on mixed\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Class Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:addFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:applyFilter\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:fetch\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:fetchObjects\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:getFeatureStatus\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:newFromArray\\(\\) has parameter \\$objects with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:objectsFilter\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:setColumns\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:where\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:where\\(\\) has parameter \\$condition with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:where\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:\\$columns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:\\$objects type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Variable \\$status in isset\\(\\) always exists and is always null\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ObjectList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\Service\\:\\:getDataView\\(\\) should return Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicestatus but returns Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/Service.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\Service\\:\\:getNotesUrls\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/Service.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$host_name\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$service_description\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Cannot access property \\$handled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Cannot access property \\$host_handled on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Cannot access property \\$host_problem on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Cannot access property \\$host_state on mixed\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Cannot access property \\$problem on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Cannot access property \\$service_acknowledged on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Cannot access property \\$state on mixed\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Cannot call method getHost\\(\\) on mixed\\.$#"
+ count: 5
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Cannot call method getName\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Cannot call static method getStateText\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ServiceList\\:\\:fetchObjects\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ServiceList\\:\\:getServiceStatesSummaryEmpty\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ServiceList\\:\\:initStateSummaries\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ServiceList\\:\\:objectsFilter\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ServiceList\\:\\:\\$columns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ServiceList\\:\\:\\$hostStateSummary has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ServiceList\\:\\:\\$serviceStateSummary has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Return type \\(Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Hostcomment\\) of method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ServiceList\\:\\:getComments\\(\\) should be compatible with return type \\(Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Comment\\) of method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:getComments\\(\\)$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Return type \\(Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Servicedowntime\\) of method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ServiceList\\:\\:getScheduledDowntimes\\(\\) should be compatible with return type \\(Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\Downtime\\) of method Icinga\\\\Module\\\\Monitoring\\\\Object\\\\ObjectList\\:\\:getScheduledDowntimes\\(\\)$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Object/ServiceList.php
+
+ -
+ message: "#^Binary operation \"\\-\" between float\\|null and 0\\|string results in an error\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:asInlinePie\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:calculatePieChartData\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:format\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:format\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:getLabel\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:getMinimumValue\\(\\) should return string\\|null but returns float\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:parse\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:toArray\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of method Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\ThresholdRange\\:\\:contains\\(\\) expects float, float\\|null given\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:\\$criticalThreshold \\(Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\ThresholdRange\\) does not accept float\\|Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\ThresholdRange\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:\\$maxValue \\(float\\) does not accept float\\|Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\ThresholdRange\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:\\$maxValue \\(float\\) in isset\\(\\) is not nullable\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:\\$minValue \\(float\\) does not accept float\\|Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\ThresholdRange\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:\\$minValue \\(float\\) in isset\\(\\) is not nullable\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:\\$value \\(float\\) does not accept float\\|Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\ThresholdRange\\|null\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:\\$value \\(float\\) in isset\\(\\) is not nullable\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\Perfdata\\:\\:\\$warningThreshold \\(Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\ThresholdRange\\) does not accept float\\|Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\ThresholdRange\\|null\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Result of && is always true\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Plugin/Perfdata.php
+
+ -
+ message: "#^Class Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\PerfdataSet implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/PerfdataSet.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\PerfdataSet\\:\\:asArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/PerfdataSet.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\PerfdataSet\\:\\:getIterator\\(\\) return type with generic class ArrayIterator does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/PerfdataSet.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\PerfdataSet\\:\\:parse\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/PerfdataSet.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\PerfdataSet\\:\\:skipSpaces\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/PerfdataSet.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Plugin\\\\PerfdataSet\\:\\:\\$perfdata type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Plugin/PerfdataSet.php
+
+ -
+ message: "#^Cannot access property \\$is_currently_running on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/ProvidedHook/ApplicationState.php
+
+ -
+ message: "#^Cannot access property \\$status_update_time on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/ProvidedHook/ApplicationState.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\ProvidedHook\\\\ApplicationState\\:\\:collectMessages\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/ProvidedHook/ApplicationState.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\ProvidedHook\\\\Health\\:\\:getProgramStatus\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/ProvidedHook/Health.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\ProvidedHook\\\\Health\\:\\:\\$programStatus \\(object\\) does not accept mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/ProvidedHook/Health.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$host_name\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/ProvidedHook/X509/Sni.php
+
+ -
+ message: "#^Cannot access property \\$host_address on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/ProvidedHook/X509/Sni.php
+
+ -
+ message: "#^Cannot access property \\$host_address6 on mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/ProvidedHook/X509/Sni.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\SecurityStep\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/SecurityStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\SecurityStep\\:\\:getReport\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/SecurityStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\SecurityStep\\:\\:\\$data has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/SecurityStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\SecurityStep\\:\\:\\$error has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/SecurityStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeEntry\\:\\:fromArray\\(\\) has parameter \\$attributes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeEntry.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeEntry\\:\\:setDateTime\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeEntry.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeEntry\\:\\:setDetailUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeEntry.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeEntry\\:\\:setLabel\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeEntry.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeEntry\\:\\:setName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeEntry.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeEntry\\:\\:setValue\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeEntry.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeEntry\\:\\:setWeight\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeEntry.php
+
+ -
+ message: "#^Class Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:__construct\\(\\) has parameter \\$identifiers with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:fetchEntries\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:fetchForecasts\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:fetchResults\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:getCalculationBase\\(\\) should return float\\|null but returns mixed\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:getGroupInfo\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:getIterator\\(\\) return type with generic class ArrayIterator does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:groupEntries\\(\\) has parameter \\$entries with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:groupEntries\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:setDisplayRange\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:setForecastRange\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:setMaximumCircleWidth\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:setMinimumCircleWidth\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:setSession\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Parameter \\#1 \\$key of function array_key_exists expects int\\|string, int\\|stdClass given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Parameter \\#2 \\$base of function log expects float, float\\|null given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Parameter \\#2 \\$filter of static method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:where\\(\\) expects string, array\\<int, \\(int\\|string\\)\\> given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Possibly invalid array key type int\\|stdClass\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:\\$calculationBase \\(float\\) does not accept mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:\\$displayGroups type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:\\$identifiers type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeLine\\:\\:\\$resultset type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeLine.php
+
+ -
+ message: "#^Class Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeRange implements generic interface Iterator but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeRange.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Timeline\\\\TimeRange\\:\\:validateTime\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Timeline/TimeRange.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\TransportStep\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/TransportStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\TransportStep\\:\\:getReport\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/TransportStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\TransportStep\\:\\:\\$data has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/TransportStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\TransportStep\\:\\:\\$error has no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/TransportStep.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Action_HelperBroker\\:\\:viewRenderer\\(\\)\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Cannot call method remove\\(\\) on null\\.$#"
+ count: 2
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Controller\\\\MonitoredObjectController\\:\\:acknowledgeProblemAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Controller\\\\MonitoredObjectController\\:\\:addCommentAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Controller\\\\MonitoredObjectController\\:\\:createTabs\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Controller\\\\MonitoredObjectController\\:\\:handleFormatRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Controller\\\\MonitoredObjectController\\:\\:handleFormatRequest\\(\\) has parameter \\$query with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Controller\\\\MonitoredObjectController\\:\\:historyAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Controller\\\\MonitoredObjectController\\:\\:prepareInit\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Controller\\\\MonitoredObjectController\\:\\:rescheduleCheckAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Controller\\\\MonitoredObjectController\\:\\:scheduleDowntimeAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Controller\\\\MonitoredObjectController\\:\\:setupQuickActionForms\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Controller\\\\MonitoredObjectController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Controller\\\\MonitoredObjectController\\:\\:tabhookAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Web\\\\Widget\\\\Tabs\\:\\:activate\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Helper\\\\PluginOutputHookRenderer\\:\\:renderCommand\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Helper/PluginOutputHookRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Helper\\\\PluginOutputHookRenderer\\:\\:renderCommand\\(\\) has parameter \\$command with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Helper/PluginOutputHookRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Helper\\\\PluginOutputHookRenderer\\:\\:renderCommand\\(\\) has parameter \\$detail with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Helper/PluginOutputHookRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Helper\\\\PluginOutputHookRenderer\\:\\:renderCommand\\(\\) has parameter \\$output with no type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Helper/PluginOutputHookRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Helper\\\\PluginOutputHookRenderer\\:\\:\\$commandMap type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Helper/PluginOutputHookRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Navigation\\\\Renderer\\\\MonitoringBadgeNavigationItemRenderer\\:\\:fetchDataView\\(\\) should return object but returns mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Navigation/Renderer/MonitoringBadgeNavigationItemRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Navigation\\\\Renderer\\\\MonitoringBadgeNavigationItemRenderer\\:\\:getColumns\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Navigation/Renderer/MonitoringBadgeNavigationItemRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Navigation\\\\Renderer\\\\MonitoringBadgeNavigationItemRenderer\\:\\:setColumns\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Navigation/Renderer/MonitoringBadgeNavigationItemRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Navigation\\\\Renderer\\\\MonitoringBadgeNavigationItemRenderer\\:\\:\\$columns type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Navigation/Renderer/MonitoringBadgeNavigationItemRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Navigation\\\\Renderer\\\\MonitoringBadgeNavigationItemRenderer\\:\\:\\$dataViews type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Navigation/Renderer/MonitoringBadgeNavigationItemRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Navigation\\\\Renderer\\\\MonitoringBadgeNavigationItemRenderer\\:\\:\\$summaries type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Navigation/Renderer/MonitoringBadgeNavigationItemRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Rest\\\\RestRequest\\:\\:curlExec\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Rest/RestRequest.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Rest\\\\RestRequest\\:\\:curlExec\\(\\) should return string but returns string\\|true\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Rest/RestRequest.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Rest\\\\RestRequest\\:\\:serializePayload\\(\\) should return string but returns mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Rest/RestRequest.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$arrays of function array_merge expects array, array\\<string, int\\|string\\>\\|false given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Rest/RestRequest.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\CustomVarTable\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type iterable\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\CustomVarTable\\:\\:assemble\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\CustomVarTable\\:\\:renderArray\\(\\) has parameter \\$array with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\CustomVarTable\\:\\:renderGroup\\(\\) has parameter \\$entries with no value type specified in iterable type iterable\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\CustomVarTable\\:\\:renderObject\\(\\) has parameter \\$object with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function key expects array\\|object, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\CustomVarTable\\:\\:renderArray\\(\\) expects array, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php
+
+ -
+ message: "#^Parameter \\#2 \\$object of method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\CustomVarTable\\:\\:renderObject\\(\\) expects array, mixed given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method ipl\\\\Html\\\\Attributes\\:\\:add\\(\\) expects array\\|bool\\|string\\|null, int given\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\CustomVarTable\\:\\:\\$data type has no value type specified in iterable type iterable\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\CustomVarTable\\:\\:\\$groups type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\SelectBox\\:\\:__construct\\(\\) has parameter \\$values with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/SelectBox.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\SelectBox\\:\\:applyRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/SelectBox.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\SelectBox\\:\\:getInterval\\(\\) should return string\\|null but returns mixed\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/SelectBox.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\SelectBox\\:\\:\\$values type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/SelectBox.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$count\\.$#"
+ count: 3
+ path: modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$filter\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$translateArgs\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$translatePlural\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php
+
+ -
+ message: "#^Access to an undefined property object\\:\\:\\$translateSingular\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\StateBadges\\:\\:add\\(\\) has parameter \\$filter with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\StateBadges\\:\\:add\\(\\) has parameter \\$translateArgs with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\StateBadges\\:\\:createBadgeGroup\\(\\) has parameter \\$states with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\StateBadges\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Monitoring\\\\Web\\\\Widget\\\\StateBadges\\:\\:\\$baseFilter \\(Icinga\\\\Data\\\\Filter\\\\Filter\\) in isset\\(\\) is not nullable\\.$#"
+ count: 1
+ path: modules/monitoring/library/Monitoring/Web/Widget/StateBadges.php
+
+ -
+ message: "#^Cannot call method generate\\(\\) on Icinga\\\\Module\\\\Setup\\\\Webserver\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Cannot call method getConfigDir\\(\\) on Icinga\\\\Module\\\\Setup\\\\Webserver\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Cannot call method getDocumentRoot\\(\\) on Icinga\\\\Module\\\\Setup\\\\Webserver\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Cannot call method getEnableFpm\\(\\) on Icinga\\\\Module\\\\Setup\\\\Webserver\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Cannot call method getFpmUri\\(\\) on Icinga\\\\Module\\\\Setup\\\\Webserver\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Cannot call method getUrlPath\\(\\) on Icinga\\\\Module\\\\Setup\\\\Webserver\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Cannot call method setDocumentRoot\\(\\) on Icinga\\\\Module\\\\Setup\\\\Webserver\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Clicommands\\\\ConfigCommand\\:\\:directoryAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Clicommands\\\\ConfigCommand\\:\\:webserverAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Offset 'message' does not exist on array\\{type\\: int, message\\: string, file\\: string, line\\: int\\}\\|null\\.$#"
+ count: 3
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function file_exists expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function file_put_contents expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$flag of method Icinga\\\\Module\\\\Setup\\\\Webserver\\:\\:setEnableFpm\\(\\) expects bool, mixed given\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function trim expects string, mixed given\\.$#"
+ count: 7
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$type of static method Icinga\\\\Module\\\\Setup\\\\Webserver\\:\\:createInstance\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Parameter \\#2 \\$permissions of function chmod expects int, float\\|int given\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/ConfigCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Clicommands\\\\TokenCommand\\:\\:createAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/TokenCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Clicommands\\\\TokenCommand\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/TokenCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function md5 expects string, int\\<0, max\\> given\\.$#"
+ count: 1
+ path: modules/setup/application/clicommands/TokenCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Controllers\\\\IndexController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/controllers/IndexController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Controllers\\\\IndexController\\:\\:restartAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/controllers/IndexController.php
+
+ -
+ message: "#^Cannot call method addError\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AdminAccountPage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AdminAccountPage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AdminAccountPage\\:\\:createUserBackend\\(\\) should return Icinga\\\\Authentication\\\\User\\\\DbUserBackend\\|Icinga\\\\Authentication\\\\User\\\\LdapUserBackend but returns Icinga\\\\Authentication\\\\User\\\\UserBackendInterface\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AdminAccountPage\\:\\:createUserGroupBackend\\(\\) should return Icinga\\\\Authentication\\\\UserGroup\\\\LdapUserGroupBackend but returns Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackendInterface&Icinga\\\\Data\\\\Selectable\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AdminAccountPage\\:\\:fetchGroups\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AdminAccountPage\\:\\:fetchUsers\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AdminAccountPage\\:\\:isValid\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AdminAccountPage\\:\\:setBackendConfig\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AdminAccountPage\\:\\:setGroupConfig\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AdminAccountPage\\:\\:setResourceConfig\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of static method Icinga\\\\Authentication\\\\UserGroup\\\\UserGroupBackend\\:\\:create\\(\\) expects string, null given\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of static method Icinga\\\\Authentication\\\\User\\\\UserBackend\\:\\:create\\(\\) expects string, null given\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\AdminAccountPage\\:\\:\\$backendConfig type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\AdminAccountPage\\:\\:\\$groupConfig \\(array\\) does not accept array\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\AdminAccountPage\\:\\:\\$groupConfig type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\AdminAccountPage\\:\\:\\$resourceConfig type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AdminAccountPage.php
+
+ -
+ message: "#^Cannot call method getElement\\(\\) on \\$this\\(Icinga\\\\Module\\\\Setup\\\\Forms\\\\AuthBackendPage\\)\\|null\\.$#"
+ count: 2
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Cannot call method getElement\\(\\) on Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\DbBackendForm\\|Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\ExternalBackendForm\\|Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\LdapBackendForm\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Cannot call method setIgnore\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Cannot call method setValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AuthBackendPage\\:\\:addSkipValidationCheckbox\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AuthBackendPage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AuthBackendPage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AuthBackendPage\\:\\:getValues\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AuthBackendPage\\:\\:isValid\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AuthBackendPage\\:\\:isValidPartial\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AuthBackendPage\\:\\:setResourceConfig\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Parameter \\#1 \\$cue of method Icinga\\\\Web\\\\Form\\:\\:setRequiredCue\\(\\) expects string, null given\\.$#"
+ count: 2
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Parameter \\#1 \\$form of method Icinga\\\\Web\\\\Form\\:\\:addSubForm\\(\\) expects Zend_Form, Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\DbBackendForm\\|Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\ExternalBackendForm\\|Icinga\\\\Forms\\\\Config\\\\UserBackend\\\\LdapBackendForm\\|null given\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\AuthBackendPage\\:\\:\\$config \\(array\\) in isset\\(\\) is not nullable\\.$#"
+ count: 2
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\AuthBackendPage\\:\\:\\$config type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AuthBackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AuthenticationPage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AuthenticationPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\AuthenticationPage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AuthenticationPage.php
+
+ -
+ message: "#^Parameter \\#1 \\$cue of method Icinga\\\\Web\\\\Form\\:\\:setRequiredCue\\(\\) expects string, null given\\.$#"
+ count: 1
+ path: modules/setup/application/forms/AuthenticationPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\DatabaseCreationPage\\:\\:addSkipValidationCheckbox\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DatabaseCreationPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\DatabaseCreationPage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DatabaseCreationPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\DatabaseCreationPage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DatabaseCreationPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\DatabaseCreationPage\\:\\:isValid\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DatabaseCreationPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\DatabaseCreationPage\\:\\:setDatabaseSetupPrivileges\\(\\) has parameter \\$privileges with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DatabaseCreationPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\DatabaseCreationPage\\:\\:setDatabaseUsagePrivileges\\(\\) has parameter \\$privileges with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DatabaseCreationPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\DatabaseCreationPage\\:\\:setResourceConfig\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DatabaseCreationPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\DatabaseCreationPage\\:\\:\\$config type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DatabaseCreationPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\DatabaseCreationPage\\:\\:\\$databaseSetupPrivileges type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DatabaseCreationPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\DatabaseCreationPage\\:\\:\\$databaseUsagePrivileges type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DatabaseCreationPage.php
+
+ -
+ message: "#^Cannot call method setValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DbResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\DbResourcePage\\:\\:addSkipValidationCheckbox\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DbResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\DbResourcePage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DbResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\DbResourcePage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DbResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\DbResourcePage\\:\\:isValid\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DbResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\DbResourcePage\\:\\:isValidPartial\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DbResourcePage.php
+
+ -
+ message: "#^Parameter \\#1 \\$version1 of function version_compare expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/setup/application/forms/DbResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\GeneralConfigPage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/GeneralConfigPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\GeneralConfigPage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/GeneralConfigPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapDiscoveryConfirmPage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryConfirmPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapDiscoveryConfirmPage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryConfirmPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapDiscoveryConfirmPage\\:\\:getValues\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryConfirmPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapDiscoveryConfirmPage\\:\\:getValues\\(\\) should return array but returns null\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryConfirmPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapDiscoveryConfirmPage\\:\\:isValid\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryConfirmPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapDiscoveryConfirmPage\\:\\:setResourceConfig\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryConfirmPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapDiscoveryConfirmPage\\:\\:\\$config type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryConfirmPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapDiscoveryConfirmPage\\:\\:\\$infoTemplate has no type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryConfirmPage.php
+
+ -
+ message: "#^Cannot call method addError\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryPage.php
+
+ -
+ message: "#^Cannot call method getMessage\\(\\) on Exception\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapDiscoveryPage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapDiscoveryPage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapDiscoveryPage\\:\\:getValues\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapDiscoveryPage\\:\\:isValid\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapDiscoveryPage\\:\\:\\$discovery \\(Icinga\\\\Protocol\\\\Ldap\\\\Discovery\\) in isset\\(\\) is not nullable\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapDiscoveryPage.php
+
+ -
+ message: "#^Cannot call method setValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapResourcePage\\:\\:addSkipValidationCheckbox\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapResourcePage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapResourcePage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapResourcePage\\:\\:isValid\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapResourcePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\LdapResourcePage\\:\\:isValidPartial\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/LdapResourcePage.php
+
+ -
+ message: "#^Call to an undefined method Zend_Form_Element\\:\\:isChecked\\(\\)\\.$#"
+ count: 1
+ path: modules/setup/application/forms/ModulePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\ModulePage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/ModulePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\ModulePage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/ModulePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\ModulePage\\:\\:getCheckedModules\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/ModulePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\ModulePage\\:\\:getModuleWizards\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/ModulePage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\ModulePage\\:\\:\\$foundIcingaDB has no type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/ModulePage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\ModulePage\\:\\:\\$modulePaths has no type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/ModulePage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\ModulePage\\:\\:\\$modules has no type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/ModulePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\RequirementsPage\\:\\:isValid\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/RequirementsPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\SummaryPage\\:\\:getSummary\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/SummaryPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\SummaryPage\\:\\:setSubjectTitle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/SummaryPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\SummaryPage\\:\\:setSummary\\(\\) has parameter \\$summary with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/SummaryPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\SummaryPage\\:\\:\\$summary type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/SummaryPage.php
+
+ -
+ message: "#^Cannot call method setValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: modules/setup/application/forms/UserGroupBackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\UserGroupBackendPage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/UserGroupBackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\UserGroupBackendPage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/UserGroupBackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\UserGroupBackendPage\\:\\:getValues\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/UserGroupBackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\UserGroupBackendPage\\:\\:setBackendConfig\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/UserGroupBackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\UserGroupBackendPage\\:\\:setResourceConfig\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/UserGroupBackendPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\UserGroupBackendPage\\:\\:\\$backendConfig type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/UserGroupBackendPage.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Forms\\\\UserGroupBackendPage\\:\\:\\$resourceConfig type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/UserGroupBackendPage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\WelcomePage\\:\\:createElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/application/forms/WelcomePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Forms\\\\WelcomePage\\:\\:createElements\\(\\) has parameter \\$formData with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/application/forms/WelcomePage.php
+
+ -
+ message: "#^Parameter \\#1 \\$cue of method Icinga\\\\Web\\\\Form\\:\\:setRequiredCue\\(\\) expects string, null given\\.$#"
+ count: 1
+ path: modules/setup/application/forms/WelcomePage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Requirement\\:\\:__construct\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Requirement\\:\\:getDescriptions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Requirement\\:\\:getState\\(\\) should return int but returns bool\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Requirement\\:\\:\\$descriptions type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of static method Icinga\\\\Application\\\\Platform\\:\\:classExists\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/ClassRequirement.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 2
+ path: modules/setup/library/Setup/Requirement/ClassRequirement.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function file_exists expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function is_readable expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function is_writable expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 3
+ path: modules/setup/library/Setup/Requirement/ConfigDirectoryRequirement.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/OSRequirement.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function ucfirst expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/OSRequirement.php
+
+ -
+ message: "#^Cannot use array destructuring on mixed\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/PhpConfigRequirement.php
+
+ -
+ message: "#^Parameter \\#1 \\$option of static method Icinga\\\\Application\\\\Platform\\:\\:getPhpConfig\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/PhpConfigRequirement.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 2
+ path: modules/setup/library/Setup/Requirement/PhpConfigRequirement.php
+
+ -
+ message: "#^Parameter \\#1 \\$extensionName of static method Icinga\\\\Application\\\\Platform\\:\\:extensionLoaded\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/PhpModuleRequirement.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 2
+ path: modules/setup/library/Setup/Requirement/PhpModuleRequirement.php
+
+ -
+ message: "#^Strict comparison using \\=\\=\\= between string and null will always evaluate to false\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/PhpModuleRequirement.php
+
+ -
+ message: "#^Cannot use array destructuring on mixed\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/PhpVersionRequirement.php
+
+ -
+ message: "#^Parameter \\#2 \\$version2 of function version_compare expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/PhpVersionRequirement.php
+
+ -
+ message: "#^Parameter \\#3 \\$operator of function version_compare expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/PhpVersionRequirement.php
+
+ -
+ message: "#^Cannot call method getState\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/SetRequirement.php
+
+ -
+ message: "#^Cannot call method getVersion\\(\\) on Icinga\\\\Application\\\\Libraries\\\\Library\\|null\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/WebLibraryRequirement.php
+
+ -
+ message: "#^Cannot use array destructuring on mixed\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/WebLibraryRequirement.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Libraries\\:\\:get\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/WebLibraryRequirement.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Libraries\\:\\:has\\(\\) expects string, mixed given\\.$#"
+ count: 2
+ path: modules/setup/library/Setup/Requirement/WebLibraryRequirement.php
+
+ -
+ message: "#^Cannot use array destructuring on mixed\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/WebModuleRequirement.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:getModule\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/WebModuleRequirement.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Application\\\\Modules\\\\Manager\\:\\:hasInstalled\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/WebModuleRequirement.php
+
+ -
+ message: "#^Parameter \\#2 \\$version2 of function version_compare expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/WebModuleRequirement.php
+
+ -
+ message: "#^Parameter \\#3 \\$operator of function version_compare expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Requirement/WebModuleRequirement.php
+
+ -
+ message: "#^Class Icinga\\\\Module\\\\Setup\\\\RequirementSet implements generic interface RecursiveIterator but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementSet.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\RequirementSet\\:\\:getAll\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementSet.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\RequirementSet\\:\\:getChildren\\(\\) return type with generic interface RecursiveIterator does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementSet.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\RequirementSet\\:\\:getChildren\\(\\) should return RecursiveIterator\\|null but returns Icinga\\\\Module\\\\Setup\\\\Requirement\\|Icinga\\\\Module\\\\Setup\\\\RequirementSet\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementSet.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\RequirementSet\\:\\:getMode\\(\\) should return int but returns string\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementSet.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\RequirementSet\\:\\:key\\(\\) should return int but returns int\\|string\\|null\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementSet.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\RequirementSet\\:\\:\\$mode \\(string\\) does not accept int\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementSet.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\RequirementSet\\:\\:\\$requirements type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementSet.php
+
+ -
+ message: "#^Cannot call method getDescriptions\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementsRenderer.php
+
+ -
+ message: "#^Cannot call method getState\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementsRenderer.php
+
+ -
+ message: "#^Cannot call method getStateText\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementsRenderer.php
+
+ -
+ message: "#^Cannot call method getTitle\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementsRenderer.php
+
+ -
+ message: "#^Cannot call method isOptional\\(\\) on mixed\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementsRenderer.php
+
+ -
+ message: "#^Class Icinga\\\\Module\\\\Setup\\\\RequirementsRenderer extends generic class RecursiveIteratorIterator but does not specify its types\\: T$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementsRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\RequirementsRenderer\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementsRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\RequirementsRenderer\\:\\:\\$tags has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/RequirementsRenderer.php
+
+ -
+ message: "#^Class Icinga\\\\Module\\\\Setup\\\\Setup implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
+ count: 1
+ path: modules/setup/library/Setup/Setup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Setup\\:\\:addStep\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Setup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Setup\\:\\:addSteps\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Setup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Setup\\:\\:addSteps\\(\\) has parameter \\$steps with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Setup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Setup\\:\\:getReport\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Setup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Setup\\:\\:getSteps\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Setup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Setup\\:\\:getSummary\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Setup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Setup\\:\\:run\\(\\) should return bool but returns bool\\|int\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Setup.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Setup\\:\\:\\$state has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Setup.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Setup\\:\\:\\$steps has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Setup.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Step\\:\\:getReport\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Step.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\AuthenticationStep\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/AuthenticationStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\AuthenticationStep\\:\\:apply\\(\\) should return bool but returns int\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/AuthenticationStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\AuthenticationStep\\:\\:createAccount\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/AuthenticationStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\AuthenticationStep\\:\\:createAuthenticationIni\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/AuthenticationStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\AuthenticationStep\\:\\:createRolesIni\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/AuthenticationStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\AuthenticationStep\\:\\:getReport\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/AuthenticationStep.php
+
+ -
+ message: "#^Parameter \\#1 \\$ds of class Icinga\\\\Authentication\\\\User\\\\DbUserBackend constructor expects Icinga\\\\Data\\\\Db\\\\DbConnection, Icinga\\\\Data\\\\Selectable given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/AuthenticationStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\AuthenticationStep\\:\\:\\$authIniError has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/AuthenticationStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\AuthenticationStep\\:\\:\\$data has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/AuthenticationStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\AuthenticationStep\\:\\:\\$dbError has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/AuthenticationStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\AuthenticationStep\\:\\:\\$permIniError has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/AuthenticationStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\DatabaseStep\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/DatabaseStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\DatabaseStep\\:\\:getReport\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/DatabaseStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\DatabaseStep\\:\\:log\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/DatabaseStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\DatabaseStep\\:\\:setupMysqlDatabase\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/DatabaseStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\DatabaseStep\\:\\:setupPgsqlDatabase\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/DatabaseStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\DatabaseStep\\:\\:\\$data has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/DatabaseStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\DatabaseStep\\:\\:\\$error has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/DatabaseStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\DatabaseStep\\:\\:\\$messages has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/DatabaseStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\GeneralConfigStep\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/GeneralConfigStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\GeneralConfigStep\\:\\:getReport\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/GeneralConfigStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\GeneralConfigStep\\:\\:\\$data has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/GeneralConfigStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\GeneralConfigStep\\:\\:\\$error has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/GeneralConfigStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\ResourceStep\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/ResourceStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\ResourceStep\\:\\:getReport\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/ResourceStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\ResourceStep\\:\\:\\$data has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/ResourceStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\ResourceStep\\:\\:\\$error has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/ResourceStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\UserGroupStep\\:\\:__construct\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/UserGroupStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\UserGroupStep\\:\\:createGroupsIni\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/UserGroupStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\UserGroupStep\\:\\:createMembership\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/UserGroupStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\UserGroupStep\\:\\:createUserGroup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/UserGroupStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\UserGroupStep\\:\\:getReport\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/UserGroupStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Steps\\\\UserGroupStep\\:\\:getSummary\\(\\) should return string but empty return statement found\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/UserGroupStep.php
+
+ -
+ message: "#^Parameter \\#1 \\$ds of class Icinga\\\\Authentication\\\\UserGroup\\\\DbUserGroupBackend constructor expects Icinga\\\\Data\\\\Db\\\\DbConnection, Icinga\\\\Data\\\\Selectable given\\.$#"
+ count: 2
+ path: modules/setup/library/Setup/Steps/UserGroupStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\UserGroupStep\\:\\:\\$data has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/UserGroupStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\UserGroupStep\\:\\:\\$groupError has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/UserGroupStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\UserGroupStep\\:\\:\\$groupIniError has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/UserGroupStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Steps\\\\UserGroupStep\\:\\:\\$memberError has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Steps/UserGroupStep.php
+
+ -
+ message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Cannot call method fetchAll\\(\\) on mixed\\.$#"
+ count: 2
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Cannot call method fetchColumn\\(\\) on mixed\\.$#"
+ count: 9
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Cannot call method fetchObject\\(\\) on mixed\\.$#"
+ count: 6
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:__construct\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:addLogin\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:assertConnectedToDb\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:assertDatabaseAccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:assertHostAccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:checkConnectivity\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:checkMysqlPrivileges\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:checkMysqlPrivileges\\(\\) has parameter \\$privileges with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:checkPgsqlPrivileges\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:checkPgsqlPrivileges\\(\\) has parameter \\$privileges with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:checkPrivileges\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:checkPrivileges\\(\\) has parameter \\$privileges with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:connect\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:exec\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:exec\\(\\) should return int but returns int\\|false\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:grantPrivileges\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:grantPrivileges\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:grantPrivileges\\(\\) has parameter \\$privileges with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:import\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:isGrantable\\(\\) has parameter \\$privileges with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:listTables\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:pdoConnect\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:query\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:reconnect\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:zendConnect\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Parameter \\#1 \\$dbname of method Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:pdoConnect\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of method PDO\\:\\:quote\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, array\\|null given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of function array_map expects array, array\\|null given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of function join expects array\\<string\\>, array\\<int, array\\|string\\|false\\> given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:\\$config type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:\\$mysqlGrantContexts type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:\\$pdoConn \\(PDO\\) does not accept null\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:\\$pgsqlGrantContexts type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Utils\\\\DbTool\\:\\:\\$zendConn \\(Zend_Db_Adapter_Pdo_Abstract\\) does not accept null\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/DbTool.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\EnableModuleStep\\:\\:__construct\\(\\) has parameter \\$moduleNames with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/EnableModuleStep.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Utils\\\\EnableModuleStep\\:\\:getReport\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/EnableModuleStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Utils\\\\EnableModuleStep\\:\\:\\$errors has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/EnableModuleStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Utils\\\\EnableModuleStep\\:\\:\\$moduleNames has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/EnableModuleStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Utils\\\\EnableModuleStep\\:\\:\\$modulePaths has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/EnableModuleStep.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\Utils\\\\EnableModuleStep\\:\\:\\$warnings has no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Utils/EnableModuleStep.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function trim expects string, string\\|false given\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Web/Form/Validator/TokenValidator.php
+
+ -
+ message: "#^Cannot call method addElement\\(\\) on Zend_Form_DisplayGroup\\|null\\.$#"
+ count: 2
+ path: modules/setup/library/Setup/WebWizard.php
+
+ -
+ message: "#^Cannot call method getSubForm\\(\\) on Icinga\\\\Web\\\\Form\\|null\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/WebWizard.php
+
+ -
+ message: "#^Cannot call method populate\\(\\) on Icinga\\\\Web\\\\Form\\|null\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/WebWizard.php
+
+ -
+ message: "#^Cannot call method setLabel\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: modules/setup/library/Setup/WebWizard.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\WebWizard\\:\\:addButtons\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/WebWizard.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\WebWizard\\:\\:clearSession\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/WebWizard.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\WebWizard\\:\\:getRequirements\\(\\) has parameter \\$skipModules with no type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/WebWizard.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\WebWizard\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/WebWizard.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\WebWizard\\:\\:setupPage\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/WebWizard.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\WebWizard\\:\\:\\$databaseCreationPrivileges type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/WebWizard.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\WebWizard\\:\\:\\$databaseSetupPrivileges type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/WebWizard.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\WebWizard\\:\\:\\$databaseTables type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/WebWizard.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Setup\\\\WebWizard\\:\\:\\$databaseUsagePrivileges type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/WebWizard.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Setup\\\\Webserver\\:\\:createInstance\\(\\) should return Icinga\\\\Module\\\\Setup\\\\Webserver but returns object\\.$#"
+ count: 1
+ path: modules/setup/library/Setup/Webserver.php
+
+ -
+ message: "#^Cannot access property \\$childNodes on DOMNode\\|null\\.$#"
+ count: 2
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Cannot access property \\$length on DOMNodeList\\<DOMNode\\>\\|false\\.$#"
+ count: 1
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Cannot access property \\$nodeValue on DOMNode\\|null\\.$#"
+ count: 2
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Cannot access property \\$parentNode on DOMNode\\|null\\.$#"
+ count: 2
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Cannot call method appendChild\\(\\) on DOMNode\\|null\\.$#"
+ count: 3
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Cannot call method getAttribute\\(\\) on DOMNode\\|null\\.$#"
+ count: 1
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Cannot call method hasChildNodes\\(\\) on DOMNode\\|null\\.$#"
+ count: 4
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Cannot call method item\\(\\) on DOMNodeList\\<DOMNode\\>\\|false\\.$#"
+ count: 4
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Cannot call method removeChild\\(\\) on DOMNode\\|null\\.$#"
+ count: 4
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Cannot call method setAttribute\\(\\) on DOMNode\\|null\\.$#"
+ count: 1
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Test\\\\Clicommands\\\\PhpCommand\\:\\:adjustPhpunitDom\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Test\\\\Clicommands\\\\PhpCommand\\:\\:getEnvironmentVariables\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Test\\\\Clicommands\\\\PhpCommand\\:\\:styleAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Test\\\\Clicommands\\\\PhpCommand\\:\\:unitAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Negated boolean expression is always true\\.$#"
+ count: 2
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function file_exists expects string, mixed given\\.$#"
+ count: 1
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function file_exists expects string, string\\|false given\\.$#"
+ count: 1
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of function realpath expects string, mixed given\\.$#"
+ count: 1
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$source of method DOMDocument\\:\\:loadXML\\(\\) expects string, string\\|false given\\.$#"
+ count: 1
+ path: modules/test/application/clicommands/PhpCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\CompileCommand\\:\\:moduleAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/CompileCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$code of method Icinga\\\\Module\\\\Translation\\\\Cli\\\\TranslationCommand\\:\\:validateLocaleCode\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/CompileCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Translation\\\\Cli\\\\TranslationCommand\\:\\:validateModuleName\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/CompileCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\RefreshCommand\\:\\:moduleAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/RefreshCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$code of method Icinga\\\\Module\\\\Translation\\\\Cli\\\\TranslationCommand\\:\\:validateLocaleCode\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/RefreshCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Translation\\\\Cli\\\\TranslationCommand\\:\\:validateModuleName\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/RefreshCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:callTranslated\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:callTranslated\\(\\) has parameter \\$arguments with no type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:callTranslated\\(\\) has parameter \\$callback with no type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:callTranslated\\(\\) has parameter \\$locale with no type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:dateformatterAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:defaultAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:getMultiTranslated\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:getMultiTranslated\\(\\) has parameter \\$arguments with no type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:getMultiTranslated\\(\\) has parameter \\$callback with no type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:getMultiTranslated\\(\\) has parameter \\$locales with no type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:getMultiTranslated\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:printTable\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:printTable\\(\\) has parameter \\$rows with no type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Clicommands\\\\TestCommand\\:\\:\\$locales has no type specified\\.$#"
+ count: 1
+ path: modules/translation/application/clicommands/TestCommand.php
+
+ -
+ message: "#^Argument of an invalid type int supplied for foreach, only iterables are supported\\.$#"
+ count: 3
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Cannot access offset int\\<0, max\\> on int\\.$#"
+ count: 2
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Cannot access offset mixed on int\\.$#"
+ count: 5
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:__construct\\(\\) has parameter \\$rows with no type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:__construct\\(\\) with return type void returns \\$this\\(Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\) but should not return anything\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:__construct\\(\\) with return type void returns false but should not return anything\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:printHeading\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:printLine\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:printLine\\(\\) has parameter \\$nl with no type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:printRow\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:printRow\\(\\) has parameter \\$rowKey with no type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:setHeading\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:setMax\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:setMax\\(\\) has parameter \\$colKey with no type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:setMax\\(\\) has parameter \\$colVal with no type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:setMax\\(\\) has parameter \\$rowKey with no type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:setMaxHeight\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:setMaxWidth\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:showHeaders\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Parameter \\#1 \\$callback of function ob_start expects callable\\(\\)\\: mixed, null given\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, int given\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Parameter \\#3 \\$flags of function ob_start expects int, true given\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:\\$cs \\(int\\) does not accept array\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:\\$cs \\(int\\) does not accept default value of type array\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:\\$head has no type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:\\$keys \\(int\\) does not accept array\\<int, int\\|string\\>\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:\\$keys \\(int\\) does not accept default value of type array\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:\\$pcen has no type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:\\$pcol has no type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:\\$prow has no type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:\\$rows type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:\\$rs \\(int\\) does not accept array\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Cli\\\\ArrayToTextTableHelper\\:\\:\\$rs \\(int\\) does not accept default value of type array\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Util\\\\GettextTranslationHelper\\:\\:compileModuleTranslation\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Util\\\\GettextTranslationHelper\\:\\:compileTranslationTable\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Util\\\\GettextTranslationHelper\\:\\:createFileCatalog\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Util\\\\GettextTranslationHelper\\:\\:createTemplateFile\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Util\\\\GettextTranslationHelper\\:\\:fixSourceLocations\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Util\\\\GettextTranslationHelper\\:\\:getSourceFileNames\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Util\\\\GettextTranslationHelper\\:\\:updateHeader\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Util\\\\GettextTranslationHelper\\:\\:updateModuleTranslations\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Translation\\\\Util\\\\GettextTranslationHelper\\:\\:updateTranslationTable\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Parameter \\#1 \\$haystack of function strpos expects string, string\\|false given\\.$#"
+ count: 2
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Parameter \\#2 \\$offset of function substr expects int, int\\<0, max\\>\\|false given\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|false given\\.$#"
+ count: 5
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\|false given\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Util\\\\GettextTranslationHelper\\:\\:\\$catalogPath \\(string\\) does not accept string\\|false\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Util\\\\GettextTranslationHelper\\:\\:\\$sourceExtensions type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Translation\\\\Util\\\\GettextTranslationHelper\\:\\:\\$templatePath \\(string\\) does not accept string\\|false\\.$#"
+ count: 1
+ path: modules/translation/library/Translation/Util/GettextTranslationHelper.php
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..9da27bc
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,67 @@
+includes:
+ - phpstan-baseline.neon
+
+parameters:
+ level: max
+
+ checkFunctionNameCase: true
+ checkInternalClassCaseSensitivity: true
+ treatPhpDocTypesAsCertain: false
+
+ paths:
+ - application
+ - library/Icinga
+ - modules/doc/application
+ - modules/migrate/application
+ - modules/monitoring/application
+ - modules/setup/application
+ - modules/test/application
+ - modules/translation/application
+ - modules/doc/library
+ - modules/migrate/library
+ - modules/monitoring/library
+ - modules/setup/library
+ - modules/translation/library
+
+ ignoreErrors:
+ - '#Unsafe usage of new static\(\)#'
+ - '#. but return statement is missing#'
+ - '#Cannot call method importNode\(\) on DOMDocument\|null.#'
+
+ # ldap_connect() returns `LDAP\Connection` in php >= 81
+ -
+ message: '#Parameter .* of function .* expects .*, .* given#'
+ count: 7
+ path: library/Icinga/Protocol/Ldap/LdapCapabilities.php
+
+ -
+ message: '#Parameter .* of (function|callable) .* expects .*, .* given#'
+ count: 75
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: '#Method Icinga\\Protocol\\Ldap\\LdapConnection::(prepareNewConnection|ldapSearch)\(\) should return (resource|bool\|resource) but returns (LDAP\\Connection\|false|array\|LDAP\\Result\|false)#'
+ count: 3
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ -
+ message: "#Cannot access offset ('count'|'dn') on array.*#"
+ count: 2
+ path: library/Icinga/Protocol/Ldap/LdapConnection.php
+
+ - '#Call to an undefined method ipl\\Sql\\Connection::exec\(\)#'
+
+ scanDirectories:
+ - vendor
+
+ excludePaths:
+ - library/Icinga/Test
+
+ universalObjectCratesClasses:
+ - ipl\Orm\Model
+ - Icinga\Data\ConfigObject
+ - Icinga\Web\View
+ - Icinga\Module\Monitoring\Object\MonitoredObject
+ - Icinga\Module\Monitoring\DataView\DataView
+ - Icinga\Web\Session\SessionNamespace
+ - Icinga\User\Preferences
diff --git a/public/css/icinga/about.less b/public/css/icinga/about.less
new file mode 100644
index 0000000..fe3a71b
--- /dev/null
+++ b/public/css/icinga/about.less
@@ -0,0 +1,113 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+#about {
+ &.content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ section {
+ width: auto;
+
+ > * {
+ margin-bottom: 2em;
+ }
+
+ .pending-migrations {
+ .name-value-table.migrations {
+ tr:not(:first-child):not(:last-child) {
+ border-top: 1px solid @gray-lighter;
+ }
+ }
+
+ a {
+ float: right;
+ margin-top: 1em;
+ color: @icinga-blue
+ }
+ }
+ }
+
+ h2 {
+ margin: 0;
+ }
+
+ .name-value-table {
+ th {
+ width: 100%;
+ }
+
+ th,
+ td {
+ white-space: nowrap;
+
+ a {
+ color: @icinga-blue
+ }
+ }
+ }
+
+ section:not(:last-child),
+ .icinga-logo {
+ margin-bottom: 2em;
+ }
+
+ .external-links {
+ .rounded-corners();
+ border: 1px solid @gray-light;
+ display: flex;
+ padding: .5em 0;
+ overflow: hidden;
+
+ .col {
+ flex: 1 1 auto;
+ text-align: center;
+ font-size: 12/14em;
+ }
+
+ .col:not(:last-child) {
+ border-right: 1px solid @gray-light;
+ }
+
+ a {
+ display: block;
+ padding: .75em 1em;
+ margin: -7/12em 0;
+ }
+
+ a:hover {
+ text-decoration: none;
+ background: @gray-light;
+ }
+
+ i {
+ font-size: 2*14/12em;
+ opacity: .8;
+ margin-bottom: .25em;
+ display: block;
+
+ &:before {
+ margin-right: 0;
+ }
+ }
+ }
+
+ footer {
+ margin-top: auto;
+ align-self: stretch;
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+
+ a {
+ i {
+ font-size: 2em;
+ }
+
+ &:hover {
+ opacity: .6;
+ }
+ }
+ }
+}
diff --git a/public/css/icinga/animation.less b/public/css/icinga/animation.less
new file mode 100644
index 0000000..aad3ffb
--- /dev/null
+++ b/public/css/icinga/animation.less
@@ -0,0 +1,366 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+.animate(@animate) {
+ -moz-animation: @animate;
+ -o-animation: @animate;
+ -webkit-animation: @animate;
+ animation: @animate;
+}
+
+@-moz-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@-webkit-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@-o-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@-ms-keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+@keyframes spin {
+ 0% {
+ -moz-transform: rotate(0deg);
+ -o-transform: rotate(0deg);
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -moz-transform: rotate(359deg);
+ -o-transform: rotate(359deg);
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+
+@-moz-keyframes move-vertical {
+ 0% {
+ -moz-transform: translate(0, 100%);
+ -o-transform: translate(0, 100%);
+ -webkit-transform: translate(0, 100%);
+ transform: translate(0, 100%);
+ }
+
+ 17% {
+ -moz-transform: translate(0, 66%);
+ -o-transform: translate(0, 66%);
+ -webkit-transform: translate(0, 66%);
+ transform: translate(0, 66%);
+ }
+
+ 33% {
+ -moz-transform: translate(0, 33%);
+ -o-transform: translate(0, 33%);
+ -webkit-transform: translate(0, 33%);
+ transform: translate(0, 33%);
+ }
+
+ 50% {
+ -moz-transform: translate(0, 0);
+ -o-transform: translate(0, 0);
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0);
+ }
+
+ 67% {
+ -moz-transform: translate(0, -33%);
+ -o-transform: translate(0, -33%);
+ -webkit-transform: translate(0, -33%);
+ transform: translate(0, -33%);
+ }
+
+ 83% {
+ -moz-transform: translate(0, -66%);
+ -o-transform: translate(0, -66%);
+ -webkit-transform: translate(0, -66%);
+ transform: translate(0, -66%);
+ }
+
+ 100% {
+ -moz-transform: translate(0, -100%);
+ -o-transform: translate(0, -100%);
+ -webkit-transform: translate(0, -100%);
+ transform: translate(0, -100%);
+ }
+}
+@-webkit-keyframes move-vertical {
+ 0% {
+ -moz-transform: translate(0, 100%);
+ -o-transform: translate(0, 100%);
+ -webkit-transform: translate(0, 100%);
+ transform: translate(0, 100%);
+ }
+
+ 17% {
+ -moz-transform: translate(0, 66%);
+ -o-transform: translate(0, 66%);
+ -webkit-transform: translate(0, 66%);
+ transform: translate(0, 66%);
+ }
+
+ 33% {
+ -moz-transform: translate(0, 33%);
+ -o-transform: translate(0, 33%);
+ -webkit-transform: translate(0, 33%);
+ transform: translate(0, 33%);
+ }
+
+ 50% {
+ -moz-transform: translate(0, 0);
+ -o-transform: translate(0, 0);
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0);
+ }
+
+ 67% {
+ -moz-transform: translate(0, -33%);
+ -o-transform: translate(0, -33%);
+ -webkit-transform: translate(0, -33%);
+ transform: translate(0, -33%);
+ }
+
+ 83% {
+ -moz-transform: translate(0, -66%);
+ -o-transform: translate(0, -66%);
+ -webkit-transform: translate(0, -66%);
+ transform: translate(0, -66%);
+ }
+
+ 100% {
+ -moz-transform: translate(0, -100%);
+ -o-transform: translate(0, -100%);
+ -webkit-transform: translate(0, -100%);
+ transform: translate(0, -100%);
+ }
+}
+@-o-keyframes move-vertical {
+ 0% {
+ -moz-transform: translate(0, 100%);
+ -o-transform: translate(0, 100%);
+ -webkit-transform: translate(0, 100%);
+ transform: translate(0, 100%);
+ }
+
+ 17% {
+ -moz-transform: translate(0, 66%);
+ -o-transform: translate(0, 66%);
+ -webkit-transform: translate(0, 66%);
+ transform: translate(0, 66%);
+ }
+
+ 33% {
+ -moz-transform: translate(0, 33%);
+ -o-transform: translate(0, 33%);
+ -webkit-transform: translate(0, 33%);
+ transform: translate(0, 33%);
+ }
+
+ 50% {
+ -moz-transform: translate(0, 0);
+ -o-transform: translate(0, 0);
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0);
+ }
+
+ 67% {
+ -moz-transform: translate(0, -33%);
+ -o-transform: translate(0, -33%);
+ -webkit-transform: translate(0, -33%);
+ transform: translate(0, -33%);
+ }
+
+ 83% {
+ -moz-transform: translate(0, -66%);
+ -o-transform: translate(0, -66%);
+ -webkit-transform: translate(0, -66%);
+ transform: translate(0, -66%);
+ }
+
+ 100% {
+ -moz-transform: translate(0, -100%);
+ -o-transform: translate(0, -100%);
+ -webkit-transform: translate(0, -100%);
+ transform: translate(0, -100%);
+ }
+}
+@-ms-keyframes move-vertical {
+ 0% {
+ -moz-transform: translate(0, 100%);
+ -o-transform: translate(0, 100%);
+ -webkit-transform: translate(0, 100%);
+ transform: translate(0, 100%);
+ }
+
+ 17% {
+ -moz-transform: translate(0, 66%);
+ -o-transform: translate(0, 66%);
+ -webkit-transform: translate(0, 66%);
+ transform: translate(0, 66%);
+ }
+
+ 33% {
+ -moz-transform: translate(0, 33%);
+ -o-transform: translate(0, 33%);
+ -webkit-transform: translate(0, 33%);
+ transform: translate(0, 33%);
+ }
+
+ 50% {
+ -moz-transform: translate(0, 0);
+ -o-transform: translate(0, 0);
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0);
+ }
+
+ 67% {
+ -moz-transform: translate(0, -33%);
+ -o-transform: translate(0, -33%);
+ -webkit-transform: translate(0, -33%);
+ transform: translate(0, -33%);
+ }
+
+ 83% {
+ -moz-transform: translate(0, -66%);
+ -o-transform: translate(0, -66%);
+ -webkit-transform: translate(0, -66%);
+ transform: translate(0, -66%);
+ }
+
+ 100% {
+ -moz-transform: translate(0, -100%);
+ -o-transform: translate(0, -100%);
+ -webkit-transform: translate(0, -100%);
+ transform: translate(0, -100%);
+ }
+}
+@keyframes move-vertical {
+ 0% {
+ -moz-transform: translate(0, 100%);
+ -o-transform: translate(0, 100%);
+ -webkit-transform: translate(0, 100%);
+ transform: translate(0, 100%);
+ }
+
+ 17% {
+ -moz-transform: translate(0, 66%);
+ -o-transform: translate(0, 66%);
+ -webkit-transform: translate(0, 66%);
+ transform: translate(0, 66%);
+ }
+
+ 33% {
+ -moz-transform: translate(0, 33%);
+ -o-transform: translate(0, 33%);
+ -webkit-transform: translate(0, 33%);
+ transform: translate(0, 33%);
+ }
+
+ 50% {
+ -moz-transform: translate(0, 0);
+ -o-transform: translate(0, 0);
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0);
+ }
+
+ 67% {
+ -moz-transform: translate(0, -33%);
+ -o-transform: translate(0, -33%);
+ -webkit-transform: translate(0, -33%);
+ transform: translate(0, -33%);
+ }
+
+ 83% {
+ -moz-transform: translate(0, -66%);
+ -o-transform: translate(0, -66%);
+ -webkit-transform: translate(0, -66%);
+ transform: translate(0, -66%);
+ }
+
+ 100% {
+ -moz-transform: translate(0, -100%);
+ -o-transform: translate(0, -100%);
+ -webkit-transform: translate(0, -100%);
+ transform: translate(0, -100%);
+ }
+}
+
+@keyframes blink {
+ 0% {
+ opacity: 0.2;
+ }
+
+ 20% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0.2;
+ }
+}
+
+@keyframes pulse {
+ 0% {
+ opacity: .5;
+ transform: scale(1);
+ }
+
+ 50% {
+ opacity: 1;
+ transform: scale(1.2);
+ }
+
+ 100% {
+ opacity: .5;
+ transform: scale(1);
+ }
+}
diff --git a/public/css/icinga/audit.less b/public/css/icinga/audit.less
new file mode 100644
index 0000000..23ab5b9
--- /dev/null
+++ b/public/css/icinga/audit.less
@@ -0,0 +1,363 @@
+// Style
+
+.privilege-audit-role-control {
+ list-style-type: none;
+
+ li {
+ .rounded-corners(3px);
+ border: 1px solid;
+ border-color: @low-sat-blue;
+
+ &.active {
+ border-color: @icinga-blue;
+ }
+ }
+}
+
+.privilege-audit {
+ &, ul, ol {
+ list-style-type: none;
+ }
+
+ .privilege-section > summary {
+ font-weight: @font-weight-bold;
+ border-bottom: 1px solid @gray-light;
+ }
+
+ .privilege-section > summary em,
+ .previews em,
+ .privilege-label em {
+ color: @text-color-light;
+ }
+ .privilege-section > summary em {
+ font-weight: normal;
+ }
+ .privilege-label em {
+ font-style: normal;
+ }
+
+ .icon {
+ color: @gray-light;
+
+ &.granted {
+ color: @color-granted;
+ }
+
+ &.refused {
+ color: @color-refused;
+ }
+
+ &.restricted {
+ color: @color-restricted;
+ }
+ }
+
+ .privilege-list > li {
+ .spacer {
+ opacity: 0;
+ .transition(opacity .5s ease-out);
+ }
+
+ &:hover .spacer {
+ .transition(opacity .25s .25s ease-in);
+ border: 0 dashed;
+ border-color: @gray-light;
+ border-top-width: .2em;
+ opacity: 1;
+ }
+ }
+
+ .vertical-line {
+ border: 0 solid;
+ border-left-width: 2px;
+
+ &.granted {
+ border-color: @color-granted;
+ }
+
+ &.refused {
+ border-color: @color-refused;
+ }
+ }
+
+ .connector {
+ border: 0 solid;
+ border-color: @gray-lighter;
+ border-bottom-width: 2px;
+
+ &.granted {
+ border-color: @color-granted;
+ }
+
+ &.refused {
+ border-color: @color-refused;
+ }
+
+ &:first-child {
+ border-width: 0 0 2px 2px;
+ border-bottom-left-radius: .5em;
+ }
+ }
+
+ .role {
+ .rounded-corners(1em);
+ border: 2px solid;
+ border-color: @gray-lighter;
+
+ &.granted {
+ border: 2px solid;
+ border-color: @color-granted;
+ }
+
+ &.refused {
+ border: 2px solid;
+ border-color: @color-refused;
+ }
+ }
+
+ .restriction {
+ font-family: @font-family-fixed;
+ background-color: @gray-lighter;
+ }
+}
+
+// Layout
+
+.privilege-audit-role-control {
+ display: inline-flex;
+ flex-wrap: wrap;
+
+ margin: 0 0 0 1em;
+ padding: 0;
+
+ li {
+ margin-top: @vertical-padding;
+
+ &:not(:first-child) {
+ margin-left: .5em;
+ }
+ }
+}
+
+.privilege-audit {
+ &, ul, ol {
+ margin: 0;
+ padding: 0;
+ }
+
+ .flex-overflow,
+ .privilege-list > li,
+ .inheritance-paths > ol {
+ display: flex;
+ }
+
+ .privilege-list > li {
+ margin-top: 1em;
+
+ > :last-child {
+ // This aids the usage of text-overflow:ellipsis in any of the children.
+ // It seems that to get this working while none of the children has a
+ // defined width, any flex item on the way up to the clipped container
+ // also must have a overflow value of "hidden".
+ // https://codepen.io/unthinkingly/pen/XMwJLG
+ overflow: hidden;
+ }
+
+ > details:last-child {
+ // The overflow above cuts off the outline of the summary otherwise
+ margin: -4px;
+ padding: 4px;
+ }
+ }
+
+ .privilege-section {
+ &:not(.collapsed) {
+ margin-bottom: 2em;
+ }
+ }
+
+ .privilege-section > summary {
+ display: flex;
+ align-items: baseline;
+ font-size: 1.167em;
+ margin: 0.556em 0 0.333em;
+
+ > :first-child {
+ flex: 3 1 auto;
+ min-width: 20em;
+ max-width: 40em / 1.167em; // privilege label width + spacer width / summary font-size
+ }
+
+ .audit-preview {
+ flex: 1 1 auto;
+
+ .icon:before {
+ width: 1.25em;
+ font-size: 1.25em / 1.167em; // privilege state icon font-size / summary font-size
+ }
+ }
+
+ em {
+ font-size: .857em;
+ }
+ }
+
+ h4,
+ .privilege-label {
+ flex-shrink: 0;
+ width: 20em;
+ margin: 0;
+ text-align: right;
+ }
+
+ ol + h4 {
+ margin-top: 1em;
+ }
+
+ .spacer {
+ flex: 20 1 auto;
+ min-width: 10em; // TODO: Mobile?
+ max-width: 18.8em; // 20em - (margin-left + margin-right)
+ margin: .6em;
+ }
+
+ .inheritance-paths,
+ .restrictions {
+ flex: 1 1 auto;
+
+ > summary {
+ line-height: 1;
+
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ > .icon:before {
+ width: 1.25em;
+ font-size: 1.25em;
+ }
+ }
+ }
+
+ .vertical-line {
+ margin-left: ~"calc(.75em - 1px)";
+ }
+
+ .connector {
+ flex: 1 1 auto;
+ width: 2em;
+ max-width: 2em;
+ min-width: 1em;
+ margin-bottom: ~"calc(1em - 1px)";
+
+ &:first-child {
+ margin-left: ~"calc(.75em - 1px)";
+ }
+
+ &.initiator {
+ z-index: 1;
+ margin-right: ~"calc(-.25em - 2px)";
+ }
+ }
+
+ .vertical-line + .connector {
+ min-width: ~"calc(.75em - 2px)";
+ width: ~"calc(.75em - 2px)";
+ flex-grow: 0;
+
+ &.initiator {
+ width: ~"calc(1em - 1px)";
+ }
+ }
+ .connector:first-child {
+ min-width: .75em;
+ width: .75em;
+ flex-grow: 0;
+
+ &.initiator {
+ width: 1em;
+ }
+ }
+
+ .role {
+ padding: .25em .5em .25em .5em;
+ line-height: 1;
+
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ .icon:before {
+ font-size: 1.25em;
+ }
+ }
+ .inheritance-paths .role {
+ min-width: 4em;
+ margin-top: .5em;
+ padding-left: .25em;
+ }
+ .restrictions .role {
+ display: inline-block;
+ }
+
+ .previews {
+ display: flex;
+ margin-top: .25em;
+
+ em {
+ // explicit margin + ((header icon width + its margin right) * 125% font-size)
+ margin: 0 1em 0 1em + ((1.25em + .2em) * 1.25em);
+ }
+ }
+
+ .links li:not(:last-child):after {
+ content: ",";
+ }
+
+ .restrictions > ul > li {
+ margin-top: .5em;
+
+ .role {
+ margin-left: 1.25em + .2em * 1.25em; // (header icon width + its margin right) * 125% font-size
+ margin-right: 1em;
+ }
+ }
+
+ .restriction {
+ font-size: .8em;
+ padding: .335em / .8em;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ .user-select(all);
+ }
+}
+
+#layout.minimal-layout,
+#layout.poor-layout {
+ .privilege-audit {
+ .privilege-section > summary > :first-child {
+ flex-grow: 99;
+ }
+
+ h4,
+ .privilege-label {
+ width: 12em;
+ }
+
+ .spacer {
+ flex: 0;
+ min-width: 0;
+ }
+ }
+}
+
+// Integrations
+
+.privilege-audit .collapsible {
+ .collapsible-control {
+ cursor: pointer;
+ .user-select(none);
+ }
+}
diff --git a/public/css/icinga/badges.less b/public/css/icinga/badges.less
new file mode 100644
index 0000000..eae95c2
--- /dev/null
+++ b/public/css/icinga/badges.less
@@ -0,0 +1,22 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+@badge-color: @body-bg-color;
+@badge-line-height: 1.2;
+@badge-padding: 0.25em;
+
+.badge {
+ .bg-stateful();
+ .rounded-corners();
+
+ background-color: @gray;
+ color: @badge-color;
+ display: inline-block;
+ font-family: @font-family-wide;
+ font-size: @font-size-small;
+ line-height: @badge-line-height;
+ min-width: 2em;
+ padding: @badge-padding;
+ text-align: center;
+ vertical-align: middle;
+ white-space: nowrap;
+}
diff --git a/public/css/icinga/base.less b/public/css/icinga/base.less
new file mode 100644
index 0000000..539a981
--- /dev/null
+++ b/public/css/icinga/base.less
@@ -0,0 +1,344 @@
+/*! Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+// Black colors
+@black: #535353;
+@white: #fff;
+
+// Gray colors
+@gray: #c4c4c4;
+@gray-semilight: #888;
+@gray-light: #5c5c5c;
+@gray-lighter: #4b4b4b;
+@gray-lightest: #3a3a3a;
+
+@disabled-gray: #9a9a9a;
+
+// State colors
+@color-ok: #44bb77;
+@color-up: @color-ok;
+@color-warning: #ffaa44;
+@color-warning-handled: #ffcc66;
+@color-critical: #ff5566;
+@color-critical-handled: #ff99aa;
+@color-critical-accentuated: darken(@color-critical, 10%);
+@color-down: @color-critical;
+@color-down-handled: @color-critical-handled;
+@color-unknown: #aa44ff;
+@color-unknown-handled: #cc77ff;
+@color-unreachable: @color-unknown;
+@color-unreachable-handled: @color-unknown-handled;
+@color-pending: #77aaff;
+
+// Icinga colors
+@icinga-blue: #00C3ED;
+@icinga-secondary: #EF4F98;
+@icinga-secondary-dark: darken(@icinga-secondary, 10%);
+@low-sat-blue: #404d72;
+@low-sat-blue-dark: #434374;
+@icinga-blue-light: fade(@icinga-blue, 50%);
+@icinga-blue-dark: #0081a6;
+
+// Notification colors
+@color-notification-error: @color-critical;
+@color-notification-info: @color-pending;
+@color-notification-success: @color-ok;
+@color-notification-warning: @color-warning;
+
+// Background color for <body>
+@body-bg-color: #282E39;
+@body-bg-color-transparent: fade(@body-bg-color, 0);
+
+// Text colors
+@text-color: @white;
+@text-color-inverted: @body-bg-color;
+@text-color-light: fade(@text-color, 75%);
+@text-color-on-icinga-blue: @body-bg-color;
+@light-text-bg-color: fade(@gray, 5%);
+
+// Text color on <a>
+@link-color: @text-color;
+
+@tr-active-color: fade(@icinga-blue, 25);
+@tr-hover-color: fade(@icinga-blue, 5);
+
+// Menu colors
+@menu-bg-color: #06062B;
+@menu-hover-bg-color: lighten(@menu-bg-color, 5%);
+@menu-search-hover-bg-color: @menu-hover-bg-color;
+@menu-active-bg-color: #181742;
+@menu-active-hover-bg-color: lighten(@menu-active-bg-color, 5%);
+@menu-color: #DBDBDB;
+@menu-active-color: @text-color;
+@menu-highlight-color: @icinga-blue;
+@menu-highlight-hover-bg-color: @icinga-blue-dark;
+@menu-2ndlvl-color: #c4c4c4;
+@menu-2ndlvl-highlight-bg-color: fade(@icinga-blue, 10);
+@menu-2ndlvl-active-bg-color: @menu-highlight-color;
+@menu-2ndlvl-active-color: @text-color-inverted;
+@menu-2ndlvl-active-hover-bg-color: darken(@menu-2ndlvl-active-bg-color, 5%);
+@menu-2ndlvl-active-hover-color: @menu-2ndlvl-active-color;
+@menu-flyout-bg-color: @body-bg-color;
+@menu-flyout-color: @text-color;
+@tab-hover-bg-color: fade(@body-bg-color, 50%);
+
+// Form colors
+@form-info-bg-color: fade(@color-ok, 20%);
+@form-error-bg-color: fade(@color-critical, 30%);
+@form-warning-bg-color: fade(@color-warning, 40%);
+@login-box-background: fade(#0B0B2F, 30%);
+
+// Other colors
+@color-granted: #59cd59;
+@color-refused: #ee7373;
+@color-restricted: #dede7d;
+
+// Font families
+@font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+@font-family-fixed: "Liberation Mono", "Lucida Console", Courier, monospace;
+@font-family-wide: Tahoma, Verdana, sans-serif;
+
+// Font sizes
+@font-size: 0.750em; // 12px
+@font-size-small: 11/12em; // 11px
+@font-size-dashboard: 3.5em; // 56px
+@font-size-dashboard-small: 1.1em; // 17px
+@font-weight-bold: 600;
+
+// Set line-height w/o unit so that the line-height is dynamically calculated as font-size * line-height
+@line-height: 1.5;
+
+@table-column-padding: 0.333em; // 4px
+
+@vertical-padding: 0.5em; // 6px
+@horizontal-padding: 1em; // 12px
+
+@light-mode: {
+ @light-body-bg-color: #F5F9FA;
+
+ @iplWebLightRules();
+
+ :root {
+ --body-bg-color: @light-body-bg-color;
+ --body-bg-color-transparent: fade(@light-body-bg-color, 0);
+ --badge-color: #F5F9FA;
+ --text-color-inverted: #F5F9FA;
+ --text-color-on-icinga-blue: #F5F9FA;
+ --menu-flyout-bg-color: #F5F9FA;
+ --tab-hover-bg-color: fade(#F5F9FA, 50%);
+
+ --menu-color: #535353;
+ --menu-bg-color: #DEECF1;
+ --menu-hover-bg-color: darken(#DEECF1, 10%);
+ --menu-search-hover-bg-color: darken(#DEECF1, 10%);
+ --menu-active-bg-color: #EDF7FC;
+ --menu-active-hover-bg-color: darken(#EDF7FC, 20%);
+ --menu-highlight-hover-bg-color: darken(#EDF7FC, 20%);
+ --menu-2ndlvl-color: #676767;
+
+ --text-color: #535353;
+ --text-color-light: fade(#535353, 75%);
+ --light-text-bg-color: fade(#7F7F7F, 5%);
+ --link-color: #535353;
+ --menu-active-color: #535353;
+ --menu-flyout-color: #535353;
+
+ --low-sat-blue: #DEECF1;
+ --low-sat-blue-dark: #c0cccd;
+
+ --gray: #819398;
+ --gray-semilight: #94a5a6;
+ --gray-light: #d0d3da;
+ --gray-lighter: #e8ecef;
+ --gray-lightest: #F7F7F7;
+
+ // ipl-web overrides
+ --base-gray: var(--gray);
+ --base-gray-light: var(--gray-light);
+ --base-gray-lighter: var(--gray-lighter);
+ --base-gray-semilight: var(--gray-semilight);
+
+ --default-text-color: var(--text-color);
+ --default-text-color-light: var(--text-color-light);
+ --default-text-color-inverted: var(--text-color-inverted);
+ --default-input-bg: var(--low-sat-blue);
+
+ --search-logical-operator-bg: fade(#819398, 50%); // --gray
+ }
+};
+
+// ipl-web overrides
+@default-bg: @body-bg-color;
+
+@base-gray: @gray;
+@base-gray-light: @gray-light;
+@base-gray-lighter: @gray-lighter;
+@base-gray-semilight: @gray-semilight;
+@base-disabled: @disabled-gray;
+
+@base-primary-color: @icinga-blue;
+@base-primary-bg: @icinga-blue;
+@base-primary-dark: @icinga-blue-dark;
+@base-primary-light: @icinga-blue-light;
+
+@default-text-color: @text-color;
+@default-text-color-light: @text-color-light;
+@default-input-bg: @low-sat-blue;
+
+@state-ok: @color-ok;
+@state-warning: @color-warning;
+@state-critical: @color-critical;
+@state-pending: @color-pending;
+@state-unknown: @color-unknown;
+
+// Make padding not affect the final computed width of an element
+html {
+ box-sizing: border-box;
+}
+details > * {
+ // children somehow default to content-box no matter the inheritance
+ box-sizing: border-box;
+}
+*,
+*:before,
+*:after {
+ -webkit-box-sizing: inherit;
+ -moz-box-sizing: inherit;
+ box-sizing: inherit;
+}
+
+a {
+ // Reset defaults
+ color: inherit;
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+:focus {
+ outline: 3px solid fade(@icinga-blue, 50%);
+ outline-offset: 1px;
+}
+
+// Default margin for block text
+blockquote, p, pre {
+ margin: 0 0 1em 0;
+}
+
+blockquote {
+ border-left: 5px solid @gray-lighter;
+ padding: 0.667em 0.333em;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-weight: @font-weight-bold;
+ margin: 0.556em 0 0.333em;
+}
+
+h1 {
+ border-bottom: 1px solid @gray-lighter;
+ font-size: 1.333em;
+}
+
+h2 {
+ font-size: 1.333em;
+}
+
+h3 {
+ font-size: 1.167em;
+}
+
+h4 {
+ font-size: 1em;
+}
+
+h5 {
+ font-size: @font-size-small;
+}
+
+h6 {
+ font-size: @font-size-small;
+ font-weight: normal;
+}
+
+pre {
+ .rounded-corners(.25em);
+ background-color: @gray-lighter;
+ font-family: @font-family-fixed;
+ font-size: @font-size-small;
+ padding: @vertical-padding @horizontal-padding;
+ white-space: pre-wrap;
+}
+
+td, th {
+ padding: @table-column-padding;
+}
+
+[class^="icon-"], [class*=" icon-"] {
+ // Smooth icons; ifont claims to have it, but it does not work in :before
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ &:before {
+ margin-left: 0;
+ }
+}
+
+// Styles for when the page is loading. JS will remove this class once the document is ready
+.loading * {
+ // Disable all transition on page load
+ -webkit-transition: none !important;
+ -moz-transition: none !important;
+ -o-transition: none !important;
+ transition: none !important;
+}
+
+.container {
+ &:before,
+ > .content:before {
+ content: "";
+ display: block;
+
+ background: url(../img/icinga-loader.gif) no-repeat center center;
+ background-color: @body-bg-color;
+ background-size: 4em 4em;
+
+ opacity: 0;
+ z-index: -1;
+ pointer-events: none;
+ .transition(none);
+ }
+
+ &.impact,
+ > .content.impact {
+ overflow: hidden;
+ position: relative;
+
+ &:before {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+
+ opacity: .7;
+ z-index: 1000;
+ pointer-events: all;
+ .transition(opacity 1s 2s linear);
+ }
+ }
+
+ &.impact:before {
+ top: 2.5em;
+ }
+}
+
+@light-mode: {
+ .container {
+ &:before,
+ > .content:before {
+ background-image: url(../img/icinga-loader-light.gif)
+ }
+ }
+};
diff --git a/public/css/icinga/compat.less b/public/css/icinga/compat.less
new file mode 100644
index 0000000..d57a189
--- /dev/null
+++ b/public/css/icinga/compat.less
@@ -0,0 +1,35 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+@colorMainLayout: @icinga-blue;
+@colorMainBackground: @body-bg-color;
+@colorMainForeground: @text-color;
+@colorMainLink: @text-color;
+@colorSecondary: @gray-lightest;
+@colorGray: @gray-lightest;
+@colorLinkDefault: @text-color;
+@colorTextDefault: @text-color;
+@colorTextDarkDefault: @text-color;
+@colorOk: @color-ok;
+@colorWarning: #ffaa44;
+@colorWarningHandled: #ffcc66;
+@colorCritical: #ff5566;
+@colorCriticalHandled: #ff99aa;
+@colorUnknown: #aa44ff;
+@colorUnknownHandled: #cc77ff;
+@colorUnreachable: #aa44ff;
+@colorUnreachableHandled: #cc77ff;
+@colorPending: #77aaff;
+@colorInvalid: #999;
+@colorFormNotificationInfo: #77aaff;
+@colorFormNotificationWarning: #ffaa44;
+@colorFormNotificationError: #ff5566;
+@colorPetrol: @icinga-blue;
+@menu-2ndlvl-highlight-color: @menu-2ndlvl-active-color;
+
+table.action {
+ .common-table();
+}
+
+table.avp {
+ .name-value-table();
+}
diff --git a/public/css/icinga/configmenu.less b/public/css/icinga/configmenu.less
new file mode 100644
index 0000000..05e50e8
--- /dev/null
+++ b/public/css/icinga/configmenu.less
@@ -0,0 +1,303 @@
+#menu {
+ margin-bottom: 3em;
+}
+
+.sidebar-collapsed #menu {
+ margin-bottom: 8em;
+}
+
+#menu .config-menu {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background-color: @menu-bg-color;
+ margin-top: auto;
+
+ > ul {
+ display: flex;
+ flex-wrap: nowrap;
+ padding: 0;
+
+ > li {
+ > a {
+ padding: 0.5em 0.5em 0.5em 0.75em;
+ line-height: 2.167em;
+ white-space: nowrap;
+ text-decoration: none;
+
+ }
+
+ &:hover .nav-level-1 {
+ display: block;
+ }
+ }
+
+ li.active a:after {
+ display: none;
+ }
+
+ .user-nav-item {
+ width: 100%;
+ overflow: hidden; // necessary for .text-ellipsis of <a>
+
+ > a {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &:not(.active):hover a,
+ &:not(.active) a:focus {
+ background: @menu-hover-bg-color;
+ }
+ }
+
+ .config-nav-item {
+ line-height: 2;
+ display: flex;
+ align-items: center;
+ position: relative;
+
+ > button {
+ background: none;
+ border: none;
+ display: block;
+ .rounded-corners();
+
+ > .state-badge {
+ position: absolute;
+ pointer-events: none;
+ }
+
+ .icon {
+ opacity: .8;
+ font-size: 1.25em;
+
+ &:before {
+ margin-right: 0;
+ }
+ }
+ }
+
+ &:hover > button {
+ background: fade(@menu-hover-bg-color, 25);
+
+ > .state-badge {
+ display: none;
+ }
+ }
+
+ button:focus {
+ background: fade(@menu-hover-bg-color, 25);
+ }
+
+ &.active > button {
+ color: @text-color-inverted;
+ background: @icinga-blue;
+ }
+ }
+
+ .state-badge {
+ line-height: 1.2;
+ padding: .25em;
+ font-family: @font-family-wide;
+ }
+ }
+
+ .nav-level-1 li {
+ &.badge-nav-item > a {
+ display: flex;
+ align-items: baseline;
+ width: 100%;
+
+ .state-badge {
+ margin-left: auto;
+ }
+ }
+ }
+
+ .nav-item-logout {
+ color: @color-critical;
+ border-top: 1px solid @gray-lighter;
+ }
+
+ .user-ball {
+ .ball();
+ .ball-size-l();
+ .ball-solid(@icinga-blue);
+
+ // icingadb-web/public/css/common.less: .user-ball
+ font-weight: bold;
+ text-transform: uppercase;
+
+ // compensate border vertically and add space to the right;
+ margin: -1px .2em -2px 0;
+ border: 1px solid @text-color-inverted;
+ font-style: normal;
+ line-height: 1.2;
+ }
+}
+
+#layout:not(.sidebar-collapsed) #menu .config-menu {
+ .user-nav-item {
+ > a {
+ padding-right: 4.75em;
+ }
+
+ &.active.selected + .config-nav-item {
+ > button {
+ color: @text-color-inverted;
+ }
+ }
+ }
+
+ .config-nav-item {
+ position: absolute;
+ right: 2.5em;
+ bottom: 0;
+ top: 0;
+
+ .state-badge {
+ left: -1em;
+ top: 0;
+ }
+ }
+
+ .flyout {
+ bottom: 100%;
+ right: -2em;
+ width: 15em;
+ }
+}
+
+.sidebar-collapsed #menu .config-menu {
+ ul {
+ flex-direction: column;
+
+ .user-ball {
+ margin-left: .25em * 1.5/2;
+ margin-right: .5em + .25em * 1.5/2;
+ width: 2em * 1.5/2 ;
+ height: 2em * 1.5/2;
+ font-size: 2/1.5em;
+ line-height: 1;
+ }
+
+ .config-nav-item {
+ padding-right: 0;
+ margin-bottom: 3em;
+
+ .icon {
+ font-size: 1.5em;
+ }
+
+ button {
+ position: relative;
+ width: 3em;
+ margin: .125em .5em;
+ padding: .5em .75em;
+
+ .state-badge {
+ right: -.25em;
+ bottom: -.25em;
+ font-size: .75em;
+
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 4em;
+ }
+ }
+ }
+ }
+
+ .flyout {
+ bottom: 0;
+ left: 100%;
+ width: 14em;
+
+ &:before {
+ left: -.6em;
+ bottom: 1em;
+ transform: rotate(135deg);
+ }
+ }
+}
+
+.flyout {
+ display: none;
+ position: absolute;
+ border: 1px solid @gray-lighter;
+ background: @body-bg-color;
+ box-shadow: 0 0 1em 0 rgba(0,0,0,.25);
+ z-index: 1;
+ .rounded-corners();
+
+ a {
+ font-size: 11/12em;
+ padding: 0.364em 0.545em 0.364em 2em;
+ line-height: 2;
+
+ &:hover {
+ text-decoration: none;
+ background: @menu-2ndlvl-highlight-bg-color;
+ }
+ }
+
+ h3 {
+ font-size: 10/12em;
+ color: @text-color-light;
+ letter-spacing: .1px;
+ padding: 0.364em 0.545em 0.364em 0.545em;
+ margin: 0;
+ }
+
+ .flyout-content {
+ overflow: auto;
+ // Partially escape to have ems calculated
+ max-height: calc(~"100vh - " 50/12em);
+ padding: .5em 0;
+ position: relative;
+ }
+
+ // Caret
+ &:before {
+ content: "";
+ display: block;
+ position: absolute;
+ transform: rotate(45deg);
+ background: @body-bg-color;
+ border-bottom: 1px solid @gray-lighter;
+ border-right: 1px solid @gray-lighter;
+ height: 1.1em;
+ width: 1.1em;
+ bottom: -.6em;
+ right: 2.5em;
+ }
+}
+
+// Prevent flyout to vanish on autorefresh
+#layout.config-flyout-open .config-nav-item {
+ .flyout {
+ display: block;
+ }
+
+ > button > .state-badge {
+ display: none;
+ }
+}
+
+#layout.minimal-layout .config-menu {
+ display: none;
+}
+
+#layout.minimal-layout #menu {
+ margin-bottom: 0;
+}
+
+#layout:not(.minimal-layout) #menu .primary-nav {
+ .user-nav-item,
+ .configuration-nav-item,
+ .system-nav-item {
+ display: none;
+ }
+}
diff --git a/public/css/icinga/controls.less b/public/css/icinga/controls.less
new file mode 100644
index 0000000..01cf152
--- /dev/null
+++ b/public/css/icinga/controls.less
@@ -0,0 +1,281 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+// TODO(el): Rename .filter to .filter-control
+
+// Hide auto sumbit info in controls
+.controls .autosubmit-info {
+ display: none;
+}
+
+
+// Backend selection control in user and group list views
+.backend-selection {
+ float: left;
+
+ .control-label-group, select {
+ display: inline-block;
+ }
+
+ select {
+ margin-left: .5em;
+ }
+}
+
+.controls input.search,
+input.search {
+ .transition(border 0.3s ease);
+ .transition(background-image 0.2s ease);
+
+ background-image: url(../img/icons/search_white.png);
+ background-repeat: no-repeat;
+ background-size: 1em 1em;
+ background-position: .25em center;
+ outline: none;
+ padding-left: 1.5em;
+ width: 20em;
+
+ &:focus {
+ background-image: url(../img/icons/search_icinga_blue.png);
+ }
+
+ &:focus:not([readonly]) {
+ border-color: @icinga-blue;
+ }
+}
+
+@light-mode: {
+ #menu input.search,
+ .controls input.search,
+ input.search {
+ background-image: url(../img/icons/search.png);
+ }
+};
+
+.backend-selection,
+.pagination-control,
+.selection-info,
+.sort-controls-container {
+ margin-bottom: 0.5em;
+}
+
+.filter {
+ // Display filter control on a new line
+ clear: both;
+ margin: .5em 0;
+
+ > a {
+ color: @icinga-blue;
+ padding: .5em;
+ line-height: 1;
+ }
+
+ > a > i {
+ text-align: center;
+ &:before {
+ margin-right: 0;
+ }
+ }
+
+ .form input {
+ padding: @vertical-padding @vertical-padding;
+ }
+}
+
+.controls .filter {
+ form .search {
+ height: 2em;
+ }
+}
+
+.controls .button-link {
+ height: 2em;
+}
+
+.limiter-control > select {
+ margin-left: .5em;
+}
+
+.pagination-control {
+ // Display the pagination-control on a new line
+ clear: both;
+ float: left;
+
+ li {
+ line-height: 1;
+
+ &.active {
+ border-bottom: 2px solid @icinga-blue;
+
+ > a,
+ > a:hover {
+ color: @icinga-blue;
+ /* Compensate border-bottom: 2px */
+ margin-bottom: -2px;
+ }
+
+ > a:hover {
+ background: none;
+ cursor: default;
+ text-decoration: none;
+ }
+ }
+
+ &.disabled {
+ color: @disabled-gray;
+ cursor: no-drop;
+ }
+
+ > a,
+ > span {
+ padding: 0.5em;
+ }
+ > a:hover {
+ background-color: @gray-lighter;
+ text-decoration: none;
+ }
+ }
+
+ .previous-page {
+ padding-left: 0;
+ }
+
+ .next-page {
+ padding-right: 0;
+ }
+}
+
+// Multi-selection info
+.selection-info {
+ float: right;
+ font-size: @font-size-small;
+
+ &:hover {
+ cursor: help;
+ }
+}
+
+.sort-control {
+ label {
+ width: auto;
+ margin-right: 0.5em;
+ }
+
+ select[name=sort] {
+ width: 12em;
+ margin-left: 0;
+ }
+
+ select[name=dir] {
+ width: 8em;
+ margin-left: 0;
+ }
+}
+
+.sort-controls-container {
+ clear: right;
+ float: right;
+ display: flex;
+
+ > *:not(:last-child) {
+ margin-right: .5em;
+ }
+}
+
+.sort-direction-control {
+ margin-left: 0.25em;
+ width: 1em;
+
+ .spinner {
+ line-height: 1;
+ }
+}
+
+.controls .icinga-controls {
+ .control-label-group {
+ margin-top: 0;
+ margin-bottom: 0;
+ line-height: 1.5em;
+ padding-top: 0.25em;
+ padding-bottom: 0.25em;
+ }
+
+ input,
+ select {
+ max-width: 16em;
+ }
+
+ select {
+ padding-right: 1.526em;
+ margin-top: 0;
+ margin-bottom: 0;
+ /* compensate inconsistent select height calculations */
+ line-height: 1;
+ max-height: 2em;
+ }
+}
+
+// Datetime picker colors
+
+// The less variables are essentially the official dark theme for the flatpickr
+@fp-calendarBackground: #3f4458;
+@fp-calendarBorderColor: darken(#3f4458, 50%);
+
+@fp-monthForeground: #fff;
+@fp-monthBackground: #3f4458;
+
+@fp-weekdaysBackground: transparent;
+@fp-weekdaysForeground: #fff;
+
+@fp-dayForeground: fadeout(white, 5%);
+@fp-dayHoverBackground: lighten(@fp-calendarBackground, 25%);
+
+@fp-todayColor: #eee;
+@fp-today_fg_color: #3f4458;
+
+@fp-selectedDayBackground: #80CBC4;
+
+.icinga-datetime-picker .flatpickr-day.today {
+ &:hover,
+ &:focus {
+ color: @fp-today_fg_color;
+ }
+}
+
+@light-mode: {
+ :root {
+ // These are actually the default colors for the flatpickr
+
+ @fp-dayForeground: #393939;
+ @fp-dayHoverBackground: #e6e6e6;
+
+ --fp-calendarBackground: #ffffff;
+ --fp-calendarBorderColor: @fp-dayHoverBackground;
+
+ --fp-arrowColor: fadeout(@fp-dayForeground, 40%);
+ --fp-arrow_hover_color: #f64747;
+
+ --fp-monthForeground: fadeout(black, 10%);
+ --fp-monthBackground: transparent;
+
+ --fp-weekdaysBackground: transparent;
+ --fp-weekdaysForeground: fadeout(black, 46%);
+ --fp-weekNumberForeground: fadeout(@fp-dayForeground, 70%);
+
+ --fp-dayForeground: @fp-dayForeground;
+ --fp-dayHoverBackground: @fp-dayHoverBackground;
+ --fp-disabledDayForeground: fadeout(@fp-dayForeground, 90%);
+ --fp-outsideRangeDayForeground: fadeout(@fp-dayForeground, 70%);
+ --fp-selectedDayBackground: #569FF7;
+ --fp-todayColor: #959ea9;
+
+ --fp-timeHoverBg: lighten(@fp-dayHoverBackground, 3);
+
+ --fp-hoverInvertedBg: fadeout(black, 95%);
+
+ --fp-numChooserSvgFillColor: fadeout(fadeout(black, 10%), 50%);
+ --fp-hoverNumChooserBg: fadeout(black, 90%);
+ --fp-numChooserBorderColor: fadeout(@fp-dayForeground, 85%);
+ }
+};
+
+// Datetime picker colors (end)
diff --git a/public/css/icinga/dev.less b/public/css/icinga/dev.less
new file mode 100644
index 0000000..a1e34be
--- /dev/null
+++ b/public/css/icinga/dev.less
@@ -0,0 +1,10 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+#fontsize-calc {
+ display: none;
+ width: 1000em;
+ height: 1em;
+ font-size: 1em;
+ position: absolute;
+ top: -2em;
+}
diff --git a/public/css/icinga/forms.less b/public/css/icinga/forms.less
new file mode 100644
index 0000000..00389ea
--- /dev/null
+++ b/public/css/icinga/forms.less
@@ -0,0 +1,596 @@
+/*! Icinga Web 2 | (c) 2019 Icinga Development Team | GPLv2+ */
+
+/**
+ Rules found in here are structured with two layers:
+
+ 1) form.icinga-form, that's what defines the general structure of our single/individual forms. It's not
+ supposed to be used for any other forms that are not the only content on the page (e.g. inline-forms)
+ 2) .icinga-controls, this defines the design of our controls. Any input that's part of a container with
+ this class gets our design applied
+ */
+
+// General form layout
+
+form.icinga-form {
+ max-width: 70em;
+ width: 80%;
+
+ .control-group {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+
+ // Negative margin-right because every child gets 1em right but we can't exclude
+ // the last element as it's impossible to identify the last *visible* element
+ margin: 1em -1em 1em 0;
+
+ > fieldset {
+ > .control-group:first-of-type {
+ margin-top: 0;
+ }
+
+ > .control-group:last-of-type {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ .control-group > :not(.control-label-group) {
+ margin-right: 1em;
+ }
+
+ .form-controls {
+ display: flex;
+ justify-content: flex-end;
+ }
+
+ &.inline {
+ width: auto;
+
+ .control-group {
+ margin: 0;
+ align-items: center;
+
+ > :not(.control-label-group) {
+ margin-right: .5em;
+ }
+
+ &:last-child {
+ margin-right: -.5em;
+ }
+ }
+ }
+}
+
+form.inline {
+ display: inline-block;
+
+ fieldset {
+ display: inline-block;
+ vertical-align: top;
+ border: none;
+ }
+}
+
+// Minimal form layout
+
+#layout.minimal-layout,
+#layout.twocols:not(.wide-layout) {
+ form.icinga-form {
+ &:not(.inline) {
+ width: 100%;
+ }
+
+ .control-label-group {
+ text-align: left;
+ padding-bottom: 0;
+ padding-left: 0;
+ margin-bottom: 0;
+ }
+
+ .toggle-switch ~ .control-info:before {
+ margin-left: 0;
+ }
+
+ .errors {
+ margin: 0;
+ }
+ }
+}
+
+#layout.minimal-layout .icinga-form {
+ .form-controls {
+ input[type="submit"] {
+ width: 100%;
+
+ &:not(:last-child) {
+ margin-bottom: 1em;
+ }
+ }
+ }
+}
+
+// Label styles
+
+form.icinga-form .control-label-group {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ line-height: 1.1em;
+ padding: .5625em .5625em .5625em 0;
+ max-height: 2.5em;
+ text-align: right;
+ width: 14em;
+}
+
+form.icinga-form.inline .control-label-group {
+ width: auto;
+ line-height: 0.857em;
+}
+
+.icinga-controls fieldset {
+ margin: 0;
+ padding: 0;
+ border: none;
+
+ legend {
+ font-weight: bold;
+ font-size: 4/3em;
+ margin: 0.556em 0 0.333em;
+ }
+}
+
+.icinga-controls .control-info {
+ line-height: 2.25em;
+ opacity: .6;
+
+ &:before {
+ margin-right: 0;
+ }
+
+ &:hover {
+ opacity: 1;
+ }
+}
+
+form.icinga-form .control-group .control-info {
+ margin-left: -.5em;
+}
+form.icinga-form .control-group .toggle-switch ~ .control-info {
+ margin-left: 0;
+}
+
+// General input styles
+
+.icinga-controls {
+ input[type="text"],
+ input[type="password"],
+ input[type="number"],
+ input[type="datetime-local"],
+ input[type="date"],
+ input[type="time"],
+ input[type="file"],
+ textarea,
+ select {
+ background-color: @low-sat-blue;
+ }
+}
+
+form.icinga-form {
+ input[type="text"],
+ input[type="password"],
+ input[type="number"],
+ input[type="datetime-local"],
+ input[type="date"],
+ input[type="time"],
+ input[type="file"],
+ .control-group > fieldset,
+ textarea,
+ select {
+ flex: 1 1 auto;
+ width: 0;
+ }
+}
+
+.icinga-controls {
+ input:not([type="radio"]),
+ .toggle-switch,
+ button,
+ select,
+ textarea {
+ border: none;
+ .rounded-corners(.25em);
+ .appearance(none);
+ }
+}
+
+.icinga-controls {
+ input:not([type="checkbox"]),
+ .toggle-switch,
+ select,
+ textarea,
+ button,
+ .toggle-switch {
+ font-size: inherit;
+ padding: @vertical-padding;
+ }
+
+ input[type="radio"] {
+ margin-right: .25em;
+ }
+}
+
+form.icinga-form {
+ .control-group .toggle-switch,
+ .form-controls .toggle-switch {
+ margin-top: 0.5em*0.666666667;
+ margin-bottom: 0.5em*0.666666667;
+ }
+}
+
+form.icinga-form select:not([multiple]) {
+ // Compensate inconsistent select height calculations
+ line-height: 1em;
+ height: 2.25em;
+}
+
+// Remove native dropdown arrow in IE10+
+.icinga-controls select::-ms-expand {
+ display: none;
+ opacity: 0;
+}
+
+.icinga-controls select:not([multiple]) {
+ padding-right: 1.5625em;
+ background-image: url(../img/select-icon.svg);
+ background-repeat: no-repeat;
+ background-position: right center;
+ background-size: contain;
+}
+
+form.icinga-form select {
+ width: 0; // Prevent selects with long option values from exceeding the container
+}
+
+form.inline select {
+ width: auto;
+}
+
+
+// Specific input styles
+
+.link-button {
+ .action-link();
+ // Reset defaults
+ background: none;
+ border: none;
+ display: inline-block;
+ padding: 0;
+
+ text-align: left;
+}
+
+.icinga-controls {
+ input ~ .spinner,
+ button ~ .spinner,
+ select ~ .spinner,
+ textarea ~ .spinner {
+ line-height: normal;
+ padding: .5em 0;
+
+ &:before {
+ vertical-align: middle;
+ margin-left: .5em;
+ opacity: 0.4;
+ }
+ }
+}
+
+/* selects get their spinner specifically placed */
+.icinga-controls select:not([multiple]) + .spinner {
+ height: 2.25em;
+ margin: 0;
+
+ &:before {
+ margin-left: -3.75em;
+ }
+}
+
+form.icinga-form .form-controls {
+ .spinner {
+ order: -1;
+ }
+
+ .btn-primary {
+ order: 99;
+ }
+}
+
+// Button styles
+
+.icinga-controls {
+ button:not([type]),
+ button[type="submit"],
+ input[type="submit"],
+ input[type="submit"].btn-confirm {
+ .button();
+ }
+
+ input[type="submit"].btn-remove {
+ .button(@body-bg-color, @color-critical, @color-critical-accentuated);
+ }
+
+ input[type="submit"].btn-cancel {
+ .button(@body-bg-color, @gray, @black);
+ }
+
+ button.noscript-apply {
+ color: @gray;
+ background-color: @gray-lightest;
+ border-color: @gray;
+ border-width: 1px;
+ }
+
+ button[type="button"] {
+ background-color: @low-sat-blue;
+ }
+}
+
+form.icinga-form {
+ button[type="button"] {
+ line-height: normal;
+ }
+}
+
+form.inline {
+ :not([type="hidden"]) {
+ & ~ button:not([type]),
+ & ~ button[type="submit"],
+ & ~ input[type="submit"],
+ & ~ input[type="submit"].btn-confirm {
+ margin-left: @horizontal-padding;
+ }
+ }
+
+ button.noscript-apply {
+ margin-left: .5em;
+ padding: .1em;
+ }
+}
+
+// Toggle styles
+
+.icinga-controls .toggle-switch {
+ cursor: pointer;
+ position: relative;
+ display: inline-block;
+ height: 1.5em;
+ width: 2.625em;
+}
+
+.icinga-controls .toggle-switch .toggle-slider {
+ position: absolute;
+ left: 0;
+ top: 0;
+
+ display: inline-block;
+ background: @low-sat-blue;
+ border: 1px solid;
+ border-color: @low-sat-blue;
+ box-sizing: content-box;
+ border-radius: 1em;
+ height: 4/3em;
+ width: 8/3em;
+ vertical-align: middle;
+}
+
+.icinga-controls .toggle-switch .toggle-slider:before {
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ background: @text-color-inverted;
+ border-radius: 1em;
+ border: 1px solid;
+ border-color: @low-sat-blue;
+ box-sizing: border-box;
+ content: "";
+ display: block;
+ height: 4/3em;
+ margin-left: 0;
+ width: 4/3em;
+
+ @transition: left .2s ease, margin .2s ease;
+ -webkit-transition: @transition;
+ -moz-transition: @transition;
+ -o-transition: @transition;
+ transition: @transition;
+}
+
+.icinga-controls input[type="checkbox"]:checked + .toggle-switch .toggle-slider {
+ background-color: @icinga-blue;
+ border: 1px solid;
+ border-color: @icinga-blue;
+}
+
+.icinga-controls input[type="checkbox"]:focus + .toggle-switch .toggle-slider {
+ box-shadow: 0 0 0 2px @body-bg-color, 0 0 0 4px @icinga-blue-light;
+}
+
+.icinga-controls input[type="checkbox"]:checked + .toggle-switch .toggle-slider:before {
+ border: 1px solid;
+ border-color: @icinga-blue;
+ left: 100%;
+ margin-left: -4/3em;
+}
+
+// Disabled inputs
+
+.icinga-controls .toggle-switch.disabled {
+ cursor: default;
+
+ & > .toggle-slider {
+ background-color: @gray-light;
+ border-color: @gray-light;
+
+ &:before {
+ background-color: @gray-lighter;
+ border-color: @gray-light;
+ }
+ }
+}
+
+form.icinga-form .control-group.disabled .control-label-group {
+ color: @disabled-gray;
+}
+
+.icinga-controls {
+ input[disabled],
+ select[disabled] {
+ background-color: @gray-lighter;
+ border-color: transparent;
+ }
+}
+
+// Errors and additional information
+
+form.icinga-form {
+ .form-notifications,
+ .form-description {
+ border-radius: .25em;
+ display: flex;
+ list-style: none;
+ align-items: center;
+ margin: 0 0 1em 0;
+ padding: .25em .5em;
+
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 0 .5em;
+ }
+
+ li {
+ list-style: none;
+ }
+
+ & .form-notification-icon,
+ & .form-description-icon {
+ font-size: 2em;
+ margin-left: .25em;
+ opacity: .4;
+ align-self: flex-start;
+ }
+ }
+
+ .form-notifications {
+ &.info {
+ background-color: @form-info-bg-color;
+ }
+
+ &.error {
+ background-color: @form-error-bg-color;
+ }
+
+ &.warning {
+ background-color: @form-warning-bg-color;
+ }
+ }
+
+ .form-description {
+ background-color: @light-text-bg-color;
+ }
+
+ .errors {
+ list-style: none;
+ display: block;
+ width: 100%;
+ margin: 0 0 0 15em;
+ padding: 0;
+
+ & > li {
+ margin: 0.5em;
+ color: #f56;
+ }
+ }
+}
+
+form.icinga-form .form-info {
+ color: @text-color-light;
+ font-size: @font-size-small;
+ list-style: none;
+ padding-left: 0;
+}
+
+// Placeholder styles
+
+.icinga-controls {
+ input::placeholder {
+ color: @disabled-gray;
+ font-style: italic;
+ opacity: 1;
+ }
+
+ input:-ms-input-placeholder {
+ color: @disabled-gray;
+ font-style: italic;
+ opacity: 1;
+ }
+}
+
+// Specific form styles
+
+.search.inline {
+ display: inline-block;
+}
+
+/* Flyover form styles */
+
+.flyover-content form:not(.inline):not([role="search"]) {
+ width: auto;
+}
+
+.flyover-content .control-label-group {
+ text-align: left;
+}
+
+.theme-mode-input {
+ display: none;
+
+ &:checked + img {
+ border-color: @icinga-blue;
+ border-radius: .25em;
+ }
+
+ & + img {
+ margin: 0 auto;
+ border: .25em solid transparent;
+ display: block;
+ width: 6em;
+ overflow: hidden;
+ box-shadow: 0 0 .25em 0 rgba(0,0,0,.4);
+ }
+
+ &[disabled] ~ img,
+ &[disabled] ~ span {
+ opacity: .25;
+ }
+
+ & ~ span {
+ display: block;
+ text-align: center;
+ }
+}
+
+#layout.minimal-layout .icinga-form {
+ .theme-mode {
+ .control-label-group {
+ width: 100%;
+ margin-bottom: .5em;
+ }
+
+ label:first-of-type {
+ margin-left: auto;
+ }
+ }
+}
diff --git a/public/css/icinga/grid.less b/public/css/icinga/grid.less
new file mode 100644
index 0000000..e061bc8
--- /dev/null
+++ b/public/css/icinga/grid.less
@@ -0,0 +1,47 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+.grid {
+ .clearfix();
+}
+
+[class^="col-"],
+[class*=" col-"] {
+ float: left;
+ // Fix that empty columns don't consume their width
+ min-height: 1px;
+}
+
+.controls {
+ [class^="col-"],
+ [class*=" col-"] {
+ padding: @vertical-padding / 2 0;
+ }
+}
+
+.col-1-2 {
+ width: 50%;
+}
+
+.col-1-3 {
+ width: 33.33%;
+}
+
+.col-2-3 {
+ width: 66.66%;
+}
+
+.col-3-3 {
+ width: 100%;
+}
+
+// TODO(el): Set proper breakpoints
+#layout.twocols,
+#layout.compact-layout,
+#layout.minimal-layout,
+#layout.poor-layout {
+ [class^="col-"],
+ [class*=" col-"] {
+ float: none;
+ width: 100%;
+ }
+}
diff --git a/public/css/icinga/health.less b/public/css/icinga/health.less
new file mode 100644
index 0000000..50cc11a
--- /dev/null
+++ b/public/css/icinga/health.less
@@ -0,0 +1,69 @@
+// Style
+
+.app-health {
+ header {
+ color: @text-color-light;
+
+ span {
+ color: @text-color;
+ }
+ }
+
+ span {
+ &.state-ok {
+ background-color: @color-ok;
+ }
+
+ &.state-warning {
+ background-color: @color-warning;
+ }
+
+ &.state-critical {
+ background-color: @color-critical;
+ }
+
+ &.state-unknown {
+ background-color: @color-unknown;
+ }
+ }
+
+ a {
+ font-weight: bold;
+ }
+
+ tbody tr, tr.active {
+ border: none;
+ }
+
+ tr:not(:last-child) td {
+ border: 0 solid;
+ border-color: @gray-light;
+ border-bottom-width: 1px;
+ }
+
+ section {
+ color: @text-color-light;
+ font-family: @font-family-fixed;
+ }
+}
+
+// Layout
+
+.app-health {
+ th {
+ width: 2.5em;
+ padding: .5em 1em 0 .5em;
+ vertical-align: top;
+ }
+
+ td {
+ padding: .5em 0;
+ }
+
+ section {
+ margin-top: .25em;
+ height: 3em;
+ overflow: hidden;
+ word-break: break-word;
+ }
+}
diff --git a/public/css/icinga/layout-structure.less b/public/css/icinga/layout-structure.less
new file mode 100644
index 0000000..b1ca8e6
--- /dev/null
+++ b/public/css/icinga/layout-structure.less
@@ -0,0 +1,167 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+html {
+ height: 100%;
+ font-family: 'default-layout';
+}
+
+body {
+ height: 100%;
+ overflow: hidden;
+}
+
+#layout {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+#content-wrapper {
+ flex: 1 1 auto;
+ display: flex;
+ height: 0;
+
+}
+
+#sidebar {
+ width: 16em;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ z-index: 2;
+}
+
+#layout:not(.minimal-layout) #sidebar:after {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 1em;
+ background: linear-gradient(to left, rgba(0,0,0,.1), rgba(0,0,0,0));
+ z-index: 0;
+ pointer-events: none;
+}
+
+#main {
+ flex: 1;
+ display: flex;
+ z-index: 1;
+}
+
+.iframe {
+ #header, #sidebar {
+ display: none;
+ }
+}
+
+#responsive-debug {
+ font-size: 0.9em;
+ font-family: Courier new, monospace;
+ padding: 0.5em;
+ width: 25em;
+ color: white;
+ height: 10em;
+ display: none;
+ position: fixed;
+ bottom: 0.5em;
+ right: 2em;
+ overflow: hidden;
+ z-index: 1000;
+ background: #333;
+ border-radius: 0.5em;
+ opacity: 0.9;
+}
+
+#layout.minimal-layout #responsive-debug {
+ font-size: 0.6em;
+}
+
+#layout.poor-layout #responsive-debug {
+ font-size: 0.7em;
+}
+
+#layout.compact-layout #responsive-debug {
+ font-size: 0.8em;
+}
+
+#layout.wide-layout #responsive-debug {
+ font-size: 1em;
+}
+
+/** Fullscreen layout **/
+#layout.fullscreen-layout {
+ #sidebar {
+ display: none;
+ }
+
+ .container .controls {
+ padding: 0;
+ }
+
+ .controls > ul.tabs {
+ margin-top: 0;
+ height: 1.5em;
+ font-size: 0.75em;
+ padding: 0.2em 0 0;
+ }
+
+ .controls > ul.tabs > li > a {
+ line-height: 1.5em;
+ }
+}
+
+.controls-separated,
+.container .controls.separated {
+ box-shadow: 0 3px 4px -4px rgba(0, 0, 0, 0.2);
+// border-bottom: 1px solid @gray-lightest;
+ padding-bottom: @horizontal-padding / 2
+}
+
+.hbox {
+ display: inline-block;
+}
+
+.hbox-item {
+ display: inline-block;
+ vertical-align: top;
+ margin-top: 0.5em;
+ margin-bottom: 0.25em;
+ margin-left: 1em;
+ margin-right: 1em;
+}
+
+.hbox-spacer {
+ display: inline-block;
+ vertical-align: top;
+ width: 2em;
+}
+
+/*
+ * Class to hide content from users but available for screen reader
+ * Based on: https://cloudfour.com/thinks/see-no-evil-hidden-content-and-accessibility/
+ */
+.sr-only {
+ border: 0;
+ clip: rect(0 0 0 0);
+ clip-path: polygon(0px 0px, 0px 0px, 0px 0px);
+ -webkit-clip-path: polygon(0px 0px, 0px 0px, 0px 0px);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: fixed; // absolute causes view port glitches in chrome (#4310)
+ width: 1px;
+ white-space: nowrap;
+}
+
+// Hide non-javascript elements if javascript is enabled
+html.js *.no-js {
+ .sr-only;
+}
+
+// Hide javascript elements if javascript is disabled
+html.no-js *.js {
+ .sr-only;
+}
diff --git a/public/css/icinga/layout.less b/public/css/icinga/layout.less
new file mode 100644
index 0000000..c37da79
--- /dev/null
+++ b/public/css/icinga/layout.less
@@ -0,0 +1,379 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+#footer {
+ bottom: 0;
+ right: 0;
+ left: 12em;
+ position: fixed;
+ z-index: 999;
+}
+
+#layout.minimal-layout #footer {
+ left: 0;
+}
+
+.sidebar-collapsed #footer {
+ left: 3em;
+}
+
+#guest-error {
+ background-color: @icinga-blue;
+ height: 100%;
+ overflow: auto;
+}
+
+#guest-error #icinga-logo {
+ .fadein();
+}
+
+#guest-error-message {
+ .fadein();
+ color: @body-bg-color;
+ font-size: 2em;
+}
+
+#header,
+#login,
+#content-wrapper {
+ font-size: @font-size;
+ line-height: @line-height;
+}
+
+#header-logo-container {
+ background: @menu-bg-color;
+ height: 6em;
+ padding: 1.25em;
+ width: 16em;
+}
+
+#header-logo,
+#mobile-menu-logo {
+ background-image: url('../img/icinga-logo.svg');
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-size: contain;
+ display: block;
+ height: 100%;
+ width: 100%;
+
+ &:focus {
+ opacity: .6;
+ outline: none;
+ }
+}
+
+#mobile-menu-logo {
+ width: 50%;
+ float: left;
+ height: 2em;
+ margin-top: .25em;
+ background-position: .75em center;
+}
+
+#mobile-menu-toggle .icon-cancel {
+ display: none;
+}
+
+#icinga-logo {
+ background-image: url('../img/icinga-logo-big.svg');
+ background-position: center bottom;
+ background-repeat: no-repeat;
+ background-size: contain; // Does not work in IE < 10
+ height: 177px;
+ margin-bottom: 2em;
+ width: 100%;
+
+ &.invert {
+ background-image: url('../img/icinga-logo-big-dark.svg');
+ }
+}
+
+#layout {
+ background-color: @body-bg-color;
+ color: @text-color;
+ font-family: @font-family;
+}
+
+#login {
+ overflow: auto;
+}
+
+@gutter: 1em;
+
+// x-column-layout
+#main {
+ .clearfix();
+
+ & > .container {
+ width: 0;
+ overflow: auto;
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+
+ &:empty {
+ display: none;
+ }
+
+ & > .content {
+ flex: 1 1 auto;
+ overflow: auto;
+ }
+
+ & > .controls {
+ > .tabs {
+ // Remove gutter for tabs
+ margin-left: -1 * @gutter;
+ margin-right: -1 * @gutter;
+ height: 2.5em;
+ }
+
+ .tabs:first-child:not(:last-child) {
+ margin-bottom: .5em;
+ }
+ }
+ }
+}
+
+// Not part of the above to relax specificity and to allow modules adjust this
+:not(.dashboard) > .container {
+ & > .controls {
+ padding-left: @gutter;
+ padding-right: @gutter;
+ }
+
+ & > .content {
+ padding: @gutter;
+ }
+}
+
+// Mobile menu
+#layout.minimal-layout #sidebar {
+ background-color: @menu-bg-color;
+}
+
+#mobile-menu-toggle {
+ color: @menu-color;
+ text-align: right;
+
+ > button {
+ background: none;
+ border: none;
+ font-size: 2em;
+ padding: 0 .5em;
+ line-height: 2;
+
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ }
+
+ i:before {
+ margin-right: 0;
+ }
+}
+
+.container,
+.error-message,
+.modal-window {
+ // Don't outline containers and error messages when focused because they receive focus for accessibility only
+ // programmatically
+ outline: none;
+}
+
+.controls {
+ > .tabs {
+ overflow: hidden;
+ }
+}
+
+// Dashboard grid
+
+.dashboard {
+ letter-spacing: -0.417em;
+
+ > .container {
+ display: inline-block;
+ letter-spacing: normal;
+ vertical-align: top;
+ // Column width controlled by #layout
+ width: 100%;
+
+ &:last-of-type {
+ // See reponsive.less for gutters
+ padding-right: 0;
+ }
+ }
+}
+
+// Notification styles
+
+#notifications {
+ // Reset defaults
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+
+#notifications > li {
+ color: @text-color;
+ display: block;
+ line-height: 2.5em;
+ border-left: .5em solid @gray-light;
+ background: @body-bg-color;
+ margin-bottom: 1px;
+ box-shadow: 0 0 1em 0 rgba(0,0,0,0.25);
+
+ .icon {
+ padding: .5em;
+ width: 3em;
+ text-align: center;
+ }
+
+ &:hover {
+ cursor: pointer;
+ }
+
+ &.error {
+ border-color: @color-notification-error;
+ background: @color-notification-error;
+ color: @text-color-on-icinga-blue;
+
+ .icon {
+ color: @text-color-on-icinga-blue;
+ }
+ }
+
+ &.info {
+ border-color: @color-notification-info;
+
+ .icon {
+ color: @color-notification-info;
+ }
+ }
+
+ &.success {
+ border-color: @color-notification-success;
+
+ .icon {
+ color: @color-notification-success;
+ }
+ }
+
+ &.warning {
+ border-color: @color-notification-warning;
+ background: @color-notification-warning;
+ color: @text-color-inverted;
+
+ .icon {
+ color: @text-color-inverted;
+ }
+ }
+}
+
+// Collapsed sidebar
+#layout:not(.minimal-layout).sidebar-collapsed {
+ #header-logo-container {
+ height: 3em;
+ padding: 0.25em 0.125em;
+ width: 4em;
+ }
+
+ #header-logo {
+ background-image: url('../img/icinga-logo-compact.svg');
+ }
+
+ #sidebar {
+ width: 4em;
+ }
+
+ #open-sidebar {
+ display: inline;
+ }
+
+ #close-sidebar {
+ display: none;
+ }
+
+ #menu {
+ .nav-level-1 {
+ > .badge-nav-item > a {
+ position: relative;
+
+ > .badge {
+ position: absolute;
+ right: .5em;
+ bottom: .25em;
+ font-size: 75%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 4em;
+ }
+ }
+
+ > .nav-item.active > a > .badge {
+ display: unset;
+ }
+ }
+
+ img.icon {
+ margin: 0 1.25em -.25em 0.25em;
+ font-size: 1.5em;
+ }
+
+ .nav-item {
+ white-space: nowrap;
+ }
+
+ .nav-item.no-icon > a {
+ padding-left: .75em;
+ }
+
+ .nav-level-1 > .nav-item i {
+ font-size: 1.5em;
+ margin-right: .5em;
+ }
+
+ > .search-control {
+ height: 3.333em;
+ }
+ }
+
+ #search {
+ padding-left: 3.75em;
+ }
+
+ #search:focus {
+ background-color: @menu-bg-color;
+ border-radius: 0 .25em .25em 0;
+ box-shadow: 0 0 .25em 0 rgba(0, 0, 0, .2);
+ color: @menu-color;
+ width: 20em;
+ position: fixed;
+ z-index: 1;
+ }
+
+ .search-input {
+ font-size: 1.25em;
+ padding-right: .625em;
+ }
+
+ .search-reset {
+ display: none;
+ }
+
+ .skip-links {
+ a, button {
+ width: 20em;
+ }
+ }
+}
+
+@light-mode: {
+ #header-logo,
+ #mobile-menu-logo,
+ #about .icinga-logo {
+ filter: brightness(0.415) sepia(1) ~"saturate(0.1)" hue-rotate(144deg);
+ }
+};
diff --git a/public/css/icinga/login-orbs.less b/public/css/icinga/login-orbs.less
new file mode 100644
index 0000000..b0426dd
--- /dev/null
+++ b/public/css/icinga/login-orbs.less
@@ -0,0 +1,104 @@
+/*! Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+#login {
+ background-image: none;
+
+ .login-form {
+ background: none;
+ box-shadow: none;
+ }
+}
+
+.orb {
+ display: block;
+ position: absolute;
+ pointer-events: none;
+ transform-origin: center center;
+}
+
+.orb img {
+ height: auto;
+ width: 100%;
+}
+
+#orb-analytics {
+ top: -19%;
+ width: 25%;
+ left: 22.5%;
+ z-index: 0;
+}
+
+#orb-analytics img {
+ opacity: .2;
+}
+
+#orb-automation {
+ bottom: -6%;
+ width: 60%;
+ left: 7%;
+ z-index: 0;
+ margin-left: -30%;
+ margin-bottom: -30%;
+}
+
+#orb-automation img {
+ opacity: .75;
+}
+
+#orb-cloud {
+ top: -6%;
+ width: 25%;
+ right: 4%;
+ z-index: 0;
+ margin-right: -12.5%;
+ margin-top: -12.5%;
+}
+
+#orb-cloud img {
+ opacity: .4;
+}
+
+#orb-notifactions {
+ top: 7%;
+ right: 46%;
+ width: 10%;
+ margin: -5%;
+}
+
+#orb-notifactions img {
+ opacity: .5;
+}
+
+#orb-metrics {
+ left: 5%;
+ top: 20%;
+ width: 35%;
+ margin: -17.5%;
+}
+
+#orb-metrics img {
+ opacity: .5;
+}
+
+#orb-icinga {
+ left: 50%;
+ top: 50%;
+ margin-top: -38.5em;
+ margin-left: -38em;
+ width: 75em;
+ z-index: 0;
+}
+
+#orb-icinga img {
+ opacity: .8;
+}
+
+#orb-infrastructure {
+ top: -36%;
+ left: -15%;
+ width: 30%;
+}
+
+#orb-infrastructure img {
+ opacity: .6;
+}
diff --git a/public/css/icinga/login.less b/public/css/icinga/login.less
new file mode 100644
index 0000000..b37dbd8
--- /dev/null
+++ b/public/css/icinga/login.less
@@ -0,0 +1,183 @@
+/*! Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+// Login page styles
+
+#login {
+ height: 100%;
+ background-color: @menu-bg-color;
+ background-image: url(../img/icingaweb2-background-orbs.jpg);
+ background-repeat: no-repeat;
+ background-size: cover;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .login-form {
+ width: 36em;
+ position: relative;
+ z-index: 10;
+ padding: 2em 6em;
+ background-color: @login-box-background;
+ .box-shadow(0, 0, 1em, 1em, @login-box-background);
+ }
+
+ #icinga-logo {
+ width: 100%;
+ max-width: 18em;
+ height: auto;
+ margin: 0 auto 2em auto;
+
+ &:after {
+ content: "";
+ display: block;
+ width: 100%;
+ padding-bottom: 35%;
+ }
+ }
+
+ .errors,
+ .form-errors {
+ list-style-type: none;
+ padding: 0.5em;
+ }
+
+ .errors {
+ background-color: @color-critical;
+ color: white;
+ }
+
+ .form-errors {
+ margin-top: 0;
+ padding: 0;
+ }
+
+ .form-errors,
+ .control-group {
+ &:not(:last-child) {
+ margin-bottom: 1em;
+ }
+ }
+
+ input[type=password],
+ input[type=text] {
+ display: block;
+ height: 2.5em;
+ margin: 0;
+ transition: none;
+ width: 100%;
+
+ &:focus {
+ .rounded-corners(3px);
+ border-radius: 0;
+ padding-bottom: 3px;
+ }
+ }
+
+ input[type="submit"]:focus {
+ outline: 3px solid;
+ outline-color: @icinga-blue-light;
+ }
+
+ input[type=submit] {
+ border-radius: .25em;
+ background: @icinga-secondary;
+ color: white;
+ border: none;
+ height: 2.5em;
+ margin: 0;
+ width: 100%;
+
+ &:hover {
+ background-color: @icinga-secondary-dark;
+ }
+ }
+
+ .config-note {
+ background-color: @color-critical;
+ margin: 0 auto 2em auto; // Center horizontally w/ bottom margin
+ max-width: 50%;
+ min-width: 24em;
+ padding: 1em;
+
+ a {
+ color: @text-color-inverted;
+ font-weight: bold;
+ }
+ }
+
+ .remember-me-box {
+ display: flex;
+ align-items: flex-start;
+
+ .toggle-switch {
+ margin-right: 1em;
+ }
+
+ .control-info {
+ line-height: 1.5;
+ margin-left: .5em;
+ }
+ }
+}
+
+#social {
+ position: fixed;
+ right: 1em; bottom: 1em;
+ letter-spacing: -.417em;
+ margin: 0;
+
+ > * {
+ letter-spacing: normal;
+ }
+
+ > li {
+ display: inline-block;
+
+ a {
+ display: block;
+ text-decoration: none;
+ -webkit-transform: scale(1, 1);
+ -moz-transform: scale(1, 1);
+ -ms-transform: scale(1, 1);
+ transform: scale(1, 1);
+ }
+
+ i {
+ font-size: 3em;
+ color: white;
+ text-shadow: 0 0 .5em #01507B;
+ }
+ }
+
+ > li a:hover {
+ -webkit-transform: scale(1.2, 1.2);
+ -moz-transform: scale(1.2, 1.2);
+ -ms-transform: scale(1.2, 1.2);
+ transform: scale(1.2, 1.2);
+ }
+
+ li:not(:last-child) {
+ margin-right: 2em;
+ }
+}
+
+#login-footer {
+ padding: .5em 0;
+
+ p {
+ margin-bottom: 0;
+ }
+
+ a {
+ text-decoration: underline;
+
+ &:hover {
+ opacity: .8;
+ }
+ }
+}
+
+.orb {
+ display: none;
+}
diff --git a/public/css/icinga/main.less b/public/css/icinga/main.less
new file mode 100644
index 0000000..11dfa30
--- /dev/null
+++ b/public/css/icinga/main.less
@@ -0,0 +1,459 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+// Url for static ipl assets
+@iplWebAssets: "../lib/icinga/icinga-php-library";
+
+// Width for the name column--th--of name-value-table
+@name-value-table-name-width: 38/3em;
+
+.action-link {
+ color: @icinga-blue;
+}
+
+.error-message {
+ font-weight: @font-weight-bold;
+}
+
+.error-reason {
+ margin-top: 4em;
+}
+
+.large-icon {
+ font-size: 200%;
+}
+
+.content-centered {
+ margin: 0 auto;
+ text-align: center;
+}
+
+.icon-col {
+ text-align: center;
+ width: 1em;
+}
+
+.preformatted {
+ font-family: @font-family-fixed;
+ white-space: pre-wrap;
+}
+
+.markdown {
+ > * {
+ margin-left: 0;
+ margin-right: 0;
+ }
+
+ > *:last-child {
+ margin-bottom: 0;
+ }
+
+ img {
+ max-width: 100%;
+ height: auto;
+ }
+
+ a {
+ border-bottom: 1px @text-color-light dotted;
+
+ &:hover, &:focus {
+ border-bottom: 1px @text-color solid;
+ text-decoration: none;
+ }
+
+ img {
+ max-width: 32em;
+ }
+
+ &.with-thumbnail {
+ img {
+ padding: 1px;
+ }
+
+ &:hover, &:focus {
+ img {
+ padding: 0;
+ }
+ }
+ }
+ }
+
+ table {
+ border-collapse: collapse;
+
+ th {
+ text-align: left;
+ background-color: @gray-lighter;
+ }
+
+ &, th, td {
+ border: 1px solid @gray-light;
+ }
+ }
+}
+
+.no-wrap {
+ white-space: nowrap;
+}
+
+.pull-right {
+ float: right;
+}
+
+.text-right {
+ text-align: right;
+}
+
+.user-avatar {
+ height: 16px;
+ width: 16px;
+}
+
+.v-center {
+ > * {
+ vertical-align: middle;
+ }
+}
+
+.section {
+ margin-bottom: 2em;
+}
+
+a:hover > .icon-cancel {
+ color: @color-critical;
+}
+
+.icon-stateful {
+ .fg-stateful();
+}
+
+// Link styles
+
+.button-link {
+ .action-link();
+ .rounded-corners(3px);
+
+ background: @low-sat-blue;
+ display: inline-block;
+ padding: 0.25em 0.5em;
+
+ &:hover {
+ background: @low-sat-blue-dark;
+ text-decoration: none;
+ }
+}
+
+// List styles
+
+.comment-list {
+ margin: 0;
+
+ > dt {
+ border-bottom: 1px solid @gray-lighter;
+ margin-bottom: 0.25em;
+
+ &:hover {
+ background-color: @gray-lightest;
+
+ > .remove-action button:not(.spinner.active) {
+ visibility: visible;
+ }
+ }
+
+ > .remove-action button:not(.spinner.active) {
+ visibility: hidden;
+ }
+ }
+
+ > dd {
+ margin: 0 0 1em 0;
+ }
+}
+
+.comment-time {
+ color: @text-color-light;
+ font-size: @font-size-small;
+}
+
+.name-value-list {
+ > dd {
+ // Reset default margin
+ margin: 0;
+ }
+
+ > dt {
+ color: @text-color-light;
+ font-size: @font-size-small;
+ }
+}
+
+// Table styles
+
+.common-table {
+ width: 100%;
+
+ td, th {
+ padding-top: 1em;
+ }
+
+ td {
+ padding-bottom: 1em;
+ }
+
+ th {
+ text-align: left;
+ padding-bottom: 0.5em;
+ }
+
+ thead {
+ border-bottom: 1px solid @gray-light;
+ }
+
+ tbody tr {
+ border-bottom: 1px solid @gray-lightest;
+ border-left: 5px solid transparent;
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+
+ tr[href].active {
+ background-color: @tr-active-color;
+ border-left-color: @icinga-blue;
+ }
+
+ tr[href]:hover {
+ background-color: @tr-hover-color;
+ cursor: pointer;
+ }
+}
+
+.name-value-table {
+ width: 100%;
+}
+
+.name-value-table > caption {
+ margin-top: .5em;
+ text-align: left;
+ font-weight: bold;
+}
+
+.name-value-table > tbody > tr > th {
+ color: @text-color-light;
+ // Reset default font-weight
+ font-weight: normal;
+ padding-left: 0;
+ text-align: left;
+ vertical-align: top;
+ width: @name-value-table-name-width;
+}
+
+/* Styles for centering content of unknown width and height both horizontally and vertically
+ *
+ * Example markup:
+ * <div class="centered-ghost">
+ * <div class="centered-content">
+ * <p>I'm centered.</p>
+ * </div>
+ * </div>
+ */
+
+.centered-content {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.centered-ghost {
+ height: 100%;
+ text-align: center;
+ letter-spacing: -0.417em; // Remove gap between content and ghost
+}
+
+.centered-ghost > * {
+ letter-spacing: normal;
+}
+
+.centered-ghost:after {
+ content: '';
+ display: inline-block;
+ height: 100%;
+ vertical-align: middle;
+}
+
+// Responsive iFrames
+
+.iframe-container {
+ position: relative;
+ height: 0;
+ overflow: hidden;
+ padding-bottom: 75%;
+ width: 100%;
+
+ & > iframe {
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: 100%;
+ width: 100%;
+ }
+}
+
+// Collapsible Control
+#collapsible-control-ghost {
+ display: none;
+}
+
+.collapsible + .collapsible-control {
+ position: relative;
+ z-index: 1;
+
+ button {
+ .rounded-corners(50%);
+
+ float: right;
+ width: 2em;
+ height: 2em;
+ padding: 0;
+ margin-top: -1em;
+ margin-right: .25em;
+
+ background: @gray-lighter;
+ color: @gray;
+ border: none;
+ -webkit-box-shadow: 0 0 1/3em rgba(0,0,0,.3);
+ -moz-box-shadow: 0 0 1/3em rgba(0,0,0,.3);
+ box-shadow: 0 0 1/3em rgba(0,0,0,.3);
+
+ &:hover {
+ background: @gray-light;
+ }
+ }
+
+ button i:before {
+ margin-right: 0;
+ }
+}
+
+details.collapsible > summary {
+ &::marker,
+ &::-webkit-details-marker {
+ display: none;
+ }
+}
+
+.collapsible[data-can-collapse]:not(.collapsed) + .collapsible-control button,
+.collapsible[data-can-collapse]:not(.collapsed) > .collapsible-control,
+details.collapsible[open] + .collapsible-control button,
+details.collapsible[open] > .collapsible-control {
+ i.expand-icon {
+ display: none;
+ }
+
+ i.collapse-icon {
+ display: inline;
+ }
+}
+
+.collapsible.collapsed + .collapsible-control button,
+.collapsible.collapsed > .collapsible-control,
+details.collapsible:not([open]) + .collapsible-control button,
+details.collapsible:not([open]) > .collapsible-control {
+ i.expand-icon {
+ display: inline;
+ }
+
+ i.collapse-icon {
+ display: none;
+ }
+}
+
+// Collapsibles
+
+.collapsible.collapsed:not(details) {
+ overflow: hidden;
+}
+
+.collapsible.collapsed:not([data-toggle-element], details) {
+ position: relative;
+
+ &:after {
+ content: "";
+ display: block;
+ height: 2em;
+ background: linear-gradient(@body-bg-color-transparent, @body-bg-color);
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 1;
+
+ opacity: 1;
+ transition: opacity 2s 1s linear;
+ }
+}
+
+.role-memberships {
+ letter-spacing: -0.417em;
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+
+ > li {
+ display: inline-block;
+ letter-spacing: normal;
+ margin: 0;
+ padding: 0 0.25em 0 0;
+
+ &:last-child {
+ padding-right: 0;
+ }
+ }
+}
+
+.module-dependencies {
+ .unmet-dependencies {
+ background-color: @color-warning;
+ color: @text-color-on-icinga-blue;
+ padding: .25em .5em;
+ margin-left: -.5em;
+ }
+
+ .name-value-table {
+ > caption {
+ font-weight: normal;
+ color: @text-color-light;
+ }
+
+ > tbody > tr > th {
+ font-weight: bold;
+ color: @text-color;
+ }
+
+ .missing {
+ color: @color-critical;
+ font-weight: bold;
+ }
+
+ td {
+ white-space: nowrap;
+
+ &.or-separator {
+ width: 100%;
+ transform: translate(0, 50%);
+ padding-left: 3em;
+
+ &::before {
+ content: "";
+ position: absolute;
+ height: 1.5em;
+ width: 1.5em;
+ left: 0.5em;
+ border-top: 3px solid @gray;
+ border-right: 3px solid @gray;
+ border-top-right-radius: .50em;
+ transform: rotate(45deg);
+ }
+ }
+ }
+ }
+}
diff --git a/public/css/icinga/menu.less b/public/css/icinga/menu.less
new file mode 100644
index 0000000..98650a2
--- /dev/null
+++ b/public/css/icinga/menu.less
@@ -0,0 +1,554 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+#menu [class^="icon-"],
+#menu [class*=" icon-"] {
+ &:before {
+ width: 1.5em;
+ }
+}
+
+@icon-width: 1.7em; // 1.5em width + 0.2em right margin
+
+#menu {
+ background-color: @menu-bg-color;
+ width: 100%;
+ flex: 1;
+ overflow: auto;
+ overflow-x: hidden;
+}
+
+#menu .nav-item {
+ vertical-align: middle;
+
+ > a {
+ position: relative;
+
+ &:focus {
+ outline: none;
+ }
+
+ &:hover {
+ text-decoration: none;
+ }
+ }
+}
+
+#layout:not(.sidebar-collapsed) #menu .nav-item > a:first-of-type {
+ // Respect overflowing content
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item {
+ overflow: hidden;
+}
+
+#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item > a {
+ // Clip overflowing content
+ overflow: hidden;
+ width: 4em;
+}
+
+#menu .nav-level-1 > .nav-item {
+ line-height: 2.167em; // 26 px
+ color: @menu-color;
+
+ &.active {
+ color: @menu-active-color;
+
+ > a > .badge {
+ display: none;
+ }
+
+ background-color: @menu-active-bg-color;
+ }
+
+ &.no-icon > a {
+ padding-left: @icon-width + .75em;
+ }
+
+ > a {
+ padding: 0.5em 0.5em 0.5em .75em;
+ }
+
+ &.active:not(.selected) > a:focus,
+ &.active:not(.selected) > a:hover {
+ background-color: @menu-active-hover-bg-color;
+ }
+
+ &:not(.selected) > a:hover,
+ &:not(.selected) > a:focus {
+ background-color: @menu-hover-bg-color;
+ }
+
+ // Balance icon weight for non active menu items
+ &:not(.active) > a > i {
+ opacity: .8;
+ }
+
+ & > a > .icon-letter:before {
+ content: attr(data-letter);
+ font-family: @font-family;
+ font-weight: 800;
+ text-transform: uppercase;
+ }
+}
+
+#menu ul:not(.nav-level-2) > .selected > a {
+ background-color: @menu-highlight-color;
+ color: @text-color-inverted;
+
+ &:focus {
+ background-color: @menu-highlight-hover-bg-color;
+ }
+
+ &:after {
+ .transform(rotate(45deg));
+
+ position: absolute;
+ right: -.75em;
+
+ background-color: @body-bg-color;
+ box-shadow: 0 0 1em 0 rgba(0,0,0,0.6);
+ content: "";
+ display: block;
+ height: 1.25em;
+ margin-top: -1.75em;
+ width: 1.25em;
+ }
+}
+
+#menu .nav-level-2 > .nav-item {
+ // Collapse menu by default
+ display: none;
+ line-height: 1.833em; // 22px
+
+ > a {
+ color: @menu-2ndlvl-color;
+ font-size: @font-size-small;
+ padding: 0.364em 0.545em 0.364em 0.545em;
+
+ &:first-of-type {
+ padding-left: (@icon-width + .75em)/@font-size-small;
+ }
+ }
+
+ &.active {
+ overflow: hidden;
+ position: relative;
+ }
+
+ // Little caret on active level-2 item
+ &.active:after {
+ .transform(rotate(45deg));
+
+ background-color: @body-bg-color;
+ box-shadow: 0 0 1em 0 rgba(0,0,0,.6);
+ content: "";
+ display: block;
+ height: 1.25em;
+ width: 1.25em;
+ position: absolute;
+ top: .5em;
+ right: -.75em;
+ z-index: 3;
+ }
+
+ &.active > a {
+ color: @menu-2ndlvl-active-color;
+ background-color: @menu-2ndlvl-active-bg-color;
+
+ &:focus {
+ &:first-of-type,
+ &:first-of-type ~ a {
+ color: @menu-2ndlvl-active-hover-color;
+ background-color: @menu-2ndlvl-active-hover-bg-color;
+ }
+ }
+ }
+}
+
+.no-js #menu .nav-level-2 > .nav-item {
+ // Expand menu if JavaScript is disabled
+ display: block;
+}
+
+#layout:not(.sidebar-collapsed) {
+ #menu .nav-level-1 > .nav-item {
+ &.active {
+ .nav-level-2 > li {
+ // Expand menu if active
+ display: block;
+ }
+ }
+ }
+}
+
+#menu img.icon {
+ line-height: 1;
+ margin: 0 0.5em -.05em 0.25em;
+ width: 1em;
+}
+
+#menu img[src*="/img/icons/"] {
+ &:not([src$="tux.png"]):not([src$="win.png"]):not([src$="_white.png"]) {
+ -webkit-filter: invert(100%);
+ -moz-filter: invert(100%);
+ -ms-filter: invert(100%);
+ filter: invert(100%);
+ }
+}
+
+.nav-item:hover img.icon {
+ opacity: .6;
+}
+
+#menu input.search {
+ background: transparent url('../img/icons/search_white.png') no-repeat 1em center;
+ background-size: 1em auto;
+ border: none;
+ color: @menu-color;
+ line-height: 2.167em;
+ padding: .25em;
+ padding-left: @icon-width + .75em;
+ width: 100%;
+
+ &:focus::placeholder {
+ color: @menu-color;
+ }
+ &:focus::-ms-input-placeholder {
+ color: @menu-color;
+ }
+
+ &.active {
+ background-color: @menu-active-bg-color;
+ }
+
+ &:hover,
+ &:focus {
+ background-color: @menu-search-hover-bg-color;
+ }
+}
+
+// Badge offset correction
+#menu > nav > .nav-level-1 > .badge-nav-item > a > .badge {
+ margin-top: 0.2em;
+}
+
+#menu .nav-level-2 > .badge-nav-item > a > .badge {
+ margin-top: 0.2em;
+ margin-right: .5em
+}
+
+// Hovered menu
+#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item.hover,
+#layout:not(.minimal-layout) #menu .nav-level-1 > .nav-item:not(.active).hover {
+ > .nav-level-2 {
+ background-color: @menu-flyout-bg-color;
+ border: 1px solid;
+ border-color: @gray-light;
+ border-radius: .25em;
+ box-shadow: 0 0 1em 0 rgba(0,0,0,.3);
+ padding: @vertical-padding 0;
+ width: 14em;
+ position: fixed;
+ z-index: 1;
+
+ &:after {
+ .transform(rotate(45deg));
+
+ background-color: @body-bg-color;
+ border-bottom: 1px solid @gray-light;
+ border-left: 1px solid @gray-light;
+ content: "";
+ display: block;
+ height: 1.1em;
+ width: 1.1em;
+ position: absolute;
+ top: 1em;
+ left: -.6em;
+ z-index: -1;
+ }
+ > .nav-item {
+ display: block;
+ padding-left: 0;
+ position: relative;
+
+ > a {
+ color: @menu-flyout-color;
+
+ &:first-of-type {
+ padding-left: 1.5em;
+ }
+ }
+
+ &:not(.active) {
+ a:hover, a:focus {
+ &:first-of-type,
+ &:first-of-type ~ a {
+ background-color: @menu-2ndlvl-highlight-bg-color;
+ }
+ }
+ }
+
+ &.active > a {
+ color: @menu-color;
+ }
+
+ // Hide activity caret when displayed as flyout
+ &:after {
+ display: none;
+ }
+ }
+ }
+
+ > a > .badge {
+ display: none;
+ }
+
+ img.icon {
+ opacity: .6;
+ }
+}
+
+#layout:not(.minimal-layout) #menu .nav-level-1 > .nav-item:not(.active).hover {
+ > .nav-level-2 {
+ // Position relative to parent
+ margin-left: 16em;
+ margin-top: -3.167em;
+ }
+}
+
+#layout:not(.minimal-layout).sidebar-collapsed #menu .nav-level-1 > .nav-item.hover {
+ > .nav-level-2 {
+ // Position relative to parent
+ margin-left: 4em;
+ margin-top: -3.333em;
+
+ > .badge-nav-item {
+ display: flex;
+
+ a:first-of-type {
+ flex: 1 1 auto;
+ width: 0;
+ }
+
+ a:first-of-type ~ a {
+ flex: 0;
+ width: auto;
+
+ &:hover,
+ &:focus {
+ .badge {
+ opacity: .6;
+ }
+ }
+ }
+ }
+ }
+}
+
+// Accessibility skip links
+.skip-links {
+ position: relative;
+ font-size: 1/.75em;
+
+ ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ li {
+ display: block;
+ a, button[type="submit"] {
+ background-color: @body-bg-color;
+ border: none;
+ left: -999px;
+ padding: @vertical-padding @horizontal-padding;
+ position: absolute;
+ width: 100%;
+ z-index: 1;
+ &:focus {
+ left: 0;
+ outline-offset: -3px;
+ }
+ }
+ button[type="submit"] {
+ text-align: left;
+ }
+ }
+ }
+}
+
+#sidebar.expanded {
+ #mobile-menu-toggle .icon-menu {
+ display: none;
+ }
+
+ #mobile-menu-toggle .icon-cancel {
+ display: inline-block;
+ }
+}
+
+.search-control {
+ position: relative;
+}
+
+.search-input:focus ~ .search-reset:hover {
+ background-color: @menu-active-hover-bg-color;
+}
+
+.search-reset {
+ background: none;
+ border: 0;
+ color: @menu-color;
+ cursor: pointer;
+ display: none;
+ height: 100%;
+ padding: 0;
+ user-select: none;
+ position: absolute;
+ right: 0;
+ top: 0;
+
+ &:focus,
+ &:hover {
+ background-color: @menu-search-hover-bg-color;
+ outline: none;
+ }
+}
+
+// Override forms.less
+input[type=text].search-input {
+ padding-right: 1.4em;
+ text-overflow: ellipsis;
+ transition: none;
+}
+
+.search-input:focus:-moz-placeholder { // FF 18-
+ color: @gray-light;
+}
+
+.search-input:focus::-moz-placeholder { // FF 19+
+ color: @gray-light;
+}
+
+.search-input:focus:-ms-input-placeholder {
+ color: @gray-light;
+}
+
+.search-input:focus::-webkit-input-placeholder {
+ color: @gray-light;
+}
+
+.search-input ~ .search-reset {
+ opacity: 0;
+}
+
+.search-input:valid ~ .search-reset {
+ display: block;
+ opacity: 1;
+}
+
+.search-input:invalid,
+.search-input:-moz-submit-invalid,
+.search-input:-moz-ui-invalid {
+ // Disable glow
+ box-shadow: none;
+}
+
+// Toggle sidebar button
+#toggle-sidebar {
+ font-size: 1/.75em;
+
+ // Reset button styles
+ background: none;
+ border: none;
+ padding: 0;
+ color: @text-color-light;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ z-index: 3;
+ line-height: 2;
+
+ i {
+ background-color: @body-bg-color;
+ border-radius: .25em 0 0 .25em;
+ font-size: 1.125em;
+ width: 2em;
+ }
+
+ &:focus {
+ outline: none;
+ }
+
+ &:hover, &:focus {
+ i {
+ color: @menu-highlight-color;
+ }
+ }
+}
+
+html.no-js #toggle-sidebar {
+ display: none;
+}
+
+#layout.minimal-layout #toggle-sidebar {
+ display: none;
+}
+
+#open-sidebar {
+ display: none;
+}
+
+#open-sidebar:before,
+#close-sidebar:before {
+ width: 1.4em;
+ margin-right: 0;
+}
+
+#layout:not(.sidebar-collapsed) #menu .nav-level-1 > .nav-item.active .nav-level-2 > li {
+ &.nav-item:not(.badge-nav-item) {
+ &:not(.selected):not(.active) a:hover,
+ &:not(.selected):not(.active) a:focus {
+ background-color: @menu-2ndlvl-highlight-bg-color;
+ }
+ }
+}
+
+#layout:not(.sidebar-collapsed) #menu .nav-level-1 > .nav-item.active .nav-level-2 > li,
+#layout:not(.sidebar-collapsed) #menu .nav-level-1 > .nav-item:not(.active).hover .nav-level-2 > li {
+ &.badge-nav-item {
+ display: flex;
+ }
+
+ &.badge-nav-item a:first-of-type {
+ flex: 1 1 auto;
+ width: 0;
+ }
+
+ &.badge-nav-item a:first-of-type ~ a {
+ flex: 0;
+ width: auto;
+
+ &:hover,
+ &:focus {
+ .badge {
+ opacity: .6;
+ }
+ }
+ }
+}
+
+#layout:not(.sidebar-collapsed) #menu .nav-level-1 > .nav-item.active .nav-level-2 > li {
+ &.badge-nav-item:not(.selected) {
+ a:hover,
+ a:focus {
+ &:first-of-type,
+ &:first-of-type ~ a {
+ background-color: @menu-2ndlvl-highlight-bg-color;
+ }
+ }
+ }
+}
diff --git a/public/css/icinga/mixins.less b/public/css/icinga/mixins.less
new file mode 100644
index 0000000..6c55512
--- /dev/null
+++ b/public/css/icinga/mixins.less
@@ -0,0 +1,201 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+.button(
+ @background-color: @body-bg-color,
+ @border-font-color: @icinga-blue,
+ @color-dark: @icinga-blue-dark
+) {
+ .rounded-corners(3px);
+
+ display: inline-flex;
+ align-items: baseline;
+ background-color: @background-color;
+ border: 2px solid @border-font-color;
+ color: @border-font-color;
+ cursor: pointer;
+ line-height: normal;
+ outline: none;
+ padding: ~"calc(@{vertical-padding} - 2px)" @horizontal-padding;
+
+ @duration: 0.2s;
+ // The trailing semicolon is needed to be able to pass this as a css list
+ .transition(background @duration, border @duration ease, color @duration ease;);
+
+ &:focus,
+ &:hover,
+ &.btn-primary {
+ background-color: @border-font-color;
+ color: @background-color;
+ }
+
+ &.btn-primary:focus,
+ &.btn-primary:hover {
+ background-color: @color-dark;
+ border-color: @color-dark;
+ color: @background-color;
+ }
+
+ &:hover {
+ text-decoration: none;
+ }
+}
+
+.clearfix {
+ &:after {
+ content: "";
+ clear: both;
+ display: table;
+ }
+}
+
+.opacity(@opacity: 0.6) {
+ opacity: @opacity;
+}
+
+.transform(@transform) {
+ -webkit-transform: @transform;
+ -moz-transform: @transform;
+ -ms-transform: @transform;
+ -o-transform: @transform;
+ transform: @transform;
+}
+
+.user-select(@user-select) {
+ -webkit-user-select: @user-select;
+ -moz-user-select: @user-select;
+ -ms-user-select: @user-select;
+ user-select: @user-select;
+}
+
+.transition (@transition) {
+ -webkit-transition: @transition;
+ -moz-transition: @transition;
+ -o-transition: @transition;
+ transition: @transition;
+}
+
+// Fadein animation
+
+/* Chrome, WebKit */
+@-webkit-keyframes fadein {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+/* FF < 16 */
+@-moz-keyframes fadein {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+/* Opera < 12.1 */
+@-o-keyframes fadein {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes fadein {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+.fadein() {
+ opacity: 0;
+
+ -webkit-animation: fadein 2s ease-in; /* Chrome, WebKit */
+ -moz-animation: fadein 2s ease-in; /* FF < 16 */
+ -o-animation: fadein 2s ease-in; /* Opera < 12.1 */
+ animation: fadein 2s ease-in;
+
+ // Make sure that after animation is done we remain at the last keyframe value (opacity: 1)
+ -webkit-animation-fill-mode: forwards;
+ -moz-animation-fill-mode: forwards;
+ -o-animation-fill-mode: forwards;
+ animation-fill-mode: forwards;
+}
+
+// Mixin for stateful foreground colors, e.g. text or icons
+.fg-stateful {
+ &.state-ok {
+ color: @color-ok;
+ }
+ &.state-up {
+ color: @color-up;
+ }
+ &.state-warning {
+ color: @color-warning;
+ &.handled {
+ color: @color-warning-handled;
+ }
+ }
+ &.state-critical {
+ color: @color-critical;
+ &.handled {
+ color: @color-critical-handled;
+ }
+ }
+ &.state-down {
+ color: @color-down;
+ &.handled {
+ color: @color-down-handled;
+ }
+ }
+ &.state-unreachable {
+ color: @color-unreachable;
+ &.handled {
+ color: @color-unreachable-handled;
+ }
+ }
+ &.state-unknown {
+ color: @color-unknown;
+ &.handled {
+ color: @color-unknown-handled;
+ }
+ }
+ &.state-pending {
+ color: @color-pending;
+ }
+}
+
+// Mixin for stateful background colors
+.bg-stateful {
+ &.state-ok {
+ background-color: @color-ok;
+ }
+ &.state-up {
+ background-color: @color-up;
+ }
+ &.state-warning {
+ background-color: @color-warning;
+ &.handled {
+ background-color: @color-warning-handled;
+ }
+ }
+ &.state-critical {
+ background-color: @color-critical;
+ &.handled {
+ background-color: @color-critical-handled;
+ }
+ }
+ &.state-down {
+ background-color: @color-down;
+ &.handled {
+ background-color: @color-down-handled;
+ }
+ }
+ &.state-unreachable {
+ background-color: @color-unreachable;
+ &.handled {
+ background-color: @color-unreachable-handled;
+ }
+ }
+ &.state-unknown {
+ background-color: @color-unknown;
+ &.handled {
+ background-color: @color-unknown-handled;
+ }
+ }
+ &.state-pending {
+ background-color: @color-pending;
+ }
+}
diff --git a/public/css/icinga/modal.less b/public/css/icinga/modal.less
new file mode 100644
index 0000000..3d497d6
--- /dev/null
+++ b/public/css/icinga/modal.less
@@ -0,0 +1,113 @@
+#layout > #modal {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ top: 0;
+
+ background-color: rgba(0, 0, 0, .6);
+ opacity: 0;
+ font-size: @font-size;
+ line-height: @line-height;
+ pointer-events: none;
+ transition: opacity .2s ease-in; // This is coupled with a `setTimout` in modal.js
+ z-index: 1000;
+
+ &.active {
+ opacity: 1;
+ pointer-events: auto;
+ }
+
+ > div {
+ height: 100%;
+ pointer-events: none;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+}
+
+#modal-content {
+ display: flex;
+ flex: 10;
+ flex-direction: column;
+ justify-content: stretch;
+
+ > .content {
+ padding: 1em;
+
+ > .icinga-form {
+ width: 100%;
+ }
+ }
+}
+
+#modal-ghost {
+ display: none;
+}
+
+.modal-area {
+ display: flex;
+ flex-direction: row;
+ flex-grow: 1;
+ justify-content: stretch;
+}
+
+.modal-header {
+ padding: .25em 0;
+ position: relative;
+ text-align: center;
+
+ > button {
+ position: absolute;
+ top: .75em;
+ right: 1em;
+
+ background-color: @gray;
+ border: none;
+ border-radius: 50%;
+ color: @text-color-inverted;
+ height: 1.5em;
+ line-height: 1em;
+ padding: 0;
+ text-align: center;
+ width: 1.5em;
+
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ }
+
+ > button:hover {
+ opacity: .8;
+ }
+
+ > button > .icon-cancel:before {
+ margin-right: 0;
+ }
+}
+
+.modal-header h1 {
+ padding: .25em;
+ margin: 0;
+}
+
+.modal-window {
+ overflow: auto;
+ pointer-events: auto;
+
+ display: flex;
+ align-items: stretch;
+ flex-direction: column;
+
+ background-color: @body-bg-color;
+ border-radius: .5em;
+ box-shadow: 0 0 2em 0 rgba(0, 0, 0, .6);
+ flex: 1;
+ margin: 0 auto;
+ max-height: 80%;
+ min-height: 40vh;
+ max-width: 60em;
+}
diff --git a/public/css/icinga/nav.less b/public/css/icinga/nav.less
new file mode 100644
index 0000000..cd8f9d0
--- /dev/null
+++ b/public/css/icinga/nav.less
@@ -0,0 +1,54 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+.badge-nav-item {
+ > a {
+ .clearfix();
+
+ > .badge {
+ float: right;
+ }
+ }
+}
+
+.dropdown-nav-item > ul {
+ display: none;
+ position: absolute;
+}
+
+.dropdown-nav-item.active > ul,
+.dropdown-nav-item:hover > ul {
+ display: block;
+}
+
+.nav {
+ // Reset defaults
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+
+ li > a,
+ li > span {
+ // Rollover
+ display: block;
+ }
+}
+
+.nav .nav-item .icon:before {
+ width: 1.5em;
+}
+
+.tab-nav {
+ .clearfix();
+
+ > li {
+ float: left;
+ }
+}
+
+.primary-nav a {
+ font-weight: 500;
+}
+
+.remove-nav-item {
+ padding-left: 2em;
+}
diff --git a/public/css/icinga/pending-migration.less b/public/css/icinga/pending-migration.less
new file mode 100644
index 0000000..bc70caa
--- /dev/null
+++ b/public/css/icinga/pending-migration.less
@@ -0,0 +1,173 @@
+// Style
+
+@visual-width: 1.5em;
+@max-view-width: 50em;
+
+.migration-state-banner, .change-database-user-description {
+ .rounded-corners();
+
+ border: 1px solid @gray-light;
+ color: @text-color;
+}
+
+.migrations {
+ a {
+ color: @icinga-blue;
+ }
+
+ .empty-state {
+ margin: 0 auto;
+ }
+
+ .list-item {
+ .visual.upgrade-failed, span.upgrade-failed, .errors-section > header > i {
+ color: @state-critical;
+ }
+
+ span.version {
+ color: @text-color;
+ }
+ }
+
+.migration-form {
+ input[type="submit"] {
+ line-height: 1.5;
+
+ &:disabled {
+ color: @disabled-gray;
+ cursor: not-allowed;
+ background: none;
+ border-color: fade(@disabled-gray, 75)
+ }
+ }
+ }
+}
+
+// Layout
+
+#layout.twocols:not(.wide-layout) .migration-form fieldset .control-label-group {
+ text-align: right;
+}
+
+.migration-state-banner, .change-database-user-description {
+ padding: 1em;
+ text-align: center;
+
+ &.change-database-user-description {
+ max-width: 50em;
+ padding: .5em;
+ }
+}
+
+.pending-migrations-hint {
+ text-align: center;
+
+ > h2 {
+ font-size: 2em;
+ }
+}
+
+.migration-controls {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.migrations {
+ .migration-form fieldset {
+ max-width: @max-view-width;
+ }
+
+ .migration-list-control {
+ padding-bottom: 1em;
+
+ > .item-list {
+ max-width: @max-view-width;
+ }
+ }
+
+ .item-list:not(.file-list) > .list-item {
+ > .main {
+ border-top: none;
+ }
+
+ footer {
+ display: block;
+ }
+ }
+
+ .list-item {
+ align-items: baseline;
+
+ .main {
+ margin-left: 0;
+ }
+
+ header {
+ align-items: baseline;
+ justify-content: flex-start;
+
+ input {
+ margin-left: auto;
+ }
+
+ .title span.upgrade-failed {
+ margin: .5em;
+ }
+ }
+
+ .caption, .errors-section pre {
+ margin-top: .25em;
+ height: auto;
+ -webkit-line-clamp: 3;
+ }
+
+ .errors-section {
+ margin: 1em -.25em;
+ border: 1px solid @state-critical;
+ padding: .25em;
+ .rounded-corners(.5em);
+
+ .status-icon {
+ margin-top: .3em;
+ margin-left: -1.5em;
+ margin-right: .25em;
+ }
+
+ .caption, header {
+ margin-left: 1.8em;
+ }
+ }
+
+ footer {
+ width: 100%;
+ padding-top: 0;
+
+ > * {
+ font-size: 1em;
+ }
+
+ .list-item:first-child .main {
+ padding-top: 0;
+ }
+
+ a {
+ margin-left: @visual-width;
+ }
+ }
+ }
+}
+
+.item-list.file-list {
+ .visual {
+ width: @visual-width;
+ }
+
+ .main {
+ margin-left: @visual-width;
+ }
+
+ .visual + .main {
+ margin-left: 0;
+ }
+}
diff --git a/public/css/icinga/php-diff.less b/public/css/icinga/php-diff.less
new file mode 100644
index 0000000..d8f6a80
--- /dev/null
+++ b/public/css/icinga/php-diff.less
@@ -0,0 +1,17 @@
+@diff-bg-color: transparent;
+@diff-text-color: @text-color;
+
+@diff-bg-color-ins-base: @color-up;
+@diff-bg-color-del-base: @color-down;
+@diff-bg-color-rep-base: @color-warning;
+
+@diff-border-color: @gray-light;
+
+@light-mode: {
+ :root {
+ --diff-bg-color: var(--body-bg-color);
+ --diff-text-color: var(--text-color);
+
+ --diff-border-color: var(--gray-light);
+ }
+};
diff --git a/public/css/icinga/print.less b/public/css/icinga/print.less
new file mode 100644
index 0000000..75d4728
--- /dev/null
+++ b/public/css/icinga/print.less
@@ -0,0 +1,39 @@
+/*! Icinga Web 2 | (c) 2015 Icinga GmbH | GPLv2+ */
+
+@media print {
+ #sidebar,
+ #migrate-popup, // Icinga DB Web
+ .controls,
+ .footer, // ipl
+ .dontprint, // Compat only, use dont-print instead
+ .dont-print {
+ display: none !important;
+ }
+
+ #main > .container {
+ overflow: visible !important;
+
+ > .content {
+ overflow: visible !important;
+ }
+ }
+
+ :root {
+ --body-bg-color: #fff !important;
+ --text-color: #535353 !important;
+ --text-color-light: #7F7F7F !important;
+ --tr-active-color: #fff !important;
+ --tr-hover-color: #fff !important;
+
+ // ipl-web overrides
+ --default-bg: #fff !important;
+ --default-text-color: #535353 !important;
+ --default-text-color-inverted: #fff !important;
+ }
+}
+
+@media not print {
+ .print-only {
+ display: none !important;
+ }
+}
diff --git a/public/css/icinga/responsive.less b/public/css/icinga/responsive.less
new file mode 100644
index 0000000..6d1cae8
--- /dev/null
+++ b/public/css/icinga/responsive.less
@@ -0,0 +1,167 @@
+/*! Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+// Not growing larger than 3840px at 1em=16px right now
+@media screen and (min-width: 240em) {
+ #header {
+ min-width: 240em;
+ }
+
+ #main {
+ width: 227em;
+ }
+}
+
+// More than 100em, usually 1600px at 1em=16px
+@media screen and (min-width: 100em) {
+ html {
+ font-family: 'wide-layout';
+ }
+}
+
+// Up to 1152px at 1em=16px
+@media screen and (max-width:72em) {
+ html {
+ font-family: 'compact-layout';
+ }
+}
+
+// Up to 752px at 1em=16px
+@media screen and (max-width: 47em) {
+ html {
+ font-family: 'poor-layout';
+ }
+}
+
+// Up to 576px at 1em=16px, should fit 320px devices
+@media screen and (max-width: 36em) {
+ html {
+ font-family: 'minimal-layout';
+ }
+}
+
+#layout.compact-layout {
+ font-size: 0.875em;
+}
+
+#layout.poor-layout {
+ font-size: 0.875em;
+
+ #layout.twocols {
+ #col1 {
+ display: none;
+ }
+
+ #main > .container {
+ width: 100%;
+ }
+ }
+
+ .dashboard > div.container {
+ width: 100%;
+ }
+}
+
+#layout:not(.minimal-layout) {
+ #mobile-menu-toggle {
+ display: none;
+ }
+}
+
+#layout.minimal-layout {
+ #sidebar {
+ width: 100%;
+ overflow: auto;
+ }
+
+ #header-logo-container {
+ width: auto;
+ height: 4em;
+ padding: 0;
+ background: inherit;
+ }
+
+ #header-logo {
+ float: left;
+ width: 9em;
+ height: 3em;
+ margin: .5em 1em;
+ background-position: left center;
+ }
+
+ #mobile-menu-toggle {
+ float: right;
+ }
+
+ #sidebar:not(.expanded) #menu {
+ display: none;
+ }
+
+ #menu {
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+ }
+
+ #content-wrapper {
+ flex-direction: column;
+ }
+
+ #main {
+ flex: 1 1 auto;
+ height: 0;
+ }
+
+ ul > .selected > a:after,
+ ul > .nav-item.active:after {
+ display: none;
+ }
+
+ .dashboard > div.container {
+ width: 100%;
+ }
+}
+
+// Dashboard
+
+.dashboard > .container {
+ padding-right: 0;
+ width: 100%;
+}
+
+#layout:not(.twocols).default-layout .dashboard > .container:not(:only-child) {
+ padding-right: @gutter;
+ width: 50%;
+}
+
+#layout:not(.twocols).wide-layout .dashboard > .container:not(:only-child) {
+ padding-right: @gutter;
+ width: 33.33%;
+}
+
+// Columns
+
+#layout.twocols #col2 {
+ border-left: 1px solid @gray-lighter;
+}
+
+#layout.twocols.wide-layout #col2 {
+ flex-grow: 2;
+}
+
+// Safe areas for iPhone X
+
+#header, #sidebar, #footer {
+ padding-left: constant(safe-area-inset-left);
+}
+
+#main, #footer {
+ padding-right: constant(safe-area-inset-right);
+}
+
+#layout.twocols #col2 {
+ border-left: 1px solid @gray-lighter;
+
+ &:empty {
+ display: flex;
+ }
+}
diff --git a/public/css/icinga/setup.less b/public/css/icinga/setup.less
new file mode 100644
index 0000000..5748c8e
--- /dev/null
+++ b/public/css/icinga/setup.less
@@ -0,0 +1,480 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+#setup-content-wrapper {
+ height: 0;
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+
+ > .setup-content {
+ height: 0;
+ overflow: auto;
+ flex: 1 1 auto;
+ }
+}
+
+.setup-header {
+ width: 100%;
+ height: 5.5em;
+ background-color: @icinga-blue;
+ text-align: center;
+
+ img {
+ width: 7.5em;
+ margin: 1.5em;
+ float: left;
+ }
+
+ form[name='setup_restart_form'] button {
+ background: none;
+ border: none;
+ color: #ffffff;
+ cursor: pointer;
+ outline: none;
+ font-size: 1.4em;
+ margin-right: 0.6em;
+ -moz-transform: scale(1, -1);
+ -webkit-transform: scale(1, -1);
+ -o-transform: scale(1, -1);
+ -ms-transform: scale(1, -1);
+ transform: scale(1, -1);
+ }
+
+ .progress-bar {
+ overflow: hidden;
+ padding-top: 1em;
+
+ .step {
+ float: left;
+
+ h1 {
+ margin: 0;
+ color: white;
+ font-size: 0.9em;
+ text-align: center;
+ border-bottom: none;
+ }
+
+ table {
+ margin-top: 0.3em;
+
+ td {
+ padding: 0;
+
+ &.left, &.right {
+ width: 50%;
+ }
+ }
+ }
+
+ div {
+ background-color: lightgrey;
+
+ &.line {
+ height: 0.4em;
+
+ &.left {
+ margin-left: 0.1em;
+ margin-right: -0.1em;
+ border-top-left-radius: 0.5em;
+ border-bottom-left-radius: 0.5em;
+ }
+
+ &.right {
+ margin-left: -0.1em;
+ margin-right: 0.1em;
+ border-top-right-radius: 0.5em;
+ border-bottom-right-radius: 0.5em;
+ }
+ }
+
+ &.bubble {
+ width: 1.2em;
+ height: 1.2em;
+ border-radius: 1.2em;
+
+ // Make sure that such a bubble overlays lines
+ position: relative;
+ z-index: 1337;
+ }
+
+ &.active {
+ background-color: white;
+ }
+
+ &.complete {
+ background-color: @color-ok;
+ }
+
+ &.visited {
+ background-color: #eee;
+ }
+ }
+ }
+ }
+}
+
+.setup-content {
+ padding: 1.5em 10em 0 10em;
+
+ h1 {
+ font-weight: bold;
+ }
+
+ form {
+ h2 {
+ font-size: 2.0em;
+ }
+ }
+}
+
+.setup-content .control-group > :not([hidden]) {
+ display: inline-block;
+ margin-right: 1em;
+}
+
+.setup-content div.buttons {
+ margin-top: 1.5em; // Yes, -top and -bottom, keep it like that...
+ margin-bottom: 1.5em;
+
+ .double {
+ position: absolute;
+ left: -1337px;
+ }
+
+ .control-button,
+ input[type="submit"] {
+ .button();
+ justify-content: center;
+ }
+
+ .control-button[disabled] {
+ background: none;
+ cursor: default;
+ color: @control-disabled-color;
+ border: 1px solid @control-disabled-color;
+
+ &:hover {
+ color: @control-disabled-color;
+ background: none;
+ border: 1px solid @control-disabled-color;
+ }
+ }
+
+ button.finish, a.button-link.login {
+ min-width: 25em;
+ }
+
+ .spinner {
+ margin-left: 1em;
+ }
+}
+
+.setup-content div.buttons + ul.hints {
+ margin-top: -1.5em;
+ margin-bottom: 1.5em;
+}
+
+form#setup_requirements {
+ margin-top: 2em;
+ padding-top: 0.5em;
+ border-top: 1px solid #888;
+
+ div.buttons div.requirements-refresh {
+ width: 25%;
+ float: right;
+ text-align: center;
+
+ a.button-like {
+ padding: 0.1em 0.4em;
+ }
+ }
+}
+
+.setup-content ul.requirements {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+
+ li {
+ margin-bottom: 1em;
+
+ & > ul {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+ }
+
+ div {
+ float: left;
+ padding-top: 0.4em;
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ }
+
+ div.title {
+ width: 25%;
+
+ h2 {
+ padding: 0;
+ margin: 0 1em 0 0;
+ border-bottom: 0;
+ }
+ }
+
+ div.description {
+ width: 50%;
+ border-left: 0.4em solid transparent;
+ border-right: 0.4em solid transparent;
+
+ ul {
+ margin: 0;
+ padding-left: 1em;
+ list-style-type: square;
+ }
+ }
+
+ div.state {
+ width: 25%;
+ color: white;
+ padding: 0.4em;
+
+ &.fulfilled {
+ background-color: @color-ok;
+ }
+
+ &.not-available {
+ color: black;
+ background-color: #e8ec70;
+ }
+
+ &.missing {
+ background-color: @color-critical;
+ }
+ }
+ }
+}
+
+#setup_ldap_discovery_confirm table {
+ margin: 1em 0;
+ border-collapse: separate;
+ border-spacing: 1em 0.2em;
+}
+
+#setup_admin_account {
+ div.instructions {
+ width: 30.2em;
+ display: inline-block;
+ }
+
+ div.radiobox {
+ vertical-align: top;
+ display: inline-block;
+ padding: 0.9em 0.2em 0;
+ }
+}
+
+.setup-content {
+ div.summary {
+ font-size: 90%;
+
+ div.page {
+ float: left;
+ width: 25em;
+ min-height: 25em;
+ padding: 0 1em 1em;
+ margin: 1em 1.5em 1.5em;
+ border: 1px dashed lightgrey;
+
+ h2 {
+ font-size: 1.2em;
+ font-weight: bold;
+ }
+
+ div.topic {
+ margin-left: 2em;
+
+ h3 {
+ font-size: 1em;
+ }
+
+ ul {
+ list-style-type: circle;
+ }
+
+ table {
+ border-spacing: 0.5em;
+ border-collapse: separate;
+ font-size: 0.9em;
+ margin-left: 2em;
+ }
+ }
+ }
+ }
+
+ form.summary {
+ clear: left;
+ }
+}
+
+#setup-finish {
+ h2 {
+ padding: 0.5em;
+ border-bottom: 0;
+ font-variant: normal;
+ font-weight: bold;
+ color: white;
+
+ &.success {
+ background-color: @color-ok;
+ }
+
+ &.failure {
+ background-color: @color-critical;
+ }
+ }
+
+ pre.log-output {
+ width: 66%;
+ height: 25em;
+ max-height: none;
+ }
+
+ div.buttons {
+ margin-top: 0;
+ text-align: center;
+
+ a {
+ padding: 0.5em;
+ }
+ }
+}
+
+.welcome-page {
+ margin-top: 3em;
+ text-align: center;
+
+ h2 {
+ font-size: 2.0em;
+ margin-bottom: 2em;
+ }
+
+ div.info {
+ padding: 0 1em;
+ background-color: #eee;
+ border: 1px solid lightgrey;
+ }
+
+ p.restart-warning {
+ color: coral;
+ font-weight: bold;
+ }
+
+ form ul.errors {
+ display: block;
+
+ list-style-type: none;
+ color: red;
+ }
+
+ div.note {
+ padding: 1em 1em 0;
+ margin: 3em auto 0;
+ text-align: left;
+ font-size: 0.9em;
+ border: 1px solid;
+ border-color: @gray-light;
+
+ h3 {
+ padding: 0.2em;
+ margin: -1em -1em 1em;
+ text-align: center;
+ color: @text-color;
+ background-color: @gray-lightest;
+ border: 1px solid;
+ border-color: @gray-light;
+ }
+
+ img {
+ float: right;
+ }
+
+ p {
+ margin: 2em 0 1em 0;
+
+ &:first-child {
+ margin-top: 1em;
+ }
+ }
+
+ div.code {
+ margin: 0 2em;
+
+ span {
+ display: block;
+ font-family: monospace;
+ }
+ }
+ }
+}
+
+#setup_monitoring_welcome {
+ .welcome-page;
+ margin-top: 0;
+ padding: 1em;
+
+ h2 {
+ margin-top: 0;
+ }
+}
+
+#setup_modules {
+ div.module {
+ float: left;
+ width: 15em;
+ height: 15em;
+ margin: 1em;
+ padding: 0.3em;
+ border: 1px solid;
+ border-color: @gray-semilight;
+ background-color: @gray-lightest;
+
+ .header {
+ height: 2.5em;
+ display: flex;
+ justify-content: space-between;
+ }
+
+ h3 {
+ margin: 0;
+ border: none;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ label {
+ cursor: pointer;
+ }
+ }
+
+ label.description {
+ display: inline-block;
+ width: 14.4em;
+ height: 12em;
+ overflow: auto;
+ cursor: pointer;
+ font-weight: normal;
+ }
+
+ input[type=checkbox] {
+ height: 10em;
+ float: right;
+ margin: 0;
+ }
+ }
+
+ div.buttons {
+ padding-top: 1em;
+ clear: both;
+ }
+}
diff --git a/public/css/icinga/spinner.less b/public/css/icinga/spinner.less
new file mode 100644
index 0000000..c1cf93e
--- /dev/null
+++ b/public/css/icinga/spinner.less
@@ -0,0 +1,42 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+.refresh-container-control > i:before {
+ margin: 0;
+}
+
+a.spinner.active > i,
+button.spinner.active > i,
+i.spinner.active {
+ &:before {
+ .animate(spin 2s infinite linear);
+
+ // icon-spin6
+ content: '\e874';
+ }
+
+ &.fa:before {
+ // fa spinner
+ content: '\f110';
+ }
+}
+
+div.spinner {
+ display: inline-block;
+ vertical-align: middle;
+
+ i {
+ visibility: hidden;
+
+ &.active {
+ visibility: visible;
+
+ &:before {
+ .animate(spin 2s infinite linear);
+ }
+ }
+
+ &:before {
+ margin: 0; // Disables wobbling
+ }
+ }
+}
diff --git a/public/css/icinga/tabs.less b/public/css/icinga/tabs.less
new file mode 100644
index 0000000..7185d61
--- /dev/null
+++ b/public/css/icinga/tabs.less
@@ -0,0 +1,109 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+// Styles for tab navigation of containers
+
+.tabs {
+ background-color: @menu-bg-color;
+ letter-spacing: -0.417em;
+}
+
+.tabs > li {
+ display: inline-block;
+ letter-spacing: normal;
+}
+
+.tabs a {
+ padding: 0 1em;
+ line-height: 2.5em;
+
+ &:focus {
+ outline-offset: -0.5em;
+ }
+}
+
+.tabs > li {
+ &:not(:last-child) {
+ margin-right: 0.5em;
+ }
+
+ > a {
+ color: @menu-color;
+
+ &:hover {
+ text-decoration: none;
+ background: @tab-hover-bg-color;
+ }
+ }
+
+ &.active > a,
+ > a:focus {
+ background-color: @body-bg-color;
+ color: @text-color;
+ }
+}
+
+.tabs > .dropdown-nav-item > a,
+.tabs > li > .close-container-control,
+.tabs > li > .refresh-container-control {
+ text-align: center;
+ width: 3em;
+}
+
+.tabs > .dropdown-nav-item:hover > a,
+.tabs > .dropdown-nav-item > a:focus,
+.tabs > li > .close-container-control:focus,
+.tabs > li > .close-container-control:hover,
+.tabs > li > .refresh-container-control:focus,
+.tabs > li > .refresh-container-control:hover {
+ background-color: @body-bg-color;
+ color: @text-color;
+ text-decoration: none;
+}
+
+.tabs > .dropdown-nav-item > ul {
+ .box-shadow();
+ .rounded-corners(0 0 0.3em 0.3em);
+
+ background-color: @body-bg-color;
+ border: 1px solid;
+ border-color: @gray-light;
+ border-top: none;
+ margin-left: -1px;
+ min-width: 14em;
+ z-index: 10;
+}
+
+.tabs > .dropdown-nav-item > ul > li:hover > a {
+ background-color: @gray-lighter;
+ text-decoration: none;
+}
+
+// Dropdown tabs after the fourth title should be right-aligned
+.tabs > li:nth-child(n+5).dropdown-nav-item > ul {
+ transform: translate(~"calc(-100% + 3em)"); // -full width + tab width
+ margin-left: 1px;
+}
+
+// TODO(el): Rename display-on-hover and move it to main.less
+.display-on-hover {
+ font-size: @font-size-small;
+ left: -999em;
+ position: relative;
+}
+
+.dropdown-nav-item > ul > li > a:focus > .display-on-hover,
+.dropdown-nav-item > ul > li:hover > a > .display-on-hover {
+ position: static;
+}
+
+.tabs > li > .close-container-control {
+ display: none;
+}
+
+#layout.twocols .tabs > li > .close-container-control {
+ display: block;
+}
+
+.close-container-btn {
+ float: right;
+}
diff --git a/public/css/icinga/widgets.less b/public/css/icinga/widgets.less
new file mode 100644
index 0000000..3518cbe
--- /dev/null
+++ b/public/css/icinga/widgets.less
@@ -0,0 +1,666 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+#announcements > ul {
+ background-color: @body-bg-color;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+
+ > li {
+ border-bottom: 1px solid @gray-lighter;
+ line-height: 1.5em;
+ padding: 0.5em 1em 0.5em 3em;
+
+ position: relative;
+
+ &:before {
+ color: @icinga-blue;
+ content: "\e811";
+ font-family: 'ifont';
+
+ position: absolute;
+ left: 1.25em;
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ a {
+ color: @icinga-blue;
+ }
+
+ .message {
+ display: inline-block;
+ vertical-align: middle;
+ padding-right: 1.5em;
+ font-size: 7/6em;
+ }
+
+ p {
+ margin-bottom: 0;
+ }
+ }
+}
+
+.acknowledge-announcement-control,
+.application-state-acknowledge-message-control {
+ background: none;
+ border: none;
+ display: block;
+ margin-top: -0.75em;
+
+ position: absolute;
+ right: .75em;
+ top: 50%;
+}
+
+.application-state-acknowledge-message-control .link-button {
+ color: #fff;
+
+ &:hover .icon-cancel {
+ color: @icinga-blue;
+ }
+}
+
+#application-state-summary > div {
+ background-color: @color-critical;
+ color: @text-color-on-icinga-blue;
+ line-height: 1.5em;
+ padding: 0.5em 1em 0.5em 3em;
+
+ position: relative;
+
+ &:before {
+ content: "\e84d";
+ font-family: 'ifont';
+
+ position: absolute;
+ text-align: center;
+ left: .4em;
+ padding: .5em;
+ width: 3em;
+ top: 0;
+ }
+
+ > section {
+ margin-left: .5em;
+ }
+
+ > form .icon-cancel:before {
+ color: @text-color-on-icinga-blue;
+ }
+}
+
+.dashboard-link {
+ .clearfix();
+ display: block;
+ max-width: 100%;
+ vertical-align: middle;
+ padding: 1em;
+ width: 36em;
+
+ &:hover {
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+
+ -webkit-background-clip: padding-box;
+ -moz-background-clip: padding;
+ background-clip: padding-box;
+
+ -webkit-box-shadow: 0 0 0.5em 0 rgba(0, 0, 0, 0.2);
+ -moz-box-shadow: 0 0 0.5em 0 rgba(0, 0, 0, 0.2);
+ box-shadow: 0 0 0.5em 0 rgba(0, 0, 0, 0.2);
+
+ background-color: @tr-hover-color;
+ text-decoration: none;
+ }
+}
+
+.dashboard.content > .container {
+ overflow-x: auto;
+}
+
+.link-meta {
+ display: table-cell;
+ vertical-align: middle;
+}
+
+.link-label {
+ font-weight: @font-weight-bold;
+}
+
+.link-description {
+ color: @text-color-light;
+}
+
+.link-icon {
+ display: table-cell;
+ padding-right: .5em;
+ vertical-align: middle;
+ text-align: center;
+
+ > i {
+ font-size: 3em;
+ opacity: 0.7;
+ line-height: 1.5;
+
+ &:before {
+ min-width: 1.25em;
+ }
+ }
+
+ > img {
+ width: 3em;
+ height: 3em;
+ margin-right: .6em;
+ }
+}
+
+table.historycolorgrid {
+ font-size: 1.5em;
+}
+
+table.historycolorgrid th {
+ width: 1em;
+ height: 1em;
+ margin: 0.5em;
+ font-size: 0.55em;
+ font-weight: bold;
+}
+
+table.historycolorgrid td {
+ width: 1em;
+ height: 1em;
+ margin: 1em;
+}
+
+table.historycolorgrid td:hover {
+ opacity: 0.5;
+}
+
+table.historycolorgrid td.weekday {
+ font-size: 0.55em;
+ font-weight: bold;
+ width: 2.5em;
+ opacity: 1.0;
+}
+
+table.historycolorgrid a, table.historycolorgrid span {
+ .rounded-corners(0.2em);
+ margin: 0;
+ text-decoration: none;
+ display: block;
+ width: 1.1em;
+ height: 1.1em;
+}
+
+table.historycolorgrid a:hover {
+ text-decoration: none;
+}
+
+table.multiselect tr[href] td {
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+}
+
+#main div.filter {
+ .search.search-input {
+ width: 8em;
+ }
+
+ form.editor {
+ input[type=text], select {
+ width: 12em;
+ height: 2em;
+ line-height: 1;
+ }
+
+ ul.tree li.active {
+ background-color: @gray-lightest;
+ }
+
+ button {
+ padding: .5em;
+ border: none;
+ background: none;
+ color: @text-color;
+
+ &:hover, &:focus {
+ color: @icinga-blue;
+ }
+ }
+
+ .buttons {
+ margin-left: 25em;
+ padding: .25em 0;
+ }
+
+ .buttons input:not(:last-child) {
+ margin-right:.5em;
+ }
+
+ ul.tree {
+ .filter-operator {
+ width: 5em;
+ }
+
+ .new-filter {
+ background: #ffb;
+ }
+
+ .filter-rule {
+ width: 4em;
+ }
+ }
+ }
+}
+
+form.role-form {
+ &.icinga-form .control-label-group {
+ width: 20em;
+ }
+
+ .control-label-group em {
+ color: @text-color-light;
+ font-style: normal;
+ }
+
+ .unrestricted-role {
+ text-decoration: line-through;
+ }
+
+ .control-label > * {
+ display: inline-block;
+ }
+
+ summary {
+ border-bottom: 1px solid @gray-light;
+ .user-select(none);
+ cursor: pointer;
+
+ font-weight: @font-weight-bold;
+ margin: 0.556em 0 0.333em;
+ font-size: 1.167em;
+
+ display: flex;
+ align-items: baseline;
+ .privilege-preview {
+ flex: 1 1 auto;
+ }
+
+ > :first-child {
+ display: inline-block;
+ width: 20em / 1.167em; // element label width / summary font-size
+ }
+
+ > :nth-last-child(1),
+ > :nth-last-child(2) {
+ font-size: .75em;
+ opacity: .6;
+ }
+
+ .privilege-preview .icon {
+ &.granted {
+ color: @color-granted;
+ }
+
+ &.refused {
+ color: @color-refused;
+ }
+
+ &.restricted {
+ color: @color-restricted;
+ }
+ }
+ }
+
+ .collapsible {
+ summary em {
+ font-size: .857em;
+ font-weight: normal;
+ color: @text-color-light;
+ }
+
+ h4 {
+ display: inline-block;
+ width: 20em;
+ margin-top: 1.5em;
+ padding-right: .5625em;
+ text-align: right;
+
+ & ~ i {
+ display: inline-block;
+ width: 2.625em;
+ margin-right: 1em;
+ text-align: center;
+
+ &.icon-ok {
+ color: @color-granted;
+ }
+
+ &.icon-cancel {
+ color: @color-refused;
+ }
+ }
+ }
+ }
+}
+
+ul.tree select:first-of-type { /* ?? */
+ margin-bottom: 0.3em;
+ margin-left: 2em;
+}
+
+ul.tree {
+ padding: 0;
+ margin: 0;
+ padding-top: .5em;
+}
+
+ul.tree ul {
+ padding-left: 1em;
+}
+
+ul.tree li {
+ margin: 0;
+ list-style-type: none;
+ position: relative;
+ padding: 0;
+}
+
+ul.tree li .handle {
+ background-image: url('../img/tree/tree-minus.gif');
+ background-repeat: no-repeat;
+ display: inline-block;
+ position: absolute;
+ width: 1.5em;
+ height: 2em;
+ left: 0em;
+ background-position: center center;
+ z-index: 1;
+ cursor: pointer;
+}
+
+ul.tree li.collapsed > .handle {
+ background-image: url('../img/tree/tree-plus.gif');
+}
+
+ul.tree li.collapsed > ul {
+ display: none;
+}
+
+ul.tree li::before, ul.tree li::after {
+ content: '';
+ position: absolute;
+ right: auto;
+ left: -0.2em;
+ border-color: @gray-light;
+ border-style: dotted;
+ border-width: 0;
+}
+
+/* This is the left vertical line */
+ul.tree li::before {
+ border-left-width: 1px;
+ top: -.5em;
+ width: 1em;
+ height: 2.5em;
+ bottom: 1em;
+}
+
+/* This is the horizontal dash in front of each item */
+ul.tree li::after {
+ border-top-width: 1px;
+ top: 1em;
+ width: 2em;
+ height: 1em;
+}
+
+/* Stop left vertical line at "mid-height" after last nodes (at each level) */
+ul.tree li:last-child::before {
+ height: 1.5em;
+}
+
+/* No border for the root element - there must be only ONE root */
+ul.tree > li::before, ul.tree > li::after {
+ display: none;
+}
+
+/* No connector before (each) root element */
+ul.tree > ul > li::before, ul.tree > ul > li::after {
+ border: 0;
+}
+
+ul.tree li a {
+ display: inline-block;
+ line-height: 2em;
+ padding: 0 .5em;
+ text-decoration: none;
+ color: @gray;
+ background-repeat: no-repeat;
+ background-position: 0.8em 0.4em;
+}
+
+ul.tree li a.error {
+ color: @color-critical-handled;
+}
+
+ul.tree li a:hover {
+ color: @text-color;
+ text-decoration: underline;
+}
+
+ul.tree li a.error:hover {
+ color: @color-critical;
+}
+
+/* charts should grow as much as possible but never beyond the current viewport's size */
+.svg-container-responsive {
+ padding: 1.5em;
+ height: 80vh;
+}
+
+.tipsy .tipsy-inner {
+ // overwrite tooltip max width, we need them to grow bigger
+ font-family: @font-family;
+ font-size: @font-size-small;
+ max-width: 300px;
+ text-align: left;
+ background-color: rgba(0,0,0,0.8);
+}
+
+.progress-label span {
+ font-size: 1.5em;
+ .animate(blink 1.4s infinite both);
+
+ &:nth-child(2) {
+ animation-delay: .2s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: .4s;
+ }
+}
+
+.flyover:not(.flyover-expanded) .flyover-content {
+ display: none;
+}
+
+.flyover {
+ position: relative;
+
+ .flyover-content {
+ background-color: @body-bg-color;
+ border: 1px solid;
+ border-color: @gray-lighter;
+ box-shadow: 0 0 .5em 0 rgba(0, 0, 0, 0.2);
+ position: absolute;
+ padding: @vertical-padding @horizontal-padding;
+ .rounded-corners();
+ }
+
+ &.flyover-arrow-top .flyover-content:before {
+ background: @body-bg-color;
+ border-left: 1px solid @gray-lighter;
+ border-top: 1px solid @gray-lighter;
+ content: "";
+ height: 1em;
+ -ms-transform: rotate(45deg);
+ transform: rotate(45deg);
+ width: 1em;
+
+ position: absolute;
+ left: 6px;
+ top: -7px;
+ }
+
+ &.flyover-right .flyover-content {
+ left: auto;
+ right: 0;
+ }
+
+ &.flyover-arrow-top.flyover-right .flyover-content:before {
+ left: auto;
+ right: 6px;
+ }
+}
+
+.slice-state-ok {
+ stroke: @color-ok;
+ background: @color-ok;
+}
+
+.slice-state-warning-handled {
+ stroke: @color-warning-handled;
+ background: @color-warning-handled;
+}
+
+.slice-state-warning {
+ stroke: @color-warning;
+ background: @color-unreachable-handled;
+}
+
+.slice-state-critical-handled {
+ stroke: @color-critical-handled;
+ background: @color-critical-handled;
+}
+
+.slice-state-critical {
+ stroke: @color-critical;
+ background: @color-critical;
+}
+
+.slice-state-unknown-handled {
+ stroke: @color-unknown-handled;
+ background: @color-unknown-handled;
+}
+
+.slice-state-unknown {
+ stroke: @color-unknown;
+ background: @color-unknown;
+}
+
+.slice-state-unreachable-handled {
+ stroke: @color-unreachable-handled;
+ background: @color-unreachable-handled;
+}
+
+.slice-state-unreachable {
+ stroke: @color-unreachable;
+ background: @color-unreachable;
+}
+
+.slice-state-pending {
+ stroke: @color-pending;
+ background: @color-pending;
+}
+
+.slice-state-not-checked {
+ stroke: @gray-light;
+ background: @gray-light;
+}
+
+.donut {
+ width: 22em;
+ height: 22em;
+ min-width: 11.5em;
+ display: table;
+}
+
+.donut-graph {
+ width: 22em;
+ height: 22em;
+}
+
+.donut-label {
+ font-weight: bold;
+ fill: @text-color;
+}
+
+.donut-label {
+ margin-top: -12.5em;
+ text-align: center;
+}
+
+.donut-label-big {
+ color: @gray-light;
+ .fg-stateful();
+ font-size: 6em;
+ line-height: 0;
+ text-anchor: middle;
+ &:hover {
+ text-decoration: none;
+ }
+}
+
+.donut-label-small {
+ fill: @text-color;
+ font-size: 1.2em;
+ text-anchor: middle;
+ -moz-transform: translateY(0.35em);
+ -ms-transform: translateY(0.35em);
+ -webkit-transform: translateY(0.35em);
+ transform: translateY(0.35em);
+}
+
+.donut-container {
+ float: left;
+
+ &:not(:last-of-type) {
+ margin-right: 10em;
+ }
+}
+
+.dashboard .donut-container .donut-legend {
+ margin-left: auto;
+}
+
+.donut-legend {
+ width: 50%;
+ padding: 0;
+ margin-left: 18em;
+ list-style-type: none;
+
+ li {
+ vertical-align: middle;
+
+ &:not(:last-child) {
+ margin-bottom: .5em;
+ }
+
+ .badge {
+ font-weight: bold;
+ margin-right: .5em;
+ vertical-align: initial;
+ }
+ }
+}
+
+html.no-js .progress-label {
+ display: none;
+}
+
diff --git a/public/css/modes/light.less b/public/css/modes/light.less
new file mode 100644
index 0000000..7044007
--- /dev/null
+++ b/public/css/modes/light.less
@@ -0,0 +1,4 @@
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+@enable-color-preference: 999999px;
+@prefer-light-color-scheme: 0px;
diff --git a/public/css/modes/none.less b/public/css/modes/none.less
new file mode 100644
index 0000000..05c618c
--- /dev/null
+++ b/public/css/modes/none.less
@@ -0,0 +1,4 @@
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+@enable-color-preference: 999999px;
+@prefer-light-color-scheme: 999999px;
diff --git a/public/css/modes/system.less b/public/css/modes/system.less
new file mode 100644
index 0000000..07bd06a
--- /dev/null
+++ b/public/css/modes/system.less
@@ -0,0 +1,4 @@
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+@enable-color-preference: 0px;
+@prefer-light-color-scheme: 999999px;
diff --git a/public/css/pdf/pdfprint.less b/public/css/pdf/pdfprint.less
new file mode 100644
index 0000000..2c68d37
--- /dev/null
+++ b/public/css/pdf/pdfprint.less
@@ -0,0 +1,103 @@
+/*! Icinga Web 2 | (c) 2014 Icinga GmbH | GPLv2+ */
+
+// Ensure styling is light, exports use a white background
+
+@gray: #7F7F7F;
+@gray-semilight: #A9A9A9;
+@gray-light: #C9C9C9;
+@gray-lighter: #EEEEEE;
+@gray-lightest: #F7F7F7;
+@icinga-blue: #0095BF;
+@low-sat-blue: #dae3e6;
+@low-sat-blue-dark: #becbcf;
+@body-bg-color: #fff;
+@text-color: @black;
+@text-color-light: @gray;
+@tr-active-color: @body-bg-color;
+@tr-hover-color: @body-bg-color;
+
+// Page layout
+
+@page {
+ margin: 1cm;
+}
+
+body {
+ font-family: sans-serif;
+ margin: 0;
+ padding-top: 37px; // ~ logo height in the header
+}
+
+.content {
+ font-size: 9pt;
+}
+
+#header,
+#footer {
+ position: fixed;
+ left: 0;
+ right: 0;
+ color: #aaa;
+ font-size: 0.9em;
+}
+
+#header {
+ top: 0;
+ border-bottom: 0.1pt solid #aaa;
+
+ .title {
+ text-align: left;
+ }
+
+ img {
+ margin-bottom: 3px;
+ }
+}
+
+#footer {
+ bottom: 0;
+ padding-top: 2em;
+}
+
+.content table {
+ margin-bottom: 3em;
+}
+
+#header table,
+#footer table {
+ width: 100%;
+ border-collapse: collapse;
+ border: none;
+}
+
+#header td,
+#header th,
+#footer td,
+#footer th {
+ padding: 0;
+ width: 50%;
+}
+
+.page-number {
+ padding-top: 0.5em;
+ border-top: 0.1pt solid #aaa;
+ text-align: center;
+}
+
+.page-number:before {
+ content: "Page " counter(page);
+}
+
+hr {
+ page-break-after: always;
+ border: 0;
+}
+
+// General style
+.state-icons,
+.overview-performance-data,
+.controls,
+.dontprint, // Compat only, use dont-print instead
+.dont-print {
+ display: none !important;
+}
diff --git a/public/css/themes/Winter.less b/public/css/themes/Winter.less
new file mode 100644
index 0000000..2edf4f8
--- /dev/null
+++ b/public/css/themes/Winter.less
@@ -0,0 +1,30 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+@icinga-blue: #2b95ff;
+@control-color: @icinga-blue;
+
+.letitsnow {
+ background-image: url('../img/winter/snow1.png'), url('../img/winter/snow2.png'), url('../img/winter/snow3.png');
+ animation: ~"snow" 10s linear infinite;
+}
+
+#header-logo {
+ background-image: url('../img/winter/logo_icinga_big_winter.png');
+}
+
+/* Snow, from http://codepen.io/NickyCDK/pen/AIonk */
+#login, #header-logo-container, #main > .container > .controls > .tabs {
+ .letitsnow()
+}
+
+@keyframes ~"snow" {
+ 0% {background-position: 0px 0px, 0px 0px, 0px 0px;}
+ 50% {background-position: 500px 500px, 100px 200px, -100px 150px;}
+ 100% {background-position: 500px 1000px, 200px 400px, -100px 300px;}
+}
+
+#menu ul.nav-level-1 > .nav-item {
+ &:focus, &:hover {
+ .letitsnow()
+ }
+}
diff --git a/public/css/themes/colorblind.less b/public/css/themes/colorblind.less
new file mode 100644
index 0000000..c6df585
--- /dev/null
+++ b/public/css/themes/colorblind.less
@@ -0,0 +1,29 @@
+/*! Icinga Web 2 | (c) 2019 Icinga Development Team | GPLv2+ */
+
+@color-ok: fade(#77E08E, 25%);
+@color-critical: #FE5566;
+@color-critical-handled: fade(@color-critical, 33%);
+@color-warning: #B0A029;
+@color-warning-handled: fade(@color-warning, 33%);
+@color-unknown: #7791E0;
+@color-unknown-handled: fade(@color-unknown, 50%);
+@color-unreachable: @color-unknown;
+@color-unreachable-handled: @color-unknown-handled;
+@color-pending: fade(#FFFFFF, 75%);
+
+/* Adapt font color to match handled / unhandled states */
+.badge,
+.state-badge {
+ font-weight: bold;
+ color: @text-color !important;
+
+ &.handled,
+ &.state-up,
+ &.state-ok {
+ color: fade(@text-color, 75%) !important;
+ }
+}
+
+.processinfo .process > div.backend-running {
+ color: @text-color;
+}
diff --git a/public/css/themes/high-contrast.less b/public/css/themes/high-contrast.less
new file mode 100644
index 0000000..b22ffd5
--- /dev/null
+++ b/public/css/themes/high-contrast.less
@@ -0,0 +1,250 @@
+/*! Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+@icinga-blue: #006D8C;
+
+// Gray colors
+@gray: #7F7F7F;
+@gray-semilight: #A9A9A9;
+@gray-light: #C9C9C9;
+@gray-lighter: #EEEEEE;
+@gray-lightest: #F7F7F7;
+@disabled-gray: #9a9a9a;
+
+// State colors
+@color-ok: #006400;
+@color-critical: #EE0000;
+@color-critical-handled: #EE0000;
+@color-warning: #8B5A00;
+@color-warning-handled: #8B5A00;
+@color-unknown: #800080;
+@color-unknown-handled: #800080;
+@color-unreachable: #800080;
+@color-unreachable-handled: #800080;
+@color-pending: #0000EE;
+
+// Icinga colors
+@low-sat-blue: #dae3e6;
+@low-sat-blue-dark: #c0cccd;
+
+// Background color for <body>
+@body-bg-color: @white;
+
+@text-color: #191919;
+@text-color-light: #555555;
+
+@menu-highlight-color: white;
+@menu-2ndlvl-color: white;
+@menu-2ndlvl-highlight-color: white;
+@menu-2ndlvl-active-hover-color: @text-color;
+
+#menu ul.nav-level-1 > .nav-item > a {
+ &:focus, &:hover {
+ text-decoration: underline;
+ }
+}
+
+#menu ul.nav-level-1 > .nav-item.active > a,
+#menu .nav-level-1 > .nav-item.active:not(.selected) > a:hover {
+ color: @text-color;
+ background-color: @white;
+}
+
+#menu .nav-level-2 > .nav-item.active {
+ background-color: @white;
+
+ a {
+ color: @text-color;
+ }
+
+ > a:focus, > a:hover {
+ opacity: 1;
+ }
+}
+
+#menu .nav-level-2 > .nav-item > a {
+ &:hover, &:focus {
+ text-decoration: underline;
+ }
+}
+
+#menu ul:not(.nav-level-2) > .selected > a {
+ color: @text-color;
+}
+
+#menu .active > a {
+ text-decoration: underline;
+}
+
+.badge:not(.handled),
+.state-badge:not(.handled) {
+ &.state-warning {
+ border: 1px solid @color-warning;
+ }
+
+ &.state-critical,
+ &.state-down {
+ border: 1px solid @color-critical;
+ }
+
+ &.state-unreachable {
+ border: 1px solid @color-unreachable;
+ }
+
+ &.state-unknown {
+ border: 1px solid @color-unknown;
+ }
+
+ &.state-ok,
+ &.state-up {
+ border: 1px solid @color-ok;
+ }
+}
+
+.badge.handled,
+.badge.state-ok,
+.state-badge.handled,
+.state-badge.state-ok {
+ background-color: @body-bg-color !important;
+ color: @text-color !important;
+
+ &.state-warning {
+ border: 1px solid @color-warning-handled;
+ }
+
+ &.state-critical,
+ &.state-down {
+ border: 1px solid @color-critical-handled;
+ }
+
+ &.state-unreachable {
+ border: 1px solid @color-unreachable-handled;
+ }
+
+ &.state-unknown {
+ border: 1px solid @color-unknown-handled;
+ }
+}
+
+.boxview a:focus {
+ color: @text-color;
+ text-decoration: underline;
+}
+
+.icinga-module.module-monitoring {
+ @timeline-notification-color: #1650CF;
+ @timeline-hard-state-color: #A24600;
+ @timeline-comment-color: #346964;
+ @timeline-ack-color: #855D18;
+ @timeline-downtime-start-color: #515151;
+ @timeline-downtime-end-color: #5e5e2f;
+
+ // Unfortunately it does not suffice to only override the timeline colors here, because our less compiler seems to
+ // have the related style block in module.less already evaluated
+
+ .timeline-notification {
+ background-color: @timeline-notification-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-notification-color, 20%);
+ }
+ }
+
+ .timeline-hard-state {
+ background-color: @timeline-hard-state-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-hard-state-color, 20%);
+ }
+ }
+
+ .timeline-comment {
+ background-color: @timeline-comment-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-comment-color, 20%);
+ }
+ }
+
+ .timeline-ack {
+ background-color: @timeline-ack-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-ack-color, 20%);
+ }
+ }
+
+ .timeline-downtime-start {
+ background-color: @timeline-downtime-start-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-downtime-start-color, 20%);
+ }
+ }
+
+ .timeline-downtime-end {
+ background-color: @timeline-downtime-end-color;
+
+ &.extrapolated {
+ background-color: lighten(@timeline-downtime-end-color, 20%);
+ }
+ }
+}
+
+.icinga-controls {
+ input:not([type="checkbox"]):not([type="radio"]),
+ .toggle-switch .toggle-slider:before,
+ .toggle-switch > .toggle-slider,
+ select,
+ textarea {
+ border: 1px solid @icinga-blue;
+ }
+
+ input[type="checkbox"]:not(:checked) + .toggle-switch .toggle-slider:before {
+ height: 1.166666667em;
+ width: 1.166666667em;
+ margin: 1px;
+ }
+}
+
+.search-suggestions {
+ input:not([type="checkbox"]):not([type="radio"]),
+ .toggle-switch .toggle-slider:before,
+ .toggle-switch > .toggle-slider,
+ select,
+ textarea {
+ border: none;
+ }
+}
+
+.icinga-module.module-icingadb .list-item.overdue {
+ background: none;
+
+ header > *:not(time),
+ .caption {
+ opacity: 1;
+ }
+}
+
+.controls input.search,
+input.search {
+ background-image: url(../img/icons/search.png);
+}
+
+.search-bar,
+.button-link,
+.view-mode-switcher > label {
+ border: 1px solid @icinga-blue;
+}
+
+// compensate for 1px border
+.filter-input-area {
+ padding: 1/12em !important;
+}
+
+.view-mode-switcher > label {
+ padding: (10/16)*.25em (10/16)*.5em !important;
+
+ &:not(:first-of-type) {
+ border-left: none;
+ }
+}
diff --git a/public/css/vendor/normalize.css b/public/css/vendor/normalize.css
new file mode 100644
index 0000000..458eea1
--- /dev/null
+++ b/public/css/vendor/normalize.css
@@ -0,0 +1,427 @@
+/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ * user zoom.
+ */
+
+html {
+ font-family: sans-serif; /* 1 */
+ -ms-text-size-adjust: 100%; /* 2 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11
+ * and Firefox.
+ * Correct `block` display not defined for `main` in IE 11.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/**
+ * 1. Correct `inline-block` display not defined in IE 8/9.
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+ */
+
+audio,
+canvas,
+progress,
+video {
+ display: inline-block; /* 1 */
+ vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9/10.
+ * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
+ */
+
+[hidden],
+template {
+ display: none;
+}
+
+/* Links
+ ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9/10.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow not hidden in IE 9/10/11.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari.
+ */
+
+figure {
+ margin: 1em 40px;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Contain overflow in all browsers.
+ */
+
+pre {
+ overflow: auto;
+}
+
+/**
+ * Address odd `em`-unit font size rendering in all browsers.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
+ * styling of `select`, unless a `border` property is set.
+ */
+
+/**
+ * 1. Correct color not being inherited.
+ * Known issue: affects color of disabled elements.
+ * 2. Correct font properties not being inherited.
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ color: inherit; /* 1 */
+ font: inherit; /* 2 */
+ margin: 0; /* 3 */
+}
+
+/**
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
+ */
+
+button {
+ overflow: visible;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+ * Correct `select` style inheritance in Firefox.
+ */
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+ cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+input {
+ line-height: normal;
+}
+
+/**
+ * It's recommended that you don't attempt to style these elements.
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
+ *
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
+ * `font-size` values of the `input`, it causes the cursor style of the
+ * decrement button to change from `default` to `text`.
+ */
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
+ * (include `-moz` to future-proof).
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box; /* 2 */
+ box-sizing: content-box;
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
+ * Safari (but not Chrome) clips the cancel button when the search input has
+ * padding (and `textfield` appearance).
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Remove default vertical scrollbar in IE 8/9/10/11.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * Don't inherit the `font-weight` (applied by a rule above).
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+ */
+
+optgroup {
+ font-weight: bold;
+}
+
+/* Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+td,
+th {
+ padding: 0;
+}
diff --git a/public/error_norewrite.html b/public/error_norewrite.html
new file mode 100644
index 0000000..d85a002
--- /dev/null
+++ b/public/error_norewrite.html
@@ -0,0 +1 @@
+<h1>The rewrite module is not enabled</h1>
diff --git a/public/error_unavailable.html b/public/error_unavailable.html
new file mode 100644
index 0000000..1c2df0e
--- /dev/null
+++ b/public/error_unavailable.html
@@ -0,0 +1,7 @@
+<h1>Backend unavailable</h1>
+<p>
+ It seems that the PHP FPM service is not running. Make sure to start PHP FPM service in order to access Icinga Web 2.
+ If you upgraded Icinga Web 2 recently, make sure to read the
+ <a href="https://icinga.com/docs/icingaweb2/latest/doc/02-Installation/">docs regarding PHP FPM</a>,
+ also locally available under <code>/usr/share/icingaweb2/doc/02-Installation.md</code>.
+</p>
diff --git a/public/font/ifont.eot b/public/font/ifont.eot
new file mode 100644
index 0000000..3a9092f
--- /dev/null
+++ b/public/font/ifont.eot
Binary files differ
diff --git a/public/font/ifont.svg b/public/font/ifont.svg
new file mode 100644
index 0000000..2747b47
--- /dev/null
+++ b/public/font/ifont.svg
@@ -0,0 +1,286 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>Copyright (C) 2018 by original authors @ fontello.com</metadata>
+<defs>
+<font id="ifont" horiz-adv-x="1000" >
+<font-face font-family="ifont" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
+<missing-glyph horiz-adv-x="1000" />
+<glyph glyph-name="dashboard" unicode="&#xe800;" d="M286 154v-108q0-22-16-37t-38-16h-178q-23 0-38 16t-16 37v108q0 22 16 38t38 15h178q23 0 38-15t16-38z m0 285v-107q0-22-16-38t-38-15h-178q-23 0-38 15t-16 38v107q0 23 16 38t38 16h178q23 0 38-16t16-38z m357-285v-108q0-22-16-37t-38-16h-178q-23 0-38 16t-16 37v108q0 22 16 38t38 15h178q23 0 38-15t16-38z m-357 571v-107q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v107q0 22 16 38t38 16h178q23 0 38-16t16-38z m357-286v-107q0-22-16-38t-38-15h-178q-23 0-38 15t-16 38v107q0 23 16 38t38 16h178q23 0 38-16t16-38z m357-285v-108q0-22-16-37t-38-16h-178q-22 0-38 16t-16 37v108q0 22 16 38t38 15h178q23 0 38-15t16-38z m-357 571v-107q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v107q0 22 16 38t38 16h178q23 0 38-16t16-38z m357-286v-107q0-22-16-38t-38-15h-178q-22 0-38 15t-16 38v107q0 23 16 38t38 16h178q23 0 38-16t16-38z m0 286v-107q0-22-16-38t-38-16h-178q-22 0-38 16t-16 38v107q0 22 16 38t38 16h178q23 0 38-16t16-38z" horiz-adv-x="1000" />
+
+<glyph glyph-name="user" unicode="&#xe801;" d="M714 69q0-60-35-104t-84-44h-476q-49 0-84 44t-35 104q0 48 5 90t17 85 33 73 52 50 76 19q73-72 174-72t175 72q42 0 75-19t52-50 33-73 18-85 4-90z m-143 495q0-88-62-151t-152-63-151 63-63 151 63 152 151 63 152-63 62-152z" horiz-adv-x="714.3" />
+
+<glyph glyph-name="users" unicode="&#xe802;" d="M331 350q-90-3-148-71h-75q-45 0-77 22t-31 66q0 197 69 197 4 0 25-11t54-24 66-12q38 0 75 13-3-21-3-37 0-78 45-143z m598-356q0-66-41-105t-108-39h-488q-68 0-108 39t-41 105q0 30 2 58t8 61 14 61 24 54 35 45 48 30 62 11q6 0 24-12t41-26 59-27 76-12 75 12 60 27 41 26 24 12q34 0 62-11t47-30 35-45 24-54 15-61 8-61 2-58z m-572 713q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z m393-214q0-89-63-152t-151-62-152 62-63 152 63 151 152 63 151-63 63-151z m321-126q0-43-31-66t-77-22h-75q-57 68-147 71 45 65 45 143 0 16-3 37 37-13 74-13 33 0 67 12t54 24 24 11q69 0 69-197z m-71 340q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="ok" unicode="&#xe803;" d="M352-10l-334 333 158 160 176-174 400 401 159-160z" horiz-adv-x="928" />
+
+<glyph glyph-name="cancel" unicode="&#xe804;" d="M799 116l-156-157-234 235-235-235-156 157 234 234-234 234 156 157 235-235 234 235 156-157-234-234z" horiz-adv-x="817" />
+
+<glyph glyph-name="plus" unicode="&#xe805;" d="M911 462l0-223-335 0 0-336-223 0 0 336-335 0 0 223 335 0 0 335 223 0 0-335 335 0z" horiz-adv-x="928" />
+
+<glyph glyph-name="minus" unicode="&#xe806;" d="M18 239l0 223 893 0 0-223-893 0z" horiz-adv-x="928" />
+
+<glyph glyph-name="folder-empty" unicode="&#xe807;" d="M464 685l447 0 0-669q0-47-33-80t-79-33l-669 0q-46 0-79 33t-33 80l0 781 446 0 0-112z m-334 0l0-223 669 0 0 112-446 0 0 111-223 0z m669-669l0 335-669 0 0-335 669 0z" horiz-adv-x="928" />
+
+<glyph glyph-name="download" unicode="&#xe808;" d="M714 100q0 15-10 25t-25 11-25-11-11-25 11-25 25-11 25 11 10 25z m143 0q0 15-10 25t-26 11-25-11-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-37t-38-16h-821q-23 0-38 16t-16 37v179q0 22 16 38t38 16h259l75-76q33-32 76-32t76 32l76 76h259q22 0 38-16t16-38z m-182 318q10-23-8-39l-250-250q-10-11-25-11t-25 11l-250 250q-17 16-8 39 10 21 33 21h143v250q0 15 11 25t25 11h143q14 0 25-11t10-25v-250h143q24 0 33-21z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="upload" unicode="&#xe809;" d="M714 29q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m143 0q0 14-10 25t-26 10-25-10-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-38t-38-16h-821q-23 0-38 16t-16 38v179q0 22 16 38t38 15h238q12-31 39-51t62-20h143q34 0 61 20t40 51h238q22 0 38-15t16-38z m-182 361q-9-22-33-22h-143v-250q0-15-10-25t-25-11h-143q-15 0-25 11t-11 25v250h-143q-23 0-33 22-9 22 8 39l250 250q10 10 25 10t25-10l250-250q18-17 8-39z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="git" unicode="&#xe80a;" d="M332 5q0 56-92 56-88 0-88-58 0-57 96-57 84 0 84 59z m-33 422q0 34-17 56t-49 23q-69 0-69-81 0-75 69-75 66 0 66 77z m150 180v-112q-20-7-44-13 9-24 9-47 0-70-41-120t-110-63q-22-5-33-15t-11-33q0-17 13-28t32-18 44-12 48-15 44-21 32-35 13-55q0-170-203-170-38 0-72 7t-65 23-49 46-18 71q0 92 102 125v3q-38 22-38 70 0 61 35 76v3q-40 13-66 60t-27 93q0 77 53 129t131 51q54 0 100-26 54 0 121 26z m178-491h-124q2 25 2 74v340q0 53-2 72h124q-3-19-3-69v-343q0-49 3-74z m335 124v-110q-40-22-97-22-35 0-60 12t-39 27-22 44-10 51-2 58v196h1v2q-4 0-11 0t-10 1q-12 0-33-3v106h54v42q0 30-4 50h127q-3-23-3-92h95v-106q-8 0-24 1t-24 1h-47v-204q0-73 48-73 34 0 61 19z m-321 528q0-32-22-57t-54-24q-32 0-54 24t-23 57q0 33 22 57t55 25q33 0 54-25t22-57z" horiz-adv-x="1000" />
+
+<glyph glyph-name="cubes" unicode="&#xe80b;" d="M357-61l214 107v176l-214-92v-191z m-36 254l226 96-226 97-225-97z m608-254l214 107v176l-214-92v-191z m-36 254l225 96-225 97-226-97z m-250 163l214 92v149l-214-92v-149z m-36 212l246 105-246 106-246-106z m607-289v-233q0-20-10-37t-29-26l-250-125q-14-8-32-8t-32 8l-250 125q-2 1-4 2-1-1-4-2l-250-125q-14-8-32-8t-31 8l-250 125q-19 9-29 26t-11 37v233q0 21 12 39t32 26l242 104v223q0 22 12 40t31 26l250 107q13 6 28 6t28-6l250-107q20-9 32-26t12-40v-223l242-104q20-8 32-26t11-39z" horiz-adv-x="1285.7" />
+
+<glyph glyph-name="database" unicode="&#xe80c;" d="M429 421q132 0 247 24t181 71v-95q0-38-57-71t-157-52-214-19-215 19-156 52-58 71v95q66-47 181-71t248-24z m0-428q132 0 247 24t181 71v-95q0-39-57-72t-157-52-214-19-215 19-156 52-58 72v95q66-47 181-71t248-24z m0 214q132 0 247 24t181 71v-95q0-38-57-71t-157-52-214-20-215 20-156 52-58 71v95q66-47 181-71t248-24z m0 643q116 0 214-19t157-52 57-72v-71q0-39-57-72t-157-52-214-19-215 19-156 52-58 72v71q0 39 58 72t156 52 215 19z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="gauge" unicode="&#xe80d;" d="M214 207q0 30-21 51t-50 21-51-21-21-51 21-50 51-21 50 21 21 50z m107 250q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m239-268l57 213q3 14-5 27t-21 16-27-3-17-22l-56-213q-33-3-60-25t-35-55q-11-43 11-81t66-50 81 11 50 66q9 33-4 65t-40 51z m369 18q0 30-21 51t-51 21-50-21-21-51 21-50 50-21 51 21 21 50z m-358 357q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m250-107q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m179-250q0-145-79-269-10-17-30-17h-782q-20 0-30 17-79 123-79 269 0 102 40 194t106 160 160 107 194 39 194-39 160-107 106-160 40-194z" horiz-adv-x="1000" />
+
+<glyph glyph-name="sitemap" unicode="&#xe80e;" d="M1000 154v-179q0-22-16-38t-38-16h-178q-22 0-38 16t-16 38v179q0 22 16 38t38 15h53v107h-285v-107h53q23 0 38-15t16-38v-179q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v179q0 22 16 38t38 15h53v107h-285v-107h53q23 0 38-15t16-38v-179q0-22-16-38t-38-16h-178q-23 0-38 16t-16 38v179q0 22 16 38t38 15h53v107q0 29 21 51t51 21h285v107h-53q-23 0-38 16t-16 37v179q0 22 16 38t38 16h178q23 0 38-16t16-38v-179q0-22-16-37t-38-16h-53v-107h285q29 0 51-21t21-51v-107h53q23 0 38-15t16-38z" horiz-adv-x="1000" />
+
+<glyph glyph-name="sort-name-up" unicode="&#xe80f;" d="M665 622h98l-40 122-6 26q-2 9-2 11h-2l-1-11q0 0-2-10t-5-16z m-254-576q0-6-6-13l-178-178q-5-5-13-5-6 0-12 5l-179 179q-8 9-4 19 4 11 17 11h107v768q0 8 5 13t13 5h107q8 0 13-5t5-13v-768h107q8 0 13-5t5-13z m466-66v-130h-326v50l206 295q7 11 12 16l6 5v1q-1 0-3 0t-5 0q-6-2-16-2h-130v-64h-67v128h317v-50l-206-296q-4-4-12-14l-6-7v-1l8 1q5 2 16 2h139v66h67z m50 501v-60h-161v60h42l-26 80h-136l-26-80h42v-60h-160v60h39l128 369h91l128-369h39z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="sort-name-down" unicode="&#xe810;" d="M665 51h98l-40 122-6 26q-2 9-2 11h-2l-1-11q0-1-2-10t-5-16z m-254-5q0-6-6-13l-178-178q-5-5-13-5-6 0-12 5l-179 179q-8 9-4 19 4 11 17 11h107v768q0 8 5 13t13 5h107q8 0 13-5t5-13v-768h107q8 0 13-5t5-13z m516-137v-59h-161v59h42l-26 80h-136l-26-80h42v-59h-160v59h39l128 370h91l128-370h39z m-50 643v-131h-326v51l206 295q7 10 12 15l6 5v2q-1 0-3-1t-5 0q-6-2-16-2h-130v-64h-67v128h317v-50l-206-295q-4-5-12-15l-6-5v-2l8 2q5 0 16 0h139v67h67z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="megaphone" unicode="&#xe811;" d="M929 493q29 0 50-21t21-51-21-50-50-21v-214q0-29-22-50t-50-22q-233 194-453 212-32-10-51-36t-17-57 22-51q-11-19-13-37t4-32 19-31 26-28 35-28q-17-32-63-46t-94-7-73 31q-4 13-17 49t-18 53-12 50-9 56 2 55 12 62h-68q-36 0-63 26t-26 63v107q0 37 26 63t63 26h268q243 0 500 215 29 0 50-22t22-50v-214z m-72-337v532q-220-168-428-191v-151q210-23 428-190z" horiz-adv-x="1000" />
+
+<glyph glyph-name="bug" unicode="&#xe812;" d="M911 314q0-14-11-25t-25-10h-125q0-96-37-162l116-117q10-11 10-25t-10-25q-10-11-25-11t-25 11l-111 110q-3-3-8-7t-24-16-36-21-46-16-54-7v500h-71v-500q-29 0-57 7t-49 19-36 22-25 18l-8 8-102-116q-11-12-27-12-13 0-24 9-11 10-11 25t8 26l113 127q-32 63-32 153h-125q-15 0-25 10t-11 25 11 25 25 11h125v164l-97 97q-11 10-11 25t11 25 25 10 25-10l97-97h471l96 97q11 10 25 10t26-10 10-25-10-25l-97-97v-164h125q15 0 25-11t11-25z m-268 322h-357q0 74 52 126t126 52 127-52 52-126z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="tasks" unicode="&#xe813;" d="M571 64h358v72h-358v-72z m-214 286h572v71h-572v-71z m357 286h215v71h-215v-71z m286-465v-142q0-15-11-25t-25-11h-928q-15 0-25 11t-11 25v142q0 15 11 26t25 10h928q15 0 25-10t11-26z m0 286v-143q0-14-11-25t-25-10h-928q-15 0-25 10t-11 25v143q0 15 11 25t25 11h928q15 0 25-11t11-25z m0 286v-143q0-14-11-25t-25-11h-928q-15 0-25 11t-11 25v143q0 14 11 25t25 11h928q15 0 25-11t11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="filter" unicode="&#xe814;" d="M783 685q9-22-8-39l-275-275v-414q0-23-22-33-7-3-14-3-15 0-25 11l-143 143q-10 11-10 25v271l-275 275q-18 17-8 39 9 22 33 22h714q23 0 33-22z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="off" unicode="&#xe815;" d="M857 350q0-87-34-166t-91-137-137-92-166-34-167 34-136 92-92 137-34 166q0 102 45 191t126 151q24 18 54 14t46-28q18-23 14-53t-28-47q-54-41-84-101t-30-127q0-58 23-111t61-91 91-61 111-23 110 23 92 61 61 91 22 111q0 68-30 127t-84 101q-23 18-28 47t14 53q17 24 47 28t53-14q81-61 126-151t45-191z m-357 429v-358q0-29-21-50t-50-21-51 21-21 50v358q0 29 21 50t51 21 50-21 21-50z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="book" unicode="&#xe816;" d="M915 583q22-31 10-72l-154-505q-10-36-42-60t-69-25h-515q-43 0-83 30t-55 74q-14 37-1 71 0 2 1 15t3 20q0 5-2 12t-2 11q1 6 5 12t9 13 9 13q13 21 25 51t17 51q2 6 0 17t0 16q2 6 9 15t10 13q12 20 23 51t14 51q1 5-1 17t0 16q2 7 12 17t13 13q10 14 23 47t16 54q0 4-2 14t-1 15q1 4 5 10t10 13 10 11q4 7 9 17t8 20 9 20 11 18 15 13 20 6 26-3l0-1q21 5 28 5h425q41 0 64-32t10-72l-153-506q-20-66-40-85t-72-20h-485q-15 0-21-8-6-9-1-24 14-39 81-39h515q16 0 31 9t20 23l167 550q4 13 3 32 21-8 33-24z m-594-1q-2-7 1-12t11-6h339q8 0 15 6t9 12l12 36q2 7-1 12t-12 6h-339q-7 0-14-6t-9-12z m-46-143q-3-7 1-12t11-6h339q7 0 14 6t10 12l11 36q3 7-1 13t-11 5h-339q-7 0-14-5t-10-13z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="paste" unicode="&#xe817;" d="M429-79h500v358h-233q-22 0-37 15t-16 38v232h-214v-643z m142 804v36q0 7-5 12t-12 6h-393q-7 0-13-6t-5-12v-36q0-7 5-13t13-5h393q7 0 12 5t5 13z m143-375h167l-167 167v-167z m286-71v-375q0-23-16-38t-38-16h-535q-23 0-38 16t-16 38v89h-303q-23 0-38 16t-16 37v750q0 23 16 38t38 16h607q22 0 38-16t15-38v-183q12-7 20-15l228-228q16-15 27-42t11-49z" horiz-adv-x="1000" />
+
+<glyph glyph-name="scissors" unicode="&#xe818;" d="M536 350q14 0 25-11t10-25-10-25-25-10-25 10-11 25 11 25 25 11z m167-36l283-222q16-11 14-31-3-20-19-28l-72-36q-7-4-16-4-10 0-17 4l-385 216-62-36q-4-3-7-3 8-28 6-54-4-43-31-83t-74-69q-74-47-154-47-76 0-124 44-51 47-44 116 4 42 31 82t73 69q74 47 155 47 46 0 84-18 5 8 13 13l68 40-68 41q-8 5-13 12-38-17-84-17-81 0-155 47-46 30-73 69t-31 82q-3 33 8 63t36 52q47 44 124 44 80 0 154-47 46-29 74-68t31-83q2-27-6-54 3-1 7-3l62-37 385 216q7 5 17 5 9 0 16-4l72-36q16-9 19-28 2-20-14-32z m-380 145q26 24 12 61t-59 65q-52 33-107 33-42 0-63-20-26-24-12-60t59-66q51-33 107-33 41 0 63 20z m-47-415q45 28 59 65t-12 60q-22 20-63 20-56 0-107-33-45-28-59-65t12-60q21-20 63-20 55 0 107 33z m99 342l54-33v7q0 20 18 31l8 4-44 26-15-14q-1-2-5-6t-7-7q-1-1-2-2t-2-1z m125-125l54-18 410 321-71 36-429-240v-64l-89-53 5-5q1-1 4-3 2-2 6-7t6-6l15-15z m393-232l71 35-290 228-99-77q-1-2-7-4z" horiz-adv-x="1000" />
+
+<glyph glyph-name="globe" unicode="&#xe819;" d="M429 779q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m153-291q-2-1-6-5t-7-6q1 0 2 3t3 6 2 4q3 4 12 8 8 4 29 7 19 5 29-6-1 1 5 7t8 7q2 1 8 3t9 4l1 12q-7-1-10 4t-3 12q0-2-4-5 0 4-2 5t-7-1-5-1q-5 2-8 5t-5 9-2 8q-1 3-5 6t-5 6q-1 1-2 3t-1 4-3 3-3 1-4-3-4-5-2-3q-2 1-4 1t-2-1-3-1-3-2q-1-2-4-2t-5-1q8 3-1 6-5 2-9 2 6 2 5 6t-5 8h3q-1 2-5 5t-10 5-7 3q-5 3-19 5t-18 1q-3-4-3-6t2-8 2-7q1-3-3-7t-3-7q0-4 7-9t6-12q-2-4-9-9t-9-6q-3-5-1-11t6-9q1-1 1-2t-2-3-3-2-4-2l-1-1q-7-3-12 3t-7 15q-4 14-9 17-13 4-16-1-3 7-23 15-14 5-33 2 4 0 0 8-4 9-10 7 1 3 2 10t0 7q2 8 7 13 1 1 4 5t5 7 1 4q19-3 28 6 2 3 6 9t6 10q5 3 8 3t8-3 8-3q8-1 8 6t-4 11q7 0 2 10-2 4-5 5-6 2-15-3-4-2 2-4-1 0-6-6t-9-10-9 3q0 0-3 7t-5 8q-5 0-9-9 1 5-6 9t-14 4q11 7-4 15-4 3-12 3t-11-2q-2-4-3-7t3-4 6-3 6-2 5-2q8-6 5-8-1 0-5-2t-6-2-4-2q-1-3 0-8t-1-8q-3 3-5 10t-4 9q4-5-14-3l-5 0q-3 0-9-1t-12-1-7 5q-3 4 0 11 0 2 2 1-2 2-6 5t-6 5q-25-8-52-23 3 0 6 1 3 1 8 4t5 3q19 7 24 4l3 2q7-9 11-14-4 3-17 1-11-3-12-7 4-6 2-10-2 2-6 6t-8 6-8 3q-9 0-13-1-81-45-131-124 4-4 7-4 2-1 3-5t1-6 6 1q5-4 2-10 1 0 25-15 10-10 11-12 2-6-5-10-1 1-5 5t-5 2q-2-3 0-10t6-7q-4 0-5-9t-2-20 0-13l1-1q-2-6 3-19t12-11q-7-1 11-24 3-4 4-5 2-1 7-4t9-6 5-5q2-3 6-13t8-13q-2-3 5-11t6-13q-1 0-2-1t-1 0q2-4 9-8t8-7q1-2 1-6t2-6 4-1q2 11-13 35-8 13-9 16-2 2-4 8t-2 8q1 0 3 0t5-2 4-3 1-1q-1-4 1-10t7-10 10-11 6-7q4-4 8-11t0-8q5 0 11-5t10-11q3-5 4-15t3-13q1-4 5-8t7-5l9-5t7-3q3-2 10-6t12-7q6-2 9-2t8 1 8 2q8 1 16-8t12-12q20-10 30-6-1 0 1-4t4-9 5-8 3-5q3-3 10-8t10-8q4 2 4 5-1-5 4-11t10-6q8 2 8 18-17-8-27 10 0 0-2 3t-2 5-1 4 0 5 2 1q5 0 6 2t-1 7-2 8q-1 4-6 11t-7 8q-3-5-9-4t-9 5q0-1-1-3t-1-4q-7 0-8 0 1 2 1 10t2 13q1 2 3 6t5 9 2 7-3 5-9 1q-11 0-15-11-1-2-2-6t-2-6-5-4q-4-2-14-1t-13 3q-8 4-13 16t-5 20q0 6 1 15t2 14-3 14q2 1 5 5t5 6q2 1 3 1t3 0 2 1 1 3q0 1-2 2-1 1-2 1 4-1 16 1t15-1q9-6 12 1 0 1-1 6t0 7q3-15 16-5 2-1 9-3t9-2q2-1 4-3t3-3 3 0 5 4q5-8 7-13 6-23 10-25 4-2 6-1t3 5 0 8-1 7l-1 5v10l0 4q-8 2-10 7t0 10 9 10q0 1 4 2t9 4 7 4q12 11 8 20 4 0 6 5 0 0-2 2t-5 2-2 2q5 2 1 8 3 2 4 7t4 5q5-6 12-1 5 5 1 9 2 4 11 6t10 5q4-1 5 1t0 7 2 7q2 2 9 5t7 2l9 7q2 2 0 2 10-1 18 6 5 6-4 11 2 4-1 5t-9 4q2 0 7 0t5 1q9 5-3 9-10 2-24-7z m-91-490q115 21 195 106-1 2-7 2t-7 2q-10 4-13 5 1 4-1 7t-5 5-7 5-6 4q-1 1-4 3t-4 3-4 2-5 2-5-1l-2-1q-2 0-3-1t-3-2-2-1 0-2q-12 10-20 13-3 0-6 3t-6 4-6 0-6-3q-3-3-4-9t-1-7q-4 3 0 10t1 10q-1 3-6 2t-6-2-7-5-5-3-4-3-5-5q-2-2-4-6t-2-6q-1 2-7 3t-5 3q1-5 2-19t3-22q4-17-7-26-15-14-16-23-2-12 7-14 0-4-5-12t-4-12q0-3 2-9z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="cloud" unicode="&#xe81a;" d="M1071 207q0-89-62-151t-152-63h-607q-103 0-177 73t-73 177q0 74 40 135t104 91q-1 16-1 24 0 118 84 202t202 84q88 0 159-49t105-129q39 35 93 35 59 0 101-42t42-101q0-42-23-77 72-17 119-75t46-134z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="flash" unicode="&#xe81b;" d="M494 534q10-11 4-24l-302-646q-7-14-23-14-2 0-8 1-9 3-14 11t-3 16l110 451-226-56q-2-1-7-1-10 0-17 7-10 8-7 21l112 461q2 8 9 13t15 5h183q11 0 18-7t7-17q0-4-2-10l-96-258 221 54q5 2 7 2 11 0 19-9z" horiz-adv-x="500" />
+
+<glyph glyph-name="barchart" unicode="&#xe81c;" d="M143 46v-107q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v107q0 8 5 13t13 5h107q8 0 13-5t5-13z m214 72v-179q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v179q0 8 5 13t13 5h107q8 0 13-5t5-13z m214 143v-322q0-8-5-13t-12-5h-108q-7 0-12 5t-5 13v322q0 8 5 13t12 5h108q7 0 12-5t5-13z m215 214v-536q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v536q0 8 5 13t13 5h107q8 0 13-5t5-13z m214 286v-822q0-8-5-13t-13-5h-107q-8 0-13 5t-5 13v822q0 8 5 13t13 5h107q8 0 13-5t5-13z" horiz-adv-x="1000" />
+
+<glyph glyph-name="down-dir" unicode="&#xe81d;" d="M571 457q0-14-10-25l-250-250q-11-11-25-11t-25 11l-250 250q-11 11-11 25t11 25 25 11h500q14 0 25-11t10-25z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="up-dir" unicode="&#xe81e;" d="M571 171q0-14-10-25t-25-10h-500q-15 0-25 10t-11 25 11 26l250 250q10 10 25 10t25-10l250-250q10-11 10-26z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="left-dir" unicode="&#xe81f;" d="M357 600v-500q0-14-10-25t-26-11-25 11l-250 250q-10 11-10 25t10 25l250 250q11 11 25 11t26-11 10-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="right-dir" unicode="&#xe820;" d="M321 350q0-14-10-25l-250-250q-11-11-25-11t-25 11-11 25v500q0 15 11 25t25 11 25-11l250-250q10-10 10-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="down-open" unicode="&#xe821;" d="M939 399l-414-413q-10-11-25-11t-25 11l-414 413q-11 11-11 26t11 25l93 92q10 11 25 11t25-11l296-296 296 296q11 11 25 11t26-11l92-92q11-11 11-25t-11-26z" horiz-adv-x="1000" />
+
+<glyph glyph-name="right-open" unicode="&#xe822;" d="M618 361l-414-415q-11-10-25-10t-25 10l-93 93q-11 11-11 25t11 25l296 297-296 296q-11 11-11 25t11 25l93 93q10 11 25 11t25-11l414-414q10-11 10-25t-10-25z" horiz-adv-x="714.3" />
+
+<glyph glyph-name="up-open" unicode="&#xe823;" d="M939 107l-92-92q-11-10-26-10t-25 10l-296 297-296-297q-11-10-25-10t-25 10l-93 92q-11 11-11 26t11 25l414 414q11 10 25 10t25-10l414-414q11-11 11-25t-11-26z" horiz-adv-x="1000" />
+
+<glyph glyph-name="left-open" unicode="&#xe824;" d="M654 682l-297-296 297-297q10-10 10-25t-10-25l-93-93q-11-10-25-10t-25 10l-414 415q-11 10-11 25t11 25l414 414q10 11 25 11t25-11l93-93q10-10 10-25t-10-25z" horiz-adv-x="714.3" />
+
+<glyph glyph-name="up-big" unicode="&#xe825;" d="M899 308q0-28-21-50l-41-42q-22-21-51-21-30 0-50 21l-165 164v-393q0-29-20-47t-51-19h-71q-30 0-51 19t-21 47v393l-164-164q-20-21-50-21t-50 21l-42 42q-21 21-21 50 0 30 21 51l363 363q20 21 50 21 30 0 51-21l363-363q21-22 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="right-big" unicode="&#xe826;" d="M821 314q0-30-20-50l-363-364q-22-20-51-20-29 0-50 20l-42 42q-22 21-22 51t22 51l163 163h-393q-29 0-47 21t-18 51v71q0 30 18 51t47 20h393l-163 165q-22 20-22 50t22 50l42 42q21 21 50 21 29 0 51-21l363-363q20-20 20-51z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="left-big" unicode="&#xe827;" d="M857 350v-71q0-30-18-51t-47-21h-393l164-164q21-20 21-50t-21-50l-42-43q-21-20-51-20-29 0-50 20l-364 364q-20 21-20 50 0 29 20 51l364 363q21 21 50 21 29 0 51-21l42-41q21-22 21-51t-21-51l-164-164h393q29 0 47-20t18-51z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="down-big" unicode="&#xe828;" d="M899 386q0-30-21-50l-363-364q-22-21-51-21-29 0-50 21l-363 364q-21 20-21 50 0 29 21 51l41 41q22 21 51 21 29 0 50-21l164-164v393q0 29 21 50t51 22h71q29 0 50-22t21-50v-393l165 164q20 21 50 21 29 0 51-21l41-41q21-22 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="resize-full-alt" unicode="&#xe829;" d="M716 548l-198-198 198-198 80 80q17 18 39 8 22-9 22-33v-250q0-14-10-25t-26-11h-250q-23 0-32 23-10 21 7 38l81 81-198 198-198-198 80-81q17-17 8-38-10-23-33-23h-250q-15 0-25 11t-11 25v250q0 24 22 33 22 10 39-8l80-80 198 198-198 198-80-80q-11-11-25-11-7 0-14 3-22 9-22 33v250q0 14 11 25t25 11h250q23 0 33-23 9-21-8-38l-80-81 198-198 198 198-81 81q-17 17-7 38 9 23 32 23h250q15 0 26-11t10-25v-250q0-24-22-33-7-3-14-3-14 0-25 11z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="resize-full" unicode="&#xe82a;" d="M421 261q0-7-5-13l-185-185 80-81q10-10 10-25t-10-25-25-11h-250q-15 0-25 11t-11 25v250q0 15 11 25t25 11 25-11l80-80 186 185q5 6 12 6t13-6l64-63q5-6 5-13z m436 482v-250q0-15-10-25t-26-11-25 11l-80 80-185-185q-6-6-13-6t-13 6l-64 64q-5 5-5 12t5 13l186 185-81 81q-10 10-10 25t10 25 25 11h250q15 0 26-11t10-25z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="resize-small" unicode="&#xe82b;" d="M429 314v-250q0-14-11-25t-25-10-25 10l-81 81-185-186q-5-5-13-5t-12 5l-64 64q-6 6-6 13t6 13l185 185-80 80q-11 11-11 25t11 25 25 11h250q14 0 25-11t11-25z m421 375q0-7-6-12l-185-186 80-80q11-11 11-25t-11-25-25-11h-250q-14 0-25 11t-10 25v250q0 14 10 25t25 10 25-10l81-80 185 185q6 5 13 5t13-5l63-64q6-5 6-13z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="move" unicode="&#xe82c;" d="M1000 350q0-14-11-25l-142-143q-11-11-26-11t-25 11-10 25v72h-215v-215h72q14 0 25-10t11-25-11-25l-143-143q-10-11-25-11t-25 11l-143 143q-11 10-11 25t11 25 25 10h72v215h-215v-72q0-14-10-25t-25-11-25 11l-143 143q-11 11-11 25t11 25l143 143q10 11 25 11t25-11 10-25v-72h215v215h-72q-14 0-25 10t-11 25 11 26l143 142q11 11 25 11t25-11l143-142q11-11 11-26t-11-25-25-10h-72v-215h215v72q0 14 10 25t25 11 26-11l142-143q11-10 11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="resize-horizontal" unicode="&#xe82d;" d="M1000 350q0-14-11-25l-142-143q-11-11-26-11t-25 11-10 25v72h-572v-72q0-14-10-25t-25-11-25 11l-143 143q-11 11-11 25t11 25l143 143q10 11 25 11t25-11 10-25v-72h572v72q0 14 10 25t25 11 26-11l142-143q11-10 11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="resize-vertical" unicode="&#xe82e;" d="M393 671q0-14-11-25t-25-10h-71v-572h71q15 0 25-10t11-25-11-25l-143-143q-10-11-25-11t-25 11l-143 143q-10 10-10 25t10 25 25 10h72v572h-72q-14 0-25 10t-10 25 10 26l143 142q11 11 25 11t25-11l143-142q11-11 11-26z" horiz-adv-x="428.6" />
+
+<glyph glyph-name="zoom-in" unicode="&#xe82f;" d="M571 404v-36q0-7-5-13t-12-5h-125v-125q0-7-6-13t-12-5h-36q-7 0-13 5t-5 13v125h-125q-7 0-12 5t-6 13v36q0 7 6 12t12 5h125v125q0 8 5 13t13 5h36q7 0 12-5t6-13v-125h125q7 0 12-5t5-12z m72-18q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-21-50t-51-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="block" unicode="&#xe830;" d="M732 352q0 90-48 164l-421-420q76-50 166-50 62 0 118 25t96 65 65 97 24 119z m-557-167l421 421q-75 50-167 50-83 0-153-40t-110-111-41-153q0-91 50-167z m682 167q0-88-34-168t-91-137-137-92-166-34-167 34-137 92-91 137-34 168 34 167 91 137 137 91 167 34 166-34 137-91 91-137 34-167z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="zoom-out" unicode="&#xe831;" d="M571 404v-36q0-7-5-13t-12-5h-322q-7 0-12 5t-6 13v36q0 7 6 12t12 5h322q7 0 12-5t5-12z m72-18q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-21-50t-51-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="lightbulb" unicode="&#xe832;" d="M411 529q0-8-6-13t-12-5-13 5-5 13q0 25-30 39t-59 14q-7 0-13 5t-5 13 5 13 13 5q28 0 55-9t49-30 21-50z m89 0q0 40-19 74t-50 57-69 35-76 12-76-12-69-35-50-57-20-74q0-57 38-101 6-6 17-18t17-19q72-85 79-166h127q8 81 79 166 6 6 17 19t17 18q38 44 38 101z m71 0q0-87-57-150-25-27-42-48t-33-54-19-60q26-15 26-46 0-20-13-35 13-15 13-36 0-29-25-45 8-13 8-26 0-26-18-40t-43-14q-11-25-34-39t-48-15-49 15-33 39q-26 0-44 14t-17 40q0 13 7 26-25 16-25 45 0 21 14 36-14 15-14 35 0 31 26 46-2 28-19 60t-33 54-41 48q-58 63-58 150 0 55 25 103t65 79 92 49 104 19 104-19 91-49 66-79 24-103z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="clock" unicode="&#xe833;" d="M500 546v-250q0-7-5-12t-13-5h-178q-8 0-13 5t-5 12v36q0 8 5 13t13 5h125v196q0 8 5 13t12 5h36q8 0 13-5t5-13z m232-196q0 83-41 152t-110 111-152 41-153-41-110-111-41-152 41-152 110-111 153-41 152 41 110 111 41 152z m125 0q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="volume-up" unicode="&#xe834;" d="M429 654v-608q0-14-11-25t-25-10-25 10l-186 186h-146q-15 0-25 11t-11 25v214q0 15 11 25t25 11h146l186 186q10 10 25 10t25-10 11-25z m214-304q0-42-24-79t-63-52q-5-3-14-3-14 0-25 10t-10 26q0 12 6 20t17 14 19 12 16 21 6 31-6 32-16 20-19 13-17 13-6 20q0 15 10 26t25 10q9 0 14-3 39-15 63-52t24-79z m143 0q0-85-48-158t-125-105q-7-3-14-3-15 0-26 11t-10 25q0 22 21 33 32 16 43 25 41 30 64 75t23 97-23 97-64 75q-11 9-43 25-21 11-21 33 0 14 10 25t25 11q8 0 15-3 78-33 125-105t48-158z m143 0q0-128-71-236t-189-158q-7-3-14-3-15 0-25 11t-11 25q0 20 22 33 4 2 12 6t13 6q25 14 46 28 68 51 107 127t38 161-38 161-107 127q-21 15-46 28-4 3-13 6t-12 6q-22 13-22 33 0 15 11 25t25 11q7 0 14-3 118-51 189-158t71-236z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="volume-down" unicode="&#xe835;" d="M429 654v-608q0-14-11-25t-25-10-25 10l-186 186h-146q-15 0-25 11t-11 25v214q0 15 11 25t25 11h146l186 186q10 10 25 10t25-10 11-25z m214-304q0-42-24-79t-63-52q-5-3-14-3-14 0-25 10t-10 26q0 12 6 20t17 14 19 12 16 21 6 31-6 32-16 20-19 13-17 13-6 20q0 15 10 26t25 10q9 0 14-3 39-15 63-52t24-79z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="volume-off" unicode="&#xe836;" d="M429 654v-608q0-14-11-25t-25-10-25 10l-186 186h-146q-15 0-25 11t-11 25v214q0 15 11 25t25 11h146l186 186q10 10 25 10t25-10 11-25z" horiz-adv-x="428.6" />
+
+<glyph glyph-name="mute" unicode="&#xe837;" d="M151 323l-56-57q-24 58-24 120v71q0 15 11 25t25 11 25-11 11-25v-71q0-30 8-63z m622 336l-202-202v-71q0-74-52-126t-126-53q-31 0-61 11l-53-54q54-28 114-28 103 0 177 73t73 177v71q0 15 11 25t25 11 25-11 10-25v-71q0-124-82-215t-203-104v-74h142q15 0 26-11t10-25-10-25-26-11h-357q-14 0-25 11t-10 25 10 25 25 11h143v74q-70 7-131 45l-142-142q-5-6-13-6t-12 6l-46 46q-6 5-6 13t6 12l689 689q5 6 12 6t13-6l46-46q6-5 6-13t-6-12z m-212 73l-347-346v285q0 74 53 127t126 52q57 0 103-33t65-85z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="mic" unicode="&#xe838;" d="M643 457v-71q0-124-82-215t-204-104v-74h143q15 0 25-11t11-25-11-25-25-11h-357q-15 0-25 11t-11 25 11 25 25 11h143v74q-121 13-204 104t-82 215v71q0 15 11 25t25 11 25-11 10-25v-71q0-103 74-177t176-73 177 73 73 177v71q0 15 11 25t25 11 25-11 11-25z m-143 214v-285q0-74-52-126t-127-53-126 53-52 126v285q0 74 52 127t126 52 127-52 52-127z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="endtime" unicode="&#xe839;" d="M661 350q0-14-11-25l-303-304q-11-10-26-10t-25 10-10 25v161h-250q-15 0-25 11t-11 25v214q0 15 11 25t25 11h250v161q0 14 10 25t25 10 26-10l303-304q11-10 11-25z m196 196v-392q0-67-47-114t-114-47h-178q-7 0-13 5t-5 13q0 2-1 11t0 15 2 13 5 11 12 3h178q37 0 64 27t26 63v392q0 37-26 64t-64 26h-174t-6 0-6 2-5 3-4 5-1 8q0 2-1 11t0 15 2 13 5 11 12 3h178q67 0 114-47t47-114z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="starttime" unicode="&#xe83a;" d="M357 46q0-2 1-11t0-14-2-14-5-11-12-3h-178q-67 0-114 47t-47 114v392q0 67 47 114t114 47h178q8 0 13-5t5-13q0-2 1-11t0-15-2-13-5-11-12-3h-178q-37 0-63-26t-27-64v-392q0-37 27-63t63-27h174t6 0 7-2 4-3 4-5 1-8z m518 304q0-14-11-25l-303-304q-11-10-25-10t-25 10-11 25v161h-250q-14 0-25 11t-11 25v214q0 15 11 25t25 11h250v161q0 14 11 25t25 10 25-10l303-304q11-10 11-25z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="calendar-empty" unicode="&#xe83b;" d="M71-79h786v572h-786v-572z m215 679v161q0 8-5 13t-13 5h-36q-8 0-13-5t-5-13v-161q0-8 5-13t13-5h36q8 0 13 5t5 13z m428 0v161q0 8-5 13t-13 5h-35q-8 0-13-5t-5-13v-161q0-8 5-13t13-5h35q8 0 13 5t5 13z m215 36v-715q0-29-22-50t-50-21h-786q-29 0-50 21t-21 50v715q0 29 21 50t50 21h72v54q0 37 26 63t63 26h36q37 0 63-26t26-63v-54h214v54q0 37 27 63t63 26h35q37 0 64-26t26-63v-54h71q29 0 50-21t22-50z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="calendar" unicode="&#xe83c;" d="M71-79h161v161h-161v-161z m197 0h178v161h-178v-161z m-197 197h161v178h-161v-178z m197 0h178v178h-178v-178z m-197 214h161v161h-161v-161z m411-411h179v161h-179v-161z m-214 411h178v161h-178v-161z m428-411h161v161h-161v-161z m-214 197h179v178h-179v-178z m-196 482v161q0 7-6 12t-12 6h-36q-7 0-12-6t-6-12v-161q0-7 6-13t12-5h36q7 0 12 5t6 13z m410-482h161v178h-161v-178z m-214 214h179v161h-179v-161z m214 0h161v161h-161v-161z m18 268v161q0 7-5 12t-13 6h-35q-7 0-13-6t-5-12v-161q0-7 5-13t13-5h35q8 0 13 5t5 13z m215 36v-715q0-29-22-50t-50-21h-786q-29 0-50 21t-21 50v715q0 29 21 50t50 21h72v54q0 37 26 63t63 26h36q37 0 63-26t26-63v-54h214v54q0 37 27 63t63 26h35q37 0 64-26t26-63v-54h71q29 0 50-21t22-50z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="wrench" unicode="&#xe83d;" d="M214 29q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m360 234l-381-381q-21-20-50-20-29 0-51 20l-59 61q-21 20-21 50 0 29 21 51l380 380q22-55 64-97t97-64z m354 243q0-22-13-59-27-75-92-122t-144-46q-104 0-177 73t-73 177 73 176 177 74q32 0 67-10t60-26q9-6 9-15t-9-16l-163-94v-125l108-60q2 2 44 27t75 45 40 20q8 0 13-5t5-14z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="sliders" unicode="&#xe83e;" d="M196 64v-71h-196v71h196z m197 72q14 0 25-11t11-25v-143q0-14-11-25t-25-11h-143q-14 0-25 11t-11 25v143q0 15 11 25t25 11h143z m89 214v-71h-482v71h482z m-357 286v-72h-125v72h125z m732-572v-71h-411v71h411z m-536 643q15 0 26-10t10-26v-142q0-15-10-25t-26-11h-142q-15 0-25 11t-11 25v142q0 15 11 26t25 10h142z m358-286q14 0 25-10t10-25v-143q0-15-10-25t-25-11h-143q-15 0-25 11t-11 25v143q0 14 11 25t25 10h143z m178-71v-71h-125v71h125z m0 286v-72h-482v72h482z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="services" unicode="&#xe83f;" d="M500 350q0 59-42 101t-101 42-101-42-42-101 42-101 101-42 101 42 42 101z m429-286q0 29-22 51t-50 21-50-21-21-51q0-29 21-50t50-21 51 21 21 50z m0 572q0 29-22 50t-50 21-50-21-21-50q0-30 21-51t50-21 51 21 21 51z m-215-235v-103q0-6-4-11t-8-6l-87-14q-6-19-18-42 19-27 50-64 4-6 4-11 0-7-4-11-12-17-46-50t-43-33q-7 0-12 4l-64 50q-21-11-43-17-6-60-13-87-4-13-17-13h-104q-6 0-11 4t-5 10l-13 85q-19 6-42 18l-66-50q-4-4-11-4-6 0-12 4-80 75-80 90 0 5 4 10 5 8 23 30t26 34q-13 24-20 46l-85 13q-5 1-9 5t-4 11v104q0 5 4 10t9 6l86 14q7 19 18 42-19 27-50 64-4 6-4 11 0 7 4 12 12 16 46 49t44 33q6 0 12-4l64-50q19 10 43 18 6 60 13 86 3 13 16 13h104q6 0 11-4t6-10l13-85q19-6 42-17l65 49q5 4 12 4 6 0 11-4 81-75 81-90 0-4-4-10-7-9-24-30t-25-34q13-27 19-46l85-12q6-2 9-6t4-11z m357-298v-78q0-9-83-17-6-15-16-29 28-63 28-77 0-2-2-4-68-40-69-40-5 0-26 27t-29 37q-11-1-17-1t-17 1q-7-11-29-37t-25-27q-1 0-69 40-3 2-3 4 0 14 29 77-10 14-17 29-83 8-83 17v78q0 9 83 18 7 16 17 29-29 63-29 77 0 2 3 4 2 1 19 11t33 19 17 9q4 0 25-26t29-38q12 1 17 1t17-1q28 40 51 63l4 1q2 0 69-39 2-2 2-4 0-14-28-77 9-13 16-29 83-9 83-18z m0 572v-78q0-9-83-18-6-15-16-29 28-63 28-77 0-2-2-4-68-39-69-39-5 0-26 26t-29 38q-11-1-17-1t-17 1q-7-12-29-38t-25-26q-1 0-69 39-3 2-3 4 0 14 29 77-10 14-17 29-83 9-83 18v78q0 9 83 17 7 16 17 29-29 63-29 77 0 2 3 4 2 1 19 11t33 19 17 9q4 0 25-26t29-37q12 1 17 1t17-1q28 39 51 62l4 1q2 0 69-39 2-2 2-4 0-14-28-77 9-13 16-29 83-8 83-17z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="service" unicode="&#xe840;" d="M571 350q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 16 20 16h124q7 0 13-5t7-12l15-103q28-9 51-20l79 59q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-12 0-7-4-13-9-12-29-37t-30-40q15-28 23-54l102-16q7-1 12-7t4-13z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="phone" unicode="&#xe841;" d="M786 158q0-15-6-39t-12-38q-11-28-68-60-52-28-103-28-15 0-30 2t-32 7-26 8-31 11-28 10q-54 20-97 47-71 44-148 120t-120 148q-27 43-46 97-2 5-10 28t-12 31-8 26-7 32-2 29q0 52 29 104 31 57 59 68 14 6 38 12t39 6q8 0 12-2 10-3 30-42 6-11 16-31t20-35 17-30q2-2 10-14t12-20 4-16q0-11-16-27t-35-31-34-30-16-25q0-5 3-13t4-11 8-14 7-10q42-77 97-132t131-97q1 0 10-6t14-8 11-5 13-2q10 0 25 16t30 34 31 35 28 16q7 0 15-4t20-12 14-10q14-8 30-17t36-20 30-17q39-19 42-29 2-4 2-12z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="file-pdf" unicode="&#xe842;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-287 331q18-14 47-31 33 4 65 4 82 0 99-27 9-13 1-29 0-1-1-1l-1-2v0q-3-21-39-21-27 0-64 11t-73 29q-123-13-219-46-85-146-135-146-8 0-15 4l-14 7q0 0-3 2-6 6-4 20 5 23 32 51t73 54q8 5 13-3 1-1 1-2 29 47 60 110 38 76 58 146-13 46-17 89t4 71q6 22 23 22h12q13 0 20-8 10-12 5-38-1-3-2-4 0-2 0-5v-17q-1-68-8-107 31-91 82-133z m-321-229q29 13 76 88-29-22-49-47t-27-41z m222 513q-9-23-2-73 1 4 4 24 0 2 4 24 1 3 3 5-1 0-1 1-1 1-1 2 0 12-7 20 0-1 0-1v-2z m-70-368q76 30 159 45-1 0-7 5t-9 8q-43 37-71 98-15-48-46-110-17-31-26-46z m361 9q-13 13-78 13 42-16 69-16 8 0 10 1 0 0-1 2z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="file-word" unicode="&#xe843;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-656 500v-59h39l92-369h88l72 271q4 11 5 25 2 9 2 14h2l1-14q1-1 2-11t3-14l72-271h89l91 369h39v59h-167v-59h50l-55-245q-3-11-4-25l-1-12h-3q0 2 0 4t-1 4 0 4q-1 2-2 11t-3 14l-81 304h-63l-81-304q-1-5-2-13t-2-12l-2-12h-2l-2 12q-1 14-3 25l-56 245h50v59h-167z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="file-excel" unicode="&#xe844;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-547 131v-59h157v59h-42l58 90q3 4 5 9t5 8 2 2h1q0-2 3-6 1-2 2-4t3-4 4-5l60-90h-43v-59h163v59h-38l-107 152 108 158h38v59h-156v-59h41l-57-89q-2-4-6-9t-5-8l-1-1h-1q0 2-3 5-3 6-9 13l-59 89h42v59h-162v-59h38l106-152-109-158h-38z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="doc-text" unicode="&#xe845;" d="M819 638q16-16 27-42t11-50v-642q0-23-15-38t-38-16h-750q-23 0-38 16t-16 38v892q0 23 16 38t38 16h500q22 0 49-11t42-27z m-248 136v-210h210q-5 17-12 23l-175 175q-6 7-23 12z m215-853v572h-232q-23 0-38 16t-16 37v233h-429v-858h715z m-572 483q0 7 5 12t13 5h393q8 0 13-5t5-12v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36z m411-125q8 0 13-5t5-13v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36q0 8 5 13t13 5h393z m0-143q8 0 13-5t5-13v-36q0-8-5-13t-13-5h-393q-8 0-13 5t-5 13v36q0 8 5 13t13 5h393z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="trash" unicode="&#xe846;" d="M286 439v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m143 0v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m142 0v-321q0-8-5-13t-12-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q7 0 12-5t5-13z m72-404v529h-500v-529q0-12 4-22t8-15 6-5h464q2 0 6 5t8 15 4 22z m-375 601h250l-27 65q-4 5-9 6h-177q-6-1-10-6z m518-18v-36q0-8-5-13t-13-5h-54v-529q0-46-26-80t-63-34h-464q-37 0-63 33t-27 79v531h-53q-8 0-13 5t-5 13v36q0 8 5 13t13 5h172l39 93q9 21 31 35t44 15h178q23 0 44-15t30-35l39-93h173q8 0 13-5t5-13z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="comment-empty" unicode="&#xe847;" d="M500 636q-114 0-213-39t-157-105-59-142q0-62 40-119t113-98l48-28-15-53q-13-51-39-97 85 36 154 96l24 21 32-3q38-5 72-5 114 0 213 39t157 105 59 142-59 142-157 105-213 39z m500-286q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12h-3q-8 0-15 6t-9 15v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 97 67 179t182 130 251 48 251-48 182-130 67-179z" horiz-adv-x="1000" />
+
+<glyph glyph-name="comment" unicode="&#xe848;" d="M1000 350q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12-10-1-17 5t-10 16v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 73 40 139t106 114 160 76 194 28q136 0 251-48t182-130 67-179z" horiz-adv-x="1000" />
+
+<glyph glyph-name="chat" unicode="&#xe849;" d="M786 421q0-77-53-143t-143-104-197-38q-48 0-98 9-70-49-155-72-21-5-48-9h-2q-6 0-12 5t-6 12q-1 1-1 3t1 4 1 3l1 3t2 3 2 3 3 3 2 2q3 3 13 14t15 16 12 17 14 21 11 25q-69 40-108 98t-40 125q0 78 53 144t143 104 197 38 197-38 143-104 53-144z m214-142q0-67-40-126t-108-98q5-14 11-25t14-21 13-16 14-17 13-14q0 0 2-2t3-3 2-3 2-3l1-3t1-3 1-4-1-3q-2-8-7-13t-12-4q-28 4-48 9-86 23-156 72-50-9-98-9-151 0-263 74 32-3 49-3 90 0 172 25t148 72q69 52 107 119t37 141q0 43-13 85 72-39 114-99t42-128z" horiz-adv-x="1000" />
+
+<glyph glyph-name="chat-empty" unicode="&#xe84a;" d="M393 636q-85 0-160-29t-118-79-44-107q0-45 30-88t83-73l54-32-19-46q19 11 34 21l25 18 30-6q43-8 85-8 85 0 160 29t118 79 43 106-43 107-118 79-160 29z m0 71q106 0 197-38t143-104 53-144-53-143-143-104-197-38q-48 0-98 9-70-49-155-72-21-5-48-9h-2q-6 0-12 5t-6 12q-1 1-1 3t1 4 1 3l1 3t2 3 2 3 3 3 2 2q3 3 13 14t15 16 12 17 14 21 11 25q-69 40-108 98t-40 125q0 78 53 144t143 104 197 38z m459-652q5-14 11-25t14-21 13-16 14-17 13-14q0 0 2-2t3-3 2-3 2-3l1-3t1-3 1-4-1-3q-2-8-7-13t-12-4q-28 4-48 9-86 23-156 72-50-9-98-9-151 0-263 74 32-3 49-3 90 0 172 25t148 72q69 52 107 119t37 141q0 43-13 85 72-39 114-99t42-128q0-67-40-126t-108-98z" horiz-adv-x="1000" />
+
+<glyph glyph-name="bell" unicode="&#xe84b;" d="M509-96q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m-372 160h726q-149 168-149 465 0 28-13 58t-39 58-67 45-95 17-95-17-67-45-39-58-13-58q0-297-149-465z m827 0q0-29-21-50t-50-21h-250q0-59-42-101t-101-42-101 42-42 101h-250q-29 0-50 21t-21 50q28 24 51 49t47 67 42 89 27 115 11 145q0 84 66 157t171 89q-5 10-5 21 0 23 16 38t38 16 38-16 16-38q0-11-5-21 106-16 171-89t66-157q0-78 11-145t28-115 41-89 48-67 50-49z" horiz-adv-x="1000" />
+
+<glyph glyph-name="bell-alt" unicode="&#xe84c;" d="M509-96q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m455 160q0-29-21-50t-50-21h-250q0-59-42-101t-101-42-101 42-42 101h-250q-29 0-50 21t-21 50q28 24 51 49t47 67 42 89 27 115 11 145q0 84 66 157t171 89q-5 10-5 21 0 23 16 38t38 16 38-16 16-38q0-11-5-21 106-16 171-89t66-157q0-78 11-145t28-115 41-89 48-67 50-49z" horiz-adv-x="1000" />
+
+<glyph glyph-name="attention-alt" unicode="&#xe84d;" d="M286 154v-125q0-15-11-25t-25-11h-143q-14 0-25 11t-11 25v125q0 14 11 25t25 10h143q15 0 25-10t11-25z m17 589l-16-429q-1-14-12-25t-25-10h-143q-14 0-25 10t-12 25l-15 429q-1 14 10 25t24 11h179q14 0 25-11t10-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="print" unicode="&#xe84e;" d="M214-7h500v143h-500v-143z m0 357h500v214h-89q-22 0-38 16t-16 38v89h-357v-357z m643-36q0 15-10 25t-26 11-25-11-10-25 10-25 25-10 26 10 10 25z m72 0v-232q0-7-6-12t-12-6h-125v-89q0-22-16-38t-38-16h-536q-22 0-37 16t-16 38v89h-125q-7 0-13 6t-5 12v232q0 44 32 76t75 31h36v304q0 22 16 38t37 16h375q23 0 50-12t42-26l85-85q15-16 27-43t11-49v-143h35q45 0 76-31t32-76z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="edit" unicode="&#xe84f;" d="M496 189l64 65-85 85-64-65v-31h53v-54h32z m245 402q-9 9-18 0l-196-196q-9-9 0-18t18 0l196 196q9 9 0 18z m45-331v-106q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-8-8-18-4-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v70q0 7 5 12l36 36q8 8 20 4t11-16z m-54 411l161-160-375-375h-161v160z m248-73l-51-52-161 161 51 52q16 15 38 15t38-15l85-85q16-16 16-38t-16-38z" horiz-adv-x="1000" />
+
+<glyph glyph-name="forward" unicode="&#xe850;" d="M1000 493q0-15-11-25l-285-286q-11-11-25-11t-25 11-11 25v143h-125q-55 0-98-3t-86-12-74-24-59-39-45-56-27-77-10-101q0-31 3-69 0-4 2-13t1-15q0-8-5-14t-13-6q-9 0-15 10-4 5-8 12t-7 17-6 13q-71 159-71 252 0 111 30 186 90 225 488 225h125v143q0 14 11 25t25 10 25-10l285-286q11-11 11-25z" horiz-adv-x="1000" />
+
+<glyph glyph-name="reply" unicode="&#xe851;" d="M1000 225q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" horiz-adv-x="1000" />
+
+<glyph glyph-name="reply-all" unicode="&#xe852;" d="M357 246v-39q0-23-22-33-7-3-14-3-15 0-25 11l-285 286q-11 10-11 25t11 25l285 286q17 17 39 8 22-10 22-33v-39l-221-222q-11-11-11-25t11-25z m643-21q0-32-9-74t-22-77-27-70-22-51l-11-22q-5-10-16-10-3 0-5 1-14 4-13 19 24 223-59 315-36 40-95 62t-150 29v-140q0-23-21-33-8-3-14-3-15 0-25 11l-286 286q-11 10-11 25t11 25l286 286q16 17 39 8 21-10 21-33v-147q230-15 335-123 94-96 94-284z" horiz-adv-x="1000" />
+
+<glyph glyph-name="eye" unicode="&#xe853;" d="M929 314q-85 132-213 197 34-58 34-125 0-103-73-177t-177-73-177 73-73 177q0 67 34 125-128-65-213-197 75-114 187-182t242-68 243 68 186 182z m-402 215q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m473-215q0-19-11-38-78-129-210-206t-279-77-279 77-210 206q-11 19-11 38t11 39q78 128 210 205t279 78 279-78 210-205q11-20 11-39z" horiz-adv-x="1000" />
+
+<glyph glyph-name="tag" unicode="&#xe854;" d="M250 600q0 30-21 51t-50 20-51-20-21-51 21-50 51-21 50 21 21 50z m595-321q0-30-20-51l-274-274q-22-21-51-21-30 0-50 21l-399 399q-21 21-36 57t-15 65v232q0 29 21 50t50 22h233q29 0 65-15t57-36l399-399q20-21 20-50z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="tags" unicode="&#xe855;" d="M250 600q0 30-21 51t-50 20-51-20-21-51 21-50 51-21 50 21 21 50z m595-321q0-30-20-51l-274-274q-22-21-51-21-30 0-50 21l-399 399q-21 21-36 57t-15 65v232q0 29 21 50t50 22h233q29 0 65-15t57-36l399-399q20-21 20-50z m215 0q0-30-21-51l-274-274q-22-21-51-21-20 0-33 8t-29 25l262 262q21 21 21 51 0 29-21 50l-399 399q-21 21-57 36t-65 15h125q29 0 65-15t57-36l399-399q21-21 21-50z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="lock-open-alt" unicode="&#xe856;" d="M589 421q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v179q0 103 74 177t176 73 177-73 73-177q0-14-10-25t-25-11h-36q-14 0-25 11t-11 25q0 59-42 101t-101 42-101-42-41-101v-179h410z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="lock-open" unicode="&#xe857;" d="M929 529v-143q0-15-11-25t-25-11h-36q-14 0-25 11t-11 25v143q0 59-41 101t-101 41-101-41-42-101v-108h53q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h375v108q0 103 73 176t177 74 176-74 74-176z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="lock" unicode="&#xe858;" d="M179 421h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="home" unicode="&#xe859;" d="M786 296v-267q0-15-11-25t-25-11h-214v214h-143v-214h-214q-15 0-25 11t-11 25v267q0 1 0 2t0 2l321 264 321-264q1-1 1-4z m124 39l-34-41q-5-5-12-6h-2q-7 0-12 3l-386 322-386-322q-7-4-13-3-7 1-12 6l-35 41q-4 6-3 13t6 12l401 334q18 15 42 15t43-15l136-113v108q0 8 5 13t13 5h107q8 0 13-5t5-13v-227l122-102q6-4 6-12t-4-13z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="info" unicode="&#xe85a;" d="M357 100v-71q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v71q0 15 11 25t25 11h35v214h-35q-15 0-25 11t-11 25v71q0 15 11 25t25 11h214q15 0 25-11t11-25v-321h35q15 0 26-11t10-25z m-71 643v-107q0-15-11-25t-25-11h-143q-14 0-25 11t-11 25v107q0 14 11 25t25 11h143q15 0 25-11t11-25z" horiz-adv-x="357.1" />
+
+<glyph glyph-name="help" unicode="&#xe85b;" d="M393 149v-134q0-9-7-15t-15-7h-134q-9 0-16 7t-7 15v134q0 9 7 16t16 6h134q9 0 15-6t7-16z m176 335q0-30-8-56t-20-43-31-33-32-25-34-19q-23-13-38-37t-15-37q0-10-7-18t-16-9h-134q-8 0-14 11t-6 20v26q0 46 37 87t79 60q33 16 47 32t14 42q0 24-26 41t-60 18q-36 0-60-16-20-14-60-64-7-9-17-9-7 0-14 4l-91 70q-8 6-9 14t3 16q89 148 259 148 45 0 90-17t81-46 59-72 23-88z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="search" unicode="&#xe85c;" d="M643 386q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="flapping" unicode="&#xe85d;" d="M372 582q-34-52-77-153-12 25-20 41t-23 35-28 32-36 19-45 8h-125q-8 0-13 5t-5 13v107q0 8 5 13t13 5h125q139 0 229-125z m628-446q0-8-5-13l-179-179q-5-5-12-5-8 0-13 6t-5 12v107q-18 0-48 0t-45-1-41 1-39 3-36 6-35 10-32 16-33 22-31 30-31 39q33 52 76 152 12-25 20-40t23-36 28-31 35-20 46-8h143v107q0 8 5 13t13 5q6 0 13-5l178-178q5-5 5-13z m0 500q0-8-5-13l-179-179q-5-5-12-5-8 0-13 6t-5 12v107h-143q-27 0-49-8t-38-25-29-34-25-44q-18-34-43-95-16-37-28-62t-30-59-36-55-41-47-50-38-60-23-71-10h-125q-8 0-13 5t-5 13v107q0 8 5 13t13 5h125q27 0 48 9t39 25 28 34 26 43q17 35 43 96 16 36 28 62t30 58 36 56 41 46 50 39 59 23 72 9h143v107q0 8 5 13t13 5q6 0 13-5l178-178q5-5 5-13z" horiz-adv-x="1000" />
+
+<glyph glyph-name="rewind" unicode="&#xe85e;" d="M532 736q170 0 289-120t119-290-119-290-289-120q-142 0-252 88l70 74q84-60 182-60 126 0 216 90t90 218-90 218-216 90q-124 0-214-87t-92-211l142 0-184-204-184 204 124 0q2 166 122 283t286 117z" horiz-adv-x="940" />
+
+<glyph glyph-name="chart-line" unicode="&#xe85f;" d="M1143-7v-72h-1143v858h71v-786h1072z m-72 696v-242q0-12-10-17t-20 4l-68 68-353-353q-6-6-13-6t-13 6l-130 130-232-233-107 108 327 326q5 6 12 6t13-6l130-130 259 259-67 68q-9 8-5 19t17 11h243q7 0 12-5t5-13z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="bell-off" unicode="&#xe860;" d="M869 375q35-199 167-311 0-29-21-50t-51-21h-250q0-59-42-101t-101-42-100 42-42 100z m-298-480q9 0 9 9t-9 8q-32 0-56 24t-24 57q0 9-9 9t-9-9q0-41 29-70t69-28z m560 893q4-6 4-14t-6-12l-1045-905q-5-5-13-4t-12 6l-47 53q-4 6-4 14t6 12l104 90q-11 17-11 36 28 24 51 49t47 67 42 89 28 115 11 145q0 84 65 157t171 89q-4 10-4 21 0 23 16 38t37 16 38-16 16-38q0-11-4-21 69-10 122-46t82-88l234 202q5 5 13 4t12-6z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="bell-off-empty" unicode="&#xe861;" d="M580-96q0 8-9 8-32 0-56 24t-24 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m-299 265l489 424q-23 49-74 82t-125 32q-51 0-94-17t-68-45-38-58-14-58q0-215-76-360z m755-105q0-29-21-50t-51-21h-250q0-59-42-101t-101-42-100 42-42 100l83 72h422q-92 105-126 256l61 55q35-199 167-311z m48 777l47-53q4-6 4-14t-6-12l-1045-905q-5-5-13-4t-12 6l-47 53q-4 6-4 14t6 12l104 90q-11 17-11 36 28 24 51 49t47 67 42 89 28 115 11 145q0 84 65 157t171 89q-4 10-4 21 0 23 16 38t37 16 38-16 16-38q0-11-4-21 69-10 122-46t82-88l234 202q5 5 13 4t12-6z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="plug" unicode="&#xe862;" d="M979 597q21-21 21-50t-21-51l-223-223 83-84-89-89q-91-91-217-104t-230 56l-202-202h-101v101l202 202q-69 103-56 230t104 217l89 89 84-83 223 223q21 21 51 21t50-21 21-50-21-51l-223-223 131-131 223 223q22 21 51 21t50-21z" horiz-adv-x="1000" />
+
+<glyph glyph-name="eye-off" unicode="&#xe863;" d="M310 105l43 79q-48 35-76 88t-27 114q0 67 34 125-128-65-213-197 94-144 239-209z m217 424q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m202 106q0-4 0-5-59-105-176-316t-176-316l-28-50q-5-9-15-9-7 0-75 39-9 6-9 16 0 7 25 49-80 36-147 96t-117 137q-11 17-11 38t11 39q86 131 212 207t277 76q50 0 100-10l31 54q5 9 15 9 3 0 10-3t18-9 18-10 18-10 10-7q9-5 9-15z m21-249q0-78-44-142t-117-91l157 280q4-25 4-47z m250-72q0-19-11-38-22-36-61-81-84-96-194-149t-234-53l41 74q119 10 219 76t169 171q-65 100-158 164l35 63q53-36 102-85t81-103q11-19 11-39z" horiz-adv-x="1000" />
+
+<glyph glyph-name="arrows-cw" unicode="&#xe864;" d="M843 261q0-3 0-4-36-150-150-243t-267-93q-81 0-157 31t-136 88l-72-72q-11-11-25-11t-25 11-11 25v250q0 14 11 25t25 11h250q14 0 25-11t10-25-10-25l-77-77q40-36 90-57t105-20q74 0 139 37t104 99q6 10 30 66 4 13 16 13h107q8 0 13-6t5-12z m14 446v-250q0-14-10-25t-26-11h-250q-14 0-25 11t-10 25 10 25l77 77q-82 77-194 77-75 0-140-37t-104-99q-6-10-29-66-5-13-17-13h-111q-7 0-13 6t-5 12v4q36 150 151 243t268 93q81 0 158-31t137-88l72 72q11 11 25 11t26-11 10-25z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="cw" unicode="&#xe865;" d="M408 760q168 0 287-116t123-282l122 0-184-206-184 206 144 0q-4 124-94 210t-214 86q-126 0-216-90t-90-218q0-126 90-216t216-90q104 0 182 60l70-76q-110-88-252-88-168 0-288 120t-120 290 120 290 288 120z" horiz-adv-x="940" />
+
+<glyph glyph-name="host" unicode="&#xe866;" d="M232 136q-37 0-63 26t-26 63v393q0 37 26 63t63 26h607q37 0 63-26t27-63v-393q0-37-27-63t-63-26h-607z m-18 482v-393q0-7 6-13t12-5h607q8 0 13 5t5 13v393q0 7-5 12t-13 6h-607q-7 0-12-6t-6-12z m768-518h89v-54q0-22-26-37t-63-16h-893q-36 0-63 16t-26 37v54h982z m-402-54q9 0 9 9t-9 9h-89q-9 0-9-9t9-9h89z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="thumbs-up" unicode="&#xe867;" d="M143 100q0 15-11 25t-25 11-25-11-11-25 11-25 25-11 25 11 11 25z m643 321q0 29-22 50t-50 22h-196q0 32 27 89t26 89q0 55-17 81t-72 27q-14-15-21-48t-17-70-33-61q-13-13-43-51-2-3-13-16t-18-23-19-24-22-25-22-19-22-15-20-6h-18v-357h18q7 0 18-1t18-4 21-6 20-7 20-6 16-6q118-41 191-41h67q107 0 107 93 0 15-2 31 16 9 26 30t10 41-10 38q29 28 29 67 0 14-5 31t-14 26q18 1 30 26t12 45z m71 1q0-50-27-91 5-18 5-38 0-43-21-81 1-12 1-24 0-56-33-99 0-78-48-123t-126-45h-72q-54 0-106 13t-121 36q-65 23-77 23h-161q-29 0-50 21t-21 50v357q0 30 21 51t50 21h153q20 13 77 86 32 42 60 72 13 14 19 48t17 70 35 60q22 21 50 21 47 0 84-18t57-57 20-104q0-51-27-107h98q58 0 101-42t42-100z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="thumbs-down" unicode="&#xe868;" d="M143 600q0 15-11 25t-25 11-25-11-11-25 11-25 25-11 25 11 11 25z m643-321q0 19-12 45t-30 26q8 10 14 27t5 31q0 38-29 66 10 17 10 38 0 21-10 41t-26 30q2 16 2 31 0 47-27 70t-76 23h-71q-73 0-191-41-3-1-16-5t-20-7-20-7-21-6-18-4-18-1h-18v-357h18q9 0 20-5t22-15 22-20 22-25 19-24 18-22 13-17q30-38 43-51 23-24 33-61t17-70 21-48q54 0 72 27t17 81q0 33-26 89t-27 89h196q28 0 50 22t22 50z m71-1q0-57-42-100t-101-42h-98q27-55 27-107 0-66-20-104-19-39-57-57t-84-18q-28 0-50 21-19 18-30 45t-14 51-10 47-17 36q-27 28-60 71-57 73-77 86h-153q-29 0-50 21t-21 51v357q0 29 21 50t50 21h161q12 0 77 23 72 24 125 36t111 13h63q78 0 126-44t48-121v-3q33-43 33-99 0-12-1-24 21-38 21-80 0-21-5-39 27-41 27-91z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="spinner" unicode="&#xe869;" d="M294 72q0-29-21-50t-51-21q-29 0-50 21t-21 50q0 30 21 51t50 21 51-21 21-51z m277-115q0-29-20-50t-51-21-50 21-21 50 21 51 50 21 51-21 20-51z m-392 393q0-30-21-50t-51-21-50 21-21 50 21 51 50 20 51-20 21-51z m670-278q0-29-21-50t-50-21q-30 0-51 21t-20 50 20 51 51 21 50-21 21-51z m-538 556q0-37-26-63t-63-26-63 26-26 63 26 63 63 26 63-26 26-63z m653-278q0-30-21-50t-50-21-51 21-21 50 21 51 51 20 50-20 21-51z m-357 393q0-45-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m296-115q0-52-37-88t-88-37q-52 0-88 37t-37 88q0 51 37 88t88 37q51 0 88-37t37-88z" horiz-adv-x="1000" />
+
+<glyph glyph-name="attach" unicode="&#xe86a;" d="M784 77q0-65-45-109t-109-44q-75 0-131 55l-434 434q-63 64-63 151 0 89 62 150t150 62q88 0 152-63l338-338q5-5 5-12 0-9-17-26t-26-17q-7 0-12 5l-339 339q-44 43-101 43-59 0-100-42t-40-101q0-58 42-101l433-433q35-36 81-36 36 0 59 24t24 59q0 46-35 81l-325 324q-14 14-33 14-16 0-27-11t-11-27q0-18 14-33l229-228q6-6 6-13 0-9-18-26t-26-17q-6 0-12 5l-229 229q-35 34-35 83 0 46 32 78t77 32q49 0 84-35l324-325q56-54 56-131z" horiz-adv-x="785.7" />
+
+<glyph glyph-name="keyboard" unicode="&#xe86b;" d="M214 198v-53q0-9-9-9h-53q-9 0-9 9v53q0 9 9 9h53q9 0 9-9z m72 143v-53q0-9-9-9h-125q-9 0-9 9v53q0 9 9 9h125q9 0 9-9z m-72 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m572-286v-53q0-9-9-9h-482q-9 0-9 9v53q0 9 9 9h482q9 0 9-9z m-357 143v-53q0-9-9-9h-54q-9 0-9 9v53q0 9 9 9h54q9 0 9-9z m-72 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m214-143v-53q0-9-8-9h-54q-9 0-9 9v53q0 9 9 9h54q8 0 8-9z m-71 143v-54q0-9-9-9h-53q-9 0-9 9v54q0 9 9 9h53q9 0 9-9z m214-143v-53q0-9-9-9h-53q-9 0-9 9v53q0 9 9 9h53q9 0 9-9z m215-143v-53q0-9-9-9h-54q-9 0-9 9v53q0 9 9 9h54q9 0 9-9z m-286 286v-54q0-9-9-9h-54q-9 0-9 9v54q0 9 9 9h54q9 0 9-9z m143 0v-54q0-9-9-9h-54q-9 0-9 9v54q0 9 9 9h54q9 0 9-9z m143 0v-196q0-9-9-9h-125q-9 0-9 9v53q0 9 9 9h62v134q0 9 9 9h54q9 0 9-9z m71-420v500h-929v-500h929z m71 500v-500q0-29-20-50t-51-21h-929q-29 0-50 21t-21 50v500q0 30 21 51t50 21h929q30 0 51-21t20-51z" horiz-adv-x="1071.4" />
+
+<glyph glyph-name="menu" unicode="&#xe86c;" d="M857 100v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-14-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="wifi" unicode="&#xe86d;" d="M571 0q-11 0-51 41t-41 52q0 18 35 30t57 13 58-13 35-30q0-11-41-52t-52-41z m151 151q-1 0-22 14t-57 28-72 14-71-14-57-28-22-14q-10 0-52 42t-42 52q0 7 5 13 44 43 109 67t130 25 131-25 109-67q5-6 5-13 0-10-42-52t-52-42z m152 152q-6 0-12 5-76 58-141 86t-150 27q-47 0-95-12t-83-29-63-35-44-30-18-12q-9 0-51 42t-42 52q0 7 6 12 74 74 178 115t212 40 213-40 178-115q6-5 6-12 0-10-42-52t-52-42z m152 151q-6 0-13 5-99 88-207 132t-235 45-234-45-207-132q-7-5-13-5-9 0-51 42t-43 52q0 7 6 13 104 104 248 161t294 57 295-57 248-161q5-6 5-13 0-10-42-52t-51-42z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="moon" unicode="&#xe86e;" d="M704 123q-30-5-61-5-102 0-188 50t-137 137-50 188q0 107 58 199-112-33-183-128t-72-214q0-72 29-139t76-113 114-77 139-28q80 0 152 34t123 96z m114 47q-53-113-159-181t-230-68q-87 0-167 34t-136 92-92 137-34 166q0 85 32 163t87 135 132 92 161 38q25 1 34-22 11-23-8-40-48-43-73-101t-26-122q0-83 41-152t111-111 152-41q66 0 127 29 23 10 40-7 8-8 10-19t-2-22z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="chart-pie" unicode="&#xe86f;" d="M429 353l304-304q-59-61-138-94t-166-34q-117 0-216 58t-155 156-58 215 58 215 155 156 216 58v-426z m104-3h431q0-88-33-167t-94-138z m396 71h-429v429q117 0 215-57t156-156 58-216z" horiz-adv-x="1000" />
+
+<glyph glyph-name="chart-area" unicode="&#xe870;" d="M1143-7v-72h-1143v858h71v-786h1072z m-214 571l142-500h-928v322l250 321 321-321z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="chart-bar" unicode="&#xe871;" d="M357 350v-286h-143v286h143z m214 286v-572h-142v572h142z m572-643v-72h-1143v858h71v-786h1072z m-357 500v-429h-143v429h143z m214 214v-643h-143v643h143z" horiz-adv-x="1142.9" />
+
+<glyph glyph-name="beaker" unicode="&#xe872;" d="M852 42q31-50 12-85t-78-36h-643q-59 0-78 36t12 85l280 443v222h-36q-14 0-25 11t-10 25 10 25 25 11h286q15 0 25-11t11-25-11-25-25-11h-36v-222z m-435 405l-151-240h397l-152 240-11 17v243h-71v-243z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="magic" unicode="&#xe873;" d="M664 526l164 163-60 60-164-163z m250 163q0-15-10-25l-718-718q-10-10-25-10t-25 10l-111 111q-10 10-10 25t10 25l718 718q10 10 25 10t25-10l111-111q10-10 10-25z m-754 106l54-16-54-17-17-55-17 55-55 17 55 16 17 55z m195-90l109-34-109-33-34-109-33 109-109 33 109 34 33 109z m519-267l55-17-55-16-17-55-17 55-54 16 54 17 17 55z m-357 357l54-16-54-17-17-55-17 55-54 17 54 16 17 55z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="spin6" unicode="&#xe874;" d="M855 9c-189-190-520-172-705 13-190 190-200 494-28 695 11 13 21 26 35 34 36 23 85 18 117-13 30-31 35-76 16-112-5-9-9-15-16-22-140-151-145-379-8-516 153-153 407-121 542 34 106 122 142 297 77 451-83 198-305 291-510 222l0 1c236 82 492-24 588-252 71-167 37-355-72-493-11-15-23-29-36-42z" horiz-adv-x="1000" />
+
+<glyph glyph-name="down-small" unicode="&#xe875;" d="M505 346q15-15 15-37t-15-37l-245-245-245 245q-15 15-15 37t15 37 37 15 37-15l120-119 0 395q0 21 15 36t36 15 37-15 16-36l0-395 120 119q15 15 36 15t36-15z" horiz-adv-x="520" />
+
+<glyph glyph-name="left-small" unicode="&#xe876;" d="M595 403q21 0 36-16t15-37-15-37-36-15l-395 0 119-119q15-15 15-37t-15-37-36-15q-23 0-38 15l-245 245 245 245q15 15 37 15t37-15 15-37-15-37l-119-118 395 0z" horiz-adv-x="646" />
+
+<glyph glyph-name="right-small" unicode="&#xe877;" d="M328 595q15 15 36 15t37-15l245-245-245-245q-15-15-36-15-22 0-37 15t-15 37 15 37l120 119-395 0q-22 0-37 15t-16 37 16 37 37 16l395 0-120 118q-15 15-15 37t15 37z" horiz-adv-x="646" />
+
+<glyph glyph-name="up-small" unicode="&#xe878;" d="M260 673l245-245q15-15 15-37t-15-37-36-15-36 15l-120 120 0-395q0-21-16-37t-37-15-36 15-15 37l0 395-120-120q-15-15-37-15t-37 15-15 37 15 37z" horiz-adv-x="520" />
+
+<glyph glyph-name="pin" unicode="&#xe879;" d="M573 37q0-23-15-38t-37-15q-21 0-37 16l-169 169-315-236 236 315-168 169q-24 23-12 56 14 32 48 32 157 0 270 57 90 45 151 171 9 24 36 32t50-13l208-209q21-23 14-50t-32-36q-127-63-172-152-56-110-56-268z" horiz-adv-x="834" />
+
+<glyph glyph-name="angle-double-left" unicode="&#xe87a;" d="M350 82q0-7-6-13l-28-28q-5-5-12-5t-13 5l-260 261q-6 5-6 12t6 13l260 260q5 6 13 6t12-6l28-28q6-5 6-13t-6-12l-219-220 219-219q6-6 6-13z m214 0q0-7-5-13l-28-28q-6-5-13-5t-13 5l-260 261q-6 5-6 12t6 13l260 260q6 6 13 6t13-6l28-28q5-5 5-13t-5-12l-220-220 220-219q5-6 5-13z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="angle-double-right" unicode="&#xe87b;" d="M332 314q0-7-5-12l-261-261q-5-5-12-5t-13 5l-28 28q-6 6-6 13t6 13l219 219-219 220q-6 5-6 12t6 13l28 28q5 6 13 6t12-6l261-260q5-5 5-13z m214 0q0-7-5-12l-260-261q-6-5-13-5t-13 5l-28 28q-5 6-5 13t5 13l219 219-219 220q-5 5-5 12t5 13l28 28q6 6 13 6t13-6l260-260q5-5 5-13z" horiz-adv-x="571.4" />
+
+<glyph glyph-name="circle" unicode="&#xe87c;" d="M857 350q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="info-circled" unicode="&#xe87d;" d="M454 810q190 2 326-130t140-322q2-190-131-327t-323-141q-190-2-327 131t-139 323q-4 190 130 327t324 139z m52-152q-42 0-65-24t-23-50q-2-28 15-44t49-16q38 0 61 22t23 54q0 58-60 58z m-120-594q30 0 84 26t106 78l-18 24q-48-36-72-36-14 0-4 38l42 160q26 96-22 96-30 0-89-29t-115-75l16-26q52 34 74 34 12 0 0-34l-36-152q-26-104 34-104z" horiz-adv-x="920" />
+
+<glyph glyph-name="twitter" unicode="&#xe87e;" d="M904 622q-37-54-90-93 0-8 0-23 0-73-21-145t-64-139-103-117-144-82-181-30q-151 0-276 81 19-2 43-2 126 0 224 77-59 1-105 36t-64 89q19-3 34-3 24 0 48 6-63 13-104 62t-41 115v2q38-21 82-23-37 25-59 64t-22 86q0 49 25 91 68-83 164-133t208-55q-5 21-5 41 0 75 53 127t127 53q79 0 132-57 61 12 115 44-21-64-80-100 52 6 104 28z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="facebook-squared" unicode="&#xe87f;" d="M696 779q67 0 114-48t47-113v-536q0-66-47-113t-114-48h-104v333h111l16 129h-127v83q0 31 13 46t51 16l68 1v115q-35 5-100 5-75 0-121-44t-45-127v-95h-112v-129h112v-333h-297q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="gplus-squared" unicode="&#xe880;" d="M512 345q0 15-4 36h-202v-74h122q-2-13-10-28t-21-29-37-25-54-10q-55 0-94 40t-39 95 39 95 94 40q52 0 86-33l58 57q-60 55-144 55-89 0-151-62t-63-152 63-151 151-63q92 0 149 58t57 151z m192-26h61v62h-61v61h-61v-61h-61v-62h61v-61h61v61z m153 299v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="attention-circled" unicode="&#xe881;" d="M429 779q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m71-696v106q0 8-5 13t-12 5h-107q-8 0-13-5t-6-13v-106q0-8 6-13t13-6h107q7 0 12 6t5 13z m-1 192l10 346q0 7-6 10-5 5-13 5h-123q-8 0-13-5-6-3-6-10l10-346q0-6 5-10t14-4h103q8 0 13 4t6 10z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="check" unicode="&#xe883;" d="M786 331v-177q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-6-5-13-5-1 0-5 1-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v141q0 8 5 13l36 35q6 6 13 6 3 0 7-2 11-4 11-16z m129 273l-455-454q-13-14-31-14t-32 14l-240 240q-14 13-14 31t14 32l61 62q14 13 32 13t32-13l147-147 361 361q13 13 31 13t32-13l62-61q13-14 13-32t-13-32z" horiz-adv-x="928.6" />
+
+<glyph glyph-name="reschedule" unicode="&#xe884;" d="M186 140l116 116 0-292-276 16 88 86q-116 122-114 290t120 288q100 100 240 116l4-102q-100-16-172-88-88-88-90-213t84-217z m332 598l276-16-88-86q116-122 114-290t-120-288q-96-98-240-118l-2 104q98 16 170 88 88 88 90 213t-84 217l-114-116z" horiz-adv-x="820" />
+
+<glyph glyph-name="warning-empty" unicode="&#xe885;" d="M514 701q-49 0-81-55l-308-513q-32-55-11-95t87-40l625 0q65 0 87 40t-12 95l-307 513q-33 55-80 55z m0 105q106 0 169-107l308-513q63-105 12-199-52-93-177-93l-625 0q-123 0-177 93-53 92 11 199l309 513q62 107 170 107z m-69-652q0 69 69 69 67 0 67-69 0-67-67-67-69 0-69 67z m146 313q0-14-6-29l-71-179q-44 108-73 179-6 15-6 29 0 32 23 55t56 24 55-24 22-55z" horiz-adv-x="1026" />
+
+<glyph glyph-name="th-list" unicode="&#xf009;" d="M0 62q0-30 21-51t51-21h32q30 0 51 21t21 51-21 51-51 21h-32q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h32q30 0 51 21t21 51-21 51-51 21h-32q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h32q30 0 51 21t21 51-21 51-51 21h-32q-30 0-51-21t-21-51z m234-576q0-30 21-51t51-21h559q30 0 51 21t21 51-21 51-51 21h-559q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h559q30 0 51 21t21 51-21 51-51 21h-559q-30 0-51-21t-21-51z m0 288q0-30 21-51t51-21h559q30 0 51 21t21 51-21 51-51 21h-559q-30 0-51-21t-21-51z" horiz-adv-x="937.5" />
+
+<glyph glyph-name="th-thumb-empty" unicode="&#xf00b;" d="M0-66v286q0 22 15 37t37 16h286q21 0 37-16t15-37v-286q0-21-15-36t-37-15h-286q-22 0-37 15t-15 36z m0 546v286q0 21 15 36t37 15h286q21 0 37-15t15-36v-286q0-22-15-37t-37-16h-286q-21 0-37 16t-15 37z m88-510h214v214h-214v-214z m0 546h214v213h-214v-213z m459-582v286q0 22 15 37t37 16h286q21 0 37-16t15-37v-286q0-21-15-36t-37-15h-286q-21 0-37 15t-15 36z m0 546v286q0 21 15 36t37 15h286q22 0 37-15t15-36v-286q0-22-15-37t-37-16h-286q-21 0-37 16t-15 37z m88-510h215v214h-215v-214z m0 546h215v213h-215v-213z" horiz-adv-x="937.5" />
+
+<glyph glyph-name="github-circled" unicode="&#xf09b;" d="M429 779q116 0 215-58t156-156 57-215q0-140-82-252t-211-155q-15-3-22 4t-7 17q0 1 0 43t0 75q0 54-29 79 32 3 57 10t53 22 45 37 30 58 11 84q0 67-44 115 21 51-4 114-16 5-46-6t-51-25l-21-13q-52 15-107 15t-108-15q-8 6-23 15t-47 22-47 7q-25-63-5-114-44-48-44-115 0-47 12-83t29-59 45-37 52-22 57-10q-21-20-27-58-12-5-25-8t-32-3-36 12-31 35q-11 18-27 29t-28 14l-11 1q-12 0-16-2t-3-7 5-8 7-6l4-3q12-6 24-21t18-29l6-13q7-21 24-34t37-17 39-3 31 1l13 3q0-22 0-50t1-30q0-10-8-17t-22-4q-129 43-211 155t-82 252q0 117 58 215t155 156 216 58z m-267-616q2 4-3 7-6 1-8-1-1-4 4-7 5-3 7 1z m18-19q4 3-1 9-6 5-9 2-4-3 1-9 5-6 9-2z m16-25q6 4 0 11-4 7-9 3-5-3 0-10t9-4z m24-23q4 4-2 10-7 7-11 2-5-5 2-11 6-6 11-1z m32-14q1 6-8 9-8 2-10-4t7-9q8-3 11 4z m35-3q0 7-10 6-9 0-9-6 0-7 10-6 9 0 9 6z m32 5q-1 7-10 5-9-1-8-8t10-4 8 7z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="history" unicode="&#xf1da;" d="M857 350q0-87-34-166t-91-137-137-92-166-34q-96 0-183 41t-147 114q-4 6-4 13t5 11l76 77q6 5 14 5 9-1 13-7 41-53 100-82t126-29q58 0 110 23t92 61 61 91 22 111-22 111-61 91-92 61-110 23q-55 0-105-20t-90-57l77-77q17-16 8-38-10-23-33-23h-250q-15 0-25 11t-11 25v250q0 24 22 33 22 10 39-8l72-72q60 57 137 88t159 31q87 0 166-34t137-92 91-137 34-166z m-357 161v-250q0-8-5-13t-13-5h-178q-8 0-13 5t-5 13v35q0 8 5 13t13 5h125v197q0 8 5 13t12 5h36q8 0 13-5t5-13z" horiz-adv-x="857.1" />
+
+<glyph glyph-name="binoculars" unicode="&#xf1e5;" d="M393 671v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />
+</font>
+</defs>
+</svg> \ No newline at end of file
diff --git a/public/font/ifont.ttf b/public/font/ifont.ttf
new file mode 100644
index 0000000..8dd6019
--- /dev/null
+++ b/public/font/ifont.ttf
Binary files differ
diff --git a/public/font/ifont.woff b/public/font/ifont.woff
new file mode 100644
index 0000000..2efa0ae
--- /dev/null
+++ b/public/font/ifont.woff
Binary files differ
diff --git a/public/font/ifont.woff2 b/public/font/ifont.woff2
new file mode 100644
index 0000000..25ca736
--- /dev/null
+++ b/public/font/ifont.woff2
Binary files differ
diff --git a/public/img/favicon.png b/public/img/favicon.png
new file mode 100644
index 0000000..d5c0044
--- /dev/null
+++ b/public/img/favicon.png
Binary files differ
diff --git a/public/img/icinga-loader-light.gif b/public/img/icinga-loader-light.gif
new file mode 100644
index 0000000..04ac07f
--- /dev/null
+++ b/public/img/icinga-loader-light.gif
Binary files differ
diff --git a/public/img/icinga-loader.gif b/public/img/icinga-loader.gif
new file mode 100644
index 0000000..727f4b4
--- /dev/null
+++ b/public/img/icinga-loader.gif
Binary files differ
diff --git a/public/img/icinga-logo-big-dark.png b/public/img/icinga-logo-big-dark.png
new file mode 100644
index 0000000..c75d194
--- /dev/null
+++ b/public/img/icinga-logo-big-dark.png
Binary files differ
diff --git a/public/img/icinga-logo-big-dark.svg b/public/img/icinga-logo-big-dark.svg
new file mode 100644
index 0000000..ed45b8b
--- /dev/null
+++ b/public/img/icinga-logo-big-dark.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 286 99" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
+ <rect id="icinga-logo-big-dark" x="0" y="0" width="286" height="99" style="fill:none;"/>
+ <clipPath id="_clip1">
+ <rect id="icinga-logo-big-dark1" serif:id="icinga-logo-big-dark" x="0" y="0" width="286" height="99"/>
+ </clipPath>
+ <g clip-path="url(#_clip1)">
+ <g>
+ <path id="Mask" d="M78.018,47.989C77.199,48.807 76.791,49.804 76.791,50.979L76.791,93.158C76.791,94.336 77.213,95.332 78.057,96.15C78.901,96.968 79.884,97.377 81.01,97.377C82.186,97.377 83.182,96.968 84,96.15C84.818,95.332 85.227,94.336 85.227,93.158L85.227,50.979C85.227,49.804 84.806,48.807 83.962,47.989C83.118,47.171 82.133,46.762 81.01,46.762C79.833,46.762 78.835,47.171 78.018,47.989ZM102.215,59.76C102.751,58.508 103.491,57.421 104.439,56.501C105.384,55.58 106.484,54.853 107.736,54.316C108.987,53.779 110.33,53.51 111.762,53.51C113.96,53.51 115.916,54.123 117.629,55.351C119.341,56.577 120.582,58.138 121.348,60.03C122.165,61.716 123.444,62.56 125.183,62.56C126.512,62.56 127.534,62.125 128.25,61.257C128.966,60.387 129.323,59.442 129.323,58.418C129.323,58.112 129.286,57.755 129.209,57.345C129.132,56.937 128.991,56.553 128.787,56.195C128.071,54.559 127.139,53.063 125.987,51.708C124.838,50.355 123.521,49.178 122.039,48.18C120.555,47.183 118.944,46.417 117.207,45.88C115.469,45.343 113.653,45.074 111.762,45.074C109.155,45.074 106.713,45.561 104.439,46.531C102.162,47.503 100.183,48.832 98.494,50.52C96.808,52.206 95.478,54.189 94.507,56.462C93.535,58.739 93.05,61.155 93.05,63.709L93.05,80.352C93.05,82.909 93.548,85.312 94.545,87.56C95.543,89.811 96.885,91.779 98.571,93.465C100.259,95.153 102.238,96.495 104.515,97.492C106.789,98.489 109.205,98.987 111.762,98.987C113.756,98.987 115.647,98.706 117.438,98.143C119.226,97.58 120.861,96.777 122.345,95.728C123.828,94.68 125.144,93.442 126.295,92.009C127.445,90.578 128.353,88.991 129.018,87.253C129.221,86.794 129.323,86.257 129.323,85.643C129.323,84.621 128.953,83.676 128.211,82.806C127.47,81.937 126.46,81.502 125.183,81.502C124.517,81.502 123.814,81.732 123.074,82.192C122.332,82.653 121.782,83.24 121.426,83.955C120.607,85.849 119.341,87.42 117.629,88.672C115.916,89.926 113.96,90.551 111.762,90.551C110.382,90.551 109.066,90.282 107.814,89.746C106.559,89.209 105.461,88.481 104.515,87.56C103.568,86.641 102.815,85.555 102.253,84.301C101.69,83.049 101.41,81.732 101.41,80.352L101.41,63.709C101.41,62.329 101.678,61.014 102.215,59.76M141.672,46.762C140.494,46.762 139.496,47.171 138.68,47.989C137.861,48.807 137.453,49.804 137.453,50.979L137.453,93.158C137.453,94.336 137.874,95.332 138.718,96.15C139.562,96.968 140.546,97.377 141.672,97.377C142.846,97.377 143.843,96.968 144.661,96.15C145.479,95.332 145.888,94.336 145.888,93.158L145.888,50.979C145.888,49.804 145.467,48.807 144.623,47.989C143.779,47.171 142.795,46.762 141.672,46.762M185.536,50.673C183.849,48.986 181.867,47.643 179.593,46.646C177.317,45.65 174.901,45.151 172.346,45.151C169.788,45.151 167.373,45.65 165.099,46.646C162.822,47.643 160.842,48.986 159.154,50.673C157.468,52.36 156.138,54.328 155.166,56.577C154.195,58.827 153.71,61.231 153.71,63.787L153.71,93.158C153.71,94.336 154.119,95.332 154.937,96.15C155.754,96.968 156.752,97.377 157.927,97.377C159.103,97.377 160.087,96.968 160.881,96.15C161.672,95.332 162.068,94.336 162.068,93.158L162.068,63.787C162.068,62.405 162.338,61.09 162.875,59.837C163.412,58.585 164.14,57.497 165.06,56.577C165.98,55.657 167.066,54.928 168.319,54.392C169.571,53.855 170.913,53.587 172.346,53.587C173.776,53.587 175.118,53.855 176.372,54.392C177.624,54.928 178.723,55.657 179.67,56.577C180.615,57.497 181.356,58.585 181.893,59.837C182.431,61.09 182.698,62.405 182.698,63.787L182.698,93.158C182.698,94.336 183.121,95.332 183.965,96.15C184.807,96.968 185.792,97.377 186.917,97.377C188.092,97.377 189.076,96.968 189.869,96.15C190.661,95.332 191.057,94.336 191.057,93.158L191.057,63.787C191.057,61.231 190.559,58.827 189.562,56.577C188.565,54.328 187.223,52.36 185.536,50.673M232.623,69.922L224.264,69.922C223.089,69.922 222.103,70.332 221.311,71.149C220.519,71.968 220.123,72.939 220.123,74.064C220.123,75.239 220.519,76.237 221.311,77.054C222.103,77.872 223.089,78.281 224.264,78.281L228.405,78.281L228.405,80.352C228.405,81.732 228.137,83.049 227.601,84.301C227.063,85.555 226.335,86.641 225.415,87.56C224.494,88.481 223.408,89.209 222.156,89.746C220.901,90.282 219.56,90.551 218.129,90.551C216.749,90.551 215.432,90.282 214.18,89.746C212.926,89.209 211.827,88.481 210.882,87.56C209.936,86.641 209.195,85.555 208.658,84.301C208.121,83.049 207.852,81.732 207.852,80.352L207.852,63.787C207.852,62.405 208.121,61.09 208.658,59.837C209.195,58.585 209.924,57.497 210.844,56.577C211.765,55.657 212.85,54.928 214.103,54.392C215.355,53.855 216.697,53.587 218.129,53.587C220.326,53.587 222.282,54.201 223.996,55.428C225.709,56.654 226.974,58.24 227.792,60.182C228.148,61.001 228.685,61.614 229.403,62.023C230.118,62.433 230.834,62.637 231.55,62.637C232.879,62.637 233.901,62.202 234.617,61.332C235.333,60.464 235.69,59.519 235.69,58.496C235.69,58.189 235.678,57.894 235.652,57.613C235.627,57.332 235.563,57.065 235.462,56.808C234.745,55.07 233.812,53.485 232.662,52.054C231.511,50.623 230.181,49.396 228.674,48.372C227.165,47.351 225.517,46.557 223.727,45.994C221.937,45.433 220.071,45.151 218.129,45.151C215.572,45.151 213.156,45.637 210.882,46.608C208.606,47.579 206.625,48.908 204.937,50.596C203.252,52.283 201.909,54.265 200.913,56.54C199.915,58.815 199.416,61.231 199.416,63.787L199.416,80.352C199.416,82.909 199.915,85.312 200.913,87.56C201.909,89.811 203.252,91.778 204.937,93.465C206.625,95.152 208.606,96.495 210.882,97.492C213.156,98.488 215.572,98.986 218.129,98.986C220.685,98.986 223.102,98.488 225.376,97.492C227.65,96.495 229.633,95.152 231.32,93.465C233.008,91.778 234.349,89.811 235.346,87.56C236.343,85.312 236.842,82.909 236.842,80.352L236.842,74.064C236.842,72.888 236.432,71.903 235.614,71.11C234.796,70.318 233.8,69.922 232.623,69.922" style="fill:rgb(51,41,49);fill-rule:nonzero;"/>
+ <path d="M257.318,75.366C257.879,74.037 258.416,72.786 258.928,71.608C259.438,70.433 259.95,69.283 260.463,68.158C260.972,67.034 261.47,65.895 261.957,64.744C262.443,63.595 262.966,62.405 263.53,61.178L269.666,75.366L257.318,75.366ZM285.616,91.701L267.364,48.986C266.954,48.065 266.214,47.35 265.14,46.838C264.884,46.684 264.615,46.583 264.335,46.531C264.053,46.48 263.784,46.454 263.53,46.454C261.791,46.454 260.512,47.298 259.694,48.986L241.443,91.701C241.339,91.907 241.263,92.162 241.213,92.469C241.161,92.775 241.136,93.056 241.136,93.312C241.136,94.284 241.505,95.218 242.248,96.111C242.988,97.007 243.999,97.453 245.278,97.453C246.094,97.453 246.836,97.249 247.502,96.84C248.165,96.43 248.702,95.792 249.111,94.923C249.879,93.031 250.658,91.179 251.451,89.362C252.243,87.548 253.047,85.694 253.866,83.803L273.27,83.803L278.024,94.923C278.688,96.61 279.966,97.453 281.859,97.453C283.135,97.453 284.145,97.007 284.887,96.111C285.628,95.218 286,94.284 286,93.312C286,92.851 285.871,92.315 285.616,91.701ZM80.591,27.652C77.112,27.915 74.507,30.948 74.767,34.423C74.807,34.951 74.917,35.455 75.076,35.935L49.432,42.259C48.984,39.778 47.922,37.37 46.218,35.278C44.84,33.588 43.176,32.272 41.357,31.329L48.881,15.691C51.373,16.477 54.201,16.036 56.379,14.265C59.818,11.459 60.332,6.397 57.531,2.959C54.726,-0.48 49.662,-0.996 46.224,1.805C42.785,4.608 42.266,9.671 45.073,13.111C45.834,14.047 46.767,14.756 47.785,15.252L40.289,30.83C35.188,28.666 29.089,29.321 24.498,33.062C23.996,33.471 23.539,33.913 23.103,34.369L14.839,27.604C15.509,26.342 15.596,24.794 14.923,23.414C13.805,21.128 11.043,20.18 8.758,21.3C6.472,22.422 5.524,25.181 6.642,27.465C7.762,29.751 10.521,30.701 12.809,29.582C13.334,29.324 13.78,28.971 14.154,28.565L22.312,35.243C17.747,40.802 17.563,48.986 22.29,54.774C23.395,56.131 24.687,57.235 26.091,58.111L16.657,71.637C14.711,70.169 12.246,69.366 9.623,69.565C3.893,69.995 -0.402,74.987 0.03,80.712C0.459,86.443 5.452,90.736 11.181,90.305C16.909,89.875 21.201,84.881 20.771,79.154C20.57,76.476 19.364,74.121 17.56,72.405L27.113,58.707C31.657,61.075 37.195,61.055 41.792,58.478L47.17,67.462C46.447,67.974 45.932,68.769 45.823,69.717C45.616,71.493 46.888,73.099 48.665,73.305C50.439,73.507 52.043,72.237 52.251,70.46C52.455,68.687 51.185,67.081 49.408,66.876C49.005,66.829 48.614,66.868 48.244,66.962L42.804,57.874C43.213,57.601 43.615,57.31 44.005,56.993C48.186,53.582 50.109,48.429 49.595,43.434L75.559,37.031C76.722,39.104 79.009,40.435 81.536,40.245C85.014,39.987 87.621,36.954 87.359,33.478C87.097,30 84.066,27.393 80.591,27.652" style="fill:rgb(51,41,49);fill-rule:nonzero;"/>
+ </g>
+ </g>
+</svg>
diff --git a/public/img/icinga-logo-big.png b/public/img/icinga-logo-big.png
new file mode 100644
index 0000000..90f64e4
--- /dev/null
+++ b/public/img/icinga-logo-big.png
Binary files differ
diff --git a/public/img/icinga-logo-big.svg b/public/img/icinga-logo-big.svg
new file mode 100644
index 0000000..740cbc8
--- /dev/null
+++ b/public/img/icinga-logo-big.svg
@@ -0,0 +1 @@
+<svg width="286" height="99" viewBox="0 0 286 99" xmlns="http://www.w3.org/2000/svg"><title>icinga-logo-big</title><g fill="#FEFEFE"><path d="M78.018 47.989c-.819.818-1.227 1.815-1.227 2.99v42.179c0 1.178.422 2.174 1.266 2.992.844.818 1.827 1.227 2.953 1.227 1.176 0 2.172-.409 2.99-1.227.818-.818 1.227-1.814 1.227-2.992v-42.179c0-1.175-.421-2.172-1.265-2.99-.844-.818-1.829-1.227-2.952-1.227-1.177 0-2.175.409-2.992 1.227zM102.215 59.76c.536-1.252 1.276-2.339 2.224-3.259.945-.921 2.045-1.648 3.297-2.185 1.251-.537 2.594-.806 4.026-.806 2.198 0 4.154.613 5.867 1.841 1.712 1.226 2.953 2.787 3.719 4.679.817 1.686 2.096 2.53 3.835 2.53 1.329 0 2.351-.435 3.067-1.303.716-.87 1.073-1.815 1.073-2.839 0-.306-.037-.663-.114-1.073-.077-.408-.218-.792-.422-1.15-.716-1.636-1.648-3.132-2.8-4.487-1.149-1.353-2.466-2.53-3.948-3.528-1.484-.997-3.095-1.763-4.832-2.3-1.738-.537-3.554-.806-5.445-.806-2.607 0-5.049.487-7.323 1.457-2.277.972-4.256 2.301-5.945 3.989-1.686 1.686-3.016 3.669-3.987 5.942-.972 2.277-1.457 4.693-1.457 7.247v16.643c0 2.557.498 4.96 1.495 7.208.998 2.251 2.34 4.219 4.026 5.905 1.688 1.688 3.667 3.03 5.944 4.027 2.274.997 4.69 1.495 7.247 1.495 1.994 0 3.885-.281 5.676-.844 1.788-.563 3.423-1.366 4.907-2.415 1.483-1.048 2.799-2.286 3.95-3.719 1.15-1.431 2.058-3.018 2.723-4.756.203-.459.305-.996.305-1.61 0-1.022-.37-1.967-1.112-2.837-.741-.869-1.751-1.304-3.028-1.304-.666 0-1.369.23-2.109.69-.742.461-1.292 1.048-1.648 1.763-.819 1.894-2.085 3.465-3.797 4.717-1.713 1.254-3.669 1.879-5.867 1.879-1.38 0-2.696-.269-3.948-.805-1.255-.537-2.353-1.265-3.299-2.186-.947-.919-1.7-2.005-2.262-3.259-.563-1.252-.843-2.569-.843-3.949v-16.643c0-1.38.268-2.695.805-3.949M141.672 46.762c-1.178 0-2.176.409-2.992 1.227-.819.818-1.227 1.815-1.227 2.99v42.179c0 1.178.421 2.174 1.265 2.992.844.818 1.828 1.227 2.954 1.227 1.174 0 2.171-.409 2.989-1.227.818-.818 1.227-1.814 1.227-2.992v-42.179c0-1.175-.421-2.172-1.265-2.99-.844-.818-1.828-1.227-2.951-1.227M185.536 50.673c-1.687-1.687-3.669-3.03-5.943-4.027-2.276-.996-4.692-1.495-7.247-1.495-2.558 0-4.973.499-7.247 1.495-2.277.997-4.257 2.34-5.945 4.027-1.686 1.687-3.016 3.655-3.988 5.904-.971 2.25-1.456 4.654-1.456 7.21v29.371c0 1.178.409 2.174 1.227 2.992.817.818 1.815 1.227 2.99 1.227 1.176 0 2.16-.409 2.954-1.227.791-.818 1.187-1.814 1.187-2.992v-29.371c0-1.382.27-2.697.807-3.95.537-1.252 1.265-2.34 2.185-3.26.92-.92 2.006-1.649 3.259-2.185 1.252-.537 2.594-.805 4.027-.805 1.43 0 2.772.268 4.026.805 1.252.536 2.351 1.265 3.298 2.185.945.92 1.686 2.008 2.223 3.26.538 1.253.805 2.568.805 3.95v29.371c0 1.178.423 2.174 1.267 2.992.842.818 1.827 1.227 2.952 1.227 1.175 0 2.159-.409 2.952-1.227.792-.818 1.188-1.814 1.188-2.992v-29.371c0-2.556-.498-4.96-1.495-7.21-.997-2.249-2.339-4.217-4.026-5.904M232.623 69.922h-8.359c-1.175 0-2.161.41-2.953 1.227-.792.819-1.188 1.79-1.188 2.915 0 1.175.396 2.173 1.188 2.99.792.818 1.778 1.227 2.953 1.227h4.141v2.071c0 1.38-.268 2.697-.804 3.949-.538 1.254-1.266 2.34-2.186 3.259-.921.921-2.007 1.649-3.259 2.186-1.255.536-2.596.805-4.027.805-1.38 0-2.697-.269-3.949-.805-1.254-.537-2.353-1.265-3.298-2.186-.946-.919-1.687-2.005-2.224-3.259-.537-1.252-.806-2.569-.806-3.949v-16.565c0-1.382.269-2.697.806-3.95.537-1.252 1.266-2.34 2.186-3.26.921-.92 2.006-1.649 3.259-2.185 1.252-.537 2.594-.805 4.026-.805 2.197 0 4.153.614 5.867 1.841 1.713 1.226 2.978 2.812 3.796 4.754.356.819.893 1.432 1.611 1.841.715.41 1.431.614 2.147.614 1.329 0 2.351-.435 3.067-1.305.716-.868 1.073-1.813 1.073-2.836 0-.307-.012-.602-.038-.883-.025-.281-.089-.548-.19-.805-.717-1.738-1.65-3.323-2.8-4.754-1.151-1.431-2.481-2.658-3.988-3.682-1.509-1.021-3.157-1.815-4.947-2.378-1.79-.561-3.656-.843-5.598-.843-2.557 0-4.973.486-7.247 1.457-2.276.971-4.257 2.3-5.945 3.988-1.685 1.687-3.028 3.669-4.024 5.944-.998 2.275-1.497 4.691-1.497 7.247v16.565c0 2.557.499 4.96 1.497 7.208.996 2.251 2.339 4.218 4.024 5.905 1.688 1.687 3.669 3.03 5.945 4.027 2.274.996 4.69 1.494 7.247 1.494 2.556 0 4.973-.498 7.247-1.494 2.274-.997 4.257-2.34 5.944-4.027 1.688-1.687 3.029-3.654 4.026-5.905.997-2.248 1.496-4.651 1.496-7.208v-6.288c0-1.176-.41-2.161-1.228-2.954-.818-.792-1.814-1.188-2.991-1.188" id="Mask"/><path d="M257.318 75.366c.561-1.329 1.098-2.58 1.61-3.758.51-1.175 1.022-2.325 1.535-3.45.509-1.124 1.007-2.263 1.494-3.414.486-1.149 1.009-2.339 1.573-3.566l6.136 14.188h-12.348zm28.298 16.335l-18.252-42.715c-.41-.921-1.15-1.636-2.224-2.148-.256-.154-.525-.255-.805-.307-.282-.051-.551-.077-.805-.077-1.739 0-3.018.844-3.836 2.532l-18.251 42.715c-.104.206-.18.461-.23.768-.052.306-.077.587-.077.843 0 .972.369 1.906 1.112 2.799.74.896 1.751 1.342 3.03 1.342.816 0 1.558-.204 2.224-.613.663-.41 1.2-1.048 1.609-1.917.768-1.892 1.547-3.744 2.34-5.561.792-1.814 1.596-3.668 2.415-5.559h19.404l4.754 11.12c.664 1.687 1.942 2.53 3.835 2.53 1.276 0 2.286-.446 3.028-1.342.741-.893 1.113-1.827 1.113-2.799 0-.461-.129-.997-.384-1.611zM80.591 27.652c-3.479.263-6.084 3.296-5.824 6.771.04.528.15 1.032.309 1.512l-25.644 6.324c-.448-2.481-1.51-4.889-3.214-6.981-1.378-1.69-3.042-3.006-4.861-3.949l7.524-15.638c2.492.786 5.32.345 7.498-1.426 3.439-2.806 3.953-7.868 1.152-11.306-2.805-3.439-7.869-3.955-11.307-1.154-3.439 2.803-3.958 7.866-1.151 11.306.761.936 1.694 1.645 2.712 2.141l-7.496 15.578c-5.101-2.164-11.2-1.509-15.791 2.232-.502.409-.959.851-1.395 1.307l-8.264-6.765c.67-1.262.757-2.81.084-4.19-1.118-2.286-3.88-3.234-6.165-2.114-2.286 1.122-3.234 3.881-2.116 6.165 1.12 2.286 3.879 3.236 6.167 2.117.525-.258.971-.611 1.345-1.017l8.158 6.678c-4.565 5.559-4.749 13.743-.022 19.531 1.105 1.357 2.397 2.461 3.801 3.337l-9.434 13.526c-1.946-1.468-4.411-2.271-7.034-2.072-5.73.43-10.025 5.422-9.593 11.147.429 5.731 5.422 10.024 11.151 9.593 5.728-.43 10.02-5.424 9.59-11.151-.201-2.678-1.407-5.033-3.211-6.749l9.553-13.698c4.544 2.368 10.082 2.348 14.679-.229l5.378 8.984c-.723.512-1.238 1.307-1.347 2.255-.207 1.776 1.065 3.382 2.842 3.588 1.774.202 3.378-1.068 3.586-2.845.204-1.773-1.066-3.379-2.843-3.584-.403-.047-.794-.008-1.164.086l-5.44-9.088c.409-.273.811-.564 1.201-.881 4.181-3.411 6.104-8.564 5.59-13.559l25.964-6.403c1.163 2.073 3.45 3.404 5.977 3.214 3.478-.258 6.085-3.291 5.823-6.767-.262-3.478-3.293-6.085-6.768-5.826"/></g></svg>
diff --git a/public/img/icinga-logo-compact-inverted.svg b/public/img/icinga-logo-compact-inverted.svg
new file mode 100644
index 0000000..e71c632
--- /dev/null
+++ b/public/img/icinga-logo-compact-inverted.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 41 35" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;">
+ <g>
+ <path d="M17.444,16.237L9.477,27.661M17.444,16.237L33.957,12.164M17.757,16.572L22.381,24.296M15.795,14.324L9.01,8.77M18.425,13.096L22.419,4.799" style="fill:none;fill-rule:nonzero;stroke:rgb(126,129,130);stroke-width:0.75px;"/>
+ <path d="M5.234,28.221C5.083,26.219 6.585,24.474 8.588,24.324C10.591,24.171 12.337,25.675 12.486,27.677C12.636,29.679 11.136,31.425 9.133,31.575C7.13,31.726 5.384,30.224 5.234,28.221" style="fill:rgb(126,129,130);fill-rule:nonzero;"/>
+ <path d="M21.246,24.377C21.317,23.756 21.878,23.312 22.5,23.383C23.121,23.455 23.565,24.017 23.494,24.637C23.42,25.258 22.86,25.702 22.24,25.631C21.619,25.56 21.174,24.998 21.246,24.377M7.546,9.604C7.155,8.805 7.486,7.839 8.286,7.449C9.085,7.057 10.05,7.389 10.442,8.188C10.832,8.987 10.502,9.953 9.702,10.343C8.902,10.734 7.938,10.402 7.546,9.604M31.367,12.036C31.275,10.821 32.186,9.762 33.403,9.669C34.618,9.578 35.678,10.489 35.77,11.705C35.861,12.921 34.95,13.98 33.733,14.072C32.518,14.163 31.458,13.252 31.367,12.036M21.386,0.631C22.588,-0.348 24.359,-0.168 25.34,1.034C26.32,2.236 26.139,4.008 24.937,4.988C23.734,5.967 21.963,5.786 20.984,4.584C20.002,3.381 20.184,1.611 21.386,0.631M13.79,11.561C16.099,9.679 19.499,10.025 21.384,12.334C23.264,14.644 22.918,18.043 20.61,19.928C18.298,21.808 14.899,21.463 13.018,19.152C11.132,16.843 11.481,13.442 13.79,11.561" style="fill:rgb(126,129,130);fill-rule:nonzero;"/>
+ </g>
+</svg>
diff --git a/public/img/icinga-logo-compact.svg b/public/img/icinga-logo-compact.svg
new file mode 100644
index 0000000..8b6a531
--- /dev/null
+++ b/public/img/icinga-logo-compact.svg
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="41px" height="35px" viewBox="0 82.75 41 35" enable-background="new 0 82.75 41 35" xml:space="preserve">
+<g>
+ <path fill="none" stroke="#FFFFFF" stroke-width="0.75" d="M17.444,98.987l-7.967,11.424 M17.444,98.987l16.513-4.073
+ M17.757,99.322l4.624,7.724 M15.795,97.074L9.01,91.52 M18.425,95.846l3.994-8.297"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter" filterUnits="userSpaceOnUse" x="5.224" y="107.062" width="7.272" height="7.272">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="5.224" y="107.062" width="7.272" height="7.272" id="f_1_">
+ <g filter="url(#Adobe_OpacityMaskFilter)">
+ <polygon id="e_1_" fill="#FFFFFF" points="5.224,107.062 5.224,114.335 12.497,114.335 12.497,107.062 "/>
+ </g>
+ </mask>
+ <path mask="url(#f_1_)" fill="#FFFFFF" d="M5.234,110.971c-0.151-2.002,1.351-3.747,3.354-3.897
+ c2.003-0.153,3.749,1.351,3.898,3.353c0.15,2.002-1.35,3.748-3.353,3.898C7.13,114.476,5.384,112.974,5.234,110.971"/>
+ <path fill="#FFFFFF" d="M21.246,107.127c0.071-0.621,0.632-1.065,1.254-0.994c0.621,0.072,1.065,0.634,0.994,1.254
+ c-0.074,0.621-0.634,1.065-1.254,0.994C21.619,108.31,21.174,107.748,21.246,107.127 M7.546,92.354
+ c-0.391-0.799-0.06-1.765,0.74-2.155c0.799-0.392,1.764-0.06,2.156,0.739c0.39,0.799,0.06,1.765-0.74,2.155
+ C8.902,93.484,7.938,93.152,7.546,92.354 M31.367,94.786c-0.092-1.215,0.819-2.274,2.036-2.367c1.215-0.091,2.275,0.82,2.367,2.036
+ c0.091,1.216-0.82,2.275-2.037,2.367C32.518,96.913,31.458,96.002,31.367,94.786 M21.386,83.381
+ c1.202-0.979,2.973-0.799,3.954,0.403c0.98,1.202,0.799,2.974-0.403,3.954c-1.203,0.979-2.974,0.798-3.953-0.404
+ C20.002,86.131,20.184,84.361,21.386,83.381 M13.79,94.311c2.309-1.882,5.709-1.536,7.594,0.773c1.88,2.31,1.534,5.709-0.774,7.594
+ c-2.312,1.88-5.711,1.535-7.592-0.776C11.132,99.593,11.481,96.192,13.79,94.311"/>
+</g>
+</svg>
diff --git a/public/img/icinga-logo-dark.svg b/public/img/icinga-logo-dark.svg
new file mode 100644
index 0000000..44be390
--- /dev/null
+++ b/public/img/icinga-logo-dark.svg
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 101 35" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;">
+ <g transform="matrix(1,0,0,1,-327,0)">
+ <g id="icinga-logo-dark" transform="matrix(1,0,0,1,327.107,0)">
+ <rect x="0" y="0" width="100" height="35" style="fill:none;"/>
+ <clipPath id="_clip1">
+ <rect x="0" y="0" width="100" height="35"/>
+ </clipPath>
+ <g clip-path="url(#_clip1)">
+ <g>
+ <path d="M26.85,32.573C26.85,32.975 27.011,33.324 27.306,33.619C27.6,33.914 27.922,34.048 28.325,34.048C28.727,34.048 29.075,33.914 29.371,33.619C29.665,33.324 29.8,32.975 29.8,32.573L29.8,17.825C29.8,17.423 29.665,17.074 29.371,16.779C29.075,16.484 28.727,16.35 28.325,16.35C27.922,16.35 27.574,16.484 27.279,16.779C26.984,17.074 26.85,17.423 26.85,17.825L26.85,32.573Z" style="fill:rgb(51,41,49);"/>
+ <g transform="matrix(1,0,0,1,32,15.61)">
+ <path d="M0.535,12.484C0.535,13.369 0.723,14.227 1.072,15.004C1.42,15.782 1.876,16.479 2.466,17.069C3.056,17.659 3.753,18.115 4.557,18.463C5.335,18.812 6.193,19 7.077,19C7.775,19 8.445,18.892 9.062,18.705C9.679,18.517 10.268,18.222 10.778,17.847C11.288,17.498 11.77,17.069 12.173,16.56C12.575,16.05 12.87,15.514 13.111,14.897C13.191,14.736 13.218,14.548 13.218,14.334C13.218,13.985 13.084,13.637 12.843,13.342C12.575,13.047 12.226,12.886 11.77,12.886C11.529,12.886 11.288,12.966 11.046,13.128C10.778,13.288 10.59,13.503 10.456,13.744C10.161,14.414 9.733,14.951 9.143,15.38C8.526,15.836 7.855,16.05 7.077,16.05C6.595,16.05 6.139,15.942 5.71,15.755C5.254,15.567 4.879,15.326 4.557,15.004C4.209,14.683 3.941,14.307 3.753,13.851C3.565,13.422 3.458,12.966 3.458,12.484L3.458,6.665C3.458,6.182 3.565,5.727 3.753,5.271C3.941,4.842 4.182,4.466 4.531,4.144C4.852,3.822 5.227,3.554 5.683,3.367C6.112,3.179 6.568,3.099 7.077,3.099C7.855,3.099 8.526,3.314 9.143,3.742C9.733,4.171 10.161,4.707 10.43,5.378C10.724,5.968 11.154,6.263 11.77,6.263C12.226,6.263 12.601,6.102 12.843,5.807C13.084,5.512 13.218,5.163 13.218,4.815C13.218,4.707 13.218,4.573 13.191,4.439C13.165,4.305 13.111,4.171 13.031,4.037C12.789,3.474 12.468,2.938 12.065,2.455C11.663,1.999 11.181,1.57 10.671,1.221C10.161,0.873 9.598,0.605 8.982,0.417C8.365,0.23 7.748,0.149 7.077,0.149C6.166,0.149 5.308,0.31 4.531,0.659C3.726,1.007 3.029,1.463 2.439,2.053C1.849,2.643 1.393,3.34 1.045,4.117C0.696,4.922 0.535,5.78 0.535,6.665L0.535,12.484Z" style="fill:rgb(51,41,49);"/>
+ </g>
+ <path d="M48.06,32.573C48.06,32.975 48.221,33.324 48.516,33.619C48.811,33.914 49.133,34.048 49.535,34.048C49.937,34.048 50.286,33.914 50.581,33.619C50.875,33.324 51.01,32.975 51.01,32.573L51.01,17.825C51.01,17.423 50.875,17.074 50.581,16.779C50.286,16.484 49.937,16.35 49.535,16.35C49.133,16.35 48.784,16.484 48.489,16.779C48.194,17.074 48.06,17.423 48.06,17.825L48.06,32.573ZM53.745,32.573C53.745,32.975 53.879,33.324 54.174,33.619C54.469,33.914 54.818,34.048 55.22,34.048C55.622,34.048 55.971,33.914 56.266,33.619C56.534,33.324 56.668,32.975 56.668,32.573L56.668,22.303C56.668,21.82 56.775,21.365 56.963,20.909C57.151,20.48 57.392,20.105 57.714,19.782C58.036,19.461 58.411,19.193 58.867,19.005C59.296,18.817 59.751,18.737 60.261,18.737C60.77,18.737 61.226,18.817 61.682,19.005C62.111,19.193 62.487,19.461 62.835,19.782C63.157,20.105 63.425,20.48 63.613,20.909C63.8,21.365 63.881,21.82 63.881,22.303L63.881,32.573C63.881,32.975 64.042,33.324 64.337,33.619C64.632,33.914 64.953,34.048 65.356,34.048C65.758,34.048 66.106,33.914 66.401,33.619C66.669,33.324 66.804,32.975 66.804,32.573L66.804,22.303C66.804,21.418 66.643,20.56 66.294,19.782C65.946,19.005 65.463,18.308 64.873,17.718C64.283,17.128 63.586,16.645 62.808,16.297C62.004,15.948 61.146,15.787 60.261,15.787C59.376,15.787 58.518,15.948 57.741,16.297C56.936,16.645 56.239,17.128 55.649,17.718C55.059,18.308 54.603,19.005 54.255,19.782C53.906,20.56 53.745,21.418 53.745,22.303L53.745,32.573Z" style="fill:rgb(51,41,49);"/>
+ <g transform="matrix(1,0,0,1,69,15.61)">
+ <path d="M0.726,12.484C0.726,13.369 0.913,14.227 1.262,15.004C1.611,15.782 2.067,16.479 2.657,17.069C3.247,17.659 3.944,18.115 4.748,18.463C5.525,18.812 6.384,19 7.268,19C8.154,19 9.012,18.812 9.816,18.463C10.594,18.115 11.291,17.659 11.881,17.069C12.471,16.479 12.954,15.782 13.302,15.004C13.65,14.227 13.811,13.369 13.811,12.484L13.811,10.285C13.811,9.883 13.677,9.534 13.382,9.239C13.087,8.971 12.739,8.837 12.337,8.837L9.414,8.837C9.012,8.837 8.663,8.971 8.395,9.266C8.1,9.561 7.966,9.883 7.966,10.285C7.966,10.687 8.1,11.036 8.395,11.331C8.663,11.626 9.012,11.76 9.414,11.76L10.862,11.76L10.862,12.484C10.862,12.966 10.781,13.422 10.594,13.851C10.406,14.307 10.138,14.683 9.816,15.004C9.495,15.326 9.119,15.567 8.69,15.755C8.234,15.942 7.778,16.05 7.268,16.05C6.786,16.05 6.33,15.942 5.901,15.755C5.445,15.567 5.07,15.326 4.748,15.004C4.4,14.683 4.158,14.307 3.971,13.851C3.783,13.422 3.676,12.966 3.676,12.484L3.676,6.692C3.676,6.209 3.783,5.754 3.971,5.297C4.158,4.868 4.4,4.493 4.722,4.171C5.043,3.849 5.418,3.581 5.874,3.394C6.303,3.206 6.759,3.126 7.268,3.126C8.046,3.126 8.717,3.34 9.333,3.769C9.923,4.198 10.352,4.761 10.647,5.431C10.781,5.727 10.969,5.941 11.21,6.075C11.452,6.209 11.72,6.29 11.961,6.29C12.417,6.29 12.792,6.129 13.034,5.834C13.275,5.539 13.409,5.19 13.409,4.842L13.409,4.52C13.382,4.439 13.355,4.332 13.329,4.252C13.087,3.635 12.766,3.099 12.364,2.589C11.961,2.079 11.479,1.651 10.969,1.302C10.433,0.954 9.843,0.659 9.226,0.471C8.609,0.283 7.939,0.176 7.268,0.176C6.384,0.176 5.525,0.337 4.748,0.686C3.944,1.034 3.247,1.489 2.657,2.079C2.067,2.67 1.611,3.367 1.262,4.144C0.913,4.949 0.726,5.807 0.726,6.692L0.726,12.484Z" style="fill:rgb(51,41,49);"/>
+ </g>
+ <path d="M94.289,26.352L89.972,26.352C90.159,25.896 90.347,25.44 90.535,25.038C90.723,24.636 90.883,24.234 91.071,23.831C91.259,23.429 91.419,23.027 91.607,22.624C91.768,22.223 91.956,21.82 92.143,21.391L94.289,26.352ZM84.421,32.063C84.394,32.144 84.367,32.225 84.34,32.331C84.313,32.438 84.313,32.546 84.313,32.626C84.313,32.975 84.448,33.297 84.716,33.592C84.957,33.914 85.306,34.075 85.762,34.075C86.057,34.075 86.298,33.994 86.539,33.86C86.781,33.726 86.968,33.485 87.102,33.19C87.371,32.519 87.639,31.876 87.934,31.232C88.202,30.615 88.47,29.972 88.765,29.302L95.549,29.302L97.212,33.19C97.453,33.78 97.882,34.075 98.552,34.075C99.008,34.075 99.357,33.914 99.625,33.592C99.866,33.297 100,32.975 100,32.626C100,32.466 99.947,32.278 99.866,32.063L93.484,17.127C93.351,16.806 93.082,16.565 92.706,16.377C92.626,16.323 92.519,16.297 92.439,16.27C92.331,16.243 92.224,16.243 92.143,16.243C91.527,16.243 91.098,16.538 90.803,17.127L84.421,32.063Z" style="fill:rgb(51,41,49);"/>
+ <path d="M12.22,16.237L4.253,27.661M12.22,16.237L28.733,12.164M12.533,16.572L17.157,24.296M10.571,14.325L3.786,8.77M13.201,13.096L17.195,4.798" style="fill:none;stroke:rgb(51,41,49);stroke-width:0.75px;"/>
+ <g transform="matrix(1,0,0,1,0,23.61)">
+ <path d="M0.01,4.61C-0.141,2.608 1.361,0.863 3.364,0.713C5.367,0.56 7.113,2.063 7.262,4.065C7.412,6.068 5.912,7.814 3.909,7.964C1.906,8.115 0.16,6.613 0.01,4.61" style="fill:rgb(51,41,49);"/>
+ </g>
+ <path d="M16.022,24.376C16.093,23.756 16.654,23.311 17.276,23.382C17.897,23.454 18.341,24.016 18.27,24.636C18.196,25.257 17.636,25.702 17.016,25.631C16.395,25.559 15.95,24.998 16.022,24.376M2.322,9.603C1.931,8.805 2.262,7.839 3.062,7.448C3.861,7.057 4.826,7.388 5.218,8.187C5.608,8.986 5.278,9.952 4.478,10.343C3.678,10.734 2.713,10.403 2.322,9.603M26.143,12.036C26.051,10.821 26.962,9.761 28.179,9.669C29.394,9.578 30.454,10.489 30.546,11.705C30.637,12.921 29.726,13.981 28.509,14.072C27.294,14.163 26.234,13.252 26.143,12.036M16.162,0.631C17.364,-0.349 19.135,-0.168 20.116,1.034C21.096,2.236 20.915,4.007 19.713,4.988C18.51,5.966 16.739,5.786 15.76,4.584C14.778,3.381 14.96,1.611 16.162,0.631M8.566,11.561C10.875,9.679 14.275,10.025 16.16,12.335C18.04,14.644 17.694,18.044 15.386,19.927C13.074,21.808 9.675,21.462 7.794,19.152C5.908,16.843 6.257,13.443 8.566,11.561" style="fill:rgb(51,41,49);"/>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/public/img/icinga-logo-inverted.svg b/public/img/icinga-logo-inverted.svg
new file mode 100644
index 0000000..ec918e3
--- /dev/null
+++ b/public/img/icinga-logo-inverted.svg
@@ -0,0 +1 @@
+<svg height="35" viewBox="0 0 100 35" width="100" xmlns="http://www.w3.org/2000/svg"><path d="m85.0767 33.7594c.671 0 1.288.081 1.905.268.616.188 1.179.456 1.689.804.51.349.992.778 1.394 1.234.403.483.724 1.019.966 1.582.08.134.134.268.16.402.027.134.027.268.027.376 0 .348-.134.697-.375.992-.242.295-.617.456-1.073.456-.616 0-1.046-.295-1.34-.885-.269-.671-.697-1.207-1.287-1.636-.617-.428-1.288-.643-2.066-.643-.509 0-.965.08-1.394.268-.456.187-.831.455-1.152.777-.349.322-.59.698-.778 1.127-.1566667.38-.2570833.7593056-.2862037 1.1546991l-.0087963.2393009v5.819c0 .482.107.938.295 1.367.188.456.456.832.804 1.153.322.322.697.563 1.153.751.429.187.885.295 1.367.295.778 0 1.449-.214 2.066-.67.59-.429 1.018-.966 1.313-1.636.134-.241.322-.456.59-.616.242-.162.483-.242.724-.242.456 0 .805.161 1.073.456.241.295.375.643.375.992 0 .1605-.0151875.306375-.0565312.4372031l-.0504688.1257969c-.241.617-.536 1.153-.938 1.663-.403.509-.885.938-1.395 1.287-.51.375-1.099.67-1.716.858-.617.187-1.287.295-1.985.295-.884 0-1.742-.188-2.52-.537-.804-.348-1.501-.804-2.091-1.394s-1.046-1.287-1.394-2.065c-.305375-.679875-.4874844-1.4217656-.5282402-2.1894961l-.0087598-.3305039v-5.819c0-.885.161-1.743.51-2.548.348-.777.804-1.474 1.394-2.064s1.287-1.046 2.092-1.394c.777-.349 1.635-.51 2.546-.51zm37.1914.027c.671 0 1.341.107 1.958.295s1.207.483 1.743.831c.51.349.992.777 1.395 1.287.402.51.723 1.046.965 1.663l.08.268v.322c0 .348-.134.697-.375.992-.242.295-.617.456-1.073.456-.241 0-.509-.081-.751-.215-.241-.134-.429-.348-.563-.644-.295-.67-.724-1.233-1.314-1.662-.616-.429-1.287-.643-2.065-.643-.509 0-.965.08-1.394.268-.456.187-.831.455-1.152.777-.322.322-.564.697-.751 1.126-.156667.3808333-.257083.7602778-.286204 1.1556944l-.008796.2393056v5.792c0 .482.107.938.295 1.367.187.456.429.832.777 1.153.322.322.697.563 1.153.751.429.187.885.295 1.367.295.51 0 .966-.108 1.422-.295.429-.188.805-.429 1.126-.751.322-.321.59-.697.778-1.153.155833-.3575.238056-.73375.261134-1.1281713l.006866-.2388287v-.724h-1.448c-.402 0-.751-.134-1.019-.429-.295-.295-.429-.644-.429-1.046s.134-.724.429-1.019c.2345-.258125.531016-.3929844.870789-.422666l.148211-.006334h2.923c.402 0 .75.134 1.045.402.258125.258125.392984.5575938.422666.8977363l.006334.1482637v2.199c0 .885-.161 1.743-.509 2.52-.348.778-.831 1.475-1.421 2.065s-1.287 1.046-2.065 1.394c-.804.349-1.662.537-2.548.537-.884 0-1.743-.188-2.52-.537-.804-.348-1.501-.804-2.091-1.394s-1.046-1.287-1.395-2.065c-.305375-.679875-.486719-1.4217656-.527283-2.1894961l-.008717-.3305039v-5.792c0-.885.187-1.743.536-2.548.349-.777.805-1.474 1.395-2.065.59-.59 1.287-1.045 2.091-1.393.777-.349 1.636-.51 2.52-.51zm15.8745.4566c.081 0 .188 0 .296.027.053333.018.118667.0355556.18.0612593l.087.0457407c.322286.1611429.565959.3612245.712758.6172478l.065242.1327522 6.382 14.936c.081.215.134.403.134.563 0 .349-.134.671-.375.966-.268.322-.617.483-1.073.483-.614167 0-1.025826-.2478819-1.275803-.7436458l-.064197-.1413542-1.663-3.888h-6.784c-.295.67-.563 1.313-.831 1.93-.295.644-.563 1.287-.832 1.958-.134.295-.321.536-.563.67-.241.134-.482.215-.777.215-.399 0-.716078-.1232656-.950564-.3697969l-.095436-.1132031c-.268-.295-.403-.617-.403-.966 0-.08 0-.188.027-.295l.027-.0985926.054-.1694074 6.382-14.936c.295-.589.724-.884 1.34-.884zm-63.8175.1067c.402 0 .75.134 1.046.429.25725.258125.3927656.5575938.422625.8977363l.006375.1482637v14.748c0 .402-.135.751-.429 1.046-.296.295-.644.429-1.046.429-.403 0-.725-.134-1.019-.429-.258125-.258125-.4136562-.5575937-.4485059-.8977363l-.0074941-.1482637v-14.748c0-.402.134-.751.429-1.046s.643-.429 1.046-.429zm21.21 0c.402 0 .751.134 1.046.429.25725.258125.3927656.5575938.422625.8977363l.006375.1482637v14.748c0 .402-.135.751-.429 1.046-.295.295-.644.429-1.046.429s-.724-.134-1.019-.429c-.258125-.258125-.4136562-.5575937-.4485059-.8977363l-.0074941-.1482637v-14.748c0-.402.134-.751.429-1.046s.644-.429 1.046-.429zm10.7255-.563c.885 0 1.743.161 2.547.51.778.348 1.475.831 2.065 1.421s1.073 1.287 1.421 2.064c.305375.68075.466813 1.42275.5024 2.1904941l.0076.3305059v10.27c0 .402-.135.751-.403 1.046-.295.295-.643.429-1.045.429-.403 0-.724-.134-1.019-.429-.258125-.258125-.413656-.5575937-.448506-.8977363l-.007494-.1482637v-10.27c0-.483-.081-.938-.268-1.394-.188-.429-.456-.804-.778-1.127-.348-.321-.724-.589-1.153-.777-.456-.188-.912-.268-1.421-.268-.51 0-.965.08-1.394.268-.456.188-.831.456-1.153.777-.322.323-.563.698-.751 1.127-.156667.38-.257083.7593056-.286204 1.1546991l-.008796.2393009v10.27c0 .402-.134.751-.402 1.046-.295.295-.644.429-1.046.429s-.751-.134-1.046-.429c-.258125-.258125-.3929844-.5575937-.422666-.8977363l-.006334-.1482637v-10.27c0-.885.161-1.743.51-2.521.348-.777.804-1.474 1.394-2.064s1.287-1.073 2.092-1.421c.777-.349 1.635-.51 2.52-.51zm-40.145-14.7524c.98 1.202.799 2.973-.403 3.954-.7135427.5800871-1.6269138.7527777-2.4563199.5464512l-2.6461727 5.4979481c.5779292.3227096 1.1058725.7577926 1.5497926 1.3018007.5578806.6851842.919745 1.4664392 1.0924916 2.2756424l8.9486237-2.2073659c-.0292334-.1184503-.0490052-.2409327-.0584153-.3666765-.092-1.215.819-2.275 2.036-2.367 1.215-.091 2.275.82 2.367 2.036.091 1.216-.82 2.276-2.037 2.367-.8211665.061503-1.5715316-.3346905-1.9997973-.9734027l-9.1509619 2.2572841c.1199705 1.6931832-.5575939 3.4169998-1.9719408 4.5715186-.0916538.0745282-.1850159.1455597-.279924.2131107l1.9393556 3.2387843c.0753599-.0066904.1525329-.005914.2306684.003005.621.072 1.065.634.994 1.254-.074.621-.634 1.066-1.254.995-.621-.072-1.066-.633-.994-1.255.0309663-.2704096.1551414-.5075304.3369336-.6830715l-1.8928981-3.1609715c-1.5260558.7939481-3.3251285.7992664-4.8352112.0718318l-3.3672948 4.8286847c.5634239.5877707.9336449 1.3674834.9987705 2.2425265.15 2.003-1.35 3.749-3.353 3.899-2.003.151-3.749-1.351-3.899-3.354-.151-2.002 1.351-3.747 3.354-3.897.8569941-.0654619 1.6669418.1722239 2.3248898.6224372l3.2886724-4.7148027c-.4356465-.2913976-.8358066-.6517899-1.1839622-1.0795345-1.6157836-1.9781784-1.5911265-4.7571267-.0989533-6.6962604l-2.8636784-2.3463302c-.1044197.0911892-.2224378.1702938-.3529683.2340906-.8.391-1.765.06-2.156-.74-.391-.798-.06-1.764.74-2.155.799-.391 1.764-.06 2.156.739.2078646.4258559.2111957.8991522.0478454 1.3075444l2.9271152 2.3991038c.1179203-.1155836.2421499-.22679.3726394-.3331482 1.5583374-1.2701564 3.6136103-1.5254897 5.3647177-.8471552l2.6265341-5.4562041c-.2974624-.1674645-.5690752-.392321-.7975518-.6728407-.982-1.203-.8-2.973.402-3.953s2.973-.799 3.954.403zm74.173 25.3177-2.146-4.961-.367037.8304444c-.05863.135-.115296.2688889-.168963.4025556l-1.072 2.414c-.094.201-.188.4155-.281875.63675l-.281125.67725z" fill="#7e8182" transform="translate(-46 -18)"/></svg> \ No newline at end of file
diff --git a/public/img/icinga-logo.png b/public/img/icinga-logo.png
new file mode 100644
index 0000000..670f45c
--- /dev/null
+++ b/public/img/icinga-logo.png
Binary files differ
diff --git a/public/img/icinga-logo.svg b/public/img/icinga-logo.svg
new file mode 100644
index 0000000..49a2ef5
--- /dev/null
+++ b/public/img/icinga-logo.svg
@@ -0,0 +1,32 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="35" viewBox="0 0 100 35" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <polygon id="a" points=".535 19 13.218 19 13.218 .149 .535 .149"/>
+ <polygon id="c" points=".726 19 13.811 19 13.811 .176 .726 .176 .726 19"/>
+ <polygon id="e" points="0 .703 0 7.975 7.273 7.975 7.273 .703 0 .703"/>
+ </defs>
+ <g fill="none" fill-rule="evenodd">
+ <path fill="#FFFFFF" d="M26.8501,32.5727 C26.8501,32.9747 27.0111,33.3237 27.3061,33.6187 C27.6001,33.9137 27.9221,34.0477 28.3251,34.0477 C28.7271,34.0477 29.0751,33.9137 29.3711,33.6187 C29.6651,33.3237 29.8001,32.9747 29.8001,32.5727 L29.8001,17.8247 C29.8001,17.4227 29.6651,17.0737 29.3711,16.7787 C29.0751,16.4837 28.7271,16.3497 28.3251,16.3497 C27.9221,16.3497 27.5741,16.4837 27.2791,16.7787 C26.9841,17.0737 26.8501,17.4227 26.8501,17.8247 L26.8501,32.5727 Z"/>
+ <g transform="translate(32 15.61)">
+ <mask id="b" fill="white">
+ <use xlink:href="#a"/>
+ </mask>
+ <path fill="#FFFFFF" d="M0.5347,12.4844 C0.5347,13.3694 0.7227,14.2274 1.0717,15.0044 C1.4197,15.7824 1.8757,16.4794 2.4657,17.0694 C3.0557,17.6594 3.7527,18.1154 4.5567,18.4634 C5.3347,18.8124 6.1927,19.0004 7.0767,19.0004 C7.7747,19.0004 8.4447,18.8924 9.0617,18.7054 C9.6787,18.5174 10.2677,18.2224 10.7777,17.8474 C11.2877,17.4984 11.7697,17.0694 12.1727,16.5604 C12.5747,16.0504 12.8697,15.5144 13.1107,14.8974 C13.1907,14.7364 13.2177,14.5484 13.2177,14.3344 C13.2177,13.9854 13.0837,13.6374 12.8427,13.3424 C12.5747,13.0474 12.2257,12.8864 11.7697,12.8864 C11.5287,12.8864 11.2877,12.9664 11.0457,13.1284 C10.7777,13.2884 10.5897,13.5034 10.4557,13.7444 C10.1607,14.4144 9.7327,14.9514 9.1427,15.3804 C8.5257,15.8364 7.8547,16.0504 7.0767,16.0504 C6.5947,16.0504 6.1387,15.9424 5.7097,15.7554 C5.2537,15.5674 4.8787,15.3264 4.5567,15.0044 C4.2087,14.6834 3.9407,14.3074 3.7527,13.8514 C3.5647,13.4224 3.4577,12.9664 3.4577,12.4844 L3.4577,6.6654 C3.4577,6.1824 3.5647,5.7274 3.7527,5.2714 C3.9407,4.8424 4.1817,4.4664 4.5307,4.1444 C4.8517,3.8224 5.2267,3.5544 5.6827,3.3674 C6.1117,3.1794 6.5677,3.0994 7.0767,3.0994 C7.8547,3.0994 8.5257,3.3144 9.1427,3.7424 C9.7327,4.1714 10.1607,4.7074 10.4297,5.3784 C10.7237,5.9684 11.1537,6.2634 11.7697,6.2634 C12.2257,6.2634 12.6007,6.1024 12.8427,5.8074 C13.0837,5.5124 13.2177,5.1634 13.2177,4.8154 C13.2177,4.7074 13.2177,4.5734 13.1907,4.4394 C13.1647,4.3054 13.1107,4.1714 13.0307,4.0374 C12.7887,3.4744 12.4677,2.9384 12.0647,2.4554 C11.6627,1.9994 11.1807,1.5704 10.6707,1.2214 C10.1607,0.8734 9.5977,0.6054 8.9817,0.4174 C8.3647,0.2304 7.7477,0.1494 7.0767,0.1494 C6.1657,0.1494 5.3077,0.3104 4.5307,0.6594 C3.7257,1.0074 3.0287,1.4634 2.4387,2.0534 C1.8487,2.6434 1.3927,3.3404 1.0447,4.1174 C0.6957,4.9224 0.5347,5.7804 0.5347,6.6654 L0.5347,12.4844 Z" mask="url(#b)"/>
+ </g>
+ <path fill="#FFFFFF" d="M48.0601 32.5727C48.0601 32.9747 48.2211 33.3237 48.5161 33.6187 48.8111 33.9137 49.1331 34.0477 49.5351 34.0477 49.9371 34.0477 50.2861 33.9137 50.5811 33.6187 50.8751 33.3237 51.0101 32.9747 51.0101 32.5727L51.0101 17.8247C51.0101 17.4227 50.8751 17.0737 50.5811 16.7787 50.2861 16.4837 49.9371 16.3497 49.5351 16.3497 49.1331 16.3497 48.7841 16.4837 48.4891 16.7787 48.1941 17.0737 48.0601 17.4227 48.0601 17.8247L48.0601 32.5727zM53.7446 32.5727C53.7446 32.9747 53.8786 33.3237 54.1736 33.6187 54.4686 33.9137 54.8176 34.0477 55.2196 34.0477 55.6216 34.0477 55.9706 33.9137 56.2656 33.6187 56.5336 33.3237 56.6676 32.9747 56.6676 32.5727L56.6676 22.3027C56.6676 21.8197 56.7746 21.3647 56.9626 20.9087 57.1506 20.4797 57.3916 20.1047 57.7136 19.7817 58.0356 19.4607 58.4106 19.1927 58.8666 19.0047 59.2956 18.8167 59.7506 18.7367 60.2606 18.7367 60.7696 18.7367 61.2256 18.8167 61.6816 19.0047 62.1106 19.1927 62.4866 19.4607 62.8346 19.7817 63.1566 20.1047 63.4246 20.4797 63.6126 20.9087 63.7996 21.3647 63.8806 21.8197 63.8806 22.3027L63.8806 32.5727C63.8806 32.9747 64.0416 33.3237 64.3366 33.6187 64.6316 33.9137 64.9526 34.0477 65.3556 34.0477 65.7576 34.0477 66.1056 33.9137 66.4006 33.6187 66.6686 33.3237 66.8036 32.9747 66.8036 32.5727L66.8036 22.3027C66.8036 21.4177 66.6426 20.5597 66.2936 19.7817 65.9456 19.0047 65.4626 18.3077 64.8726 17.7177 64.2826 17.1277 63.5856 16.6447 62.8076 16.2967 62.0036 15.9477 61.1456 15.7867 60.2606 15.7867 59.3756 15.7867 58.5176 15.9477 57.7406 16.2967 56.9356 16.6447 56.2386 17.1277 55.6486 17.7177 55.0586 18.3077 54.6026 19.0047 54.2546 19.7817 53.9056 20.5597 53.7446 21.4177 53.7446 22.3027L53.7446 32.5727z"/>
+ <g transform="translate(69 15.61)">
+ <mask id="d" fill="white">
+ <use xlink:href="#c"/>
+ </mask>
+ <path fill="#FFFFFF" d="M0.7261,12.4844 C0.7261,13.3694 0.9131,14.2274 1.2621,15.0044 C1.6111,15.7824 2.0671,16.4794 2.6571,17.0694 C3.2471,17.6594 3.9441,18.1154 4.7481,18.4634 C5.5251,18.8124 6.3841,19.0004 7.2681,19.0004 C8.1541,19.0004 9.0121,18.8124 9.8161,18.4634 C10.5941,18.1154 11.2911,17.6594 11.8811,17.0694 C12.4711,16.4794 12.9541,15.7824 13.3021,15.0044 C13.6501,14.2274 13.8111,13.3694 13.8111,12.4844 L13.8111,10.2854 C13.8111,9.8834 13.6771,9.5344 13.3821,9.2394 C13.0871,8.9714 12.7391,8.8374 12.3371,8.8374 L9.4141,8.8374 C9.0121,8.8374 8.6631,8.9714 8.3951,9.2664 C8.1001,9.5614 7.9661,9.8834 7.9661,10.2854 C7.9661,10.6874 8.1001,11.0364 8.3951,11.3314 C8.6631,11.6264 9.0121,11.7604 9.4141,11.7604 L10.8621,11.7604 L10.8621,12.4844 C10.8621,12.9664 10.7811,13.4224 10.5941,13.8514 C10.4061,14.3074 10.1381,14.6834 9.8161,15.0044 C9.4951,15.3264 9.1191,15.5674 8.6901,15.7554 C8.2341,15.9424 7.7781,16.0504 7.2681,16.0504 C6.7861,16.0504 6.3301,15.9424 5.9011,15.7554 C5.4451,15.5674 5.0701,15.3264 4.7481,15.0044 C4.4001,14.6834 4.1581,14.3074 3.9711,13.8514 C3.7831,13.4224 3.6761,12.9664 3.6761,12.4844 L3.6761,6.6924 C3.6761,6.2094 3.7831,5.7544 3.9711,5.2974 C4.1581,4.8684 4.4001,4.4934 4.7221,4.1714 C5.0431,3.8494 5.4181,3.5814 5.8741,3.3944 C6.3031,3.2064 6.7591,3.1264 7.2681,3.1264 C8.0461,3.1264 8.7171,3.3404 9.3331,3.7694 C9.9231,4.1984 10.3521,4.7614 10.6471,5.4314 C10.7811,5.7274 10.9691,5.9414 11.2101,6.0754 C11.4521,6.2094 11.7201,6.2904 11.9611,6.2904 C12.4171,6.2904 12.7921,6.1294 13.0341,5.8344 C13.2751,5.5394 13.4091,5.1904 13.4091,4.8424 L13.4091,4.5204 C13.3821,4.4394 13.3551,4.3324 13.3291,4.2524 C13.0871,3.6354 12.7661,3.0994 12.3641,2.5894 C11.9611,2.0794 11.4791,1.6514 10.9691,1.3024 C10.4331,0.9544 9.8431,0.6594 9.2261,0.4714 C8.6091,0.2834 7.9391,0.1764 7.2681,0.1764 C6.3841,0.1764 5.5251,0.3374 4.7481,0.6864 C3.9441,1.0344 3.2471,1.4894 2.6571,2.0794 C2.0671,2.6704 1.6111,3.3674 1.2621,4.1444 C0.9131,4.9494 0.7261,5.8074 0.7261,6.6924 L0.7261,12.4844 Z" mask="url(#d)"/>
+ </g>
+ <path fill="#FFFFFF" d="M94.2886,26.352 L89.9716,26.352 C90.1586,25.896 90.3466,25.44 90.5346,25.038 C90.7226,24.636 90.8826,24.234 91.0706,23.831 C91.2586,23.429 91.4186,23.027 91.6066,22.624 C91.7676,22.223 91.9556,21.82 92.1426,21.391 L94.2886,26.352 Z M84.4206,32.063 C84.3936,32.144 84.3666,32.225 84.3396,32.331 C84.3126,32.438 84.3126,32.546 84.3126,32.626 C84.3126,32.975 84.4476,33.297 84.7156,33.592 C84.9566,33.914 85.3056,34.075 85.7616,34.075 C86.0566,34.075 86.2976,33.994 86.5386,33.86 C86.7806,33.726 86.9676,33.485 87.1016,33.19 C87.3706,32.519 87.6386,31.876 87.9336,31.232 C88.2016,30.615 88.4696,29.972 88.7646,29.302 L95.5486,29.302 L97.2116,33.19 C97.4526,33.78 97.8816,34.075 98.5516,34.075 C99.0076,34.075 99.3566,33.914 99.6246,33.592 C99.8656,33.297 99.9996,32.975 99.9996,32.626 C99.9996,32.466 99.9466,32.278 99.8656,32.063 L93.4836,17.127 C93.3506,16.806 93.0816,16.565 92.7056,16.377 C92.6256,16.323 92.5186,16.297 92.4386,16.27 C92.3306,16.243 92.2236,16.243 92.1426,16.243 C91.5266,16.243 91.0976,16.538 90.8026,17.127 L84.4206,32.063 Z"/>
+ <path stroke="#FFFFFF" stroke-width=".75" d="M12.2197 16.2373L4.2527 27.6613M12.2197 16.2373L28.7327 12.1643M12.5327 16.5717L17.1567 24.2957M10.5713 14.3247L3.7863 8.7697M13.2007 13.0962L17.1947 4.7982"/>
+ <g transform="translate(0 23.61)">
+ <mask id="f" fill="white">
+ <use xlink:href="#e"/>
+ </mask>
+ <path fill="#FFFFFF" d="M0.0103,4.6104 C-0.1407,2.6084 1.3613,0.8634 3.3643,0.7134 C5.3673,0.5604 7.1133,2.0634 7.2623,4.0654 C7.4123,6.0684 5.9123,7.8144 3.9093,7.9644 C1.9063,8.1154 0.1603,6.6134 0.0103,4.6104" mask="url(#f)"/>
+ </g>
+ <path fill="#FFFFFF" d="M16.022 24.3764C16.093 23.7564 16.654 23.3114 17.276 23.3824 17.897 23.4544 18.341 24.0164 18.27 24.6364 18.196 25.2574 17.636 25.7024 17.016 25.6314 16.395 25.5594 15.95 24.9984 16.022 24.3764M2.3223 9.603C1.9313 8.805 2.2623 7.839 3.0623 7.448 3.8613 7.057 4.8263 7.388 5.2183 8.187 5.6083 8.986 5.2783 9.952 4.4783 10.343 3.6783 10.734 2.7133 10.403 2.3223 9.603M26.1426 12.0361C26.0506 10.8211 26.9616 9.7611 28.1786 9.6691 29.3936 9.5781 30.4536 10.4891 30.5456 11.7051 30.6366 12.9211 29.7256 13.9811 28.5086 14.0721 27.2936 14.1631 26.2336 13.2521 26.1426 12.0361M16.1616.6313C17.3636-.3487 19.1346-.1677 20.1156 1.0343 21.0956 2.2363 20.9146 4.0073 19.7126 4.9883 18.5096 5.9663 16.7386 5.7863 15.7596 4.5843 14.7776 3.3813 14.9596 1.6113 16.1616.6313M8.5659 11.5605C10.8749 9.6785 14.2749 10.0245 16.1599 12.3345 18.0399 14.6435 17.6939 18.0435 15.3859 19.9275 13.0739 21.8075 9.6749 21.4625 7.7939 19.1515 5.9079 16.8425 6.2569 13.4425 8.5659 11.5605"/>
+ </g>
+</svg>
diff --git a/public/img/icingaweb2-background-orbs.jpg b/public/img/icingaweb2-background-orbs.jpg
new file mode 100644
index 0000000..bb6d40e
--- /dev/null
+++ b/public/img/icingaweb2-background-orbs.jpg
Binary files differ
diff --git a/public/img/icingaweb2-background.jpg b/public/img/icingaweb2-background.jpg
new file mode 100644
index 0000000..6a36024
--- /dev/null
+++ b/public/img/icingaweb2-background.jpg
Binary files differ
diff --git a/public/img/icons/acknowledgement.png b/public/img/icons/acknowledgement.png
new file mode 100644
index 0000000..eb03d2d
--- /dev/null
+++ b/public/img/icons/acknowledgement.png
Binary files differ
diff --git a/public/img/icons/acknowledgement_petrol.png b/public/img/icons/acknowledgement_petrol.png
new file mode 100644
index 0000000..e8345ea
--- /dev/null
+++ b/public/img/icons/acknowledgement_petrol.png
Binary files differ
diff --git a/public/img/icons/active_checks_disabled.png b/public/img/icons/active_checks_disabled.png
new file mode 100644
index 0000000..c21e445
--- /dev/null
+++ b/public/img/icons/active_checks_disabled.png
Binary files differ
diff --git a/public/img/icons/active_checks_disabled_petrol.png b/public/img/icons/active_checks_disabled_petrol.png
new file mode 100644
index 0000000..5614589
--- /dev/null
+++ b/public/img/icons/active_checks_disabled_petrol.png
Binary files differ
diff --git a/public/img/icons/active_passive_checks_disabled.png b/public/img/icons/active_passive_checks_disabled.png
new file mode 100644
index 0000000..664ecec
--- /dev/null
+++ b/public/img/icons/active_passive_checks_disabled.png
Binary files differ
diff --git a/public/img/icons/active_passive_checks_disabled_petrol.png b/public/img/icons/active_passive_checks_disabled_petrol.png
new file mode 100644
index 0000000..c82bfd9
--- /dev/null
+++ b/public/img/icons/active_passive_checks_disabled_petrol.png
Binary files differ
diff --git a/public/img/icons/comment.png b/public/img/icons/comment.png
new file mode 100644
index 0000000..b7d88a0
--- /dev/null
+++ b/public/img/icons/comment.png
Binary files differ
diff --git a/public/img/icons/comment_petrol.png b/public/img/icons/comment_petrol.png
new file mode 100644
index 0000000..53c1223
--- /dev/null
+++ b/public/img/icons/comment_petrol.png
Binary files differ
diff --git a/public/img/icons/configuration.png b/public/img/icons/configuration.png
new file mode 100644
index 0000000..fb52cd6
--- /dev/null
+++ b/public/img/icons/configuration.png
Binary files differ
diff --git a/public/img/icons/configuration_petrol.png b/public/img/icons/configuration_petrol.png
new file mode 100644
index 0000000..8168133
--- /dev/null
+++ b/public/img/icons/configuration_petrol.png
Binary files differ
diff --git a/public/img/icons/create.png b/public/img/icons/create.png
new file mode 100644
index 0000000..b9bedf8
--- /dev/null
+++ b/public/img/icons/create.png
Binary files differ
diff --git a/public/img/icons/create_petrol.png b/public/img/icons/create_petrol.png
new file mode 100644
index 0000000..4e104af
--- /dev/null
+++ b/public/img/icons/create_petrol.png
Binary files differ
diff --git a/public/img/icons/csv.png b/public/img/icons/csv.png
new file mode 100644
index 0000000..5b41917
--- /dev/null
+++ b/public/img/icons/csv.png
Binary files differ
diff --git a/public/img/icons/csv_petrol.png b/public/img/icons/csv_petrol.png
new file mode 100644
index 0000000..05d53f0
--- /dev/null
+++ b/public/img/icons/csv_petrol.png
Binary files differ
diff --git a/public/img/icons/dashboard.png b/public/img/icons/dashboard.png
new file mode 100644
index 0000000..e5f9c09
--- /dev/null
+++ b/public/img/icons/dashboard.png
Binary files differ
diff --git a/public/img/icons/dashboard_petrol.png b/public/img/icons/dashboard_petrol.png
new file mode 100644
index 0000000..04936ba
--- /dev/null
+++ b/public/img/icons/dashboard_petrol.png
Binary files differ
diff --git a/public/img/icons/disabled.png b/public/img/icons/disabled.png
new file mode 100644
index 0000000..54d0364
--- /dev/null
+++ b/public/img/icons/disabled.png
Binary files differ
diff --git a/public/img/icons/disabled_petrol.png b/public/img/icons/disabled_petrol.png
new file mode 100644
index 0000000..7319fb7
--- /dev/null
+++ b/public/img/icons/disabled_petrol.png
Binary files differ
diff --git a/public/img/icons/down.png b/public/img/icons/down.png
new file mode 100644
index 0000000..a844413
--- /dev/null
+++ b/public/img/icons/down.png
Binary files differ
diff --git a/public/img/icons/down_petrol.png b/public/img/icons/down_petrol.png
new file mode 100644
index 0000000..dda1283
--- /dev/null
+++ b/public/img/icons/down_petrol.png
Binary files differ
diff --git a/public/img/icons/downtime_end.png b/public/img/icons/downtime_end.png
new file mode 100644
index 0000000..e1d0d97
--- /dev/null
+++ b/public/img/icons/downtime_end.png
Binary files differ
diff --git a/public/img/icons/downtime_end_petrol.png b/public/img/icons/downtime_end_petrol.png
new file mode 100644
index 0000000..5d0666b
--- /dev/null
+++ b/public/img/icons/downtime_end_petrol.png
Binary files differ
diff --git a/public/img/icons/downtime_start.png b/public/img/icons/downtime_start.png
new file mode 100644
index 0000000..8e33280
--- /dev/null
+++ b/public/img/icons/downtime_start.png
Binary files differ
diff --git a/public/img/icons/downtime_start__petrol.png b/public/img/icons/downtime_start__petrol.png
new file mode 100644
index 0000000..9d56acd
--- /dev/null
+++ b/public/img/icons/downtime_start__petrol.png
Binary files differ
diff --git a/public/img/icons/edit.png b/public/img/icons/edit.png
new file mode 100644
index 0000000..00d8e29
--- /dev/null
+++ b/public/img/icons/edit.png
Binary files differ
diff --git a/public/img/icons/edit_petrol.png b/public/img/icons/edit_petrol.png
new file mode 100644
index 0000000..8199b01
--- /dev/null
+++ b/public/img/icons/edit_petrol.png
Binary files differ
diff --git a/public/img/icons/error.png b/public/img/icons/error.png
new file mode 100644
index 0000000..6245899
--- /dev/null
+++ b/public/img/icons/error.png
Binary files differ
diff --git a/public/img/icons/error_petrol.png b/public/img/icons/error_petrol.png
new file mode 100644
index 0000000..817ec76
--- /dev/null
+++ b/public/img/icons/error_petrol.png
Binary files differ
diff --git a/public/img/icons/error_white.png b/public/img/icons/error_white.png
new file mode 100644
index 0000000..0f50874
--- /dev/null
+++ b/public/img/icons/error_white.png
Binary files differ
diff --git a/public/img/icons/expand.png b/public/img/icons/expand.png
new file mode 100644
index 0000000..8ee1725
--- /dev/null
+++ b/public/img/icons/expand.png
Binary files differ
diff --git a/public/img/icons/expand_petrol.png b/public/img/icons/expand_petrol.png
new file mode 100644
index 0000000..a794ef5
--- /dev/null
+++ b/public/img/icons/expand_petrol.png
Binary files differ
diff --git a/public/img/icons/flapping.png b/public/img/icons/flapping.png
new file mode 100644
index 0000000..4b253e0
--- /dev/null
+++ b/public/img/icons/flapping.png
Binary files differ
diff --git a/public/img/icons/flapping_petrol.png b/public/img/icons/flapping_petrol.png
new file mode 100644
index 0000000..c6d045a
--- /dev/null
+++ b/public/img/icons/flapping_petrol.png
Binary files differ
diff --git a/public/img/icons/history.png b/public/img/icons/history.png
new file mode 100644
index 0000000..db4520d
--- /dev/null
+++ b/public/img/icons/history.png
Binary files differ
diff --git a/public/img/icons/history_petrol.png b/public/img/icons/history_petrol.png
new file mode 100644
index 0000000..1aba36b
--- /dev/null
+++ b/public/img/icons/history_petrol.png
Binary files differ
diff --git a/public/img/icons/host.png b/public/img/icons/host.png
new file mode 100644
index 0000000..33e2f5d
--- /dev/null
+++ b/public/img/icons/host.png
Binary files differ
diff --git a/public/img/icons/host_petrol.png b/public/img/icons/host_petrol.png
new file mode 100644
index 0000000..6568fe6
--- /dev/null
+++ b/public/img/icons/host_petrol.png
Binary files differ
diff --git a/public/img/icons/hostgroup.png b/public/img/icons/hostgroup.png
new file mode 100644
index 0000000..e71e3c9
--- /dev/null
+++ b/public/img/icons/hostgroup.png
Binary files differ
diff --git a/public/img/icons/hostgroup_petrol.png b/public/img/icons/hostgroup_petrol.png
new file mode 100644
index 0000000..68dbd19
--- /dev/null
+++ b/public/img/icons/hostgroup_petrol.png
Binary files differ
diff --git a/public/img/icons/in_downtime.png b/public/img/icons/in_downtime.png
new file mode 100644
index 0000000..d609df6
--- /dev/null
+++ b/public/img/icons/in_downtime.png
Binary files differ
diff --git a/public/img/icons/in_downtime_petrol.png b/public/img/icons/in_downtime_petrol.png
new file mode 100644
index 0000000..f6ee49a
--- /dev/null
+++ b/public/img/icons/in_downtime_petrol.png
Binary files differ
diff --git a/public/img/icons/json.png b/public/img/icons/json.png
new file mode 100644
index 0000000..d8e74c3
--- /dev/null
+++ b/public/img/icons/json.png
Binary files differ
diff --git a/public/img/icons/json_petrol.png b/public/img/icons/json_petrol.png
new file mode 100644
index 0000000..0197455
--- /dev/null
+++ b/public/img/icons/json_petrol.png
Binary files differ
diff --git a/public/img/icons/logout.png b/public/img/icons/logout.png
new file mode 100644
index 0000000..b93487a
--- /dev/null
+++ b/public/img/icons/logout.png
Binary files differ
diff --git a/public/img/icons/logout_petrol.png b/public/img/icons/logout_petrol.png
new file mode 100644
index 0000000..82ece09
--- /dev/null
+++ b/public/img/icons/logout_petrol.png
Binary files differ
diff --git a/public/img/icons/next.png b/public/img/icons/next.png
new file mode 100644
index 0000000..905e598
--- /dev/null
+++ b/public/img/icons/next.png
Binary files differ
diff --git a/public/img/icons/next_petrol.png b/public/img/icons/next_petrol.png
new file mode 100644
index 0000000..488df30
--- /dev/null
+++ b/public/img/icons/next_petrol.png
Binary files differ
diff --git a/public/img/icons/notification.png b/public/img/icons/notification.png
new file mode 100644
index 0000000..ea805d5
--- /dev/null
+++ b/public/img/icons/notification.png
Binary files differ
diff --git a/public/img/icons/notification_disabled.png b/public/img/icons/notification_disabled.png
new file mode 100644
index 0000000..ffcf1e6
--- /dev/null
+++ b/public/img/icons/notification_disabled.png
Binary files differ
diff --git a/public/img/icons/notification_disabled_petrol.png b/public/img/icons/notification_disabled_petrol.png
new file mode 100644
index 0000000..96dde74
--- /dev/null
+++ b/public/img/icons/notification_disabled_petrol.png
Binary files differ
diff --git a/public/img/icons/notification_petrol.png b/public/img/icons/notification_petrol.png
new file mode 100644
index 0000000..fc2fde8
--- /dev/null
+++ b/public/img/icons/notification_petrol.png
Binary files differ
diff --git a/public/img/icons/pdf.png b/public/img/icons/pdf.png
new file mode 100644
index 0000000..eefcef6
--- /dev/null
+++ b/public/img/icons/pdf.png
Binary files differ
diff --git a/public/img/icons/pdf_petrol.png b/public/img/icons/pdf_petrol.png
new file mode 100644
index 0000000..ad044e6
--- /dev/null
+++ b/public/img/icons/pdf_petrol.png
Binary files differ
diff --git a/public/img/icons/prev.png b/public/img/icons/prev.png
new file mode 100644
index 0000000..edfabd3
--- /dev/null
+++ b/public/img/icons/prev.png
Binary files differ
diff --git a/public/img/icons/prev_petrol.png b/public/img/icons/prev_petrol.png
new file mode 100644
index 0000000..9152912
--- /dev/null
+++ b/public/img/icons/prev_petrol.png
Binary files differ
diff --git a/public/img/icons/refresh.png b/public/img/icons/refresh.png
new file mode 100644
index 0000000..b0222a5
--- /dev/null
+++ b/public/img/icons/refresh.png
Binary files differ
diff --git a/public/img/icons/refresh_petrol.png b/public/img/icons/refresh_petrol.png
new file mode 100644
index 0000000..50e8155
--- /dev/null
+++ b/public/img/icons/refresh_petrol.png
Binary files differ
diff --git a/public/img/icons/remove.png b/public/img/icons/remove.png
new file mode 100644
index 0000000..98ada74
--- /dev/null
+++ b/public/img/icons/remove.png
Binary files differ
diff --git a/public/img/icons/remove_petrol.png b/public/img/icons/remove_petrol.png
new file mode 100644
index 0000000..8ec49a2
--- /dev/null
+++ b/public/img/icons/remove_petrol.png
Binary files differ
diff --git a/public/img/icons/reschedule.png b/public/img/icons/reschedule.png
new file mode 100644
index 0000000..5efc20d
--- /dev/null
+++ b/public/img/icons/reschedule.png
Binary files differ
diff --git a/public/img/icons/reschedule_petrol.png b/public/img/icons/reschedule_petrol.png
new file mode 100644
index 0000000..3a24c90
--- /dev/null
+++ b/public/img/icons/reschedule_petrol.png
Binary files differ
diff --git a/public/img/icons/save.png b/public/img/icons/save.png
new file mode 100644
index 0000000..2db2719
--- /dev/null
+++ b/public/img/icons/save.png
Binary files differ
diff --git a/public/img/icons/save_petrol.png b/public/img/icons/save_petrol.png
new file mode 100644
index 0000000..3e42cd7
--- /dev/null
+++ b/public/img/icons/save_petrol.png
Binary files differ
diff --git a/public/img/icons/search.png b/public/img/icons/search.png
new file mode 100644
index 0000000..a66c3ca
--- /dev/null
+++ b/public/img/icons/search.png
Binary files differ
diff --git a/public/img/icons/search_icinga_blue.png b/public/img/icons/search_icinga_blue.png
new file mode 100644
index 0000000..0a284ca
--- /dev/null
+++ b/public/img/icons/search_icinga_blue.png
Binary files differ
diff --git a/public/img/icons/search_petrol.png b/public/img/icons/search_petrol.png
new file mode 100644
index 0000000..c02b702
--- /dev/null
+++ b/public/img/icons/search_petrol.png
Binary files differ
diff --git a/public/img/icons/search_white.png b/public/img/icons/search_white.png
new file mode 100644
index 0000000..638be2e
--- /dev/null
+++ b/public/img/icons/search_white.png
Binary files differ
diff --git a/public/img/icons/service.png b/public/img/icons/service.png
new file mode 100644
index 0000000..9df3b48
--- /dev/null
+++ b/public/img/icons/service.png
Binary files differ
diff --git a/public/img/icons/service_petrol.png b/public/img/icons/service_petrol.png
new file mode 100644
index 0000000..ea165a2
--- /dev/null
+++ b/public/img/icons/service_petrol.png
Binary files differ
diff --git a/public/img/icons/servicegroup.png b/public/img/icons/servicegroup.png
new file mode 100644
index 0000000..d4b87df
--- /dev/null
+++ b/public/img/icons/servicegroup.png
Binary files differ
diff --git a/public/img/icons/servicegroup_petrol.png b/public/img/icons/servicegroup_petrol.png
new file mode 100644
index 0000000..8b963c0
--- /dev/null
+++ b/public/img/icons/servicegroup_petrol.png
Binary files differ
diff --git a/public/img/icons/softstate.png b/public/img/icons/softstate.png
new file mode 100644
index 0000000..8efa442
--- /dev/null
+++ b/public/img/icons/softstate.png
Binary files differ
diff --git a/public/img/icons/submit.png b/public/img/icons/submit.png
new file mode 100644
index 0000000..40e4826
--- /dev/null
+++ b/public/img/icons/submit.png
Binary files differ
diff --git a/public/img/icons/submit_petrol.png b/public/img/icons/submit_petrol.png
new file mode 100644
index 0000000..b0b3156
--- /dev/null
+++ b/public/img/icons/submit_petrol.png
Binary files differ
diff --git a/public/img/icons/success.png b/public/img/icons/success.png
new file mode 100644
index 0000000..2f95639
--- /dev/null
+++ b/public/img/icons/success.png
Binary files differ
diff --git a/public/img/icons/success_petrol.png b/public/img/icons/success_petrol.png
new file mode 100644
index 0000000..302461f
--- /dev/null
+++ b/public/img/icons/success_petrol.png
Binary files differ
diff --git a/public/img/icons/tux.png b/public/img/icons/tux.png
new file mode 100644
index 0000000..c51a0b4
--- /dev/null
+++ b/public/img/icons/tux.png
Binary files differ
diff --git a/public/img/icons/uebersicht.png b/public/img/icons/uebersicht.png
new file mode 100644
index 0000000..501edc4
--- /dev/null
+++ b/public/img/icons/uebersicht.png
Binary files differ
diff --git a/public/img/icons/unhandled.png b/public/img/icons/unhandled.png
new file mode 100644
index 0000000..59dab84
--- /dev/null
+++ b/public/img/icons/unhandled.png
Binary files differ
diff --git a/public/img/icons/unhandled_petrol.png b/public/img/icons/unhandled_petrol.png
new file mode 100644
index 0000000..3972337
--- /dev/null
+++ b/public/img/icons/unhandled_petrol.png
Binary files differ
diff --git a/public/img/icons/up.png b/public/img/icons/up.png
new file mode 100644
index 0000000..beef484
--- /dev/null
+++ b/public/img/icons/up.png
Binary files differ
diff --git a/public/img/icons/up_petrol.png b/public/img/icons/up_petrol.png
new file mode 100644
index 0000000..946bef1
--- /dev/null
+++ b/public/img/icons/up_petrol.png
Binary files differ
diff --git a/public/img/icons/user.png b/public/img/icons/user.png
new file mode 100644
index 0000000..694ddfc
--- /dev/null
+++ b/public/img/icons/user.png
Binary files differ
diff --git a/public/img/icons/user_petrol.png b/public/img/icons/user_petrol.png
new file mode 100644
index 0000000..3fae64d
--- /dev/null
+++ b/public/img/icons/user_petrol.png
Binary files differ
diff --git a/public/img/icons/win.png b/public/img/icons/win.png
new file mode 100644
index 0000000..7ffcea7
--- /dev/null
+++ b/public/img/icons/win.png
Binary files differ
diff --git a/public/img/orb-analytics.png b/public/img/orb-analytics.png
new file mode 100644
index 0000000..7adae6f
--- /dev/null
+++ b/public/img/orb-analytics.png
Binary files differ
diff --git a/public/img/orb-automation.png b/public/img/orb-automation.png
new file mode 100644
index 0000000..eb292b5
--- /dev/null
+++ b/public/img/orb-automation.png
Binary files differ
diff --git a/public/img/orb-cloud.png b/public/img/orb-cloud.png
new file mode 100644
index 0000000..f5e4a64
--- /dev/null
+++ b/public/img/orb-cloud.png
Binary files differ
diff --git a/public/img/orb-icinga.png b/public/img/orb-icinga.png
new file mode 100644
index 0000000..9f7f2a6
--- /dev/null
+++ b/public/img/orb-icinga.png
Binary files differ
diff --git a/public/img/orb-infrastructure.png b/public/img/orb-infrastructure.png
new file mode 100644
index 0000000..8713965
--- /dev/null
+++ b/public/img/orb-infrastructure.png
Binary files differ
diff --git a/public/img/orb-metrics.png b/public/img/orb-metrics.png
new file mode 100644
index 0000000..199f21c
--- /dev/null
+++ b/public/img/orb-metrics.png
Binary files differ
diff --git a/public/img/orb-notifications.png b/public/img/orb-notifications.png
new file mode 100644
index 0000000..297ba58
--- /dev/null
+++ b/public/img/orb-notifications.png
Binary files differ
diff --git a/public/img/select-icon-2x.png b/public/img/select-icon-2x.png
new file mode 100644
index 0000000..57845df
--- /dev/null
+++ b/public/img/select-icon-2x.png
Binary files differ
diff --git a/public/img/select-icon.png b/public/img/select-icon.png
new file mode 100644
index 0000000..e0d9a5a
--- /dev/null
+++ b/public/img/select-icon.png
Binary files differ
diff --git a/public/img/select-icon.svg b/public/img/select-icon.svg
new file mode 100644
index 0000000..cc8a011
--- /dev/null
+++ b/public/img/select-icon.svg
@@ -0,0 +1 @@
+<svg height="32" viewBox="0 0 24 32" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m5.20126707.78766623 4.45386238 4.20402191c.16721345.15783356.16291017.40979541-.00961164.56277256-.081158.0719638-.18974398.11220597-.3027668.11220597h-8.90772462c-.24025844 0-.43502639-.17818569-.43502639-.39798892 0-.10340014.04398717-.20274128.12264801-.27698961l4.45386234-4.20402191c.16721345-.15783357.44262326-.16177048.61514507-.00879333.00325382.00288518.00645805.00581661.00961165.00879333z" fill="#00c3ed" transform="matrix(1 0 0 -1 7 20.666667)"/></svg> \ No newline at end of file
diff --git a/public/img/textarea-corner-2x.png b/public/img/textarea-corner-2x.png
new file mode 100644
index 0000000..ee9cb50
--- /dev/null
+++ b/public/img/textarea-corner-2x.png
Binary files differ
diff --git a/public/img/textarea-corner.png b/public/img/textarea-corner.png
new file mode 100644
index 0000000..3a2242c
--- /dev/null
+++ b/public/img/textarea-corner.png
Binary files differ
diff --git a/public/img/theme-mode-thumbnail-dark.svg b/public/img/theme-mode-thumbnail-dark.svg
new file mode 100644
index 0000000..40f93e8
--- /dev/null
+++ b/public/img/theme-mode-thumbnail-dark.svg
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="800px" height="480px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <g transform="matrix(1,0,0,1,-984,0)">
+ <g id="dark" transform="matrix(1,0,0,0.8,984.021,0)">
+ <rect x="0" y="0" width="800" height="600" style="fill:none;"/>
+ <clipPath id="_clip1">
+ <rect x="0" y="0" width="800" height="600"/>
+ </clipPath>
+ <g clip-path="url(#_clip1)">
+ <g transform="matrix(1,0,0,1.25,0,9.09495e-13)">
+ <clipPath id="_clip2">
+ <rect x="0" y="-0" width="800" height="480"/>
+ </clipPath>
+ <g clip-path="url(#_clip2)">
+ <g transform="matrix(1.81105,0,0,1,0,0)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(7,5,44);"/>
+ </g>
+ <g transform="matrix(1.81105,0,0,0.30498,-1.13687e-13,190.035)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(25,21,68);"/>
+ </g>
+ <g transform="matrix(0.77088,0,0,0.290377,-644.149,483.662)">
+ <rect x="835.602" y="-1337.3" width="326.111" height="326.111" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g>
+ <path d="M251.392,110.852L220.365,141.879L251.392,172.906L251.392,353.271L188.167,353.271C182.556,353.271 178.008,357.819 178.008,363.43L178.008,416.496C178.008,422.107 182.556,426.655 188.167,426.655L251.392,426.655L251.392,600L800,600L800,-0L251.392,-0L251.392,110.852Z" style="fill:rgb(39,46,58);"/>
+ <path d="M251.392,110.852L220.365,141.879L251.392,172.906L251.392,353.271L188.167,353.271C182.556,353.271 178.008,357.819 178.008,363.43L178.008,416.496C178.008,422.107 182.556,426.655 188.167,426.655L251.392,426.655L251.392,600L800,600L800,-0L251.392,-0L251.392,110.852Z" style="fill:rgb(39,46,58);"/>
+ </g>
+ <g transform="matrix(4.8965,0,0,4.8965,-290.421,-2191.75)">
+ <g transform="matrix(1,0,0,1,1341.37,-355.199)">
+ <path d="M-1178.23,822.298C-1178.23,819.787 -1179.23,817.378 -1181,815.602C-1182.78,813.826 -1185.19,812.828 -1187.7,812.828C-1192.51,812.828 -1198.34,812.828 -1203.16,812.828C-1205.67,812.828 -1208.08,813.826 -1209.85,815.602C-1211.63,817.378 -1212.63,819.787 -1212.63,822.298C-1212.63,822.299 -1212.63,822.3 -1212.63,822.301C-1212.63,824.813 -1211.63,827.222 -1209.85,828.997C-1208.08,830.773 -1205.67,831.771 -1203.16,831.771C-1198.34,831.771 -1192.51,831.771 -1187.7,831.771C-1185.19,831.771 -1182.78,830.773 -1181,828.997C-1179.23,827.222 -1178.23,824.813 -1178.23,822.301C-1178.23,822.3 -1178.23,822.299 -1178.23,822.298Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(0.500319,0,0,0.90861,751.454,-280.049)">
+ <path d="M-1178.23,822.298C-1178.23,819.786 -1180.04,817.378 -1183.26,815.602C-1186.49,813.826 -1190.86,812.828 -1195.42,812.828L-1195.43,812.828C-1199.99,812.828 -1204.37,813.826 -1207.59,815.602C-1210.82,817.378 -1212.63,819.786 -1212.63,822.298L-1212.63,822.302C-1212.63,824.813 -1210.82,827.222 -1207.59,828.998C-1204.37,830.774 -1199.99,831.771 -1195.43,831.771L-1195.42,831.771C-1190.86,831.771 -1186.49,830.774 -1183.26,828.998C-1180.04,827.222 -1178.23,824.813 -1178.23,822.302L-1178.23,822.298Z" style="fill:rgb(39,46,58);"/>
+ </g>
+ </g>
+ <g transform="matrix(4.8965,0,0,4.8965,-96.0097,-2195.22)">
+ <g transform="matrix(1,0,0,1,1341.37,-355.199)">
+ <path d="M-1178.23,822.298C-1178.23,819.787 -1179.23,817.378 -1181,815.602C-1182.78,813.826 -1185.19,812.828 -1187.7,812.828C-1192.51,812.828 -1198.34,812.828 -1203.16,812.828C-1205.67,812.828 -1208.08,813.826 -1209.85,815.602C-1211.63,817.378 -1212.63,819.787 -1212.63,822.298C-1212.63,822.299 -1212.63,822.3 -1212.63,822.301C-1212.63,824.813 -1211.63,827.222 -1209.85,828.997C-1208.08,830.773 -1205.67,831.771 -1203.16,831.771C-1198.34,831.771 -1192.51,831.771 -1187.7,831.771C-1185.19,831.771 -1182.78,830.773 -1181,828.997C-1179.23,827.222 -1178.23,824.813 -1178.23,822.301C-1178.23,822.3 -1178.23,822.299 -1178.23,822.298Z" style="fill:rgb(62,77,116);"/>
+ </g>
+ <g transform="matrix(0.500319,0,0,0.90861,751.454,-280.049)">
+ <path d="M-1178.23,822.298C-1178.23,819.786 -1180.04,817.378 -1183.26,815.602C-1186.49,813.826 -1190.86,812.828 -1195.42,812.828L-1195.43,812.828C-1199.99,812.828 -1204.37,813.826 -1207.59,815.602C-1210.82,817.378 -1212.63,819.786 -1212.63,822.298L-1212.63,822.302C-1212.63,824.813 -1210.82,827.222 -1207.59,828.998C-1204.37,830.774 -1199.99,831.771 -1195.43,831.771L-1195.42,831.771C-1190.86,831.771 -1186.49,830.774 -1183.26,828.998C-1180.04,827.222 -1178.23,824.813 -1178.23,822.302L-1178.23,822.298Z" style="fill:rgb(39,46,58);"/>
+ </g>
+ </g>
+ <g transform="matrix(1.39597,0,0,1,-120.257,-227.628)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534C617.133,400.534 403.105,400.534 344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054L675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(1.39597,0,0,1,-120.257,-71.831)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534C617.133,400.534 403.105,400.534 344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054C403.105,523.054 617.133,523.054 675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434ZM687.21,421.434L687.21,502.154C687.21,510.935 682.111,518.054 675.821,518.054C617.133,518.054 403.105,518.054 344.417,518.054C338.127,518.054 333.028,510.935 333.028,502.154L333.028,421.434C333.028,412.653 338.127,405.534 344.417,405.534C344.417,405.534 675.768,405.534 675.821,405.534C682.111,405.534 687.21,412.653 687.21,421.434Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(4.71406,0,0,4.71406,191.129,368.75)">
+ <path d="M8.977,8.086L9.507,7.554C9.727,7.336 9.727,6.98 9.507,6.759L7.248,4.5L9.505,2.238C9.725,2.02 9.725,1.664 9.505,1.444L8.975,0.914C8.757,0.694 8.401,0.694 8.18,0.914L4.993,4.102C4.773,4.322 4.773,4.678 4.995,4.898L8.183,8.086C8.401,8.306 8.757,8.306 8.977,8.086ZM4.475,8.086L5.005,7.556C5.225,7.336 5.225,6.98 5.005,6.762L2.748,4.5L5.007,2.241C5.227,2.02 5.227,1.664 5.007,1.446L4.477,0.914C4.257,0.694 3.901,0.694 3.683,0.914L0.495,4.102C0.273,4.322 0.273,4.678 0.493,4.898L3.68,8.086C3.901,8.306 4.257,8.306 4.475,8.086Z" style="fill:rgb(0,196,240);fill-rule:nonzero;"/>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/public/img/theme-mode-thumbnail-light.svg b/public/img/theme-mode-thumbnail-light.svg
new file mode 100644
index 0000000..a04f441
--- /dev/null
+++ b/public/img/theme-mode-thumbnail-light.svg
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="800px" height="480px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
+ <g transform="matrix(1,0,0,1,-984,-674)">
+ <g id="light" transform="matrix(1,0,0,0.8,984.021,674)">
+ <rect x="0" y="0" width="800" height="600" style="fill:none;"/>
+ <g>
+ <clipPath id="_clip1">
+ <rect x="0" y="0" width="800" height="600"/>
+ </clipPath>
+ <g clip-path="url(#_clip1)">
+ <g transform="matrix(1.81105,0,0,1.25,0,-1.13687e-13)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(219,236,242);"/>
+ </g>
+ <g transform="matrix(1.81105,0,0,0.391381,-1.13687e-13,237.544)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(235,247,253);"/>
+ </g>
+ <g transform="matrix(0.77088,0,0,0.362971,-644.149,604.578)">
+ <rect x="835.602" y="-1337.3" width="326.111" height="326.111" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g>
+ <path d="M251.392,139.576L220.365,178.36L251.392,217.144L251.392,750L800,750L800,-0L251.392,-0L251.392,139.576ZM251.392,434.008L188.167,434.008C182.556,434.008 178.008,439.693 178.008,446.707L178.008,513.039C178.008,520.053 182.556,525.739 188.167,525.739L251.392,525.739L251.392,434.008Z" style="fill:rgb(244,249,250);"/>
+ <path d="M251.392,139.576L220.365,178.36L251.392,217.144L251.392,750L800,750L800,-0L251.392,-0L251.392,139.576ZM251.392,434.008L188.167,434.008C182.556,434.008 178.008,439.693 178.008,446.707L178.008,513.039C178.008,520.053 182.556,525.739 188.167,525.739L251.392,525.739L251.392,434.008Z" style="fill:rgb(244,249,250);"/>
+ </g>
+ <g transform="matrix(4.8965,0,0,6.12063,-290.421,-2739.68)">
+ <g transform="matrix(1,0,0,1,1341.37,-355.199)">
+ <path d="M-1178.23,822.298C-1178.23,819.787 -1179.23,817.378 -1181,815.602C-1182.78,813.826 -1185.19,812.828 -1187.7,812.828C-1192.51,812.828 -1198.34,812.828 -1203.16,812.828C-1205.67,812.828 -1208.08,813.826 -1209.85,815.602C-1211.63,817.378 -1212.63,819.787 -1212.63,822.298C-1212.63,822.299 -1212.63,822.3 -1212.63,822.301C-1212.63,824.813 -1211.63,827.222 -1209.85,828.997C-1208.08,830.773 -1205.67,831.771 -1203.16,831.771C-1198.34,831.771 -1192.51,831.771 -1187.7,831.771C-1185.19,831.771 -1182.78,830.773 -1181,828.997C-1179.23,827.222 -1178.23,824.813 -1178.23,822.301C-1178.23,822.3 -1178.23,822.299 -1178.23,822.298Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(0.500319,0,0,0.90861,751.454,-280.049)">
+ <path d="M-1178.23,822.298C-1178.23,819.786 -1180.04,817.378 -1183.26,815.602C-1186.49,813.826 -1190.86,812.828 -1195.42,812.828L-1195.43,812.828C-1199.99,812.828 -1204.37,813.826 -1207.59,815.602C-1210.82,817.378 -1212.63,819.786 -1212.63,822.298L-1212.63,822.302C-1212.63,824.813 -1210.82,827.222 -1207.59,828.998C-1204.37,830.774 -1199.99,831.771 -1195.43,831.771L-1195.42,831.771C-1190.86,831.771 -1186.49,830.774 -1183.26,828.998C-1180.04,827.222 -1178.23,824.813 -1178.23,822.302L-1178.23,822.298Z" style="fill:rgb(244,249,250);"/>
+ </g>
+ </g>
+ <g transform="matrix(4.8965,0,0,6.12063,-96.0097,-2741.58)">
+ <g transform="matrix(1,0,0,1,1341.37,-355.199)">
+ <path d="M-1178.23,822.298C-1178.23,819.787 -1179.23,817.378 -1181,815.602C-1182.78,813.826 -1185.19,812.828 -1187.7,812.828C-1192.51,812.828 -1198.34,812.828 -1203.16,812.828C-1205.67,812.828 -1208.08,813.826 -1209.85,815.602C-1211.63,817.378 -1212.63,819.787 -1212.63,822.298C-1212.63,822.299 -1212.63,822.3 -1212.63,822.301C-1212.63,824.813 -1211.63,827.222 -1209.85,828.997C-1208.08,830.773 -1205.67,831.771 -1203.16,831.771C-1198.34,831.771 -1192.51,831.771 -1187.7,831.771C-1185.19,831.771 -1182.78,830.773 -1181,828.997C-1179.23,827.222 -1178.23,824.813 -1178.23,822.301C-1178.23,822.3 -1178.23,822.299 -1178.23,822.298Z" style="fill:rgb(219,236,242);"/>
+ </g>
+ <g transform="matrix(0.500319,0,0,0.90861,736.813,-280.049)">
+ <path d="M-1178.23,822.298C-1178.23,819.786 -1180.04,817.378 -1183.26,815.602C-1186.49,813.826 -1190.86,812.828 -1195.42,812.828L-1195.43,812.828C-1199.99,812.828 -1204.37,813.826 -1207.59,815.602C-1210.82,817.378 -1212.63,819.786 -1212.63,822.298L-1212.63,822.302C-1212.63,824.813 -1210.82,827.222 -1207.59,828.998C-1204.37,830.774 -1199.99,831.771 -1195.43,831.771L-1195.42,831.771C-1190.86,831.771 -1186.49,830.774 -1183.26,828.998C-1180.04,827.222 -1178.23,824.813 -1178.23,822.302L-1178.23,822.298Z" style="fill:rgb(244,249,250);"/>
+ </g>
+ </g>
+ <g transform="matrix(1.39597,0,0,1.25,-120.257,-292.116)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534C617.133,400.534 403.105,400.534 344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054L675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(1.39597,0,0,1.25,-120.257,-97.3693)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534C617.133,400.534 403.105,400.534 344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054L675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434Z" style="fill:none;stroke:rgb(0,196,240);stroke-width:4.12px;"/>
+ </g>
+ <g transform="matrix(4.71406,0,0,5.89258,191.129,453.357)">
+ <path d="M8.977,8.086L9.507,7.554C9.727,7.336 9.727,6.98 9.507,6.759L7.248,4.5L9.505,2.238C9.725,2.02 9.725,1.664 9.505,1.444L8.975,0.914C8.757,0.694 8.401,0.694 8.18,0.914L4.993,4.102C4.773,4.322 4.773,4.678 4.995,4.898L8.183,8.086C8.401,8.306 8.757,8.306 8.977,8.086ZM4.475,8.086L5.005,7.556C5.225,7.336 5.225,6.98 5.005,6.762L2.748,4.5L5.007,2.241C5.227,2.02 5.227,1.664 5.007,1.446L4.477,0.914C4.257,0.694 3.901,0.694 3.683,0.914L0.495,4.102C0.273,4.322 0.273,4.678 0.493,4.898L3.68,8.086C3.901,8.306 4.257,8.306 4.475,8.086Z" style="fill:rgb(0,196,240);fill-rule:nonzero;"/>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/public/img/theme-mode-thumbnail-system.svg b/public/img/theme-mode-thumbnail-system.svg
new file mode 100644
index 0000000..55e2168
--- /dev/null
+++ b/public/img/theme-mode-thumbnail-system.svg
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="800px" height="480px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
+ <g transform="matrix(1,0,0,1,-984,-1360)">
+ <g id="auto" transform="matrix(1,0,0,0.8,984.021,1360)">
+ <rect x="0" y="0" width="800" height="600" style="fill:none;"/>
+ <clipPath id="_clip1">
+ <rect x="0" y="0" width="800" height="600"/>
+ </clipPath>
+ <g clip-path="url(#_clip1)">
+ <g id="light">
+ <clipPath id="_clip2">
+ <rect x="0" y="0" width="800" height="600"/>
+ </clipPath>
+ <g clip-path="url(#_clip2)">
+ <g transform="matrix(1.81105,0,0,1.25,0,-1.13687e-13)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(219,236,242);"/>
+ </g>
+ <g transform="matrix(1.81105,0,0,0.391381,-1.13687e-13,237.544)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(235,247,253);"/>
+ </g>
+ <g transform="matrix(0.77088,0,0,0.362971,-644.149,604.578)">
+ <rect x="835.602" y="-1337.3" width="326.111" height="326.111" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g>
+ <path d="M251.392,139.576L220.365,178.36L251.392,217.144L251.392,750L800,750L800,-0L251.392,-0L251.392,139.576ZM251.392,434.008L188.167,434.008C182.556,434.008 178.008,439.693 178.008,446.707L178.008,513.039C178.008,520.053 182.556,525.739 188.167,525.739L251.392,525.739L251.392,434.008Z" style="fill:rgb(244,249,250);"/>
+ <path d="M251.392,139.576L220.365,178.36L251.392,217.144L251.392,750L800,750L800,-0L251.392,-0L251.392,139.576ZM251.392,434.008L188.167,434.008C182.556,434.008 178.008,439.693 178.008,446.707L178.008,513.039C178.008,520.053 182.556,525.739 188.167,525.739L251.392,525.739L251.392,434.008Z" style="fill:rgb(244,249,250);"/>
+ </g>
+ <g transform="matrix(4.8965,0,0,6.12063,-290.421,-2739.68)">
+ <g transform="matrix(1,0,0,1,1341.37,-355.199)">
+ <path d="M-1178.23,822.298C-1178.23,819.787 -1179.23,817.378 -1181,815.602C-1182.78,813.826 -1185.19,812.828 -1187.7,812.828C-1192.51,812.828 -1198.34,812.828 -1203.16,812.828C-1205.67,812.828 -1208.08,813.826 -1209.85,815.602C-1211.63,817.378 -1212.63,819.787 -1212.63,822.298C-1212.63,822.299 -1212.63,822.3 -1212.63,822.301C-1212.63,824.813 -1211.63,827.222 -1209.85,828.997C-1208.08,830.773 -1205.67,831.771 -1203.16,831.771C-1198.34,831.771 -1192.51,831.771 -1187.7,831.771C-1185.19,831.771 -1182.78,830.773 -1181,828.997C-1179.23,827.222 -1178.23,824.813 -1178.23,822.301C-1178.23,822.3 -1178.23,822.299 -1178.23,822.298Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(0.500319,0,0,0.90861,751.454,-280.049)">
+ <path d="M-1178.23,822.298C-1178.23,819.786 -1180.04,817.378 -1183.26,815.602C-1186.49,813.826 -1190.86,812.828 -1195.42,812.828L-1195.43,812.828C-1199.99,812.828 -1204.37,813.826 -1207.59,815.602C-1210.82,817.378 -1212.63,819.786 -1212.63,822.298L-1212.63,822.302C-1212.63,824.813 -1210.82,827.222 -1207.59,828.998C-1204.37,830.774 -1199.99,831.771 -1195.43,831.771L-1195.42,831.771C-1190.86,831.771 -1186.49,830.774 -1183.26,828.998C-1180.04,827.222 -1178.23,824.813 -1178.23,822.302L-1178.23,822.298Z" style="fill:rgb(244,249,250);"/>
+ </g>
+ </g>
+ <g transform="matrix(4.8965,0,0,6.12063,-96.0097,-2741.58)">
+ <g transform="matrix(1,0,0,1,1341.37,-355.199)">
+ <path d="M-1178.23,822.298C-1178.23,819.787 -1179.23,817.378 -1181,815.602C-1182.78,813.826 -1185.19,812.828 -1187.7,812.828C-1192.51,812.828 -1198.34,812.828 -1203.16,812.828C-1205.67,812.828 -1208.08,813.826 -1209.85,815.602C-1211.63,817.378 -1212.63,819.787 -1212.63,822.298C-1212.63,822.299 -1212.63,822.3 -1212.63,822.301C-1212.63,824.813 -1211.63,827.222 -1209.85,828.997C-1208.08,830.773 -1205.67,831.771 -1203.16,831.771C-1198.34,831.771 -1192.51,831.771 -1187.7,831.771C-1185.19,831.771 -1182.78,830.773 -1181,828.997C-1179.23,827.222 -1178.23,824.813 -1178.23,822.301C-1178.23,822.3 -1178.23,822.299 -1178.23,822.298Z" style="fill:rgb(219,236,242);"/>
+ </g>
+ <g transform="matrix(0.500319,0,0,0.90861,736.813,-280.049)">
+ <path d="M-1178.23,822.298C-1178.23,819.786 -1180.04,817.378 -1183.26,815.602C-1186.49,813.826 -1190.86,812.828 -1195.42,812.828L-1195.43,812.828C-1199.99,812.828 -1204.37,813.826 -1207.59,815.602C-1210.82,817.378 -1212.63,819.786 -1212.63,822.298L-1212.63,822.302C-1212.63,824.813 -1210.82,827.222 -1207.59,828.998C-1204.37,830.774 -1199.99,831.771 -1195.43,831.771L-1195.42,831.771C-1190.86,831.771 -1186.49,830.774 -1183.26,828.998C-1180.04,827.222 -1178.23,824.813 -1178.23,822.302L-1178.23,822.298Z" style="fill:rgb(244,249,250);"/>
+ </g>
+ </g>
+ <g transform="matrix(1.39597,0,0,1.25,-120.257,-292.116)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534C617.133,400.534 403.105,400.534 344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054L675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(1.39597,0,0,1.25,-120.257,-97.3693)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534C617.133,400.534 403.105,400.534 344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054L675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434Z" style="fill:none;stroke:rgb(0,196,240);stroke-width:4.12px;"/>
+ </g>
+ <g transform="matrix(4.71406,0,0,5.89258,191.129,453.357)">
+ <path d="M8.977,8.086L9.507,7.554C9.727,7.336 9.727,6.98 9.507,6.759L7.248,4.5L9.505,2.238C9.725,2.02 9.725,1.664 9.505,1.444L8.975,0.914C8.757,0.694 8.401,0.694 8.18,0.914L4.993,4.102C4.773,4.322 4.773,4.678 4.995,4.898L8.183,8.086C8.401,8.306 8.757,8.306 8.977,8.086ZM4.475,8.086L5.005,7.556C5.225,7.336 5.225,6.98 5.005,6.762L2.748,4.5L5.007,2.241C5.227,2.02 5.227,1.664 5.007,1.446L4.477,0.914C4.257,0.694 3.901,0.694 3.683,0.914L0.495,4.102C0.273,4.322 0.273,4.678 0.493,4.898L3.68,8.086C3.901,8.306 4.257,8.306 4.475,8.086Z" style="fill:rgb(0,196,240);fill-rule:nonzero;"/>
+ </g>
+ </g>
+ </g>
+ <g id="dark" transform="matrix(1,0,0,1.25,400,1.13864e-12)">
+ <clipPath id="_clip3">
+ <rect x="0" y="-0" width="800" height="480"/>
+ </clipPath>
+ <g clip-path="url(#_clip3)">
+ <g transform="matrix(1.81105,0,0,1,0,0)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(7,5,44);"/>
+ </g>
+ <g transform="matrix(1.81105,0,0,0.30498,-1.13687e-13,190.035)">
+ <rect x="0" y="0" width="138.81" height="600" style="fill:rgb(25,21,68);"/>
+ </g>
+ <g transform="matrix(0.77088,0,0,0.290377,-644.149,483.662)">
+ <rect x="835.602" y="-1337.3" width="326.111" height="326.111" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g>
+ <path d="M251.392,110.852L220.365,141.879L251.392,172.906L251.392,353.271L188.167,353.271C182.556,353.271 178.008,357.819 178.008,363.43L178.008,416.496C178.008,422.107 182.556,426.655 188.167,426.655L251.392,426.655L251.392,600L800,600L800,-0L251.392,-0L251.392,110.852Z" style="fill:rgb(39,46,58);"/>
+ <path d="M251.392,110.852L220.365,141.879L251.392,172.906L251.392,353.271L188.167,353.271C182.556,353.271 178.008,357.819 178.008,363.43L178.008,416.496C178.008,422.107 182.556,426.655 188.167,426.655L251.392,426.655L251.392,600L800,600L800,-0L251.392,-0L251.392,110.852Z" style="fill:rgb(39,46,58);"/>
+ </g>
+ <g transform="matrix(4.8965,0,0,4.8965,6277.58,-3930.98)">
+ <path d="M-1178.23,822.298C-1178.23,819.787 -1179.23,817.378 -1181,815.602C-1182.78,813.826 -1185.19,812.828 -1187.7,812.828C-1192.51,812.828 -1198.34,812.828 -1203.16,812.828C-1205.67,812.828 -1208.08,813.826 -1209.85,815.602C-1211.63,817.378 -1212.63,819.787 -1212.63,822.298C-1212.63,822.299 -1212.63,822.3 -1212.63,822.301C-1212.63,824.813 -1211.63,827.222 -1209.85,828.997C-1208.08,830.773 -1205.67,831.771 -1203.16,831.771C-1198.34,831.771 -1192.51,831.771 -1187.7,831.771C-1185.19,831.771 -1182.78,830.773 -1181,828.997C-1179.23,827.222 -1178.23,824.813 -1178.23,822.301C-1178.23,822.3 -1178.23,822.299 -1178.23,822.298Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(1.39597,0,0,1,-120.257,-227.628)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534L344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054L675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(1.39597,0,0,1,-120.257,-71.831)">
+ <path d="M690.792,421.434C690.792,409.891 684.089,400.534 675.821,400.534C617.133,400.534 403.105,400.534 344.417,400.534C336.149,400.534 329.446,409.891 329.446,421.434L329.446,502.154C329.446,513.697 336.149,523.054 344.417,523.054L675.821,523.054C684.089,523.054 690.792,513.697 690.792,502.154L690.792,421.434ZM687.21,421.434L687.21,502.154C687.21,510.935 682.111,518.054 675.821,518.054C675.821,518.054 344.417,518.054 344.417,518.054C338.127,518.054 333.028,510.935 333.028,502.154L333.028,421.434C333.028,412.653 338.127,405.534 344.417,405.534C403.105,405.534 617.133,405.534 675.821,405.534C682.111,405.534 687.21,412.653 687.21,421.434Z" style="fill:rgb(0,196,240);"/>
+ </g>
+ <g transform="matrix(4.71406,0,0,4.71406,191.129,368.75)">
+ <path d="M8.977,8.086L9.507,7.554C9.727,7.336 9.727,6.98 9.507,6.759L7.248,4.5L9.505,2.238C9.725,2.02 9.725,1.664 9.505,1.444L8.975,0.914C8.757,0.694 8.401,0.694 8.18,0.914L4.993,4.102C4.773,4.322 4.773,4.678 4.995,4.898L8.183,8.086C8.401,8.306 8.757,8.306 8.977,8.086ZM4.475,8.086L5.005,7.556C5.225,7.336 5.225,6.98 5.005,6.762L2.748,4.5L5.007,2.241C5.227,2.02 5.227,1.664 5.007,1.446L4.477,0.914C4.257,0.694 3.901,0.694 3.683,0.914L0.495,4.102C0.273,4.322 0.273,4.678 0.493,4.898L3.68,8.086C3.901,8.306 4.257,8.306 4.475,8.086Z" style="fill:rgb(0,196,240);fill-rule:nonzero;"/>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/public/img/touch-icon.png b/public/img/touch-icon.png
new file mode 100644
index 0000000..c5d5fdd
--- /dev/null
+++ b/public/img/touch-icon.png
Binary files differ
diff --git a/public/img/tree/tree-minus.gif b/public/img/tree/tree-minus.gif
new file mode 100644
index 0000000..551817e
--- /dev/null
+++ b/public/img/tree/tree-minus.gif
Binary files differ
diff --git a/public/img/tree/tree-plus.gif b/public/img/tree/tree-plus.gif
new file mode 100644
index 0000000..72446cc
--- /dev/null
+++ b/public/img/tree/tree-plus.gif
Binary files differ
diff --git a/public/img/website-icon.svg b/public/img/website-icon.svg
new file mode 100644
index 0000000..6ca6ee5
--- /dev/null
+++ b/public/img/website-icon.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
+ <path d="M8.37,2.58c-0.126,-0.074 -0.241,-0.171 -0.339,-0.29c-0.436,-0.535 -0.356,-1.323 0.179,-1.759c0.535,-0.436 1.323,-0.355 1.759,0.179c0.436,0.535 0.356,1.323 -0.179,1.759c-0.276,0.225 -0.619,0.312 -0.947,0.271l-1.112,2.75c0.369,0.169 0.705,0.423 0.979,0.758c0.236,0.29 0.403,0.614 0.502,0.952l3.299,-1.008c-0.003,-0.022 -0.005,-0.044 -0.007,-0.067c-0.062,-0.825 0.557,-1.545 1.384,-1.608c0.825,-0.062 1.545,0.557 1.608,1.384c0.062,0.826 -0.558,1.546 -1.384,1.608c-0.63,0.047 -1.199,-0.302 -1.46,-0.838l-3.343,1.022c0.091,0.885 -0.251,1.797 -0.991,2.402c-0.031,0.024 -0.062,0.049 -0.093,0.072l0.629,1.051c0.292,-0.211 0.662,-0.317 1.048,-0.273c0.823,0.095 1.412,0.841 1.317,1.662c-0.097,0.823 -0.839,1.412 -1.662,1.319c-0.823,-0.096 -1.413,-0.84 -1.317,-1.663c0.029,-0.251 0.118,-0.481 0.252,-0.676l-0.694,-1.159c-0.806,0.397 -1.753,0.367 -2.526,-0.058l-1.839,2.234c0.316,0.324 0.525,0.758 0.561,1.246c0.083,1.101 -0.742,2.062 -1.844,2.144c-1.102,0.083 -2.062,-0.743 -2.144,-1.845c-0.083,-1.101 0.742,-2.061 1.845,-2.143c0.434,-0.033 0.847,0.075 1.192,0.287l1.814,-2.204c-0.138,-0.114 -0.268,-0.243 -0.385,-0.388c-0.805,-0.986 -0.806,-2.364 -0.086,-3.344l-1.849,-1.514c-0.031,0.02 -0.064,0.038 -0.098,0.055c-0.496,0.243 -1.094,0.037 -1.337,-0.459c-0.243,-0.495 -0.037,-1.094 0.459,-1.337c0.496,-0.243 1.094,-0.037 1.337,0.458c0.147,0.302 0.129,0.642 -0.019,0.917l1.839,1.506c0.046,-0.044 0.095,-0.086 0.145,-0.127c0.696,-0.567 1.587,-0.735 2.397,-0.531l1.11,-2.745Z" />
+</svg>
diff --git a/public/img/winter/logo_icinga_big_winter.png b/public/img/winter/logo_icinga_big_winter.png
new file mode 100644
index 0000000..9fbabd3
--- /dev/null
+++ b/public/img/winter/logo_icinga_big_winter.png
Binary files differ
diff --git a/public/img/winter/snow1.png b/public/img/winter/snow1.png
new file mode 100644
index 0000000..034f5b5
--- /dev/null
+++ b/public/img/winter/snow1.png
Binary files differ
diff --git a/public/img/winter/snow2.png b/public/img/winter/snow2.png
new file mode 100644
index 0000000..325487b
--- /dev/null
+++ b/public/img/winter/snow2.png
Binary files differ
diff --git a/public/img/winter/snow3.png b/public/img/winter/snow3.png
new file mode 100644
index 0000000..5f40f16
--- /dev/null
+++ b/public/img/winter/snow3.png
Binary files differ
diff --git a/public/index.php b/public/index.php
new file mode 100644
index 0000000..39a7891
--- /dev/null
+++ b/public/index.php
@@ -0,0 +1,4 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+require_once dirname(__DIR__) . '/library/Icinga/Application/webrouter.php';
diff --git a/public/js/bootstrap.js b/public/js/bootstrap.js
new file mode 100644
index 0000000..425c8ab
--- /dev/null
+++ b/public/js/bootstrap.js
@@ -0,0 +1,28 @@
+;(function () {
+ let html = document.documentElement;
+ window.name = html.dataset.icingaWindowName;
+ window.icinga = new Icinga({
+ baseUrl: html.dataset.icingaBaseUrl,
+ locale: html.lang,
+ timezone: html.dataset.icingaTimezone
+ });
+
+ if (! ('icingaIsIframe' in document.documentElement.dataset)) {
+ html.classList.replace('no-js', 'js');
+ }
+
+ if (window.getComputedStyle) {
+ let matched;
+ let element = document.getElementById('layout');
+ let name = window
+ .getComputedStyle(html)['font-family']
+ .replace(/['",]/g, '');
+
+ if (null !== (matched = name.match(/^([a-z]+)-layout$/))) {
+ element.classList.replace('default-layout', name);
+ if ('object' === typeof window.console) {
+ window.console.log('Icinga Web 2: setting initial layout to ' + name);
+ }
+ }
+ }
+})();
diff --git a/public/js/define.js b/public/js/define.js
new file mode 100644
index 0000000..a3ce8c6
--- /dev/null
+++ b/public/js/define.js
@@ -0,0 +1,118 @@
+/*! Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+(function(window) {
+
+ 'use strict';
+
+ /**
+ * Provide a reference to be later required by foreign code
+ *
+ * @param {string} name Optional, defaults to the name (and path) of the file
+ * @param {string[]} requirements Optional, list of required references, may be relative if from the same package
+ * @param {function} factory Required, function that accepts as many params as there are requirements and that
+ * produces a value to be referenced
+ */
+ var define = function (name, requirements, factory) {
+ define.defines[name] = {
+ requirements: requirements,
+ factory: factory,
+ ref: null
+ }
+
+ define.resolve(name);
+ }
+
+ /**
+ * Return whether the given name references a value
+ *
+ * @param {string} name The absolute name of the reference
+ * @return {boolean}
+ */
+ define.has = function (name) {
+ return name in define.defines && define.defines[name]['ref'] !== null;
+ }
+
+ /**
+ * Get the value of a reference
+ *
+ * @param {string} name The absolute name of the reference
+ * @return {*}
+ */
+ define.get = function (name) {
+ return define.defines[name]['ref'];
+ }
+
+ /**
+ * Set the value of a reference
+ *
+ * @param {string} name The absolute name of the reference
+ * @param {*} ref The value to reference
+ */
+ define.set = function (name, ref) {
+ define.defines[name]['ref'] = ref;
+ }
+
+ /**
+ * Resolve a reference and, if successful, dependent references
+ *
+ * @param {string} name The absolute name of the reference
+ * @return {boolean}
+ */
+ define.resolve = function (name) {
+ var requirements = define.defines[name]['requirements'];
+
+ var exports, ref;
+ var requiredRefs = [];
+ for (var i = 0; i < requirements.length; i++) {
+ if (define.has(requirements[i])) {
+ ref = define.get(requirements[i]);
+ } else if (requirements[i] === 'exports') {
+ exports = ref = {};
+ } else {
+ return false;
+ }
+
+ requiredRefs.push(ref);
+ }
+
+ var factory = define.defines[name]['factory'];
+ var resolved = factory.apply(null, requiredRefs);
+
+ if (typeof exports === 'object') {
+ if (typeof resolved !== 'undefined') {
+ throw new Error('Factory for ' + name + ' returned, although exports were populated');
+ }
+
+ resolved = exports;
+ }
+
+ define.set(name, resolved);
+
+ for (var definedName in define.defines) {
+ if (define.defines[definedName]['requirements'].indexOf(name) >= 0) {
+ define.resolve(definedName);
+ }
+ }
+ }
+
+ /**
+ * Require a reference
+ *
+ * @param {string} name The absolute name of the reference
+ * @return {*}
+ */
+ var require = function(name) {
+ if (define.has(name)) {
+ return define.get(name);
+ }
+
+ throw new ReferenceError(name + ' is not defined');
+ }
+
+ define.icinga = true;
+ define.defines = {};
+
+ window.define = define;
+ window.require = require;
+
+})(window);
diff --git a/public/js/helpers.js b/public/js/helpers.js
new file mode 100644
index 0000000..bcb2736
--- /dev/null
+++ b/public/js/helpers.js
@@ -0,0 +1,91 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/* jQuery Plugins */
+(function ($) {
+
+ 'use strict';
+
+ /* Get data value or default */
+ $.fn.getData = function (name, fallback) {
+ var value = this.data(name);
+ if (typeof value !== 'undefined') {
+ return value;
+ }
+
+ return fallback;
+ };
+
+ /* Whether a HTML tag has a specific attribute */
+ $.fn.hasAttr = function(name) {
+ // We have inconsistent behaviour across browsers (false VS undef)
+ var val = this.attr(name);
+ return typeof val !== 'undefined' && val !== false;
+ };
+
+ /* Get class list */
+ $.fn.classes = function (callback) {
+
+ var classes = [];
+
+ $.each(this, function (i, el) {
+ var c = $(el).attr('class');
+ if (typeof c === 'string') {
+ $.each(c.split(/\s+/), function(i, p) {
+ if (classes.indexOf(p) === -1) {
+ classes.push(p);
+ }
+ });
+ }
+ });
+
+ if (typeof callback === 'function') {
+ for (var i in classes) {
+ if (classes.hasOwnProperty(i)) {
+ callback(classes[i]);
+ }
+ }
+ }
+
+ return classes;
+ };
+
+ /* Serialize form elements to an object */
+ $.fn.serializeObject = function()
+ {
+ var o = {};
+ var a = this.serializeArray();
+ $.each(a, function() {
+ if (o[this.name] !== undefined) {
+ if (!o[this.name].push) {
+ o[this.name] = [o[this.name]];
+ }
+ o[this.name].push(this.value || '');
+ } else {
+ o[this.name] = this.value || '';
+ }
+ });
+ return o;
+ };
+
+ $.fn.offsetTopRelativeTo = function($ancestor) {
+ if (typeof $ancestor === 'undefined') {
+ return false;
+ }
+
+ var el = this[0];
+ var offset = el.offsetTop;
+ var $parent = $(el.offsetParent);
+
+ if ($parent.is('body') || $parent.is($ancestor)) {
+ return offset;
+ }
+
+ if (el.tagName === 'TR') {
+ // TODO: Didn't found a better way, this will probably break sooner or later
+ return $parent.offsetTopRelativeTo($ancestor);
+ }
+
+ return offset + $parent.offsetTopRelativeTo($ancestor);
+ };
+
+})(jQuery);
diff --git a/public/js/icinga.js b/public/js/icinga.js
new file mode 100644
index 0000000..e8b8bcc
--- /dev/null
+++ b/public/js/icinga.js
@@ -0,0 +1,279 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga starts here.
+ *
+ * Usage example:
+ *
+ * <code>
+ * var icinga = new Icinga({
+ * baseUrl: '/icinga',
+ * });
+ * </code>
+ */
+(function(window, $) {
+
+ 'use strict';
+
+ var Icinga = function (config) {
+
+ this.initialized = false;
+
+ /**
+ * Our config object
+ */
+ this.config = config;
+
+ /**
+ * Icinga.Logger
+ */
+ this.logger = null;
+
+ /**
+ * Icinga.UI
+ */
+ this.ui = null;
+
+ /**
+ * Icinga.Loader
+ */
+ this.loader = null;
+
+ /**
+ * Icinga.Events
+ */
+ this.events = null;
+
+ /**
+ * Icinga.Timer
+ */
+ this.timer = null;
+
+ /**
+ * Icinga.History
+ */
+ this.history = null;
+
+ /**
+ * Icinga.Utils
+ */
+ this.utils = null;
+
+ /**
+ * Additional site behavior
+ */
+ this.behaviors = {};
+
+ /**
+ * Loaded modules
+ */
+ this.modules = {};
+
+ var _this = this;
+ $(document).ready(function () {
+ _this.initialize();
+ _this = null;
+ });
+ };
+
+ Icinga.prototype = {
+
+ /**
+ * Icinga startup, will be triggerd once the document is ready
+ */
+ initialize: function () {
+ if (this.initialized) {
+ return false;
+ }
+
+ this.timezone = new Icinga.Timezone();
+ this.utils = new Icinga.Utils(this);
+ this.logger = new Icinga.Logger(this);
+ this.timer = new Icinga.Timer(this);
+ this.ui = new Icinga.UI(this);
+ this.loader = new Icinga.Loader(this);
+ this.events = new Icinga.Events(this);
+ this.history = new Icinga.History(this);
+ var _this = this;
+ $.each(Icinga.Behaviors, function(name, Behavior) {
+ _this.behaviors[name.toLowerCase()] = new Behavior(_this);
+ });
+
+ this.timezone.initialize();
+ this.timer.initialize();
+ this.events.initialize();
+ this.history.initialize();
+ this.ui.initialize();
+ this.loader.initialize();
+
+ this.logger.info('Icinga is ready, running on jQuery ', $().jquery);
+ this.initialized = true;
+
+ // Trigger our own post-init event, `onLoad` is not reliable enough
+ $(document).trigger('icinga-init');
+ },
+
+ /**
+ * Load a given module by name
+ *
+ * @param {string} name
+ *
+ * @return {boolean}
+ */
+ loadModule: function (name) {
+
+ if (this.isLoadedModule(name)) {
+ this.logger.error('Cannot load module ' + name + ' twice');
+ return false;
+ }
+
+ if (! this.hasModule(name)) {
+ this.logger.error('Cannot find module ' + name);
+ return false;
+ }
+
+ this.modules[name] = new Icinga.Module(
+ this,
+ name,
+ Icinga.availableModules[name]
+ );
+ return true;
+ },
+
+ /**
+ * Whether a module matching the given name exists or is loaded
+ *
+ * @param {string} name
+ *
+ * @return {boolean}
+ */
+ hasModule: function (name) {
+ return this.isLoadedModule(name) ||
+ 'undefined' !== typeof Icinga.availableModules[name];
+ },
+
+ /**
+ * Return whether the given module is loaded
+ *
+ * @param {string} name The name of the module
+ *
+ * @returns {Boolean}
+ */
+ isLoadedModule: function (name) {
+ return 'undefined' !== typeof this.modules[name];
+ },
+
+ /**
+ * Ensure we have loaded the javascript code for a module
+ *
+ * @param {string} moduleName
+ */
+ ensureModule: function(moduleName) {
+ if (this.hasModule(moduleName) && ! this.isLoadedModule(moduleName)) {
+ this.loadModule(moduleName);
+ }
+ },
+
+ /**
+ * If a container contains sub-containers for other modules,
+ * make sure the javascript code for each module is loaded.
+ *
+ * Containers are identified by "data-icinga-module" which
+ * holds the module name.
+ *
+ * @param container
+ */
+ ensureSubModules: function (container) {
+ var icinga = this;
+
+ $(container).find('[data-icinga-module]').each(function () {
+ var moduleName = $(this).data('icingaModule');
+ if (moduleName) {
+ icinga.ensureModule(moduleName);
+ }
+ });
+ },
+
+ /**
+ * Get a module by name
+ *
+ * @param {string} name
+ *
+ * @return {object}
+ */
+ module: function (name) {
+
+ if (this.hasModule(name) && !this.isLoadedModule(name)) {
+ this.modules[name] = new Icinga.Module(
+ this,
+ name,
+ Icinga.availableModules[name]
+ );
+ }
+
+ return this.modules[name];
+ },
+
+ /**
+ * Clean up and unload all Icinga components
+ */
+ destroy: function () {
+
+ $.each(this.modules, function (name, module) {
+ module.destroy();
+ });
+
+ this.timezone.destroy();
+ this.timer.destroy();
+ this.events.destroy();
+ this.loader.destroy();
+ this.ui.destroy();
+ this.logger.debug('Icinga has been destroyed');
+ this.logger.destroy();
+ this.utils.destroy();
+
+ this.modules = [];
+ this.timer = this.events = this.loader = this.ui = this.logger =
+ this.utils = null;
+ this.initialized = false;
+ },
+
+ reload: function () {
+ setTimeout(function () {
+ var oldjQuery = window.jQuery;
+ var oldConfig = window.icinga.config;
+ var oldIcinga = window.Icinga;
+ window.icinga.destroy();
+ window.Icinga = undefined;
+ window.$ = undefined;
+ window.jQuery = undefined;
+ jQuery = undefined;
+ $ = undefined;
+
+ oldjQuery.getScript(
+ oldConfig.baseUrl.replace(/\/$/, '') + '/js/icinga.min.js'
+ ).done(function () {
+ var jQuery = window.jQuery;
+ window.icinga = new window.Icinga(oldConfig);
+ window.icinga.initialize();
+ window.icinga.ui.reloadCss();
+ oldjQuery = undefined;
+ oldConfig = undefined;
+ oldIcinga = undefined;
+ }).fail(function () {
+ window.jQuery = oldjQuery;
+ window.$ = window.jQuery;
+ window.Icinga = oldIcinga;
+ window.icinga = new Icinga(oldConfig);
+ window.icinga.ui.reloadCss();
+ });
+ }, 0);
+ }
+
+ };
+
+ window.Icinga = Icinga;
+
+ Icinga.availableModules = {};
+
+})(window, jQuery);
diff --git a/public/js/icinga/behavior/actiontable.js b/public/js/icinga/behavior/actiontable.js
new file mode 100644
index 0000000..0f914f7
--- /dev/null
+++ b/public/js/icinga/behavior/actiontable.js
@@ -0,0 +1,498 @@
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Behavior.ActionTable
+ *
+ * A multi selection that distincts between the table rows using the row action URL filter
+ */
+(function(Icinga, $) {
+
+ "use strict";
+
+ /**
+ * Remove one leading and trailing bracket and all text outside those brackets
+ *
+ * @param str {String}
+ * @returns {string}
+ */
+ var stripBrackets = function (str) {
+ return str.replace(/^[^\(]*\(/, '').replace(/\)[^\)]*$/, '');
+ };
+
+ /**
+ * Parse the filter query contained in the given url filter string
+ *
+ * @param filterString {String}
+ *
+ * @returns {Array} An object containing each row filter
+ */
+ var parseSelectionQuery = function(filterString) {
+ var selections = [];
+ $.each(stripBrackets(filterString).split('|'), function(i, row) {
+ var tuple = {};
+ $.each(stripBrackets(row).split('&'), function(i, keyValue) {
+ var s = keyValue.split('=');
+ tuple[s[0]] = decodeURIComponent(s[1]);
+ });
+ selections.push(tuple);
+ });
+ return selections;
+ };
+
+ /**
+ * Handle the selection of an action table
+ *
+ * @param table {HTMLElement} The table
+ * @param icinga {Icinga}
+ *
+ * @constructor
+ */
+ var Selection = function(table, icinga) {
+ this.$el = $(table);
+ this.icinga = icinga;
+ this.col = this.$el.closest('div.container').attr('id');
+
+ if (this.hasMultiselection()) {
+ if (! this.getMultiselectionKeys().length) {
+ icinga.logger.error('multiselect table has no data-icinga-multiselect-data');
+ }
+ if (! this.getMultiselectionUrl()) {
+ icinga.logger.error('multiselect table has no data-icinga-multiselect-url');
+ }
+ }
+ };
+
+ Selection.prototype = {
+
+ /**
+ * The container id in which this selection happens
+ */
+ col: null,
+
+ /**
+ * Return all rows as jQuery selector
+ *
+ * @returns {jQuery}
+ */
+ rows: function() {
+ return this.$el.find('tr');
+ },
+
+ /**
+ * Return all row action links as jQuery selector
+ *
+ * @returns {jQuery}
+ */
+ rowActions: function() {
+ return this.$el.find('tr[href]');
+ },
+
+ /**
+ * Return all selected rows as jQuery selector
+ *
+ * @returns {jQuery}
+ */
+ selections: function() {
+ return this.$el.find('tr.active');
+ },
+
+ /**
+ * If this selection allows selecting multiple rows
+ *
+ * @returns {Boolean}
+ */
+ hasMultiselection: function() {
+ return this.$el.hasClass('multiselect');
+ },
+
+ /**
+ * Return all filter keys that are significant when applying the selection
+ *
+ * @returns {Array}
+ */
+ getMultiselectionKeys: function() {
+ var data = this.$el.data('icinga-multiselect-data');
+ return (data && data.split(',')) || [];
+ },
+
+ /**
+ * Return the main target URL that is used when multi selecting rows
+ *
+ * This URL may differ from the url that is used when applying single rows
+ *
+ * @returns {String}
+ */
+ getMultiselectionUrl: function() {
+ return this.$el.data('icinga-multiselect-url');
+ },
+
+ /**
+ * Check whether the given url is
+ *
+ * @param {String} url
+ */
+ hasMultiselectionUrl: function(url) {
+ var urls = this.$el.data('icinga-multiselect-url').split(' ');
+
+ var related = this.$el.data('icinga-multiselect-controllers');
+ if (related && related.length) {
+ urls = urls.concat(this.$el.data('icinga-multiselect-controllers').split(' '));
+ }
+
+ var hasSelection = false;
+ $.each(urls, function (i, object) {
+ if (url.indexOf(object) === 0) {
+ hasSelection = true;
+ }
+ });
+ return hasSelection;
+ },
+
+ /**
+ * Read all filter data from the given row
+ *
+ * @param row {jQuery} The row element
+ *
+ * @returns {Object} An object containing all filter data in this row as key-value pairs
+ */
+ getRowData: function(row) {
+ var params = this.icinga.utils.parseUrl(row.attr('href')).params;
+ var tuple = {};
+ var keys = this.getMultiselectionKeys();
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ for (var j = 0; j < params.length; j++) {
+ if (params[j].key === key && (params[j].value || params[j].value === null)) {
+ tuple[key] = params[j].value ? decodeURIComponent(params[j].value): params[j].value;
+ break;
+ }
+ }
+ }
+ return tuple;
+ },
+
+ /**
+ * Deselect all selected rows
+ */
+ clear: function() {
+ this.selections().removeClass('active');
+ },
+
+ /**
+ * Add all rows that match the given filter to the selection
+ *
+ * @param filter {jQuery|Object} Either an object containing filter variables or the actual row to select
+ */
+ select: function(filter) {
+ if (filter instanceof jQuery) {
+ filter.addClass('active');
+ return;
+ }
+ var _this = this;
+ this.rowActions()
+ .filter(
+ function (i, el) {
+ return _this.icinga.utils.objectsEqual(_this.getRowData($(el)), filter);
+ }
+ )
+ .closest('tr')
+ .addClass('active');
+ },
+
+ /**
+ * Toggle the selection of the row between on and off
+ *
+ * @param row {jQuery} The row to toggle
+ */
+ toggle: function(row) {
+ row.toggleClass('active');
+ },
+
+ /**
+ * Add a new selection range to the closest table, using the selected row as
+ * range target.
+ *
+ * @param row {jQuery} The target of the selected range.
+ *
+ * @returns {boolean} If the selection was changed.
+ */
+ range: function(row) {
+ var from, to;
+ var selected = row.first().get(0);
+ this.rows().each(function(i, el) {
+ if ($(el).hasClass('active') || el === selected) {
+ if (!from) {
+ from = el;
+ }
+ to = el;
+ }
+ });
+ var inRange = false;
+ this.rows().each(function(i, el) {
+ if (el === from) {
+ inRange = true;
+ }
+ if (inRange) {
+ $(el).addClass('active');
+ }
+ if (el === to) {
+ inRange = false;
+ }
+ });
+ return false;
+ },
+
+ /**
+ * Select rows that target the given url
+ *
+ * @param url {String} The target url
+ */
+ selectUrl: function(url) {
+ var formerHref = this.$el.closest('.container').data('icinga-actiontable-former-href')
+
+ var $row = this.rows().filter('[href="' + url + '"]');
+
+ if ($row.length) {
+ this.clear();
+ $row.addClass('active');
+ } else {
+ if (this.col !== 'col2') {
+ // rows sometimes need to be displayed as active when related actions
+ // like command actions are being opened. Do not do this for col2, as it
+ // would always select the opened URL itself.
+ var $row = this.rows().filter('[href$="' + icinga.utils.parseUrl(url).query + '"]');
+ if ($row.length) {
+ this.clear();
+ $row.addClass('active');
+ } else {
+ var $row = this.rows().filter('[href$="' + formerHref + '"]');
+ if ($row.length) {
+ this.clear();
+ $row.addClass('active');
+ } else {
+ var tbl = this.$el;
+ if (ActionTable.prototype.tables(
+ tbl.closest('.dashboard').find('.container')).not(tbl).find('tr.active').length
+ ) {
+ this.clear();
+ }
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Convert all currently selected rows into an url query string
+ *
+ * @returns {String} The filter string
+ */
+ toQuery: function() {
+ var _this = this;
+ var selections = this.selections();
+ var queries = [];
+ var utils = this.icinga.utils;
+ if (selections.length === 1) {
+ return $(selections[0]).attr('href');
+ } else if (selections.length > 1 && _this.hasMultiselection()) {
+ selections.each(function (i, el) {
+ var parts = [];
+ $.each(_this.getRowData($(el)), function(key, value) {
+ var condition = utils.fixedEncodeURIComponent(key);
+ if (value !== null) {
+ condition += '=' + utils.fixedEncodeURIComponent(value);
+ }
+
+ parts.push(condition);
+ });
+ queries.push('(' + parts.join('&') + ')');
+ });
+ return _this.getMultiselectionUrl() + '?(' + queries.join('|') + ')';
+ } else {
+ return '';
+ }
+ },
+
+ /**
+ * Refresh the displayed active columns using the current page location
+ */
+ refresh: function() {
+ var hash = icinga.history.getCol2State().replace(/^#!/, '');
+ if (this.hasMultiselection()) {
+ var query = parseSelectionQuery(hash);
+ if (query.length > 1 && this.hasMultiselectionUrl(this.icinga.utils.parseUrl(hash).path)) {
+ this.clear();
+ // select all rows with matching filters
+ var _this = this;
+ $.each(query, function(i, selection) {
+ _this.select(selection);
+ });
+ }
+ if (query.length > 1) {
+ return;
+ }
+ }
+ this.selectUrl(hash);
+ }
+ };
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ var ActionTable = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+
+ /**
+ * If currently loading
+ *
+ * @var Boolean
+ */
+ this.loading = false;
+
+ this.on('rendered', '#main .container', this.onRendered, this);
+ this.on('beforerender', '#main .container', this.beforeRender, this);
+ this.on('click', 'table.action tr[href], table.table-row-selectable tr[href]', this.onRowClicked, this);
+ };
+ ActionTable.prototype = new Icinga.EventListener();
+
+ /**
+ * Return all active tables in this table, or in the context as jQuery selector
+ *
+ * @param context {HTMLElement}
+ * @returns {jQuery}
+ */
+ ActionTable.prototype.tables = function(context) {
+ if (context) {
+ return $(context).find('table.action, table.table-row-selectable');
+ }
+ return $('table.action, table.table-row-selectable');
+ };
+
+ /**
+ * Handle clicks on table rows and update selection and history
+ */
+ ActionTable.prototype.onRowClicked = function (event) {
+ var _this = event.data.self;
+ var $target = $(event.target);
+ var $tr = $(event.currentTarget);
+ var table = new Selection($tr.closest('table.action, table.table-row-selectable')[0], _this.icinga);
+
+ if ($tr.closest('[data-no-icinga-ajax]').length > 0) {
+ return true;
+ }
+
+ // some rows may contain form actions that trigger a different action, pass those through
+ if (!$target.hasClass('rowaction') && $target.closest('form').length &&
+ ($target.closest('a').length || // allow regular link clinks
+ $target.closest('button').length || // allow submitting forms
+ $target.closest('input').length || $target.closest('label').length)) { // allow selecting form elements
+ return;
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+
+ // update selection
+ if (table.hasMultiselection()) {
+ if (event.ctrlKey || event.metaKey) {
+ // add to selection
+ table.toggle($tr);
+ } else if (event.shiftKey) {
+ // range selection
+ table.range($tr);
+ } else {
+ // single selection
+ table.clear();
+ table.select($tr);
+ }
+ } else {
+ table.clear();
+ table.select($tr);
+ }
+
+ var count = table.selections().length;
+ if (count > 0) {
+ var query = table.toQuery();
+ _this.icinga.loader.loadUrl(query, _this.icinga.loader.getLinkTargetFor($tr));
+ } else {
+ if (_this.icinga.loader.getLinkTargetFor($tr).attr('id') === 'col2') {
+ _this.icinga.ui.layout1col();
+ }
+ }
+
+ // redraw all table selections
+ _this.tables().each(function () {
+ new Selection(this, _this.icinga).refresh();
+ });
+
+ // update selection info
+ $('.selection-info-count').text(count);
+ return false;
+ };
+
+ /**
+ * Render the selection and prepare selection rows
+ */
+ ActionTable.prototype.onRendered = function(evt) {
+ var container = evt.target;
+ var _this = evt.data.self;
+
+ if (evt.currentTarget !== container) {
+ // Nested containers are not processed multiple times
+ return;
+ }
+
+ // initialize all rows with the correct row action
+ $('table.action tr, table.table-row-selectable tr', container).each(function(idx, el) {
+
+ // decide which row action to use: links declared with the class rowaction take
+ // the highest precedence before hrefs defined in the tr itself and regular links
+ var $a = $('a[href].rowaction', el).first();
+ if ($a.length) {
+ $(el).attr('href', $a.attr('href'));
+ return;
+ }
+ if ($(el).attr('href') && $(el).attr('href').length) {
+ return;
+ }
+ $a = $('a[href]', el).first();
+ if ($a.length) {
+ $(el).attr('href', $a.attr('href'));
+ }
+ });
+
+ // draw all active selections that have disappeared on reload
+ _this.tables().each(function(i, el) {
+ new Selection(el, _this.icinga).refresh();
+ });
+
+ // update displayed selection counter
+ var table = new Selection(_this.tables(container).first());
+ $(container).find('.selection-info-count').text(table.selections().length);
+ };
+
+ ActionTable.prototype.beforeRender = function(evt) {
+ var container = evt.target;
+ var _this = evt.data.self;
+
+ if (evt.currentTarget !== container) {
+ // Nested containers are not processed multiple times
+ return;
+ }
+
+ var active = _this.tables().find('tr.active');
+ if (active.length) {
+ $(container).data('icinga-actiontable-former-href', active.attr('href'));
+ }
+ };
+
+ ActionTable.prototype.clearAll = function () {
+ var _this = this;
+ this.tables().each(function () {
+ new Selection(this, _this.icinga).clear();
+ });
+ $('.selection-info-count').text('0');
+ };
+
+ Icinga.Behaviors.ActionTable = ActionTable;
+
+}) (Icinga, jQuery);
diff --git a/public/js/icinga/behavior/application-state.js b/public/js/icinga/behavior/application-state.js
new file mode 100644
index 0000000..8c0e2fd
--- /dev/null
+++ b/public/js/icinga/behavior/application-state.js
@@ -0,0 +1,40 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ var ApplicationState = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.on('rendered', '#layout', this.onRendered, this);
+ this.icinga = icinga;
+ };
+
+ ApplicationState.prototype = new Icinga.EventListener();
+
+ ApplicationState.prototype.onRendered = function(e) {
+ if (e.currentTarget !== e.target) {
+ // Nested containers are ignored
+ return;
+ }
+
+ if (! $('#application-state').length
+ && ! $('#login').length
+ && ! $('#guest-error').length
+ && ! $('#setup').length
+ ) {
+ var _this = e.data.self;
+
+ $('#layout').append(
+ '<div id="application-state" class="container" hidden data-icinga-url="'
+ + _this.icinga.loader.baseUrl
+ + '/application-state" data-icinga-refresh="60"></div>'
+ );
+ }
+ };
+
+ Icinga.Behaviors.ApplicationState = ApplicationState;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/behavior/autofocus.js b/public/js/icinga/behavior/autofocus.js
new file mode 100644
index 0000000..e131d9e
--- /dev/null
+++ b/public/js/icinga/behavior/autofocus.js
@@ -0,0 +1,28 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ var Autofocus = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.on('rendered', this.onRendered, this);
+ };
+
+ Autofocus.prototype = new Icinga.EventListener();
+
+ Autofocus.prototype.onRendered = function(e) {
+ setTimeout(function() {
+ if (document.activeElement === e.target
+ || document.activeElement === document.body
+ ) {
+ e.data.self.icinga.ui.focusElement($(e.target).find('.autofocus'));
+ }
+ }, 0);
+ };
+
+ Icinga.Behaviors.Autofocus = Autofocus;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/behavior/collapsible.js b/public/js/icinga/behavior/collapsible.js
new file mode 100644
index 0000000..16f7195
--- /dev/null
+++ b/public/js/icinga/behavior/collapsible.js
@@ -0,0 +1,470 @@
+/*! Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+;(function(Icinga) {
+
+ 'use strict';
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ let $ = window.$;
+
+ try {
+ $ = require('icinga/icinga-php-library/notjQuery');
+ } catch (e) {
+ console.warn('[Collapsible] notjQuery unavailable. Using jQuery for now');
+ }
+
+ /**
+ * Behavior for collapsible containers.
+ *
+ * @param icinga Icinga The current Icinga Object
+ */
+ class Collapsible extends Icinga.EventListener {
+ constructor(icinga) {
+ super(icinga);
+
+ this.on('layout-change', this.onLayoutChange, this);
+ this.on('rendered', '#main > .container, #modal-content', this.onRendered, this);
+ this.on('click', '.collapsible + .collapsible-control, .collapsible .collapsible-control',
+ this.onControlClicked, this);
+
+ this.icinga = icinga;
+ this.defaultVisibleRows = 2;
+ this.defaultVisibleHeight = 36;
+
+ this.state = new Icinga.Storage.StorageAwareMap.withStorage(
+ Icinga.Storage.BehaviorStorage('collapsible'),
+ 'expanded'
+ )
+ .on('add', this.onExpand, this)
+ .on('delete', this.onCollapse, this);
+ }
+
+ /**
+ * Initializes all collapsibles. Triggered on rendering of a container.
+ *
+ * @param event Event The `onRender` event triggered by the rendered container
+ */
+ onRendered(event) {
+ let _this = event.data.self,
+ toCollapse = [],
+ toExpand = [];
+
+ event.target.querySelectorAll('.collapsible').forEach(collapsible => {
+ // Assumes that any newly rendered elements are expanded
+ if (! ('canCollapse' in collapsible.dataset) && _this.canCollapse(collapsible)) {
+ if (_this.setupCollapsible(collapsible)) {
+ toCollapse.push([collapsible, _this.calculateCollapsedHeight(collapsible)]);
+ } else if (_this.isDetails(collapsible)) {
+ // Except if it's a <details> element, which may not be expanded by default
+ toExpand.push(collapsible);
+ }
+ }
+ });
+
+ // Elements are all collapsed in a row now, after height calculations are done.
+ // This avoids reflows since instantly collapsing an element will cause one if
+ // the height of the next element is being calculated.
+ for (const collapseInfo of toCollapse) {
+ _this.collapse(collapseInfo[0], collapseInfo[1]);
+ }
+
+ for (const collapsible of toExpand) {
+ _this.expand(collapsible);
+ }
+ }
+
+ /**
+ * Updates all collapsibles.
+ *
+ * @param event Event The `layout-change` event triggered by window resizing or column changes
+ */
+ onLayoutChange(event) {
+ let _this = event.data.self;
+ let toCollapse = [];
+
+ document.querySelectorAll('.collapsible').forEach(collapsible => {
+ if ('canCollapse' in collapsible.dataset) {
+ if (! _this.canCollapse(collapsible)) {
+ let toggleSelector = collapsible.dataset.toggleElement;
+ if (! this.isDetails(collapsible)) {
+ if (! toggleSelector) {
+ collapsible.nextElementSibling.remove();
+ } else {
+ let toggle = document.getElementById(toggleSelector);
+ if (toggle) {
+ toggle.classList.remove('collapsed');
+ delete toggle.dataset.canCollapse;
+ }
+ }
+ }
+
+ delete collapsible.dataset.canCollapse;
+ _this.expand(collapsible);
+ }
+ } else if (_this.canCollapse(collapsible) && _this.setupCollapsible(collapsible)) {
+ // It's expanded but shouldn't
+ toCollapse.push([collapsible, _this.calculateCollapsedHeight(collapsible)]);
+ }
+ });
+
+ setTimeout(function () {
+ for (const collapseInfo of toCollapse) {
+ _this.collapse(collapseInfo[0], collapseInfo[1]);
+ }
+ }, 0);
+ }
+
+ /**
+ * A collapsible got expanded in another window, try to apply this here as well
+ *
+ * @param {string} collapsiblePath
+ */
+ onExpand(collapsiblePath) {
+ let collapsible = document.querySelector(collapsiblePath);
+
+ if (collapsible && 'canCollapse' in collapsible.dataset) {
+ if ('stateCollapses' in collapsible.dataset) {
+ this.collapse(collapsible, this.calculateCollapsedHeight(collapsible));
+ } else {
+ this.expand(collapsible);
+ }
+ }
+ }
+
+ /**
+ * A collapsible got collapsed in another window, try to apply this here as well
+ *
+ * @param {string} collapsiblePath
+ */
+ onCollapse(collapsiblePath) {
+ let collapsible = document.querySelector(collapsiblePath);
+
+ if (collapsible && this.canCollapse(collapsible)) {
+ if ('stateCollapses' in collapsible.dataset) {
+ this.expand(collapsible);
+ } else {
+ this.collapse(collapsible, this.calculateCollapsedHeight(collapsible));
+ }
+ }
+ }
+
+ /**
+ * Event handler for toggling collapsibles. Switches the collapsed state of the respective container.
+ *
+ * @param event Event The `onClick` event triggered by the clicked collapsible-control element
+ */
+ onControlClicked(event) {
+ let _this = event.data.self,
+ target = event.currentTarget;
+
+ let collapsible = target.previousElementSibling;
+ if ('collapsibleAt' in target.dataset) {
+ collapsible = document.querySelector(target.dataset.collapsibleAt);
+ } else if (! collapsible) {
+ collapsible = target.closest('.collapsible');
+ }
+
+ if (! collapsible) {
+ _this.icinga.logger.error(
+ '[Collapsible] Collapsible control has no associated .collapsible: ', target);
+
+ return;
+ } else if ('noPersistence' in collapsible.dataset) {
+ if (collapsible.classList.contains('collapsed')) {
+ _this.expand(collapsible);
+ } else {
+ _this.collapse(collapsible, _this.calculateCollapsedHeight(collapsible));
+ }
+ } else {
+ let collapsiblePath = _this.icinga.utils.getCSSPath(collapsible),
+ stateCollapses = 'stateCollapses' in collapsible.dataset;
+
+ if (_this.state.has(collapsiblePath)) {
+ _this.state.delete(collapsiblePath);
+
+ if (stateCollapses) {
+ _this.expand(collapsible);
+ } else {
+ _this.collapse(collapsible, _this.calculateCollapsedHeight(collapsible));
+ }
+ } else {
+ _this.state.set(collapsiblePath);
+
+ if (stateCollapses) {
+ _this.collapse(collapsible, _this.calculateCollapsedHeight(collapsible));
+ } else {
+ _this.expand(collapsible);
+ }
+ }
+ }
+
+ if (_this.isDetails(collapsible)) {
+ // The browser handles these clicks as well, and would toggle the state again
+ event.preventDefault();
+ }
+ }
+
+ /**
+ * Setup the given collapsible
+ *
+ * @param collapsible The given collapsible container element
+ *
+ * @returns {boolean} Whether it needs to collapse or not
+ */
+ setupCollapsible(collapsible) {
+ if (this.isDetails(collapsible)) {
+ let summary = collapsible.querySelector(':scope > summary');
+ if (! summary.classList.contains('collapsible-control')) {
+ summary.classList.add('collapsible-control');
+ }
+
+ if (collapsible.open) {
+ collapsible.dataset.stateCollapses = '';
+ }
+ } else if (!! collapsible.dataset.toggleElement) {
+ let toggleSelector = collapsible.dataset.toggleElement,
+ toggle = collapsible.querySelector(toggleSelector),
+ externalToggle = false;
+ if (! toggle) {
+ if (collapsible.nextElementSibling && collapsible.nextElementSibling.matches(toggleSelector)) {
+ toggle = collapsible.nextElementSibling;
+ } else {
+ externalToggle = true;
+ toggle = document.getElementById(toggleSelector);
+ }
+ }
+
+ if (! toggle) {
+ if (externalToggle) {
+ this.icinga.logger.error(
+ '[Collapsible] External control with id `'
+ + toggleSelector
+ + '` not found for .collapsible',
+ collapsible
+ );
+ } else {
+ this.icinga.logger.error(
+ '[Collapsible] Control `' + toggleSelector + '` not found in .collapsible', collapsible);
+ }
+
+ return false;
+ } else if (externalToggle) {
+ collapsible.dataset.hasExternalToggle = '';
+
+ toggle.dataset.canCollapse = '';
+ toggle.dataset.collapsibleAt = this.icinga.utils.getCSSPath(collapsible);
+ $(toggle).on('click', e => {
+ // Only required as onControlClicked() is compatible with Icinga.EventListener
+ e.data = { self: this };
+ this.onControlClicked(e);
+ });
+ } else if (! toggle.classList.contains('collapsible-control')) {
+ toggle.classList.add('collapsible-control');
+ }
+ } else {
+ setTimeout(function () {
+ let collapsibleControl = document
+ .getElementById('collapsible-control-ghost')
+ .cloneNode(true);
+ collapsibleControl.removeAttribute('id');
+ collapsible.parentNode.insertBefore(collapsibleControl, collapsible.nextElementSibling);
+ }, 0);
+ }
+
+ collapsible.dataset.canCollapse = '';
+
+ if ('noPersistence' in collapsible.dataset) {
+ return ! ('stateCollapses' in collapsible.dataset);
+ }
+
+ if ('stateCollapses' in collapsible.dataset) {
+ return this.state.has(this.icinga.utils.getCSSPath(collapsible));
+ } else {
+ return ! this.state.has(this.icinga.utils.getCSSPath(collapsible));
+ }
+ }
+
+ /**
+ * Return an appropriate row element selector
+ *
+ * @param collapsible The given collapsible container element
+ *
+ * @returns {string}
+ */
+ getRowSelector(collapsible) {
+ if (!! collapsible.dataset.visibleHeight) {
+ return '';
+ }
+
+ if (collapsible.tagName === 'TABLE') {
+ return ':scope > tbody > tr';
+ } else if (collapsible.tagName === 'UL' || collapsible.tagName === 'OL') {
+ return ':scope > li:not(.collapsible-control)';
+ }
+
+ return '';
+ }
+
+ /**
+ * Check whether the given collapsible needs to collapse
+ *
+ * @param collapsible The given collapsible container element
+ *
+ * @returns {boolean}
+ */
+ canCollapse(collapsible) {
+ if (this.isDetails(collapsible)) {
+ return collapsible.querySelector(':scope > summary') !== null;
+ }
+
+ let rowSelector = this.getRowSelector(collapsible);
+ if (!! rowSelector) {
+ let collapseAfter = Number(collapsible.dataset.collapseAfter)
+ if (isNaN(collapseAfter)) {
+ collapseAfter = Number(collapsible.dataset.visibleRows);
+ if (isNaN(collapseAfter)) {
+ collapseAfter = this.defaultVisibleRows;
+ }
+
+ collapseAfter *= 2;
+ }
+
+ if (collapseAfter === 0) {
+ return true;
+ }
+
+ return collapsible.querySelectorAll(rowSelector).length > collapseAfter;
+ } else {
+ let maxHeight = Number(collapsible.dataset.visibleHeight);
+ if (isNaN(maxHeight)) {
+ maxHeight = this.defaultVisibleHeight;
+ } else if (maxHeight === 0) {
+ return true;
+ }
+
+ let actualHeight = collapsible.scrollHeight - parseFloat(
+ window.getComputedStyle(collapsible).getPropertyValue('padding-top')
+ );
+
+ return actualHeight >= maxHeight * 2;
+ }
+ }
+
+ /**
+ * Calculate the height the given collapsible should have when collapsed
+ *
+ * @param collapsible
+ */
+ calculateCollapsedHeight(collapsible) {
+ let height;
+
+ if (this.isDetails(collapsible)) {
+ return -1;
+ }
+
+ let rowSelector = this.getRowSelector(collapsible);
+ if (!! rowSelector) {
+ height = collapsible.scrollHeight;
+ height -= parseFloat(window.getComputedStyle(collapsible).getPropertyValue('padding-bottom'));
+
+ let visibleRows = Number(collapsible.dataset.visibleRows);
+ if (isNaN(visibleRows)) {
+ visibleRows = this.defaultVisibleRows;
+ }
+
+ let rows = Array.from(collapsible.querySelectorAll(rowSelector)).slice(visibleRows);
+ for (let i = 0; i < rows.length; i++) {
+ let row = rows[i];
+
+ if (row.previousElementSibling === null) { // very first element
+ height -= row.offsetHeight;
+ height -= parseFloat(window.getComputedStyle(row).getPropertyValue('margin-top'));
+ } else if (i < rows.length - 1) { // every element but the last one
+ let prevBottomBorderAt = row.previousElementSibling.offsetTop;
+ prevBottomBorderAt += row.previousElementSibling.offsetHeight;
+ height -= row.offsetTop - prevBottomBorderAt + row.offsetHeight;
+ } else { // the last element
+ height -= row.offsetHeight;
+ height -= parseFloat(window.getComputedStyle(row).getPropertyValue('margin-top'));
+ height -= parseFloat(window.getComputedStyle(row).getPropertyValue('margin-bottom'));
+ }
+ }
+ } else {
+ height = Number(collapsible.dataset.visibleHeight);
+ if (isNaN(height)) {
+ height = this.defaultVisibleHeight;
+ }
+
+ height += parseFloat(window.getComputedStyle(collapsible).getPropertyValue('padding-top'));
+
+ if (
+ !! collapsible.dataset.toggleElement
+ && ! ('hasExternalToggle' in collapsible.dataset)
+ && (! collapsible.nextElementSibling
+ || ! collapsible.nextElementSibling.matches(collapsible.dataset.toggleElement))
+ ) {
+ let toggle = collapsible.querySelector(collapsible.dataset.toggleElement);
+ height += toggle.offsetHeight; // TODO: Very expensive at times. (50ms+) Check why!
+ height += parseFloat(window.getComputedStyle(toggle).getPropertyValue('margin-top'));
+ height += parseFloat(window.getComputedStyle(toggle).getPropertyValue('margin-bottom'));
+ }
+ }
+
+ return height;
+ }
+
+ /**
+ * Collapse the given collapsible
+ *
+ * @param collapsible The given collapsible container element
+ * @param toHeight {int} The height in pixels to collapse to
+ */
+ collapse(collapsible, toHeight) {
+ if (this.isDetails(collapsible)) {
+ collapsible.open = false;
+ } else {
+ collapsible.style.cssText = 'display: block; height: ' + toHeight + 'px; padding-bottom: 0';
+
+ if ('hasExternalToggle' in collapsible.dataset) {
+ document.getElementById(collapsible.dataset.toggleElement).classList.add('collapsed');
+ }
+ }
+
+ collapsible.classList.add('collapsed');
+ }
+
+ /**
+ * Expand the given collapsible
+ *
+ * @param collapsible The given collapsible container element
+ */
+ expand(collapsible) {
+ collapsible.classList.remove('collapsed');
+
+ if (this.isDetails(collapsible)) {
+ collapsible.open = true;
+ } else {
+ collapsible.style.cssText = '';
+
+ if ('hasExternalToggle' in collapsible.dataset) {
+ document.getElementById(collapsible.dataset.toggleElement).classList.remove('collapsed');
+ }
+ }
+ }
+
+ /**
+ * Get whether the given collapsible is a <details> element
+ *
+ * @param collapsible
+ *
+ * @return {Boolean}
+ */
+ isDetails(collapsible) {
+ return collapsible.tagName === 'DETAILS';
+ }
+ }
+
+ Icinga.Behaviors.Collapsible = Collapsible;
+
+})(Icinga);
diff --git a/public/js/icinga/behavior/copy-to-clipboard.js b/public/js/icinga/behavior/copy-to-clipboard.js
new file mode 100644
index 0000000..cdd2615
--- /dev/null
+++ b/public/js/icinga/behavior/copy-to-clipboard.js
@@ -0,0 +1,41 @@
+(function (Icinga) {
+
+ "use strict";
+
+ try {
+ var CopyToClipboard = require('icinga/icinga-php-library/widget/CopyToClipboard');
+ } catch (e) {
+ console.warn('Unable to provide copy to clipboard feature. Libraries not available:', e);
+ return;
+ }
+
+ class CopyToClipboardBehavior extends Icinga.EventListener {
+ constructor(icinga)
+ {
+ super(icinga);
+
+ this.on('rendered', '#main > .container', this.onRendered, this);
+
+ /**
+ * Clipboard buttons
+ *
+ * @type {WeakMap<object, CopyToClipboard>}
+ * @private
+ */
+ this._clipboards = new WeakMap();
+ }
+
+ onRendered(event)
+ {
+ let _this = event.data.self;
+
+ event.currentTarget.querySelectorAll('[data-icinga-clipboard]').forEach(button => {
+ _this._clipboards.set(button, new CopyToClipboard(button));
+ });
+ }
+ }
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ Icinga.Behaviors.CopyToClipboardBehavior = CopyToClipboardBehavior;
+})(Icinga);
diff --git a/public/js/icinga/behavior/datetime-picker.js b/public/js/icinga/behavior/datetime-picker.js
new file mode 100644
index 0000000..fb0ddff
--- /dev/null
+++ b/public/js/icinga/behavior/datetime-picker.js
@@ -0,0 +1,222 @@
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+/**
+ * DatetimePicker - Behavior for inputs that should show a date and time picker
+ */
+;(function(Icinga, $) {
+
+ 'use strict';
+
+ try {
+ var Flatpickr = require('icinga/icinga-php-library/vendor/flatpickr');
+ var notjQuery = require('icinga/icinga-php-library/notjQuery');
+ } catch (e) {
+ console.warn('Unable to provide datetime picker. Libraries not available:', e);
+ return;
+ }
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ /**
+ * Behavior for datetime pickers.
+ *
+ * @param icinga {Icinga} The current Icinga Object
+ */
+ var DatetimePicker = function(icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.icinga = icinga;
+
+ /**
+ * The formats the server expects
+ *
+ * In a syntax flatpickr understands. Based on https://flatpickr.js.org/formatting/
+ *
+ * @type {string}
+ */
+ this.server_full_format = 'Y-m-d\\TH:i:S';
+ this.server_date_format = 'Y-m-d';
+ this.server_time_format = 'H:i:S';
+
+ /**
+ * The flatpickr instances created
+ *
+ * @type {Map<Flatpickr, string>}
+ * @private
+ */
+ this._pickers = new Map();
+
+ this.on('rendered', '#main > .container, #modal-content', this.onRendered, this);
+ this.on('close-column', this.onCloseContainer, this);
+ this.on('close-modal', this.onCloseContainer, this);
+ };
+
+ DatetimePicker.prototype = new Icinga.EventListener();
+
+ /**
+ * Add flatpickr widget on selected inputs
+ *
+ * @param event {Event}
+ */
+ DatetimePicker.prototype.onRendered = function(event) {
+ var _this = event.data.self;
+ var containerId = event.target.dataset.icingaContainerId;
+ var inputs = event.target.querySelectorAll('input[data-use-datetime-picker]');
+
+ // Cleanup left-over pickers from the previous content
+ _this.cleanupPickers(containerId);
+
+ $.each(inputs, function () {
+ if (this.type !== 'text') {
+ // Ignore native inputs. Browser widgets are (mostly) superior.
+ // TODO: This makes the type distinction below useless.
+ // Refactor this once we decided how we continue here in the future.
+ return;
+ }
+
+ var server_format = _this.server_full_format;
+ if (this.type === 'date') {
+ server_format = _this.server_date_format;
+ } else if (this.type === 'time') {
+ server_format = _this.server_time_format;
+ }
+
+ // Inject calendar container into a new empty div, inside the column/modal but outside the form.
+ // See https://github.com/flatpickr/flatpickr/issues/2054 for details.
+ var appendTo = document.createElement('div');
+ this.form.parentNode.insertBefore(appendTo, this.form.nextSibling);
+
+ var enableTime = server_format !== _this.server_date_format;
+ var disableDate = server_format === _this.server_time_format;
+ var dateTimeFormatter = _this.createFormatter(! disableDate, enableTime);
+ var options = {
+ locale: _this.loadFlatpickrLocale(),
+ appendTo: appendTo,
+ altInput: true,
+ enableTime: enableTime,
+ noCalendar: disableDate,
+ dateFormat: server_format,
+ formatDate: function (date, format, locale) {
+ return format === this.dateFormat
+ ? Flatpickr.formatDate(date, format, locale)
+ : dateTimeFormatter.format(date);
+ }
+ };
+
+ for (name in this.dataset) {
+ if (name.length > 9 && name.substr(0, 9) === 'flatpickr') {
+ var value = this.dataset[name];
+ if (value === '') {
+ value = true;
+ }
+
+ options[name.charAt(9).toLowerCase() + name.substr(10)] = value;
+ }
+ }
+
+ var element = this;
+ if (!! options.wrap) {
+ element = this.parentNode;
+ }
+
+ var fp = Flatpickr(element, options);
+ fp.calendarContainer.classList.add('icinga-datetime-picker');
+
+ if (! !!options.wrap) {
+ this.parentNode.insertBefore(_this.renderIcon(), fp.altInput.nextSibling);
+ }
+
+ _this._pickers.set(fp, containerId);
+ });
+ };
+
+ /**
+ * Cleanup all flatpickr instances in the closed container
+ *
+ * @param event {Event}
+ */
+ DatetimePicker.prototype.onCloseContainer = function (event) {
+ var _this = event.data.self;
+ var containerId = event.target.dataset.icingaContainerId;
+
+ _this.cleanupPickers(containerId);
+ };
+
+ /**
+ * Destroy all flatpickr instances in the container with the given id
+ *
+ * @param containerId {String}
+ */
+ DatetimePicker.prototype.cleanupPickers = function (containerId) {
+ this._pickers.forEach(function (cId, fp) {
+ if (cId === containerId) {
+ this._pickers.delete(fp);
+ fp.destroy();
+ }
+ }, this);
+ };
+
+ /**
+ * Close all other flatpickr instances and keep the given one
+ *
+ * @param fp {Flatpickr}
+ */
+ DatetimePicker.prototype.closePickers = function (fp) {
+ var containerId = this._pickers.get(fp);
+ this._pickers.forEach(function (cId, fp2) {
+ if (cId === containerId && fp2 !== fp) {
+ fp2.close();
+ }
+ }, this);
+ };
+
+ DatetimePicker.prototype.createFormatter = function (withDate, withTime) {
+ var options = {};
+ if (withDate) {
+ options.year = 'numeric';
+ options.month = 'numeric';
+ options.day = 'numeric';
+ }
+ if (withTime) {
+ options.hour = 'numeric';
+ options.minute = 'numeric';
+ options.timeZoneName = 'short';
+ options.timeZone = this.icinga.config.timezone;
+ }
+
+ return new Intl.DateTimeFormat([this.icinga.config.locale, 'en'], options);
+ };
+
+ DatetimePicker.prototype.loadFlatpickrLocale = function () {
+ switch (this.icinga.config.locale) {
+ case 'ar':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/ar').Arabic;
+ case 'de':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/de').German;
+ case 'es':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/es').Spanish;
+ case 'fi':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/fi').Finnish;
+ case 'fr':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/fr').French;
+ case 'it':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/it').Italian;
+ case 'ja':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/ja').Japanese;
+ case 'pt':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/pt').Portuguese;
+ case 'ru':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/ru').Russian;
+ case 'uk':
+ return require('icinga/icinga-php-library/vendor/flatpickr/l10n/uk').Ukrainian;
+ default:
+ return 'default';
+ }
+ };
+
+ DatetimePicker.prototype.renderIcon = function () {
+ return notjQuery.render('<i class="icon fa fa-calendar" role="image"></i>');
+ };
+
+ Icinga.Behaviors.DatetimePicker = DatetimePicker;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/behavior/detach.js b/public/js/icinga/behavior/detach.js
new file mode 100644
index 0000000..16fe157
--- /dev/null
+++ b/public/js/icinga/behavior/detach.js
@@ -0,0 +1,73 @@
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Behavior.Detach
+ *
+ * Detaches DOM elements before an auto-refresh and attaches them back afterwards
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ function Detach(icinga) {
+ Icinga.EventListener.call(this, icinga);
+ }
+
+ Detach.prototype = new Icinga.EventListener();
+
+ /**
+ * Mutates the HTML before it is placed in the DOM after a reload
+ *
+ * @param content {string} The content to be rendered
+ * @param $container {jQuery} The target container
+ * @param action {string} The URL that caused the reload
+ * @param autorefresh {bool} Whether the rendering is due to an auto-refresh
+ *
+ * @return {string|null} The content to be rendered or null, when nothing should be changed
+ */
+ Detach.prototype.renderHook = function(content, $container, action, autorefresh) {
+ // Exit early
+ if (! autorefresh) {
+ return content;
+ } else {
+ var containerId = $container.attr('id');
+
+ if (containerId === 'menu' || containerId === 'application-state') {
+ return content;
+ }
+ }
+
+ if (! $container.find('.detach:first').length) {
+ return content;
+ }
+
+ var $content = $('<div></div>').append(content);
+ var icinga = this.icinga;
+
+ $content.find('.detach').each(function() {
+ // Selector only works w/ IDs because it was initially built to work w/ absolute paths only
+ var $detachTarget = $(this);
+ var detachTargetId = $detachTarget.attr('id');
+ if (detachTargetId === undefined) {
+ return;
+ }
+
+ var selector = '#' + detachTargetId + ':first';
+ var $detachSource = $container.find(selector);
+
+ if ($detachSource.length) {
+ icinga.logger.debug('Detaching ' + selector);
+ $detachSource.detach();
+ $detachTarget.replaceWith($detachSource);
+ $detachTarget.remove();
+ }
+ });
+
+ return $content.html();
+ };
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ Icinga.Behaviors.Detach = Detach;
+
+}) (Icinga, jQuery);
diff --git a/public/js/icinga/behavior/dropdown.js b/public/js/icinga/behavior/dropdown.js
new file mode 100644
index 0000000..691e634
--- /dev/null
+++ b/public/js/icinga/behavior/dropdown.js
@@ -0,0 +1,66 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+;(function(Icinga, $) {
+
+ "use strict";
+
+ /**
+ * Toggle the CSS class active of the dropdown navigation item
+ *
+ * Called when the dropdown toggle has been activated via mouse or keyobard. This will expand/collpase the dropdown
+ * menu according to CSS.
+ *
+ * @param {object} e Event
+ */
+ function setActive(e) {
+ $(this).parent().toggleClass('active');
+ }
+
+ /**
+ * Clear active state of the dropdown navigation item when the mouse leaves the navigation item
+ *
+ * @param {object} e Event
+ */
+ function clearActive(e) {
+ $(this).removeClass('active');
+ }
+
+ /**
+ * Clear active state of the dropdown navigation item when the navigation items loses focus
+ *
+ * @param {object} e Event
+ */
+ function clearFocus(e) {
+ var $dropdown = $(this);
+ // Timeout is required to wait for the next element in the DOM to receive focus
+ setTimeout(function() {
+ if (! $.contains($dropdown[0], document.activeElement)) {
+ $dropdown.removeClass('active');
+ }
+ }, 10);
+ }
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ /**
+ * Behavior for dropdown navigation items
+ *
+ * The dropdown behavior listens for activity on dropdown navigation items for toggling the CSS class
+ * active on them. CSS is responsible for the expanded and collapsed state.
+ *
+ * @param {Icinga} icinga
+ *
+ * @constructor
+ */
+ var Dropdown = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.on('click', '.dropdown-nav-item > a', setActive, this);
+ this.on('mouseleave', '.dropdown-nav-item', clearActive, this);
+ this.on('focusout', '.dropdown-nav-item', clearFocus, this);
+ };
+
+ Dropdown.prototype = new Icinga.EventListener();
+
+ Icinga.Behaviors.Dropdown = Dropdown;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/behavior/filtereditor.js b/public/js/icinga/behavior/filtereditor.js
new file mode 100644
index 0000000..ffcad01
--- /dev/null
+++ b/public/js/icinga/behavior/filtereditor.js
@@ -0,0 +1,77 @@
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Behavior.FilterEditor
+ *
+ * Initially expanded, but collapsable subtrees
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ var containerId = /^col(\d+)$/;
+ var filterEditors = {};
+
+ function FilterEditor(icinga) {
+ Icinga.EventListener.call(this, icinga);
+
+ this.on('beforerender', '#main > .container', this.beforeRender, this);
+ this.on('rendered', '#main > .container', this.onRendered, this);
+ }
+
+ FilterEditor.prototype = new Icinga.EventListener();
+
+ FilterEditor.prototype.beforeRender = function(event) {
+ if (event.currentTarget !== event.target) {
+ // Nested containers are ignored
+ return;
+ }
+
+ var $container = $(event.target);
+ var match = containerId.exec($container.attr('id'));
+
+ if (match !== null) {
+ var id = match[1];
+ var subTrees = {};
+ filterEditors[id] = subTrees;
+
+ $container.find('.tree .handle').each(function () {
+ var $li = $(this).closest('li');
+
+ subTrees[$li.find('select').first().attr('name')] = $li.hasClass('collapsed');
+ });
+ }
+ };
+
+ FilterEditor.prototype.onRendered = function(event) {
+ if (event.currentTarget !== event.target) {
+ // Nested containers are ignored
+ return;
+ }
+
+ var $container = $(event.target);
+ var match = containerId.exec($container.attr('id'));
+
+ if (match !== null) {
+ var id = match[1];
+
+ if (typeof filterEditors[id] !== "undefined") {
+ var subTrees = filterEditors[id];
+ delete filterEditors[id];
+
+ $container.find('.tree .handle').each(function () {
+ var $li = $(this).closest('li');
+ var name = $li.find('select').first().attr('name');
+ if (typeof subTrees[name] !== "undefined" && subTrees[name] !== $li.hasClass('collapsed')) {
+ $li.toggleClass('collapsed');
+ }
+ });
+ }
+ }
+ };
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ Icinga.Behaviors.FilterEditor = FilterEditor;
+
+}) (Icinga, jQuery);
diff --git a/public/js/icinga/behavior/flyover.js b/public/js/icinga/behavior/flyover.js
new file mode 100644
index 0000000..207d577
--- /dev/null
+++ b/public/js/icinga/behavior/flyover.js
@@ -0,0 +1,85 @@
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Behavior.Flyover
+ *
+ * A toggleable flyover
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ var expandedFlyovers = {};
+
+ function Flyover(icinga) {
+ Icinga.EventListener.call(this, icinga);
+
+ this.on('rendered', '#main > .container', this.onRendered, this);
+ this.on('click', this.onClick, this);
+ this.on('click', '.flyover-toggle', this.onClickFlyoverToggle, this);
+ }
+
+ Flyover.prototype = new Icinga.EventListener();
+
+ Flyover.prototype.onRendered = function(event) {
+ // Re-expand expanded containers after an auto-refresh
+
+ $(event.target).find('.flyover').each(function() {
+ var $this = $(this);
+
+ if (typeof expandedFlyovers['#' + $this.attr('id')] !== 'undefined') {
+ var $container = $this.closest('.container');
+
+ if ($this.offset().left - $container.offset().left > $container.innerWidth() / 2) {
+ $this.addClass('flyover-right');
+ }
+
+ $this.toggleClass('flyover-expanded');
+ }
+ });
+ };
+
+ Flyover.prototype.onClick = function(event) {
+ // Close flyover on click outside the flyover
+ var $target = $(event.target);
+
+ if (! $target.closest('.flyover').length) {
+ var _this = event.data.self;
+ $.each(expandedFlyovers, function (id) {
+ _this.onClickFlyoverToggle({target: $('.flyover-toggle', id)[0]});
+ });
+ }
+ };
+
+ Flyover.prototype.onClickFlyoverToggle = function(event) {
+ var $flyover = $(event.target).closest('.flyover');
+
+ $flyover.toggleClass('flyover-expanded');
+
+ var $container = $flyover.closest('.container');
+ if ($flyover.hasClass('flyover-expanded')) {
+ if ($flyover.offset().left - $container.offset().left > $container.innerWidth() / 2) {
+ $flyover.addClass('flyover-right');
+ }
+
+ if ($flyover.is('[data-flyover-suspends-auto-refresh]')) {
+ $container[0].dataset.suspendAutorefresh = '';
+ }
+
+ expandedFlyovers['#' + $flyover.attr('id')] = null;
+ } else {
+ $flyover.removeClass('flyover-right');
+
+ if ($flyover.is('[data-flyover-suspends-auto-refresh]')) {
+ delete $container[0].dataset.suspendAutorefresh;
+ }
+
+ delete expandedFlyovers['#' + $flyover.attr('id')];
+ }
+ };
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ Icinga.Behaviors.Flyover = Flyover;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/behavior/form.js b/public/js/icinga/behavior/form.js
new file mode 100644
index 0000000..ca9db3b
--- /dev/null
+++ b/public/js/icinga/behavior/form.js
@@ -0,0 +1,96 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Controls behavior of form elements, depending reload and
+ */
+(function(Icinga, $) {
+
+ "use strict";
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ var Form = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.on('rendered', '.container', this.onRendered, this);
+
+ // store the modification state of all input fields
+ this.inputs = new WeakMap();
+ };
+ Form.prototype = new Icinga.EventListener();
+
+ /**
+ * @param event
+ */
+ Form.prototype.onRendered = function (event) {
+ var _this = event.data.self;
+ var container = event.target;
+
+ container.querySelectorAll('form input').forEach(function (input) {
+ if (! _this.inputs.has(input) && input.type !== 'hidden') {
+ _this.inputs.set(input, input.value);
+ _this.icinga.logger.debug('registering "' + input.value + '" as original input value');
+ }
+ });
+ };
+
+ /**
+ * Mutates the HTML before it is placed in the DOM after a reload
+ *
+ * @param content {String} The content to be rendered
+ * @param $container {jQuery} The target container where the html will be rendered in
+ * @param action {String} The action-url that caused the reload
+ * @param autorefresh {Boolean} Whether the rendering is due to an autoRefresh
+ * @param autoSubmit {Boolean} Whether the rendering is due to an autoSubmit
+ *
+ * @returns {string|NULL} The content to be rendered, or NULL, when nothing should be changed
+ */
+ Form.prototype.renderHook = function(content, $container, action, autorefresh, autoSubmit) {
+ if ($container.attr('id') === 'menu') {
+ var $search = $container.find('#search');
+ if ($search[0] === document.activeElement) {
+ return null;
+ }
+ if ($search.length) {
+ var $content = $('<div></div>').append(content);
+ $content.find('#search').attr('value', $search.val()).addClass('active');
+ return $content.html();
+ }
+ return content;
+ }
+
+ if (! autorefresh || autoSubmit) {
+ return content;
+ }
+
+ var _this = this;
+ var changed = false;
+ $container[0].querySelectorAll('form input').forEach(function (input) {
+ if (_this.inputs.has(input) && _this.inputs.get(input) !== input.value) {
+ changed = true;
+ _this.icinga.logger.debug(
+ '"' + _this.inputs.get(input) + '" was changed ("' + input.value + '") and aborts reload...'
+ );
+ }
+ });
+ if (changed) {
+ return null;
+ }
+
+ var origFocus = document.activeElement;
+ var containerId = $container.attr('id');
+ if ($container.has(origFocus).length
+ && $(origFocus).length
+ && ! $(origFocus).hasClass('autofocus')
+ && $(origFocus).closest('form').length
+ && $(origFocus).not(':input[type=button], :input[type=submit], :input[type=reset]').length
+ ) {
+ this.icinga.logger.debug('Not changing content for ' + containerId + ' form has focus');
+ return null;
+ }
+
+ return content;
+ };
+
+ Icinga.Behaviors.Form = Form;
+
+}) (Icinga, jQuery);
diff --git a/public/js/icinga/behavior/input-enrichment.js b/public/js/icinga/behavior/input-enrichment.js
new file mode 100644
index 0000000..1540941
--- /dev/null
+++ b/public/js/icinga/behavior/input-enrichment.js
@@ -0,0 +1,148 @@
+/* Icinga Web 2 | (c) 2020 Icinga GmbH | GPLv2+ */
+
+/**
+ * InputEnrichment - Behavior for forms with enriched inputs
+ */
+(function(Icinga) {
+
+ "use strict";
+
+ try {
+ var SearchBar = require('icinga/icinga-php-library/widget/SearchBar');
+ var SearchEditor = require('icinga/icinga-php-library/widget/SearchEditor');
+ var FilterInput = require('icinga/icinga-php-library/widget/FilterInput');
+ var TermInput = require('icinga/icinga-php-library/widget/TermInput');
+ var Completer = require('icinga/icinga-php-library/widget/Completer');
+ } catch (e) {
+ console.warn('Unable to provide input enrichments. Libraries not available:', e);
+ return;
+ }
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ /**
+ * @param icinga
+ * @constructor
+ */
+ let InputEnrichment = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+
+ this.on('beforerender', '#main > .container, #modal-content', this.onBeforeRender, this);
+ this.on('rendered', '#main > .container, #modal-content', this.onRendered, this);
+
+ /**
+ * Enriched inputs
+ *
+ * @type {WeakMap<object, SearchEditor|SearchBar|FilterInput|TermInput|Completer>}
+ * @private
+ */
+ this._enrichments = new WeakMap();
+
+ /**
+ * Cached enrichments
+ *
+ * Holds values only during the time between `beforerender` and `rendered`
+ *
+ * @type {{}}
+ * @private
+ */
+ this._cachedEnrichments = {};
+ };
+ InputEnrichment.prototype = new Icinga.EventListener();
+
+ /**
+ * @param data
+ */
+ InputEnrichment.prototype.update = function (data) {
+ var input = document.querySelector(data[0]);
+ if (input !== null && this._enrichments.has(input)) {
+ this._enrichments.get(input).updateTerms(data[1]);
+ }
+ };
+
+ /**
+ * @param event
+ * @param content
+ * @param action
+ * @param autorefresh
+ * @param scripted
+ */
+ InputEnrichment.prototype.onBeforeRender = function (event, content, action, autorefresh, scripted) {
+ if (! autorefresh) {
+ return;
+ }
+
+ let _this = event.data.self;
+ let inputs = event.target.querySelectorAll('[data-enrichment-type]');
+
+ // Remember current instances
+ inputs.forEach((input) => {
+ let enrichment = _this._enrichments.get(input);
+ if (enrichment) {
+ _this._cachedEnrichments[_this.icinga.utils.getDomPath(input).join(' > ')] = enrichment;
+ }
+ });
+ };
+
+ /**
+ * @param event
+ * @param autorefresh
+ * @param scripted
+ */
+ InputEnrichment.prototype.onRendered = function (event, autorefresh, scripted) {
+ let _this = event.data.self;
+ let container = event.target;
+
+ if (autorefresh) {
+ // Apply remembered instances
+ for (let inputPath in _this._cachedEnrichments) {
+ let enrichment = _this._cachedEnrichments[inputPath];
+ let input = container.querySelector(inputPath);
+ if (input !== null) {
+ enrichment.refresh(input);
+ _this._enrichments.set(input, enrichment);
+ } else {
+ enrichment.destroy();
+ }
+
+ delete _this._cachedEnrichments[inputPath];
+ }
+ }
+
+ // Create new instances
+ let inputs = container.querySelectorAll('[data-enrichment-type]');
+ inputs.forEach((input) => {
+ let enrichment = _this._enrichments.get(input);
+ if (! enrichment) {
+ switch (input.dataset.enrichmentType) {
+ case 'search-bar':
+ enrichment = (new SearchBar(input)).bind();
+ break;
+ case 'search-editor':
+ enrichment = (new SearchEditor(input)).bind();
+ break;
+ case 'filter':
+ enrichment = (new FilterInput(input)).bind();
+ enrichment.restoreTerms();
+
+ if (_this._enrichments.has(input.form)) {
+ _this._enrichments.get(input.form).setFilterInput(enrichment);
+ }
+
+ break;
+ case 'terms':
+ enrichment = (new TermInput(input)).bind();
+ enrichment.restoreTerms();
+ break;
+ case 'completion':
+ enrichment = (new Completer(input)).bind();
+ }
+
+ _this._enrichments.set(input, enrichment);
+ }
+ });
+ };
+
+ Icinga.Behaviors.InputEnrichment = InputEnrichment;
+
+})(Icinga);
diff --git a/public/js/icinga/behavior/modal.js b/public/js/icinga/behavior/modal.js
new file mode 100644
index 0000000..4a13f31
--- /dev/null
+++ b/public/js/icinga/behavior/modal.js
@@ -0,0 +1,254 @@
+/*! Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+;(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ /**
+ * Behavior for modal dialogs.
+ *
+ * @param icinga {Icinga} The current Icinga Object
+ */
+ var Modal = function(icinga) {
+ Icinga.EventListener.call(this, icinga);
+
+ this.icinga = icinga;
+ this.$layout = $('#layout');
+ this.$ghost = $('#modal-ghost');
+
+ this.on('submit', '#modal form', this.onFormSubmit, this);
+ this.on('change', '#modal form select.autosubmit', this.onFormAutoSubmit, this);
+ this.on('change', '#modal form input.autosubmit', this.onFormAutoSubmit, this);
+ this.on('click', '[data-icinga-modal]', this.onModalToggleClick, this);
+ this.on('mousedown', '#layout > #modal', this.onModalLeave, this);
+ this.on('click', '.modal-header > button', this.onModalClose, this);
+ this.on('keydown', this.onKeyDown, this);
+ };
+
+ Modal.prototype = new Icinga.EventListener();
+
+ /**
+ * Event handler for toggling modals. Shows the link target in a modal dialog.
+ *
+ * @param event {Event} The `onClick` event triggered by the clicked modal-toggle element
+ * @returns {boolean}
+ */
+ Modal.prototype.onModalToggleClick = function(event) {
+ var _this = event.data.self;
+ var $a = $(event.currentTarget);
+ var url = $a.attr('href');
+ var $modal = _this.$ghost.clone();
+ var $redirectTarget = $a.closest('.container');
+
+ _this.modalOpener = event.currentTarget;
+
+ // Disable pointer events to block further function calls
+ _this.modalOpener.style.pointerEvents = 'none';
+
+ // Add showCompact, we don't want controls in a modal
+ url = _this.icinga.utils.addUrlFlag(url, 'showCompact');
+
+ // Set the toggle's container to use it as redirect target
+ $modal.data('redirectTarget', $redirectTarget);
+
+ // Final preparations, the id is required so that it's not `display:none` anymore
+ $modal.attr('id', 'modal');
+ _this.$layout.append($modal);
+
+ var req = _this.icinga.loader.loadUrl(url, $modal.find('#modal-content'));
+ req.addToHistory = false;
+ req.done(function () {
+ _this.setTitle($modal, req.$target.data('icingaTitle').replace(/\s::\s.*/, ''));
+ _this.show($modal);
+ _this.focus($modal);
+ });
+ req.fail(function (req, _, errorThrown) {
+ if (req.status >= 500) {
+ // Yes, that's done twice (by us and by the base fail handler),
+ // but `renderContentToContainer` does too many useful things..
+ _this.icinga.loader.renderContentToContainer(req.responseText, $redirectTarget, req.action);
+ } else if (req.status > 0) {
+ var msg = $(req.responseText).find('.error-message').text();
+ if (msg && msg !== errorThrown) {
+ errorThrown += ': ' + msg;
+ }
+
+ _this.icinga.loader.createNotice('error', errorThrown);
+ }
+
+ _this.hide($modal);
+ });
+
+ return false;
+ };
+
+ /**
+ * Event handler for form submits within a modal.
+ *
+ * @param event {Event} The `submit` event triggered by a form within the modal
+ * @param $autoSubmittedBy {jQuery} The element triggering the auto submit, if any
+ * @returns {boolean}
+ */
+ Modal.prototype.onFormSubmit = function(event) {
+ var _this = event.data.self;
+ var $form = $(event.currentTarget).closest('form');
+ var $modal = $form.closest('#modal');
+
+ var $button;
+ var $rememberedSubmittButton = $form.data('submitButton');
+ if (typeof $rememberedSubmittButton != 'undefined') {
+ if ($form.has($rememberedSubmittButton)) {
+ $button = $rememberedSubmittButton;
+ }
+ $form.removeData('submitButton');
+ }
+
+ let $autoSubmittedBy;
+ if (! $autoSubmittedBy && event.detail && event.detail.submittedBy) {
+ $autoSubmittedBy = $(event.detail.submittedBy);
+ }
+
+ // Prevent our other JS from running
+ $modal[0].dataset.noIcingaAjax = '';
+
+ var req = _this.icinga.loader.submitForm($form, $autoSubmittedBy, $button);
+ req.addToHistory = false;
+ req.done(function (data, textStatus, req) {
+ var title = req.getResponseHeader('X-Icinga-Title');
+ if (!! title) {
+ _this.setTitle($modal, decodeURIComponent(title).replace(/\s::\s.*/, ''));
+ }
+
+ if (req.getResponseHeader('X-Icinga-Redirect')) {
+ _this.hide($modal);
+ }
+ }).always(function () {
+ delete $modal[0].dataset.noIcingaAjax;
+ });
+
+ if (! ('baseTarget' in $form[0].dataset)) {
+ req.$redirectTarget = $modal.data('redirectTarget');
+ }
+
+ if (typeof $autoSubmittedBy === 'undefined') {
+ // otherwise the form is submitted several times by clicking the "Submit" button several times
+ $form.find('input[type=submit],button[type=submit],button:not([type])').prop('disabled', true);
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ };
+
+ /**
+ * Event handler for form auto submits within a modal.
+ *
+ * @param event {Event} The `change` event triggered by a form input within the modal
+ * @returns {boolean}
+ */
+ Modal.prototype.onFormAutoSubmit = function(event) {
+ let form = event.currentTarget.form;
+ let modal = form.closest('#modal');
+
+ // Prevent our other JS from running
+ modal.dataset.noIcingaAjax = '';
+
+ form.dispatchEvent(new CustomEvent('submit', {
+ cancelable: true,
+ bubbles: true,
+ detail: { submittedBy: event.currentTarget }
+ }));
+ };
+
+ /**
+ * Event handler for closing the modal. Closes it when the user clicks on the overlay.
+ *
+ * @param event {Event} The `click` event triggered by clicking on the overlay
+ */
+ Modal.prototype.onModalLeave = function(event) {
+ var _this = event.data.self;
+ var $target = $(event.target);
+
+ if ($target.is('#modal')) {
+ _this.hide($target);
+ }
+ };
+
+ /**
+ * Event handler for closing the modal. Closes it when the user clicks on the close button.
+ *
+ * @param event {Event} The `click` event triggered by clicking on the close button
+ */
+ Modal.prototype.onModalClose = function(event) {
+ var _this = event.data.self;
+
+ _this.hide($(event.currentTarget).closest('#modal'));
+ };
+
+ /**
+ * Event handler for closing the modal. Closes it when the user pushed ESC.
+ *
+ * @param event {Event} The `keydown` event triggered by pushing a key
+ */
+ Modal.prototype.onKeyDown = function(event) {
+ var _this = event.data.self;
+
+ if (! event.isDefaultPrevented() && event.key === 'Escape') {
+ let $modal = _this.$layout.children('#modal');
+ if ($modal.length) {
+ _this.hide($modal);
+ }
+ }
+ };
+
+ /**
+ * Make final preparations and add the modal to the DOM
+ *
+ * @param $modal {jQuery} The modal element
+ */
+ Modal.prototype.show = function($modal) {
+ $modal.addClass('active');
+ };
+
+ /**
+ * Set a title for the modal
+ *
+ * @param $modal {jQuery} The modal element
+ * @param title {string} The title
+ */
+ Modal.prototype.setTitle = function($modal, title) {
+ $modal.find('.modal-header > h1').html(title);
+ };
+
+ /**
+ * Focus the modal
+ *
+ * @param $modal {jQuery} The modal element
+ */
+ Modal.prototype.focus = function($modal) {
+ this.icinga.ui.focusElement($modal.find('.modal-window'));
+ };
+
+ /**
+ * Hide the modal and remove it from the DOM
+ *
+ * @param $modal {jQuery} The modal element
+ */
+ Modal.prototype.hide = function($modal) {
+ // Remove pointerEvent none style to make the button clickable again
+ this.modalOpener.style.pointerEvents = '';
+ this.modalOpener = null;
+
+ $modal.removeClass('active');
+ // Using `setTimeout` here to let the transition finish
+ setTimeout(function () {
+ $modal.find('#modal-content').trigger('close-modal');
+ $modal.remove();
+ }, 200);
+ };
+
+ Icinga.Behaviors.Modal = Modal;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/behavior/navigation.js b/public/js/icinga/behavior/navigation.js
new file mode 100644
index 0000000..df0e1e6
--- /dev/null
+++ b/public/js/icinga/behavior/navigation.js
@@ -0,0 +1,464 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+(function(Icinga, $) {
+
+ "use strict";
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ var Navigation = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.on('click', '#menu a', this.linkClicked, this);
+ this.on('click', '#menu tr[href]', this.linkClicked, this);
+ this.on('rendered', '#menu', this.onRendered, this);
+ this.on('mouseenter', '#menu .primary-nav .nav-level-1 > .nav-item', this.showFlyoutMenu, this);
+ this.on('mouseleave', '#menu .primary-nav', this.hideFlyoutMenu, this);
+ this.on('click', '#toggle-sidebar', this.toggleSidebar, this);
+
+ this.on('click', '#menu .config-nav-item button', this.toggleConfigFlyout, this);
+ this.on('mouseenter', '#menu .config-menu .config-nav-item', this.showConfigFlyout, this);
+ this.on('mouseleave', '#menu .config-menu .config-nav-item', this.hideConfigFlyout, this);
+
+ this.on('keydown', '#menu .config-menu .config-nav-item', this.onKeyDown, this);
+
+ /**
+ * The DOM-Path of the active item
+ *
+ * @see getDomPath
+ *
+ * @type {null|Array}
+ */
+ this.active = null;
+
+ /**
+ * The menu
+ *
+ * @type {jQuery}
+ */
+ this.$menu = null;
+
+ /**
+ * Local storage
+ *
+ * @type {Icinga.Storage}
+ */
+ this.storage = Icinga.Storage.BehaviorStorage('navigation');
+
+ this.storage.setBackend(window.sessionStorage);
+
+ // Restore collapsed sidebar if necessary
+ if (this.storage.get('sidebar-collapsed')) {
+ $('#layout').addClass('sidebar-collapsed');
+ }
+ };
+
+ Navigation.prototype = new Icinga.EventListener();
+
+ /**
+ * Activate menu items if their class is set to active or if the current URL matches their link
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.onRendered = function(e) {
+ var _this = e.data.self;
+
+ _this.$menu = $(e.target);
+
+ if (! _this.active) {
+ // There is no stored menu item, therefore it is assumed that this is the first rendering
+ // of the navigation after the page has been opened.
+
+ // initialise the menu selected by the backend as active.
+ var $active = _this.$menu.find('li.active');
+ if ($active.length) {
+ $active.each(function() {
+ _this.setActiveAndSelected($(this));
+ });
+ } else {
+ // if no item is marked as active, try to select the menu from the current URL
+ _this.setActiveAndSelectedByUrl($('#col1').data('icingaUrl'));
+ }
+ }
+
+ _this.refresh();
+ };
+
+ /**
+ * Re-render the menu selection according to the current state
+ */
+ Navigation.prototype.refresh = function() {
+ // restore selection to current active element
+ if (this.active) {
+ var $el = $(this.icinga.utils.getElementByDomPath(this.active));
+ this.setActiveAndSelected($el);
+
+ /*
+ * Recreate the html content of the menu item to force the browser to update the layout, or else
+ * the link would only be visible as active after another click or page reload in Gecko and WebKit.
+ *
+ * fixes #7897
+ */
+ if ($el.is('li')) {
+ $el.html($el.html());
+ }
+ }
+ };
+
+ /**
+ * Handle a link click in the menu
+ *
+ * @param event
+ */
+ Navigation.prototype.linkClicked = function(event) {
+ var $a = $(this);
+ var href = $a.attr('href');
+ var _this = event.data.self;
+ var icinga = _this.icinga;
+
+ // Check for ctrl or cmd click to open new tab and don't unfold other menus
+ if (event.ctrlKey || event.metaKey) {
+ return false;
+ }
+
+ if (href.match(/#/)) {
+ // ...it may be a menu section without a dedicated link.
+ // Switch the active menu item:
+ _this.setActiveAndSelected($a);
+ } else {
+ _this.setActiveAndSelected($(event.target));
+ }
+
+ // update target url of the menu container to the clicked link
+ var $menu = $('#menu');
+ var menuDataUrl = icinga.utils.parseUrl($menu.data('icinga-url'));
+ menuDataUrl = icinga.utils.addUrlParams(menuDataUrl.path, { url: href });
+ $menu.data('icinga-url', menuDataUrl);
+ };
+
+ /**
+ * Activate a menu item based on the current URL
+ *
+ * Activate a menu item that is an exact match or fall back to items that match the base URL
+ *
+ * @param url {String} The url to match
+ */
+ Navigation.prototype.setActiveAndSelectedByUrl = function(url) {
+ var $menu = $('#menu');
+
+ if (! $menu.length) {
+ return;
+ }
+
+ // try to active the first item that has an exact URL match
+ this.setActiveAndSelected($menu.find('[href="' + url + '"]'));
+
+ // the url may point to the search field, which must be activated too
+ if (! this.active) {
+ this.setActiveAndSelected($menu.find('form[action="' + this.icinga.utils.parseUrl(url).path + '"]'));
+ }
+
+ // some urls may have custom filters which won't match any menu item, in that case search
+ // for a menu item that points to the base action without any filters
+ if (! this.active) {
+ this.setActiveAndSelected($menu.find('[href="' + this.icinga.utils.parseUrl(url).path + '"]').first());
+ }
+ };
+
+ /**
+ * Try to select a new URL by
+ *
+ * @param url
+ */
+ Navigation.prototype.trySetActiveAndSelectedByUrl = function(url) {
+ var active = this.active;
+ this.setActiveAndSelectedByUrl(url);
+
+ if (! this.active && active) {
+ this.setActiveAndSelected($(this.icinga.utils.getElementByDomPath(active)));
+ }
+ };
+
+ /**
+ * Remove all active elements
+ */
+ Navigation.prototype.clear = function() {
+ if (this.$menu) {
+ this.$menu.find('.active').removeClass('active');
+ }
+ };
+
+ /**
+ * Remove all selected elements
+ */
+ Navigation.prototype.clearSelected = function() {
+ if (this.$menu) {
+ this.$menu.find('.selected').removeClass('selected');
+ }
+ };
+
+ /**
+ * Select all menu items in the selector as active and unfold surrounding menus when necessary
+ *
+ * @param $item {jQuery} The jQuery selector
+ */
+ Navigation.prototype.select = function($item) {
+ // support selecting the url of the menu entry
+ var $input = $item.find('input');
+ $item = $item.closest('li');
+
+ if ($item.length) {
+ // select the current item
+ var $selectedMenu = $item.addClass('active');
+
+ // unfold the containing menu
+ var $outerMenu = $selectedMenu.parent().closest('li');
+ if ($outerMenu.length) {
+ $outerMenu.addClass('active');
+ }
+ } else if ($input.length) {
+ $input.addClass('active');
+ }
+ };
+
+ Navigation.prototype.setActiveAndSelected = function ($el) {
+ if ($el.length > 1) {
+ $el.each((key, el) => {
+ if (! this.active) {
+ this.setActiveAndSelected($(el));
+ }
+ });
+ } else if ($el.length) {
+ let parent = $el[0].closest('.nav-level-1 > .nav-item, .config-menu');
+
+ if ($el[0].offsetHeight || $el[0].offsetWidth || parent.offsetHeight || parent.offsetWidth) {
+ // It's either a visible menu item or a config menu item
+ this.setActive($el);
+ this.setSelected($el);
+ }
+ }
+ };
+
+ /**
+ * Change the active menu element
+ *
+ * @param $el {jQuery} A selector pointing to the active element
+ */
+ Navigation.prototype.setActive = function($el) {
+ this.clear();
+ this.select($el);
+ if ($el.closest('li')[0]) {
+ this.active = this.icinga.utils.getDomPath($el.closest('li')[0]);
+ } else if ($el.find('input')[0]) {
+ this.active = this.icinga.utils.getDomPath($el[0]);
+ } else {
+ this.active = null;
+ }
+ // TODO: push to history
+ };
+
+ Navigation.prototype.setSelected = function($el) {
+ this.clearSelected();
+ $el = $el.closest('li');
+
+ if ($el.length) {
+ $el.addClass('selected');
+ }
+ };
+
+ /**
+ * Reset the active element to nothing
+ */
+ Navigation.prototype.resetActive = function() {
+ this.clear();
+ this.active = null;
+ };
+
+ /**
+ * Reset the selected element to nothing
+ */
+ Navigation.prototype.resetSelected = function() {
+ this.clearSelected();
+ this.selected = null;
+ };
+
+ /**
+ * Show the fly-out menu
+ *
+ * @param e
+ */
+ Navigation.prototype.showFlyoutMenu = function(e) {
+ var $layout = $('#layout');
+
+ if ($layout.hasClass('minimal-layout')) {
+ return;
+ }
+
+ var $target = $(this);
+ var $flyout = $target.find('.nav-level-2');
+
+ if (! $flyout.length) {
+ $layout.removeClass('menu-hovered');
+ $target.siblings().not($target).removeClass('hover');
+ return;
+ }
+
+ var delay = 300;
+
+ if ($layout.hasClass('menu-hovered')) {
+ delay = 0;
+ }
+
+ setTimeout(function() {
+ try {
+ if (! $target.is(':hover')) {
+ return;
+ }
+ } catch(e) { /* Bypass because if IE8 */ }
+
+ $layout.addClass('menu-hovered');
+ $target.siblings().not($target).removeClass('hover');
+ $target.addClass('hover');
+
+ $flyout.css({
+ bottom: 'auto',
+ top: $target.offset().top + $target.outerHeight()
+ });
+
+ var rect = $flyout[0].getBoundingClientRect();
+
+ if (rect.y + rect.height > window.innerHeight) {
+ $flyout.css({
+ bottom: 0,
+ top: 'auto'
+ });
+ }
+ }, delay);
+ };
+
+ /**
+ * Hide the fly-out menu
+ *
+ * @param e
+ */
+ Navigation.prototype.hideFlyoutMenu = function(e) {
+ var $layout = $('#layout');
+ var $nav = $(e.currentTarget);
+ var $hovered = $nav.find('.nav-level-1 > .nav-item.hover');
+
+ if (! $hovered.length) {
+ $layout.removeClass('menu-hovered');
+
+ return;
+ }
+
+ setTimeout(function() {
+ try {
+ if ($hovered.is(':hover') || $nav.is(':hover')) {
+ return;
+ }
+ } catch(e) { /* Bypass because if IE8 */ };
+ $hovered.removeClass('hover');
+ $layout.removeClass('menu-hovered');
+ }, 600);
+ };
+
+ /**
+ * Collapse or expand sidebar
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.toggleSidebar = function(e) {
+ var _this = e.data.self;
+ var $layout = $('#layout');
+ $layout.toggleClass('sidebar-collapsed');
+ _this.storage.set('sidebar-collapsed', $layout.is('.sidebar-collapsed'));
+ $(window).trigger('resize');
+ };
+
+ /**
+ * Toggle config flyout visibility
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.toggleConfigFlyout = function(e) {
+ var _this = e.data.self;
+ if ($('#layout').is('.config-flyout-open')) {
+ _this.hideConfigFlyout(e);
+ } else {
+ _this.showConfigFlyout(e);
+ }
+ }
+
+ /**
+ * Hide config flyout
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.hideConfigFlyout = function(e) {
+ $('#layout').removeClass('config-flyout-open');
+ if (e.target) {
+ delete $(e.target).closest('.container')[0].dataset.suspendAutorefresh;
+ }
+ }
+
+ /**
+ * Show config flyout
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.showConfigFlyout = function(e) {
+ $('#layout').addClass('config-flyout-open');
+ $(e.target).closest('.container')[0].dataset.suspendAutorefresh = '';
+ }
+
+ /**
+ * Hide, config flyout when "Enter" key is pressed to follow `.flyout` nav item link
+ *
+ * @param {Object} e Event
+ */
+ Navigation.prototype.onKeyDown = function(e) {
+ var _this = e.data.self;
+
+ if (e.key == 'Enter' && $(document.activeElement).is('.flyout a')) {
+ _this.hideConfigFlyout(e);
+ }
+ }
+
+ /**
+ * Called when the history changes
+ *
+ * @param url The url of the new state
+ * @param data The active menu item of the new state
+ */
+ Navigation.prototype.onPopState = function (url, data) {
+ // 1. get selection data and set active menu
+ if (data) {
+ var active = this.icinga.utils.getElementByDomPath(data);
+ if (!active) {
+ this.logger.fail(
+ 'Could not restore active menu from history, path in DOM not found.',
+ data,
+ url
+ );
+ return;
+ }
+ this.setActiveAndSelected($(active))
+ } else {
+ this.resetActive();
+ this.resetSelected();
+ }
+ };
+
+ /**
+ * Called when the current state gets pushed onto the history, can return a value
+ * to be preserved as the current state
+ *
+ * @returns {null|Array} The currently active menu item
+ */
+ Navigation.prototype.onPushState = function () {
+ return this.active;
+ };
+
+ Icinga.Behaviors.Navigation = Navigation;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/behavior/selectable.js b/public/js/icinga/behavior/selectable.js
new file mode 100644
index 0000000..3f32840
--- /dev/null
+++ b/public/js/icinga/behavior/selectable.js
@@ -0,0 +1,49 @@
+/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+;(function(Icinga, $) {
+ 'use strict';
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ /**
+ * Select all contents from the target of the given event
+ *
+ * @param {object} e Event
+ */
+ function onSelect(e) {
+ var b = document.body,
+ r;
+ if (b.createTextRange) {
+ r = b.createTextRange();
+ r.moveToElementText(e.target);
+ r.select();
+ } else if (window.getSelection) {
+ var s = window.getSelection();
+ r = document.createRange();
+ r.selectNodeContents(e.target);
+ s.removeAllRanges();
+ s.addRange(r);
+ }
+ }
+
+ /**
+ * Behavior for text that is selectable via double click
+ *
+ * @param {Icinga} icinga
+ *
+ * @constructor
+ */
+ var Selectable = function(icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.on('rendered', this.onRendered, this);
+ };
+
+ $.extend(Selectable.prototype, new Icinga.EventListener(), {
+ onRendered: function(e) {
+ $(e.target).find('.selectable').on('dblclick', onSelect);
+ }
+ });
+
+ Icinga.Behaviors.Selectable = Selectable;
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/eventlistener.js b/public/js/icinga/eventlistener.js
new file mode 100644
index 0000000..678e775
--- /dev/null
+++ b/public/js/icinga/eventlistener.js
@@ -0,0 +1,78 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * EventListener contains event handlers and can bind / and unbind them from
+ * event emitting objects
+ */
+(function(Icinga, $) {
+
+ "use strict";
+
+ var EventListener = function (icinga) {
+ this.icinga = icinga;
+ this.handlers = [];
+ };
+
+ /**
+ * Add an handler to this EventLister
+ *
+ * @param evt {String} The name of the triggering event
+ * @param cond {String} The filter condition
+ * @param fn {Function} The event handler to execute
+ * @param scope {Object} The optional 'this' of the called function
+ */
+ EventListener.prototype.on = function(evt, cond, fn, scope) {
+ if (typeof cond === 'function') {
+ scope = fn;
+ fn = cond;
+ cond = 'body';
+ }
+ this.icinga.logger.debug('on: ' + evt + '(' + cond + ')');
+ this.handlers.push({ evt: evt, cond: cond, fn: fn, scope: scope });
+ };
+
+ /**
+ * Bind all listeners to the given event emitter
+ *
+ * All event handlers will be executed when the associated event is
+ * triggered on the given Emitter.
+ *
+ * @param emitter {String} An event emitter that supports the function
+ * 'on' to register listeners
+ */
+ EventListener.prototype.bind = function (emitter) {
+ var _this = this;
+
+ if (typeof emitter.jquery === 'undefined') {
+ emitter = $(emitter);
+ }
+
+ $.each(this.handlers, function(i, handler) {
+ _this.icinga.logger.debug('bind: ' + handler.evt + '(' + handler.cond + ')');
+ emitter.on(
+ handler.evt, handler.cond,
+ {
+ self: handler.scope || emitter,
+ icinga: _this.icinga
+ }, handler.fn
+ );
+ });
+ };
+
+ /**
+ * Unbind all listeners from the given event emitter
+ *
+ * @param emitter {String} An event emitter that supports the function
+ * 'off' to un-register listeners.
+ */
+ EventListener.prototype.unbind = function (emitter) {
+ var _this = this;
+ $.each(this.handlers, function(i, handler) {
+ _this.icinga.logger.debug('unbind: ' + handler.evt + '(' + handler.cond + ')');
+ emitter.off(handler.evt, handler.cond, handler.fn);
+ });
+ };
+
+ Icinga.EventListener = EventListener;
+
+}) (Icinga, jQuery);
diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js
new file mode 100644
index 0000000..1878d8f
--- /dev/null
+++ b/public/js/icinga/events.js
@@ -0,0 +1,425 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Events
+ *
+ * Event handlers
+ */
+(function (Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Events = function (icinga) {
+ this.icinga = icinga;
+
+ this.searchValue = '';
+ this.searchTimer = null;
+ };
+
+ Icinga.Events.prototype = {
+
+ /**
+ * Icinga will call our initialize() function once it's ready
+ */
+ initialize: function () {
+ this.applyGlobalDefaults();
+ },
+
+ /**
+ * Global default event handlers
+ */
+ applyGlobalDefaults: function () {
+ $(document).on('visibilitychange', { self: this }, this.onVisibilityChange);
+
+ $.each(this.icinga.behaviors, function (name, behavior) {
+ behavior.bind($(document));
+ });
+
+ // Initialize module javascript (Applies only to module.js code)
+ this.icinga.ensureSubModules(document);
+
+ // We catch resize events
+ $(window).on('resize', { self: this.icinga.ui }, this.icinga.ui.onWindowResize);
+
+ // Trigger 'rendered' event also on page loads
+ $(document).on('icinga-init', { self: this }, this.onInit);
+
+ // Destroy Icinga, clean up and interrupt pending requests on unload
+ $( window ).on('unload', { self: this }, this.onUnload);
+ $( window ).on('beforeunload', { self: this }, this.onUnload);
+
+ // Remove notifications on click
+ $(document).on('click', '#notifications li', function () { $(this).remove(); });
+
+ // We want to catch each link click
+ $(document).on('click', 'a', { self: this }, this.linkClicked);
+ $(document).on('click', 'tr[href]', { self: this }, this.linkClicked);
+
+ $(document).on('click', 'input[type="submit"], button[type="submit"]', this.rememberSubmitButton);
+ // We catch all form submit events
+ $(document).on('submit', 'form', { self: this }, this.submitForm);
+
+ // We support an 'autosubmit' class on dropdown form elements
+ $(document).on('change', 'form select.autosubmit', { self: this }, this.autoSubmitForm);
+ $(document).on('change', 'form input.autosubmit', { self: this }, this.autoSubmitForm);
+
+ // Automatically check a radio button once a specific input is focused
+ $(document).on('focus', 'form select[data-related-radiobtn]', { self: this }, this.autoCheckRadioButton);
+ $(document).on('focus', 'form input[data-related-radiobtn]', { self: this }, this.autoCheckRadioButton);
+
+ $(document).on('rendered', '#menu', { self: this }, this.onRenderedMenu);
+ $(document).on('keyup', '#search', { self: this }, this.autoSubmitSearch);
+
+ $(document).on('click', '.tree .handle', { self: this }, this.treeNodeToggle);
+
+ $(document).on('click', '#search + .search-reset', this.clearSearch);
+
+ $(document).on('rendered', '.container', { self: this }, this.loadDashlets);
+
+ // TBD: a global autocompletion handler
+ // $(document).on('keyup', 'form.auto input', this.formChangeDelayed);
+ // $(document).on('change', 'form.auto input', this.formChanged);
+ // $(document).on('change', 'form.auto select', this.submitForm);
+ },
+
+ treeNodeToggle: function () {
+ var $parent = $(this).closest('li');
+ if ($parent.hasClass('collapsed')) {
+ $('li', $parent).addClass('collapsed');
+ $parent.removeClass('collapsed');
+ } else {
+ $parent.addClass('collapsed');
+ }
+ },
+
+ onInit: function (event) {
+ $('body').removeClass('loading');
+
+ // Trigger the initial `rendered` events
+ $('.container').trigger('rendered');
+
+ // Additionally trigger a `rendered` event on the layout, some behaviors may
+ // want to differentiate whether a container or the entire layout is rendered
+ $('#layout').trigger('rendered');
+ },
+
+ onUnload: function (event) {
+ var icinga = event.data.self.icinga;
+ icinga.logger.info('Unloading Icinga');
+ icinga.destroy();
+ },
+
+ onVisibilityChange: function (event) {
+ var icinga = event.data.self.icinga;
+
+ if (document.visibilityState === undefined || document.visibilityState === 'visible') {
+ icinga.loader.autorefreshSuspended = false;
+ icinga.logger.debug('Page visible, enabling auto-refresh');
+ } else {
+ icinga.loader.autorefreshSuspended = true;
+ icinga.logger.debug('Page invisible, disabling auto-refresh');
+ }
+ },
+
+ autoCheckRadioButton: function (event) {
+ var $input = $(event.currentTarget);
+ var $radio = $('#' + $input.attr('data-related-radiobtn'));
+ if ($radio.length) {
+ $radio.prop('checked', true);
+ }
+ return true;
+ },
+
+ onRenderedMenu: function(event) {
+ var _this = event.data.self;
+ var $target = $(event.target);
+
+ var $searchField = $target.find('input.search');
+ // Remember initial search field value if any
+ if ($searchField.length && $searchField.val().length) {
+ _this.searchValue = $searchField.val();
+ }
+ },
+
+ autoSubmitSearch: function(event) {
+ var _this = event.data.self;
+ var $searchField = $(event.target);
+
+ if ($searchField.val() === _this.searchValue) {
+ return;
+ }
+ _this.searchValue = $searchField.val();
+
+ if (_this.searchTimer !== null) {
+ clearTimeout(_this.searchTimer);
+ _this.searchTimer = null;
+ }
+ var _event = $.extend({}, event); // event seems gc'd once the timeout is over
+ _this.searchTimer = setTimeout(function () {
+ _this.submitForm(_event, $searchField);
+ _this.searchTimer = null;
+ }, 500);
+ },
+
+ loadDashlets: function(event) {
+ var _this = event.data.self;
+ var $target = $(event.target);
+
+ if ($target.children('.dashboard').length) {
+ $target.find('.dashboard > .container').each(function () {
+ var $dashlet = $(this);
+ var url = $dashlet.data('icingaUrl');
+ if (typeof url !== 'undefined') {
+ _this.icinga.loader.loadUrl(url, $dashlet).autorefresh = true;
+ }
+ });
+ }
+ },
+
+ rememberSubmitButton: function(e) {
+ var $button = $(this);
+ var $form = $button.closest('form');
+ $form.data('submitButton', $button);
+ },
+
+ autoSubmitForm: function (event) {
+ let form = event.currentTarget.form;
+
+ if (form.closest('[data-no-icinga-ajax]')) {
+ return;
+ }
+
+ form.dispatchEvent(new CustomEvent('submit', {
+ cancelable: true,
+ bubbles: true,
+ detail: { submittedBy: event.currentTarget }
+ }));
+ },
+
+ /**
+ *
+ */
+ submitForm: function (event, $autoSubmittedBy) {
+ var _this = event.data.self;
+
+ // .closest is not required unless subelements to trigger this
+ var $form = $(event.currentTarget).closest('form');
+
+ if ($form.closest('[data-no-icinga-ajax]').length > 0) {
+ return true;
+ }
+
+ var $button;
+ var $rememberedSubmittButton = $form.data('submitButton');
+ if (typeof $rememberedSubmittButton != 'undefined') {
+ if ($form.has($rememberedSubmittButton)) {
+ $button = $rememberedSubmittButton;
+ }
+ $form.removeData('submitButton');
+ }
+
+ if (typeof $button === 'undefined') {
+ var $el;
+
+ if (typeof event.originalEvent !== 'undefined'
+ && typeof event.originalEvent.explicitOriginalTarget === 'object') { // Firefox
+ $el = $(event.originalEvent.explicitOriginalTarget);
+ _this.icinga.logger.debug('events/submitForm: Button is event.originalEvent.explicitOriginalTarget');
+ } else {
+ $el = $(event.currentTarget);
+ _this.icinga.logger.debug('events/submitForm: Button is event.currentTarget');
+ }
+
+ if ($el && ($el.is('input[type=submit]') || $el.is('button[type=submit]'))) {
+ $button = $el;
+ } else {
+ _this.icinga.logger.debug(
+ 'events/submitForm: Can not determine submit button, using the first one in form'
+ );
+ }
+ }
+
+ if (! $autoSubmittedBy && event.detail && event.detail.submittedBy) {
+ $autoSubmittedBy = $(event.detail.submittedBy);
+ }
+
+ _this.icinga.loader.submitForm($form, $autoSubmittedBy, $button);
+
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ },
+
+ handleExternalTarget: function($node) {
+ var linkTarget = $node.attr('target');
+
+ // TODO: Let remote links pass through. Right now they only work
+ // combined with target="_blank" or target="_self"
+ // window.open is used as return true; didn't work reliable
+ if (linkTarget === '_blank' || linkTarget === '_self') {
+ window.open($node.attr('href'), linkTarget);
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Someone clicked a link or tr[href]
+ */
+ linkClicked: function (event) {
+ var _this = event.data.self;
+ var icinga = _this.icinga;
+ var $a = $(this);
+ var $eventTarget = $(event.target);
+ var href = $a.attr('href');
+ var linkTarget = $a.attr('target');
+ var $target;
+ var formerUrl;
+
+ if (! href) {
+ return;
+ }
+
+ if (href.match(/^(?:(?:mailto|javascript|data):|[a-z]+:\/\/)/)) {
+ event.stopPropagation();
+ return true;
+ }
+
+ if ($a.closest('[data-no-icinga-ajax]').length > 0) {
+ return true;
+ }
+
+ // Check for ctrl or cmd click to open new tab unless clicking on a multiselect row
+ if ((event.ctrlKey || event.metaKey) && href !== '#' && $a.is('a')) {
+ window.open(href, linkTarget);
+ return false;
+ }
+
+ // Special checks for link clicks in action tables
+ if (! $a.is('tr[href]') && $a.closest('table.action').length > 0) {
+
+ // ignore clicks to ANY link with special key pressed
+ if ($a.closest('table.multiselect').length > 0 && (event.ctrlKey || event.metaKey || event.shiftKey)) {
+ return true;
+ }
+
+ // ignore inner links matching the row URL
+ if ($a.attr('href') === $a.closest('tr[href]').attr('href')) {
+ return true;
+ }
+ }
+
+ // window.open is used as return true; didn't work reliable
+ if (linkTarget === '_blank' || linkTarget === '_self') {
+ window.open(href, linkTarget);
+ return false;
+ }
+
+ if (! $eventTarget.is($a)) {
+ if ($eventTarget.is('input') || $eventTarget.is('button')) {
+ // Ignore form elements in action rows
+ return;
+ } else {
+ var $button = $('input[type=submit]:focus').add('button[type=submit]:focus');
+ if ($button.length > 0 && $.contains($button[0], $eventTarget[0])) {
+ // Ignore any descendant of form elements
+ return;
+ }
+ }
+ }
+
+ // ignore multiselect table row clicks
+ if ($a.is('tr') && $a.closest('table.multiselect').length > 0) {
+ return;
+ }
+
+ // Handle all other links as XHR requests
+ event.stopPropagation();
+ event.preventDefault();
+
+ // This is an anchor only
+ if (href.substr(0, 1) === '#' && href.length > 1
+ && href.substr(1, 1) !== '!'
+ ) {
+ icinga.ui.focusElement(href.substr(1), $a.closest('.container'));
+ return;
+ }
+
+ // activate spinner indicator
+ if ($a.hasClass('spinner')) {
+ $a.addClass('active');
+ }
+
+ // If link has hash tag...
+ if (href.match(/#/)) {
+ if (href === '#') {
+ if ($a.hasClass('close-container-control')) {
+ if (! icinga.ui.isOneColLayout()) {
+ var $cont = $a.closest('.container').first();
+ if ($cont.attr('id') === 'col1') {
+ icinga.ui.moveToLeft();
+ icinga.ui.layout1col();
+ } else {
+ icinga.ui.layout1col();
+ }
+ icinga.history.pushCurrentState();
+ }
+ }
+ return false;
+ }
+ $target = icinga.loader.getLinkTargetFor($a);
+
+ formerUrl = $target.data('icingaUrl');
+ if (typeof formerUrl !== 'undefined' && formerUrl.split(/#/)[0] === href.split(/#/)[0]) {
+ icinga.ui.focusElement(href.split(/#/)[1], $target);
+ $target.data('icingaUrl', href);
+ if (formerUrl !== href) {
+ icinga.history.pushCurrentState();
+ }
+ return false;
+ }
+ } else {
+ $target = icinga.loader.getLinkTargetFor($a);
+ }
+
+ // Load link URL
+ icinga.loader.loadUrl(href, $target);
+
+ if ($a.closest('#menu').length > 0) {
+ // Menu links should remove all but the first layout column
+ icinga.ui.layout1col();
+ }
+
+ return false;
+ },
+
+ clearSearch: function (event) {
+ $(event.target).parent().find('#search').attr('value', '');
+ },
+
+ unbindGlobalHandlers: function () {
+ $.each(this.icinga.behaviors, function (name, behavior) {
+ behavior.unbind($(document));
+ });
+ $(window).off('resize', this.onWindowResize);
+ $(window).off('load', this.onLoad);
+ $(window).off('unload', this.onUnload);
+ $(window).off('beforeunload', this.onUnload);
+ $(document).off('scroll', '.container', this.onContainerScroll);
+ $(document).off('click', 'a', this.linkClicked);
+ $(document).off('submit', 'form', this.submitForm);
+ $(document).off('change', 'form select.autosubmit', this.submitForm);
+ $(document).off('change', 'form input.autosubmit', this.submitForm);
+ $(document).off('focus', 'form select[data-related-radiobtn]', this.autoCheckRadioButton);
+ $(document).off('focus', 'form input[data-related-radiobtn]', this.autoCheckRadioButton);
+ $(document).off('visibilitychange', this.onVisibilityChange);
+ },
+
+ destroy: function() {
+ // This is gonna be hard, clean up the mess
+ this.unbindGlobalHandlers();
+ this.icinga = null;
+ }
+ };
+
+}(Icinga, jQuery));
diff --git a/public/js/icinga/history.js b/public/js/icinga/history.js
new file mode 100644
index 0000000..150be7c
--- /dev/null
+++ b/public/js/icinga/history.js
@@ -0,0 +1,338 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.History
+ *
+ * This is where we care about the browser History API
+ */
+(function (Icinga, $) {
+
+ 'use strict';
+
+ Icinga.History = function (icinga) {
+
+ /**
+ * YES, we need Icinga
+ */
+ this.icinga = icinga;
+
+ /**
+ * Our base url
+ */
+ this.baseUrl = icinga.config.baseUrl;
+
+ /**
+ * Initial URL at load time
+ */
+ this.initialUrl = location.href;
+
+ /**
+ * Whether the History API is enabled
+ */
+ this.enabled = false;
+ };
+
+ Icinga.History.prototype = {
+
+ /**
+ * Icinga will call our initialize() function once it's ready
+ */
+ initialize: function () {
+
+ // History API will not be enabled without browser support, no fallback
+ if ('undefined' !== typeof window.history &&
+ typeof window.history.pushState === 'function'
+ ) {
+ this.enabled = true;
+ this.icinga.logger.debug('History API enabled');
+ this.applyLocationBar(true);
+ $(window).on('popstate', { self: this }, this.onHistoryChange);
+ }
+
+ },
+
+ /**
+ * Get the current state (url and title) as object
+ *
+ * @returns {object}
+ */
+ getCurrentState: function () {
+ if (! this.enabled) {
+ return null;
+ }
+
+ var title = null;
+ var url = null;
+
+ // We only store URLs of containers sitting directly under #main:
+ $('#main > .container').each(function (idx, container) {
+ var $container = $(container),
+ cUrl = $container.data('icingaUrl'),
+ cTitle = $container.data('icingaTitle');
+
+ // TODO: I'd prefer to have the rightmost URL first
+ if ('undefined' !== typeof cUrl) {
+ // TODO: solve this on server side cUrl = icinga.utils.removeUrlParams(cUrl, blacklist);
+ if (! url) {
+ url = cUrl;
+ } else {
+ url = url + '#!' + cUrl;
+ }
+ }
+
+ if (typeof cTitle !== 'undefined') {
+ title = cTitle; // Only uses the rightmost title
+ }
+ });
+
+ return {
+ title: title,
+ url: url,
+ };
+ },
+
+ /**
+ * Detect active URLs and push combined URL to history
+ *
+ * TODO: How should we handle POST requests? e.g. search VS login
+ */
+ pushCurrentState: function () {
+ // No history API, no action
+ if (! this.enabled) {
+ return;
+ }
+
+ var state = this.getCurrentState();
+
+ // Did we find any URL? Then push it!
+ if (state.url) {
+ this.icinga.logger.debug('Pushing current state to history');
+ this.push(state.url);
+ }
+ if (state.title) {
+ this.icinga.ui.setTitle(state.title);
+ }
+ },
+
+ /**
+ * Replace the current history entry with the current state
+ */
+ replaceCurrentState: function () {
+ if (! this.enabled) {
+ return;
+ }
+
+ var state = this.getCurrentState();
+
+ if (state.url) {
+ this.icinga.logger.debug('Replacing current history state');
+ this.lastPushUrl = state.url;
+ window.history.replaceState(
+ this.getBehaviorState(),
+ null,
+ state.url
+ );
+ }
+ },
+
+ /**
+ * Push the given url as the new history state, unless the history is disabled
+ *
+ * @param {string} url The full url path, including anchor
+ */
+ pushUrl: function (url) {
+ // No history API, no action
+ if (!this.enabled) {
+ return;
+ }
+ this.push(url);
+ },
+
+ /**
+ * Execute the history state, preserving the current state of behaviors
+ *
+ * Used internally by the history and should not be called externally, instead use {@link pushUrl}.
+ *
+ * @param {string} url
+ */
+ push: function (url) {
+ url = url.replace(/[\?&]?_(render|reload)=[a-z0-9]+/g, '');
+ if (this.lastPushUrl === url) {
+ this.icinga.logger.debug(
+ 'Ignoring history state push for url ' + url + ' as it\' currently on top of the stack'
+ );
+ return;
+ }
+ this.lastPushUrl = url;
+ window.history.pushState(
+ this.getBehaviorState(),
+ null,
+ url
+ );
+ },
+
+ /**
+ * Fetch the current state of all JS behaviors that need history support
+ *
+ * @return {Object} A key-value map, mapping behavior names to state
+ */
+ getBehaviorState: function () {
+ var data = {};
+ $.each(this.icinga.behaviors, function (i, behavior) {
+ if (behavior.onPushState instanceof Function) {
+ data[i] = behavior.onPushState();
+ }
+ });
+ return data;
+ },
+
+ /**
+ * Event handler for pop events
+ *
+ * TODO: Fix active selection, multiple cols
+ */
+ onHistoryChange: function (event) {
+
+ var _this = event.data.self,
+ icinga = _this.icinga;
+
+ icinga.logger.debug('Got a history change');
+
+ // We might find browsers showing strange behaviour, this log could help
+ if (event.originalEvent.state === null) {
+ icinga.logger.debug('No more history steps available');
+ } else {
+ icinga.logger.debug('History state', event.originalEvent.state);
+ }
+
+ // keep the last pushed url in sync with history changes
+ _this.lastPushUrl = location.href;
+
+ _this.applyLocationBar();
+
+ // notify behaviors of the state change
+ $.each(this.icinga.behaviors, function (i, behavior) {
+ if (behavior.onPopState instanceof Function && history.state) {
+ behavior.onPopState(location.href, history.state[i]);
+ }
+ });
+ },
+
+ /**
+ * Update the application containers to match the current url
+ *
+ * Read the pane url from the current URL and load the corresponding panes into containers to
+ * match the current history state.
+ *
+ * @param {Boolean} onload Set to true when the main pane should not be updated, defaults to false
+ */
+ applyLocationBar: function (onload = false) {
+ let col2State = this.getCol2State();
+
+ if (onload && document.querySelector('#layout > #login')) {
+ // The user landed on the login
+ let redirectInput = document.querySelector('#login form input[name=redirect]');
+ redirectInput.value = redirectInput.value + col2State;
+ return;
+ }
+
+ let col1 = document.getElementById('col1'),
+ col2 = document.getElementById('col2'),
+ col1Url = document.location.pathname + document.location.search;
+
+ let col2Url;
+ if (col2State && col2State.match(/^#!/)) {
+ col2Url = col2State.split(/#!/)[1];
+ }
+
+ // This uses jQuery only because of its internal data attribute cache -.-
+ let currentCol1Url = $(col1).data('icingaUrl'),
+ currentCol2Url = $(col2).data('icingaUrl');
+
+ let loadCol1 = ! onload,
+ loadCol2 = !! col2Url;
+ if (currentCol2Url === col1Url) {
+ // User navigated forward
+ this.icinga.ui.moveToLeft();
+ loadCol1 = false;
+ } else if (currentCol1Url === col2Url) {
+ // User navigated back
+ this.icinga.ui.moveToRight();
+ loadCol2 = false;
+ }
+
+ if (loadCol1 && currentCol1Url !== col1Url) {
+ let anchor = this.getPaneAnchor(0);
+ if (anchor) {
+ col1Url += '#' + anchor;
+ }
+
+ this.icinga.loader.loadUrl(col1Url, $(col1)).addToHistory = false;
+ }
+
+ if (loadCol2 && currentCol2Url !== col2Url) {
+ let col2Req = this.icinga.loader.loadUrl(col2Url, $(col2));
+ col2Req.addToHistory = false;
+ col2Req.scripted = onload;
+
+ this.icinga.ui.layout2col();
+ } else if (! loadCol2 && ! col2Url) {
+ this.icinga.ui.layout1col();
+ }
+ },
+
+ /**
+ * Get the state of the selected pane
+ *
+ * @param col {int} The column index 0 or 1
+ *
+ * @returns {String} The string representing the state
+ */
+ getPaneAnchor: function (col) {
+ if (col !== 1 && col !== 0) {
+ throw 'Trying to get anchor for non-existing column: ' + col;
+ }
+ var panes = document.location.toString().split('#!')[col];
+ return panes && panes.split('#')[1] || '';
+ },
+
+ /**
+ * Get the side pane state after (and including) the #!
+ *
+ * @returns {string} The pane url
+ */
+ getCol2State: function () {
+ var hash = document.location.hash;
+ if (hash) {
+ if (hash.match(/^#[^!]/)) {
+ var hashs = hash.split('#');
+ hashs.shift();
+ hashs.shift();
+ hash = '#' + hashs.join('#');
+ }
+ }
+ return hash || '';
+ },
+
+ /**
+ * Return the main pane state fragment
+ *
+ * @returns {string} The main url including anchors, without #!
+ */
+ getCol1State: function () {
+ var anchor = this.getPaneAnchor(0);
+ var hash = window.location.pathname + window.location.search +
+ (anchor.length ? ('#' + anchor) : '');
+ return hash || '';
+ },
+
+ /**
+ * Cleanup
+ */
+ destroy: function () {
+ $(window).off('popstate', this.onHistoryChange);
+ this.icinga = null;
+ }
+ };
+
+}(Icinga, jQuery));
diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js
new file mode 100644
index 0000000..97891a7
--- /dev/null
+++ b/public/js/icinga/loader.js
@@ -0,0 +1,1367 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Loader
+ *
+ * This is where we take care of XHR requests, responses and failures.
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Loader = function (icinga) {
+
+ /**
+ * YES, we need Icinga
+ */
+ this.icinga = icinga;
+
+ /**
+ * Our base url
+ */
+ this.baseUrl = icinga.config.baseUrl;
+
+ this.failureNotice = null;
+
+ /**
+ * Pending requests
+ */
+ this.requests = {};
+
+ this.iconCache = {};
+
+ /**
+ * Whether auto-refresh is enabled
+ */
+ this.autorefreshEnabled = true;
+
+ /**
+ * Whether auto-refresh is suspended due to visibility of page
+ */
+ this.autorefreshSuspended = false;
+ };
+
+ Icinga.Loader.prototype = {
+
+ initialize: function () {
+ this.icinga.timer.register(this.autorefresh, this, 500);
+ },
+
+ submitForm: function ($form, $autoSubmittedBy, $button) {
+ var icinga = this.icinga;
+ var url = $form.attr('action');
+ var method = $form.attr('method');
+ var encoding = $form.attr('enctype');
+ var progressTimer;
+ var $target;
+ var data;
+
+ if (typeof method === 'undefined') {
+ method = 'POST';
+ } else {
+ method = method.toUpperCase();
+ }
+
+ if (typeof encoding === 'undefined') {
+ encoding = 'application/x-www-form-urlencoded';
+ }
+
+ if (typeof $autoSubmittedBy === 'undefined') {
+ $autoSubmittedBy = false;
+ }
+
+ if (typeof $button === 'undefined') {
+ $button = $('input[type=submit]:focus', $form).add('button[type=submit]:focus', $form);
+ }
+
+ if ($button.length === 0) {
+ $button = $('input[type=submit]', $form).add('button[type=submit]', $form).first();
+ }
+
+ if ($button.length) {
+ // Activate spinner
+ if ($button.hasClass('spinner')) {
+ $button.addClass('active');
+ }
+
+ $target = this.getLinkTargetFor($button);
+ } else {
+ $target = this.getLinkTargetFor($form);
+ }
+
+ // Overwrite the URL only if the form is not auto submitted
+ if (! $autoSubmittedBy && $button.hasAttr('formaction')) {
+ url = $button.attr('formaction');
+ }
+
+ if (! url) {
+ // Use the URL of the target container if the form's action is not set
+ url = $target.closest('.container').data('icinga-url');
+ }
+
+ icinga.logger.debug('Submitting form: ' + method + ' ' + url, method);
+
+ if (method === 'GET') {
+ var dataObj = $form.serializeObject();
+
+ if (! $autoSubmittedBy) {
+ if ($button.length && $button.attr('name') !== 'undefined') {
+ dataObj[$button.attr('name')] = $button.attr('value');
+ }
+ }
+
+ url = icinga.utils.addUrlParams(url, dataObj);
+ } else {
+ if (encoding === 'multipart/form-data') {
+ data = new window.FormData($form[0]);
+ } else {
+ data = $form.serializeArray();
+ }
+
+ if (! $autoSubmittedBy) {
+ if ($button.length && $button.attr('name') !== 'undefined') {
+ if (encoding === 'multipart/form-data') {
+ data.append($button.attr('name'), $button.attr('value'));
+ } else {
+ data.push({
+ name: $button.attr('name'),
+ value: $button.attr('value')
+ });
+ }
+ }
+ }
+ }
+
+ // Disable all form controls to prevent resubmission except for our search input
+ // Note that disabled form inputs will not be enabled via JavaScript again
+ if (! $autoSubmittedBy
+ && ! $form.is('[role="search"]')
+ && $target.attr('id') === $form.closest('.container').attr('id')
+ ) {
+ $form.find('input[type=submit],button[type=submit],button:not([type])').prop('disabled', true);
+ }
+
+ // Show a spinner depending on how the form is being submitted
+ if ($autoSubmittedBy && $autoSubmittedBy.siblings('.spinner').length) {
+ $autoSubmittedBy.siblings('.spinner').first().addClass('active');
+ } else if ($button.length && $button.is('button') && $button.hasClass('animated')) {
+ $button.addClass('active');
+ } else if ($button.length && $button.attr('data-progress-label')) {
+ var isInput = $button.is('input');
+ if (isInput) {
+ $button.prop('value', $button.attr('data-progress-label') + '...');
+ } else {
+ $button.html($button.attr('data-progress-label') + '...');
+ }
+
+ // Use a fixed width to prevent the button from wobbling
+ $button.css('width', $button.css('width'));
+
+ progressTimer = icinga.timer.register(function () {
+ var label = isInput ? $button.prop('value') : $button.html();
+ var dots = label.substr(-3);
+
+ // Using empty spaces here to prevent centered labels from wobbling
+ if (dots === '...') {
+ label = label.slice(0, -2) + ' ';
+ } else if (dots === '.. ') {
+ label = label.slice(0, -1) + '.';
+ } else if (dots === '. ') {
+ label = label.slice(0, -2) + '. ';
+ }
+
+ if (isInput) {
+ $button.prop('value', label);
+ } else {
+ $button.html(label);
+ }
+ }, null, 100);
+ } else if ($button.length && $button.next().hasClass('spinner')) {
+ $('i', $button.next()).addClass('active');
+ } else if ($form.attr('data-progress-element')) {
+ var $progressElement = $('#' + $form.attr('data-progress-element'));
+ if ($progressElement.length) {
+ if ($progressElement.hasClass('spinner')) {
+ $('i', $progressElement).addClass('active');
+ } else {
+ $('i.spinner', $progressElement).addClass('active');
+ }
+ }
+ }
+
+ let extraHeaders = {};
+ if ($autoSubmittedBy) {
+ let id;
+ if (($autoSubmittedBy.attr('name') || $autoSubmittedBy.attr('id'))) {
+ id = $autoSubmittedBy.attr('name') || $autoSubmittedBy.attr('id');
+ } else {
+ let formSelector = icinga.utils.getCSSPath($form);
+ let nearestKnownParent = $autoSubmittedBy.closest(
+ formSelector + ' [name],' + formSelector + ' [id]'
+ );
+ if (nearestKnownParent) {
+ id = nearestKnownParent.attr('name') || nearestKnownParent.attr('id');
+ }
+ }
+
+ if (id) {
+ extraHeaders['X-Icinga-AutoSubmittedBy'] = id;
+ }
+ }
+
+ var req = this.loadUrl(url, $target, data, method, undefined, undefined, undefined, extraHeaders);
+ req.forceFocus = $autoSubmittedBy ? $autoSubmittedBy : $button.length ? $button : null;
+ req.autosubmit = !! $autoSubmittedBy;
+ req.addToHistory = method === 'GET';
+ req.progressTimer = progressTimer;
+
+ if ($autoSubmittedBy) {
+ if ($autoSubmittedBy.closest('.controls').length) {
+ $('.content', req.$target).addClass('impact');
+ } else {
+ req.$target.addClass('impact');
+ }
+ }
+
+ return req;
+ },
+
+ /**
+ * Load the given URL to the given target
+ *
+ * @param {string} url URL to be loaded
+ * @param {object} target Target jQuery element
+ * @param {object} data Optional parameters, usually for POST requests
+ * @param {string} method HTTP method, default is 'GET'
+ * @param {string} action How to handle the response ('replace' or 'append'), default is 'replace'
+ * @param {boolean} autorefresh Whether the cause is a autorefresh or not
+ * @param {object} progressTimer A timer to be stopped when the request is done
+ * @param {object} extraHeaders Extra header entries
+ */
+ loadUrl: function (url, $target, data, method, action, autorefresh, progressTimer, extraHeaders) {
+ var id = null;
+
+ // Default method is GET
+ if ('undefined' === typeof method) {
+ method = 'GET';
+ }
+ if ('undefined' === typeof action) {
+ action = 'replace';
+ }
+ if ('undefined' === typeof autorefresh) {
+ autorefresh = false;
+ }
+
+ this.icinga.logger.debug('Loading ', url, ' to ', $target);
+
+ // We should do better and ignore requests without target and/or id
+ if (typeof $target !== 'undefined' && $target.attr('id')) {
+ id = $target.attr('id');
+ }
+
+ // If we have a pending request for the same target...
+ if (typeof this.requests[id] !== 'undefined') {
+ // ... ignore the new request if it is already pending with the same URL. Only abort GETs, as those
+ // are the only methods that are guaranteed to return the same value
+ if (this.requests[id].url === url && method === 'GET') {
+ if (autorefresh) {
+ return false;
+ }
+
+ this.icinga.logger.debug('Request to ', url, ' is already running for ', $target);
+ return this.requests[id];
+ }
+
+ // ...or abort the former request otherwise
+ this.icinga.logger.debug(
+ 'Aborting pending request loading ',
+ url,
+ ' to ',
+ $target
+ );
+
+ this.requests[id].abort();
+ }
+
+ // Not sure whether we need this Accept-header
+ var headers = { 'X-Icinga-Accept': 'text/html' };
+
+ if (!! id) {
+ headers['X-Icinga-Container'] = id;
+ }
+
+ if (autorefresh) {
+ headers['X-Icinga-Autorefresh'] = '1';
+ }
+
+ if ($target.is('#col2')) {
+ headers['X-Icinga-Col1-State'] = this.icinga.history.getCol1State();
+ headers['X-Icinga-Col2-State'] = this.icinga.history.getCol2State().replace(/^#!/, '');
+ }
+
+ // Ask for a new window id in case we don't already have one
+ if (this.icinga.ui.hasWindowId()) {
+ var windowId = this.icinga.ui.getWindowId();
+ var containerId = this.icinga.ui.getUniqueContainerId($target);
+ if (containerId) {
+ windowId = windowId + '_' + containerId;
+ }
+ headers['X-Icinga-WindowId'] = windowId;
+ } else {
+ headers['X-Icinga-WindowId'] = 'undefined';
+ }
+
+ if (typeof extraHeaders !== 'undefined') {
+ headers = $.extend(headers, extraHeaders);
+ }
+
+ // This is jQuery's default content type
+ var contentType = 'application/x-www-form-urlencoded; charset=UTF-8';
+
+ var isFormData = typeof window.FormData !== 'undefined' && data instanceof window.FormData;
+ if (isFormData) {
+ // Setting false is mandatory as the form's data
+ // won't be recognized by the server otherwise
+ contentType = false;
+ }
+
+ var _this = this;
+ var req = $.ajax({
+ type : method,
+ url : url,
+ data : data,
+ headers: headers,
+ context: _this,
+ contentType: contentType,
+ processData: ! isFormData
+ });
+
+ req.$target = $target;
+ req.$redirectTarget = $target;
+ req.url = url;
+ req.done(this.onResponse);
+ req.fail(this.onFailure);
+ req.always(this.onComplete);
+ req.autorefresh = autorefresh;
+ req.autosubmit = false;
+ req.scripted = false;
+ req.method = method;
+ req.action = action;
+ req.addToHistory = true;
+ req.progressTimer = progressTimer;
+
+ if (url.match(/#/)) {
+ req.forceFocus = url.split(/#/)[1];
+ }
+
+ if (id) {
+ this.requests[id] = req;
+ }
+ if (! autorefresh) {
+ setTimeout(function () {
+ // The column may have not been shown before. To make the transition
+ // delay working we have to wait for the column getting rendered
+ if (! req.autosubmit && req.state() === 'pending') {
+ req.$target.addClass('impact');
+ }
+ }, 0);
+ }
+ this.icinga.ui.refreshDebug();
+ return req;
+ },
+
+ /**
+ * Create an URL relative to the Icinga base Url, still unused
+ *
+ * @param {string} url Relative url
+ */
+ url: function (url) {
+ if (typeof url === 'undefined') {
+ return this.baseUrl;
+ }
+ return this.baseUrl + url;
+ },
+
+ stopPendingRequestsFor: function ($el) {
+ var id;
+ if (typeof $el === 'undefined' || ! (id = $el.attr('id'))) {
+ return;
+ }
+
+ if (typeof this.requests[id] !== 'undefined') {
+ this.requests[id].abort();
+ }
+ },
+
+ filterAutorefreshingContainers: function () {
+ return $(this).data('icingaRefresh') > 0 && ! $(this).is('[data-suspend-autorefresh]');
+ },
+
+ autorefresh: function () {
+ var _this = this;
+
+ $('.container').filter(this.filterAutorefreshingContainers).each(function (idx, el) {
+ var $el = $(el);
+ var id = $el.attr('id');
+
+ // Always request application-state
+ if (id !== 'application-state' && (! _this.autorefreshEnabled || _this.autorefreshSuspended)) {
+ // Continue
+ return true;
+ }
+
+ if (typeof _this.requests[id] !== 'undefined') {
+ _this.icinga.logger.debug('No refresh, request pending for ', id);
+ return;
+ }
+
+ var interval = $el.data('icingaRefresh');
+ var lastUpdate = $el.data('lastUpdate');
+
+ if (typeof interval === 'undefined' || ! interval) {
+ _this.icinga.logger.info('No interval, setting default', id);
+ interval = 10;
+ }
+
+ if (typeof lastUpdate === 'undefined' || ! lastUpdate) {
+ _this.icinga.logger.info('No lastUpdate, setting one', id);
+ $el.data('lastUpdate',(new Date()).getTime());
+ return;
+ }
+ interval = interval * 1000;
+
+ // TODO:
+ if ((lastUpdate + interval) > (new Date()).getTime()) {
+ // self.icinga.logger.info(
+ // 'Skipping refresh',
+ // id,
+ // lastUpdate,
+ // interval,
+ // (new Date()).getTime()
+ // );
+ return;
+ }
+
+ if (_this.loadUrl($el.data('icingaUrl'), $el, undefined, undefined, undefined, true) === false) {
+ _this.icinga.logger.debug(
+ 'NOT autorefreshing ' + id + ', even if ' + interval + ' ms passed. Request pending?'
+ );
+ } else {
+ _this.icinga.logger.debug(
+ 'Autorefreshing ' + id + ' ' + interval + ' ms passed'
+ );
+ }
+ el = null;
+ });
+ },
+
+ /**
+ * Disable the autorefresh mechanism
+ */
+ disableAutorefresh: function () {
+ this.autorefreshEnabled = false;
+ },
+
+ /**
+ * Enable the autorefresh mechanism
+ */
+ enableAutorefresh: function () {
+ this.autorefreshEnabled = true;
+ },
+
+ processNotificationHeader: function(req) {
+ var header = req.getResponseHeader('X-Icinga-Notification');
+ var _this = this;
+ if (! header) return false;
+ var list = header.split('&');
+ $.each(list, function(idx, el) {
+ var parts = decodeURIComponent(el).split(' ');
+ _this.createNotice(parts.shift(), parts.join(' '));
+ });
+ return true;
+ },
+
+ /**
+ * Process the X-Icinga-Redirect HTTP Response Header
+ *
+ * If the response includes the X-Icinga-Redirect header, redirects to the URL associated with the header.
+ *
+ * @param {object} req Current request
+ *
+ * @returns {boolean} Whether we're about to redirect
+ */
+ processRedirectHeader: function(req) {
+ var icinga = this.icinga,
+ $redirectTarget = req.$redirectTarget,
+ redirect = req.getResponseHeader('X-Icinga-Redirect');
+
+ if (! redirect) {
+ return false;
+ }
+
+ redirect = decodeURIComponent(redirect);
+ if (redirect.match(/__SELF__/)) {
+ if (req.autorefresh) {
+ // Redirect to the current window's URL in case it's an auto-refresh request. If authenticated
+ // externally this ensures seamless re-login if the session's expired
+ redirect = redirect.replace(
+ /__SELF__/,
+ encodeURIComponent(
+ document.location.pathname + document.location.search + document.location.hash
+ )
+ );
+ } else {
+ // Redirect to the URL which required authentication. When clicking a link this ensures that we
+ // redirect to the link's URL instead of the current window's URL (see above)
+ redirect = redirect.replace(/__SELF__/, req.url);
+ }
+ } else if (redirect.match(/__BACK__/)) {
+ if (req.$redirectTarget.is('#col1')) {
+ icinga.logger.warn('Cannot navigate back. Redirect target is #col1');
+ return false;
+ }
+
+ var originUrl = req.$target.data('icingaUrl');
+
+ $(window).on('popstate.__back__', { self: this }, function (event) {
+ const _this = event.data.self;
+ let $refreshTarget = $('#col2');
+ let refreshUrl;
+
+ const hash = icinga.history.getCol2State();
+ if (hash && hash.match(/^#!/)) {
+ // TODO: These three lines are copied from history.js, I don't like this
+ var parts = hash.split(/#!/);
+
+ if (parts[1] === originUrl) {
+ // After a page load a call to back() seems not to have an effect
+ icinga.ui.layout1col();
+ } else {
+ refreshUrl = parts[1];
+ }
+ }
+
+ if (typeof refreshUrl === 'undefined' && icinga.ui.isOneColLayout()) {
+ refreshUrl = icinga.history.getCol1State();
+ $refreshTarget = $('#col1');
+ }
+
+ const refreshReq = _this.loadUrl(refreshUrl, $refreshTarget);
+ refreshReq.autoRefreshInterval = req.getResponseHeader('X-Icinga-Refresh');
+ refreshReq.autorefresh = true;
+ refreshReq.scripted = true;
+
+ $(window).off('popstate.__back__');
+ });
+
+ // Navigate back, no redirect desired
+ window.history.back();
+
+ return true;
+ } else if (redirect.match(/__CLOSE__/)) {
+ if (req.$target.is('#col1') && req.$redirectTarget.is('#col1')) {
+ icinga.logger.warn('Cannot close #col1');
+ return false;
+ }
+
+ if (req.$redirectTarget.is('.container') && ! req.$redirectTarget.is('#main > :scope')) {
+ // If it is a container that is not a top level container, we just empty it
+ req.$redirectTarget.empty();
+ return true;
+ }
+
+ if (! req.$redirectTarget.is('#col2')) {
+ icinga.logger.debug('Cannot close container', req.$redirectTarget);
+ return false;
+ }
+
+ // Close right column as requested
+ icinga.ui.layout1col();
+
+ if (!! req.getResponseHeader('X-Icinga-Extra-Updates')) {
+ icinga.logger.debug('Not refreshing #col1 due to outstanding extra updates');
+ return true;
+ }
+
+ $redirectTarget = $('#col1');
+ redirect = icinga.history.getCol1State();
+ } else if (redirect.match(/__REFRESH__/)) {
+ if (req.$redirectTarget.is('#col1')) {
+ redirect = icinga.history.getCol1State();
+ } else if (req.$redirectTarget.is('#col2')) {
+ redirect = icinga.history.getCol2State().replace(/^#!/, '');
+ } else {
+ icinga.logger.error('Unable to refresh. Not a primary column: ', req.$redirectTarget);
+ return false;
+ }
+ }
+
+ var useHttp = req.getResponseHeader('X-Icinga-Redirect-Http');
+ if (useHttp === 'yes') {
+ window.location.replace(redirect);
+ return true;
+ }
+
+ this.redirectToUrl(redirect, $redirectTarget, req);
+ return true;
+ },
+
+ /**
+ * Redirect to the given url
+ *
+ * @param {string} url
+ * @param {object} $target
+ * @param {XMLHttpRequest} referrer
+ */
+ redirectToUrl: function (url, $target, referrer) {
+ var icinga = this.icinga,
+ rerenderLayout,
+ autoRefreshInterval,
+ forceFocus,
+ origin;
+
+ if (typeof referrer !== 'undefined') {
+ rerenderLayout = referrer.getResponseHeader('X-Icinga-Rerender-Layout');
+ autoRefreshInterval = referrer.getResponseHeader('X-Icinga-Refresh');
+ forceFocus = referrer.forceFocus;
+ origin = referrer.url;
+ }
+
+ icinga.logger.debug(
+ 'Got redirect for ', $target, ', URL was ' + url
+ );
+
+ if (rerenderLayout) {
+ var parts = url.split(/#!/);
+ url = parts.shift();
+ var redirectionUrl = icinga.utils.addUrlFlag(url, 'renderLayout');
+ var r = this.loadUrl(redirectionUrl, $('#layout'));
+ r.historyUrl = url;
+ r.referrer = referrer;
+ if (parts.length) {
+ r.loadNext = parts;
+ } else if (!! document.location.hash) {
+ // Retain detail URL if the layout is rerendered
+ parts = document.location.hash.split('#!').splice(1);
+ if (parts.length) {
+ r.loadNext = $.grep(parts, function (url) {
+ if (url !== origin) {
+ icinga.logger.debug('Retaining detail url ' + url);
+ return true;
+ }
+
+ icinga.logger.debug('Discarding detail url ' + url + ' as it\'s the origin of the redirect');
+ return false;
+ });
+ }
+ }
+ } else {
+ if (url.match(/#!/)) {
+ var parts = url.split(/#!/);
+ icinga.ui.layout2col();
+ this.loadUrl(parts.shift(), $('#col1'));
+ this.loadUrl(parts.shift(), $('#col2'));
+ } else {
+ var req = this.loadUrl(url, $target);
+ req.forceFocus = url === origin ? forceFocus : null;
+ req.autoRefreshInterval = autoRefreshInterval;
+ req.referrer = referrer;
+ }
+ }
+ },
+
+ /**
+ * Handle successful XHR response
+ */
+ onResponse: function (data, textStatus, req) {
+ var _this = this;
+ if (this.failureNotice !== null) {
+ if (! this.failureNotice.hasClass('fading-out')) {
+ this.failureNotice.remove();
+ }
+ this.failureNotice = null;
+ }
+
+ var target = req.getResponseHeader('X-Icinga-Container');
+ var newBody = false;
+ var oldNotifications = false;
+ var isRedirect = !! req.getResponseHeader('X-Icinga-Redirect');
+ if (target) {
+ if (target === 'ignore') {
+ return;
+ }
+
+ var $newTarget = this.identifyLinkTarget(target, req.$target);
+ if ($newTarget.length) {
+ if (isRedirect) {
+ req.$redirectTarget = $newTarget;
+ } else {
+ // If we change the target, oncomplete will fail to clean up.
+ // This fixes the problem, not using req.$target would be better
+ delete this.requests[req.$target.attr('id')];
+
+ req.$target = $newTarget;
+ }
+
+ if (target === 'layout') {
+ oldNotifications = $('#notifications li').detach();
+ this.icinga.ui.layout1col();
+ newBody = true;
+ } else if ($newTarget.attr('id') === 'col2') {
+ if (_this.icinga.ui.isOneColLayout()) {
+ _this.icinga.ui.layout2col();
+ } else if (target === '_next') {
+ _this.icinga.ui.moveToLeft();
+ }
+ }
+ }
+ }
+
+ if (req.autorefresh && req.$target.is('[data-suspend-autorefresh]')) {
+ return;
+ }
+
+ this.icinga.logger.debug(
+ 'Got response for ', req.$target, ', URL was ' + req.url
+ );
+ this.processNotificationHeader(req);
+
+ var cssreload = req.getResponseHeader('X-Icinga-Reload-Css');
+ if (cssreload) {
+ this.icinga.ui.reloadCss();
+ }
+
+ if (isRedirect) {
+ return;
+ }
+
+ if (req.getResponseHeader('X-Icinga-Announcements') === 'refresh') {
+ var announceReq = _this.loadUrl(_this.url('/layout/announcements'), $('#announcements'));
+ announceReq.addToHistory = false;
+ announceReq.scripted = true;
+ }
+
+ var classes;
+
+ if (target !== 'layout') {
+ var moduleName = req.getResponseHeader('X-Icinga-Module');
+ classes = $.grep(req.$target.classes(), function (el) {
+ if (el === 'icinga-module' || el.match(/^module\-/)) {
+ return false;
+ }
+ return true;
+ });
+ if (moduleName) {
+ // Lazy load module javascript (Applies only to module.js code)
+ _this.icinga.ensureModule(moduleName);
+
+ req.$target.data('icingaModule', moduleName);
+ classes.push('icinga-module');
+ classes.push('module-' + moduleName);
+ } else {
+ req.$target.removeData('icingaModule');
+ if (req.$target.attr('data-icinga-module')) {
+ req.$target.removeAttr('data-icinga-module');
+ }
+ }
+ req.$target.attr('class', classes.join(' '));
+
+ var refresh = req.autoRefreshInterval || req.getResponseHeader('X-Icinga-Refresh');
+ if (refresh) {
+ req.$target.data('icingaRefresh', refresh);
+ } else {
+ req.$target.removeData('icingaRefresh');
+ if (req.$target.attr('data-icinga-refresh')) {
+ req.$target.removeAttr('data-icinga-refresh');
+ }
+ }
+ }
+
+ var title = req.getResponseHeader('X-Icinga-Title');
+ if (title && (target === 'layout' || req.$target.is('#layout'))) {
+ this.icinga.ui.setTitle(decodeURIComponent(title));
+ } else if (title && ! req.autorefresh && req.$target.closest('.dashboard').length === 0) {
+ req.$target.data('icingaTitle', decodeURIComponent(title));
+ }
+
+ // Set a window identifier if the server asks us to do so
+ var windowId = req.getResponseHeader('X-Icinga-WindowId');
+ if (windowId) {
+ this.icinga.ui.setWindowId(windowId);
+ }
+
+ var autoSubmit = false;
+ var currentUrl = this.icinga.utils.parseUrl(req.$target.data('icingaUrl'));
+ if (req.method === 'POST') {
+ var newUrl = this.icinga.utils.parseUrl(req.url);
+ if (newUrl.path === currentUrl.path && this.icinga.utils.arraysEqual(newUrl.params, currentUrl.params)) {
+ autoSubmit = true;
+ }
+ }
+
+ req.$target.data('icingaUrl', req.url);
+
+ if (typeof req.progressTimer !== 'undefined') {
+ this.icinga.timer.unregister(req.progressTimer);
+ }
+
+ var contentSeparator = req.getResponseHeader('X-Icinga-Multipart-Content');
+ if (!! contentSeparator) {
+ var locationQuery = req.getResponseHeader('X-Icinga-Location-Query');
+ if (locationQuery !== null) {
+ let url = currentUrl.path + (locationQuery ? '?' + locationQuery : '');
+ if (req.autosubmit || autoSubmit) {
+ // Also update a form's action if it doesn't differ from the container's url
+ var $form = $(req.forceFocus).closest('form');
+ var formAction = $form.attr('action');
+ if (!! formAction) {
+ formAction = this.icinga.utils.parseUrl(formAction);
+ if (formAction.path === currentUrl.path
+ && this.icinga.utils.arraysEqual(formAction.params, currentUrl.params)
+ ) {
+ $form.attr('action', url);
+ }
+ }
+ }
+
+ req.$target.data('icingaUrl', url);
+ this.icinga.history.replaceCurrentState();
+ }
+
+ $.each(req.responseText.split(contentSeparator), function (idx, el) {
+ var match = el.match(/for=(Behavior:)?(\S+)\s+([^]*)/m);
+ if (!! match) {
+ if (match[1]) {
+ var behavior = _this.icinga.behaviors[match[2].toLowerCase()];
+ if (typeof behavior !== 'undefined' && typeof behavior.update === 'function') {
+ behavior.update(JSON.parse(match[3]));
+ } else {
+ _this.icinga.logger.warn(
+ 'Invalid behavior. Cannot update behavior "' + match[2] + '"');
+ }
+ } else {
+ var $target = $('#' + match[2]);
+ if ($target.length) {
+ var forceFocus;
+ if (req.forceFocus
+ && typeof req.forceFocus.jquery !== 'undefined'
+ && $.contains($target[0], req.forceFocus[0])
+ ) {
+ forceFocus = req.forceFocus;
+ }
+
+ _this.renderContentToContainer(
+ match[3],
+ $target,
+ 'replace',
+ req.autorefresh,
+ forceFocus,
+ req.autosubmit || autoSubmit,
+ req.scripted
+ );
+ } else {
+ _this.icinga.logger.warn(
+ 'Invalid target ID. Cannot render multipart to #' + match[2]);
+ }
+ }
+ } else {
+ _this.icinga.logger.error('Ill-formed multipart', el);
+ }
+ })
+ } else {
+ this.renderContentToContainer(
+ req.responseText,
+ req.$target,
+ req.action,
+ req.autorefresh,
+ req.forceFocus,
+ req.autosubmit || autoSubmit,
+ req.scripted
+ );
+ }
+
+ if (oldNotifications) {
+ oldNotifications.appendTo($('#notifications'));
+ }
+ if (newBody) {
+ this.icinga.ui.fixDebugVisibility().triggerWindowResize();
+ }
+ },
+
+ /**
+ * Regardless of whether a request succeeded of failed, clean up
+ */
+ onComplete: function (dataOrReq, textStatus, reqOrError) {
+ var _this = this;
+ var req;
+
+ if (typeof dataOrReq === 'object') {
+ req = dataOrReq;
+ } else {
+ req = reqOrError;
+ }
+
+ if (req.getResponseHeader('X-Icinga-Reload-Window') === 'yes') {
+ window.location.reload();
+ return;
+ }
+
+ req.$target.data('lastUpdate', (new Date()).getTime());
+ delete this.requests[req.$target.attr('id')];
+ this.icinga.ui.fadeNotificationsAway();
+
+ var extraUpdates = req.getResponseHeader('X-Icinga-Extra-Updates');
+ if (!! extraUpdates && req.getResponseHeader('X-Icinga-Redirect-Http') !== 'yes') {
+ $.each(extraUpdates.split(','), function (idx, el) {
+ var parts = el.trim().split(';');
+ var $target;
+ var url;
+ if (parts.length === 2) {
+ $target = $(parts[0].startsWith('#') ? parts[0] : '#' + parts[0]);
+ if (! $target.length) {
+ _this.icinga.logger.warn('Invalid target ID. Cannot load extra URL', el);
+ return;
+ }
+
+ url = parts[1];
+ } else if (parts.length === 1) {
+ $target = $(parts[0]).closest(".container").not(req.$target);
+ if (! $target.length) {
+ _this.icinga.logger.warn('Invalid target ID. Cannot load extra URL', el);
+ return;
+ }
+
+ url = $target.data('icingaUrl');
+ if (! url) {
+ _this.icinga.logger.debug(
+ 'Superfluous extra update. The target\'s container has no url', el);
+ return;
+ }
+ } else {
+ _this.icinga.logger.error('Invalid extra update', el);
+ return;
+ }
+
+ if (url === '__CLOSE__') {
+ if ($target.is('#col2')) {
+ _this.icinga.ui.layout1col();
+ } else if ($target.is('#main > :scope')) {
+ _this.icinga.logger.warn('Invalid target ID. Cannot close ', $target);
+ } else if ($target.is('.container')) {
+ // If it is a container that is not a top level container, we just empty it
+ $target.empty();
+ }
+ } else {
+ _this.loadUrl(url, $target).addToHistory = false;
+ }
+ });
+ }
+
+ if ((textStatus === 'abort' && typeof req.referrer !== 'undefined') || this.processRedirectHeader(req)) {
+ return;
+ }
+
+ // Remove 'impact' class if there was such
+ if (req.$target.hasClass('impact')) {
+ req.$target.removeClass('impact');
+ } else {
+ var $impact = req.$target.find('.impact').first();
+ if ($impact.length) {
+ $impact.removeClass('impact');
+ }
+ }
+
+ if (! req.autorefresh && ! req.autosubmit) {
+ // TODO: Hook for response/url?
+ var url = req.url;
+
+ if (req.$target[0].id === 'col1') {
+ this.icinga.behaviors.navigation.trySetActiveAndSelectedByUrl(url);
+ }
+
+ var $forms = $('[action="' + this.icinga.utils.parseUrl(url).path + '"]');
+ var $matches = $.merge($('[href="' + url + '"]'), $forms);
+ $matches.each(function (idx, el) {
+ var $el = $(el);
+ if ($el.closest('#menu').length) {
+ if ($el.is('form')) {
+ $('input', $el).addClass('active');
+ }
+ // Interrupt .each, only one menu item shall be active
+ return false;
+ }
+ });
+ }
+
+ // Update history when necessary
+ if (! req.autorefresh && req.addToHistory) {
+ if (req.$target.hasClass('container')) {
+ // We only want to care about top-level containers
+ if (req.$target.parent().closest('.container').length === 0) {
+ this.icinga.history.pushCurrentState();
+ }
+ } else {
+ // Request wasn't for a container, so it's usually the body
+ // or the full layout. Push request URL to history:
+ var url = typeof req.historyUrl !== 'undefined' ? req.historyUrl : req.url;
+ this.icinga.history.pushUrl(url);
+ }
+ }
+
+ if (typeof req.loadNext !== 'undefined' && req.loadNext.length) {
+ if ($('#col2').length) {
+ var r = this.loadUrl(req.loadNext[0], $('#col2'));
+ r.addToHistory = req.addToHistory;
+ this.icinga.ui.layout2col();
+ } else {
+ this.icinga.logger.error('Failed to load URL for #col2', req.loadNext);
+ }
+ }
+
+ // Lazy load module javascript (Applies only to module.js code)
+ this.icinga.ensureSubModules(req.$target);
+
+ req.$target.find('.container').each(function () {
+ $(this).trigger('rendered', [req.autorefresh, req.scripted, req.autosubmit]);
+ });
+ req.$target.trigger('rendered', [req.autorefresh, req.scripted, req.autosubmit]);
+
+ this.icinga.ui.refreshDebug();
+ },
+
+ /**
+ * Handle failed XHR response
+ */
+ onFailure: function (req, textStatus, errorThrown) {
+ var url = req.url;
+
+ /*
+ * Test if a manual actions comes in and autorefresh is active: Stop refreshing
+ */
+ if (req.addToHistory && ! req.autorefresh) {
+ req.$target.data('icingaRefresh', 0);
+ req.$target.data('icingaUrl', url);
+ }
+
+ if (typeof req.progressTimer !== 'undefined') {
+ this.icinga.timer.unregister(req.progressTimer);
+ }
+
+ if (req.status > 0 && req.status < 501) {
+ this.icinga.logger.error(
+ req.status,
+ errorThrown + ':',
+ $(req.responseText).text().replace(/\s+/g, ' ').slice(0, 100)
+ );
+ this.renderContentToContainer(
+ req.responseText,
+ req.$target,
+ req.action,
+ req.autorefresh,
+ undefined,
+ req.autosubmit,
+ req.scripted
+ );
+ } else {
+ if (errorThrown === 'abort') {
+ this.icinga.logger.debug(
+ 'Request to ' + url + ' has been aborted for ',
+ req.$target
+ );
+
+ if (req.scripted) {
+ req.addToHistory = false;
+ }
+ } else {
+ if (this.failureNotice === null) {
+ var now = new Date();
+ var padString = this.icinga.utils.padString;
+ this.failureNotice = this.createNotice(
+ 'error',
+ 'The connection to the Icinga web server was lost at '
+ + now.getFullYear()
+ + '-' + padString(now.getMonth() + 1, 0, 2)
+ + '-' + padString(now.getDate(), 0, 2)
+ + ' ' + padString(now.getHours(), 0, 2)
+ + ':' + padString(now.getMinutes(), 0, 2)
+ + '.',
+ true
+ );
+ }
+
+ this.icinga.logger.error(
+ 'Failed to contact web server loading ',
+ url,
+ ' for ',
+ req.$target
+ );
+ }
+ }
+ },
+
+ /**
+ * Create a notification. Can be improved.
+ */
+ createNotice: function (severity, message, persist) {
+ var c = severity,
+ icon;
+ if (persist) {
+ c += ' persist';
+ }
+
+ switch (severity) {
+ case 'success':
+ icon = 'check-circle';
+ break;
+ case 'error':
+ icon = 'times';
+ break;
+ case 'warning':
+ icon = 'exclamation-triangle';
+ break;
+ case 'info':
+ icon = 'info-circle';
+ break;
+ }
+
+ var $notice = $(
+ '<li class="' + c + '">' +
+ '<i class="icon fa fa-' + icon + '"></i>' +
+ this.icinga.utils.escape(message) + '</li>'
+ ).appendTo($('#notifications'));
+
+ if (!persist) {
+ this.icinga.ui.fadeNotificationsAway();
+ }
+
+ return $notice;
+ },
+
+ /**
+ * Detect the link/form target for a given element (link, form, whatever)
+ *
+ * @param {object} $el jQuery set with the element
+ * @param {boolean} prepare Pass `false` to disable column preparation
+ */
+ getLinkTargetFor: function($el, prepare)
+ {
+ if (typeof prepare === 'undefined') {
+ prepare = true;
+ }
+
+ // If everything else fails, our target is the first column...
+ var $target = $('#col1');
+
+ // ...but usually we will use our own container...
+ var $container = $el.closest('.container');
+ if ($container.length) {
+ $target = $container;
+ }
+
+ // You can of course override the default behaviour:
+ if ($el.closest('[data-base-target]').length) {
+ var targetId = $el.closest('[data-base-target]').data('baseTarget');
+
+ $target = this.identifyLinkTarget(targetId, $el);
+ if (! $target.length) {
+ this.icinga.logger.warn('Link target "#' + targetId + '" does not exist in DOM.');
+ }
+ }
+
+ if (prepare) {
+ this.icinga.ui.prepareColumnFor($el, $target);
+ }
+
+ return $target;
+ },
+
+ /**
+ * Identify link target by the given id
+ *
+ * The id may also be one of the column aliases: `_next`, `_self` and `_main`
+ *
+ * @param {string} id
+ * @param {object} $of
+ * @return {object}
+ */
+ identifyLinkTarget: function (id, $of) {
+ var $target;
+
+ if (id === '_next') {
+ if (this.icinga.ui.hasOnlyOneColumn()) {
+ $target = $('#col1');
+ } else {
+ $target = $('#col2');
+ }
+ } else if (id === '_self') {
+ $target = $of.closest('.container');
+ } else if (id === '_main') {
+ $target = $('#col1');
+ } else {
+ $target = $('#' + id);
+ }
+
+ return $target;
+ },
+
+ /**
+ * Smoothly render given HTML to given container
+ */
+ renderContentToContainer: function (content, $container, action, autorefresh, forceFocus, autoSubmit, scripted) {
+ // Container update happens here
+ var scrollPos = false;
+ var _this = this;
+ var containerId = $container.attr('id');
+
+ var activeElementPath = false;
+ var navigationAnchor = false;
+ var focusFallback = false;
+
+ if (forceFocus && forceFocus.length) {
+ if (typeof forceFocus === 'string') {
+ navigationAnchor = forceFocus;
+ } else {
+ activeElementPath = this.icinga.utils.getCSSPath($(forceFocus));
+ }
+ } else if (document.activeElement && document.activeElement.id === 'search') {
+ activeElementPath = '#search';
+ } else if (document.activeElement
+ && document.activeElement !== document.body
+ && $.contains($container[0], document.activeElement)
+ ) {
+ // Active element in container
+ var $activeElement = $(document.activeElement);
+ var $pagination = $activeElement.closest('.pagination-control');
+ if ($pagination.length) {
+ focusFallback = {
+ 'parent': this.icinga.utils.getCSSPath($pagination),
+ 'child': '.active > a'
+ };
+ }
+ activeElementPath = this.icinga.utils.getCSSPath($activeElement);
+ }
+
+ var scrollTarget = $container;
+ if (typeof containerId !== 'undefined') {
+ if (autorefresh || autoSubmit) {
+ if ($container.css('display') === 'flex' && $container.is('.container')) {
+ var $scrollableContent = $container.children('.content');
+ scrollPos = {
+ x: $scrollableContent.scrollTop(),
+ y: $scrollableContent.scrollLeft()
+ };
+ scrollTarget = _this.icinga.utils.getCSSPath($scrollableContent);
+ } else {
+ scrollPos = {
+ x: $container.scrollTop(),
+ y: $container.scrollLeft()
+ };
+ }
+ } else {
+ scrollPos = {
+ x: 0,
+ y: 0
+ }
+ }
+ }
+
+ $container.trigger('beforerender', [content, action, autorefresh, scripted, autoSubmit]);
+
+ var discard = false;
+ $.each(_this.icinga.behaviors, function(name, behavior) {
+ if (behavior.renderHook) {
+ var changed = behavior.renderHook(content, $container, action, autorefresh, autoSubmit);
+ if (changed === null) {
+ discard = true;
+ } else {
+ content = changed;
+ }
+ }
+ });
+
+ $('.container', $container).each(function() {
+ _this.stopPendingRequestsFor($(this));
+ });
+
+ if (! discard) {
+ if ($container.closest('.dashboard').length) {
+ var title = $('h1', $container).first().detach();
+ $container.html(title).append(content);
+ } else if (action === 'replace') {
+ $container.html(content);
+ } else {
+ $container.append(content);
+ }
+ }
+
+ this.icinga.ui.assignUniqueContainerIds();
+
+ if (! discard && navigationAnchor) {
+ var $element = $container.find('#' + navigationAnchor);
+ if ($element.length) {
+ // data-icinga-no-scroll-on-focus is NOT designed to avoid scrolling for non-XHR requests
+ setTimeout(this.icinga.ui.focusElement.bind(this.icinga.ui), 0,
+ $element, $container, ! $element.is('[data-icinga-no-scroll-on-focus]'));
+ }
+ } else if (! activeElementPath) {
+ // Active element was not in this container
+ if (! autorefresh && ! autoSubmit && ! scripted) {
+ setTimeout(function() {
+ if (typeof $container.attr('tabindex') === 'undefined') {
+ $container.attr('tabindex', -1);
+ }
+ // Do not touch focus in case a module or component already placed it
+ if ($(document.activeElement).closest('.container').attr('id') !== containerId) {
+ _this.icinga.ui.focusElement($container);
+ }
+ }, 0);
+ }
+ } else {
+ setTimeout(function() {
+ var $activeElement = $(activeElementPath);
+
+ if ($activeElement.length && $activeElement.is(':visible')) {
+ $activeElement[0].focus({preventScroll: autorefresh || autoSubmit});
+ } else if (! autorefresh && ! autoSubmit && ! scripted) {
+ if (focusFallback) {
+ _this.icinga.ui.focusElement($(focusFallback.parent).find(focusFallback.child));
+ } else if (typeof $container.attr('tabindex') === 'undefined') {
+ $container.attr('tabindex', -1);
+ }
+ _this.icinga.ui.focusElement($container);
+ }
+ }, 0);
+ }
+
+ if (scrollPos !== false) {
+ var $scrollTarget = $(scrollTarget);
+
+ // Fallback for browsers without support for focus({preventScroll: true})
+ requestAnimationFrame(() => {
+ if ($scrollTarget.scrollTop() !== scrollPos.x) {
+ $scrollTarget.scrollTop(scrollPos.x);
+ }
+ if ($scrollTarget.scrollLeft() !== scrollPos.y) {
+ $scrollTarget.scrollLeft(scrollPos.y);
+ }
+ });
+ }
+
+ // Re-enable all click events (disabled as of performance reasons)
+ // $('*').off('click');
+ },
+
+ /**
+ * On shutdown we kill all pending requests
+ */
+ destroy: function() {
+ $.each(this.requests, function(id, request) {
+ request.abort();
+ });
+ this.icinga = null;
+ this.requests = {};
+ }
+
+ };
+
+}(Icinga, jQuery));
diff --git a/public/js/icinga/logger.js b/public/js/icinga/logger.js
new file mode 100644
index 0000000..471393c
--- /dev/null
+++ b/public/js/icinga/logger.js
@@ -0,0 +1,129 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Logger
+ *
+ * Well, log output. Rocket science.
+ */
+(function (Icinga) {
+
+ 'use strict';
+
+ Icinga.Logger = function (icinga) {
+
+ this.icinga = icinga;
+
+ this.logLevel = 'info';
+
+ this.logLevels = {
+ 'debug': 0,
+ 'info' : 1,
+ 'warn' : 2,
+ 'error': 3
+ };
+
+ };
+
+ Icinga.Logger.prototype = {
+
+ /**
+ * Whether the browser has a console object
+ */
+ hasConsole: function () {
+ return 'undefined' !== typeof console;
+ },
+
+ /**
+ * Raise or lower current log level
+ *
+ * Messages below this threshold will be silently discarded
+ */
+ setLevel: function (level) {
+ if ('undefined' !== typeof this.numericLevel(level)) {
+ this.logLevel = level;
+ }
+ return this;
+ },
+
+ /**
+ * Log a debug message
+ */
+ debug: function () {
+ return this.writeToConsole('debug', arguments);
+ },
+
+ /**
+ * Log an informational message
+ */
+ info: function () {
+ return this.writeToConsole('info', arguments);
+ },
+
+ /**
+ * Log a warning message
+ */
+ warn: function () {
+ return this.writeToConsole('warn', arguments);
+ },
+
+ /**
+ * Log an error message
+ */
+ error: function () {
+ return this.writeToConsole('error', arguments);
+ },
+
+ /**
+ * Write a log message with the given level to the console
+ */
+ writeToConsole: function (level, args) {
+
+ args = Array.prototype.slice.call(args);
+
+ // We want our log messages to carry precise timestamps
+ args.unshift(this.icinga.utils.timeWithMs());
+
+ if (this.hasConsole() && this.hasLogLevel(level)) {
+ if (typeof console[level] !== 'undefined') {
+ if (typeof console[level].apply === 'function') {
+ console[level].apply(console, args);
+ } else {
+ args.unshift('[' + level + ']');
+ console[level](args.join(' '));
+ }
+ } else if ('undefined' !== typeof console.log) {
+ args.unshift('[' + level + ']');
+ console.log(args.join(' '));
+ }
+ }
+ return this;
+ },
+
+ /**
+ * Return the numeric identifier for a given log level
+ */
+ numericLevel: function (level) {
+ var ret = this.logLevels[level];
+ if ('undefined' === typeof ret) {
+ throw 'Got invalid log level ' + level;
+ }
+ return ret;
+ },
+
+ /**
+ * Whether a given log level exists
+ */
+ hasLogLevel: function (level) {
+ return this.numericLevel(level) >= this.numericLevel(this.logLevel);
+ },
+
+ /**
+ * There isn't much to clean up here
+ */
+ destroy: function () {
+ this.enabled = false;
+ this.icinga = null;
+ }
+ };
+
+}(Icinga));
diff --git a/public/js/icinga/module.js b/public/js/icinga/module.js
new file mode 100644
index 0000000..2c2368e
--- /dev/null
+++ b/public/js/icinga/module.js
@@ -0,0 +1,134 @@
+/*! Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+/**
+ * This is how we bootstrap JS code in our modules
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Module = function (icinga, name, prototyp) {
+
+ // The Icinga instance
+ this.icinga = icinga;
+
+ // Applied event handlers
+ this.handlers = [];
+
+ // Event handlers registered by this module
+ this.registeredHandlers = [];
+
+ // The module name
+ this.name = name;
+
+ // The JS prototype for this module
+ this.prototyp = prototyp;
+
+ // Once initialized, this will be an instance of the modules prototype
+ this.object = {};
+
+ // Initialize this module
+ this.initialize();
+ };
+
+ Icinga.Module.prototype = {
+
+ initialize: function () {
+
+ if (typeof this.prototyp !== 'function') {
+ this.icinga.logger.error(
+ 'Unable to load module "' + this.name + '", constructor is missing'
+ );
+ return false;
+ }
+
+ try {
+
+ // The constructor of the modules prototype must be prepared to get an
+ // instance of Icinga.Module
+ this.object = new this.prototyp(this);
+ this.applyHandlers();
+ } catch(e) {
+ this.icinga.logger.error(
+ 'Failed to load module ' + this.name + ': ',
+ e
+ );
+
+ return false;
+ }
+
+ // That's all, the module is ready
+ this.icinga.logger.debug(
+ 'Module ' + this.name + ' has been initialized'
+ );
+
+ return true;
+ },
+
+ /**
+ * Register this modules event handlers
+ */
+ on: function (event, filter, handler) {
+ if (typeof handler === 'undefined') {
+ handler = filter;
+ filter = '.module-' + this.name;
+ } else {
+ filter = '.module-' + this.name + ' ' + filter;
+ }
+ this.registeredHandlers.push({event: event, filter: filter, handler: handler});
+
+ },
+
+ applyHandlers: function () {
+ var _this = this;
+
+ $.each(this.registeredHandlers, function (key, on) {
+ _this.bindEventHandler(
+ on.event,
+ on.filter,
+ on.handler
+ );
+ });
+ _this = null;
+
+ return this;
+ },
+
+ /**
+ * Effectively bind the given event handler
+ */
+ bindEventHandler: function (event, filter, handler) {
+ var _this = this;
+ this.icinga.logger.debug('Bound ' + filter + ' .' + event + '()');
+ this.handlers.push([event, filter, handler]);
+ $(document).on(event, filter, handler.bind(_this.object));
+ },
+
+ /**
+ * Unbind all event handlers bound by this module
+ */
+ unbindEventHandlers: function () {
+ $.each(this.handlers, function (idx, handler) {
+ $(document).off(handler[0], handler[1], handler[2]);
+ });
+ },
+
+ /**
+ * Allow to destroy and clean up this module
+ */
+ destroy: function () {
+
+ this.unbindEventHandlers();
+
+ if (typeof this.object.destroy === 'function') {
+ this.object.destroy();
+ }
+
+ this.object = null;
+ this.icinga = null;
+ this.prototyp = null;
+ }
+
+ };
+
+}(Icinga, jQuery));
diff --git a/public/js/icinga/storage.js b/public/js/icinga/storage.js
new file mode 100644
index 0000000..fa312d2
--- /dev/null
+++ b/public/js/icinga/storage.js
@@ -0,0 +1,549 @@
+/*! Icinga Web 2 | (c) 2019 Icinga GmbH | GPLv2+ */
+
+;(function(Icinga) {
+
+ 'use strict';
+
+ const KEY_TTL = 7776000000; // 90 days (90×24×60×60×1000)
+
+ /**
+ * Icinga.Storage
+ *
+ * localStorage access
+ *
+ * @param {string} prefix
+ */
+ Icinga.Storage = function(prefix) {
+
+ /**
+ * Prefix to use for keys
+ *
+ * @type {string}
+ */
+ this.prefix = prefix;
+
+ /**
+ * Storage backend
+ *
+ * @type {Storage}
+ */
+ this.backend = window.localStorage;
+ };
+
+ /**
+ * Callbacks for storage events on particular keys
+ *
+ * @type {{function}}
+ */
+ Icinga.Storage.subscribers = {};
+
+ /**
+ * Pass storage events to subscribers
+ *
+ * @param {StorageEvent} event
+ */
+ window.addEventListener('storage', function(event) {
+ var url = icinga.utils.parseUrl(event.url);
+ if (! url.path.startsWith(icinga.config.baseUrl)) {
+ // A localStorage is shared between all paths on the same origin.
+ // So we need to make sure it's us who made a change.
+ return;
+ }
+
+ if (typeof Icinga.Storage.subscribers[event.key] !== 'undefined') {
+ var newValue = null,
+ oldValue = null;
+ if (!! event.newValue) {
+ try {
+ newValue = JSON.parse(event.newValue);
+ } catch(error) {
+ icinga.logger.error('[Storage] Failed to parse new value (\`' + event.newValue
+ + '\`) for key "' + event.key + '". Error was: ' + error);
+ event.storageArea.removeItem(event.key);
+ return;
+ }
+ }
+ if (!! event.oldValue) {
+ try {
+ oldValue = JSON.parse(event.oldValue);
+ } catch(error) {
+ icinga.logger.warn('[Storage] Failed to parse old value (\`' + event.oldValue
+ + '\`) of key "' + event.key + '". Error was: ' + error);
+ oldValue = null;
+ }
+ }
+
+ Icinga.Storage.subscribers[event.key].forEach(function (subscriber) {
+ subscriber[0].call(subscriber[1], newValue, oldValue, event);
+ });
+ }
+ });
+
+ /**
+ * Create a new storage with `behavior.<name>` as prefix
+ *
+ * @param {string} name
+ *
+ * @returns {Icinga.Storage}
+ */
+ Icinga.Storage.BehaviorStorage = function(name) {
+ return new Icinga.Storage('behavior.' + name);
+ };
+
+ Icinga.Storage.prototype = {
+
+ /**
+ * Set the storage backend
+ *
+ * @param {Storage} backend
+ */
+ setBackend: function(backend) {
+ this.backend = backend;
+ },
+
+ /**
+ * Prefix the given key
+ *
+ * @param {string} key
+ *
+ * @returns {string}
+ */
+ prefixKey: function(key) {
+ var prefix = 'icinga.';
+ if (typeof this.prefix !== 'undefined') {
+ prefix = prefix + this.prefix + '.';
+ }
+
+ return prefix + key;
+ },
+
+ /**
+ * Store the given key-value pair
+ *
+ * @param {string} key
+ * @param {*} value
+ *
+ * @returns {void}
+ */
+ set: function(key, value) {
+ this.backend.setItem(this.prefixKey(key), JSON.stringify(value));
+ },
+
+ /**
+ * Get value for the given key
+ *
+ * @param {string} key
+ *
+ * @returns {*}
+ */
+ get: function(key) {
+ key = this.prefixKey(key);
+ var value = this.backend.getItem(key);
+
+ try {
+ return JSON.parse(value);
+ } catch(error) {
+ icinga.logger.error('[Storage] Failed to parse value (\`' + value
+ + '\`) of key "' + key + '". Error was: ' + error);
+ this.backend.removeItem(key);
+ return null;
+ }
+ },
+
+ /**
+ * Remove given key from storage
+ *
+ * @param {string} key
+ *
+ * @returns {void}
+ */
+ remove: function(key) {
+ this.backend.removeItem(this.prefixKey(key));
+ },
+
+ /**
+ * Subscribe with a callback for events on a particular key
+ *
+ * @param {string} key
+ * @param {function} callback
+ * @param {object} context
+ *
+ * @returns {void}
+ */
+ onChange: function(key, callback, context) {
+ if (this.backend !== window.localStorage) {
+ throw new Error('[Storage] Only the localStorage emits events');
+ }
+
+ var prefixedKey = this.prefixKey(key);
+
+ if (typeof Icinga.Storage.subscribers[prefixedKey] === 'undefined') {
+ Icinga.Storage.subscribers[prefixedKey] = [];
+ }
+
+ Icinga.Storage.subscribers[prefixedKey].push([callback, context]);
+ }
+ };
+
+ /**
+ * Icinga.Storage.StorageAwareMap
+ *
+ * @param {object} items
+ * @constructor
+ */
+ Icinga.Storage.StorageAwareMap = function(items) {
+
+ /**
+ * Storage object
+ *
+ * @type {Icinga.Storage}
+ */
+ this.storage = undefined;
+
+ /**
+ * Storage key
+ *
+ * @type {string}
+ */
+ this.key = undefined;
+
+ /**
+ * Event listeners for our internal events
+ *
+ * @type {{}}
+ */
+ this.eventListeners = {
+ 'add': [],
+ 'delete': []
+ };
+
+ /**
+ * The internal (real) map
+ *
+ * @type {Map<*>}
+ */
+ this.data = new Map();
+
+ // items is not passed directly because IE11 doesn't support constructor arguments
+ if (typeof items !== 'undefined' && !! items) {
+ Object.keys(items).forEach(function(key) {
+ this.data.set(key, items[key]);
+ }, this);
+ }
+ };
+
+ /**
+ * Create a new StorageAwareMap for the given storage and key
+ *
+ * @param {Icinga.Storage} storage
+ * @param {string} key
+ *
+ * @returns {Icinga.Storage.StorageAwareMap}
+ */
+ Icinga.Storage.StorageAwareMap.withStorage = function(storage, key) {
+ var items = storage.get(key);
+ if (typeof items !== 'undefined' && !! items) {
+ Object.keys(items).forEach(function(key) {
+ var value = items[key];
+
+ if (typeof value !== 'object' || typeof value['lastAccess'] === 'undefined') {
+ items[key] = {'value': value, 'lastAccess': Date.now()};
+ } else if (Date.now() - value['lastAccess'] > KEY_TTL) {
+ delete items[key];
+ }
+ }, this);
+ }
+
+ if (!! items && Object.keys(items).length) {
+ storage.set(key, items);
+ } else if (items !== null) {
+ storage.remove(key);
+ }
+
+ return (new Icinga.Storage.StorageAwareMap(items).setStorage(storage, key));
+ };
+
+ Icinga.Storage.StorageAwareMap.prototype = {
+
+ /**
+ * Bind this map to the given storage and key
+ *
+ * @param {Icinga.Storage} storage
+ * @param {string} key
+ *
+ * @returns {this}
+ */
+ setStorage: function(storage, key) {
+ this.storage = storage;
+ this.key = key;
+
+ if (storage.backend === window.localStorage) {
+ storage.onChange(key, this.onChange, this);
+ }
+
+ return this;
+ },
+
+ /**
+ * Return a boolean indicating this map got a storage
+ *
+ * @returns {boolean}
+ */
+ hasStorage: function() {
+ return typeof this.storage !== 'undefined' && typeof this.key !== 'undefined';
+ },
+
+ /**
+ * Update the storage
+ *
+ * @returns {void}
+ */
+ updateStorage: function() {
+ if (! this.hasStorage()) {
+ return;
+ }
+
+ if (this.size > 0) {
+ this.storage.set(this.key, this.toObject());
+ } else {
+ this.storage.remove(this.key);
+ }
+ },
+
+ /**
+ * Update the map
+ *
+ * @param {object} newValue
+ */
+ onChange: function(newValue) {
+ // Check for deletions first. Uses keys() to iterate over a copy
+ this.keys().forEach(function (key) {
+ if (newValue === null || typeof newValue[key] === 'undefined') {
+ var value = this.data.get(key)['value'];
+ this.data.delete(key);
+ this.trigger('delete', key, value);
+ }
+ }, this);
+
+ if (newValue === null) {
+ return;
+ }
+
+ // Now check for new entries
+ Object.keys(newValue).forEach(function(key) {
+ var known = this.data.has(key);
+ // Always override any known value as we want to keep track of all `lastAccess` changes
+ this.data.set(key, newValue[key]);
+
+ if (! known) {
+ this.trigger('add', key, newValue[key]['value']);
+ }
+ }, this);
+ },
+
+ /**
+ * Register an event handler to handle storage updates
+ *
+ * Available events are: add, delete. The callback receives the
+ * key and its value as first and second argument, respectively.
+ *
+ * @param {string} event
+ * @param {function} callback
+ * @param {object} thisArg
+ *
+ * @returns {this}
+ */
+ on: function(event, callback, thisArg) {
+ if (typeof this.eventListeners[event] === 'undefined') {
+ throw new Error('Invalid event "' + event + '"');
+ }
+
+ this.eventListeners[event].push([callback, thisArg]);
+ return this;
+ },
+
+ /**
+ * Trigger all event handlers for the given event
+ *
+ * @param {string} event
+ * @param {string} key
+ * @param {*} value
+ */
+ trigger: function(event, key, value) {
+ this.eventListeners[event].forEach(function (handler) {
+ var thisArg = handler[1];
+ if (typeof thisArg === 'undefined') {
+ thisArg = this;
+ }
+
+ handler[0].call(thisArg, key, value);
+ });
+ },
+
+ /**
+ * Return the number of key/value pairs in the map
+ *
+ * @returns {number}
+ */
+ get size() {
+ return this.data.size;
+ },
+
+ /**
+ * Set the value for the key in the map
+ *
+ * @param {string} key
+ * @param {*} value Default null
+ *
+ * @returns {this}
+ */
+ set: function(key, value) {
+ if (typeof value === 'undefined') {
+ value = null;
+ }
+
+ this.data.set(key, {'value': value, 'lastAccess': Date.now()});
+
+ this.updateStorage();
+ return this;
+ },
+
+ /**
+ * Remove all key/value pairs from the map
+ *
+ * @returns {void}
+ */
+ clear: function() {
+ this.data.clear();
+ this.updateStorage();
+ },
+
+ /**
+ * Remove the given key from the map
+ *
+ * @param {string} key
+ *
+ * @returns {boolean}
+ */
+ delete: function(key) {
+ var retVal = this.data.delete(key);
+
+ this.updateStorage();
+ return retVal;
+ },
+
+ /**
+ * Return a list of [key, value] pairs for every item in the map
+ *
+ * @returns {Array}
+ */
+ entries: function() {
+ var list = [];
+
+ if (this.size > 0) {
+ this.data.forEach(function (value, key) {
+ list.push([key, value['value']]);
+ });
+ }
+
+ return list;
+ },
+
+ /**
+ * Execute a provided function once for each item in the map, in insertion order
+ *
+ * @param {function} callback
+ * @param {object} thisArg
+ *
+ * @returns {void}
+ */
+ forEach: function(callback, thisArg) {
+ if (typeof thisArg === 'undefined') {
+ thisArg = this;
+ }
+
+ this.data.forEach(function(value, key) {
+ callback.call(thisArg, value['value'], key);
+ });
+ },
+
+ /**
+ * Return the value associated to the key, or undefined if there is none
+ *
+ * @param {string} key
+ *
+ * @returns {*}
+ */
+ get: function(key) {
+ var value = this.data.get(key)['value'];
+ this.set(key, value); // Update `lastAccess`
+
+ return value;
+ },
+
+ /**
+ * Return a boolean asserting whether a value has been associated to the key in the map
+ *
+ * @param {string} key
+ *
+ * @returns {boolean}
+ */
+ has: function(key) {
+ return this.data.has(key);
+ },
+
+ /**
+ * Return an array of keys in the map
+ *
+ * @returns {Array}
+ */
+ keys: function() {
+ var list = [];
+
+ if (this.size > 0) {
+ // .forEach() is used because IE11 doesn't support .keys()
+ this.data.forEach(function(_, key) {
+ list.push(key);
+ });
+ }
+
+ return list;
+ },
+
+ /**
+ * Return an array of values in the map
+ *
+ * @returns {Array}
+ */
+ values: function() {
+ var list = [];
+
+ if (this.size > 0) {
+ // .forEach() is used because IE11 doesn't support .values()
+ this.data.forEach(function(value) {
+ list.push(value['value']);
+ });
+ }
+
+ return list;
+ },
+
+ /**
+ * Return this map as simple object
+ *
+ * @returns {object}
+ */
+ toObject: function() {
+ var obj = {};
+
+ if (this.size > 0) {
+ this.data.forEach(function (value, key) {
+ obj[key] = value;
+ });
+ }
+
+ return obj;
+ }
+ };
+
+}(Icinga));
diff --git a/public/js/icinga/timer.js b/public/js/icinga/timer.js
new file mode 100644
index 0000000..0fea4d9
--- /dev/null
+++ b/public/js/icinga/timer.js
@@ -0,0 +1,176 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.Timer
+ *
+ * Timer events are triggered once a second. Runs all reegistered callback
+ * functions and is able to preserve a desired scope.
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Timer = function (icinga) {
+
+ /**
+ * We keep a reference to the Icinga instance even if we don't need it
+ */
+ this.icinga = icinga;
+
+ /**
+ * The Interval object
+ */
+ this.ticker = null;
+
+ /**
+ * Fixed default interval is 250ms
+ */
+ this.interval = 250;
+
+ /**
+ * Our registerd observers
+ */
+ this.observers = [];
+
+ /**
+ * Counter
+ */
+ this.stepCounter = 0;
+
+ this.start = (new Date()).getTime();
+
+
+ this.lastRuntime = [];
+
+ this.isRunning = false;
+ };
+
+ Icinga.Timer.prototype = {
+
+ /**
+ * The initialization function starts our ticker
+ */
+ initialize: function () {
+ this.isRunning = true;
+
+ var _this = this;
+ var f = function () {
+ if (_this.isRunning) {
+ _this.tick();
+ setTimeout(f, _this.interval);
+ }
+ };
+ f();
+ },
+
+ /**
+ * We will trigger our tick function once a second. It will call each
+ * registered observer.
+ */
+ tick: function () {
+
+ var icinga = this.icinga;
+
+ $.each(this.observers, function (idx, observer) {
+ if (observer.isDue()) {
+ observer.run();
+ } else {
+ // Not due
+ }
+ });
+ icinga = null;
+ },
+
+ /**
+ * Register a given callback function to be run within an optional scope.
+ */
+ register: function (callback, scope, interval) {
+
+ var observer;
+
+ try {
+
+ if (typeof scope === 'undefined') {
+ observer = new Icinga.Timer.Interval(callback, interval);
+ } else {
+ observer = new Icinga.Timer.Interval(
+ callback.bind(scope),
+ interval
+ );
+ }
+
+ this.observers.push(observer);
+
+ } catch(err) {
+ this.icinga.logger.error(err);
+ }
+
+ return observer;
+ },
+
+ unregister: function (observer) {
+
+ var idx = $.inArray(observer, this.observers);
+ if (idx > -1) {
+ this.observers.splice(idx, 1);
+ }
+
+ return this;
+ },
+
+ /**
+ * Our destroy function will clean up everything. Unused right now.
+ */
+ destroy: function () {
+ this.isRunning = false;
+
+ this.icinga = null;
+ $.each(this.observers, function (idx, observer) {
+ observer.destroy();
+ });
+
+ this.observers = [];
+ }
+ };
+
+ Icinga.Timer.Interval = function (callback, interval) {
+
+ if ('undefined' === typeof interval) {
+ throw 'Timer interval is required';
+ }
+
+ if (interval < 100) {
+ throw 'Timer interval cannot be less than 100ms, got ' + interval;
+ }
+
+ this.lastRun = (new Date()).getTime();
+
+ this.interval = interval;
+
+ this.scheduledNextRun = this.lastRun + interval;
+
+ this.callback = callback;
+ };
+
+ Icinga.Timer.Interval.prototype = {
+
+ isDue: function () {
+ return this.scheduledNextRun < (new Date()).getTime();
+ },
+
+ run: function () {
+ this.lastRun = (new Date()).getTime();
+
+ while (this.scheduledNextRun < this.lastRun) {
+ this.scheduledNextRun += this.interval;
+ }
+
+ this.callback();
+ },
+
+ destroy: function () {
+ this.callback = null;
+ }
+ };
+
+}(Icinga, jQuery));
diff --git a/public/js/icinga/timezone.js b/public/js/icinga/timezone.js
new file mode 100644
index 0000000..1c2647b
--- /dev/null
+++ b/public/js/icinga/timezone.js
@@ -0,0 +1,105 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+(function(Icinga, $) {
+
+ 'use strict';
+
+ /**
+ * Get the maximum timezone offset
+ *
+ * @returns {Number}
+ */
+ Date.prototype.getStdTimezoneOffset = function() {
+ var year = new Date().getFullYear();
+ var offsetInJanuary = new Date(year, 0, 2).getTimezoneOffset();
+ var offsetInJune = new Date(year, 5, 2).getTimezoneOffset();
+
+ return Math.max(offsetInJanuary, offsetInJune);
+ };
+
+ /**
+ * Test for daylight saving time zone
+ *
+ * @returns {boolean}
+ */
+ Date.prototype.isDst = function() {
+ return this.getStdTimezoneOffset() !== this.getTimezoneOffset();
+ };
+
+ /**
+ * Write timezone information into a cookie
+ *
+ * @constructor
+ */
+ Icinga.Timezone = function() {
+ this.cookieName = 'icingaweb2-tzo';
+ };
+
+ Icinga.Timezone.prototype = {
+ /**
+ * Initialize interface method
+ */
+ initialize: function () {
+ this.writeTimezone();
+ },
+
+ destroy: function() {
+ // PASS
+ },
+
+ /**
+ * Write timezone information into cookie
+ */
+ writeTimezone: function() {
+ var date = new Date();
+ var timezoneOffset = (date.getTimezoneOffset()*60) * -1;
+ var dst = date.isDst();
+
+ if (this.readCookie(this.cookieName)) {
+ return;
+ }
+
+ this.writeCookie(this.cookieName, timezoneOffset + '-' + Number(dst), 1);
+ },
+
+ /**
+ * Write cookie data
+ *
+ * @param {String} name
+ * @param {String} value
+ * @param {Number} days
+ */
+ writeCookie: function(name, value, days) {
+ var expires = '';
+
+ if (days) {
+ var date = new Date();
+ date.setTime(date.getTime()+(days*24*60*60*1000));
+ var expires = '; expires=' + date.toGMTString();
+ }
+ document.cookie = name + '=' + value + expires + '; path=/';
+ },
+
+ /**
+ * Read cookie data
+ *
+ * @param {String} name
+ * @returns {*}
+ */
+ readCookie: function(name) {
+ var nameEq = name + '=';
+ var ca = document.cookie.split(';');
+ for(var i=0;i < ca.length;i++) {
+ var c = ca[i];
+ while (c.charAt(0)==' ') {
+ c = c.substring(1,c.length);
+ }
+ if (c.indexOf(nameEq) == 0) {
+ return c.substring(nameEq.length,c.length);
+ }
+ }
+ return null;
+ }
+ };
+
+})(Icinga, jQuery);
diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js
new file mode 100644
index 0000000..0e6ee82
--- /dev/null
+++ b/public/js/icinga/ui.js
@@ -0,0 +1,645 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga.UI
+ *
+ * Our user interface
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.UI = function (icinga) {
+
+ this.icinga = icinga;
+
+ this.currentLayout = 'default';
+
+ this.debug = false;
+
+ this.debugTimer = null;
+
+ this.timeCounterTimer = null;
+
+ // detect currentLayout
+ var classList = $('#layout').attr('class').split(/\s+/);
+ var _this = this;
+ var matched;
+ $.each(classList, function(index, item) {
+ if (null !== (matched = item.match(/^([a-z]+)-layout$/))) {
+ var layout = matched[1];
+ if (layout !== 'fullscreen') {
+ _this.currentLayout = layout;
+ // Break loop
+ return false;
+ }
+ }
+ });
+ };
+
+ Icinga.UI.prototype = {
+
+ initialize: function () {
+ $('html').removeClass('no-js').addClass('js');
+ this.enableTimeCounters();
+ this.triggerWindowResize();
+ this.fadeNotificationsAway();
+
+ $(document).on('click', '#mobile-menu-toggle', this.toggleMobileMenu);
+ $(document).on('keypress', '#search',{ self: this, type: 'key' }, this.closeMobileMenu);
+ $(document).on('mouseleave', '#sidebar', { self: this, type: 'leave' }, this.closeMobileMenu);
+ $(document).on('click', '#sidebar a', { self: this, type: 'navigate' }, this.closeMobileMenu);
+ },
+
+ fadeNotificationsAway: function() {
+ var icinga = this.icinga;
+ $('#notifications li')
+ .not('.fading-out')
+ .not('.persist')
+ .addClass('fading-out')
+ .delay(7000)
+ .fadeOut('slow',
+ function() {
+ $(this).remove();
+ });
+ },
+
+ toggleDebug: function() {
+ if (this.debug) {
+ return this.disableDebug();
+ } else {
+ return this.enableDebug();
+ }
+ },
+
+ enableDebug: function () {
+ if (this.debug === true) { return this; }
+ this.debug = true;
+ this.debugTimer = this.icinga.timer.register(
+ this.refreshDebug,
+ this,
+ 1000
+ );
+ this.fixDebugVisibility();
+
+ return this;
+ },
+
+ fixDebugVisibility: function () {
+ if (this.debug) {
+ $('#responsive-debug').css({display: 'block'});
+ } else {
+ $('#responsive-debug').css({display: 'none'});
+ }
+ return this;
+ },
+
+ disableDebug: function () {
+ if (this.debug === false) { return; }
+
+ this.debug = false;
+ this.icinga.timer.unregister(this.debugTimer);
+ this.debugTimer = null;
+ this.fixDebugVisibility();
+ return this;
+ },
+
+ reloadCss: function () {
+ var icinga = this.icinga;
+ icinga.logger.info('Reloading CSS');
+ $('link').each(function() {
+ var $oldLink = $(this);
+ if ($oldLink.hasAttr('type') && $oldLink.attr('type').indexOf('css') > -1) {
+ var $newLink = $oldLink.clone().attr(
+ 'href',
+ icinga.utils.addUrlParams(
+ $oldLink.attr('href'),
+ { id: new Date().getTime() } // Only required for Firefox to reload CSS automatically
+ )
+ ).on('load', function() {
+ $oldLink.remove();
+ $('head').trigger('css-reloaded');
+ });
+
+ $newLink.appendTo($('head'));
+ }
+ });
+ },
+
+ enableTimeCounters: function () {
+ this.timeCounterTimer = this.icinga.timer.register(
+ this.refreshTimeSince,
+ this,
+ 1000
+ );
+ return this;
+ },
+
+ disableTimeCounters: function () {
+ this.icinga.timer.unregister(this.timeCounterTimer);
+ this.timeCounterTimer = null;
+ return this;
+ },
+
+ /**
+ * Focus the given element and scroll to its position
+ *
+ * @param {string} element The name or id of the element to focus
+ * @param {object} [$container] The container containing the element
+ * @param {boolean} [scroll] Whether the viewport should be scrolled to the focused element
+ */
+ focusElement: function(element, $container, scroll) {
+ var $element = element;
+
+ if (typeof scroll === 'undefined') {
+ scroll = true;
+ }
+
+ if (typeof element === 'string') {
+ if ($container && $container.length) {
+ $element = $container.find('#' + element);
+ } else {
+ $element = $('#' + element);
+ }
+
+ if (! $element.length) {
+ // The name attribute is actually deprecated, on anchor tags,
+ // but we'll possibly handle links from another source
+ // (module etc) so that's used as a fallback
+ if ($container && $container.length) {
+ $element = $container.find('[name="' + element.replace(/'/, '\\\'') + '"]');
+ } else {
+ $element = $('[name="' + element.replace(/'/, '\\\'') + '"]');
+ }
+ }
+ }
+
+ if ($element.length) {
+ if (! this.isFocusable($element)) {
+ $element.attr('tabindex', -1);
+ }
+
+ $element[0].focus();
+
+ if (scroll && $container && $container.length) {
+ if (! $container.is('.container')) {
+ $container = $container.closest('.container');
+ }
+
+ if ($container.css('display') === 'flex' && $container.is('.container')) {
+ var $controls = $container.find('.controls');
+ var $content = $container.find('.content');
+ $content.scrollTop($element.offsetTopRelativeTo($content) - $controls.outerHeight() - (
+ $element.outerHeight(true) - $element.innerHeight()
+ ));
+ } else {
+ $container.scrollTop($element.first().position().top);
+ }
+ }
+ }
+ },
+
+ isFocusable: function ($element) {
+ return $element.is('*[tabindex], a[href], input:not([disabled]), button:not([disabled])' +
+ ', select:not([disabled]), textarea:not([disabled]), iframe, area[href], object' +
+ ', embed, *[contenteditable]');
+ },
+
+ moveToLeft: function () {
+ var col2 = this.cutContainer($('#col2'));
+ var kill = this.cutContainer($('#col1'));
+ this.pasteContainer($('#col1'), col2);
+ this.icinga.behaviors.navigation.trySetActiveAndSelectedByUrl($('#col1').data('icingaUrl'));
+ $('#col1').trigger('column-moved', 'col2');
+ },
+
+ moveToRight: function () {
+ let col1 = document.getElementById('col1'),
+ col2 = document.getElementById('col2'),
+ col1Backup = this.cutContainer($(col1));
+
+ this.cutContainer($(col2)); // Clear col2 states
+ this.pasteContainer($(col2), col1Backup);
+ this.layout2col();
+ $(col2).trigger('column-moved', 'col1');
+ },
+
+ cutContainer: function ($col) {
+ var props = {
+ 'elements': $('#' + $col.attr('id') + ' > *').detach(),
+ 'data': {
+ 'data-icinga-url': $col.data('icingaUrl'),
+ 'data-icinga-title': $col.data('icingaTitle'),
+ 'data-icinga-refresh': $col.data('icingaRefresh'),
+ 'data-last-update': $col.data('lastUpdate'),
+ 'data-icinga-module': $col.data('icingaModule'),
+ 'data-icinga-container-id': $col[0].dataset.icingaContainerId
+ },
+ 'class': $col.attr('class')
+ };
+ this.icinga.loader.stopPendingRequestsFor($col);
+ $col.removeData('icingaUrl');
+ $col.removeData('icingaTitle');
+ $col.removeData('icingaRefresh');
+ $col.removeData('lastUpdate');
+ $col.removeData('icingaModule');
+ delete $col[0].dataset.icingaContainerId;
+ $col.removeAttr('class').attr('class', 'container');
+ return props;
+ },
+
+ pasteContainer: function ($col, backup) {
+ backup['elements'].appendTo($col);
+ $col.attr('class', backup['class']); // TODO: ie memleak? remove first?
+ $col.data('icingaUrl', backup['data']['data-icinga-url']);
+ $col.data('icingaTitle', backup['data']['data-icinga-title']);
+ $col.data('icingaRefresh', backup['data']['data-icinga-refresh']);
+ $col.data('lastUpdate', backup['data']['data-last-update']);
+ $col.data('icingaModule', backup['data']['data-icinga-module']);
+ $col[0].dataset.icingaContainerId = backup['data']['data-icinga-container-id'];
+ },
+
+ triggerWindowResize: function () {
+ this.onWindowResize({data: {self: this}});
+ },
+
+ /**
+ * Our window got resized, let's fix our UI
+ */
+ onWindowResize: function (event) {
+ var _this = event.data.self;
+
+ if (_this.layoutHasBeenChanged()) {
+ _this.icinga.logger.info(
+ 'Layout change detected, switching to',
+ _this.currentLayout
+ );
+ }
+
+ _this.refreshDebug();
+ },
+
+ /**
+ * Returns whether the layout is too small for more than one column
+ *
+ * @returns {boolean} True when more than one column is available
+ */
+ hasOnlyOneColumn: function () {
+ return this.currentLayout === 'poor' || this.currentLayout === 'minimal';
+ },
+
+ layoutHasBeenChanged: function () {
+
+ var layout = $('html').css('fontFamily').replace(/['",]/g, '');
+ var matched;
+
+ if (null !== (matched = layout.match(/^([a-z]+)-layout$/))) {
+ if (matched[1] === this.currentLayout &&
+ $('#layout').hasClass(layout)
+ ) {
+ return false;
+ } else {
+ $('#layout').removeClass(this.currentLayout + '-layout').addClass(layout);
+ this.currentLayout = matched[1];
+ if (this.currentLayout === 'poor' || this.currentLayout === 'minimal') {
+ this.layout1col();
+ } else if (this.icinga.initialized) {
+ // layout1col() also triggers this, that's why an else is required
+ $('#layout').trigger('layout-change');
+ }
+ return true;
+ }
+ }
+ this.icinga.logger.error(
+ 'Someone messed up our responsiveness hacks, html font-family is',
+ layout
+ );
+ return false;
+ },
+
+ /**
+ * Returns whether only one column is displayed
+ *
+ * @returns {boolean} True when only one column is displayed
+ */
+ isOneColLayout: function () {
+ return ! $('#layout').hasClass('twocols');
+ },
+
+ layout1col: function () {
+ if (this.isOneColLayout()) { return; }
+ this.icinga.logger.debug('Switching to single col');
+ $('#layout').removeClass('twocols');
+ this.closeContainer($('#col2'));
+
+ if (this.icinga.initialized) {
+ $('#layout').trigger('layout-change');
+ }
+
+ // one-column layouts never have any selection active
+ $('#col1').removeData('icinga-actiontable-former-href');
+ this.icinga.behaviors.actiontable.clearAll();
+ },
+
+ closeContainer: function($c) {
+ this.icinga.loader.stopPendingRequestsFor($c);
+ $c.removeData('icingaUrl');
+ $c.removeData('icingaTitle');
+ $c.removeData('icingaRefresh');
+ $c.removeData('lastUpdate');
+ $c.removeData('icingaModule');
+ delete $c[0].dataset.icingaContainerId;
+ $c.removeAttr('class').attr('class', 'container');
+ $c.trigger('close-column');
+ this.icinga.history.pushCurrentState();
+ $c.html('');
+ },
+
+ layout2col: function () {
+ if (! this.isOneColLayout()) { return; }
+ this.icinga.logger.debug('Switching to double col');
+ $('#layout').addClass('twocols');
+
+ if (this.icinga.initialized) {
+ $('#layout').trigger('layout-change');
+ }
+ },
+
+ prepareColumnFor: function ($el, $target) {
+ var explicitTarget;
+
+ if ($target.attr('id') === 'col2') {
+ if ($el.closest('#col2').length) {
+ explicitTarget = $el.closest('[data-base-target]').data('baseTarget');
+ if (typeof explicitTarget !== 'undefined' && explicitTarget === '_next') {
+ this.moveToLeft();
+ }
+ } else {
+ this.layout2col();
+ }
+ } else { // if ($target.attr('id') === 'col1')
+ explicitTarget = $el.closest('[data-base-target]').data('baseTarget');
+ if (typeof explicitTarget !== 'undefined' && explicitTarget === '_main') {
+ this.layout1col();
+ }
+ }
+ },
+
+ getAvailableColumnSpace: function () {
+ return $('#main').width() / this.getDefaultFontSize();
+ },
+
+ setColumnCount: function (count) {
+ if (count === 3) {
+ $('#main > .container').css({
+ width: '33.33333%'
+ });
+ } else if (count === 2) {
+ $('#main > .container').css({
+ width: '50%'
+ });
+ } else {
+ $('#main > .container').css({
+ width: '100%'
+ });
+ }
+ },
+
+ setTitle: function (title) {
+ document.title = title;
+ return this;
+ },
+
+ getColumnCount: function () {
+ return $('#main > .container').length;
+ },
+
+ /**
+ * Assign a unique ID to each .container without such
+ *
+ * This usually applies to dashlets
+ */
+ assignUniqueContainerIds: function() {
+ var currentMax = 0;
+ $('.container').each(function() {
+ var $el = $(this);
+ var m;
+ if (!$el.attr('id')) {
+ return;
+ }
+ if (m = $el.attr('id').match(/^ciu_(\d+)$/)) {
+ if (parseInt(m[1]) > currentMax) {
+ currentMax = parseInt(m[1]);
+ }
+ }
+ });
+ $('.container').each(function() {
+ var $el = $(this);
+ if (!!$el.attr('id')) {
+ return;
+ }
+ currentMax++;
+ $el.attr('id', 'ciu_' + currentMax);
+ });
+ },
+
+ refreshDebug: function () {
+ if (! this.debug) {
+ return;
+ }
+
+ var size = this.getDefaultFontSize().toString();
+ var winWidth = $( window ).width();
+ var winHeight = $( window ).height();
+ var loading = '';
+
+ $.each(this.icinga.loader.requests, function (el, req) {
+ if (loading === '') {
+ loading = '<br />Loading:<br />';
+ }
+ loading += el + ' => ' + encodeURI(req.url);
+ });
+
+ $('#responsive-debug').html(
+ ' Time: ' +
+ this.icinga.utils.formatHHiiss(new Date()) +
+ '<br /> 1em: ' +
+ size +
+ 'px<br /> Win: ' +
+ winWidth +
+ 'x'+
+ winHeight +
+ 'px<br />' +
+ ' Layout: ' +
+ this.currentLayout +
+ loading
+ );
+ },
+
+ /**
+ * Refresh partial time counters
+ *
+ * This function runs every second.
+ */
+ refreshTimeSince: function () {
+ $('.time-ago, .time-since').each(function (idx, el) {
+ var partialTime = /(\d{1,2})m (\d{1,2})s/.exec(el.innerHTML);
+ if (partialTime !== null) {
+ var minute = parseInt(partialTime[1], 10),
+ second = parseInt(partialTime[2], 10);
+ if (second < 59) {
+ ++second;
+ } else {
+ ++minute;
+ second = 0;
+ }
+ el.innerHTML = el.innerHTML.substring(0, partialTime.index) + minute.toString() + 'm '
+ + second.toString() + 's' + el.innerHTML.substring(partialTime.index + partialTime[0].length);
+ }
+ });
+
+ $('.time-until').each(function (idx, el) {
+ var partialTime = /(-?)(\d{1,2})m (\d{1,2})s/.exec(el.innerHTML);
+ if (partialTime !== null) {
+ var minute = parseInt(partialTime[2], 10),
+ second = parseInt(partialTime[3], 10),
+ invert = partialTime[1];
+ if (invert.length) {
+ // Count up because partial time is negative
+ if (second < 59) {
+ ++second;
+ } else {
+ ++minute;
+ second = 0;
+ }
+ } else {
+ // Count down because partial time is positive
+ if (second === 0) {
+ if (minute === 0) {
+ // Invert counter
+ minute = 0;
+ second = 1;
+ invert = '-';
+ } else {
+ --minute;
+ second = 59;
+ }
+ } else {
+ --second;
+ }
+
+ if (minute === 0 && second === 0 && el.dataset.agoLabel) {
+ el.innerText = el.dataset.agoLabel;
+ el.classList.remove('time-until');
+ el.classList.add('time-ago');
+
+ return;
+ }
+ }
+ el.innerHTML = el.innerHTML.substring(0, partialTime.index) + invert + minute.toString() + 'm '
+ + second.toString() + 's' + el.innerHTML.substring(partialTime.index + partialTime[0].length);
+ }
+ });
+ },
+
+ createFontSizeCalculator: function () {
+ var $el = $('<div id="fontsize-calc">&nbsp;</div>');
+ $('#layout').append($el);
+ return $el;
+ },
+
+ getDefaultFontSize: function () {
+ var $calc = $('#fontsize-calc');
+ if (! $calc.length) {
+ $calc = this.createFontSizeCalculator();
+ }
+ return $calc.width() / 1000;
+ },
+
+ /**
+ * Toggle mobile menu
+ *
+ * @param {object} e Event
+ */
+ toggleMobileMenu: function(e) {
+ $('#sidebar').toggleClass('expanded');
+ },
+
+ /**
+ * Close mobile menu when the enter key is pressed during search or the user leaves the sidebar
+ *
+ * @param {object} e Event
+ */
+ closeMobileMenu: function(e) {
+ if (e.data.self.currentLayout !== 'minimal') {
+ return;
+ }
+
+ if (e.data.type === 'key') {
+ if (e.which === 13) {
+ $('#sidebar').removeClass('expanded');
+ $(e.target)[0].blur();
+ }
+ } else {
+ $('#sidebar').removeClass('expanded');
+ }
+ },
+
+ toggleFullscreen: function () {
+ $('#layout').toggleClass('fullscreen-layout');
+ },
+
+ getUniqueContainerId: function (container) {
+ if (typeof container.jquery !== 'undefined') {
+ if (! container.length) {
+ return null;
+ }
+
+ container = container[0];
+ } else if (typeof container === 'undefined') {
+ return null;
+ }
+
+ var containerId = container.dataset.icingaContainerId || null;
+ if (containerId === null) {
+ /**
+ * Only generate an id if it's not for col1 or the menu (which are using the non-suffixed window id).
+ * This is based on the assumption that the server only knows about the menu and first column
+ * and therefore does not need to protect its ids. (As the menu is most likely part of the sidebar)
+ */
+ var col1 = document.getElementById('col1');
+ if (container.id !== 'menu' && col1 !== null && ! col1.contains(container)) {
+ containerId = this.icinga.utils.generateId(6); // Random because the content may move
+ container.dataset.icingaContainerId = containerId;
+ }
+ }
+
+ return containerId;
+ },
+
+ getWindowId: function () {
+ if (! this.hasWindowId()) {
+ return undefined;
+ }
+ return window.name.match(/^Icinga-([a-zA-Z0-9]+)$/)[1];
+ },
+
+ hasWindowId: function () {
+ var res = window.name.match(/^Icinga-([a-zA-Z0-9]+)$/);
+ return typeof res === 'object' && null !== res;
+ },
+
+ setWindowId: function (id) {
+ this.icinga.logger.debug('Setting new window id', id);
+ window.name = 'Icinga-' + id;
+ },
+
+ destroy: function () {
+ // This is gonna be hard, clean up the mess
+ this.icinga = null;
+ this.debugTimer = null;
+ this.timeCounterTimer = null;
+ }
+ };
+
+}(Icinga, jQuery));
diff --git a/public/js/icinga/utils.js b/public/js/icinga/utils.js
new file mode 100644
index 0000000..280e4f6
--- /dev/null
+++ b/public/js/icinga/utils.js
@@ -0,0 +1,582 @@
+/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+/**
+ * Icinga utility functions
+ */
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Utils = function (icinga) {
+
+ /**
+ * Utility functions may need access to their Icinga instance
+ */
+ this.icinga = icinga;
+
+ /**
+ * We will use this to create an URL helper only once
+ */
+ this.urlHelper = null;
+ };
+
+ Icinga.Utils.prototype = {
+
+ timeWithMs: function (now) {
+
+ if (typeof now === 'undefined') {
+ now = new Date();
+ }
+
+ var ms = now.getMilliseconds() + '';
+ while (ms.length < 3) {
+ ms = '0' + ms;
+ }
+
+ return now.toLocaleTimeString() + '.' + ms;
+ },
+
+ timeShort: function (now) {
+
+ if (typeof now === 'undefined') {
+ now = new Date();
+ }
+
+ return now.toLocaleTimeString().replace(/:\d{2}$/, '');
+ },
+
+ formatHHiiss: function (date) {
+ var hours = date.getHours();
+ var minutes = date.getMinutes();
+ var seconds = date.getSeconds();
+ if (hours < 10) hours = '0' + hours;
+ if (minutes < 10) minutes = '0' + minutes;
+ if (seconds < 10) seconds = '0' + seconds;
+ return hours + ':' + minutes + ':' + seconds;
+ },
+
+ /**
+ * Format the given byte-value into a human-readable string
+ *
+ * @param {number} The amount of bytes to format
+ * @returns {string} The formatted string
+ */
+ formatBytes: function (bytes) {
+ var log2 = Math.log(bytes) / Math.LN2;
+ var pot = Math.floor(log2 / 10);
+ var unit = (['b', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB'])[pot];
+ return ((bytes / Math.pow(1024, pot)).toFixed(2)) + ' ' + unit;
+ },
+
+ /**
+ * Return whether the given element is visible in the users view
+ *
+ * Borrowed from: http://stackoverflow.com/q/487073
+ *
+ * @param {selector} element The element to check
+ * @returns {Boolean}
+ */
+ isVisible: function(element) {
+ var $element = $(element);
+ if (!$element.length) {
+ return false;
+ }
+
+ var docViewTop = $(window).scrollTop();
+ var docViewBottom = docViewTop + $(window).height();
+ var elemTop = $element.offset().top;
+ var elemBottom = elemTop + $element.height();
+
+ return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom) &&
+ (elemBottom <= docViewBottom) && (elemTop >= docViewTop));
+ },
+
+ getUrlHelper: function () {
+ if (this.urlHelper === null) {
+ this.urlHelper = document.createElement('a');
+ }
+
+ return this.urlHelper;
+ },
+
+ /**
+ * Parse a given Url and return an object
+ */
+ parseUrl: function (url) {
+
+ var a = this.getUrlHelper();
+ a.href = url;
+
+ var result = {
+ source : url,
+ protocol: a.protocol.replace(':', ''),
+ host : a.hostname,
+ port : a.port,
+ query : a.search,
+ file : (a.pathname.match(/\/([^\/?#]+)$/i) || [,''])[1],
+ hash : a.hash.replace('#',''),
+ path : a.pathname.replace(/^([^\/])/,'/$1'),
+ relative: (a.href.match(/tps?:\/\/[^\/]+(.+)/) || [,''])[1],
+ segments: a.pathname.replace(/^\//,'').split('/'),
+ params : this.parseParams(a)
+ };
+ a = null;
+
+ return result;
+ },
+
+ // Local URLs only
+ addUrlParams: function (url, params) {
+ var parts = this.parseUrl(url),
+ result = parts.path,
+ newparams = parts.params;
+
+ // We overwrite existing params
+ $.each(params, function (key, value) {
+ key = encodeURIComponent(key);
+ value = typeof value !== 'string' || !! value ? encodeURIComponent(value) : null;
+
+ var found = false;
+ for (var i = 0; i < newparams.length; i++) {
+ if (newparams[i].key === key) {
+ newparams[i].value = value;
+ found = true;
+ break;
+ }
+ }
+
+ if (! found) {
+ newparams.push({ key: key, value: value });
+ }
+ });
+
+ if (newparams.length) {
+ result += '?' + this.buildQuery(newparams);
+ }
+
+ if (parts.hash.length) {
+ result += '#' + parts.hash;
+ }
+
+ return result;
+ },
+
+ // Local URLs only
+ removeUrlParams: function (url, params) {
+ var parts = this.parseUrl(url),
+ result = parts.path,
+ newparams = parts.params;
+
+ $.each(params, function (_, key) {
+ key = encodeURIComponent(key);
+
+ for (var i = 0; i < newparams.length; i++) {
+ if (newparams[i].key === key) {
+ newparams.splice(i, 1);
+ return;
+ }
+ }
+ });
+
+ if (newparams.length) {
+ result += '?' + this.buildQuery(newparams);
+ }
+
+ if (parts.hash.length) {
+ result += '#' + parts.hash;
+ }
+
+ return result;
+ },
+
+ /**
+ * Return a query string for the given params
+ *
+ * @param {Array} params
+ * @return {string}
+ */
+ buildQuery: function (params) {
+ var query = '';
+
+ for (var i = 0; i < params.length; i++) {
+ if (!! query) {
+ query += '&';
+ }
+
+ query += params[i].key;
+ switch (params[i].value) {
+ case true:
+ break;
+ case false:
+ query += '=0';
+ break;
+ case null:
+ query += '=';
+ break;
+ default:
+ query += '=' + params[i].value;
+ }
+ }
+
+ return query;
+ },
+
+ /**
+ * Parse url params
+ */
+ parseParams: function (a) {
+ var params = [],
+ segment = a.search.replace(/^\?/,'').split('&'),
+ len = segment.length,
+ i = 0,
+ key,
+ value,
+ equalPos;
+
+ for (; i < len; i++) {
+ if (! segment[i]) {
+ continue;
+ }
+
+ equalPos = segment[i].indexOf('=');
+ if (equalPos !== -1) {
+ key = segment[i].slice(0, equalPos);
+ value = segment[i].slice(equalPos + 1);
+ } else {
+ key = segment[i];
+ value = true;
+ }
+
+ params.push({ key: key, value: value });
+ }
+
+ return params;
+ },
+
+ /**
+ * Add the specified flag to the given URL
+ *
+ * @param {string} url
+ * @param {string} flag
+ *
+ * @returns {string}
+ */
+ addUrlFlag: function (url, flag) {
+ var pos = url.search(/#(?!!)/);
+
+ if (url.indexOf('?') !== -1) {
+ flag = '&' + flag;
+ } else {
+ flag = '?' + flag;
+ }
+
+ if (pos === -1) {
+ return url + flag;
+ }
+
+ return url.slice(0, pos) + flag + url.slice(pos);
+ },
+
+ /**
+ * Check whether two HTMLElements overlap
+ *
+ * @param a {HTMLElement}
+ * @param b {HTMLElement}
+ *
+ * @returns {Boolean} whether elements overlap, will return false when one
+ * element is not in the DOM
+ */
+ elementsOverlap: function(a, b)
+ {
+ // a bounds
+ var aoff = $(a).offset();
+ if (!aoff) {
+ return false;
+ }
+ var at = aoff.top;
+ var ah = a.offsetHeight || (a.getBBox && a.getBBox().height);
+ var al = aoff.left;
+ var aw = a.offsetWidth || (a.getBBox && a.getBBox().width);
+
+ // b bounds
+ var boff = $(b).offset();
+ if (!boff) {
+ return false;
+ }
+ var bt = boff.top;
+ var bh = b.offsetHeight || (b.getBBox && b.getBBox().height);
+ var bl = boff.left;
+ var bw = b.offsetWidth || (b.getBBox && b.getBBox().width);
+
+ return !(at > (bt + bh) || bt > (at + ah)) && !(bl > (al + aw) || al > (bl + bw));
+ },
+
+ /**
+ * Create a selector that can be used to fetch the element the same position in the DOM-Tree
+ *
+ * Create the path to the given element in the DOM-Tree, comparable to an X-Path. Climb the
+ * DOM tree upwards until an element with an unique ID is found, this id is used as the anchor,
+ * all other elements will be addressed by their position in the parent.
+ *
+ * @param {HTMLElement} el The element to extract the path for.
+ *
+ * @returns {Array} The path of the element, that can be passed to getElementByPath
+ */
+ getDomPath: function (el) {
+ if (! el) {
+ return [];
+ }
+ if (el.id !== '') {
+ return ['#' + el.id];
+ }
+ if (el === document.body) {
+ return ['body'];
+ }
+
+ var siblings = el.parentNode.childNodes;
+ var index = 0;
+ for (var i = 0; i < siblings.length; i ++) {
+ if (siblings[i].nodeType === 1) {
+ index ++;
+ }
+
+ if (siblings[i] === el) {
+ var p = this.getDomPath(el.parentNode);
+ p.push(':nth-child(' + (index) + ')');
+ return p;
+ }
+ }
+ },
+
+ /**
+ * Get the CSS selector to the given node
+ *
+ * @param {HTMLElement} element
+ *
+ * @returns {string}
+ */
+ getCSSPath: function(element) {
+ if (typeof element === 'undefined') {
+ throw 'Requires a element';
+ }
+
+ if (typeof element.jquery !== 'undefined') {
+ if (! element.length) {
+ throw 'Requires a element';
+ }
+
+ element = element[0];
+ }
+
+ var path = [];
+
+ while (true) {
+ let id = element.id;
+ if (typeof id !== 'undefined' && typeof id !== 'string') {
+ // Sometimes there may be a form element with the name "id"
+ id = element.getAttribute("id");
+ }
+
+ if (!! id) {
+ // Only use ids if they're truly unique
+ let results = document.querySelectorAll('* #' + this.escapeCSSSelector(id));
+ if (results.length === 1) {
+ path.push('#' + id);
+ break;
+ }
+ }
+
+ var tagName = element.tagName;
+ var parent = element.parentElement;
+
+ if (! parent) {
+ path.push(tagName.toLowerCase());
+ break;
+ }
+
+ if (parent.children.length) {
+ var index = 0;
+ do {
+ if (element.tagName === tagName) {
+ index++;
+ }
+ } while ((element = element.previousElementSibling));
+
+ path.push(tagName.toLowerCase() + ':nth-of-type(' + index + ')');
+ } else {
+ path.push(tagName.toLowerCase());
+ }
+
+ element = parent;
+ }
+
+ return path.reverse().join(' > ');
+ },
+
+ /**
+ * Escape the given string to be used in a CSS selector
+ *
+ * @param {string} selector
+ * @returns {string}
+ */
+ escapeCSSSelector: function (selector) {
+ if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
+ return CSS.escape(selector);
+ }
+
+ return selector.replaceAll(/^(\d)/, '\\\\3$1 ');
+ },
+
+ /**
+ * Climbs up the given dom path and returns the element
+ *
+ * This is the counterpart
+ *
+ * @param path {Array} The selector
+ * @returns {HTMLElement} The corresponding element
+ */
+ getElementByDomPath: function (path) {
+ var $element;
+ $.each(path, function (i, selector) {
+ if (! $element) {
+ $element = $(selector);
+ } else {
+ $element = $element.children(selector).first();
+ if (! $element[0]) {
+ return false;
+ }
+ }
+ });
+ return $element[0];
+ },
+
+ objectKeys: Object.keys || function (obj) {
+ var keys = [];
+ $.each(obj, function (key) {
+ keys.push(key);
+ });
+ return keys;
+ },
+
+ objectsEqual: function equals(obj1, obj2) {
+ var obj1Keys = Object.keys(obj1);
+ var obj2Keys = Object.keys(obj2);
+ if (obj1Keys.length !== obj2Keys.length) {
+ return false;
+ }
+
+ return obj1Keys.concat(obj2Keys)
+ .every(function (key) {
+ return obj1[key] === obj2[key];
+ });
+ },
+
+ arraysEqual: function (array1, array2) {
+ if (array1.length !== array2.length) {
+ return false;
+ }
+
+ var value1, value2;
+ for (var i = 0; i < array1.length; i++) {
+ value1 = array1[i];
+ value2 = array2[i];
+
+ if (typeof value1 === 'object') {
+ if (typeof value2 !== 'object' || ! this.objectsEqual(value1, value2)) {
+ return false;
+ }
+ } else if (value1 !== value2) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * Cleanup
+ */
+ destroy: function () {
+ this.urlHelper = null;
+ this.icinga = null;
+ },
+
+ /**
+ * Encode the parenthesis too
+ *
+ * @param str {String} A component of a URI
+ *
+ * @returns {String} Encoded component
+ */
+ fixedEncodeURIComponent: function (str) {
+ return encodeURIComponent(str).replace(/[()]/g, function(c) {
+ return '%' + c.charCodeAt(0).toString(16);
+ });
+ },
+
+ escape: function (str) {
+ return String(str).replace(
+ /[&<>"']/gm,
+ function (c) {
+ return {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ '"': '&quot;',
+ "'": '&#039;'
+ }[c];
+ }
+ );
+ },
+
+ /**
+ * Pad a string with another one
+ *
+ * @param {String} str the string to pad
+ * @param {String} padding the string to use for padding
+ * @param {Number} minLength the minimum length of the result
+ *
+ * @returns {String} the padded string
+ */
+ padString: function(str, padding, minLength) {
+ str = String(str);
+ padding = String(padding);
+ while (str.length < minLength) {
+ str = padding + str;
+ }
+ return str;
+ },
+
+ /**
+ * Shuffle a string
+ *
+ * @param {String} str The string to shuffle
+ *
+ * @returns {String} The shuffled string
+ */
+ shuffleString: function(str) {
+ var a = str.split(""),
+ n = a.length;
+
+ for(var i = n - 1; i > 0; i--) {
+ var j = Math.floor(Math.random() * (i + 1));
+ var tmp = a[i];
+ a[i] = a[j];
+ a[j] = tmp;
+ }
+ return a.join("");
+ },
+
+ /**
+ * Generate an id
+ *
+ * @param {Number} len The desired length of the id
+ *
+ * @returns {String} The id
+ */
+ generateId: function(len) {
+ return this.shuffleString('abcefghijklmnopqrstuvwxyz').substr(0, len);
+ }
+ };
+
+}(Icinga, jQuery));
diff --git a/public/js/logout.js b/public/js/logout.js
new file mode 100644
index 0000000..b9ac400
--- /dev/null
+++ b/public/js/logout.js
@@ -0,0 +1,16 @@
+;(function () {
+ /**
+ * When JavaScript is available, trigger an XmlHTTPRequest with the non-existing user 'logout' and abort it
+ * before it is able to finish. This will cause the browser to show a new authentication prompt in the next
+ * request.
+ */
+ document.getElementById('logout-in-progress').hidden = true;
+ document.getElementById('logout-successful').hidden = false;
+ try {
+ var xhttp = new XMLHttpRequest();
+ xhttp.open('GET', 'arbitrary url', true, 'logout', 'logout');
+ xhttp.send('');
+ xhttp.abort();
+ } catch (e) {
+ }
+})();
diff --git a/schema/mysql-upgrades/2.0.0beta3-2.0.0rc1.sql b/schema/mysql-upgrades/2.0.0beta3-2.0.0rc1.sql
new file mode 100644
index 0000000..e9d4f86
--- /dev/null
+++ b/schema/mysql-upgrades/2.0.0beta3-2.0.0rc1.sql
@@ -0,0 +1,26 @@
+# Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+
+
+DROP TABLE `icingaweb_group_membership`;
+DROP TABLE `icingaweb_group`;
+
+CREATE TABLE `icingaweb_group`(
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
+ `parent` int(10) unsigned NULL DEFAULT NULL,
+ `ctime` timestamp NULL DEFAULT NULL,
+ `mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `idx_name` (`name`),
+ CONSTRAINT `fk_icingaweb_group_parent_id` FOREIGN KEY (`parent`)
+ REFERENCES `icingaweb_group` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE `icingaweb_group_membership`(
+ `group_id` int(10) unsigned NOT NULL,
+ `username` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
+ `ctime` timestamp NULL DEFAULT NULL,
+ `mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`group_id`,`username`),
+ CONSTRAINT `fk_icingaweb_group_membership_icingaweb_group` FOREIGN KEY (`group_id`)
+ REFERENCES `icingaweb_group` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/schema/mysql-upgrades/2.11.0.sql b/schema/mysql-upgrades/2.11.0.sql
new file mode 100644
index 0000000..99f18cf
--- /dev/null
+++ b/schema/mysql-upgrades/2.11.0.sql
@@ -0,0 +1,33 @@
+ALTER TABLE `icingaweb_group` ROW_FORMAT=DYNAMIC;
+ALTER TABLE `icingaweb_group_membership` ROW_FORMAT=DYNAMIC;
+ALTER TABLE `icingaweb_user` ROW_FORMAT=DYNAMIC;
+ALTER TABLE `icingaweb_user_preference` ROW_FORMAT=DYNAMIC;
+ALTER TABLE `icingaweb_rememberme` ROW_FORMAT=DYNAMIC;
+
+ALTER TABLE `icingaweb_group` CONVERT TO CHARACTER SET utf8mb4;
+ALTER TABLE `icingaweb_group_membership` CONVERT TO CHARACTER SET utf8mb4;
+ALTER TABLE `icingaweb_user` CONVERT TO CHARACTER SET utf8mb4;
+ALTER TABLE `icingaweb_user_preference` CONVERT TO CHARACTER SET utf8mb4;
+
+ALTER TABLE `icingaweb_group`
+ MODIFY COLUMN `name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL;
+ALTER TABLE `icingaweb_group_membership`
+ MODIFY COLUMN `username` varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL;
+ALTER TABLE `icingaweb_user`
+ MODIFY COLUMN `name` varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL;
+
+ALTER TABLE `icingaweb_user_preference`
+ MODIFY COLUMN `username` varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL,
+ MODIFY COLUMN `section` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
+ MODIFY COLUMN `name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL;
+
+CREATE TABLE icingaweb_schema (
+ id int unsigned NOT NULL AUTO_INCREMENT,
+ version smallint unsigned NOT NULL,
+ timestamp int unsigned NOT NULL,
+
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC;
+
+INSERT INTO icingaweb_schema (version, timestamp)
+ VALUES (6, UNIX_TIMESTAMP());
diff --git a/schema/mysql-upgrades/2.12.0.sql b/schema/mysql-upgrades/2.12.0.sql
new file mode 100644
index 0000000..f2630ac
--- /dev/null
+++ b/schema/mysql-upgrades/2.12.0.sql
@@ -0,0 +1,11 @@
+ALTER TABLE icingaweb_schema
+ MODIFY COLUMN timestamp bigint unsigned NOT NULL,
+ MODIFY COLUMN version varchar(64) NOT NULL,
+ ADD COLUMN success enum('n', 'y') DEFAULT NULL,
+ ADD COLUMN reason text DEFAULT NULL,
+ ADD CONSTRAINT idx_icingaweb_schema_version UNIQUE (version);
+
+UPDATE icingaweb_schema SET timestamp = timestamp * 1000, success = 'y';
+
+INSERT INTO icingaweb_schema (version, timestamp, success, reason)
+ VALUES('2.12.0', UNIX_TIMESTAMP() * 1000, 'y', NULL);
diff --git a/schema/mysql-upgrades/2.5.0.sql b/schema/mysql-upgrades/2.5.0.sql
new file mode 100644
index 0000000..08a05c0
--- /dev/null
+++ b/schema/mysql-upgrades/2.5.0.sql
@@ -0,0 +1,5 @@
+# Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+
+
+ALTER TABLE `icingaweb_group_membership` MODIFY COLUMN `username` varchar(254) COLLATE utf8_unicode_ci NOT NULL;
+ALTER TABLE `icingaweb_user` MODIFY COLUMN `name` varchar(254) COLLATE utf8_unicode_ci NOT NULL;
+ALTER TABLE `icingaweb_user_preference` MODIFY COLUMN `username` varchar(254) COLLATE utf8_unicode_ci NOT NULL;
diff --git a/schema/mysql-upgrades/2.9.0.sql b/schema/mysql-upgrades/2.9.0.sql
new file mode 100644
index 0000000..7ed7ad6
--- /dev/null
+++ b/schema/mysql-upgrades/2.9.0.sql
@@ -0,0 +1,11 @@
+CREATE TABLE `icingaweb_rememberme`(
+ id int(10) unsigned NOT NULL AUTO_INCREMENT,
+ username varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL,
+ passphrase varchar(256) NOT NULL,
+ random_iv varchar(24) NOT NULL,
+ http_user_agent text NOT NULL,
+ expires_at timestamp NULL DEFAULT NULL,
+ ctime timestamp NULL DEFAULT NULL,
+ mtime timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
diff --git a/schema/mysql-upgrades/2.9.1.sql b/schema/mysql-upgrades/2.9.1.sql
new file mode 100644
index 0000000..a7af6ce
--- /dev/null
+++ b/schema/mysql-upgrades/2.9.1.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `icingaweb_rememberme`
+ MODIFY random_iv varchar(32) NOT NULL;
diff --git a/schema/mysql.schema.sql b/schema/mysql.schema.sql
new file mode 100644
index 0000000..1eb71a8
--- /dev/null
+++ b/schema/mysql.schema.sql
@@ -0,0 +1,68 @@
+# Icinga Web 2 | (c) 2014 Icinga GmbH | GPLv2+
+
+CREATE TABLE `icingaweb_group`(
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `parent` int(10) unsigned NULL DEFAULT NULL,
+ `ctime` timestamp NULL DEFAULT NULL,
+ `mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `idx_name` (`name`),
+ CONSTRAINT `fk_icingaweb_group_parent_id` FOREIGN KEY (`parent`)
+ REFERENCES `icingaweb_group` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE `icingaweb_group_membership`(
+ `group_id` int(10) unsigned NOT NULL,
+ `username` varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `ctime` timestamp NULL DEFAULT NULL,
+ `mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`group_id`,`username`),
+ CONSTRAINT `fk_icingaweb_group_membership_icingaweb_group` FOREIGN KEY (`group_id`)
+ REFERENCES `icingaweb_group` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE `icingaweb_user`(
+ `name` varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `active` tinyint(1) NOT NULL,
+ `password_hash` varbinary(255) NOT NULL,
+ `ctime` timestamp NULL DEFAULT NULL,
+ `mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE `icingaweb_user_preference`(
+ `username` varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `section` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `value` varchar(255) NOT NULL,
+ `ctime` timestamp NULL DEFAULT NULL,
+ `mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`username`,`section`,`name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE `icingaweb_rememberme`(
+ id int(10) unsigned NOT NULL AUTO_INCREMENT,
+ username varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL,
+ passphrase varchar(256) NOT NULL,
+ random_iv varchar(32) NOT NULL,
+ http_user_agent text NOT NULL,
+ expires_at timestamp NULL DEFAULT NULL,
+ ctime timestamp NULL DEFAULT NULL,
+ mtime timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE icingaweb_schema (
+ id int unsigned NOT NULL AUTO_INCREMENT,
+ version varchar(64) NOT NULL,
+ timestamp bigint unsigned NOT NULL,
+ success enum('n', 'y') DEFAULT NULL,
+ reason text DEFAULT NULL,
+
+ PRIMARY KEY (id),
+ CONSTRAINT idx_icingaweb_schema_version UNIQUE (version)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC;
+
+INSERT INTO icingaweb_schema (version, timestamp, success)
+ VALUES ('2.12.0', UNIX_TIMESTAMP() * 1000, 'y');
diff --git a/schema/pgsql-upgrades/2.0.0beta3-2.0.0rc1.sql b/schema/pgsql-upgrades/2.0.0beta3-2.0.0rc1.sql
new file mode 100644
index 0000000..7b5b575
--- /dev/null
+++ b/schema/pgsql-upgrades/2.0.0beta3-2.0.0rc1.sql
@@ -0,0 +1,60 @@
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+DROP TABLE "icingaweb_group_membership";
+DROP TABLE "icingaweb_group";
+
+CREATE OR REPLACE FUNCTION unix_timestamp(timestamp with time zone) RETURNS bigint AS '
+ SELECT EXTRACT(EPOCH FROM $1)::bigint AS result
+' LANGUAGE sql;
+
+CREATE TABLE "icingaweb_group" (
+ "id" serial,
+ "name" character varying(64) NOT NULL,
+ "parent" int NULL DEFAULT NULL,
+ "ctime" timestamp NULL DEFAULT NULL,
+ "mtime" timestamp NULL DEFAULT NULL
+);
+
+ALTER TABLE ONLY "icingaweb_group"
+ ADD CONSTRAINT pk_icingaweb_group
+ PRIMARY KEY (
+ "id"
+);
+
+CREATE UNIQUE INDEX idx_icingaweb_group
+ ON "icingaweb_group"
+ USING btree (
+ lower((name)::text)
+);
+
+ALTER TABLE ONLY "icingaweb_group"
+ ADD CONSTRAINT fk_icingaweb_group_parent_id
+ FOREIGN KEY (
+ "parent"
+ )
+ REFERENCES "icingaweb_group" (
+ "id"
+);
+
+CREATE TABLE "icingaweb_group_membership" (
+ "group_id" int NOT NULL,
+ "username" character varying(64) NOT NULL,
+ "ctime" timestamp NULL DEFAULT NULL,
+ "mtime" timestamp NULL DEFAULT NULL
+);
+
+ALTER TABLE ONLY "icingaweb_group_membership"
+ ADD CONSTRAINT pk_icingaweb_group_membership
+ FOREIGN KEY (
+ "group_id"
+ )
+ REFERENCES "icingaweb_group" (
+ "id"
+);
+
+CREATE UNIQUE INDEX idx_icingaweb_group_membership
+ ON "icingaweb_group_membership"
+ USING btree (
+ group_id,
+ lower((username)::text)
+);
diff --git a/schema/pgsql-upgrades/2.11.0.sql b/schema/pgsql-upgrades/2.11.0.sql
new file mode 100644
index 0000000..9e57ce7
--- /dev/null
+++ b/schema/pgsql-upgrades/2.11.0.sql
@@ -0,0 +1,10 @@
+CREATE TABLE "icingaweb_schema" (
+ "id" serial,
+ "version" smallint NOT NULL,
+ "timestamp" int NOT NULL,
+
+ CONSTRAINT pk_icingaweb_schema PRIMARY KEY ("id")
+);
+
+INSERT INTO icingaweb_schema ("version", "timestamp")
+ VALUES (6, extract(epoch from now()));
diff --git a/schema/pgsql-upgrades/2.12.0.sql b/schema/pgsql-upgrades/2.12.0.sql
new file mode 100644
index 0000000..2a5818e
--- /dev/null
+++ b/schema/pgsql-upgrades/2.12.0.sql
@@ -0,0 +1,13 @@
+CREATE TYPE boolenum AS ENUM ('n', 'y');
+
+ALTER TABLE icingaweb_schema
+ ALTER COLUMN timestamp TYPE bigint,
+ ALTER COLUMN version TYPE varchar(64),
+ ADD COLUMN success boolenum DEFAULT NULL,
+ ADD COLUMN reason text DEFAULT NULL,
+ ADD CONSTRAINT idx_icingaweb_schema_version UNIQUE (version);
+
+UPDATE icingaweb_schema SET timestamp = timestamp * 1000, success = 'y';
+
+INSERT INTO icingaweb_schema (version, timestamp, success, reason)
+ VALUES('2.12.0', EXTRACT(EPOCH FROM now()) * 1000, 'y', NULL);
diff --git a/schema/pgsql-upgrades/2.5.0.sql b/schema/pgsql-upgrades/2.5.0.sql
new file mode 100644
index 0000000..8139281
--- /dev/null
+++ b/schema/pgsql-upgrades/2.5.0.sql
@@ -0,0 +1,5 @@
+/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
+
+ALTER TABLE "icingaweb_group_membership" ALTER COLUMN "username" TYPE character varying(254);
+ALTER TABLE "icingaweb_user" ALTER COLUMN "name" TYPE character varying(254);
+ALTER TABLE "icingaweb_user_preference" ALTER COLUMN "username" TYPE character varying(254);
diff --git a/schema/pgsql-upgrades/2.9.0.sql b/schema/pgsql-upgrades/2.9.0.sql
new file mode 100644
index 0000000..c017b91
--- /dev/null
+++ b/schema/pgsql-upgrades/2.9.0.sql
@@ -0,0 +1,16 @@
+CREATE TABLE "icingaweb_rememberme" (
+ "id" serial,
+ "username" character varying(254) NOT NULL,
+ "passphrase" character varying(256) NOT NULL,
+ "random_iv" character varying(24) NOT NULL,
+ "http_user_agent" text NOT NULL,
+ "expires_at" timestamp NULL DEFAULT NULL,
+ "ctime" timestamp NULL DEFAULT NULL,
+ "mtime" timestamp NULL DEFAULT NULL
+);
+
+ALTER TABLE ONLY "icingaweb_rememberme"
+ ADD CONSTRAINT pk_icingaweb_rememberme
+ PRIMARY KEY (
+ "id"
+);
diff --git a/schema/pgsql-upgrades/2.9.1.sql b/schema/pgsql-upgrades/2.9.1.sql
new file mode 100644
index 0000000..02c683f
--- /dev/null
+++ b/schema/pgsql-upgrades/2.9.1.sql
@@ -0,0 +1,2 @@
+ALTER TABLE ONLY "icingaweb_rememberme"
+ ALTER COLUMN random_iv type character varying(32);
diff --git a/schema/pgsql.schema.sql b/schema/pgsql.schema.sql
new file mode 100644
index 0000000..3a5413b
--- /dev/null
+++ b/schema/pgsql.schema.sql
@@ -0,0 +1,135 @@
+/* Icinga Web 2 | (c) 2014 Icinga GmbH | GPLv2+ */
+
+CREATE OR REPLACE FUNCTION unix_timestamp(timestamp with time zone) RETURNS bigint AS '
+ SELECT EXTRACT(EPOCH FROM $1)::bigint AS result
+' LANGUAGE sql;
+
+CREATE TABLE "icingaweb_group" (
+ "id" serial,
+ "name" character varying(64) NOT NULL,
+ "parent" int NULL DEFAULT NULL,
+ "ctime" timestamp NULL DEFAULT NULL,
+ "mtime" timestamp NULL DEFAULT NULL
+);
+
+ALTER TABLE ONLY "icingaweb_group"
+ ADD CONSTRAINT pk_icingaweb_group
+ PRIMARY KEY (
+ "id"
+);
+
+CREATE UNIQUE INDEX idx_icingaweb_group
+ ON "icingaweb_group"
+ USING btree (
+ lower((name)::text)
+);
+
+ALTER TABLE ONLY "icingaweb_group"
+ ADD CONSTRAINT fk_icingaweb_group_parent_id
+ FOREIGN KEY (
+ "parent"
+ )
+ REFERENCES "icingaweb_group" (
+ "id"
+);
+
+CREATE TABLE "icingaweb_group_membership" (
+ "group_id" int NOT NULL,
+ "username" character varying(254) NOT NULL,
+ "ctime" timestamp NULL DEFAULT NULL,
+ "mtime" timestamp NULL DEFAULT NULL
+);
+
+ALTER TABLE ONLY "icingaweb_group_membership"
+ ADD CONSTRAINT pk_icingaweb_group_membership
+ FOREIGN KEY (
+ "group_id"
+ )
+ REFERENCES "icingaweb_group" (
+ "id"
+);
+
+CREATE UNIQUE INDEX idx_icingaweb_group_membership
+ ON "icingaweb_group_membership"
+ USING btree (
+ group_id,
+ lower((username)::text)
+);
+
+CREATE TABLE "icingaweb_user" (
+ "name" character varying(254) NOT NULL,
+ "active" smallint NOT NULL,
+ "password_hash" bytea NOT NULL,
+ "ctime" timestamp NULL DEFAULT NULL,
+ "mtime" timestamp NULL DEFAULT NULL
+);
+
+ALTER TABLE ONLY "icingaweb_user"
+ ADD CONSTRAINT pk_icingaweb_user
+ PRIMARY KEY (
+ "name"
+);
+
+CREATE UNIQUE INDEX idx_icingaweb_user
+ ON "icingaweb_user"
+ USING btree (
+ lower((name)::text)
+);
+
+CREATE TABLE "icingaweb_user_preference" (
+ "username" character varying(254) NOT NULL,
+ "name" character varying(64) NOT NULL,
+ "section" character varying(64) NOT NULL,
+ "value" character varying(255) NOT NULL,
+ "ctime" timestamp NULL DEFAULT NULL,
+ "mtime" timestamp NULL DEFAULT NULL
+);
+
+ALTER TABLE ONLY "icingaweb_user_preference"
+ ADD CONSTRAINT pk_icingaweb_user_preference
+ PRIMARY KEY (
+ "username",
+ "section",
+ "name"
+);
+
+CREATE UNIQUE INDEX idx_icingaweb_user_preference
+ ON "icingaweb_user_preference"
+ USING btree (
+ lower((username)::text),
+ lower((section)::text),
+ lower((name)::text)
+);
+
+CREATE TABLE "icingaweb_rememberme" (
+ "id" serial,
+ "username" character varying(254) NOT NULL,
+ "passphrase" character varying(256) NOT NULL,
+ "random_iv" character varying(32) NOT NULL,
+ "http_user_agent" text NOT NULL,
+ "expires_at" timestamp NULL DEFAULT NULL,
+ "ctime" timestamp NULL DEFAULT NULL,
+ "mtime" timestamp NULL DEFAULT NULL
+);
+
+ALTER TABLE ONLY "icingaweb_rememberme"
+ ADD CONSTRAINT pk_icingaweb_rememberme
+ PRIMARY KEY (
+ "id"
+);
+
+CREATE TYPE boolenum AS ENUM ('n', 'y');
+
+CREATE TABLE "icingaweb_schema" (
+ "id" serial,
+ "version" varchar(64) NOT NULL,
+ "timestamp" bigint NOT NULL,
+ "success" boolenum DEFAULT NULL,
+ "reason" text DEFAULT NULL,
+
+ CONSTRAINT pk_icingaweb_schema PRIMARY KEY ("id"),
+ CONSTRAINT idx_icingaweb_schema_version UNIQUE (version)
+);
+
+INSERT INTO icingaweb_schema (version, timestamp, success)
+ VALUES ('2.12.0', extract(epoch from now()) * 1000, 'y');